タグ別アーカイブ: python

gensimとword2vec

何かと話題のword2vecですが、自分も使ってみようと思って試行錯誤したことを書いてみます。間違いなどあればぜひコメントください。

GoogleのC実装

https://code.google.com/p/word2vec/にある、C言語による実装は独立したアプリケーションで、Apache 2.0ライセンスで提供されています。

ビルドした状態でword2vecコマンドを使って学習、モデルを作成し、distance等のコマンドからモデルをもとにデータを出力します。以下はword2vecを引数なしで実行したときの実行例です。

./word2vec -train data.txt -output vec.txt -size 200 -window 5 \
 -sample 1e-4 -negative 5 -hs 0 -binary 0 -cbow 1 -iter 3

各オプションの意味は以下になります。

  • train: 学習対象のテキストデータ(コーパス)
  • size: 次元数(デフォルトは100)
  • window: skip-gramの対象とする単語の最大数を指定(多分)デフォルトは5
  • sample: 単語を無視する際の頻度の閾値(デフォルトは1e-3)
  • threads: 処理するスレッド数
  • hs: Hierarchical Softmax(階層的Softmax)を使う(1)/使わない(0)
  • negative: ネガティブサンプリングに用いる単語数
  • binary: 出力結果をバイナリ形式にする(1)/テキスト形式(0)
  • cbow: Continuous Bag-of-Words 連続単語集合モデルを使う(1)/使わない(0)
  • iter: 学習する回数(イテレーション)の指定。デフォルトは5

CBOWとSkip-gramについてはFiS Projectの記事がわかりやすいです。階層的SoftmaxとネガティブサンプリングについてはnishioさんによるQiitaの記事が詳しいです。

gensim(Python)実装

gensimはPythonとCを組み合わせて記述された、文書解析ソフトウェアパッケージです。ライセンスはLGPL3+で、ソースはhttps://github.com/piskvorky/gensimで公開されています。

もともと複数の計算モデルを扱えるようになっていたようなのですが、2013年9月ごろにword2vecが追加されたようです。webで紹介される実装はこちらの方が多いようです。

Google実装はサンプル実装という面が強く、コマンドからできることに限りがあります。それに比べるとgensimはできることが多い(任意の数の単語のpositive/negativeな演算ができる)のですが、モデル構築にメモリをより使用します。

1GBのテキストをコーパスとしてgensimのword2vecに与えたところ、メインメモリ8GBのマシンでスラッシングを起こしてしまいました。Google実装であれば、1GB程度のテキストはオンメモリで処理できるようです。また、POSIX threadによるマルチスレッド処理にも対応しています。

幸いなことに、gensimのWord2VecではGoogle実装のモデルを読み込ませることができます。大きなデータを扱う際には、これらを組み合わせるとよい感じです。

from gensim.models import word2vec
model = word2vec.Word2Vec.load_word2vec_format("model.bin", binary=True)

pythonで処理したモデルもファイルに保存することができます。

from gensim.models import word2vec

model = word2vec.Word2Vec.load_word2vec_format("model.bin", binary=True)
model.save("gensim-model.bin")
#model.load("gensim-model.bin")

gensimでは、trainメソッドで後から追加学習させることもできるのですが、既に構築したモデルに存在しない単語は扱えない(語彙が増やせない)ようです。これに気付かず、分割したテキストデータをちょっとづつ学習させるというようなことをやらせようとしてはまりました。

結局、大きなコーパスを取り扱う際にはC実装でモデルを構築して、それをgensimで読み込むという使い方をするのが良いようです。

その他参考にした資料

追記(2015/5/20)

gensimでword2vecの処理がシングルスレッドでしかできないような表現でしたが、Word2Vecの引数にworker=2等とすることでマルチコア動作することに気付きました。それでも処理速度はGoogle実装の方が早いとは思いますが、記録として残しておきます。

OpenCVのテンプレートマッチング

3DSのプチコン3号をどうにかしたいという目標を持ったので、3DSタッチパネルディスプレイに表示されるソフトウェアキーボードを、OpenCVを使って位置を認識させる、ということを試してみました。

まずはあらかじめ、キーボードの写真を撮っておきます。撮影にはNexus5の内蔵カメラを使いました。画像が若干傾いていたので、撮った写真をgimpで加工し、ほぼ長方形になるよう調整しました。

プチコンキーボード

この画像から、キーを一つ切り出します。とりあえず「1」のキーを切り取ってみました。

petit-kbd-1

キーボードの全体画像からこの「1」のキーを検出する方法として、テンプレートマッチングを行ってみます。OpenCVのサイトにそのものずばりのC++サンプルが掲載されています。自分はどこかのページで見たコードを参考に以下のようなものを作成しました。

#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>

int
main(int argc, char **argv)
{
    cv::Mat search_img = cv::imread("../petitcom-kbd-fix.png", 1);
    cv::Mat tmp_img = cv::imread("../petit-kbd-1.png", 1);

    cv::Mat result_img;
    cv::matchTemplate(search_img, tmp_img, result_img, CV_TM_CCOEFF_NORMED);

    cv::Rect roi_rect(0, 0, tmp_img.cols, tmp_img.rows);
    cv::Point max_pt;
    double maxVal;
    cv::minMaxLoc(result_img, NULL, &maxVal, NULL, &max_pt);
    roi_rect.x = max_pt.x;
    roi_rect.y = max_pt.y;

    std::cout<< "(" << max_pt.x << "," << max_pt.y << ") score="
             << maxVal << std::endl;
    cv::rectangle(search_img, roi_rect, cv::Scalar(0, 0, 255, 3));

    std::cout << "rectangle: (" << roi_rect.x << ", " <<
        roi_rect.y << ") size (" << roi_rect.width << ", " <<
        roi_rect.height << ")" << std::endl;

    cv::imwrite("output.png", search_img);
    return 0;
}

search_imgを対象画像とし、tmp_imgにマッチする領域をtemplateMatch関数で算出します。アルゴリズムの選択には、特に何も考えずサンプルのまま(CV_TM_CCOEFF_NORMED/Collection coefficient)にしました。これにより、演算結果がresult_imgに代入されます。

その結果から、minMaxLoc関数を用いてresult_imgから値が最大となる位置を算出します。cv::Matは行列データ型であり、minMaxLocは行列内の最大値、最小値を持つ座標とその値を取得する関数です。今回の場合、必要なのは最大値の座標だけです。

サンプルプログラムではrectangle関数で該当する矩形領域に線を引いています。

自分はC++に不慣れなので、同じことをPythonでやってみようとしました。いろいろと試行錯誤の結果、以下のようなコードになりました。

import cv2

src = cv2.imread("../petitcom-kbd-fix.png", 1)
tmp = cv2.imread("../petit-kbd-1.png", 1)

res = cv2.matchTemplate(src, tmp, cv2.TM_CCOEFF_NORMED)

(minval, maxval, minloc, maxloc) = cv2.minMaxLoc(res)

print "({0}, {1}) score = {2}\n".format(maxloc[0], maxloc[1], maxval)

(h, w, d) = tmp.shape

rect_1 = (maxloc[0], maxloc[1])
rect_2 = (maxloc[0] + w, maxloc[1] + h)
print "({0}, {1}) score = {2}\n".format(maxloc[0], maxloc[1], maxval)
print "size ({0}, {1})\n".format(w, h)
cv2.rectangle(src, rect_1, rect_2, 0x00ff00)

cv2.imwrite("py-output.png", src)

rectangleに与えるべき引数がC++とはちょっと異なります。C++では矩形の起点と高さ+幅を与えるのに対し、Pythonでは2つの対角上の頂点の座標を指定します。最初この違いに気付かなくて、思うような結果にならず悩みました。

今回はスケールが同一の画像でのテンプレートマッチングだったので期待通りの結果を得ることができましたが、実際の利用をする場合には異なるサイズや、若干傾いた画像、他に余計なものが写りこんでいる画像などを相手にする必要があるので、もっといろいろな下処理などが必要になるものと思われますが、まずは第一歩を進めることができました。