Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

Doc2Vecによる文書ベクトル推論の安定化について

はじめまして,Sansan DSOC R&Dグループ インターンの小林といいます。
2月下旬から3月末までの間,主に自然言語処理 (NLP) に関連した研究開発に挑戦させて頂きました。大学でNLPを専攻している訳では無いですが,他の研究員の方やインターンの先輩とのディスカッションなど,とにかく刺激的な日々でした。

本稿はNLPブログということで,近年のNLPでスタンダードとなっている,単語・文書の埋め込み手法に言及します。

TL; DR

  • Doc2Vecはお手軽に便利に使える
  • 学習済み単語埋め込みを用いたラベル推論をしてみた
  • 文書ベクトルを算出するDoc2Vec.infer_vector()の機能の精度と安定性についての実験と考察をしてみた
  • Doc2Vec.infer_vector()を呼ぶ際は,推論した文書ベクトルのばらつきを減らすために短い文書ほどパラメータepochsを大きめにした方が良さそう

Word2Vec / Doc2Vecについて

Word2Vec とはその名の通り Word-to-Vector を実現する分散表現(単語埋め込み)手法であり,以下のような単語同士の加減算が可能であるとして,5年程前に一躍有名になったとのこと。

マドリード − スペイン + フランス = パリ *1

各単語のベクトル表現により,内積計算で単語間の類似度を数値化することが出来るため,非常に便利です。

参考:コサイン類似度について
参考:Word2Vec のニューラルネットワーク学習過程を理解する

Doc2Vec は,Word2Vecでの単語のベクトル化手法(CBoW, Skip-Gram)をベースに,単語の羅列である文書もベクトルにしてしまおうというもので,多数の文書の中から,文書IDを入力値に,その文書内からランダムに選択された単語を予測することで文書全体の意味を獲得する手法(DBoW : Distributed Bag of Words)と,文脈窓の単語と文書IDを結合したものから中心の単語を予測することで文書の文脈情報を取得する手法があります。(PV-DM : Paragraph Vector - Distributed Memory) 一般的に後者の方が表現の精度が良いですが,前者の方が省メモリであるとされています。
参考:Distributed Representations of Sentences and Documents
参考:Paragraph Vector DBOWの憂鬱

f:id:atsu_jg43yr:20190329144217p:plain
DBoWの模式図:論文より引用

最近は単一モデルで各種タスクのSoTAを達成したBERT *2,ELMo *3のようなAttention / Transformerベースのモデルが主流となりつつあり,NLPをやっている方は発表から5年も経っているWord2Vec, Doc2Vecに対してレガシー感を覚えるかも知れません。

しかし,Doc2Vecは,

  1. gensimによる実装で早い・使いやすい・学習させやすい
  2. DBoWなどは省メモリ*4である
  3. 技術ブログなど,日本語での文献やエントリが非常に豊富 *5

という点を考慮すると,まだまだ便利に使えるものなのではないでしょうか。特に,Doc2Vecの利点はその実装から簡単に得られるParagraph Vector (文書ベクトル) によって単語と可変長の文書を同次元のベクトル表現にし,同列に扱えることと,モデル利用が簡単であることが利点だと考えます。

Doc2Vecによる文書ベクトルと単語ベクトルを同じ空間で扱い内積計算 (類似度算出) することで,例えば『Webからスクレイピングした大量のテキストデータの中から任意のトピックを含むデータのみを抽出したい』というような,特定の文書に対して教師データ無しで任意のラベルセットの中から一つに分類したい状況に応用できます。

文書ベクトルによるニュース文書属性判定を試す

そこで,今回は「お手軽」にこれら文書ベクトルを用いて,文書の属性判定を行ってみます。

なんとも有り難いことに,@yag_ays ことR&Dの奥田が 日本語Wikipediaを用いて学習したDoc2Vecモデルを公開しているので、そちらを使用させていだたきます。このモデルでは,各文書を300次元のベクトルで表現します。

この学習済みモデルを用いて「Sansan株式会社」 から 「名刺」 を引き算したところ、某総合コンサルティングファームの名前が出力されました。Wikipediaの膨大な情報から,なかなか上手く表現が出来ていそうです。

また,Doc2Vecに入力する文書は,単語毎に区切られてる必要があるため,形態素解析エンジンMeCabと,新語・固有名詞に強いNEologdという辞書を用いて文書を区切ります。

上記2つを用いてテキストのベクトル化&類似度の計算を行うための,簡単なクラスを実装しました。GitHubにて公開いたしましたので,もしよろしければお試しください。

github.com

タスク:スポーツニュースの内容属性の推定

今回は,タスクとしてLivedoorニュースコーパス内のSports Watchの記事に対して,属性の推定を行いたいと思います。

news.livedoor.com

  • LabelEstimator クラス
from LabelEstimator import LabelEstimator

DOC2VEC = "../PATH/TO/models/jawiki.doc2vec.dbow300d.model"
NEOLOGD = "/usr/local/lib/mecab/dic/mecab-ipadic-neologd"

model = LabelEstimator(DOC2VEC, NEOLOGD)

任意で分類したいラベルを決めます。私が好きなスポーツを指定して,この中からどれかのラベルをニュース記事に付与してもらいます。

model.set_labels(["野球", "サッカー", "バスケットボール", "テニス", "卓球"])

estimate(str)で,文書のラベルをset_labels(list)された中から推定します。例えば,

model.estimate("イチローが現役選手を引退した。")

という入力に対しては,

>>> 出力結果:「野球」
ラベル スコア
野球 0.519857
バスケットボール 0.429808
サッカー 0.415668
テニス 0.405853
卓球 0.390150

という結果を返します。「野球」という文字列は本文には入っていませんが,直感的な結果が出ました。それでは,LivedoorニュースコーパスのSports Watch の中から,関連したトピックのみを抽出する目的で,複数のスポーツの単語の中からどの単語に最も類似・関連した内容かを分類させます。

  • LivedoorNews:Sports-Watch .txtファイルを配置します。
./text
   |_sports-watch/
     |_sports-watch-???????.txt
     :
  • ファイルの読み込みとテキストの軽〜い下処理をします。
def clean_sportswatch_txt(s: str) -> str:
    s = s.replace("【Sports Watch】", "")
    s = re.sub(r"(https?|ftp)(:\/\/[-_\.!~*\'()a-zA-Z0-9;\/?:\@&=\+\$,%#]+)", "" , s)
    s = re.sub(re.compile("[\d!-/:-@[-`{-~]"), '', s)
    return s

documents = []
for file in tqdm(glob("text/sports-watch/sports*.txt"),
                 desc="Loading .txt files"):
    with open(file, "r") as f:
        documents.append(clean_sportswatch_txt(f.read()))

以下の記事を

香川、アジア杯は「優勝した大会ではなくてケガをして終わった大会」 日深夜、日本テレビ「NEWS ZERO」では、アジアカップ準決勝で右第中足骨を骨折し、先月日に手術を行ったばかりの日本代表・香川真司にインタビューを行った模様を放送した。 「入院している時は部屋での生活が基本ですから、いろんなことを考えたり。アジアカップのことを振り返ったり、ケガしたことを振り返ったり、一人になるとマイナスなことを考えたりしました」と語る香川は、同大会を「正直なところ、優勝した大会ではなくてケガをして終わった大会になってしまったので。また、それによってドルトムントでのプレーもできなくなり、やっぱ、色んなことを悩みましたし、感じました。自分自身にとっては悔しい大会」と表現した...

推定すると,

model.estimate(documents[8])
>>> 出力結果:「サッカー」
ラベル スコア
サッカー 0.450402
卓球 0.400607
野球 0.370349
バスケットボール 0.323022
テニス 0.318613

と,サッカーの記事であると推定が出来ています。結果として,アノテーション等の面倒な作業をすることなく,コーパスの情報を抽出して分類できました。学習済モデル内部に格納されているWord2Vecの単語ベクトルと,Doc2Vecの機能であるinfer_vector()による文書に対するベクトル表現の獲得を同時にかつ手軽に実行できる,gensimDoc2Vecの強みであると言えます。

Doc2Vecによる文書ベクトル推論の問題点

しかし,様々な記事を調べてみると,「Doc2Vec.infer_vector()は精度がよくない」というコメントが散見されます。確かに,実際に自身で使っていても以下の問題にぶち当たりました。

  1. 同一文書に対するinfer_vector() の実行毎に得られるベクトルが異なる。
  2. infer_vector()単語のみ等の極端に短い文書を変換しDoc2Vec.similar_by_vector(v)で類似語の検索をすると,全く関係の無さそうな単語がたくさん出てくる。

これらの問題へのアプローチとしてgensimのソースを読んだりしていると,いくつかのissueやStackoverflowで,同様の問題である,「同じ文書に対してベクトルを算出したのにcos類似度が1じゃない」*6「実行毎に結果が変わる」*7などの報告が確認されました。

それらフォーラムでのgensim開発者@gojomo氏の解答によると,

  • infer_vector()はモデルの学習時と同様の処理が走っており,単一文書とそのタグを用いたParagraph Vectorの学習を行い,その結果をベクトルとして返している。

  • DBoWでは文書タグのみを用いるので未知語(Out-of-Vocab)は無視される。PV-DMは入力として文書内の単語ベクトルが必要なので未知語のベクトルに関しては乱数で処理される(Source)

  • CythonでなくPythonによる実装の場合は乱数のシードを固定すればランダム性が抑えられるのではというアイデアが共有されている。*8

  • infer_vector()の結果の初期値として文中の単語ベクトルの平均を用いるのはどうか?というアイデア・それに関する議論 *9

  • あるStackOverflowのフォーラムにて,gojomo氏の提案・共有したスニペットはで20回反復学習していた。

というように,基本的にDoc2Vec.infer_vector()による未知文書のベクトルの推定時には学習と同じ処理が回るので,epochsもしくはstepsパラメータで反復回数をデフォルトの5より大きく指定することで推測結果の安定性と精度が上がるとのことです。

def infer_vector(self, doc_words, alpha=None, min_alpha=None, epochs=None, steps=None):
        """Infer a vector for given post-bulk training document."""

*10

ということで,上記での議論は正しいのかを確かめ,今後のDoc2Vecでの文書ベクトル推論の精度向上のために, 「一体何回内部の学習処理を反復したらベクトルの推論結果は安定するのか」
そして「短い単語数の文書に対してのPV算出はどうするべきなのか」 を検証していきたいと思います。

精度検証実験

上記検証のために以下の実験を考えました。

  • ①「何回内部のtrainを反復させたら文書ベクトルの推論結果は安定するのか」

    • 「同一文書に対するinfer_vector()を2度行い,cos類似度を算出する」という処理を複数回繰り返すことで,ベクトル化の精度とその安定性を計測出来ると仮定し,複数回算出されるcos類似度の平均と分散をその指標とする。
    • 理想はcos類似度=1である (同一のベクトル) が,実際はそうではないことが確認されているので,そのcos類似度がinfer_vector(epochs=N)の反復回数Nの増加によりどれほど1に近づくのかを計測し,反復による効果を測定する。
    • 同様に,理想的なcos類似度の分散は0である (実行毎に同一の結果が返ってくる) が,実際はそうでは無いので,反復回数Nの増加によりどれほど0に近づくのかを計測し,反復による効果を測定する。
    • 当然の事ながらepochsを増加させるほど計算量も増えるので,なるべく少ない計算量で効果的な精度と安定性が得られると思われるepochs数について考察する。
  • ②「文書の単語数と推論結果の精度・安定性を調べる」

    • 単語数が極端に少ない時には上手く動作しない (そもそも想定されて作られていない) ことが確認されている。
    • 異なる単語数をもつ様々な文書に対して①と同等の実験を行い,単語数Nの文書に対してベクトルを推論する際の適切なepochs数を考察する。

実験実行と結果

実験① 以下サンプルテキストに対する独立した2度の文書ベクトル推論(infer_vector()の実行) ×100試行

サンプルテキスト (Sansanプロダクトサイトより引用)

名刺を企業の資産に変える最高精度の「AI名刺管理」
AI+手入力で名刺をほぼ100%正確にデータ化。
高度なAI技術により、会社・人物単位で名刺情報を管理できるため、
昇進や異動などの人事異動情報も自動で集約。
正確な人物データを共有できるので、社内に眠る人脈を全社で有効活用することができます。

を,各epochsパラメータ毎に行いました。

f:id:atsu_jg43yr:20190329124556p:plain
100試行中の同一文書のベクトル間のcos類似度

上記プロットをみると,epochsの値を大きくするほど同一文書間のcos類似度が1に近づき,かつ試行毎のばらつきも抑えられていることがわかります。大方予想通りです。

では次に,どの程度のepochs数なら効率が良いかを考えるために,上記実験におけるepochs数と100試行間のcos類似度の平均値分散の関係を調べます。まず,「精度」を評価する指標である100試行間のcos類似度の平均値を見てみます。

f:id:atsu_jg43yr:20190329125643p:plain
epoch数とcos類似度平均の関係

cos類似度のスコア平均はepochsの増加とともに1に近づくため,文書ベクトルの精度が向上する傾向があると言えます。おおよそepochs=30あたりで同一文書に対するcos類似度が0.99を超え,その後は上昇が緩やかに落ち着きます。epochs=200時点で0.997前後までは上昇します。しかし,epochs=50から4倍の時間をかけて200回反復させたとしてもこの程度の伸びなら,epochs=30からepochs=50程度にしておくのが直感的には効率が良さそうです。

次に,「安定性」の指標である100試行間の分散値の推移を見てみます。

f:id:atsu_jg43yr:20190329125634p:plain
epoch数とcos類似度分散の関係

cos類似度のスコア分散はepochsの増加とともに分散が減少しており,文書ベクトルの推論結果の安定性が向上していると言えます。

実験② 複数の文書に対する独立した2度の文書ベクトル推論(infer_vector()の実行) ×100試行でのcos類似度平均と分散の測定

まず,上記サンプルテキストとライブドアニュースコーパス:Sports Watchの文書を用いて,複数の単語数を持つ文書を用意します。

単語のみ (N=1) から,コーパス内の上限である5688単語まで,12段階で分けた文書を用意します。各単語数は以下の通り。

[1, 2, 4, 15, 27, 45, 57, 80, 202, 500, 1002, 5688]

*11

これらの文書に対して,infer_vector()100試行でのcos類似度平均と分散の測定をepochsを増加させながら (5から200まで) 行った結果は以下のようになります。

f:id:atsu_jg43yr:20190408012959p:plain
単語数別のcos類似度平均の推移

まず結果から確認できるのは,全体を通して文書の単語数が多いほどcos類似度は高くなる傾向があり,epochs=5の時点で単語数5688の文書の平均cos類似度は0.983であるのに対し,単語数1の文書は0.722,単語数2では0.820とかなり低くなります。同一文書に対してのcos類似度が0.72ではベクトルがうまく表現を獲得出来ているとは言い難いでしょう。 *12 しかし,単語数1の文書でも,epochs=50で0.955,epochs=200で0.982までは上昇します。全体としてepochs=40を超えたあたりからcos類似度の上昇が落ち着く傾向が見て取れます。

f:id:atsu_jg43yr:20190408012946p:plain
単語数別のcos類似度分散の推移

分散の方も,全体を通して1文書あたりの単語数が多いほど低い傾向があり,得られる結果が安定しています。epochs=50程度でどの単語数の文書でも殆ど同じ安定性を得られると捉えて良さそうです。

考察・まとめ

epochs数と文書ベクトル推論結果の精度・安定性において関連があるということは実験結果から明らかであり,開発者であるgojomo氏の主張と一致します。また,スニペットでのepochs=20という設定も非常に妥当であると感じます。(ならなぜデフォルト値をその値にしないのかは不明)

また,たとえ1単語のみからなる文書でも (その場合はWord2Vec等での単語ベクトル表現の方が適切ですが) epochs数を上げれば通常の文書と同様な精度・安定性でのベクトル化を行うことが可能であると分かりました。ただし,デフォルトのepochs=5である場合は良いベクトル表現が獲得できているとは言えず,またその単語が未知語であった場合はベクトル化が不可能になってしまうため,fastTextのsubwordやsimstring, 単語埋め込みを単語埋め込みに埋め込む -前編- - Sansan Builders Box のような何かしらの未知語処理を行う必要があります。

さらに,今回の実験では50単語あたりを超えた文書からデフォルトepochs=5でのcos類似度に差がつきにくくなったため,大方50単語以上の文書であればepochsを100や200のように大きく設定せずとも30程度で動作が安定するのではないかと考えられます。より長い文書に対しては,計算時間削減のためにepochsをデフォルトの5のままベクトル化するといったアプローチも効果的であると思います。

ただし,今回の実験で取り扱わなかった "英語・日本語が混ざっている文書"などのような,実データに良く見られる例で同等の結果が得られるとは限らず,実際に単一文書ではなくLivedoorニュースコーパス全体の文書からランダムに100文を選定し,適宜形態素解析(分かち書き)にかけて2度推論,同じくcos類似度の計算を行う…という追加実験では,精度の向上は見られたものの安定性(cos類似度の分散)は収束しない挙動を見せる場合もありました。*13

しかし,何れにせよデフォルトのepochs=5より少し多め (文書の長さに応じて20から50) の反復を行った方が,Doc2Vecをより効果的に使う上では良いでしょう。
本稿執筆中に,Doc2Vecのハイパーパラメータ探索についての研究:Paragraph Vectorのための効率的なパラメータサーチの検討を見つけたので,こちらも目を通しておきたいと思います。

おわりに

本稿の内容は基本的にインターンでの業務で取り入れたり,調べて学んだりした技術に関連するものですが,様々なバックグラウンドを持つDSOCの研究員の方々とのディスカッション・何気ない会話を通して,短い1ヶ月という間でも本当に多くの学びを得ることが出来たと感じています。学生の方にはSansan DSOCでのインターンシップ参加を心の底からお勧めしたいと思います。

*1:Distributed Representations of Words and Phrases and their Compositionality

*2:arXiv:BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding

*3:arXiv:Deep contextualized word representations

*4:論文Distributed Representations of Sentences and Documentsで言及がある

*5:例えば,Qiitaの記事タグ数ではWord2Vecがニューラル言語モデルの中では圧倒的に多い。各件数は,word2vec : 163 / doc2vec :35 / fastText : 38 / ELMO : 27 / BERT : 17 となっている

*6:Stackoverflow : How to use the infer_vector in gensim.doc2vec?

*7:GitHub gensim issue : misc ways to improve infer_vector

*8:GitHub gensim issue : Doc2Vec infer_vector() could (as option?) offer deterministic result

*9:GitHub gensim issue : seed doc2vec.infer_vector() with mean of word vectors to get more stable results

*10:gensim.models.Doc2Vec.infer_vector()のソース

*11:この時80単語までの文書はサンプルテキストから単語・フレーズの抜き出し&文を少しずつ足していくことで生成し,それ以上の文書はSports Watch内から単語数で記事を探索し抽出しました。

*12:ちなみに今回利用している学習済みモデルでは「サッカー」と「バレーボール」のcos類似度が0.70程度

*13:詳細はGitHubのリポジトリ上のjupyter notebookで公開しています

© Sansan, Inc.