自然言語処理⑦~Doc2Vec~
前回までは比較的単語に形態素解析から単語のベクトル化などをして文書の中に着目してきましたが、今回のDoc2Vecでは文書をベクトルで扱っていくため以前よりもすこし視野が広くなって行きます。
こちらもほんとにたくさんのことができるのですが、自分のできる範囲でアウトプットや使えそうな実践データを用いながら探索していけたらと思います。
では、今回もよろしくお願いいたします。
・Doc2Vecとは
基本的にはword2vecの派生です。
後ほど行う文書分類を行うにはこのDoc2Vecを利用します。
word2vecと違い、語順を考慮したり前回までのLDA以上の性能(?)が発揮できるそうです。
・Doc2Vecの主な理論
Word2VecのCBoWやskip-gramと似たような理論がDoc2Vecにもありはじめにそちらを紹介しておきます(実践だけで良いという方は流し見とかでもいいと思います。)
・DMPV
Word2VecにおけるCBowのようなアルゴリズム。
CBoWと違い、文書IDという要素を加えて単語を予測するという手法です。
図を見て行きます。
その前にCBoWを図だけでおさらいしておきます。
では、DMPVを見て行きます(小文字でdmpvとかの表記の人もいますが、あまり気にしてないので、両方使いますw)
いろいろ書いてるので、順に行きましょう。
まず、そもそも用意するデータは文書とそれにIDを振っている文書IDが必要となります(特にIDは連番とかで大丈夫なはずです)
そして、ある単語w_tというものがマスキングされている時にそのドキュメントid=iのと直前のn-1単語(単語というのは形態素解析されたそれぞれの形態素)で予測をするものとなります。(上の図はちょっと厳密でないですw)
前回のCBoWのinputにはない文書IDも入れることで予測をしていくことが特徴で、この文書IDはone-hot(例えば文書ID=1なら、(1, 0, 0, ...)みたいな)表現で入力されます。
実際には単語たちを行列で表現し次元圧縮したのちに文書IDと圧縮されたベクトルを繋ぎ合わせて入力するのですが、あまりコーディングの際にそこまでの厳密性は求められない(はず。素人考え)と思っているので、まぁ、単語とIDを入力するんだな、くらいで行きましょう
また、各入力と中間層の間に重みづけなどを考慮するのですがそこまで踏み込むほど実践で必要なのか(現時点で自分が)不明のためあくまで外観程度で留めておきます。
これにより実はWord2Vecではできない、未知の文書による単語予測が可能になります。
(詳しい説明は割愛しますが、2層のニューラルネットワークかつ文書とIDの入力により予測モデルのようなイメージと思ってもらえばいいです。)
・DBoW
こちらはWord2Vecのskip-gramに対応していると言われますが、時たまBoWに近いと言われたりもしますが、個人的にどちらでもいいかなと思いますw
文書ID を入力して単語を予測する手法で、単語の順番自体は考慮されずに出力されます。
こちらは特にこれ以上解説もないので、この辺にします。
基本的にはdmpvの方が精度が良く十分な精度を出すと言われています。
実際のgensimのデフォルトもdmpvとなっていますが、ほんとにシビアに精度を突き詰めたいという時はDBoW とdmpvを組み合わせた手法もあるそうです。
・Doc2Vecを実装してみる
理論も一息ついたところで本題に。
いつも通りデータの準備だけします。
今回使うデータは以前(どっかの投稿で添付した)国会のデータを使います。
見た感じlivedoorのコーパスを使っているサイトが多かったので、見慣れないコーパスの方がいいかなと思いこちらを使います。
コピペで何も考えずにすすめましょ。
!apt install mecab
!apt install libmecab-dev
!pip install mecab-python3
!pip install unidic-lite
# 形態素分析ライブラリーMeCab と 辞書(mecab-ipadic-NEologd)のインストール
!apt-get -q -y install sudo file mecab libmecab-dev mecab-ipadic-utf8 git curl python-mecab > /dev/null
!git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git > /dev/null
!echo yes | mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n > /dev/null 2>&1
!pip install mecab-python3 > /dev/null
# シンボリックリンクによるエラー回避
!ln -s /etc/mecabrc /usr/local/etc/mecabrc
!pip install mojimoji
!wget http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt
def text_to_words(text, stop_word_pass='./Japanese.txt'):
"""
to make the stopword list
parameter:
text: text data
stop_word_pass: path that exists stopwords list
"""
stopword_list = []
with open(stop_word_pass, 'r') as f:
stopword_list = f.readlines()
stopword_list = [x.strip() for x in stopword_list if x.strip()]
# 形態素解析を始める
path = "-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd"
m = MeCab.Tagger(path)
# m = MeCab.Tagger('-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd')
m.parse('')
# text = normalize_text(text)
text = mojimoji.zen_to_han(text, kana=False)
m_text = m.parse(text)
basic_words = []
# mecabの出力結果を単語ごとにリスト化
m_text = m_text.split('\n')
for row in m_text:
# Tab区切りで形態素解析、単語部のみ取得
word = row.split('\t')[0]
# 最終業はEOS
if word == 'EOS':
break
else:
pos = row.split('\t')[1]
slice_ = pos.split(',')
# 品詞の取得
parts = slice_[0]
if parts == '記号':
continue
# 活用語の場合は活用指定のない原型を取得
elif slice_[0] in ('形容詞', '動詞') and slice_[-3] not in stopword_list:
basic_words.append(slice_[-3])
elif slice_[0] == '名詞' and word not in stopword_list:
basic_words.append(word)
basic_words = ' '.join(basic_words)
return basic_words
import pandas as pd
import MeCab
import mojimoji
from gensim.models.doc2vec import Doc2Vec, TaggedDocument
df_input = pd.read_csv('kokkai.csv', header=0)
df_input['text_ana'] = df_input['text'].apply(text_to_words)
今回文書IDというものをつかべく、TaggedDocumentを読み込み準備しておきます。
keys = df_input[['date', 'house', 'meeting']].drop_duplicates().values.tolist()
tagged_corpus = []
for key_list in keys:
df = df_input[(df_input['date'] == key_list[0]) &
(df_input['house'] == key_list[1]) &
(df_input['meeting'] == key_list[2])]
df = df.sort_values('speech_order')
sentence = ' \n '.join(df['text_ana'].values.tolist())
sentence = sentence.split(' ')
tag = [f'{key_list[0]}{key_list[1]}{key_list[2]}']
tagged_corpus.append(TaggedDocument(sentence, tag))
基本的なコードは割と見やすいのかなとは思います。
まずdate, house, meetingを一意に取り出してそれらを順番に取り出したのち、照準にしてtext_anaの要素を改行でjoinしています
keys = df_input[['date', 'house', 'meeting']].drop_duplicates().values.tolist()
tagged_corpus = []
for key_list in keys:
df = df_input[(df_input['date'] == key_list[0]) &
(df_input['house'] == key_list[1]) &
(df_input['meeting'] == key_list[2])]
print('-'*10)
print(df)
df = df.sort_values('speech_order')
print('-'*10)
print(df)
sentence = ' \n '.join(df['text_ana'].values.tolist())
print('-'*10)
print(sentence)
sentence = sentence.split(' ')
tag = [f'{key_list[0]}{key_list[1]}{key_list[2]}']
tagged_corpus.append(TaggedDocument(sentence, tag))
それらを空白で区切ってリストとして保持してtagged documentに入れます。
なっがい前処理ですね。。
classgensim.models.doc2vec.TaggedDocument(words, tags)
TaggedDocumentはclassなんですね。。
クラスなので、適当にtagged_corpusの始めの要素を持ってきて中身を見てみましょう。
TaggedDocumentのもつ属性にwords, tagsがあるのでそれを参照します。
print(tagged_corpus[0].words[:10], tagged_corpus[0].tags)
こんなのが格納されてるんだ〜くらいでいいと思いますw
では、これを使ってDoc2Vecを用いてみましょう!
model = Doc2Vec(documents=tagged_corpus, vector_size=300, window=3)
model.docvecs.most_similar('2019-12-03参議院経済産業委員会')
では見て行きます
classgensim.models.doc2vec.Doc2Vec(documents=None, corpus_file=None, vector_size=100, dm_mean=None, dm=1, dbow_words=0, dm_concat=0, dm_tag_count=1, dv=None, dv_mapfile=None, comment=None, trim_rule=None, callbacks=(), window=5, epochs=10, shrink_windows=True, **kwargs)
documents: TaggedDocumentで生成されたリスト
corpus_file: LineSentence のフォーマットにてコーパスファイルの指定
dm: 1のときdmpv、0のときDBoW
vector_size: 特徴量ベクトルの次元数指定
window: 文章予測に使う単語を前後どこまで参照するか
alpha: 最初の学習率設定
min_count: 指定数値以下の出現単語は除外
epochs: エポック数指定
残りは割愛
少し色々見て行きましょう。
まずdocvecs.most_similarによって指定した単語と近しいdocvecsを取得(defaultは10個、変更したい場合はtopn=xを追加)
similarity(d1, d2)により指定したd1, d2 のコサイン類似度を出力してくれます。
ほかにも様々なメソッドがありますため、公式のドキュメントやhelp関数などで参照してみましょう!
・終わり
今回まででおそらく入門〜基礎くらいは終わったのかなと思います。
なかなか大変な気もしますが、RNNやLSTM、transformerなどまだまだ投稿(勉強)しますので、引き続きお楽しみに〜
一旦ニューラルネットとかに戻そうかとも思っていますが、どうしましょうかね・・
では、また次回!
ーー余談ーー
高頻度の投稿のせいか、わりと予想以上の人に見ていただいているのですが、かなり偏った内容のため、誰が見ているのか謎い(そもそも自分のアウトプット用のアカウントですしw)のですが、ありがたいですね