カレンダー記事で学習した doc2vec で遊ぶ

この記事は、[富士通ソーシアルサイエンスラボラトリ Advent Calendar](https://qiita.com/advent-calendar/2018/ssl) 24日目の記事です。

## はじめに

AI・機械学習ばやりの昨今、追いかけておきたい技術は多数あります。
しかし複数のニューラルネットワークの組み合わせや多層化などで、なかなか簡単に試せない技術も増えてきました。
最新とは言えませんが、この記事では比較的気軽に試せる自然言語処理向けの Doc2Vec を使って機械学習の実行の流れを見てみたいと思います。

## Doc2Vec の考え方

Doc2Vec は単語のベクトル表現を学習する Word2Vec を任意の長さの文に使用できるように拡張したものです。

単語のベクトル表現というとピンとこない感じですが、 Word2Doc で良く使われるベクトルを用いた計算の例は以下です。

> 「パリ」 - 「フランス」 + 「日本」 = 「東京」

各単語の要素を数値で持つことで、上記のように単語の計算が可能になります。
この数値は例えば以下のような中身になります。

||パリ|フランス|日本|東京|
| --- | --- | --- | --- | --- |
|地名|0.9|0.1|0.05|0.9|
|国名|0.06|0.96|0.98|0.09|
|中心|0.95|0.23|0.3|0.92|
|フランス|0.92|0.99|0.12|0.08|
|日本|0.01|0.02|0.95|0.97|

この数値がうまく単語の特徴を表すような値になっていると、パリからフランスの要素を引いて、日本の要素を足すと東京になる、といった計算が可能になります。
Word2Vec/Doc2Vec ではニューラルネットワークを使って、文章の塊から上記の数値を学習していきます。

仕組みについてに興味がある方は、以下の記事を参照してみてください。
https://deepage.net/bigdata/machine_learning/2016/09/02/word2vec_power_of_word_vector.html
https://deepage.net/machine_learning/2017/01/08/doc2vec.html

## 試してみる内容

[富士通ソーシアルサイエンスラボラトリ Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ssl)の各記事を学習データとして使用し、記事間の類似度などを見てみたいと思います。

できるだけ直前の記事まで使用しますので、結果は現時点(12/16)で予想つきません。
たぶんオチもつきません。

学習データとしてはテキストの量が不足すると思いますので、本格的に試されたい方は Wikipedia や青空文庫のデータで試してみてください。

* [15分でできる日本語Word2Vec](https://qiita.com/makaishi2/items/63b7986f6da93dc55edd)
* [doc2vecでWikipediaを学習する](http://tadaoyamaoka.hatenablog.com/entry/2017/04/29/122128)

## 実行環境

Google Colaboratory 上の Notebook 環境を使用しました。
Google Colaboratory は Web 上でコード・機械学習を実行できる Google のプラットフォームで、無料で使用できます。IPython/Jupyter Notebook をご存知の方は、そのクラウド版と考えてください。
詳しく知りたい場合は下記記事が参考になります。
https://www.codexa.net/how-to-use-google-colaboratory/

python 3.6, mecab, gensim を使用します。gensim は自然言語処理を行うライブラリで、 Word2Vec/Doc2Vec の実装も含まれています。今回の学習はそれほど時間はかからず、ローカルマシンでもこれらを揃えれば十分実行可能です。
mecab はテキストデータを単語単位に分割するために使用しますが、後述する課題があります。

Notebook のファイルと学習用データは以下に置いてあります。
https://github.com/go5025/doc2vec-cal-201812

## 学習データ

富士通ソーシアルサイエンスラボラトリ Advent Calendar 2018 の 12/1〜12/22 までの記事を使用させていただきました。
"end_of_text" を記事の区切りとして、22の記事を連結して一つのテキストファイルを作成しました。文章の部分はブラウザー上で手でコピーしています。


## コードと実行結果

Google Colaboratory/Jupyter Notebook に合わせて記載します。
! で始まる箇所は、環境作成で使用している OSコマンドを実行するマジックコマンドです。

### 環境設定
Google Colab で新規ページを作成すると、gensim はすでに入っているので、mecab 関連のパッケージを追加すれば OK です。お手元の Ubuntu などで実行する場合は gensim もインストールしてください。

```
!apt install mecab libmecab-dev mecab-ipadic-utf8 git make curl xz-utils file -y
```

だったのですが、なんか学習結果がイマイチです。
今回 mecab をテキストデータを単語単位に分割するために使用しているのですが、どうもその分割結果が変です。

```python:sample
import MeCab

text = "私はバナナが好きです。"

mecabTagger = MeCab.Tagger("mecabrc")
mecabTagger.parse("")
node = mecabTagger.parseToNode(text)
while node:
    word = node.surface
    print(word+": "+ node.feature)
    node = node.next
```
としたときに期待される結果は以下です。

```python:正しい結果
: BOS/EOS,*,*,*,*,*,*,*,*
私: 名詞,代名詞,一般,*,*,*,私,ワタシ,ワタシ
は: 助詞,係助詞,*,*,*,*,は,ハ,ワ
バナナ: 名詞,一般,*,*,*,*,バナナ,バナナ,バナナ
が: 助詞,格助詞,一般,*,*,*,が,ガ,ガ
好き: 名詞,形容動詞語幹,*,*,*,*,好き,スキ,スキ
です: 助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
。: 記号,句点,*,*,*,*,。,。,。
: BOS/EOS,*,*,*,*,*,*,*,*
```

しかし、単に mecab の最新版を入れた時は以下のように出力されます。

```python:mecab-python3&nbsp;0.996.1の結果
私はバナナが好きです。: BOS/EOS,*,*,*,*,*,*,*,*
私はバナナが好きです。: 名詞,代名詞,一般,*,*,*,私,ワタシ,ワタシ
はバナナが好きです。: 助詞,係助詞,*,*,*,*,は,ハ,ワ
バナナが好きです。: 名詞,一般,*,*,*,*,バナナ,バナナ,バナナ
が好きです。: 助詞,格助詞,一般,*,*,*,が,ガ,ガ
好きです。: 名詞,形容動詞語幹,*,*,*,*,好き,スキ,スキ
です。: 助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
。: 記号,句点,*,*,*,*,。,。,。
: BOS/EOS,*,*,*,*,*,*,*,*
```

調べたところ、2018-12-16時点最新の mecab-python3 0.996.1 は以下のバグの対応が完全ではなさそうです。
https://github.com/SamuraiT/mecab-python3/issues/19

仕方ないので mecab-python の旧バージョンを入れて暫定対応としておきます。

```
!pip install mecab-python3==0.7
```

### 学習の実行

学習データをテキストファイルから読み込み学習します。

```python
import MeCab
from gensim.models.doc2vec import Doc2Vec
from gensim.models.doc2vec import TaggedDocument

text = open('simuraly-data.txt', 'r').read()
documents = text.replace("\n", "").split("end_of_text")

def words(text):
    # 文章を単語に分割する
    word_list = []
    tagger = MeCab.Tagger('-Ochasen')
    tagger.parse('')
    node = tagger.parseToNode(text)

    file = open('words.txt', 'a')
    while node:
        word_type = node.feature.split(",")[0]
        word = node.surface
        if word == '*' and node.feature.split(",")[6].isalpha():
            word = node.feature.split(",")[6]

        if len(word) > 1 and word.isalpha() and word_type in ["名詞"]:
            word_list.append(word.lower())
            file.write(str(word + "\n"))
            
        node = node.next
    
    file.close()
    return word_list

# テキストごとに単語分割して学習データを作成する
train_data = []
file = open('documents.txt', 'w')

for i, document in enumerate(documents):
    train_data.append(TaggedDocument(words=words(document), tags=['text' + str(i + 1)]))
    file.write("*****" + str(words(document)) + "\n")

file.close()
    

# doc2vec の学習を実行
model = Doc2Vec(documents=train_data, min_count=1, alpha=0.005, epochs=40, sample=20, dm=1)
```

学習実行の本体は最後の一文です。先頭から、ファイル読み込み、単語分割の関数、学習データの作成を行って、学習を実行する流れです。
本格的に実行する場合は評価方法を決めてパラメータ等調整を行いますが、今回は結果を見ながら手でざっくりと調整しました。


### 学習結果の確認

各カレンダー記事間の類似度を表示してみます。
これは機械学習の推論と呼ばれる計算で、学習をして、推論するというステップが機械学習の典型的な流れです。

```python
for i, document in enumerate(documents):
    print('text' + str(i + 1) + "\t" + document.replace("\n", "")[0:50])
    for items in model.docvecs.most_similar('text' + str(i + 1), topn=2):
        print("\t" + str(items[0]) + " : \t"+ str(items[1]) + "\t")
```

各カレンダー記事 text 1-22 に対する類似記事とその類似度を出力します。
マイコンに関する記事 1, 2, 5, 7、python のログに関する記事 6, 19、proxy に関する記事 8, 18、が類似となっているあたりは感覚に近い結果のようです。すべて良い結果というレベルではありませんが、学習はひとまずできています。

* text1	マイコンボードを中継器にして PC に電子部品を接続し、Ruby で制御してみた（発光ダイオード編）
 	text7 : 	0.9987452030181885
 	text5 : 	0.9986720085144043

* text2	マイコンボードを中継器にして PC に電子部品を接続し、Ruby で制御してみた（温度センサ編）Ru
	text7 : 	0.9979823231697083
	text1 : 	0.9978688955307007
* text3	フロントエンド初心者が、ReactでHelloWorldしてみた（ブラウザ上編）初心者ブラウザHel
	text21 : 	0.9979885220527649
	text20 : 	0.9979159235954285
* text4	フロントエンド初心者が、Vue.jsでHelloWorldしてみた（ブラウザ上編）初心者ブラウザHe
	text20 : 	0.9967581033706665
	text21 : 	0.9964686036109924
* text5	マイコンボードを中継器にして PC に電子部品を接続し、Ruby で制御してみた（サーボモータ編）R
	text1 : 	0.9986720085144043
	text7 : 	0.9985793232917786
* text6	最初に覚えて欲しいPythonのロギングPython21はじめに簡単な使い捨てのコードならprint
	text19 : 	0.9987303018569946
	text7 : 	0.9971414804458618
* text7	マイコンボードを中継器にして PC に電子部品を接続し、Ruby で制御してみた（人感センサ編）Ru
	text20 : 	0.9989395141601562
	text1 : 	0.9987452626228333
* text8	proxy が存在するネットワークから、インターネット上の何かにアクセスするproxy5この記事は、
	text18 : 	0.8662922382354736
	text16 : 	0.8357861638069153
* text9	Adobe XD CCでモックアップをサクサク作ろうAdobeXDモックアップ1今更ですが、Adob
	text17 : 	0.9987900853157043
	text20 : 	0.9986022710800171
* text10	蠟引紙（ろうびきし）でアドベントツリー作ってみたクリスマスデザイン,UI蝋3＜ 序章 ＞先日、妻に急
	text16 : 	0.9987809658050537
	text20 : 	0.9987393617630005
* text11	RaspberryPi で人感カメラを作ってみたPythonRaspberryPiIoT4開始IoT
	text16 : 	0.9988595247268677
	text20 : 	0.9988433122634888
* text12	Python in RedHat Enterprise Linux 8PythonLinuxRHEL
	text20 : 	0.9978363513946533
	text17 : 	0.9974120855331421
* text13	手抜きのjavascript -var編-JavaScript1富士通ソーシアルサイエンスラボラトリ
	text20 : 	0.9978794455528259
	text7 : 	0.9976962804794312
* text14	なんか作るPythoncurses2なんか作ってみるPythonと何かでcursesを使って見た目動
	text20 : 	0.9948053359985352
	text7 : 	0.9946098327636719
* text15	Excelの入力規則でつくる食卓カレンダーExcel,入力規則2この記事は、富士通ソーシアルサイエン
	text20 : 	0.9979256391525269
	text21 : 	0.9978295564651489
* text16	CodiMDによるドキュメント作成のすすめMarkdownHackMDCodiMD4たまたま弊社のア
	text20 : 	0.9992533326148987
	text17 : 	0.9991676807403564
* text17	Webで動くピタゴラスイッチ風のおもちゃJavaScriptTypeScript物理演算Matter
	text20 : 	0.9992940425872803
	text16 : 	0.9991676211357117
* text18	認証プロキシをFiddlerで超えるHTTPFiddler3はじめに行く手を阻む認証プロキシを、Fi
	text16 : 	0.9972044825553894
	text10 : 	0.9966017007827759
* text19	PythonのloggingにTRACEレベルを増やすPython4はじめに前回「最初に覚えて欲しい
	text6 : 	0.9987303018569946
	text7 : 	0.9975227117538452
* text20	何も買わずに試すディープラーニングディープラーニングKerasTensorFlow4はじめに　相変わ
	text17 : 	0.9992940425872803
	text16 : 	0.9992532730102539
* text21	Cytoscape.jsを用いてデータを可視化するgraphcytoscape.js5Cytosca
	text20 : 	0.9991275072097778
	text17 : 	0.9991148114204407
* text22	ルーラーを表示して文字位置を揃えるofficeWordPowerPointドキュメント2はじめにルー
	text20 : 	0.99702388048172
	text21 : 	0.9970059394836426

### 単語間の演算を行う

単語の類似度も出力してみます。コマンドの単語に類似している単語は何でしょうか?

```python
for result in model.most_similar(positive="コマンド", topn=10):
    print(str(result))
```

それなりに関連のありそうな単語が並びました。

> ('設定', 0.9952303171157837)
('以下', 0.9942916631698608)
('情報', 0.9937410950660706)
('ファイル', 0.9932984113693237)
('fiddler', 0.9932637214660645)
('作成', 0.99315345287323)
('環境', 0.99250328540802)
('記事', 0.9917477369308472)
('id', 0.9915721416473389)
('必要', 0.9906970262527466)

それでは user を加えてみます。

```python
for result in model.most_similar(positive=["コマンド", "user"], topn=10):
    print(str(result))
```

するとネットワークに関連する単語が出力されます。

> ('https', 0.991931676864624)
('ネットワーク', 0.9820094108581543)
('存在', 0.9805724024772644)
('情報', 0.978228747844696)
('ファイル', 0.977412223815918)
('設定', 0.973896861076355)
('git', 0.9735790491104126)
('fiddler', 0.9706029295921326)
('インターネット', 0.9678129553794861)
('作成', 0.9671698808670044)

ここから http を引いてみます。

```python
for result in model.most_similar(positive=["コマンド", "user"], negative=["http"], topn=10):
    print(str(result))
```

ネットワーク関連の単語が無くなりました。

> ('python', 0.9798836708068848)
('pc', 0.9794877767562866)
('arduino', 0.9788479804992676)
('pip', 0.978778600692749)
('name', 0.9787502288818359)
('self', 0.9783775806427002)
('制御', 0.9783043265342712)
('インストール', 0.9781792759895325)
('keras', 0.9780073165893555)
('logger', 0.9779103994369507)

パリ + フランス〜 ほど面白いところまでは行きませんでしたが、演算はできているようです。

# おわりに

Doc2Vec を使うと文書間の類似度などを簡単に計算することができます。
単語の演算なども行うことができますが、精度を求める場合にはデータの整備、パラメーターの調整などが必要になります。
データ量が少なければ学習も短時間で試せますので、Doc2Vec で機械学習入門してみてはいかがでしょう。

それでは良いお年を。