CNNを利用したセンチメント分析
■ CNNを利用したセンチメント分析
Deep Learningを利用したテキスト解析が去年の12月ごろから盛んです。ネットの日本語のQIITAやはてなブックマークを見ていると、流行したのが去年から今年の頭あたりでインパクトの強い文献は出尽くしているように見えます。2015年度12月を前後にLong short-term memoryやリカレントニューラルネットワークは自然言語処理、テキストマイニングで多大な功績を残しているケースが多いようです。
Deep Learningの基礎である、ニューラルネットワークは理論が提唱され有効性が実証されるが実現可能なコンピュータリソースが足りずに長いスリープタイムを経てきました。今回もまた長い眠りに入ってしまうのでしょうか。今回はビックなインパクトをシン・ゴジラなみに社会にぶちかましてほしいものです。
■ 代表的なDeep Learningを用いたセンチメント分析手法
Word Level CNN: CNNですが、単語単位で学習します[1]。今回はこのWord LevelのCNNについてTensor Flowを用いた場合の述べます。
Character level CNN: 一般的にCNNは画像の分類などで用いられる手法ですが、十分に効果を発揮することが示されています。[2]
Character level LSTM (RNN) : QIITAでの性能評価でよいスコアを示しています[3]。翻訳モデルやシーケンシャルなインプットデータで高い力を発揮するRNNです。
■ CNNで言語情報を扱うことができるのか
一般的な言語を扱うタスクとしてはchar-rnnやseq2seqなどが有名です。char-rnnはシェイクスピアの書いた台本を学習し、seq2seqは英語とフランス語を対にすることで高い精度の翻訳性能を出しています。これらはRNNという技術をベースにしており、画像処理の基本となるCNNとはまた違ったものであります。
CNNは画像の分類問題で高い性能を発揮して手書きの数字を分類するMNISTでは95%以上の高い分類性能を発揮します。CNNの入力はピクセルとして扱われるのですが、これを言語処理に転用しようとした時に言語処理のデータでは到底画像のピクセルのような大きなデータにはなりえません。この時、word2vecという単語をベクトル空間に変換する技術が大きな役割を占めることになります。
蛇足ですが、word2vecは単語を数百次元のベクトル値として表現することが可能な技術で、単語のベクトル値どうし、足し算と引き算ができるので面白い結果を得ることができます。
ex) 人間 + 偉い - 男 = 女王様
ベクトル化したデータをもとに、マトリックスを組むと以下の様な値を得ることができます。このマトリックスの画像のピクセル郡との類似性を利用してCNNで処理するのです。
input: 尾頭さん の 可愛すぎ て 素敵すぎ て もう
converted:
尾頭さん -> [0.27, 0.33, … ]
の -> [0.0 , 0.1, … ]
可愛すぎ-> [0.11, 0.2, …]
...
■ アウトプットは単純な超低次元なベクトル
いくらニューラルネットワークが高次元なネットワークを処理することができると言っても、多くの場合、多クラス問題では、優秀な画像処理のモデルのVGGNetで1000次元が限界ではあります。一般的なチュートリアル的存在のMNISTは数字の0~9を認識するだけなのでアウトプットは10次元です。
インプットしたデータに比べて次元が減るのは当たり前ではあるのですが、何回コンボリューション(畳み込み)を行うのかで大きく変わってくるもであります。CNNの言語処理のタスクではあまり多くの研究がなされていないのか、あまりパラメータチューニングを主とした先行研究がありません。
アウトプットの低次元なベクトルの一つの要素に対して一つのクラスに属するという意味を持たせることが多いようです。
[1, 0] -> [positive-class, negative-class]
この場合、数字の1がフラグで、positive-classに属する、negative-classに属さないと解釈できます。
予想(prediction)の段階で内部スコアは [0.9764311, 0.12234]みたいな値になるはずです、多分。
学習の段階ではとにかく予想と真実が外れまくることが多々あります。その時、以下の組み込み関数で誤差を計算してAdamと呼ばれる最適化アルゴリズムで誤差を最小化するようにニューラルネットワークのW(weight)を更新します。
softmax_cross_entropy_with_logits(“予想値”, “正解”)
■ 学習するデータ
Character Levelで学習する場合、単語の並びと発生が重要視されるので分かち書き等は必要とされません。対して、Word Levelでは単語を一つのインプットと考えるため、分かち書きにしてプログラム内部でリスト形式ないしBag-of-wordに変換する必要があります。分かち書きをリストやテンソルにマッピングするほうが単語の位置情報が欠損しないので良いとされていますが、実際のとろこF値やスコアを観測するとインパクトのある大差は無いようにも思われます。
学習に対してPositiveとNegativeに分類する場合、必要な学習データと正解データの組み合わせは以下のようになります。Char Levelの場合は通常分かち書きのようなスペースは必要とされません。
{学習データ: “私は その 映画 が 大好き です 。 とても 、 楽しかった”, 正解データ: [1, 0] } @ポジティブ
{学習データ: “シン ・ ゴジラ を 見た 。 人 が 残酷 に 死ん で 気分 が 悪い”, 正解データ: [0, 1] } @ネガティブ
もっと、多値、多クラスに分類したい場合は正解データの次元を必要なだけ拡張すれば大丈夫です。多すぎるクラスはまた別の問題をはらみますがここでは議論しません。
[1, 0] -> [1, 0, 0]
■ Word Level CNNおおよそのモデル図
[1] 図.1
文字情報をベクトル化する必要があるのですが、最初にCNNによるセンチメント分析が提唱されたときはWord2Vecを用いておりました。TensorFlowなどの機械学習フレームワークでは、最初の文章マトリックスをinput_xを引数とした以下のnn.embedding_lookupビルトイン関数でベクトル化できます。
Wは機会学習の一連の作業を通じて学習していくパラメータですが、random_uniform関数で-1.0 ~ 1.0までの乱数で初期化します。
W = tf.Variable(
tf.random_uniform([vocab_size, embedding_size], -1.0, 1.0),
name="W")
self.embedded_chars = tf.nn.embedding_lookup(W, self.input_x)
■ コンボリューションとプーリング
for i, filter_size in enumerate(filter_sizes):
with tf.name_scope("conv-maxpool-%s" % filter_size):
# Convolution Layer
filter_shape = [filter_size, embedding_size, 1, num_filters]
W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1), name="W")
b = tf.Variable(tf.constant(0.1, shape=[num_filters]), name="b")
conv = tf.nn.conv2d(
self.embedded_chars_expanded,
W,
strides=[1, 1, 1, 1],
padding="VALID",
name="conv")
# Apply nonlinearity
h = tf.nn.relu(tf.nn.bias_add(conv, b), name="relu")
# Maxpooling over the outputs
pooled = tf.nn.max_pool(
h,
ksize=[1, sequence_length - filter_size + 1, 1, 1],
strides=[1, 1, 1, 1],
padding='VALID',
name="pool")
pooled_outputs.append(pooled)
コンボリューションではビルドイン関数を用いて、文章のembeddedしたリストを、フィルターのサイズにnn.conv2d関数を利用して畳み込みを行います。nn.relu非線形関数を通すことで出力結果を得ます。
今回のCNNによるセンチメント分析では、マックスプーリングする際に様々なサイズのネットワークに畳み込みを行ったデータを用いてマックスプーリングします。このような手法を取るのは必要であるからと紹介ブログにありました。
[2] 図.2
■ PoolingとFlatten
Pooling層は画像処理のCNNにおいては、次元圧縮と出力結果を固定長のベクトルに正規化する役割があります。
次元圧縮に関しては下の図を確認していただけるとわかりやすでしょう。
[3] 図.3
固定長のベクトルにするということはどういうことなのでしょうか。図.2でマックスプーリングの操作をよく見ていただけると確認できるかと思いますが、まずマックスプーリングをhの出力結果にかけると次元削減されたデータを得ることができます。この時、出力結果は nn.max_pool関数で取得する操作が行われます。更にリストとして保存され、それをFlatten(フラット化)することで、固定長にします。
このリストの大きさは、フィルタのサイズに依存するので、実質固定長であると捉えることができます。
# Combine all the pooled features
num_filters_total = num_filters * len(filter_sizes)
self.h_pool = tf.concat(3, pooled_outputs)
self.h_pool_flat = tf.reshape(self.h_pool, [-1, num_filters_total])
上記の例では、pooled_outputsのマックスプーリングのリストデータをconcat関数とreshape関数でflattenしている例です。
■ このモデルは単語の語順を意識してくれるか
この表題に関して、このCNNのモデルは実にユニークな発想で単語の語順の特徴を保存したまま評価するということを行っています。通常、語順など時系列データなどはRNNに任せるのですが、CNNに関してはフィルタのサイズを幾つか試行的に試すことでその語順を保存したまま、意味を学習しようという試みです。
最初、なぜフィルタサイズを3,4,5の3つに分けるのか意味がわからなかったのですが、語順を維持したままのインプットデータのベクトルとして畳み込みを行っていたので、この時点である程度畳み込みされたフィルタは、語順は意識されたモデルになっています。出力結果をマックスプーリングして、語順データをないものにしているように見えますが、出力結果自体が、語順を強く意識した値を持つベクトルになっており、ベクトルの値の中にパラメータとしてすでに入ってしまっていると考えるのが自然なようです。(この筆者は、かなりラフにとらえたもので、正しく日本語にできているか自信がありません[4])
■ このモデルのCNNは、とにかく学習完了までめっちゃ早い
実際に動かしてみて思ったのですが、accurarryが1になるまでの収束がとても早かったです。2014年のMac Book Airで5分ほどでしょうか。同様のTensor Flowの翻訳のSeq2Seqが数日かかることを考えると驚異的な速さであると感じます。
■ 性能がとても高い
フィルタが3,4,5などの変則的な値を用いているので、アドホック感が強いな、、、と思っていたのですが、元論文では非常に高いパフォーマンスを発揮していることがわかるかと思います。CNN-*のサフィックスは事前学習の様々な状態らしいです[5]。
実装もそんなに難しく無いし、収束も早いので、いろいろ試してみると良いかもしれません。
[4] 図. 4
■ リファレンス
[1] http://arxiv.org/pdf/1408.5882v2.pdf
[2] https://papers.nips.cc/paper/5782-character-level-convolutional-networks-for-text-classification.pdf
[3] http://qiita.com/inuscript/items/54daa5aedde599e2637c
[4] http://tkengo.github.io/blog/2016/03/11/understanding-convolutional-neural-networks-for-nlp/
[5] http://functionp.com/2016/04/16/cnn%E3%81%A7%E6%96%87%E3%81%AE%E5%88%86%E6%95%A3%E8%A1%A8%E7%8F%BE/