コード読みながら理解する機械学習〜porn_sieve〜
2016/08/25
porn_sieveは、好みの動画(xvideos)を数値評価することでアナタに最適な動画(xvideos)を提供してくれるPython製アプリだ。
最適な動画を選定するために「機械学習」ってのが使われているわけですね。
アヤメの分類とか全く興味持てないし、退屈過ぎてシンドかった。
下世話なネタだったら理解を深められそうだよね。
その過程をメモ。
下記あたりが話題に出てくるよ。
- sklearn
- 教師あり学習
- 回帰
- データをベクトル化する
- 説明変数と目的変数
- ランダムフォレスト
- 主成分分析
ここのrefit_from_scratch
って処理を読めば終わりなので、以降は読まなくてOK
Contents
モデルを作る
マンガ中毒な知人=モデル
機械学習と一言に言っても色々な種類がある。
porn_sieveは「ユーザーが複数の動画を評価したデータをもと、未知の動画の価値を予測」してる。
未知の動画を評価するためには「この動画は100点満点中80点、こっちの動画は30点…」といったようにラベル付されたデータが複数(多い方がいい)必要になる。
これは現実世界でも経験してる気がする。
例えば、マンガ中毒な知人と会話してるときによくあるのが…
「進撃の巨人とか、GANTZみたいな理不尽な状況に巻き込まれて簡単に人が死ぬようなマンガ好きなんだけど、なんかオススメある?」
「あー、それだったら…神様の言うとおりとか好きそう」
とか
「野球マンガとかあんまり好きじゃないんだよね。まずルールよく分からんし、あとスポーツマンガ全体に変な特殊能力合戦みたいになってすぐインフレ起こすイメージあってさ…」
「あー、そういや前に嘘喰いとか金と銀みたいなマンガは好きって言ってたよね?それならワンナウツとか面白いと思うよ。野球マンガだけど、インフレ要素一切ないし、主人公と敵との頭脳・心理戦がメインだからハマると思うなあ」
「ワンナウツ面白かったわ!」
「それはよかった。さすがライアーゲームの作者って感じだよねー」
「他の野球マンガも読んでみたくなったわ」
「グラゼニとかも面白いよ。普通さ主人公がエースピッチャーとかとんでもないスラッガーだったりするじゃん?年俸も億単位でもらってるし。ワンナウツもそういう感じっちゃそういう感じ。でも、グラゼニの主人公は普通の中継ぎ投手なんだよね。年俸も1800万だし笑そういうプロ野球選手の現実な面を描いてて違った視点から野球を見ることができて面白いね。」
「あー、それ面白そうだわ」
…みたいなマンガのレコメンドエンジン(人の叡智)ね。
このように、事前に与えられたデータ群を元にして、未知のデータを評価するような手法を「教師あり学習」というらしい。
で、こういう「教師あり学習」をやってくれる仕組みってのがモデル。
マンガ中毒の知人の例で言えば、モデルはまさに知人のことだよね。
で、事前与えられたデータ群ってのは
「神様の言うとおり」をオススメするとき
* 進撃の巨人が好き
* GANTZが好き
「ワンナウツ」をオススメするとき
* スゴい能力合戦になるインフレ系スポーツマンガは嫌い
* 嘘喰いが好き
* 金と銀が好き
「グラゼニ」をオススメする
* 野球マンガが好き
* スゴい能力合戦になるインフレ系スポーツマンガは嫌い
こんな感じだろうか。
未知のデータってのは、被推薦者がまだ読んだことのないマンガを指す。
- 神様の言うとおり
- ワンナウツ
- グラゼニ
事前データ群から嗜好を学習して、未知のデータの価値や種類を予想する。
Gmailでスパムメール判別してくれるけど、これも教師あり学習の一例(実際にどんなアルゴリズムでやってるか知らないので、もしかしたら教師なし学習でクラスタして判定してるかもしれないし、強化学習だったりするのかもね…)。
大量に蓄積されてる過去のメールからスパムメールか否かを学習して、新たに届いたメールがスパムか否か判定する。
で。
当たり前だけど、学習がイイカンジにできないと予想内容はお粗末になる。
進撃の巨人やGANTZが好きだって言ってるのに、花より男子オススメされても「え?!なんで少女マンガなの?しかも恋愛モノでしょ?」となる。(セレンディピティ笑と主張すれば許されるかもしれないが、文脈を全く踏まえられていない)
スパムメールも然り、なんの脈絡もなく「おめでとうございます!グリーンカードが当選しました!!発行のお手続きはこちらから…」みたいなメールが受信箱に入ってたら「いやいや、これは明らかにスパムメールだろうよ。。。」って思う。
マンガ中毒な知人は優秀なモデル。
与えられた「理不尽な状況に巻き込まれて簡単に人が死ぬようなマンガ」の事例から、類似したマンガを提案してくれる。
「インフレ系スポーツマンガが嫌い(インフレしなければ好きになる可能性がある)」&「嘘喰いや金と銀などの心理戦を中心に置いたマンガ」という事例から、スポーツマンガではあるが、能力のインフレがなくて心理戦要素が強い「ワンナウツ」を提案してくれる。
少し劣化したモデルだと「インフレ系スポーツマンガが嫌い」という事前データを「スポーツマンガが嫌い」と足切りしてしまって、スポーツマンガ以外の心理戦系マンガを提案する。「アカギ」とか「ライアーゲーム」とかね。
まあ、このレベルの提案でもダメではない気がするよね。
ただ「ワンナウツ」の提案と比べたら感動は少ないかな。
モデルに求める要件によって、作り込みの複雑さや学習時間が変わってきそう。
porn_sieveとモデル
porn_sieveではランダムフォレストというモデルを使ってる。
雑に説明すると、ランダムフォレストは中に「決定木」というモデルを大量に抱えてる。
未知のデータがランダムフォレストに与えられたら、そのデータを中で抱えてる決定木たちに渡して答えをそれぞれ出してもらう。
各決定木が導いた答えを多数決するなり平均したりして、最終的な答えを出力するわけ。
複数モデルを内包、統括して学習することをアンサンブル学習という。
各決定木が多数決を使うときと、平均値を使うときは何が違うの?
求める答えが離散値か連続値かによって使い分ける。
離散値の場合は多数決
各決定木が、このメールはスパムか否か?って問いに対してYES or NOって出力を出すので、多数決がいいよね。
こういう問題を分類問題という。
連続値の場合は平均値
各決定木が、このマンガは何点?って問いに対して60.0点とか98.6点って出力を出すので、平均値を使うと良さ気だよね。
こういう問題を回帰問題という。
porn_sieveでは動画を実数で評価するので回帰問題となるわけ。(仮に、この動画好き?嫌いって評価方法だったら分類問題)
処理内容を以下に抜粋。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
temp_model = RandomForest(max_features="sqrt", n_jobs=-1) temp_enc = CountVectorizer() X = [] # binary matrix the presence of tags Z = [] # additional numerical data Y = [] # target (to predict) values db_size = self.db.size() for data in self.db.yield_some(250): feedback = data["feedback"] tags = data[ "tags" ] if feedback and tags: Y.append( feedback ) X.append(" ".join(tags)) Z.append(self.fmt_numerical(data)) X = temp_enc.fit_transform(X) X = hstack((X, coo_matrix(Z))) self.allX = X pca = PCA(min(X.shape[0], 200)) reduced_X = pca.fit_transform(X.todense()) temp_model.fit(reduced_X, Y) |
pythonにはskleanという機械学習関係のアルゴリズムを多数実装したライブラリがある。
もちろんランダムフォレストだって用意されてる。
porn_sieveでは回帰問題を扱ってるのでRandomForestRegressor
をimportする。
1 |
from sklearn.ensemble import RandomForestRegressor as RandomForest |
分かりやすさのためかas
を使って単なるRandomForestとしてる。
sklearnの実装が素晴らしいのか知らないが、モデルを作るためにやることはスゴいシンプルで下記の2つだけ
- モデルの器をつくる
- 「特徴ベクトル」と「ラベル」の配列を突っ込んで
fit
で学習させる
コードは以下のとおり。
1 2 3 |
temp_model = RandomForest(max_features="sqrt", n_jobs=-1) # blah blah blah... temp_model.fit(reduced_X, Y) |
reduce_X
ってのが特徴ベクトルの配列で、Y
ってのがラベルの配列ですね。
じゃあ、この特徴ベクトルとラベルって何なの?
って思うので、この辺を次から見ていきたい。
特徴ベクトルとラベル?
この動画がどんなモノなの?っていう情報を何かしらのルールで表現する必要がある。
マンガだったら、ジャンル、雑誌名、作者、連載していた年代などなど…
これらの情報を1次元配列をしたものが特徴ベクトルと呼ばれてる。高尚な感じあるね。
特徴ベクトルは画一的なモノではなく取り扱う問題によって変わるらしいよ。
porn_sieveでは動画のタグ情報の出現頻度と、動画の再生時間や平均レビューを組み合わせたベクトルになってる。
[‘tagA’, ‘tagB’, … ‘tagX’, ‘再生時間’, ‘平均レビュー’ … ‘Awesome Data’]
って感じに項目があるとすると、実際の特徴ベクトルは下記のような感じになってる。
[0(この動画にはtagAは含まれていない),1(この動画にはtagBが含まれている), … 0, 1000(動画再生時間は秒単位になってるはず), 3.5(平均レビューね), … XXX]
こんな感じのベクトルを学習用に用意した動画毎に作っていく。
porn_sieveで特徴ベクトルを作ってるところはこんな感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
temp_enc = CountVectorizer() X = [] # binary matrix the presence of tags Z = [] # additional numerical data Y = [] # target (to predict) values db_size = self.db.size() for data in self.db.yield_some(250): feedback = data["feedback"] tags = data[ "tags" ] if feedback and tags: Y.append( feedback ) X.append(" ".join(tags)) Z.append(self.fmt_numerical(data)) X = temp_enc.fit_transform(X) X = hstack((X, coo_matrix(Z))) self.allX = X pca = PCA(min(X.shape[0], 200)) reduced_X = pca.fit_transform(X.todense()) |
DBからとってきた学習データをX,Y,Zのハコに突っ込んでゆく。
Xには作品のタグ情報をスペースでjoin
した文字列の配列。
joinする理由はCountVectorizer()が’this is my job.’といったスペース区切りの文章から出現頻度のベクトルを作るから。
Yには作品の評価がつっこまれてる。
Zにはスクレイピングしてきた時間やLIKE数のような数値データの配列がつっこまれてる。
https://github.com/PornSieve/porn_sieve/blob/master/predict.py#L98
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
def fmt_numerical(self, data): """ There are two categories of data Porn Sieve gathers: 1.Tag data, represented as a binary, mostly zero array of numbers 2. Data which is continuous, such as duration, average review, etc. For the tags, I can just use CountVectorizer out-of-the-box, but for the other data, we need to put it all together in a list on our own. """ nums = [] # sorted to ensure the data is always in the same order for k in sorted(data.keys()): if k in ["feedback", "img"]: pass elif type(data[k]) == list: pass elif data[k] == None: nums.append(0) elif (k == "scrape_date") and (type(data[k]) != float): stamp = datetime.strptime(data[k], "%Y-%m-%d %H:%M:%S.%f") epoch = datetime.utcfromtimestamp(0) nums.append((stamp - epoch).total_seconds()) elif np.isreal(data[k]): nums.append(data[k]) return nums |
X,Y,Zに必要な値を詰め込んだら、これを学習するために少し加工する。
CountVectorizer
で各作品のタグ情報を出現頻度のベクトル(1次元配列)に変換する。(正確には違う形式なんだが。。。)
1 |
X = temp_enc.fit_transform(X) |
つぎにhstackを使って行列Xの右隣にZを合体させてる
1 |
X = hstack((X, coo_matrix(Z))) |
見た目なイメージ的にはこんな感じ
[‘tagA’, ‘tagB’, … ‘tagX’,] + [‘平均レビュー’ … ‘Awesome Data’] = [‘tagA’, ‘tagB’, … ‘tagX’, ‘平均レビュー’ … ‘Awesome Data’]
ん…
このcoo_matrix
って何?
cooってのは『行列の情報を壊さずに、データ量を節約できる形式』のことなんだって。
そんな形式にしないで、普通に行列合体すればいいじゃん、って思う。
が、いずれ困る。
出現頻度ベクトルのようなデータを素直な行列として扱うとデータ量が莫大になる。
この出現頻度のベクトルってのは『疎な配列』になるってことから理解する必要がある。
CountVectorizer
で各作品のタグ情報を出現頻度のベクトルに変換する、って書いたけどこの出現頻度のベクトルってどんな見た目してるのか見てゆく。
まず、Xに含まれるtagの種類数が出現頻度ベクトルの長さになる。
…どういうこと?
具体例で考えてみる。
Xが以下のようになってるとする。
X[1] : [‘A B C D E F’]
X[2] : [‘A X’]
X[3] : [‘C’]
X[1]の作品には’A B C D E F’の6つのタグが含まれてる。っことね。
タグの出現頻度頻度のベクトルをどうやって作るか。
まず全てのタグ情報を並べる。(重複を除いて)
上の3作品を例にして並べてみると…累計7つのタグが登場してることが分かる。
‘A’, ‘B’ ‘C’, ‘D’ ‘E’ ‘F’ ‘X’
次にX[1]のタグ出現頻度を数えてみる。
‘X’以外1回登場してるので
A:1回, B:1回, C:1回, D:1回, E:1回, F:1回, X:0回
X[2]は’A’と’X’が登場するので
A:1回, B:0回, C:0回, D:0回, E:0回, F:0回, X:1回
X[3]は’C’が登場するので
A:0回, B:0回, C:1回, D:0回, E:0回, F:0回, X:0回
この頻度をベクトル(1次元配列)にしてえけど、ラベルが邪魔だよね。
Aを0番目、Bを1番目…ってすれば良くない?
すると
X[1]のタグ出現頻度ベクトルは…
[1, 1, 1, 1, 1, 1, 0]
X[2]のタグ出現頻度ベクトルは…
[1, 0, 0, 0, 0, 0, 1]
X[3]のタグ出現頻度ベクトルは…
[0, 0, 1, 0, 0, 0, 0]
と、できるわけ。
で、この配列の長さはタグの数の7になる。当たり前だわな。。
とりあえず、タグの種類の数が出現頻度ベクトルの長さに対応することは理解できた。
これ、作品数が3じゃなくて100とか1000とかあったらどうなるだろう?
タグの種類も増えそうだよね。
タグ出現頻度ベクトルの長さが1000とか超えてもおかしくない。
で、ちょっと考えてみると。
ある作品についてるタグなんてせいぜい10件とかなわけ。
このときのタグ出現頻度ベクトルってほとんどが『0』でポツポツと『1』が出てくるって感じになる。
こういう状態を『疎』という。
で、この『疎』なベクトルを馬鹿真面目にメモリ空間に載せるとメモリを食いつぶしちゃう。
「ここは『0』です」っていう無駄のようで必要な情報に溢れかえるから。
で、メモリ食いつぶしちゃうってことは、PCが計算するために必要な領域(よく作業机に例えられるよね)が無くなるってこと。
作業机が本やらノートやらペンに埋もれてたら、机ではもう作業できないよね。。
なのでCountVectorizer
は少し変わった形式で出現頻度ベクトルを圧縮する。
実際にprintなりして見るとわかるけど
1 2 3 4 5 6 7 8 9 10 11 12 |
(0, 10) 1 (0, 29) 1 (0, 23) 1 (0, 34) 1 (0, 37) 1 (0, 12) 1 (0, 21) 1 (0, 30) 1 (0, 11) 1 (0, 33) 1 (0, 8) 1 (0, 22) 1 |
こんな感じ。
(0, 10) 1
↑これはX[0]の作品では10番目のタグが出現してる。ってこと。
『ある作品にはこのタグが出現してる』っていう目撃情報だけを記録するわけ。
で、この目撃情報リストに載ってない場所はもれなく『0』である、とみなしてる。
こうすればベクトルの中身は壊さずに、領域を節約することができる。
で、この形式をCOO Formatっていうらしいよ。
http://www.scipy-lectures.org/advanced/scipy_sparse/coo_matrix.html
だからcoo_matrix
なのね。
そう。
CountVectorizer
を通すとXがCOO形式に変換されるので、Zの中身もCOO形式にして足並みを揃えてから合体させる。
次は主成分分析を行う。
文書群を疎なベクトルに変換すると次元数がやたら大きくなる。
このベクトルをそのまま学習させると計算量が増えたり、『次元の呪い』という現象が発生して汎化性能が上がらないといった現象が起こる。ってネットに書いてた。
意味のある特徴は残しつつ、次元数は減らしたい。という欲望がわいてくるわけだ。
この欲望を主成分分析は叶えてくれる。
scikit-learnネ申はPCA(主成分分析の略称)だって用意してくれている。
1 2 |
pca = PCA(min(X.shape[0], 200)) reduced_X = pca.fit_transform(X.todense()) |
これだけで高次元なベクトルを200次元(もしくはXの次元数←元のベクトルが200次元もないならムリに200次元にしなくていいものね)まで落としてくれる。
で、この次元を落としたベクトルを使ってランダムフォレストを学習させてゆく。
もっと自分好みなporn_sieveにしたいんだけど!!????!!!?
いくらか学習させてみたけど全然ダメだな、と思ったら自分なりに色々試してみるとよさそう。
他のモデルを使ってみたり、モデルのパラメータを変えてみたり、特徴ベクトルとして扱う情報を加えてみたり(例:動画のサムネをベクトル化してみる)…
また、単純にデータ量が足りないケースや、教師データに偏りがあったりする場合もある(好きな動画ばかり教師データに追加してしまっていた…とかね)。
いやあ、機械学習って奥深いですね…
日本ならxvideosよりDMMかな?
そういうや、porn_sieveの内容をDMMの動画作品評価に適用したモノを作ってる。
興味あったらみてね。
336px
336px
関連記事
-
-
KerasのCNNを使って文書分類する
Co …