Word Mover's Distance(6)fastWMDの実行環境を整える

Word Mover's Distance (6) Prepare the execution environment for fastWMD


2021/06/03
藤田昭人


本稿では、 前回 紹介したwikiPageSelectorを使って、fastWMDの入力となる 3種のデータファイルを生成するスクリプト wiki-xml-to-txt.py の動作を確認したいと思います。

タイトルを「fastWMD の実行環境を整える」としましたが、 fastWMD ではWMDおよびその派生アルゴリズムの エラー率と実行時間に着目しているため、 Word Mover's Distance に本来必要な 文字列処理(トークン化やストップワードの除去など)を 評価プログラムの外に追い出しています。 なので、fastWMD のコードをベースに Word Mover's Distance のAPIを実装するとなると、 付属スクリプトの処理内容も理解しておく必要があります。


Wikipedia のダンプファイルからデータを抽出する

まずは 前回 と同様に wikiPageSelector を使って Wikipedia のダンプファイルからデータを抽出します。 前々回 紹介した トリプレット・データ を使って必要なページデータを選択できます。

ここでは wikipedia-hand-triplets-release.txt を使う 次のスクリプトを書きました。

$ cat ../run

../wikiPageSelector/wikiPageSelector \
../triplets-data/wikipedia-hand-triplets-release.txt \
~/Wikipedia/enwiki-20210420-pages-articles-multistream.xml > wikipedia-triplets-dump-20210420.xml
$ time ../run
real    111m21.582s
user    110m12.387s
sys 1m3.588s
$ 

手作業で作成されたはずの wikipedia-hand-triplets-release.txt は定義されている URL は比較的少数なのですが、 スクリプトが実行を終えるのに2時間弱かかりました。


スクリプトの実行

次に付属スクリプトwiki-xml-to-txt.py を実行しますが、 その前にちょこっと準備が必要です。

スクリプトを修正する

まず、スクリプトの入力となる3つのデータファイルの定義を修正します。 修正箇所は次のとおりです。

$ diff -u ../wiki-xml-to-txt.py.orig ../wiki-xml-to-txt.py
--- ../wiki-xml-to-txt.py.orig  2021-06-01 00:09:11.000000000 +0900
+++ ../wiki-xml-to-txt.py   2021-06-01 00:22:53.000000000 +0900
@@ -8,7 +8,7 @@
 import xmltodict

 filepaths = [
-   'hand/wikipedia-triplets-dump-20190918203353.xml'
+   'wikipedia-triplets-dump-20210420.xml'
 ]


@@ -66,7 +66,7 @@
 embeddings_batch = []
 embeddings = []
 old_tid2new_tid = dict()
-word_embeddings_path = "<PATH-TO-EMBEDDINGS>/GoogleNews-vectors-negative300.bin"
+word_embeddings_path = "GoogleNews-vectors-negative300.bin"

 # REFERENCE TO READ WORD2VEC BINARY FILE: https://gist.github.com/ottokart/4031dfb471ad5c11d97ad72cbc01b934
 FLOAT_SIZE = 4
@@ -117,7 +117,7 @@
        fp.write("%s %d\n" % (token, idx))

 # DUMP TRIPLETS IN INDEX FORMAT
-with open("hand/wikipedia-triplets-release.txt") as fp, open("wikipedia-triplets.txt", mode='w') as fp_out:
+with open("../triplets-data/wikipedia-hand-triplets-release.txt") as fp, open("wikipedia-triplets.txt", mode='w') as fp_out:
    for line in fp:
        if not line.strip() or line.startswith('#'):
            continue
$ 
実行に必要な Python パッケージをインストールする

次に wiki-xml-to-txt.py が使用している Python パッケージの nltk, numpy, xmltodict をインストールします。

$ pip install nltk
$ pip install numpy
$ pip install xmltodict
$ 

Python パッケージのインストール方法については 他の文献を参考にしてください。

スクリプトを実行する

スクリプトが正常に実行されると 次のように表示されます。

$ python ../wiki-xml-to-txt.py
FILE:  wikipedia-triplets-dump-20210420.xml
NUMBER OF URLS: 448
NUMBER OF TEXTS: 448
NUMBER OF TOKENS (RAW): 217549
NUMBER OF TOKENS (EMBEDDINGS): 128391
NUMBER OF TOKENS (EMBEDDINGS): 128391
NUMBER OF PAPERS: 448
$ ls
GoogleNews-vectors-negative300.bin
wikipedia-embeddings.txt
wikipedia-papers.txt
wikipedia-tokens.txt
wikipedia-triplets-dump-20210420.xml
wikipedia-triplets.txt
$ 

これでwikipedia-hand-triplets-release.txtに対応する データセットが生成できました*1


まとめ

以上、本稿では fastWMD を実行する環境を追ってきました。

文書間距離計算のエラー率と実行速度に注目する fastWMD 論文 の性質上、ベンチマークの核となる fast_wmd コマンドには 文書間距離計算のみが実装されているように見えますが、 Word Mover's Distance アルゴリズムは本来 比較する2つの文書(文字列)を引数に取り 文書間距離を返す関数として実装されるべきです。 fastWMD の実装において文書そのものを取り扱う処理は 付属するスクリプト wiki-xml-to-txt.py で実装しているように見えます。

Wikipedia ページを対象にした wiki-xml-to-txt.py の処理内容は概ね次のようです*2

  1. Wikipedia のダンプデータ(XML)からページデータを抽出しトークン化を行う
  2. トークン毎、ユニークなローカル ID を割り振る
  3. 既存の Word2Vec 学習済みデータからアクティブなトークンのベクトルのみ抽出して wikipedia-embeddings.txt に出力する
  4. アクティブなトークンの ID とトークンの対応表を wikipedia-tokens.txt に出力する
  5. トリプレット・データを読み込み URL → ID 変換を行なって wikipedia-triplets.txt に出力する
  6. Wikipedia ページデータをトークン → ID 変換を行なって、ID列を wikipedia-papers.txt に出力する


都合6回を費やし、ようやく fastWMD 論文 に伴う実装をひと通り把握することができました。 次回からは fastWMD の実装を流用して、 対話システムへの WMD の適用に挑戦したいと思います。

以上

*1:前々回 も紹介したように Dropbox からダウンロードできるのは トリプレット・データ wikipedia_2014_09_27_examples.txt から生成したデータセットのみです。

*2:次のページでは 一般的な自然言語処理における前処理について解説してます。

qiita.com

ご参考まで。

Word Mover's Distance(5)Wikipedia の Page データを抽出する

Word Mover's Distance (5) Extracting Page Data from Wikipedia XML Dump


2021/05/22
藤田昭人


前回 はトリプレットを紹介しました。 でも、実際に fastWMD を実行する際には トリプレット・データを取り込んでいるように見えないことに お気づきの方も多いかと思います。

本稿ではそのあたりのカラクリを説明した上で、 その前処理のために必要な Wikipedia のバックアップから Page データを抽出するツール wikiPageSelector を紹介したいと思います。


付属スクリプトで生成される4つのファイル

前回紹介した triplets コマンドの実行スクリプトを再掲します。

#!/bin/sh -x
../build/triplets \
--trip ../dataset/triplets/wikipedia/wikipedia-triplets.txt \
--docs ../dataset/triplets/wikipedia/wikipedia-papers.txt \
--emb ../dataset/triplets/wikipedia/wikipedia-embeddings.txt \
--verbose false --num_clusters 289 --max_iter -1 --func cosine --r 16

オプション trip, docs, emb として triplets, papers, embeddings の 3つのデータファイルがコマンドに引き渡されてますが、 いずれも数字が羅列されているテキストファイルです。 この3つのデータファイルと tokens ファイルは fastWMD のリポジトリに付属するスクリプト wiki-xml-to-txt.py で生成することができます。

このスクリプトを読んでみたところ、 次の4つの処理を実行していることがわかりました。

  1. 解析対象となるWikipediaページ(あるいは論文)をトークンに分解
  2. 句読点と意味のないトークン、いわゆるストップ・ワードを除外
  3. トークン毎にユニークな ID を設定
  4. トリプレットデータ、解析対象データ、埋め込みデータについて、トークンを ID に差し替えたファイルを生成

Wikipediaデータセットについて各ファイル毎に説明すると…

wikipedia-triplets.txt は 前回紹介したトリプレット・ファイルを ID に置き換えたファイルです。 トリプレット・ファイルは1行に3つの Wikipedia ページの URL を定義していますが、 各々のタイトルをトークンとする3つの ID に置き換えられています。

wikipedia-papers.txt は対象となる文章を ID 列に置き換えたもので、 各 Wikipedia ページの記述が使われています。 ストップ・ワードが除外されているので トークン列に逆変換しても意味不明でしょうが。

wikipedia-embeddings.txt は埋め込みデータ、 すなわち Word2Vec の学習済みデータです。 元データはトークン毎にベクトルが定義されていますが、 トークン部分が ID に置き換えられています。

wikipedia-tokens.txt は ID とトークンの対応表で、 fastWMD の実行には使われません。 このファイルを使うと ID からトークンへの逆変換が可能です。

この一連の手順は自然言語処理では 典型的な前処理だと思います。 本来 WMD では二つの文章を入力とするAPIが 用意されることが多いと思うのですが、 敢えてトークンにIDを割り当てて ベンチマーク・プログラムの外へ追い出してるのは、 文章間距離の計算速度に注目する fastWMD では 文字列操作によりコストのかかる前処理を 評価から除外するためなんじゃないかと想像してます。


スクリプトに埋め込まれている暗黙のデータ

コードに埋め込まれているので わかりにくいのですが、スクリプト wiki-xml-to-txt.py を実行するにはトリプレット・データ以外に 更に2つデータを用意する必要があります。

ひとつ目はWord2Vecの学習済みデータです。 スクリプトには次のような記述があります。

word_embeddings_path = "<PATH-TO-EMBEDDINGS>/GoogleNews-vectors-negative300.bin"

このファイル名を見れば ピンと来る方もいらっしゃるでしょ😁 これはオリジナルのWord2Vecのページで 紹介されている学習済みデータです。 以前 Word2Vecの記事 で紹介した次の Google のサイトから入手できます。

code.google.com

このページの Pre-trained word and phrase vectors あたりを見てください。 ダウンロードの方法が記述されています。

ふたつ目は解析対象となる文章データです。 スクリプトには次の記述を見ると Wikipediaのダンプファイルを そのまま取り込めるように見えます。

filepaths = [
    'hand/wikipedia-triplets-dump-20190918203353.xml'
]

そこで、Wikipedia英語版の 最新バックアップをダウンロードして来て 試してみたのですが… サイズが大き過ぎてズッコケました*1。 改めてWikipediaのダンプファイルを見てみると…

$ ls -l enwiki*
-rw-r--r--@ 1 fujita  staff  83108609305  5  6 23:25 enwiki-20210420-pages-articles-multistream.xml
-rw-r--r--@ 1 fujita  staff  19517540474  5  6 23:25 enwiki-20210420-pages-articles-multistream.xml.bz2
$ 

圧縮状態でも18.2GB、 解凍するとなんと77.4GBにもなる 超巨大ファイルです。 もちろん気づいてなかった訳ではないのですが、 内心「Python だからシレ〜っと動くかも?」 との淡い期待を抱いたのです。 期待はあっさりと打ち砕かれました。


Wikipedia のダンプファイルから Page データを抽出する

この種の、いわゆるビッグデータ処理は ある意味今どき感のあるプログラミングなんですけども、 例えて言えば「大型トラックで市街地をドライブする」ようなもので、 プログラミングは「曲がり角のたびに立ち往生する」といった感じになります*2。 やはり過去の経験どおり ベーシックなアプローチでないと乗り越えられないようです。

という訳で wikiPageSelector なる単純なCプログラムを書きました。 ソースコードgithub に置きましたので、 次のページからダウンロードしてください。

github.com

このプログラムは Wikipedia のダンプファイルから、 必要なPageデータだけを抜き出し、 サブセットの xml 形式として標準出力に出力させるだけのプログラムです。 「必要な Page データ」は「トリプレット・データに登場するページ 」と解釈しました。 第1引数にトリプレット・データ・ファイルのパスを、 第2引数に解凍後の Wikipedia のダンプファイルのパスを 指定します。

次に 最新のWikipedia英語版のダンプファイル を対象に time コマンドを噛ませて 抽出処理に要する時間を計測した例を示します。

$ time ./wikiPageSelector \
../triplets-data/wikipedia_2014_09_27_examples.txt \
~/Wikipedia/enwiki-20210420-pages-articles-multistream.xml 
・・・
21150000(99%): Still Life with Jupiter Tonans

real    344m41.938s
user    342m45.742s
sys 1m25.288s
$ time ./wikiPageSelector \
../triplets-data/wikipedia_2014_09_27_examples.txt \
~/Wikipedia/enwiki-20210420-pages-articles-multistream.xml 
・・・
21150000(99%): Still Life with Jupiter Tonans

real    339m57.163s
user    337m54.073s
sys 1m22.468s
$ 

ご覧のとおり、1回目が5時間44分、2回目が5時間39分かかりました。

標準出力をリダイレクトしてファイルに落とすと…

$ ls -l enwiki*
-rw-r--r--  1 fujita  staff  712146936  5 19 15:42 enwiki-20210420-pages-selected-1.xml
-rw-r--r--  1 fujita  staff  712146936  5 20 06:15 enwiki-20210420-pages-selected-2.xml
$ 

いずれも680MB程度の xml ファイルになりました。


まとめ

本稿では fastWMD 論文の評価実験を再現するための道具立てを調べて、 Wikipedia の Page データを抽出するツール wikiPageSelector を紹介しました。

正直、僕も論文の評価実験を再現すること自体には あまり意味を感じている訳ではないのですが、 明文化されてない評価実験のための準備作業の手順を追っていくことで、 WMDビッグデータを必要とするアルゴリズムであり、 その処理には非常に時間を要することが再確認できました。

システム、特に対話的なシステムに WMD を取り込むためには いろいろ工夫をしなければならないことが明らかになって良かったと思っています。 先端研究の成果を実用システムに応用するには 相応の二次的な研究開発が必要なんですねぇ。

以上

*1:実はこのあたりから雲行きが怪しくなって来ました😁

*2:これがブログの執筆スケジュールを破綻させた要因だったのですが😀

Word Mover's Distance(4)トリプレット(triplets)

Word Mover's Distance(4) triplets


2021/05/10
藤田昭人


本稿から数回に分けて トリプレット(triplets) について紹介します。

トリプレット(triplets) は fastWMD 論文の評価実験に登場します。 対話システムのコーパスとして 既存の書籍の文面を活用しようとしている僕には これ、非常に興味深い評価実験で 使用しているデータセットなど さらに深掘りしようと考えています。


fastWMD論文の2つの評価実験

一般に、 機械学習の研究では「データを学習」することが前提なので、 提案されているアルゴリズムのロジックを追うだけでは 妥当性の確認は難しいような気がしています。通常 「このデータにはこのアルゴリズムが有効」 といったようにデータとアルゴリズムがセットで提案されます。 それ故、この種の研究論文ではアルゴリズムのロジックの記述だけでなく 評価実験にはデータセットの詳細が記されています。

くだんの fastWMD 論文を再掲します。

arxiv.org

この論文では高速化WMDの評価として次の2つの実験結果を示しています。

Document classification via k-NN(K近傍法による文書分類)

この K近傍法 による文書分類の評価は
WMDのオリジナルである kusner論文 の実験を踏襲しています。

DATASET WCD WMD RWMD(S) RWMD(L) REL-RWMD(L)
20NEWS 1.87 6,244 842 68.0 13.4
AMAZON 0.30 351 71.7 22.1 4.47
BBCSPORT 0.01 21 3.72 1.38 0.27
CLASSIC 0.24 213 45.6 10.8 2.26
OHSUMED 0.47 1,002 158 25.1 5.20
RECIPE 0.09 106 27.6 2.13 0.55
REUTERS 0.27 181 47.7 10.9 1.67
TWITTER 0.04 3.32 1.23 0.43 0.18

実験ではkusner論文と全く同じデータセットを使用して、 同じようにエラーレートを評価していますが、 さらに各データセット毎にアルゴリズムを変えた実行時間の計測を追加しています。

Identifying related documents(関連文書の識別)

もうひとつの評価実験では Dai論文 の評価方法を踏襲しています。Dai論文は Paragraph Vector、すなわち 文書の分散表現 を評価した研究論文で、次の WikipediaarXiv の2つの文書類似性データセットを評価実験に用いています。

http://cs.stanford.edu/~quocle/triplets-data.tar.gz

Dai論文では Paragraph Vector を生成するアルゴリズムとして Le と Mikolovの論文、 いわゆる doc2vec を想定していたようですが、 そのライバルと目される WMD を使って Dai論文と同様の評価を行なったのが fastWMD 論文ということなのでしょう。


トリプレットとは?

前述の「文書類似性データセット」のデータをダウンロードします。 このアーカイブには3種類の トリプレット と README が収録されています。

$ tar tvzf triplets-data.tar.gz
-rw-r-----  0 adai   eng       938  7 11  2015 README
-rw-r-----  0 adai   eng       535  7 11  2015 LICENSE
-rw-r-----  0 adai   eng   2024923  6 24  2015 arxiv_2014_09_27_examples.txt
-rw-r-----  0 adai   eng   2971663  6 24  2015 wikipedia_2014_09_27_examples.txt
-rw-r-----  0 adai   eng     22746  6 24  2015 wikipedia-hand-triplets-release.txt
$ 

README を覗いてみると、 次のような トリプレット(triplets) に関する簡潔な説明が確認できます。

This is a dataset for evaluating document similarity models. In each file, each line consists of a triplet of URLs, either all from Wikipedia or all from arXiv.org. The triplets in the file 'wikipedia-hand-triplets-release.txt' were hand generated whereas the other two files were generated automatically be examining Wikipedia and arXiv.org document categories. The content of URLs one and two should be more similar than the content of URLs two and three.

これはドキュメントの類似性モデルを評価するためのデータセットです。 各ファイルでは、各行は3つのURLで構成されており、すべてWikipediaからのURLか、すべてarXiv.orgからのURLです。 ファイル 'wikipedia-hand-triplets-release.txt' の トリプレット は手動で生成されましたが、 他の2つのファイルは、WikipediaarXiv.orgのドキュメントカテゴリを調べて自動的に生成されました。 URL1 と URL2 の内容は、URL2 と URL3 の内容よりも類似している必要があります。

補足すると…

例えば 'wikipedia-hand-triplets-release.txt'の(1行目はコメントなので) 2行目は(スペースで区切られて)次の3つのURLが定義されています。

各 URL は Wikipedia 英語版のページですが、 'Deep learning' と 'Machine learning' の関係の方が 'Deep learning' と 'Computer network' の関係よりも意味的に類似していますよね? このように「URLで指定される3つの文書の意味的な関係の定義」を Dai論文では トリプレット と呼んでいるようです。


fastWMD用の Wikipedia データセット

fastWMD では Dai 論文の「文書類似性データセット」から 4種類のデータファイルを生成して*1入力データとして使います。 もっとも、生成されたデータファイルは 以下の Dropbox のサイトからダウンロードが可能です。

www.dropbox.com

Wikipedia のデータセットは 'wikipedia.tar.gz' です。 その tarball の中身は次のとおり。

$ tar tvzf wikipedia.tar.gz
-rw-rw-r--  0 mwerner_local mwerner_local 337304  9 25  2019 wikipedia-triplets.txt
-rw-rw-r--  0 mwerner_local mwerner_local 6195900  9 25  2019 wikipedia-tokens.txt
-rw-rw-r--  0 mwerner_local mwerner_local 303509985  9 25  2019 wikipedia-papers.txt
-rw-rw-r--  0 mwerner_local mwerner_local 2609813322  9 25  2019 wikipedia-embeddings.txt
$ 

この4つのデータファイルがあれば、 前回 生成した triplets コマンドが実行できます。


triplets コマンドの実行

fastWMD用の Wikipedia データセットを使って 前回 ビルドした triplets コマンドを実行してみます。 fastWMD論文を参考に次のスクリプト triplets-cosine.sh を作成しました。

#!/bin/sh -x

../build/triplets \
--trip ../dataset/triplets/wikipedia/wikipedia-triplets.txt \
--docs ../dataset/triplets/wikipedia/wikipedia-papers.txt \
--emb ../dataset/triplets/wikipedia/wikipedia-embeddings.txt \
--verbose false --num_clusters 289 --max_iter -1 --func cosine --r 16

このスクリプトはビルドツリーを仮定してますが、 Github のfastWMDのビルドツリー にもスクリプトを追加しましてありますので参考してください。


まとめ

本稿では トリプレット(triplets) の解説を軸に、 Dai論文の「文書類似性データセット」、および それから生成された fastWMD 用データセットについて 紹介しました。

前述のスクリプトを実行すると、 実行時間が一番早いはずの cosine でも 実行終了まで結構待たされますが、 実は wikipedia-embeddings.txt は 以前紹介した Word2Vec の学習済みデータです。 大規模なベクトルデータなので ロードするのに時間がかかります。

以降、しばらくは Wikipedia 用データセットに注目して fastWMD の挙動を調べて行きたいと思います。

以上



*1:データファイルを生成するために、fastWMD では arXiv 向けの parser_arxiv_papers.py と、Wikipedia 向けの wiki-xml-to-txt.py の2種類のPythonスクリプトが用意されています。 詳細はのちほど。

Word Mover's Distance(3)fastWMD のビルド(後編)

Word Mover's Distance (2) Building fastWMD (Part 2)


2021/05/06
藤田昭人


前回 に引き続き fastWMD のビルドのサクッとした説明を続けます。

とりあえず Github の fastWMD のページを再掲しておきましょう。

github.com


build-project.shを実行する

前回の手順で Google OR-ToolsEigen3 はインストール済みであれば、あと1箇所修正すれば build-project.sh を実行できます。

CMakeLists.txt を修正する

修正が必要なのは CMakeLists.txt で、 コンパイル時のヘッダーファイルのパスに /usr/local/include/eigen3 を追加してやります。 次に fastWMD の CMakeLists.txt を示します。

cmake_minimum_required(VERSION 3.5)
project(fast_wmd)

set(CMAKE_CXX_STANDARD 11)

add_executable(fast_wmd src/main.cpp src/Tools.cpp src/Tools.hpp src/Clustering.cpp src/Clustering.hpp src/Distances.cpp src/Distances.hpp)

# Add ortools
find_library(ORTOOLS_LIB ortools)
target_link_libraries(fast_wmd "${ORTOOLS_LIB}")

# Enable parallelization
# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fopenmp")

# Disable vectorize for fair comparison between distance methods
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-tree-vectorize -DEIGEN_DONT_VECTORIZE=1")

# Add optimization parameter
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3")

見てのとおり非常に短いファイルです。 cmake を使ったことのない方には記法の違いに面食らうかもしれませんが、 よく見れば Makefile と同じ考え方で修正すれば良いようです。 ここでは fastWMD が無事コンパイルできるように 前述のヘッダーファイルのパスを追加する修正を示します。

$ diff -u CMakeLists.txt.orig CMakeLists.txt
--- CMakeLists.txt.orig 2020-07-23 09:10:25.000000000 +0900
+++ CMakeLists.txt  2021-05-04 18:02:12.000000000 +0900
@@ -16,4 +16,7 @@
 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-tree-vectorize -DEIGEN_DONT_VECTORIZE=1")

 # Add optimization parameter
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3")
\ No newline at end of file
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3")
+
+# Add local include for eigen3
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I/usr/local/include")

見てもらえばわかるように、コンパイル時のフラグを定義するマクロ CMAKE_CXX_FLAGS に '-I/usr/local/include' を追加してるだけです。

fastWMD のビルド

これで build-project.sh が正常に実行できるようになりました。 ビルドのログを次に示します。

$ sh -x build-project.sh
+ mkdir build
+ cd build
+ cmake ..
-- The C compiler identification is AppleClang 12.0.5.12050022
-- The CXX compiler identification is AppleClang 12.0.5.12050022
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/fujita/xtr/BookBot/WMD/WMD-3/fastWMD/build
+ make
[ 20%] Building CXX object CMakeFiles/fast_wmd.dir/src/main.cpp.o
[ 40%] Building CXX object CMakeFiles/fast_wmd.dir/src/Tools.cpp.o
[ 60%] Building CXX object CMakeFiles/fast_wmd.dir/src/Clustering.cpp.o
[ 80%] Building CXX object CMakeFiles/fast_wmd.dir/src/Distances.cpp.o
[100%] Linking CXX executable fast_wmd
[100%] Built target fast_wmd
+ cd ..
$ 

ビルド用の build ディレクトリを作成して、移動し、cmake を実行しています。 これで build ディレクトリ以下にコンパイル用の Makefile が生成されます。 あとは make を実行するだけ。無事 fast_wmd コマンドが生成されました。


kusner コマンドと triplets コマンドを作ってみる

前回も説明しましたが、fast_wmd コマンドは論文 "Speeding up Word Mover’s Distance and its variants via properties of distances between embeddings" の評価パートを書くために、用意されたデータに 文書間距離を計算するさまざまアルゴリズムを一括で適用して、 実行時間とエラーレートだけを表示するバッチ型のプログラムです。

アルゴリズム自体は Distance.cpp/Distance.hpp に実装されていて、 それを呼び出す独自のプログラムをいろいろ作りたいわけですが、 そのためのビルド環境は流用したいところ。 そこで試しに fast_wmd コマンドを2種類の評価実験の 各々に対応する kusner コマンドと triplets コマンドを 作成してみました。両コマンドのソースコード kusner.cpp, triplets.cpp を 含むビルドツリーは Github の次のページにアップしました。

github.com

ここでは CMakeLists.txt の修正点だけ示します。 add_executable と target_link_libraries を追加すると kusner コマンドと triplets コマンドがビルドされるようになります。

$ diff -u ../../fast-wmd/fast-wmd-master/CMakeLists.txt CMakeLists.txt
--- ../../fast-wmd/fast-wmd-master/CMakeLists.txt   2021-05-04 18:02:12.000000000 +0900
+++ CMakeLists.txt  2021-05-05 13:35:15.000000000 +0900
@@ -5,15 +5,22 @@

 add_executable(fast_wmd src/main.cpp src/Tools.cpp src/Tools.hpp src/Clustering.cpp src/Clustering.hpp src/Distances.cpp src/Distances.hpp)

+add_executable(kusner src/kusner.cpp src/Tools.cpp src/Tools.hpp src/Clustering.cpp src/Clustering.hpp src/Distances.cpp src/Distances.hpp)
+
+add_executable(triplets src/triplets.cpp src/Tools.cpp src/Tools.hpp src/Clustering.cpp src/Clustering.hpp src/Distances.cpp src/Distances.hpp)
+
 # Add ortools
 find_library(ORTOOLS_LIB ortools)
 target_link_libraries(fast_wmd "${ORTOOLS_LIB}")
+target_link_libraries(kusner "${ORTOOLS_LIB}")
+target_link_libraries(triplets "${ORTOOLS_LIB}")

 # Enable parallelization
 # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fopenmp")

 # Disable vectorize for fair comparison between distance methods
 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-tree-vectorize -DEIGEN_DONT_VECTORIZE=1")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-writable-strings")

 # Add optimization parameter
 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3")


まとめ

本稿では CMakeLists.txt の修正法のトピックを 中心に fastWMD のビルド方法について紹介しました。

かつて、X Window System の imake や GNU の autoconf や automake のお世話になっていた 僕のようなオールドユーザーにとって cmake は、 マニュアル を眺めていると多機能ぶりに「ちょっと面食らう」ツールですが、 「基本は Makefile機械的に生成してくれるコマンド」 と理解すると、少し取っ付きやすく感じます。

次回は、せっかく作った triplets コマンドを使って、 もう少し WMD に踏み込んだトピックを紹介したいと考えてます。

以上

f:id:Akito_Fujita:20210503123933p:plain

Word Mover's Distance(2)fastWMD のビルド(前編)

Word Mover's Distance (2) Building fastWMD (Part 1)


2021/05/03
藤田昭人


前回、 概要を紹介したので、 続けて fastWMD のビルドについてサクッと手短に説明したいと思います。 実はいろいろ試行錯誤があったのですが、 そのあたりを端折って手順をつらつらと書き出してみたところ、 思いのほか長くなったので前後編に分けました。

実際、fastWMD の実装は論文の評価パートのためのコードのようで、 データ採りに必要最低限の内容しか含まれていない印象です。 README等で示されている手順に補足説明や追加作業を加えておきます。


まずはソースコードのダウンロード

fastWMD のソースコードGithub からダウンロードできます。

github.com

ちなみに Python のラッパーも収録されたバージョンは こちら をご覧ください。

インストール環境について

次に、インストール環境について。fastWMD の README.md によれば、次の環境で動作を確認したとのこと。

  • Ubuntu 16.04 and 18.04
  • Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz,with 8 GB of RAM

ちなみに以降の作業は次の環境で実行しました。

しかし Windows 等、他のプラットフォームでも 概ね同じ手順でインストールできると思います。 たぶん(笑)

インストールの手順を確認すると install_dependencies.shbuild-project.sh を順次実行するだけなのですが、 試してみたところ install_dependencies.sh でズッコケました。 本当に Ubuntu だけ インストールできる仕様になっているようです。


install_dependencies.sh の手順を手作業で実行する

確認したところ install_dependencies.sh では、fastWMD のビルドに必要な Google OR-ToolsEigen3 をインストールしているだけでした。 どうやらディレクトリ構成やセキュリティ・チェックなど 環境の違いがズッコケる原因のようなので、 パッケージを手作業でインストールします。

Google OR-tools

Google OR-tools はその名のとおり オペレーションズ・リサーチ関連、 以前紹介した lpSolve と同じカテゴリのパッケージのようです。 WMDに必要な最適輸送問題を解くアルゴリズムを 呼び出しているのでしょう。

このパッケージ、C++だけでなく、PythonJavaC# もサポートしているようですが、 必要なのは C++ です。実は当初はバイナリ配布のインストールを試みたのですが macOS Big Sur のセキュリティ・チェックで弾かれました。 (Google が配布しているので、今はこの問題を回避できるのかもしれませんが…) やむなくソースコードからインストールをしました。手順は以下のページです。

developers.google.com

この手順説明は macOS での C++ プログラムのビルド環境の 一般的な設定についても丁寧に説明しているので、 もし C++ プログラムを動かしたことがなければ ソースコードからのインストールの実行をお勧めします。 (どのみち fastWMD のビルドの際に設定をしなければなりません)

Eigen3

Eigenは、線形代数、行列およびベクトル演算、幾何学的変換、 数値ソルバーとそれに関連するアルゴリズムのための テンプレート・ヘッダーによる高水準 C++ ライブラリです。 おそらく fastWMD ではベクトル表現や基本演算のために使っているのでしょう。 ソースコードは次のページからダウンロードできます。

eigen.tuxfamily.org

で、ソースツリーを眺めていたら CMake 関連のファイルを見つけたので 「テンプレート・ライブラリなのに?」 と思っていたら Eigen の INSTALL メモには次のような記述がありました。

Eigen consists only of header files, hence there is nothing to compile before you can use it. Moreover, these header files do not depend on your platform, they are the same for everybody.

Eigen はヘッダファイルのみで構成されていますので、使用前にコンパイルする必要はありません。さらに、これらのヘッダファイルはプラットフォームに依存しません。プラットフォームに依存せず、誰でも同じものが使えます。

…ということなので、配布ファイルを展開して しかるべきヘッダーファイルのパスに入れることにしました。 実はちょっと試行錯誤があったのですが、 次の手順でOKなようです。

$ tar xzf eigen-3.3.9.tar.gz
$ sudo mv eigen-3.3.9 /usr/local/include/eigen3
$ 


まとめ

以上、fastWMD のビルドにまでは至ってませんが、 記事的な分量のキリが良いので、本稿はここまで。 間をあけずに後編をアップしますので、 よろしくお願いします。

以上



f:id:Akito_Fujita:20210503123933p:plain

Word Mover's Distance(1)fastWMD ー 高速化の試み

Word Mover's Distance(1) fastWMD


2021/04/29
藤田昭人


本稿では、論文 "Speeding up Word Mover’s Distance and its variants via properties of distances between embeddings" を眺めながら Word Mover's Distance (WMD) の高速化の試みをザックリと紹介したいと思います。


Word2Vec に思うこと

まずは Word2Vec について… 2013年に論文発表されたWord2Vec。 正直に告白すると当時は 「これ、いったい何に使えるんや?」 と思ったものです*1。 冒頭の論文では、Word2Vec の価値(そして意義)について次のように語られています。

数年前までは、意味的な関係(semantic relations)を取得する適切な方法がなかったので、 意味的な関係はほとんど利用されていませんでした。 それ故、研究者たちはこの問題を軽減する方法として、オントロジーを使用する決心をしましたが、 これはアプリケーションが外部の知識ベースに依存することを意味していました。 このシナリオが変わったのは,Word2Vec とその亜種が登場してからです。

この Word2Vec で分析できるとされる 「意味的な関係」が、 人工知能研究が始まって以来の命題である 「知能」に対する理解を 大きく変えつつあると僕は実感しています。

文章の類似性を測る WMD にとって その文章を構成する単語の類似性を測る Word2Vec は 必須のアルゴリズムですが、 同時に WMD は Word2Vec の有用性を 具体的に提示する重要なアプリケーション (のひとつ)でもあるように思います。


WMDの高速化の試み

実際「よく似た内容の文章を見つける」という 類似文書検索は日常的に役立つ アプリケーションでしょう。 その機能的な核となる文章間の類似度を 高い精度で計測できるWMDアルゴリズムは 需要が高い。それ故に登場した当初から 指摘されていた「分析に時間がかかる」という 課題には多くの研究者がチャレンジして来ました。 昨年(2020年)発表された冒頭の論文は、 WMD の Earth Mover's Distance (EMD) の部分、 すなわち以前紹介した最適輸送問題の解法に着目した高速化手法を扱っています。 参考文献には次の4本の先行研究も掲載されていて、 高速化はWMDが登場した2015年以来 毎年のように成果が発表される、 今もホットな研究領域のように見えます。

年号 論文
2015 From word embeddings to document distances
2017 Linear-complexity relaxed word mover’s distance with gpu acceleration
2018 Word mover’s embedding: From word2vec to document embedding
2019 Linear-complexity data-parallel earth movers distance approximations
2020 'Speeding up Word Mover’s Distance and its variants via properties of distances between embeddings'

そもそも高速化問題とは、 最適輸送問題の解法 が輸送元と輸送先の組み合わせを総あたりで計算することに起因します。 WMDに適用した場合には、その計算量は扱う語数の3乗に比例するそうで、 対象とする文書が大きい過ぎると「いつまで経っても終わらない」計算になります。

この問題は2015年のオリジナルのWMD論文でも指摘されていて、 WMDと同じ精度でもっと高速に計算する手法が提案されました。 冒頭の論文は2015年以来の先行研究を網羅した上で、 独自の手法を提案しています。


fastWMD ー C++ による WMD 実装

僕が、冒頭の論文に注目するもうひとつの理由は、 提案の評価に使われた(先行研究も含む) C++ による WMD 実装が Github で公開されていることです。

github.com

もちろん「実装コードがある」というのはありがたいことですが、 特に fastWMD では先行研究の成果であるアルゴリズムも実装されているので コードレベルで直接比較できることは大変ありがたいです。 ザッと調べてみたところ fastWMD に実装されているアルゴリズムは オリジナルの Word Mover ’s Distance (WMD) に加えて次の3つです。

  • Relaxed Word Mover ’s Distance (RWMD)
  • Linear-Complexity Relaxed Word Mover’s Distance (LC-RWMD)
  • Related Relaxed Word Mover ’s Distance (Rel-RWMD)

RWMD はオリジナルの論文で言及されていた高速化手法で、 最適輸送問題の制約条件をカットすることにより計算量を削減するアイデアです。 LC-RWMD は前処理を追加して計算量が語数に比例するように組み替えた高速化手法で、 並列化が容易になるよう配慮されている(?)反面、計算精度が低下するらしい。 Rel-RWMD は与えられた単語に対して 関連性がある単語グループと関連性がない単語グループに分ける 閾値を決めて計算の枝刈りをする高速化手法で、 Word2Vec では対象となるデータセット内での単語埋め込み間の意味的距離は 次のようなグラフを描く特性を活用しているそうです。

f:id:Akito_Fujita:20210429220216p:plain
Amazonのデータセットに含まれる全ての単語埋め込みから、単語 “cat” までの距離

Word2Vecを説明した記事 では distance コマンドを紹介しましたが、 その表示でグラフを描くとこのようなカーブを描くようですね。 Rel-RWMD の弱点は閾値は単語ごとに異なる可能性があることでしょうか?


まとめ

本稿では冒頭の論文を軸に fastWMD がサポートする WMDの高速化手法についてザックリと紹介しました。 この他にもWMDの高速化手法はたくさん提案されているようですが、 やはり実装がある fastWMD から手を付けるべきでしょう。

僕は対話システムへの応用を考えているので、 必ずしも大規模な文書が想定される訳ではないように思います。 いずれの手法が有効なのか?は対話システムの設計次第でしょう。 個々の高速化手法の詳細が気になるところですが、 以後、まずは fastWMD のビルド方法から説明することにします。

以上



*1:その僕がまさか 記事 を書くとはね。

Word Mover's Distance(WMD)

Word Mover's Distance


2021/04/27
藤田昭人


僕は一度気が削がれると なかなか元の状態に戻れないヤツなのですが…
とある キッカケ を掴んでブログ執筆を再開することができました。

ここからの数回は Word Mover's Distance (WMD) について取り上げます。

BookBot の紹介をして以来、 Word2VecEarth Mover's Distance と対話機能の説明からドンドン離れて行くので 「迷走している」 と感じている方もいらっしゃるかもしれませんが、 この WMD の説明で「なるほど」と納得してもらえる…つもりです(笑)


WMD って何?

WMDは、類似文書検索(あるいは 概念検索 ともいう)、つまり 「文書検索で任意の文を与えると、 その文と意味的に近い文を検索する」 ためのアルゴリズムなんだそうです。

言い換えれば 「2つの文の意味的な距離を測る」 アルゴリズムなんですが…

  1. 文を単語単位に分解し、
  2. Word2Vec を使って、各々の単語毎に「単語の意味的な距離」を測り、
  3. EMD を使って、単語間の意味的距離を合算する

…のがWMD、つまり直感的には WMD = Word2Vec + EMD と説明すると分かり易いでしょう。

実は、コーパスに書籍を使うBookBotでは、応答する場合に 「質問文と意味的に近い文を書籍の中から探す」 必要があるため、このアルゴリズムに着目しました。


WMDに関する「論文」的な話

WMD は2015年に発表された論文 "From Word Embeddings To Document Distances" が初出なようで、ごく最近登場したアルゴリズムのようです。

この論文を読むには類似文書検索などの 統計的自然言語処理の基礎知識」 が必要で、特に「テキストを行列に変換する」 ベクトル空間モデル単語埋め込み、 単語分散表現など (これ全部同じことを意味する用語のようなんですが) が基礎になっています。

WMDをザッと理解したい場合には 前述の論文の日本語による概要説明が役に立ちます。

yubessy.hatenablog.com

この概要説明でも次の定番のオバマ元大統領に関する例文が登場します。

Sym Sentance
D0 'The President greets the press in Chicago.'
D1 'Obama speaks to the media in Illinois.'
D2 'The band gave a concert in Japan.'


この3つの例文の間の意味的距離の計算方法を図示したのが次の図です。

f:id:Akito_Fujita:20210423195546p:plain

この図を見ると Word2Vecで求めた単語どうしの意味的距離輸送問題の解法で集約して 文の間の意味的距離を計算していることが直感的に理解できます。


WMDをちょっと試してみる

WMD を手軽に試してみるには Python を使うのが一番楽です。
次のブログ記事では Python を対話的に使って WMD を計算する手順を説明しています。

hironsan.hatenablog.com

Python3 を使ってこの手順どおりにWMDを計算してみました。

$ python3
Python 3.9.1 (v3.9.1:1e5d33e9b9, Dec  7 2020, 12:10:52) 
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from gensim.models.keyedvectors import KeyedVectors
from gensim.models.keyedvectors import KeyedVectors
>>> model = KeyedVectors.load_word2vec_format('./GoogleNews-vectors-negative300.bin', binary=True)
model = KeyedVectors.load_word2vec_format('./GoogleNews-vectors-negative300.bin', binary=True)
>>> model.most_similar(positive=['japanese'])
model.most_similar(positive=['japanese'])
[('japan', 0.6607722043991089), ('chinese', 0.6502295732498169), ('Japanese', 0.6149078607559204), ('korean', 0.6051568984985352), ('german', 0.5999272465705872), ('american', 0.5906798243522644), ('asian', 0.5839767456054688), ('san', 0.5834755897521973), ('jap', 0.5764404535293579), ('swedish', 0.5720360279083252)]
>>> sent1 = 'But other sources close to the sale said Vivendi was keeping the door open to further bids and hoped to see bidders interested in individual assets team up.'.split()
sent1 = 'But other sources close to the sale said Vivendi was keeping the door open to further bids and hoped to see bidders interested in individual assets team up.'.split()
>>> sent2 = 'But other sources close to the sale said Vivendi was keeping the door open for further bids in the next day or two.'.split()
sent2 = 'But other sources close to the sale said Vivendi was keeping the door open for further bids in the next day or two.'.split()
>>> distance = model.wmdistance(sent1, sent2)
distance = model.wmdistance(sent1, sent2)
>>> print(distance)
print(distance)
0.8738126733213613
>>> d0 = 'The President greets the press in Chicago.'.split()
d0 = 'The President greets the press in Chicago.'.split()
>>> d1 = 'Obama speaks to the media in Illinois.'.split()
d1 = 'Obama speaks to the media in Illinois.'.split()
>>> d2 = 'The band gave a concert in Japan.'.split()
d2 = 'The band gave a concert in Japan.'.split()
>>> dist01 = model.wmdistance(d0, d1)
dist01 = model.wmdistance(d0, d1)
>>> dist02 = model.wmdistance(d0, d2)
dist02 = model.wmdistance(d0, d2)
>>> print(dist01)
print(dist01)
1.968269276774589
>>> print(dist02)
print(dist02)
2.2929445610887638
>>> quit()
quit()
$ 

ここではブログ記事と同じ手順で、 前述の3つの例文の意味的距離も計算しています。 D0-D1 間の距離が 1.968269276774589 に対し、 D0-D2 間の距離は 2.2929445610887638 となりました。 Word2Vecの学習済みデータが異なるので、 前述の図とは値が異なりますが、 D0-D1 間の距離の方が D0-D2 間の距離よりも近いことがわかります。


まとめ

本稿ではWMDについて大変ザックリと説明しました*1

やはり Python は Gensim などパッケージが充実しているので、 今どきの統計的自然言語処理のちょっとした実験には大変便利な言語です。 日本語による解説記事もたくさんありますしね。

でも、さまざまなパッケージが高度にインテグレートされているので、 アルゴリズムの中身を調べようとすると骨が折れると言うのが僕の印象です。 とにかく覚えなければならないことが多いので、正直ウザい(笑) そもそも "Simple is Beautiful" の世代の僕には、 C言語のような「必要最小限」アプローチがフィットしているようです。

以降の解説では敢えて Python を使わないアプローチで WMD に迫ってみたいと考えてます。

以上

*1:前回 語ったように「週3回更新」が可能か?試してみる気になりました。
10分〜15分で読み切れるように 1回の記事の分量を調整したつもりですが、 いかがだったでしょうか?