言語処理100本ノック 2015年版 (73)
今日は73番だけでいきます。
73. 学習
72で抽出した素性を用いて,ロジスティック回帰モデルを学習せよ.
ロジスティック回帰モデルとは、「確率化された分類モデル」の一種です。「確率化された分類モデル」は全体の確率を0から1までの間に押し込まないといけないのですが、そのときにロジスティック関数を使うので「ロジスティック回帰モデル」と言います。
分類モデル
+確率化分類モデル
+ロジスティック回帰モデル
確率化分類モデルとしては他にも「対数線形モデル」などがあります。こちらは前回紹介した『言語処理のための機械学習入門』に入っています。
設問冒頭にもありましたがやりたいのは「文を肯定的(ポジティブ)もしくは否定的(ネガティブ)に分類する」ということです。72で作成した素性とロジスティック回帰モデルを使って分類器を作っていくのが今回の設問です。
で、なぜロジスティック関数を使うのかと言うと、計算が便利だからです。本来は「累積密度関数」を使いたいんだけれども、こちらは膨大な計算が必要になるので、劇似の関数でありながら計算量が少なくて済む「ロジスティック関数」を使うわけです。言ってみれば「代用品」だったわけですが、代用品のほうが便利かつ性能としても十分なので「正規品」を駆逐してしまいました。
で、この「ロジスティック回帰モデルを学習せよ」というのがたいていの人には意味が分からないと思うんですが、ようは「この文書sentiment.txt内の素性がどれだけ『肯定的』か『否定的』かを一つ一つ計算しろ」と言うことです。
例えば、hope(希望)という単語が pos にしか出てこなければこの素性hopeの数値を+1にし、逆にdespair(絶望)という単語が neg にしか出てこないのであれば素性despairの数値を-1にしてみよう、ということです。そして、 pos と neg に同数出てくる素性food(食料)は0の値を与える。
素性がどれだけ『肯定的』か『否定的』かはこんな形で表します
w = [全素性ベクトルのリスト]
このwを「重みベクトル」と言います。
さっきの例でいくと、
w = ["hope","despair","food"]
= [1,-1,0]
のようになります。
pythonをやっている人ならすぐに分かると思いますが、辞書化してしまえば良いのです。
で、この重みの計算をロジスティック回帰でやれと。ロジスティック回帰は解析的に解けないので、最適化法を使います。
最適化法としては
・勾配降下法
・確率的勾配降下法
・ミニバッチ確率的勾配降下法
・共益勾配法
・準ニュートン法(BFGS,L-BFGS)
などいろいろありますが、今回は確率的勾配降下法を使います。
weight = weight - 学習率×(予測確率-正解ラベル)×特徴ベクトル
この式に至るまでの説明はさすがに省きます。
「機械学習 はじめよう 第19回 ロジスティック回帰の学習」
http://gihyo.jp/dev/serial/01/machine-learning/0019
なんかがいいです。この記事より前の「第15回 分類問題ことはじめ」あたりから読んだ方がいいと思います。
特徴ベクトルというのは元データをベクトル化したものです。例えば"hope,food"という文ならこの例だと[1,0,1]になります。
予測確率は重みと特徴ベクトルの内積を識別関数に入れた結果のことです。
予測確率=識別関数(重みベクトルと特徴ベクトルの内積)
識別関数として今回はロジスティック関数σ(x) = 1 / ( 1 + exp(-x))を使うわけです。
で、(予測確率-正解ラベル)を計算することで予測関数が正解とどのぐらいかけ離れているかが分かります。
例えば予測確率が0.7で、正解ラベルが1だった場合、(0.7-1)で-0.3ぐらい調整しましょうということになります。最終的には引き算なので係数がマイナスになると重みベクトルが増えることに気をつけてください。逆に正解ラベルが0だった場合、(0.7-0)で0.7ぐらい調整される、この場合だとマイナスされるわけです。
で、こいつらに学習率を掛けます。学習率は徐々に減らしていきます。
つまり上の式は、
新しい重みベクトル = 前の重みベクトル - (学習率×訓練文に対する調整率×訓練文のベクトル)
ということになります。
今回はいつものようなフルスクラッチではなく、こちらのコードを参考にしています。
『線形識別モデル(PRML第4章) - 確率的識別モデル (4.3),ラプラス近似 (4.4), ベイズロジスティック回帰 (4.5) 』
http://www.chokkan.org/publication/survey/prml_chapter4_discriminative_slides.pdf
collections.defaultdict とリスト内包表記を使った内積の計算方法が衝撃的でした。
それ以降の部分は本文に合わせて変更しております。
本来は学習率も、
・最初の値は何にすべきで
・どうやって減少させていくのか
を考えなければなりません。
学習率の他にも素性ベクトルの初期値をどうするのかという問題があります。0から1までのランダムな値を与えることもありますが、今回は後で変更できるようにしつつ1を入れておきました。
#!/usr/bin/env python import codecs import collections import math fsen = codecs.open('72.txt', 'r', 'latin_1') fout = codecs.open('73.txt', 'w', 'latin_1') eta0 = 0.6 #学習率 etan = 0.9999 #学習率減少率 guard = 0.0002 #桁溢れ防止 def sigmoid(x): return 1.0 / (1.0 + math.exp(-x)) def update(W, features, label, eta): #内積の計算 a = sum([W[x] for x in features]) init_feature = 1 predict = sigmoid(a) #ラベルの正規化 label = (label + 1) / 2 #重みベクトルの計算 for x in features: dif = eta * ( predict -label ) * init_feature #差が0に近づきすぎると無視 if (W[x] - dif) > guard or ( W[x] - dif) < (guard * -1): W[x] = W[x] - dif if __name__ == "__main__": t = 0 W = collections.defaultdict(float) #重みベクトル計算 for line in fsen: features = line[:-1].split(" ") update(W, features[1:], float(features[0]), eta0 * ( etan **t)) t += 1 #ファイルへ書き出し for x in W.items(): line = "\t".join( map( str,list(x) ) ) fout.writelines(line+"\n")
で、残念ながらこの学習がうまく行っているかどうかは、この重みベクトルを使って実際に分類するまで分かりません。