前回の記事では、Azure Cosmos DB Gremlin API のグラフデータを Linkurious Enterprise で可視化してみました。
gooner.hateblo.jp
グラフデータベースは、データの関連性を辿れる特徴を活用して、EC サイトなどで購入した商品と関連のある商品をレコメンドするケースにも使われます。
今回は、Azure Cosmos DB Gremlin API を使ってお勧めのアニメをレコメンドしてくれる機能を作ってみます。
データを用意する
今回は Kaggle 上で公開されている Anime Recommendations Database を利用することにしました。
このサイトから、anime.csv と rating.csv の2つのデータをダウンロードできます。
anime_id,name,genre,type,episodes,rating,members 32281,Kimi no Na wa.,"Drama, Romance, School, Supernatural",Movie,1,9.37,200630 5114,Fullmetal Alchemist: Brotherhood,"Action, Adventure, Drama, Fantasy, Magic, Military, Shounen",TV,64,9.26,793665 28977,Gintama°,"Action, Comedy, Historical, Parody, Samurai, Sci-Fi, Shounen",TV,51,9.25,114262
user_id,anime_id,rating 1,20,-1 1,24,-1 1,79,-1
その他にも、ユーザーのマスターデータが欲しかったので、Mockaroo のサイトで適当に 1,000 件を作りました。
user_id,user_name 1,vmonni0 2,cdonaghie1 3,araymond2
グラフデータベースを作成する
Azure Portal から Gremlin API を選択して Azure Cosmos DB を作成し、適当な名前で新しい Graph を作成します。
ノードを作成する
C# のコンソールアプリを作り、Gremlin.Net を利用してデータをアップロードします。
www.nuget.org
GremlinClient のインスタンスを生成して、ノードやエッジを操作していきます。インスタンスを生成するパラメータはあまり精査しておらず適当です。
var Host = "xxx.gremlin.cosmos.azure.com"; var PrimaryKey = "xxx"; var Database = "sample-database"; var Container = "recommend-graph"; var containerLink = "/dbs/" + Database + "/colls/" + Container; var gremlinServer = new GremlinServer(Host, 443, enableSsl: true, username: containerLink, password: PrimaryKey); var connectionPoolSettings = new ConnectionPoolSettings() { MaxInProcessPerConnection = 10, PoolSize = 30, ReconnectionAttempts = 3, ReconnectionBaseDelay = TimeSpan.FromMilliseconds(500) }; var webSocketConfiguration = new Action<ClientWebSocketOptions>(options => { options.KeepAliveInterval = TimeSpan.FromSeconds(10); }); using (var gremlinClient = new GremlinClient(gremlinServer, new GraphSON2Reader(), new GraphSON2Writer(), "application/vnd.gremlin-v2.0+json", connectionPoolSettings, webSocketConfiguration)) { // ここにロジックを書いていく }
anime.csv から読み取ったレコードでノードを作成します。12,295 件あります。
anime
という label をもつ Vertex を登録し、property には name
だけを設定します。
using (var reader = new StreamReader("anime.csv")) using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { while (csv.Read()) { var record = csv.GetRecord<Anime>(); var query = $"g.addV('anime').property('id', '{record.anime_id}').property('title', '{record.name}').property('pk', '{record.anime_id}')"; await gremlinClient.SubmitAsync<dynamic>(query); } }
同じように、user.csv から読み取ったレコードでノードを作成します。
user
という label をもつ Vertex を登録し、property には name
だけを設定します。
using (var reader = new StreamReader("user.csv")) using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { while (csv.Read()) { var record = csv.GetRecord<User>(); var query = $"g.addV('user').property('id', 'user{record.user_id}').property('name', '{record.user_name}').property('pk', 'user{record.user_id}')"; await gremlinClient.SubmitAsync<dynamic>(query); } }
エッジを作成する
rating-demo.csv から読み取ったレコードでエッジを作成することで、ユーザーが評価するアニメのレイティングを表現します。元データは 780 万件あったので、1,000 人分のレイティング(96,480 件)に減らしました。
rates
という Edge を登録し、user
と anime
の関係を表します。
using (var reader = new StreamReader("rating-demo.csv")) using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { while (csv.Read()) { var record = csv.GetRecord<Rating>(); try { var query = $"g.V().hasLabel('user').has('id', 'user{record.user_id}').addE('rates').property('weight', {record.rating}).to(g.V().has('id', '{record.anime_id}'))"; await gremlinClient.SubmitAsync<dynamic>(query); } catch (ArgumentOutOfRangeException ex) when (ex.ParamName == "ValueKind") { // ↓ の例外が発生するが登録自体は成功しているため、正常終了とする // JSON type not supported. (Parameter 'ValueKind') // Actual value was Number. } } }
動作確認用のデータを登録する
レコメンドさせたいユーザーを登録します。
g.addV('user').property('id', 'user9999').property('name', 'test9999').property('pk', 'user9999')
anime.csv には ONE PIECE の作品がいくつか登録されています。
21,One Piece,"Action, Adventure, Comedy, Drama, Fantasy, Shounen, Super Power",TV,Unknown,8.58,504862 4155,One Piece Film: Strong World,"Action, Adventure, Comedy, Drama, Fantasy, Shounen",Movie,1,8.42,85020 12859,One Piece Film: Z,"Action, Adventure, Comedy, Drama, Fantasy, Shounen",Movie,1,8.39,76051 31490,One Piece Film: Gold,"Action, Adventure, Comedy, Drama, Fantasy, Shounen",Movie,1,8.32,18642 16287,One Piece: Romance Dawn,"Action, Comedy, Fantasy, Shounen, Super Power",OVA,1,7.60,8326
このユーザーには、ONE PIECE の関連作品を高評価するエッジを登録します。
g.V().hasLabel('user').has('id', 'user9999').addE('rates').property('weight', 10).to(g.V().has('id', '21')) g.V().hasLabel('user').has('id', 'user9999').addE('rates').property('weight', 10).to(g.V().has('id', '4155')) g.V().hasLabel('user').has('id', 'user9999').addE('rates').property('weight', 10).to(g.V().has('id', '12859')) g.V().hasLabel('user').has('id', 'user9999').addE('rates').property('weight', 10).to(g.V().has('id', '31490')) g.V().hasLabel('user').has('id', 'user9999').addE('rates').property('weight', 10).to(g.V().has('id', '16287'))
レコメンドする
この ONE PIECE が好きなユーザーに、お勧めの作品をレコメンドする Gremlin クエリを組み立てていきます。
まず、このユーザーが高評価している作品を取得するクエリ。
g.V().hasLabel('user').has('id', 'user9999').outE('rates').has('weight', gte(8)).inV().as('exclude')
つぎに、このユーザーが高評価している作品を高評価しているユーザーを取得するクエリ。
g.V().hasLabel('user').has('id', 'user9999').outE('rates').has('weight', gte(8)).inV().as('exclude') .inE('rates').has('weight', gte(8)).outV()
さらに、このユーザーが高評価している作品を高評価しているユーザーが高評価している作品(このユーザーが観た作品は除く)を取得するクエリ。
g.V().hasLabel('user').has('id', 'user9999').outE('rates').has('weight', gte(8)).inV().as('exclude') .inE('rates').has('weight', gte(8)).outV() .outE('rates').has('weight', gte(8)).inV().where(neq('exclude'))
さいごに、作品の重複を除外して、評価順にソートするクエリ。
g.V().hasLabel('user').has('id', 'user9999').outE('rates').has('weight', gte(8)).inV().as('exclude') .inE('rates').has('weight', gte(8)).outV() .outE('rates').has('weight', gte(8)).inV().where(neq('exclude')) .dedup().order().by(inE('rates').count(), decr).limit(5).values('title')
さいごの Gremlin クエリを実行した結果が、こちらです。
レコメンドの精度を判定するのは難しいですが、幅広い作品が登録されている中で、ONE PIECE と同じ週刊少年ジャンプの「デスノート」や「進撃の巨人」、アクション・アドベンチャー系の「ソードアート・オンライン」がランクインしており、わりと妥当な結果な気がします。
まとめ
Azure Cosmos DB Gremlin API を使って、お勧めのアニメをレコメンドしてくれる機能を作ってみました。アニメのジャンルやタイプを関連づけることで、より精度の高いレコメンドができると思います。
Gremlin.Net を使ってデータをアップロードするコードは、こちらのリポジトリで公開しています。
github.com