検索結果スクレイピング＆doc2vecで企業の業種分類（2）

[検索結果スクレイピング＆doc2vecで企業の業種分類（1）](https://qiita.com/tizuo/items/542c7484bdcee77c59fb)の続きです。今回はDoc2vecでスクレイピングしてきたデータを学習させます。
# 今回の構成
- [Google検索結果からのサイトURLの取得＆検索結果テキストの取得（beautifulsoup）](https://qiita.com/tizuo/items/542c7484bdcee77c59fb)
- Doc2vecでベクトル化←いまココ
- [デンドログラム作成](https://qiita.com/tizuo/items/8000ea71e49248c35260)

この回ではテキストをMeCabで分かち書きして配列に格納し、Doc2vecで学習させます。
[Doc2Vecの仕組みとgensimを使った文書類似度算出チュートリアル](https://deepage.net/machine_learning/2017/01/08/doc2vec.html)と、[文章をベクトル化して類似文章の検索](https://qiita.com/akira_/items/f9bb46cad6834da32367)を参考に、新ヴァージョンのgensimで動くように変更したのと、ファイル名をラベルとしたドキュメントの配列を作成して学習させています。

# 読ませたファイルの規模
- 2kb～10kbのテキスト250個
- トータルの単語数：20万語
- ユニーク単語数：2万語

# ライブラリのインポート
今回は分かち書きをしますので、MeCabを使います。

```python:
import os
import sys
import re
import MeCab
import collections
from gensim import models
from gensim.models.doc2vec import LabeledSentence
```

# テキストファイルの配列格納
以下の処理をする関数を作成します。

- 指定したディレクトリのファイルを読み込み、ファイル名を値としたリストを作成
- ファイルを開いて読む
- 文章を分かち書きして動詞、形容詞、名詞だけ残す
- ファイル名をタグとして、分かち書きした文章とセットの配列を作る

```python:
path = "./keywords"

# 全てのファイルのリストを取得
def get_all_files(directory):
    for root, dirs, files in os.walk(directory):
        for file in files:
            yield re.match('[0-9]+', format(file)).group()
            
# ファイルから文章を返す
def read_document(fn):
    with open(path + "/" + fn + ".txt", 'r', encoding='utf-8', errors='ignore') as f:
        return f.read()

# 文章から単語に分解して返す
def split_into_words(doc, name=''):
    mecab = MeCab.Tagger("-Ochasen")
    #valid_doc = trim_doc(doc)
    lines = mecab.parse(doc).splitlines()
    words = []
    for line in lines:
        chunks = line.split('\t')
        if len(chunks) > 3 and (chunks[3].startswith('動詞') or chunks[3].startswith('形容詞') or (chunks[3].startswith('名詞') and not chunks[3].startswith('名詞-数'))):
            words.append(chunks[0])
    return LabeledSentence(words=words, tags=[name])

# ファイルから単語のリストを取得
def corpus_to_sentences(corpus):
    docs = [read_document(x) for x in corpus]
    for idx, (doc, name) in enumerate(zip(docs, corpus)):
        sys.stdout.write('\r前処理中 {} / {}'.format(idx, len(corpus)))
        yield split_into_words(doc, name)
```
## 配列格納処理の実行
```python:
corpus = list(get_all_files(path))
sentences = list(corpus_to_sentences(corpus))
```
#モデル構築
ハイパーパラメーターを設定して学習させます。
前者の参照元に学習時の最適値が出ていましたが、今回はサンプルが少ない（ユニークで2万語程度）せいか、参照元のパラメタと同じくらいがちょうど良かったです。どのパラメタもいじると顕著に結果に影響します。

```python:
model = models.Doc2Vec(size=400, alpha=0.0015, sample=1e-4, min_count=1, workers=4)
model.build_vocab(sentences)
```
## ハイパーパラメーター
- size：作成する次元の数
- alpha：学習率
- sample：単語の出現頻度がこの値よりも大きい場合は無視されるしきい値
- min_count：この値よりも出現回数が小さい単語を破棄する
- workers：処理させるスレッドの数

# 学習
ある文章から単語を連想させ、その単語を渡して近しい文章を出したときに、自分が一番上位だった文章が100個中90個以上だとOKと判定する処理を入れています。
18回くらいまでいくとOKが出るようになりました。

```python:
token_count = sum([len(sentence) for sentence in sentences])

for x in range(30):
    print(x)
    model.train(sentences, total_examples = token_count, epochs = 10)
    ranks = []
    for doc_id in range(100):
        inferred_vector = model.infer_vector(sentences[doc_id].words)
        sims = model.docvecs.most_similar([inferred_vector], topn=len(model.docvecs))
        rank = [docid for docid, sim in sims].index(sentences[doc_id].tags[0])
        ranks.append(rank)
        if collections.Counter(ranks)[0] >= 90:
            print('ok')
            break
```

#モデルの保存
このさきのクラスタリングにも利用するので、後日参照できるよう保存しておきます。

```python:
model.save("doc2vec.model")
```

# 検証
とてもアナログですが、以下を試しながらチューニングしました。

- 任意の単語で近しい企業が上がってくるか
- 適当な企業を選択し、近しい企業のリストが本当に近しいか

![image.png](https://qiita-image-store.s3.amazonaws.com/0/193707/57af3106-8136-fa7f-5d05-78d8555e3c7f.png)

コードは[参照サイト](https://qiita.com/akira_/items/f9bb46cad6834da32367#%E5%8D%98%E8%AA%9E%E3%81%A7%E6%96%87%E7%AB%A0%E3%82%92%E6%A4%9C%E7%B4%A2)のまんまなので割愛。

## パラメタで試してみたことと結果
当然ですが、ハイパーパラメーターのチューニングでかなり結果が変わります。
クロスバリデーションができないので、手動でパラメタを変えてみた範囲でわかったことは以下。

| 試験内容       |       結果 |
|:-----------------|-----------------------:|
|繰り返しの中で、0.025から0.0001まで学習率を線形減衰させる|収束しない。ちょうど良いところで固定して学習させたほうが良かった。|
|epoch変更|繰り返し数がトータルで1000回（epoch=33）だと収束しない。epoch=5よりもepoch=10のほうがうまく分類できた|
|size変更|多いほど良くなる|

さて、次回はいよいよデンドログラムを作成します。