旧型機(word2vec)を魔改造して新型機(BERT)に一泡吹かせたい件/advent callender 2021

本記事は[ABEJAアドベントカレンダー2021](https://qiita.com/advent-calendar/2021/abeja)の18日目の記事です。

# イントロダクション
お疲れ様です、ABEJAの高橋（DS＆ENGの方）です。
皆さまは、この年の瀬をお忙しくお過ごしのことかと思います。
今回は、少し肩の力を抜いてリラックスして読める内容でお届けしたいと思います。
ジャンルはニューラル自然言語処理モデルです。

さて、みなさんの中でガンダムをはじめロボットもの作品が好きな方なら、こんなシチュエーションにハートが熱くなる方がいらっしゃるのではないでしょうか？

```
旧型機の調整や運用を創意工夫して、
新型機をギャフンと言わせる（or 撃破する）シチュエーション
```

~~近年、とある公式作品中で魔改造したザクが大暴れして、ガンダムを撃破したりガンダムに化けたりしているらしい~~

そこで今回は自然言語処理モデルの世界で、往年の実績モデル `word2vec` （旧型機）に創意工夫を凝らし、華々しくデビューを飾って自然言語処理の世界を一変させた新型モデル・ `BERT` に一泡吹かせることができるか？を実験したいと思います。


# word2vec
word2vec と言えば、自然言語テキストをベクトル（数百次元など）に変換することで、自然言語処理の分野にブレイクスルーをもたらした名モデルということは皆さまもご存じかと思います。

具体的には、それぞれの言葉の意味をベクトルに変換し、その四則演算やベクトル間の距離の計算を利用して様々なタスクに役立てることを可能にしたのです。

例えば、言葉のベクトルを使って、次のような関係を求めることができます。


$ V_{王} + V_{女性} = V_{女王} $

$ V_{パリ} - V_{フランス} = V_{東京} - V_{日本}$

word2vec については、有益なエントリがたくさんオンラインで公開されています。
詳しく知りたい方は、ぜひ検索してみてください。
（参考までにエントリ末にリンクの一覧を添付しておきます）


# BERT
このエントリを開いている方は、既にBERTのことをよくご存じのことが多いかもしれません。
BERTは、**Bidirectional Encoder Representations from Transformers** の略であり、日本語に翻訳すると `Transformer(ネットワーク)がもたらす双方向のエンコード表現` となります。
2018年10月に Google の `Jacob Devlin`氏らが発表し、これまでの自然言語処理分野におけるタスクの最高スコアを更新しました。
文脈を考慮した表現を可能としているのが特徴で、自然言語処理の世界にブレイクスルーをもたらしました。

そして、あっという間に派生型のモデルや論文が数多く世に送り出され、豊かな生態系を築き上げるに至っています。（これは皆さまのご記憶に比較的、新しいことでしょう）

word2vec, BERTは共に、自然言語処理の世界にブレイクスルーをもたらし、一時代を築き上げた名モデルであり、まさに`往年の名旧型機` 対 `彗星のごとく現れた新型機` という好対照を成す組み合わせかと思います。



# ポリシー：旧型機でどう立ち向かうか？
旧型機で新型機に一泡吹かせるというシチュエーションを考えたとき、大事なことは何でしょうか？
私は **`縛り（制約）とロマン`** と思います。

有り余る資源や最新テクノロジーをつぎ込むのでは、そこには `ロマン` は無いかもしれません。というか、そのような真似をしたらもはや旧型機とは呼べないシロモノになってしまうでしょう。
例えると、「`とんでもない怪物を倒すために、人間をやめて相手以上の怪物になる`」ようなアプローチであり、そういう悲壮感溢れる展開も心を打つのですが、今回のテーマとはポリシー的に相克しそうなので避けたいところです。

（Hellsing や BERSERK という作品をご存じであれば、人間という弱い存在でありながら、知恵や勇気を振り絞り、超常的な怪物や神に等しい存在に立ち向かう構図に熱い共感を覚えるかもしれません）

そこで今回は、次のような方針で取り組みたいと思います。


## 乏しいリソースをやりくりして立ち向かう
時代はクラウド、その気になれば（＝お金をかければ）相当量の計算資源を確保することが容易です。
しかし、それは本テーマのポリシーに反するので、今回は乏しいリソースをやりくりすべく、ローカルマシンでモデルの構築・改良に挑戦します。

様々な作業用アプリケーションが並列起動する中、リソースを独占することは難しく、油断すればあっという間にメモリ不足でプロセスが中断してしまうでしょう。

メモリをいかに節約して大規模なデータを扱うのか創意工夫が試されます。


## 戦場を絞り込む、狙い定める
正面からぶつかって力押しで勝とうというのは、やはり本テーマのポリシーに反します。
あらゆる場面で新型機に勝つというのは、汎用性能で相手を上回るということであり、それはもはやは旧型機とは言えないでしょう。

限られた手札、リソースで新型機に一泡吹かせるには、地の利などの環境や相手の置かれた状況を考慮した戦場選びが大事です。
そこで今回は、トピックの分類（＝カテゴリの分類）というタスクに絞って、BERTに勝負を挑むことにします。

文の構造や文法を考慮したタスクとなると、BERTとの間の性能差を埋めることは難しいですが、それらを考慮せずに遂行可能な分類タスクであれば、限定的ながら勝算が見えるかもしれません。


# 作戦：word2vec のチューニング
今回の word2vec のチューニングのポイントは以下です。

- 学習データの情報密度を上げる
- 省メモリで学習データを読み込めるようにする

具体的に見ていきましょう。


## 学習データの情報密度を上げる
word2vec では、`CBOW` と `Skip-gram` の二つのアルゴリズムを使用可能ですが、そのいずれも指定したウィンドウ(=学習処理に反映される範囲の広さ）の中に出現する単語の共起を学習することでモデルを構築します。

入力された文章を形態素（＝トークン）に分割して入力するのですが、形態素の列を見ると助詞や助動詞など文法構造をもたらす単語が多く目につきます。
それらは非常に出現頻度が高く、一般的な単語であり、固有の特徴量を持っているとは言い難い単語です。

それらがウィンドウの中の多くの場所を占めているのです。
例文を形態素に分けてみましょう。

```text
西フランク王国/が/断絶/する/と/、/987年/に/パリ/伯/ユーグ・カペー/が/
フランス/王/に/推挙/ され/た/こと/から/、/パリ/は/フランス王国/の/首都/と/なっ/た/。
```

仮にウィンドウのサイズを `5` とすると、ウィンドウの中に入ってくる形態素列は次のようになります。

```text
西フランク王国
が
断絶
する
と
```

文の特徴に関与すると思われる形態素は `西フランク王国` と `断絶` であり、残り３つの形態素は文法構造を与えるための助詞や一般動詞です。ウィンドウの半分以上を、情報量をほとんど持たない（＝特徴をほとんど持たない）単語が占めていることが伺えます。

そこで、文法構造を与えるための助詞や一般動詞などを取り除くフィルタを実装し、ウィンドウ中の情報量を高めたいと思います。
先ほどの例文の場合、ウィンドウ内に以下のような形態素列が入力されるようにしたいと思います。

```
西フランク王国
断絶
987年
パリ
伯
```

先ほどのウィンドウと比べて、ウィンドウ内の情報量が高まっていることが分かると思います。
このようにして、モデルに食べさせるデータの情報量を高める前処理をすることで、少しでもBERTに肉薄しようというわけです。

BERTの場合、文の構造を考慮した学習が必要なので、このようなトリミングはご法度でしょう。
トピックの分類タスクに限れば、word2vec に有利な状況が一つ出来上がるかもしれません。
（＝戦場を絞り込むことで、有利な戦術が使える状況を確保する）


## 省メモリでデータを読み込めるようにする
限られた資源、貧弱な武装でどうやって新型機に勝つか？
こうした制約・縛りがあると、なおのこと創造性が喚起されるように思うのうのは私だけでしょうか？

ということで今回は、ゴツいクラウド環境ではなく、ローカル環境でいかにメモリを圧迫せずにデータを読み込めるかを考えたいと思います。

### メモリの問題が起きる箇所
word2vec のモデルを学習する際、学習データを格納した引数(以下のコード例の中では `sentences`)を指定するのですが、この学習データをメモリ上に展開する時に **メモリ不足の問題** が発生する危険があります（特にローカル環境では、ブラウザであったりIDEなど他の大きなアプリケーションが動いているため）

```python
# word2vec モデルの学習コードの例
from gensim.models import word2vec

# sentences に学習データが入っている
word2vec_model = word2vec.Word2Vec(
        sentences,
        sg=1,
        size=100,
        window=5
    )
```

小規模な学習データならメモリ不足の問題とは無関係なのですが、**wikipediaの全文を放り込む**（:scream:）となると話は別となります！

大量のタブを開いたブラウザが動いている、機能豊富なIDEが起動しているなど、既にメモリが大量に占有されているシチュエーションは容易に起こり得ます。

そんなシチュエーションで、追加で**数ＧＢのコーパス**をメモリ上に配列展開しようものなら、何が起きるか分かったものではありません。（モデルの学習処理が始まる前に、エラーを吐いて強制終了する恐れが強い）

この問題にどうやって対処するのが適切なプラクティスなのでしょうか？？

### 省メモリ・逐次読み込みを実現するモジュール
[gensim のソースコード](https://github.com/RaRe-Technologies/gensim/blob/develop/gensim/models/word2vec.py)や[公式サイト](https://radimrehurek.com/gensim/models/word2vec.html)を拝見すると、ちゃんと解決策が用意されていることがわかります。

`LineSentence` クラスと `PathLineSentences` クラスが、その解決策をもたらします。

#### `LineSentence` クラス
- 学習データファイルのパスを指定すると、逐次読み込みをしてくれます。
- `LineSentence` オブジェクトを word2vec モデルの引数に指定することで、学習データを逐次読み込みしながらモデルの学習処理を進めてくれるようになります。

```python
# LineSentenceを使った学習処理のコード例

sentences = LineSentence('./some_path/training_data.txt')

# ./some_path/training_data.txt から逐次読み込みをして、モデル学習
word2vec_model = word2vec.Word2Vec(
        sentences,
        sg=1,
        size=100,
        window=5
    )
```

学習データファイルは、形態素どうしが半角スペースで区切られたフォーマット(`LineSentence フォーマット`)で書き出しておく必要があります。

```
# LineSentence フォーマットの例
西フランク王国 が 断絶 する と 、 987年 に パリ 伯 ユーグ・カペー が フランス王 に 推挙 され た こと から 、 パリ は フランス王国 の 首都 と なっ た 。 
王権 の 強化 に したがっ て 首都 も 発達 し 、 王宮 として シテ宮 が 建築 され た 。
```

学習データが一つのファイルに集約されている場合、`LineSentence` クラスが活躍してくれそうですね。
ただ、学習データが複数のファイルに分かれている場合はどうでしょうか？


#### PathLineSentences クラス
学習データが複数のファイルに分割されている場合、ひと手間かけて複数のファイルを統合するスクリプトを書く手もあります。
しかし、とても便利なクラス `PathLineSentences` を使えば、その手間をかけずに済ませることができます。

`PathLineSentences` クラスを使うと、指定したディレクトリの直下にあるファイルを読み込むことができます。（サブディレクトリ内の走査まではしてくれないので、そこだけ注意）

```python
# PathLineSentencesを使った学習処理のコード例

sentences = PathLineSentences('./some_path/training_data_folder')

# ./some_path/training_data_folder 直下にあるファイルを次々と読み込んでモデル学習
word2vec_model = word2vec.Word2Vec(
        sentences,
        sg=1,
        size=100,
        window=5
    )
```


## 学習処理の工夫
今回、wikipediaコーパスを使った学習をするのですが、これが非常に重い処理で大変時間がかかります。
何かの拍子にエラーが発生して、学習処理が中断したら血涙モノの精神ダメージが不可避です。
そんな時、各epoch（学習サイクル）ごとに途中の学習経過を保存出来たらいいですね。

gensim の`Word2Vec` クラスは、こういった学習処理の工夫を実現するため `コールバック` という仕組みを提供しています。

- [gensim公式サイトの文書](https://radimrehurek.com/gensim/models/callbacks.html)

具体的には、`CallbackAny2Vec` クラスを継承したコールバック処理用のクラスを実装します。
学習処理時に発生する各イベントに対応したコールバックAPIの中身を実装していきます。

- `on_epoch_begin` : 各epoch開始時に呼び出されます。
- `on_epoch_end` : 各epoch終了時に呼び出されます。
- `on_train_begin` : 学習処理開始時に呼び出されます。
- `on_train_end` : 学習処理完了時に呼び出されます。


今回は、自前のコールバッククラスを実装し、各epoch終了時にモデルの途中経過を保存する機能を実装しました。
これで停電に見舞われても、最後の学習処理のところから学習を再開することが可能になります。


## いざ！モデル構築
ということで、word2vec モデルの学習処理を走らせます。  
限られたリソースの中でコンパクトなモデルを作りたいと思います。
（リッチなパラメータを与え、大きなモデルを作ろうとするとローカルマシンではリソース不足になりがちのため・・・）

目的は分類タスク用のモデルを作ることにあります。
あまりレアな言葉には対応せず、最小出現頻度で足切りを行い、総単語種類数をトリミングして、ネットワークをスリムにしたいと思います。
そうすることで、学習速度も上がるでしょう。

```python
    # 学習処理
    model = Word2Vec(sentences=training_data_reader,
                     vector_size=100, 
                     window=10,  
                     sg=1,
                     min_count=20, 
                     hs=0,
                     negative=5,
                     callbacks=[training_callback],
                     epochs=20
                     )
```

学習処理のコードを実行した際の結果はこちらです。
軽量にしたつもりでしたが、それでも結構時間がかかりましたね（12時間ほど）

```bash
$ python src/sandbox/train_model.py -i ./training_data/wikipedia -o ./models/d100_window10/ja-wikipedia
{'output_model_path': './models/d100_window10/ja-wikipedia', 'input_training_data': './training_data/wikipedia'}
2021-12-16 16:58:58.833326 training start
2021-12-16 16:58:58.833391 [Training] epoch = 0 start
2021-12-16 17:33:18.536595 [Training] epoch = 0 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch0.model
2021-12-16 17:33:19.665817 [Training] epoch = 1 start
2021-12-16 18:06:36.247259 [Training] epoch = 1 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch1.model
2021-12-16 18:06:37.116509 [Training] epoch = 2 start
2021-12-16 18:43:02.331792 [Training] epoch = 2 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch2.model
2021-12-16 18:43:03.114047 [Training] epoch = 3 start
2021-12-16 19:17:19.403493 [Training] epoch = 3 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch3.model
2021-12-16 19:17:19.946114 [Training] epoch = 4 start
2021-12-16 19:49:41.979083 [Training] epoch = 4 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch4.model
2021-12-16 19:49:42.549742 [Training] epoch = 5 start
2021-12-16 20:23:28.671037 [Training] epoch = 5 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch5.model
2021-12-16 20:23:29.268717 [Training] epoch = 6 start
2021-12-16 20:58:24.685932 [Training] epoch = 6 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch6.model
2021-12-16 20:58:26.117920 [Training] epoch = 7 start
2021-12-16 21:36:36.736938 [Training] epoch = 7 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch7.model
2021-12-16 21:36:38.686473 [Training] epoch = 8 start
2021-12-16 22:15:10.114415 [Training] epoch = 8 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch8.model
2021-12-16 22:15:15.179403 [Training] epoch = 9 start
2021-12-16 22:53:28.125719 [Training] epoch = 9 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch9.model
2021-12-16 22:53:32.762171 [Training] epoch = 10 start
2021-12-16 23:31:46.759506 [Training] epoch = 10 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch10.model
2021-12-16 23:31:52.937476 [Training] epoch = 11 start
2021-12-17 00:10:19.310852 [Training] epoch = 11 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch11.model
2021-12-17 00:10:24.748994 [Training] epoch = 12 start
2021-12-17 00:44:04.873795 [Training] epoch = 12 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch12.model
2021-12-17 00:44:09.334819 [Training] epoch = 13 start
2021-12-17 01:17:42.771947 [Training] epoch = 13 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch13.model
2021-12-17 01:17:47.992210 [Training] epoch = 14 start
2021-12-17 01:55:37.999417 [Training] epoch = 14 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch14.model
2021-12-17 01:55:43.662240 [Training] epoch = 15 start
2021-12-17 02:34:32.929495 [Training] epoch = 15 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch15.model
2021-12-17 02:34:38.754219 [Training] epoch = 16 start
2021-12-17 03:13:31.315595 [Training] epoch = 16 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch16.model
2021-12-17 03:13:39.695883 [Training] epoch = 17 start
2021-12-17 03:48:43.894636 [Training] epoch = 17 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch17.model
2021-12-17 03:48:48.573325 [Training] epoch = 18 start
2021-12-17 04:22:50.385430 [Training] epoch = 18 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch18.model
2021-12-17 04:22:54.550231 [Training] epoch = 19 start
2021-12-17 04:55:43.908206 [Training] epoch = 19 end
[Training] saved ./models/d100_window10/ja-wikipedia_epoch19.model
2021-12-17 04:55:52.270325 [Training] saved ./models/d100_window10/ja-wikipedia.model
```


### 簡易検証

得られた埋め込みベクトル（＝意味ベクトル）を簡単に確認します。
意味が近い言葉の候補や、意味を合成した際の近しい言葉の候補も、悪くは無さそうです。

```
# 機械学習
  [('Neural', 0.8911773562431335),
   ('データマイニング', 0.8751947283744812),
   ('深層学習', 0.8720446825027466),
   ('自然言語処理', 0.8282868266105652),
   ('人工知能', 0.8240522146224976),
   ('learning', 0.8152662515640259),
   ('コンピュータビジョン', 0.8145481944084167),
   ('ビッグデータ', 0.8053210377693176),
   ('BERT', 0.7915328741073608),
   ('分散コンピューティング', 0.7902379035949707)]

# 東京 - 日本 + フランス
  [('パリ', 0.7976235747337341),
   ('パリ郊外', 0.7250342965126038),
   ('ブリュッセル', 0.7231894135475159),
   ('レンヌ', 0.7184333801269531),
   ('モンペリエ', 0.717383623123169),
   ('トゥールーズ', 0.7163522839546204),
   ('リヨン', 0.7158567905426025),
   ('ストラスブール', 0.7084429264068604),
   ('マルセイユ', 0.6942373514175415),
   ('ルーアン', 0.6869782209396362)]
 
# 東京 - 日本 + 米国
  [('ワシントンDC', 0.6836080551147461),
   ('ニューヨーク', 0.6714104413986206),
   ('ロスアンゼルス', 0.6656925082206726),
   ('ロサンゼルス', 0.6654309630393982),
   ('シカゴ', 0.644919216632843),
   ('ロサンジェルス', 0.639432430267334),
   ('シアトル', 0.6307895183563232),
   ('ワシントンD.C.', 0.6285443902015686),
   ('サンフランシスコ', 0.6192886829376221),
   ('ダラス', 0.6138312220573425)]

# 庶民 + お金
  [('金持ち', 0.7925633788108826),
   ('貧乏', 0.7875364422798157),
   ('商売', 0.7605671882629395),
   ('小遣い', 0.7553476095199585),
   ('買える', 0.7506135106086731),
   ('売る', 0.7455703020095825),
   ('貧乏人', 0.7388562560081482),
   ('立身出世', 0.7225774526596069),
   ('貧しい', 0.7139792442321777),
   ('大金持ち', 0.7105273604393005)]
 
# スポーツ + ボール + バット
  [('ミット', 0.7923768758773804),
   ('ランニング', 0.779607892036438),
   ('キャッチャー', 0.7782055139541626),
   ('グリップエンド', 0.7619487643241882),
   ('野球', 0.7541038990020752),
   ('スライディング', 0.752403974533081),
   ('スイング', 0.7481467127799988),
   ('打球', 0.7468277812004089),
   ('相手チーム', 0.7409756779670715),
   ('ホームベース', 0.7388991117477417)]
 
# YouTuber
  [('Youtuber', 0.9112383723258972),
   ('TikToker', 0.8226187229156494),
   ('タレント', 0.8141645789146423),
   ('ゲーム実況', 0.8028210997581482),
   ('お笑い芸人', 0.7896425724029541),
   ('UUUM', 0.7693575024604797),
   ('VTuber', 0.7684944272041321),
   ('バーチャルYouTuber', 0.760256290435791),
   ('インフルエンサー', 0.7564404010772705),
   ('動画投稿', 0.7522768974304199)]
 
# 筋トレ
  [('筋力トレーニング', 0.8195765614509583),
   ('エクササイズ', 0.7853349447250366),
   ('筋肉トレーニング', 0.7505686283111572),
   ('ダイエット', 0.7471257448196411),
   ('腹筋運動', 0.744681715965271),
   ('特技', 0.7334994077682495),
   ('腕立て伏せ', 0.7310249209403992),
   ('趣味', 0.7249144315719604),
   ('ウェイトトレーニング', 0.7243321537971497),
   ('映画鑑賞', 0.7214395403862)]
 
```

# いざ対決！ word2vec改 vs BERT（round 1)
モデルが出来たので、いよいよ新旧名モデル対決の舞台を整えたいと思います。

- 使用するコーパスは、みんな大好きライブドアコーパス([Link](https://www.rondhuit.com/download/ldcc-20140209.tar.gz))
- 記事のカテゴリを予測し、その精度を競う。
- BERTは、huggingface の BERT(東北大学乾研究室謹製モデル）を使用
    - BERT側の分類モデルの実装については、[こちらのエントリ](https://qiita.com/nekoumei/items/7b911c61324f16c43e7e)が参考になります。(特にgithubの方）

 
打倒すべきBERT側の性能はこちらになります（抜粋）

```
Accuracy: 0.9566 
F1-Score: 0.952272
```

~~つ、強い・・・まるで勝てる気がしない~~

対するこちらは、word2vec改 モデルでライブドアコーパスをベクトル化し、SVMで分類を試みます。
その結果は・・・

```
  Accuracy:  0.8816086760054225
                precision    recall  f1-score   support

             0       0.84      0.89      0.86       253
             1       0.87      0.96      0.91       248
             2       0.83      0.69      0.76       143
             3       0.95      0.94      0.95       260
             4       0.86      0.90      0.88       219
             5       0.88      0.85      0.86       251
             6       0.96      0.95      0.95       278
             7       0.86      0.87      0.87       278
             8       0.85      0.80      0.83       283

      accuracy                           0.88      2213
     macro avg       0.88      0.87      0.87      2213
  weighted avg       0.88      0.88      0.88      2213
```

およそ **７ポイント** の圧倒的な差を付けられてKO!!

第一ラウンドは、BERTの圧倒的性能の前にボコボコにされてしまいました・・・
~~そもそもこのテーマ自体が無理ゲーだったか・・・~~

しかし、物語はここで終わりではありません。

モデル本体に加え、周辺装備（前処理や後処理）も合わせて改良を施し、捲土重来・逆襲の機会をうかがうとしましょう！


# 第二ラウンドへ備えて
第一ラウンドでは、圧倒的な性能差を見せつけられてしまいました。
一旦、分析と考察を重ね、次ラウンドへ向けた整備・調整に取り組みたいと思います。

## 考察と分析

- Wikipediaから学習した word2vec モデルを使うことで、（恐らく汎用的に）テキストから意味ベクトルの形で情報を引き出すことはできているだろう。
- しかしその一方で、意味ベクトルに変換する過程で抽象化・汎用化するがゆえの**情報の欠落**もあるだろう。  
  - （利便性、汎用性とのトレードオフ）
- 低頻度だが、決定的な情報量を持つキーワードや文字列があるかもしれない。
  - それはデータセット固有の特徴であり、汎用的に使えるものではないかもしれないが
- Wikipedia から学習した word2vec には、データセット固有のコンテキスト情報が欠けているのだろう（それはやむを得ない）
  
- そうなると、データセット固有の情報、ハイ・コンテキストな情報をどのように抽出するかが重要になるのではないか？
- **ライブドアコーパスから word2vec モデルを作ってみて、特徴量に加えてみてはどうか？**

ということで、ツラツラと思考したところで、**ライブドアコーパス版のword2vecモデル**を作ってみようと思います。

二つの word2vec モデルがコラボレーションして（旧型機のエンジンのニコイチ化）、新型機(BERT)を迎え撃とうと思います。

**名付けてツイン・ドライブ作戦！**

## ライブドアコーパス版のword2vecモデル構築
ライブドアコーパスとWikipediaコーパスを見比べたとき、記事の書き方の違いがハッキリしているので、学習データの作り方も変えてみます。

ライブドアコーパスでは、箇条書きのような短い文章や文節が並んでいる箇所が多く見られます。
複数行の短文や文節間における単語共起が手掛かりになる可能性が考えられます。
そこで、今回は記事まるごとに一つの入力としてまとめてみようと思います。
（Wikipedia版では、文単位で入力を分割）

ということで、学習用コードを組んで早速実行します。

```bash
$ python src/sandbox/train_model_livedoor.py -i ./corpus/livedoor/text -o ./models/livedoor/ja-livedoor
  {'output_model_path': './models/livedoor/ja-livedoor', 'input_training_data': './corpus/livedoor/text'}
  7376it [00:23, 317.14it/s]
  2021-12-19 15:21:42.922474 training start
  2021-12-19 15:21:42.922531 [Training] epoch = 0 start
  2021-12-19 15:22:19.552294 [Training] epoch = 0 end
  [Training] saved ./models/livedoor/ja-livedoor_epoch0.model
  2021-12-19 15:22:19.571568 [Training] epoch = 1 start
  2021-12-19 15:22:56.067837 [Training] epoch = 1 end
  [Training] saved ./models/livedoor/ja-livedoor_epoch1.model
  2021-12-19 15:22:56.080784 [Training] epoch = 2 start
  2021-12-19 15:23:35.104295 [Training] epoch = 2 end
  [Training] saved ./models/livedoor/ja-livedoor_epoch2.model

    （・・・中略・・・）
    
  [Training] saved ./models/livedoor/ja-livedoor_epoch47.model
  2021-12-19 15:54:39.664579 [Training] epoch = 48 start
  2021-12-19 15:55:20.091559 [Training] epoch = 48 end
  [Training] saved ./models/livedoor/ja-livedoor_epoch48.model
  2021-12-19 15:55:20.098453 [Training] epoch = 49 start
  2021-12-19 15:56:00.688529 [Training] epoch = 49 end
  [Training] saved ./models/livedoor/ja-livedoor_epoch49.model
  2021-12-19 15:56:00.712410 [Training] saved ./models/livedoor/ja-livedoor.model
  ```
  
### 動作確認
出来上がったライブドアコーパス版の word2vec モデルの動作を確認してみましょう。

```bash  
$ python src/sandbox/verify_model_livedoor.py -m ./models/livedoor/ja-livedoor
{'model_path': './models/livedoor/ja-livedoor.model'}
  
# 合コン
  [('飲み会', 0.6194847226142883),
   ('盛り上がる', 0.5741316676139832),
   ('婚活', 0.5482997894287109),
   ('男性', 0.5401411056518555),
   ('水谷', 0.5365508198738098),
   ('肉食女子', 0.5236541032791138),
   ('誘う', 0.5194903612136841),
   ('熊谷', 0.5189611315727234),
   ('なでしこ', 0.5155131220817566),
   ('知り合う', 0.5095832943916321)]

# 映画
  [('観る', 0.7804310321807861),
   ('作品', 0.7793624401092529),
   ('本作', 0.7492493391036987),
   ('全国ロードショー', 0.7175984978675842),
   ('主演', 0.7151556611061096),
   ('ENTER', 0.7098091840744019),
   ('全国公開', 0.7064754366874695),
   ('公開', 0.6930786967277527),
   ('作', 0.6877883672714233),
   ('MOVIE', 0.6816978454589844)]

# 男性
  [('女性', 0.8674346804618835),
   ('人', 0.6900421380996704),
   ('男', 0.6876373887062073),
   ('異性', 0.6854085326194763),
   ('彼氏', 0.682496964931488),
   ('既婚', 0.6812127828598022),
   ('付き合う', 0.6752278208732605),
   ('歳', 0.6696258783340454),
   ('年上', 0.6679908633232117),
   ('女', 0.6511443257331848)]
 
# お腹
  [('食べる', 0.6185864806175232),
   ('張り', 0.5708512663841248),
   ('授乳', 0.5589483976364136),
   ('太る', 0.5546635389328003),
   ('冷え', 0.5544472932815552),
   ('おなか', 0.5496912002563477),
   ('二の腕', 0.5486074090003967),
   ('摂る', 0.5450237393379211),
   ('お肉。', 0.532545268535614),
   ('母乳', 0.5304957032203674)]

# スポーツ + ボール + バット
  [('選手', 0.6550242304801941),
   ('ゴルフ', 0.6252200603485107),
   ('プレー', 0.62102210521698),
   ('スイング', 0.5904982686042786),
   ('野球', 0.5855013132095337),
   ('ホームラン', 0.579775333404541),
   ('球', 0.575089156627655),
   ('曲がる', 0.5615918040275574),
   ('桑田', 0.5606072545051575),
   ('試合', 0.556659996509552)]
 
# 恋愛
  [('恋', 0.7438102960586548),
   ('異性', 0.6826393604278564),
   ('結婚', 0.6707363724708557),
   ('彼氏', 0.6607388854026794),
   ('付き合う', 0.6485260725021362),
   ('結婚願望', 0.643389105796814),
   ('男性', 0.6369950771331787),
   ('女', 0.6349220871925354),
   ('女性', 0.6166598796844482),
   ('恋愛結婚', 0.6131727695465088)]
```  

  類似単語の傾向は、Wikipediaコーパス版のモデルとは異なっているようです。（一般名詞だけでなく、固有名詞も類似度上位に入ってくる点）
  ライブドアコーパス特有の傾向が反映されていることが見て取れます。


## ライブドアコーパス版 word2vec も取り入れた分類モデルの実装
  
  Wikipedia版 word2vec と、ライブドアコーパス版 word2vec のそれぞれから意味ベクトルを取り出し、結合して使います。
  
```plaintext
  
  +------------------------------------+-------------------------------------------+
  |   Wikipedia版 word2vec で作った     |  ライブドアコーパス版 word2vec で作った     |
  |        意味ベクトル                 |         意味ベクトル                       |
  +------------------------------------+-------------------------------------------+
  
```
  
各サンプルの特徴量は、これまでの Wikipedia版 word2vec で作った意味ベクトル（100次元）から、そこにライブドアコーパス版 word2vec で作った意味ベクトル（100次元）を加えたもの（＝200次元）になります。

果たして、ツイン・ドライブはどこまで性能差を縮めてくれるでしょうか？？


### 性能評価結果

  ライブドアコーパス版の word2vec のベクトルを加えたところ、なんと精度が大きく向上し、f1スコアが**`約0.92`**まで達しました。
　ラウンド1から比べて**`約4ポイントの改善`**です。

　戦場の地形に特化した装備、改良を加えた結果、**新型機をヒヤリとさせるところまで来た？**といったところでしょうか。
  
```bash
$ python src/sandbox/evaluate_word2vec_plus_livedoor.py  -i ./corpus/livedoor/text -mw ./models/d100_window10/ja-wikipedia.model -ml ./models/livedoor/ja-livedoor.model
  
  # SVM
  Accuracy:  0.9193766937669376
                precision    recall  f1-score   support

             0       0.91      0.96      0.93       178
             1       0.92      0.98      0.95       158   # 新型機と平均的に互角？
             2       0.84      0.80      0.82       100
             3       0.99      0.99      0.99       171   # 新型機と互角かそれ以上？
             4       0.93      0.91      0.92       146
             5       0.94      0.88      0.91       165
             6       0.97      0.97      0.97       193   # 新型機と互角化それ以上？
             7       0.86      0.89      0.87       177
             8       0.88      0.85      0.87       188

      accuracy                           0.92      1476
     macro avg       0.92      0.91      0.91      1476
  weighted avg       0.92      0.92      0.92      1476
```

ライブドアコーパスのカテゴリ毎で見ると、ところどころで精度が **`0.95`** を上回るものがあることがわかります。
新型機を時々追い込んだ局面があったのかもしれませんね（小並感



## 第二ラウンド結果：まあまあ戦えるようになったかも

ということで、第二ラウンドは装備の調整・改良の結果、**新型機をヒヤリとさせる局面が見られる**ところまで来たようです。

少なくとも全カテゴリで一方的にボコボコにされた第一ラウンドからは、大きな前進があったのではないかと思います。

~~地形に特化した整備調整・改良という名前のチートとか言わないで~~


(Now Updating models...)


# お知らせ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138258/1ff0a722-1477-d2b1-2847-ef4198ee2b86.png)

現在ABEJAでは一緒に~~旧型モデルで新型モデルに一矢報いる闘い~~ AIの社会実装を進める仲間を募集しています。
テクノロジーが世の中をガラっと書き換えてゆく時代で、あなたもその最先端で働いてみませんか？
ちょっとでも気になったら、すぐにコンタクト！そういう時代です。

ABEJAでは以下のページで、たくさんの職種を募集中です！
- https://hrmos.co/pages/abeja/jobs

最先端の世界に飛び込むチャンスがすぐそこに！
思い立ったが吉日・ぜひお気軽にコンタクトを取ってくださいね！