word2vecとLSTMを使って文章の忍術属性印象[火,水,木,金,土]を推定してみた

ゼミで新しいメンバーが入ってくる度に、なんでword2vecを使うのか、なんでLSTMで推定するといいのか、といった話を何度もしている気がしたので、応用例と共に文章として認めることにしました。
なので基本的に機械学習に疎い人向けにも書いてますが、敢えて少し踏み込んだ話もしています。

本記事を読むにあたって、ニューラルネットをなんとなくでも知っていれば、踏み込んだ話まで理解できるかもしれません。
また、使ったライブラリが昔に自分が勉強がてら作成したものなので、具体的なコードの扱いなどについてはこの記事で触れないことにします。

![Screen Shot 2016-06-22 at 14.01.10.png](https://qiita-image-store.s3.amazonaws.com/0/104478/95cf983a-989b-3aed-48ac-b13fcaeb2003.png "Screen Shot 2016-06-22 at 14.01.10.png")

先日[VRハッカソン](http://www.moguravr.com/japan-vr-hackathon/)で作成したゲームの一部の機能としてこの推定器を作ってました。
そのゲームでは、ユーザに任意のフレーズを忍術として発してもらい、そのフレーズの属性値を推定してそれに合わせた忍術を発生させて、襲い掛かってくる忍者達を一掃する、という内容のものです。
処理の流れとしては、音声認識->文章->属性推定->エフェクト発生となりますが、この投稿では、文章を入力したときにどのように5属性の印象値を推定するのかについて書きたいと思います。

最初に属性印象推定の大まかなイメージを掴んでいただいてから、word2vecやLSTMなどについてざっと説明し、最後にそれらを使ったらどういうものができるの？という一例としてハッカソンで作ったものについて説明してみます。

## どういうことができるようになるの？

次の例のように、任意のフレーズを無理やり忍術として解釈して[火,水,木,金,土]の確率を算出できるようになります。

「火遁豪火球の術」
　　-> [0.9999019, 4.380458E-6, 2.6455573E-6, 7.223793E-5, 1.882742E-5]
　　　　
「ふきのとう乱舞」
　　-> [6.445799E-8, 0.0028536594, 0.98062956, 3.2902983E-5, 0.01648381]
　　　　
「素敵な言葉」
　　-> [0.0010024917, 0.61122584, 0.013072551, 8.203876E-6, 0.3746909]　　　

「使える人」
　　-> [0.054361355, 0.010380201, 0.004228816, 0.39485654, 0.5361731]

「メイド喫茶で女の子といちゃいちゃ」
　　-> [0.014652798, 5.4791853E-6, 8.034433E-4, 0.9845352, 3.0879391E-6]


上の例だと「火遁豪火球の術」は火属性、「ふきのとう乱舞」は木属性、「素敵な言葉」は水属性、「使える人」は土属性、「メイド喫茶で女の子といちゃいちゃ」は金属性という結果がでました。
ちなみにどれもハッカソン中での実際の入力です（※とりわけ音声認識が上手く稼働したもの)

どのように属性印象を推定するのか話す前に、推定先の5属性について書きます。

## 五行思想
[五行思想(wikipedia)](https://ja.wikipedia.org/wiki/%E4%BA%94%E8%A1%8C%E6%80%9D%E6%83%B3)

万物は[火,水,木,金,土]の5種類の元素からなる、という考えです。
記述されてるそれぞれの属性の意味を参考にし、独断と偏見で雑に次のように設定しました。

火: 火っぽいもの、攻撃的なもの、エネルギーが大きそうなもの
水: 水っぽいもの、観念的なもの、スピリチュアルそうなもの
木: 木っぽいもの、風っぽいもの、成長などを表しそうなもの、土に比べて動的。
金: メタルっぽいもの、自然物よりは人工物を表しそうなもの。
土: 土っぽいもの、育成や保護を表しそうなもの、木に比べて静的。

綺麗に分類できるものでもありませんので、実際に判断を迷うようなものも多々あります。
例えば、"子犬"とかどこに分類されるんでしょうね。

実は忍術や魔法それ自体に詳しいわけではないので、ハッカソンが始まってからそれっぽいものを採用したというだけです。
他に面白いものを知っている方は教えていただければ幸いです。


## 属性印象推定の大まかなイメージ

フレーズとそれのもつ属性の印象、というペアをたくさん用意することで、未知のフレーズが入力された時に、既にあるフレーズ:属性印象ペアを参考にして、どんな属性印象になるのかを計算します。

例えば「火炎放射,1,0,0,0,0」という形式で、火炎放射は火属性だよ、と教えて上げることができます。
他に「ハイドロポンプ:0,1,0,0,0」として、ハイドロポンプは水属性だよ、というデータも加えてみます。
ここで、もし未知のデータとして「炎上」というワードが来たらどうなるでしょう？
もし「火炎」と「炎上」の単語類似度が「火炎」と「ハイドロ」の単語類似度よりも高ければ、「火炎放射」に紐付けられた属性印象値を強く参考にするので、
「炎上」-> [0.99,0.01,0,0,0]
というように推定することができるようになります。

## word2vec

### なんでword2vecを使う？

word2vecを使うことで、「火炎」、「ハイドロ」、「炎上」などの単語の距離を表現することができるようになります。
ヒトは何かしらの観点から「火」や「炎」や「爆発」などは互いに似ていて、「水」や、「シャワー」「お茶」なども互いに似ていると感じます。
しかし、計算機上において、単語はただのシンボルでしかないため、「火」と「炎」は似ている、という感覚が反映されません。

実際に、少し昔までは単語を辞書のインデックス番号で表現することが一般的でした。例えば、
辞書=["火","炎","水"]の場合、
"火"=[1,0,0]
"炎"=[0,1,0]
"水"=[0,0,1]
といった具合です(厳密にはハッシュが使われます)。
もちろん単語が近い、遠いという表現はありません。

これに対して、word2vecは、例えば次のように単語を表現します。
"火"=[0.72, 0.66, -0.1]
"炎"=[0,88, 0.54, -0.3]
"水"=[0.03, 0.91, -0.4]
見ていただけるだけで察していただけるとは思いますが、"火"が"水"よりも"炎"に似ている、ということが表現されています。

このように、単語を実数値のベクトルで表現する考え方を単語の分散表現やword embeddingと呼びます。
word2vecは、単語を上例のように表現する、一つの有名な手法(もしくは実装)を示します。

### どうやって単語の表現を獲得するの？

その単語が出現した時に、その単語の周囲の単語が似ていればそれらの単語は似ているだろう、という仮定にもとづいて処理されています。
例えば"火"と"炎"であれば、「火が燃えている」と「炎が燃えている」のように近い文脈で出現することが多くなります(「水が燃えている」は大抵存在しない)。
他にも"水"と"お茶"であれば「水を飲む」、「お茶を飲む」のように近い文脈で発生しやすい単語同士となっています。

日本語の場合だと形態素解析を行い、window-sizeというパラメータを設定し、そのパラメータの範囲にある単語を周囲の単語として取り上げます(word2vec実装の場合、厳密にはwindow幅はランダムに小さくなります)。

例) 「塵 も 積もれ ば 山 と なる」
window-size = 2で"山"の周辺の単語を取り上げる場合、["積もれ" "ば" "と" "なる"]
これで["山", ["積もれ" "ば" "と" "なる"]]という形式で"山"の周辺に出やすい言葉が得られました。
そして、このペアを3層のニューラルネットに当てはめることで学習を行います。

![Screen Shot 2016-06-23 at 10.22.17.png](https://qiita-image-store.s3.amazonaws.com/0/104478/b3161697-74cd-266c-1a5d-43f43fb2ee95.png "Screen Shot 2016-06-23 at 10.22.17.png")


入力に"山"をセットし、出力に["積もれ" "ば" "と" "なる"]をセットし、誤差逆伝播でニューラルネットモデルの学習を行います。
図の緑色の箇所がこの瞬間に発生する誤差逆伝播のパスになります。
この作業をすべての文章に存在する単語に対して行うことで、周囲の単語が似ている単語ほど中間層での出力が似てくるようになります。

ちなみにこれらはskip-gramと呼ばれる手法で、CBOWという方法もあります。
また、一般的に単語の数は10万~100万以上の単語数になるため、出力層の数もそれだけの数になり、計算がとても重たくなります。
そのために、計算を出来るだけ影響がでないように間引く学習の方法としてnegative samplingやhierachical softmaxなどの手法が使われています。
negative samplingは文脈に存在しない単語を一定の基準に沿っていくつか選択し、文脈に存在する逆の事例として学習の時に使われます。
hierarchical softmaxは、出力層をバイナリの木構造で表現することで、計算時間をlogに落としこむ手法です。

重要なパラメータはwindow-sizeと中間層のsizeになります。
window-sizeは単語の周囲の単語を拾う大きさのパラメータですが、これが小さいとトピックを全然学習せずに、単語の品詞を強く学習するようになり、大きすぎると、トピックの影響が多くなり、単語の品詞をあまり学習しなくなります。
中間層のsizeについてですが、これは表現するベクトルの大きさになります。
"火"=[0.72, 0.66, -0.1]の場合だとsize=3となっています。
このパラメータはデータの量や質の幅広さによって変わってきます。
一般的に大量のデータ、幅広い質のデータを処理する際にはsizeは大きい方がよいとされています。

ちなみに任意の単語の分散表現は、このニューラルネットモデルにその単語を入力した時の中間層の値となっています。
つまり、"火"を入力したら、[0.72, 0.66, -0.1]が中間層の出力として出てきます。

入力層-中間層の結合(word embedding)を得ることが目的なので、学習を今後も継続するのでなければ中間層-出力層の結合は捨ててしまって大丈夫です。
むしろword2 embeddingを使った上での学習を行う段階であれば、中間層-出力層も大量のメモリを使っているので、破棄する必要があるかもしれません。

このようにword2vecを使うことで単語の意味空間を形成し、任意の単語をその空間上のベクトルとして表現することが可能になりました。

次は、単語から5属性印象への対応をどのようにして構築するかの説明になります。

## LSTM

### なんでLSTMを使うのか？

属性印象推定の大まかなイメージの項で説明したように、未知のデータが来た時に、既存の教師データと照合することで、そのデータがどのような数値を持つのかを予測する事ができるようになります。
今回の場合だと、フレーズが入力された場合に、5属性印象のそれぞれの成分の強さを表します。
出力層ではsoftmaxを活性化関数とするころで、5属性印象を確率で表現しています(5属性の推定値が全部足して1になる)。

LSTMはRNN(Recurrent neural Network)と呼ばれる手法の一つです。
RNNは普通のニューラルネットとは異なり、データの時系列の意味を表現することができます。
LSTM(Long Short Term memory)は、時系列上の長期の関係が学習しにくいという、従来のRNNの弱点を反映して提案されたモデルです。
自然言語処理以外では音楽といった時系列が特徴的なデータに使われます。
自然言語処理でいう時系列は、この単語がきて、この単語が来て、この単語がきた時、という単語の出現位置が時間に対応します。

単語の時系列を捉える意義について説明します。
例えば、「シュレディンガーの猫」と「猫のシュレディンガー」は意味が異なりますし、「あいつの波動によって凍てつく」と「あいつの凍てつく波動」も単語だけなら似ていますが、属性印象が変わってきそうです(これらに対しては形態素解析の辞書をカスタマイズするという対策もありますが、ものすごく大変です)。
他にも、「縁の下のちから持ち」であれば土属性が高い印象がありますが、「持ち/の/ちから/下/の/縁/」のように順番を考慮しないと火属性が高い印象になります。
今回のフレーズの5属性印象であればこの問題はあまり顕在しないかもしれませんが、辞書をハッカソンで作るのは避けたかったですし、ハッカソン中で学習を行う時間がある程度確保できそうだったので、時系列の効果も取り込めるLSTMを使いました。

### LSTMユニットの構造と挙動

![Screen Shot 2016-06-22 at 11.35.15.png](https://qiita-image-store.s3.amazonaws.com/0/104478/b69a5506-0f0c-7f27-1717-fa3b53b92c87.png "Screen Shot 2016-06-22 at 11.35.15.png")

図をかいてみましたが、なんだかややこしい感じですね...！
LSTMはcellと呼ばれる情報を貯蓄するメモリと、ユニットへの入力の他にinput, forget, outputのそれぞれのgateが存在しており、任意のタイミングでの入力とその時のcellの値によってユニットの出力が変化します。
またcellの値は今までの入力の大きさとそれぞれのgateがどのように作動するかによって変動します。
各部毎にどのように動くのか見ていきます。

ユニット下部の(h)の箇所への集積がそのユニットへの入力になります。
ちなみに(h)はtanh(hyperbolic tangent)関数を表しています。
ここを通った段階で、ユニットへの入力が-1~1の値に変形されます。

左下にある(s)はinput-gateで、(s)はsigmoid関数を表しています。
sigmoid関数は入力を0~1に変換します。
ユニットへの入力がtanh関数で変形された後に、その値の大きさを0~1の大きさで掛けあわせる仕事をします。
sigmoid関数の結果が0に近い場合ほぼすべての入力を受け付けなくなり、1に近い場合ほどユニットの入力をそのまま通過させるのでgateという名前がついています(他のgateも謂れは同じです)。

次にcell、LSTMユニットの肝の部分です。
0の状態で初期化されますが、この場合はユニットの入力*input-gateを通り抜けてきた値がセットされます。
既に文脈を持った状態、0以外の場合であれば、ユニットの入力*input-gateにcell*forget-gateの値を足したものがここにセットされます。

forget-gateもinput-gateのようにgateの働きをしますが、今度はcellの状態に対して機能します。
簡単のため、input-gateが機能してユニットへの入力*input-gateが0だった場合、forget-gateが1に近ければcellの状態はそのままの状態で更新されますが、0に近ければcellの値が0になります。
文脈を忘れる、という機能をもつため、forget-gateという名前なんですね。

最後にoutput-gateですが、そのユニットの最終的な出力の強さを操作します。
tanhで-1~1に変換されたcellの値に対して、0~1を掛け合わせることでユニットの出力を調整します。

他にもドット線の接続がそれぞれのgateに向かって伸びていますが、これはpeepholeコネクションと呼ばれるものです。
これによって、それぞれのgateの開閉をcellの値によってもコントロールできる余地が生まれます。

適切な学習ができたであろう時のLSTMユニットにおけるそれぞれのgateの挙動のイメージですが、例えば「猫のシュレディンガー」と「シュレディンガーの猫」で説明してみます。

「猫のシュレディンガー」において"猫"であることに重きをおくモデルだった場合、"猫"が入力される際にinput-gateが開き、少なくともcellに情報が蓄えられます。
"の"が次に入力されますが、ここで少なくともforget-gateが開いたままでcellの情報は蓄えられたままになる必要があります。
"シュレディンガー"が最後に入力されますが、ここで"シュレディンガー"が重要ではない場合にはinput-gateを閉じることで、cellの値が影響しないようになります。
猫の名前がシュレディンガーであることに意味がある場合では、また別のLSTMユニットのinput-gateが開き"猫"と"シュレディンガー"の影響によって出力層に対して影響します。

「シュレディンガーの猫」自体にモデルの意味がある場合、最初に"シュレディンガー"が入力され、input-gateを開くことで"シュレディンガー"を取り込みます。
"の"が次に入力されますが、少なくともここではforget-gateは開いたままでcellをそのまま更新する必要があります。
"猫"が最後に入力されますが、ここで"シュレディンガー"の情報と"猫"の情報が組み合わされることで、出力層での活性が行われる流れになります。
output-gateに関して説明できていないですが、ここは従来のニューラルネットワークの活性化関数の使われ方と同じイメージで大丈夫なのではないでしょうか。

さて、ここまでで一つのLSTMユニットがどのように動作しているのかを見てました。
もっと大局的に見るとどうなっているんでしょう？

図では１つのLSTMユニットと下のレイヤーに2つのユニットの場合の関係を示しています。
RNNの一種ということで、下のレイヤーからの入力以外に自分の出力が自身とそれぞれのgateに入力されます。
ぐちゃっとするので書ききれなかったのですが、一般的にLSTMユニットは100以上の数は使われますので、このような構造のユニットが横に並ぶことになります。
加えて、同じレイヤーのLSTMユニットの出力も任意のLSTMユニットの入力, gateの入力に成り得ますので、例えばLSTMの数が100だった場合、ユニットとgateの接続の総数は100(ユニットに対して)*100(ユニットの出力)*4(ユニットの入力+3つのgate)で40000となりますし、しかもまだ下のレイヤーからの入力もあります！

さて、これでかなりの数の接続が存在することがわかりました。
学習の段階では、そのすべての接続の強さを学習することになるので、結構時間がかかります。

しかし、時系列を考慮することによって「ちから/持ち」と「縁/の/下/の/ちから/持ち」が異なる入力として考慮されます。
前者は"ちから"が入力になるときに各ユニットのcellは0ですが、後者は"ちから"の前に["縁","の","下","の"]の影響が存在しており、各ユニットのcellの値が設定され、出力が異なったものになります。


## 文章の忍術属性印象推定モデル

![Screen Shot 2016-06-22 at 13.58.15.png](https://qiita-image-store.s3.amazonaws.com/0/104478/7b6955f2-0999-d8c9-fa46-8129c335b11a.png "Screen Shot 2016-06-22 at 13.58.15.png")

図の左半分は全体の構成を簡単に表しています。
形態素解析については端折っていますが、文を形態素解析して、それぞれの形態素をword2vecで学習した分散表現に変換した後、順にLSTMレイヤーの入力とし、それぞれのLSTMの出力を出力層に渡します。
また既に触れたように、出力層の活性化関数はsoftmaxにしており、これによって5属性印象の確率を求めることができます。

![Screen Shot 2016-06-23 at 10.51.15.png](https://qiita-image-store.s3.amazonaws.com/0/104478/0e989a70-1351-cc40-0bca-8611d7539615.png "Screen Shot 2016-06-23 at 10.51.15.png")

真ん中の四角い箱はLSTMの中間層を表しています。
形態素はモデルに１つずつ入力していき、それぞれの段階で出力層での値を得ることができますが、今回は形態素を入力する途中の出力層の値には特に意味が無いので、上図のように全ての形態素を入力した段階での出力層の値を得ます。
従って、一番右の状態でのLSTMユニット群の出力が出力層に影響しますが、そのLSTMユニット群の出力は再帰的に今までの自らの出力、ひいては今までの入力に影響を受けていることがわかると思います。

さほど大きなメモリを積んでいない手元のMacBookProで学習させるためにword embeddingのsizeは200、ハッカソン中に学習が終わってくれないと困るのでLSTMのsizeを100にした中間層１つとしました。

学習に使うデータは自分を含めた2名で、563個ほど作れました。
文章としては技っぽいものと、技っぽくないものの2つを意識的に分けて集めました。

技っぽいものは今までやったゲームの記憶を頼りに検索エンジンと目で頑張りました。
ポケモンの技やドラクエの技なんかが含まれています(他にも色々あるけどわからない)。

技っぽくないものは、twitterを眺めて出てきた声に出されそうなフレーズ、ことわざ、料理の名前、動物の名前、クラシックやアニソンなどの楽曲名といったものをチョイスしました。

技っぽい単語をword2vec上の辞書に登録されていないと、LSTMに入力するときにただの未知語としてしか扱えなくなってしまうので、wikipedia内で検討をつけて文章をスクレイピングしました。
技っぽいもの以外にも、忍者に関連しそうな単語はこの段階でかき集めました("水蜘蛛"や"NARUTO-ナルト-"など)。

word2vecの学習にはtwitterの投稿(数GB)と、wikipediaから集めた技っぽいもの、忍者っぽいものの文章を使いました。
仮眠とる前に仕掛けて起きたらできてたので、多分2,3時間ほどでできました。

LSTMの学習には、563個の文章に対して5属性印象の観点から独断と偏見でラベルをつけたデータを使いました。
SGDで学習率は0.025で学習させ、確か5,6時間ほどで学習が落ち着いてきたと記憶しています(CPUで頑張って計算させました)。
ハッカソン終了の4時間前に属性印象推定モデル構築は終わったので、とても安心した記憶があります。

出来上がったモデルの評価についてですが、少しでもデータを増やしたい状態だったので、学習用と評価用にデータを分けていません。
研究などでしたら、予めちゃんと分けておきましょう。

と、前置きをした上で感覚的に説明させていただくと、4割ほどは「そうだな！」という感じでした。
また、残りの3割ほどは、「んーまぁ納得できる」という感じで3割は「おや？」という感じです。
例えば下のが「おや？」の例です。

「埼玉県」
　　-> [0.0062928256, 0.96879363, 0.0060950983, 0.018352237, 4.661977E-4]

「ハンドソープ」
　　-> [0.19788364, 0.021860108, 0.55465204, 0.090227336, 0.1353769]

「埼玉県」に海のイメージはないので観念的な何かと思われたんでしょうか？
「ハンドソープ」は木の匂いでもするのか、もしくは飲んだら栄養たっぷりなのかもしれません。

他にも「子犬」で火属性の忍術が出てきますが、これはそもそもどこに分類されるのかが厳密にできなさそうですね。
無属性/その他のような属性を用意すればこの違和感は吸収できるのかもしれませんが、ほとんど無属性になると退屈になってしまいそうです。

一方で、コテコテの技名をいうと大体それっぽい属性印象を認識してくれますし、分類基準が雑な今回の課題でここまで精度が出てくれましたし、時間をかけて学習データを作った甲斐はあったかなと思っています。

word2vecを使わないで、単語をそのままLSTMに入れたのでは、563個のデータなんて圧倒的に少ないので、word2vecの恩恵はかなり感じました。
(単語を直にLSTMに入力すると、563データに含まれる形態素でしか推定できないです)

またLSTM(RNN)の恩恵は他のモデルと比較してないのでわかりませんが、「すごい」だけだと土属性、「手裏剣」だと金属製なのですが、「すごい手裏剣」だと火属性になるので、時系列の効果が考慮されている場合があるのだなと思っています。
もしかしたら「すごい手裏剣」は必殺技名みたいなもの、というようにモデルに学習されているのかもしれません。

## 今後

どうせだしAPIを公開しようと思っているのですが、embeddingだけで1.5GB近くあるのでどこにどうやって用意しようか思案中です。。。

ちなみに今回作成したVR含めたコンテンツでは、確率が最大の属性でのエフェクトのみを出していましたが、火: 0.7, 土: 0.3のようにブレンドした状態でエフェクトが出せたらもっとかっこ良くなるかなーと思っております。
もちろん忍術の分類を変更する可能性もありますが。

ゲームの開発は継続しようか、と仲間内で決まったので、今後機会があれば是非HMDを被ってゲームを体験してみてください。