ランキング学習2　実装編

[Aidemy](https://aidemy.net/)　2020/12/3
#はじめに
　こんにちは、んがょぺです！バリバリの文系ですが、AIの可能性に興味を持ったのがきっかけで、AI特化型スクール「Aidemy」に通い、勉強しています。ここで得られた知識を皆さんと共有したいと思い、Qiitaでまとめています。以前のまとめ記事も多くの方に読んでいただけてとても嬉しいです。ありがとうございます！
　今回は、ランキング学習の2つ目の投稿になります。どうぞよろしくお願いします。

＊本記事は「Aidemy」での学習内容を「自分の言葉で」まとめたものになります。表現の間違いや勘違いを含む可能性があります。ご了承ください。

今回学ぶこと
・
・


#前準備
##データの準備
・今回は__LETORデータセット__を使っていく。これは、__ランキング学習__の際に使われるデータセットであり、Microsoftによって公開されている。
・LETORデータセットには様々な形式が用意されているが、今回は__教師あり学習用のデータ__を使用する。また、バージョンはMQ2008を使用する。このデータセットは、__「正解ラベル（0,1,2）」「クエリID」「46次元の特徴ベクトル」__を含む。

・コード（今回はあらかじめ取得したものを使用）![スクリーンショット 2020-11-23 16.45.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/5dc85fd5-7459-925f-b418-a00247aa625e.png)

・結果（一部のみ）![スクリーンショット 2020-11-23 16.46.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/4fa6900b-6aec-32da-4101-bdc5fc0b22fc.png)

##データの整形
・前項で取得したデータセットについて、__「訓練/テストデータへの分割」「ラベル・特徴ベクトル・クエリへの分割」__の2つを行う。
・「訓練/テストデータへの分割」は通常の機械学習の時と同様、__「train_test_split」__で行う。テストサイズはデフォルトで0.25なので、今回はそのままで使用する。
・「ラベル・特徴ベクトル・クエリへの分割」について、__「ラベル（y）」__は__dataの0列目__にあり、順序はそのままで取り出す。__「特徴ベクトル（X）」__は__dataの2〜47行目__にある。__「クエリ（qlist,qid）」__は__dataの1行目__にあり、「qlist」はクエリの重複があっても良いが、「qid」はクエリの重複は削除される。
・以下はここまでを行ったコードである。qid以外は__DataFrame型__、qidのみ__ndarray型__で作成する。さらにqidについては、「qlist」について__「drop_duplicates()」__を使うことで__重複を削除する__ことができる。

![スクリーンショット 2020-11-24 0.11.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/632f70d6-15b2-e958-b535-fe0ce63e2ffb.png)

・結果（それぞれ前から5つ取得）![スクリーンショット 2020-11-24 0.16.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/f2beb6a8-7e32-701a-1771-85530a5cca27.png)

#二値分類
##予測と重み更新
・ここでは__パーセプトロン__のアルゴリズムを実装していく。パーセプトロンは予測と重みベクトルの更新を繰り返して学習を行う。今回は、ラベルを予測する関数__「predict」__、予測ラベルと正解ラベルが異なっていた時のみ重みベクトルを更新する関数__「modify」__を実装する。
・実装は以下の通り。

![スクリーンショット 2020-11-24 11.07.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/7fae4752-325f-cb4c-46db-5227e756bd7c.png)

・コードの説明として、「predict」は特徴ベクトルXと重みベクトルwを受け取り、__「np.dot()」__で__内積__を取り、この符号が正なら__「+1」__、負なら__「-1」__を返すようにする。これは__「np.sign()」__で実装できる。
・__「modify」__は、__学習率c__を決め、渡された予測ラベルpredict_yと正解ラベルactual_yが同一でない場合は__「w += actual_y\*c\*X」__で重みベクトルを修正し、これを返している。

・また、これらの関数に渡すデータについて、特徴ベクトルは__正規化__し、ラベルyは行列の形を加工している。

![スクリーンショット 2020-11-24 11.07.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/c021ca81-28c1-b482-524c-23243b0ae996.png)

・最後に、実際にfor文の中で、__すべてのトレーニングデータについて__「predict」と「modify」関数を使い、重みベクトルを学習させている。

![スクリーンショット 2020-11-24 11.08.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/6cc6a576-c344-f814-7d25-c61ee32bbcac.png)

・結果![スクリーンショット 2020-11-24 11.08.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/b17a6231-be93-5679-7e3f-f295b189834c.png)

##ランキングの作成、評価
・前項の操作によって、重みベクトルが学習されたので、これと特徴ベクトルXの__内積__を取ることで__ランキング関数を算出__し、これに基づいて各特徴ベクトル$X_i$にスコアをつけ、その__降順に並べたランキング__を生成する。
・さらに、性能評価指標である__「precision@K」「Average Presicion」「Reciprocial Rank」__を実装してランキングを評価する。

・ランキング関数を算出する関数__「f()」__は、そのまま特徴ベクトルXと更新した重みwの内積を返せば良い。また、ランキングは__「sort_values(ascending=False)」__でスコアの高い順（降順）に並べ替える。

・コード![スクリーンショット 2020-11-24 13.39.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/1feebc11-ca41-64e9-b819-2c8b3db98bd1.png)

・結果![スクリーンショット 2020-11-24 14.58.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/74970e81-17aa-d7aa-f954-08f2b49a77ca.png)


・precision@Kを実装する関数__「precision_at()」__は、ランキングk位までの__適合である割合__を算出すればよいが、ランキングk位までにデータが存在しない場合、__分母が0__になってしまいエラーが発生するため、この時は「0」を返すようにする。これを__「ゼロ除算」__という。
・ランキングi位が適合であるかどうかは__「label」の「i番目」が「1」__であるかどうかでわかる。

・AveragePrecisionを実装する関数__「AP()」___は分子がランキングk位までprecition_at()で算出した値の合計（__sum__）、分母がランキングの個数(__devby__)として、平均を取る。ここでもゼロ除算を行う。

・ReciprocialRankを実装する関数__「RR()」__は、初めて「適合」が出てきた順位の__逆数__を取れば良いので、普通に__for文で適合かどうかを探していき__、適合なものがあった時点でその時の順位iについて__「1/(i+1)」__を返せば良い。(i+1なのはランキングは1位からであるが、インデックスは0からスタートしているため)また、今回は分母が0になることはないので、ゼロ除算は必要ない。

・コード![スクリーンショット 2020-11-24 14.58.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/7a8207f4-2a51-4a8a-3121-a728ec0bad2e.png)

・結果![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/0872a265-5193-8450-dda8-b1a640c4671d.png)
￼
#PRank
##予測と重み更新
・__PRank__はランキング関数と複数の__閾値__によって__離散値のラベル__を予測する手法である。よって、重みベクトルwだけでなく、閾値$\tau$も学習させる必要がある。

・ラベルを学習し予測する関数__「predict()」__について、渡された__label_list__と__b__からlabelとthを一つずつ取り出し、__「np.dot(X,w)-th<0」__を最初に満たすものが__最小のラベル値__となるので、これを__予測ラベルとして返す__。
・閾値ベクトル$\tau$を更新する関数__「tau()」__について、Chapter1で確認したように、__r__が「予測ラベル以上正解ラベル未満」なら「+1」、「正解ラベル以上予測ラベル未満」なら「-1」、それ以外なら「0」が$\tau^r_0$となる。よってここでもその通りに記述すれば良い。
・重みベクトルwを更新する関数__「modify\_w()」__について、$w_t+1$は、$w_t$に、$\tau$の__要素和__を特徴ベクトルの各要素と__掛け合わせたものを足す__ことで算出できる。コードでは、__「tau()」__に__「np.sum()」__を使って$\tau$の要素和を算出し、これに__「X」__をかけたものを、元の__w__に足している。
・__閾値b__を更新する関数__「modify_b()」__について、これは、bから、その時点での__閾値ベクトル__$\tau$を引くことで算出できる。

![スクリーンショット 2020-11-24 15.52.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/99b71e70-a6fa-cb96-fc3f-2a3a4bde1e0a.png)

・あとは、__train_X__のそれぞれについて、__predict()__で予測ラベルを定義し、__modify\_w__と__modify\_b__でそれぞれwとbを更新することで学習させる。そして、更新した値を使ってテストデータのラベルを予測し、正解ラベルと照らし合わせる。

・コード![スクリーンショット 2020-11-24 15.53.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/b6605223-7c22-dccf-4626-032af99ebe0f.png)

・結果![スクリーンショット 2020-11-24 15.53.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/5d40ebc6-8483-c21b-a82b-745e299ba4f2.png)

##ランキングの評価
・二値分類の時とは違い、PRankは__ラベルが0以上の離散値__で与えられているので、このような場合の評価指標は__DCG,NDCG__を使うと良い。
・__DCG__はランキング第二位以降のk位に「$1/log2_(k)$」をかけることで実装する。第一位はそのまま足せば良いので、__「ranking["actual\_label"].iloc[0]」__で第一位（iloc=0）のラベルを返り値__「dcg」__に格納する。第二位以降はforループで取り出し、__「np.log2(i+1)」__で掛け合わせることで算出する。
・__NDCG__は、算出したDCGを、ランキング中の__ラベルが全て最高値__であった場合のDCG__「dcg_perfect」__で割ることで実装する。
・ラベルの最高値__「max_label」__は、__label_listの最後尾の値__を見れば良い。最高値「max_label」はそのまま__「dcg_perfect」__に足し合わせ、それ以下のランキングのものについては、最高値の部分をDCGと同じようにわりびいて「dcg_perfect」に足し合わせる。

・コード![スクリーンショット 2020-11-24 16.27.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/6d04ade3-a28b-8990-0c60-c56e0b85afb5.png)

#Pairwise
##ペアの作成
・__Pairwise__では、学習データとして__「クエリ文書ペア」__を使うので、まずはこれを作成する。この作成にあたり、__「makepair」「pairmerge」「qmerge」__の3つの関数を作成する。各関数の意味などについては以下で説明する。
・Pairwiseで組とするのは、__同一クエリで異なるラベルのもの__のみである。そのため、データ（train_X）を__クエリごとに分類し__、さらにそれを__ラベルごとに分類__しなければならない。ここまで行って、初めて組を作ることができる。
・まずは__「makepair()」__で、クエリとラベルを指定して__train_Xのペアのリスト__を作成する。この時、それぞれのラベルを__「score1」「score2」__として関数に渡す。
・makepair()で作成されたtrain_Xのペアについて、__クエリを指定__してそれぞれの組ごとに__ラベルを統合__する関数__「pairmerge()」__を作成する。
・最後に__クエリを統合__する関数__「qmerge()」__を作成することで、__「クエリとラベルが統合されたtrain_Xのペア」__が作成される。図示すると次のようになる。

![スクリーンショット 2020-11-24 23.53.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/3067161a-1f68-8bb1-35ab-d03518d3f130.png)

・具体的なコードについて、「makepair()」は、引数で渡されたscore1,score2,qを参照して、__「ラベルがscore1で、かつクエリがqであるX」__を__「X1」__、同様にscore2について__「X2」__を作成し、yについても同じように__「y1」「y2」__を作成する。
・この「X1」「X2」について、それぞれの組ごとに__「X1.iloc[i,:]-X2.iloc[j,:]」__で差分をとったものを返り値として設定するDataFrame__「return_X」__に格納する。「y1」「y2」についても同様に__「return_y」__に格納し、これらを返す。

・コード![スクリーンショット 2020-11-25 0.26.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/568e68f1-53ff-3c9f-e944-23be1bd2dce7.png)

・「pairmerge()」について、こちらは__「クエリは指定し、ラベルは自由」__であるので、「クエリは渡された__qで固定__、ラベルは__(0,1,2)__から選択」として「makepair()」関数で組を作成する。
・作成した全ての組について、Xとyそれぞれについて__「pd.concat()」__で__連結__して、「pair_X」「pair_y」として返す。

・「qmerge()」に関しては、、__for文__で全てのクエリについて「pairmerge()」を適用することで、「ラベルとクエリが統合されたXとy」が作成されるので、これを返せば良い。

・コード![スクリーンショット 2020-11-25 0.26.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/05e56609-ec35-3997-456f-ad0779b77906.png)

##予測と重み更新
・基本的な学習、予測の流れは二値分類の時と同じ__パーセプトロン__のアルゴリズムを実装していく。すなわち、ラベルの予測と重みベクトルの更新を繰り返すことで学習を進めていく。関数についても、ラベルを予測する関数__「predict()」__と、予測が異なった場合に重みベクトルを更新する関数__「modify()」__を実装していく。

・predict()については、特徴ベクトルと重みベクトルの内積を__「np.dot(X,w)」__で算出し、これに__「np.sign()」__を使うことで符号が正なら「+1」、負なら「-1」を返すようにする。
・modify()についても、predict_yとactual_yに__「np.sign()」__を使った上で符号が同じかを比較し、異なる場合は、__「w+=c\*np.sign(actual_y)\*X」と__して重みを更新する。
・2つの関数が定義できたら、for文で全てのtrain_Xについてpredict関数とmodify関数を使って重みベクトルを学習させる。

・コード![スクリーンショット 2020-11-25 0.43.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698700/e39b26eb-ba37-37b5-d836-02169cf9dc72.png)

##ランキングの評価
・ランキング関数によって各特徴ベクトルにスコアをつけ、その__降順でランキングを作成__する。このランキングについて、他の手法でも扱った__precision@K,AP,RR__でランキングを評価する。これらは全て二値分類の時と同じコードで実装できるので、くわしくはそちらを参照。

#まとめ
・ランキング学習のデータセットである__「LETORデータセット」__を使って学習データを作成する。__train_test_split()__を使う。
・この学習データを使って__重みベクトルを学習__する。__二値分類__は__パーセプトロン__のアルゴリズムで作成できるので、予測関数__「predict()」__と、更新関数__「modify()」__を実装する。また、__「precision@K」__などのランキング評価指標を使って評価を行う。
・__PRank__はラベルが__離散値__なので、__閾値ベクトル__$\tau$も学習させる。評価については、__DCG、NDCG__が有効である。
・__Pairwise__は訓練データを__「文書クエリペア」__にして渡す。それ以外は基本的に二値分類と同じ。

今回は以上です。最後まで読んでいただき、ありがとうございました。