BrainPad株式会社ブレインパッドのデータ活用に関する取り組みや製品・サービス開発の裏側、社員の日常などをご紹介します。~分析エントリーこんにちは、アナリティクスサービス本部の小田です。本連載ではレコメンデーションシステムについて、気の向くままにゆるゆると考察を行っています。第1回ではユーザベース協調フィルタリングの概念およびロジックについて考察を行いました。今回、ようやく訪れた連載第2回は、ユーザベース協調フィルタリングを実装し、実際のデータに対して適用してみたいと思います。協調フィルタリングをスクラッチで実装する前に、まず既存のパッケージを使って手っ取り早く試してみたいと思います。協調フィルタリングのようなポピュラーなアルゴリズムはパッケージとして出回っていますので、適用するだけならRでもPythonでも2-3行で可能です。■利用するデータセットまずは利用するデータについての説明です。system)を研究・評価する際によく利用されるパブリックなデータセットがいくつかありますが、今回はその中でも最も広く利用されている「MovieLens」という映画の評価データを利用します。その他のパブリックデータセットに興味がある方は、以下のウェブサイトにまとまっていますので、一読されると良いかと思います。アメリカンジョークのデータ(Jester)なども有名で、結構面白いです。ちなみにジョークのデータは評価スケールが100段階らしいです。日本にもお笑いの評価データとかあるんでしょうか。Researchプロジェクトにより収集されているデータセットです。いう映画作品レビューサイトの作品評価データで、MovieLensでは個人が自由に映画作品を評価・レビューできます。データセットは取得された時期やデータサイズ(ユーザ数や作品数)によっていくつか種類があるのですが、今回は1998年にデータ公開された小規模なデータセットを利用します。データ概要は次のとおりです。少なくとも20作品を評価しているユーザのみ対象とする1997年9月19日-1998年4月22日の約7ヶ月間に取得MovieLensなどの有名なパブリックデータは、RやPythonのレコメンデーション用パッケージに同梱されていますので、パッケージをインストールして簡単に利用することができます。本ブログでも、Rのパッケージ{recommederlab}に含まれるMovieLensのデータを使いたいと思います。{recommederlab}パッケージは、レコメンドシステムのアルゴリズム比較やテストを行うためのプラットフォームの提供を謳っており、パッケージを使って協調フィルタリングや行列分解などポピュラーなアルゴリズムを簡単に試すことができます。それでは早速MovieLensデータセットを読み込んで内容を確認してみましょう。1664{recommenderlab}からロードしたデータセットは、realRatingMatrixというパッケージ独自のスパース行列で保持しています。ここでは読みやすさを優先し、一般的なmatrix型に変換した上で処理を行う方針をとっています。先頭部分の内容確認head(MovieLense.mtx[,1:10])各行がユーザ番号、各列が作品名を表示しており、それぞれの評価値が1-5で記載されています(評価行列)。以下のように先頭部分を確認しただけでも、相当数の未評価NAが存在することが確認できます。4次に評価行列の全体像を画像で確認してみましょう。評価行列のイメージ描写image(MovieLense)評価行列の全体をプロット縦軸に各ユーザが、横軸に各作品が並んでいます。黒くなっている部分は評価5、白い部分は未評価NAです。なんとなく全体像を直感的に把握できたような気になります。一人あたりの評価数の分布もついでに確認しておきましょう。全作品約1664に対して、ほとんどのユーザが数百程度の作品しか評価していないことがわかります。■{recommenderlab}で協調フィルタリング{recommederlab}のgithubページのREADMEに記載された例を参考に、ユーザベース協調フィルタリングを適用してみます。コードは次のようになります。3.209573主要部分のコードは学習と適用(推定)で実質2行です。{recommenderlab}ではRecommender関数のmethod引数の値を変えることで他のアルゴリズムを簡単に試すことが可能です。前述の{recommenderlab}のような協調フィルタリングは基礎的な内容さえ理解していれば、スクラッチからでも比較的簡単に実装可能です。実装するにあたり第1回で考察した内容から一歩踏み込んで留意すべきポイントがあります。する何人の評価を参考にするか(近傍数k)今回はこれらのポイントを押さえつつ、協調フィルタリングをスクラッチで実装してみたいと思います。■想定ケースと手順のおさらい想定ケースは先程の{recommenderlab}と同じとします。第1回のおさらいになりますが、ユーザベース協調フィルタリングの実施手順は次のとおりです。類似度の高いユーザを探す【留意点】何を類似度に使うか第1回の例では類似度を測る指標として、ユークリッド距離の逆数を使いました。ユークリッド距離はシンプルで大変馴染みのある指標なのですが、MovieLensのような評価値データにユーザベース協調フィルタリングを適用する際は、コサイン類似度を使うことが多いです。シンプルでかつ、過去事例で精度が良いことが確認されているから、というのが主な理由でしょう。コサイン類似度は、作品評価値ベクトル空間においてユーザを表すベクトル同士がなす角と解釈できます。-1~1の値をとり、2つのベクトルが同じ方向を向いている場合は1になります。コサイン類似度のイメージ定義式としては次のようになっています。コードで書くとこんな感じです。deno)}実際には、レコメンド対象ユーザ(101番ユーザ)と各学習用ユーザ(1-100番ユーザ)同士の類似度を計算しますので、上記にアレンジを加えて、レコメンド対象ユーザの評価行列と各学習用ユーザの評価行列の2つを引数にとるような関数にします。一般的な類似度関数の仕様も、このように2つの行列を引数に渡せるようになっています。deno)}【留意点】ユーザごとの「甘め」、「辛め」にどう対処するかユーザによって全体的に高い評価を付けがちな人(甘めの人)と、全体的に低い評価を付けがちな人(辛めな人)がいます。甘めの人がつけた評価4と、辛めの人がつけた評価2は、値は異なっていても実質的な意味合いには相違ないことがあります。類似度を計算する際にはこうしたユーザごとの評価の偏り(バイアス)をなるべく除外するために、事前に評価行列を中心化をすることが多いです。具体的には評価値から各ユーザの評価平均値を引いて、すべての評価値を「ユーザの平均値からの差」の表現に直してあげます。「この作品は、50番目のユーザにとって平均的な作品より1.5低く評価されている」のような表現です。この中心化された値でモデル構築、予測をした後、予測値に再度平均値を足してもとの評価値に戻します。中心化のイメージ中心化のための関数は例えば次のようになります。NAにどう対処するか上記で評価行列の内容を確認しましたが、評価行列は未評価NAという欠損だらけです。欠損をどう評価するかがひとつ問題となります。欠損のある2つのユーザの類似度をどう計算するか欠損のある2ユーザの類似度は「2ユーザが共通して評価している作品のみで算出する(pairwise)」という方法がシンプルでわかりやすいのですが、2ユーザがともに評価している作品の評価値のみしか計算に使えないため、使えるデータが少なくなるという欠点はあります。この欠点への対処としては、平均値で補完するなどの、いわゆる欠損値の対処手法を適用することになります。{recommenderlab}の場合はどうしているかというと、未評価NAを0として解釈しています。内部的にna_as_zeroという変数があり、これがデフォルトではFALSEになっているので本来意図している挙動と異なりますが、最終的にコサイン類似度を計算している{proxy}パッケージの仕様に起因するようです。いずれにせよ結果として「ユーザが評価していない映画は、悪い評価を付けたのと同じだ」という解釈を前提としています。強めの条件ではありますが、類似度の計算としては欠損を考える必要もなくスムーズですね。最終的に精度が良ければ文句は出ないでしょう。類似度を計算するとき、例えば共通して評価した5作品すべての評価値が一致する2ユーザと、共通して評価した20作品すべての評価値が一致する2ユーザではコサイン類似度は同じ1ですが、この2つのケースに違いはないのでしょうか。20作品すべてが一致した方が類似度の信頼性は高いと考えられます。この場合は共通する数が多いペアの評価値に重みをかけたり、一定数の共通評価をしていない類似度は無効にするなどの対処をすることはあります。類似度の高い上位kユーザの評価を参考に、ユーザ101番が評価していない作品の評価値を推定する類似度の計算ができたので、最も類似度の高いk人のユーザを参考に、ユーザ101番の未評価作品の評価値を推定したいと思います。次のaとbのサブステップで実施します。類似度の高い上位kユーザを抽出ユーザを類似度の降順で並べて、上位k件をとってくるだけです。return(knn.mtx)}関数の途中でmatrix型にcastしていますが、この部分は整合性をとるために入れているだけなので重要ではありません。類似度の高いユーザの意見(評価値)をより強く推定値に反映させるため、類似度の加重平均を算出し、前処理で中心化した分を足し戻した値が予測値となります。neighbor)の集合とします。類似度の高い上位kユーザを抽出"のkはどうやって決めたら良いのでしょうか。このkは近傍数と呼ばれ、上位何人の評価を参考にするかを調節する値です。一般にkが小さい場合は、類似度の高い少数のユーザの評価のみを参考にするため、一部の類似者の評価特徴が強く出た、嗜好の"尖った"予測になりやすいです。逆にkが大きい場合は、より多くのユーザの評価値の平均を予測に使うため、"なめらかな"予測になります。これは機械学習でいう「過学習」や「頑健性」の議論と同じで、kが小さい方が複雑で表現力は高くなりますが、一部の嗜好性のみに適合してしまいノイズに過剰に反応してしまう、いわゆる"過学習"が起きる可能性があります。kを大きくしすぎると「頑健性」は高まるものの、細かい嗜好の差異をうまく表現できなくなる可能性があります。最適なkはケースバイケースではありますが、過去の研究から30-100の値を使うことが多いようです。{recommenderlab}ではデフォルト値が25人(k=25)となっていますので、今回のケースではk=25を使って推定をしています。推定評価値の高い上位N(N=10)の映画を抽出するレコメンド対象のユーザがすでに評価している(視聴している)作品を除外した上で、推定した評価値が最も高い上位10の作品を抽出します。n=10)■コード全体を確認ここまでの内容を使ってレコメンド処理をすべて記載してみます。n=10)協調ベースフィルタリングは、スクラッチで書いても大した分量にはならないことが確認できると思います。上記の推定結果は、前述の{recommenderlab}での推定値と一致します。■複数ターゲットに対応したコード上記のコードでは、わかりやすく見せるためレコメンド対象ユーザを101番一人だけに絞っていましたが、コードを一部変更することで複数ユーザのレコメンドリストも一気に算出可能です。以下リンクにコードの例を記載してます。サンプルコード■(おまけ){recommenderlab}のモデルがやっていること{recommenderlab}での協調フィルタリングの実行例を冒頭で確認しましたが、中はどういう処理になっているのでしょう。実行部分のコードは以下の2行です。予測recオブジェクトの中身を確認してみましょう。NULL確認すると分かるのですが、このオブジェクトの中身はtrainつまり学習データそのものです。ベーシックな協調フィルタリングは、データをモデル化をしているわけではなく、すべての学習データを保持しているだけなのです。predict関数を実施する時に初めてレコメンド対象ユーザと保持している学習用ユーザすべての距離の組み合わせを計算し、推定します。協調フィルタリングは、このような仕組みのためどうしても処理コストがかかってしまいます。今回はユーザベース協調フィルタリングをスクラッチで実装してみました。シンプルなロジックですが、実装する際に検討すべきポイントが、それなりに多いことをおわかりいただけたと思います。今回の実装だと、速度面も含め改善の余地が大いにありそうです。次回は、レコメンデーションの評価指標について触れ、モデルの評価・改善、協調フィルタリング以外の手法との精度比較を行いたいと思います。それではごきげんよう。●「レコメンドつれづれ」の連載記事blog.brainpad.co.jpblog.brainpad.co.jpブレインパッドは、データサイエンティストの皆さまを積極的に採用しています。データサイエンティストの他にもエンジニアやコンサルなどの職種も募集していますので、ぜひ採用ページをご確認ください。採用ページスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。