Aidemy 2020/12/3
はじめに
こんにちは、んがょぺです!バリバリの文系ですが、AIの可能性に興味を持ったのがきっかけで、AI特化型スクール「Aidemy」に通い、勉強しています。ここで得られた知識を皆さんと共有したいと思い、Qiitaでまとめています。以前のまとめ記事も多くの方に読んでいただけてとても嬉しいです。ありがとうございます!
今回は、ランキング学習の2つ目の投稿になります。どうぞよろしくお願いします。
*本記事は「Aidemy」での学習内容を「自分の言葉で」まとめたものになります。表現の間違いや勘違いを含む可能性があります。ご了承ください。
今回学ぶこと
・
・
前準備
データの準備
・今回はLETORデータセットを使っていく。これは、ランキング学習の際に使われるデータセットであり、Microsoftによって公開されている。
・LETORデータセットには様々な形式が用意されているが、今回は教師あり学習用のデータを使用する。また、バージョンはMQ2008を使用する。このデータセットは、「正解ラベル(0,1,2)」「クエリID」「46次元の特徴ベクトル」を含む。
データの整形
・前項で取得したデータセットについて、「訓練/テストデータへの分割」「ラベル・特徴ベクトル・クエリへの分割」の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()」を使うことで重複を削除することができる。
二値分類
予測と重み更新
・ここではパーセプトロンのアルゴリズムを実装していく。パーセプトロンは予測と重みベクトルの更新を繰り返して学習を行う。今回は、ラベルを予測する関数「predict」、予測ラベルと正解ラベルが異なっていた時のみ重みベクトルを更新する関数「modify」を実装する。
・実装は以下の通り。
・コードの説明として、「predict」は特徴ベクトルXと重みベクトルwを受け取り、「np.dot()」で内積を取り、この符号が正なら「+1」、負なら「-1」を返すようにする。これは「np.sign()」で実装できる。
・「modify」は、学習率cを決め、渡された予測ラベルpredict_yと正解ラベルactual_yが同一でない場合は「w += actual_y*c*X」で重みベクトルを修正し、これを返している。
・また、これらの関数に渡すデータについて、特徴ベクトルは正規化し、ラベルyは行列の形を加工している。
・最後に、実際にfor文の中で、すべてのトレーニングデータについて「predict」と「modify」関数を使い、重みベクトルを学習させている。
ランキングの作成、評価
・前項の操作によって、重みベクトルが学習されたので、これと特徴ベクトルXの内積を取ることでランキング関数を算出し、これに基づいて各特徴ベクトル$X_i$にスコアをつけ、その降順に並べたランキングを生成する。
・さらに、性能評価指標である「precision@K」「Average Presicion」「Reciprocial Rank」を実装してランキングを評価する。
・ランキング関数を算出する関数「f()」は、そのまま特徴ベクトルXと更新した重みwの内積を返せば良い。また、ランキングは「sort_values(ascending=False)」でスコアの高い順(降順)に並べ替える。
・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になることはないので、ゼロ除算は必要ない。
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$を引くことで算出できる。
・あとは、train_Xのそれぞれについて、predict()で予測ラベルを定義し、modify_wとmodify_bでそれぞれwとbを更新することで学習させる。そして、更新した値を使ってテストデータのラベルを予測し、正解ラベルと照らし合わせる。
ランキングの評価
・二値分類の時とは違い、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」に足し合わせる。
Pairwise
ペアの作成
・Pairwiseでは、学習データとして「クエリ文書ペア」を使うので、まずはこれを作成する。この作成にあたり、「makepair」「pairmerge」「qmerge」の3つの関数を作成する。各関数の意味などについては以下で説明する。
・Pairwiseで組とするのは、同一クエリで異なるラベルのもののみである。そのため、データ(train_X)をクエリごとに分類し、さらにそれをラベルごとに分類しなければならない。ここまで行って、初めて組を作ることができる。
・まずは「makepair()」で、クエリとラベルを指定してtrain_Xのペアのリストを作成する。この時、それぞれのラベルを「score1」「score2」として関数に渡す。
・makepair()で作成されたtrain_Xのペアについて、クエリを指定してそれぞれの組ごとにラベルを統合する関数「pairmerge()」を作成する。
・最後にクエリを統合する関数「qmerge()」を作成することで、「クエリとラベルが統合されたtrain_Xのペア」が作成される。図示すると次のようになる。
・具体的なコードについて、「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」_に格納し、これらを返す。
・「pairmerge()」について、こちらは「クエリは指定し、ラベルは自由」であるので、「クエリは渡されたqで固定、ラベルは(0,1,2)から選択」として「makepair()」関数で組を作成する。
・作成した全ての組について、Xとyそれぞれについて「pd.concat()」で連結して、「pair_X」「pair_y」として返す。
・「qmerge()」に関しては、、for文で全てのクエリについて「pairmerge()」を適用することで、「ラベルとクエリが統合されたXとy」が作成されるので、これを返せば良い。
予測と重み更新
・基本的な学習、予測の流れは二値分類の時と同じパーセプトロンのアルゴリズムを実装していく。すなわち、ラベルの予測と重みベクトルの更新を繰り返すことで学習を進めていく。関数についても、ラベルを予測する関数「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関数を使って重みベクトルを学習させる。
ランキングの評価
・ランキング関数によって各特徴ベクトルにスコアをつけ、その降順でランキングを作成する。このランキングについて、他の手法でも扱ったprecision@K,AP,RRでランキングを評価する。これらは全て二値分類の時と同じコードで実装できるので、くわしくはそちらを参照。
まとめ
・ランキング学習のデータセットである「LETORデータセット」を使って学習データを作成する。train_test_split()を使う。
・この学習データを使って重みベクトルを学習する。二値分類はパーセプトロンのアルゴリズムで作成できるので、予測関数「predict()」と、更新関数「modify()」を実装する。また、「precision@K」などのランキング評価指標を使って評価を行う。
・PRankはラベルが離散値なので、閾値ベクトル$\tau$も学習させる。評価については、DCG、NDCGが有効である。
・Pairwiseは訓練データを「文書クエリペア」にして渡す。それ以外は基本的に二値分類と同じ。
今回は以上です。最後まで読んでいただき、ありがとうございました。