単語をベクトル化するword2vec(gensim)を使い、指定した二単語間の関連度を算出する

word2vec

2014年から2015年辺りに流行った、単語をベクトル化して評価する手法。 有名なのは、

king – man + woman = queen

学習データとなるコーパスを準備する

無料かつ簡単に手に入るWikipediaのdumpファイルから持ってきます。

https://dumps.wikimedia.org/jawiki/latest/ の jawiki-latest-pages-articles.xml.bz2 をダウンロードします。

xmlファイルのままでは使えないので、 これをwp2txtを使ってplain.txtに変換します:

$ gem install wp2txt
$ wp2txt jawiki-latest-pages-articles.xml.bz2

ファイルが大量に作成されるので、次のように連結します:

$ cat jawiki-latest-pages-articles.xml-* > jawiki.txt

word2vecでは、単語ごとにスペースで区切られている必要があるので、日本語形態素解析器であるMecabを使って分かち書きします。

まずは、Mecabと標準辞書(IPA)をインストールします:

$ brew install mecab mecab-ipadic

さらにmecab-ipadic-NEologdというカスタム辞書をインストールします:

$ git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
$ cd mecab-ipadic-neologd
$ ./bin/install-mecab-ipadic-neologd -n

Mecabと辞書のインストールが完了しました。まずはカスタム辞書のインストール先を調べます:

$ echo `mecab-config --dicdir`"/mecab-ipadic-neologd"
/usr/local/lib/mecab/dic/mecab-ipadic-neologd

Mecabの -d オプションにこのパスを指定して、分かち書きを行います:

$ mecab -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd \
-Owakati jawiki.txt > jawiki_wakati.txt

これで学習データの準備が整いました。

word2vecの実装

参考

  • https://radimrehurek.com/gensim/models/word2vec.html
  • http://rare-technologies.com/word2vec-tutorial/
  • http://tjo.hatenablog.com/entry/2014/06/19/233949
  • http://sucrose.hatenablog.com/entry/2013/10/29/001041

Pythonのgensimを使って、word2vecを使用します。cythonを入れると学習時間が短縮されるみたいです。

$ easy_install gensim numpy scipy
$ pip install cython

まずは、学習のためのスクリプトを記述、実行します:

train.py

# -*- coding: utf-8 -*-
from gensim.models import word2vec
import logging

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

sentences = word2vec.Text8Corpus('jawiki_wakati.txt')

model = word2vec.Word2Vec(sentences, size=200, min_count=20, window=15)

model.save("jawiki_wakati.model")
$ python train.py

あまりにも時間がかかりすぎる場合はファイルを分割します。最終的に100MBほどに分割して学習させました。

これでモデルができました。ちょっと動作を確かめてみましょう。

similarity.py

# -*- coding: utf-8 -*-
from gensim.models import word2vec
import logging
import sys

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

model = word2vec.Word2Vec.load("jawiki_wakati.model")
argvs = sys.argv
print model.similarity(argvs[1], argvs[2])

これを実行します。

$ python similarity.py 日本 フィリピン
2016-09-09 21:57:52,064 : INFO : loading Word2Vec object from jawiki_wakati.model
2016-09-09 21:58:03,569 : INFO : loading syn0 from jawiki_wakati.model.syn0.npy with mmap=None
2016-09-09 21:58:03,956 : INFO : loading syn1 from jawiki_wakati.model.syn1.npy with mmap=None
2016-09-09 21:58:04,573 : INFO : setting ignored attribute syn0norm to None
0.262511504266

学習量が少ないので正確性にはかけるのでしょうが、ちゃんと単語類似度を返しています。

APIとして使う

ほかのプログラミング言語から使用しやすいように、APIとして使えるようにしてみます。

フレームワークにはFalconを使用、WSGIサーバーのgunicornも一緒にインストールします。

$ pip install falcon gunicorn

簡単に、2つのトークンを受け取って、類似度をJSONで返すAPIを作成します:

server.py

# -*- coding:utf-8 -*-
import json
import falcon
from gensim.models import word2vec
import logging

class Server(object):

    def __init__(self):
        self.logger = logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
        self.model = word2vec.Word2Vec.load("jawiki_wakati.model")

    def on_get(self, req, res):

        s1 = req.get_param('s1')
        s2 = req.get_param('s2')

        content = {
            "similarity": self.model.similarity(s1, s2)
        }

        res.body = json.dumps(content)

app = falcon.API()
app.add_route("/", Server())

サーバーを動かしてみます:

$ gunicorn server:app

次のようにGETリクエストを送ると、JSONで返ってきます:

$ curl "127.0.0.1:8000?s1=日本&s2=フィリピン"
{"similarity": 0.26251150426566316}

Docker

デプロイしやすいようにDockerで動かしてみます。

現在の環境はこれ:

Dockerファイルを追加:

Dockerfile

FROM python:2.7.9

RUN pip install --upgrade pip

WORKDIR /gensim-api
COPY requirements.txt /gensim-api
RUN  pip install -r requirements.txt
COPY . /gensim-api

管理が楽なのでDocker Composeを使用:

docker-compose.yml

api:
    image: gensim-api
    command: gunicorn --reload -b 0.0.0.0:5000 server:app
    volumes:
        - .:/gensim-api
    ports:
        - "5000:5000"

次のコマンドでDockerイメージを作成し、コンテナを起動します:

$ docker build -t gensim-api .
$ docker-compose up

最後に

上記のように、gensimを使えば簡単に単語の類似度が算出できることが分かりました。学習データの準備さえ乗り越えれば、あとはどうってことないと思います。

ソースコードはこちら:

https://github.com/uraway/gensim-api

今のところ、学習量は100MBなんですが、続けて学習させることも可能っぽいので、適度に更新しておきます。

単語の類似度以外にもいろいろ出来そうです。

Atom linter-rubocop でのエラー

linter-rubocop 0.5.0

Error: /Users/uraway/.rvm/rubies/ruby-2.3.1/lib/ruby/site_ruby/2.3.0/rubygems.rb:270:in `find_spec_for_exe': can't find gem rubocop (>= 0.a) (Gem::GemNotFoundException)
    from /Users/uraway/.rvm/rubies/ruby-2.3.1/lib/ruby/site_ruby/2.3.0/rubygems.rb:298:in `activate_bin_path'
    from /Users/uraway/.rvm/gems/ruby-2.3.1/bin/rubocop:22:in `<main>'
    from /Users/uraway/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `eval'
    from /Users/uraway/.rvm/gems/ruby-2.3.1/bin/ruby_executable_hooks:15:in `<main>'
    at /Users/uraway/.atom/packages/linter-rubocop/lib/index.coffee:57:15
    at process._tickCallback (internal/process/next_tick.js:103:7)

linter-rubocopが参照しているrubocopの場所がどうもおかしい。

とりあえずlinter-rubocopのドキュメントを見ると、解決法が:

rubocopのパスを調べて

which rubocop
/Users/uraway/.rvm/gems/ruby-2.3.1/bin/rubocop

bin/wrappers/に置き変えて、linter-rubocopのexecutablePathにペースト:

/Users/uraway/.rvm/gems/ruby-2.3.1/wrappers/rubocop

エラーはでなくなった。

"KeyError: key not found"

問題

Railsにて、新たに設定した環境変数が機能しない:

myproject/config/initializers/constants.rb:13:in `fetch': key not found: "CONSTANT" (KeyError)

原因

RailsのPreloaderであるspringを入れていたため、環境変数が更新されていなかった。

解決法

$ ./bin/spring stop

Python: プロジェクト別に依存モジュールの管理

requirements.txtのみで管理するのは複雑すぎて無理。 pip-toolsを使って楽に管理しよう。

pip-tools

インストール

$ pip install --upgrade pip
$ pip install pip-tools

pip-compile

requirements.inにプロジェクトに使用するモジュールのリストを作る:

gensim==0.9.1
numpy
scipy
falcon
gunicorn

次のようにコマンドを実行すると、依存モジュールのリストrequirements.txtが作成される:

$ pip-compile requirements.in
#
# This file is autogenerated by pip-compile
# To update, run:
#
#    pip-compile --output-file requirements.txt requirements.in
#
falcon==1.0.0
gensim==0.9.1
gunicorn==19.6.0
numpy==1.11.1
python-mimeparse==1.5.2   # via falcon
scipy==0.18.0
six==1.4.1                # via falcon

pip-sync

requirements.txtに書いてあるとおりに、依存モジュールを環境にインストール・アップグレード・アンインストールする。 npm installbundle installのようなもの。

開発用モジュールを分けて管理しているときは同時に指定する:

$ pip-sync dev-requirements.txt requirements.txt

UNIX系OSでの .gz ファイルの展開コマンド

たまに使おうとすると忘れていたりするのでメモ

.gz ファイルの展開

$ gunzip file.gz
$ gzip -d file.gz

もともファイルを保持しつつ展開

$ gunzip -k file.gz
$ gzip -dk file.gz

.gz ファイルを展開せずに標準出力

$ gzip -dc file.gz
$ gzcat file.gz

プログラミングを学び始めて10ヶ月経ったけど、フィリピンでWebエンジニアやってます

要約

フィリピンでエンジニアするワケ

  • 新人でも仕事を任せてもらえるから
  • 新興国で働くこと魅力

ブログ

プログラミングに初めて触ったのが去年の10月終わりごろでした。ブログも同時期に勉強の記録として始めたので、 その頃の記事も残っていますが、見直してみるとひどいものです。

例えばこの記事なんて、 コードスニペットすら正しく記述できていないという有様(直してもいない)。



さて、ここ1ヶ月全くブログ記事を書いていません。というのも、タイトル通り、1ヶ月前からフィリピン・セブ島で、 Webエンジニア・インターン生として働き始めて、忙しくしており、ブログ記事を書く時間が取れないからです。

自分の価値を高める

なぜフィリピンでWebエンジニアなのか。

もちろん仕事の縁があったというのが一番の理由ではありますが、「自分の価値」を高めることの出来る環境に身を置きたかった、というのも理由のひとつです。

当たり前の話ですが、フィリピンには日本人エンジニアはほとんどいません。でも、フィリピンにいる日本人エンジニアにしか出来ない仕事というのも確かにあって、 僕のようなプログラミングを学び始めてほんの数ヶ月の若造にも仕事はたんまりとあるのです。

つまり、日本では有象無象駆け出しプログラマーの僕でも、ありがたいことにフィリピンでは一人前のプログラマーとしての仕事が存在します。 これは日本のスタートアップに入っても同じことが言える場合がありそうですね。

相対的に価値が高まって、仕事をもらえるというだけでなく、仕事をこなすことで自分の能力を高め、さらに価値が高まる。 そういうスパイラルをひそかに期待していたりします。

仕事がいっぱいあるからといって、眠る時間を惜しんでまで働くというようなことはしていません。 結構自由に働かせてもらっていることもあり、特にストレスが溜まることもなく、気楽といえば気楽な仕事です。 起床時間によって出勤時間も異なるので、周りからはいい加減な奴だと思われているかもしれませんが、まあ否定はしません。

職場として、生活の場としてのセブ島

リゾートでもなく、語学留学でもない、働く場として、このフィリピン・セブ島を見ると、僕が感じたのはまず、思っていたより悪くない、ということです。 僕が日本の職場環境を知らないので、どこがどう違うと細かく比較は出来ませんが、ごくたまにネットが遅くなる以外に、業務に支障はありません。

他には冷房がキツすぎるぐらい。お腹を冷やすと壊しやすいのでこれは由々しき問題です。

生活の場として、セブ島を見ると、これは僕も日本との比較ができるので、色々と不満はあります。例えば、南国なのでゴキブリがやたらイカツイだとか、 トイレにペーパーを流すと詰まるだとか、水道水を(シャワーや歯磨きで)口にするとたまに当たるだとか。

こうした不満はお金で解決できるのかもしれませんが、解決したらしたで、どうせまた世界最高水準の生活環境を誇る日本と比較してしまって、 新たな不満が湧き出てくるに違いない。仕事があって、三食しっかり食べることができて、熱いシャワーを浴びることができて、ベッドで眠ることが出来る、 それで十分だと思うようになりました。

日本とフィリピン

今現在の日本社会は、高度経済成長期に若者だった人々を主人公にしたゲームの、エンディング後の世界なんじゃないだろうかと思います。 今の日本の世界最高水準の生活環境と、老人への手厚い保障は、彼らへのゲームクリア後のご褒美なんだろうと。それを僕ら若者は、おこぼれにあずかっているだけなのだと。

対して、新興国は、ゲームを始めて少し経って、今まさにがんがん成長しています。しかし状況は依然不安定で、ゲームをクリアするか、ゲームオーバーになるか、この先どう転ぶかは誰にも分からない。

それぞれの状況を比べてみた時、生まれた時から不景気な社会で過ごしてきた僕にとって、どちらが面白いことが多く起きるだろう?そう考えると、フィリピンのような新興国で働く魅力みたいなものが見える気がします。

若者全員が海外で一度は働くべき、英語を勉強すべき、なんて非現実的なことだと思いますし、それが全員に必要なことだとは絶対に思いません。

行きたい人だけが行けばいい。英語も学びたい人が学べばいい。適材適所。

ずっと日本にいたほうがやはり安定しますし、もしかしたら最終的には後悔しない人生を送れるかもしれませんしね。

ただ、僕は行きたいから、面白そうだから、行く。それだけ

Word Cloud + kuromoji + Processing で日本語文章をビジュアライズ

完成品:

Word Cloud

文章中で出現頻度に重みを付け、その重みに応じた大きさで単語群を図示する手法。

Processing で行うには WordCram が手っ取り早く、デザインも良い。

いかにして日本語文章中の単語を抜き出すか

WordCram に何の手も加えていない日本語文章を読み込ませたところで、日本語単語への分かち書き方法を知らないため、不自然な Word Cloud が出来上がる。

そこで、日本語解析器である kuromoji を使い、対象文章を解析し、名詞を読みこませることにした。

下準備

WordCram

さて、まずは外部ライブラリである WordCram を 手元の Processing にインストールする。

  • こちらから Processing 3.0 に対応した WordCram 1.0.0 の zip ファイルをダウンロードし、解凍する。
  • Processing の sketchbook フォルダを開く。(Processing > Preferences > Sketchbook location)

  • そのフォルダ内の libraries に、解凍した WordCram フォルダをコピーする。

基本的な使い方は次の通り:

import wordcram.*;

// Set up the Processing sketch
size(1000, 600);
colorMode(HSB);
background(230);

// Make a wordcram from a random wikipedia page.
new WordCram(this)
  .fromWebPage("https://en.wikipedia.org/wiki/Special:Random")
  .withColors(color(30), color(110),
              color(random(255), 240, 200))
  .sizedByWeight(5, 120)
  .withFont("Copse")
  .drawAll();

kuromoji

kuromoji は Java 用のライブラリなので少し手を加える必要がある。

  • こちらから zip ファイルをダウンロードし解凍する。
  • lib フォルダ内に kuromoji-0.7.7.jar ファイルがあるので、これを kuromoji.jar にリネーム。
  • sketchbook フォルダの librarieskuromoji/librariesを作成し、先ほどの kuromoji.jar ファイルを配置する。

基本的な使い方は次の通り:

import org.atilika.kuromoji.Token;
import org.atilika.kuromoji.Tokenizer;
import java.util.*;

Tokenizer tokenizer = Tokenizer.builder().build();
List<Token> tokens = tokenizer.tokenize("吾輩は猫である。");

for (Token token : tokens) {
    println("PartOfSpeech:" + token.getPartOfSpeech());
    println("surfaceForm:" + token.getSurfaceForm());
    println("baseForm:" + token.getBaseForm());
}

出力:

PartOfSpeech:名詞,代名詞,一般,*
surfaceForm:吾輩
baseForm:吾輩
PartOfSpeech:助詞,係助詞,*,*
surfaceForm:は
baseForm:は
PartOfSpeech:名詞,一般,*,*
surfaceForm:猫
baseForm:猫
PartOfSpeech:助動詞,*,*,*
surfaceForm:で
baseForm:だ
PartOfSpeech:助動詞,*,*,*
surfaceForm:ある
baseForm:ある
PartOfSpeech:記号,句点,*,*
surfaceForm:。
baseForm:。

日本語文章を Word Cloud でビジュアライズ

これら2つのライブラリを活用して、ホッテントリのフィードを解析、Word Cloud を作成する。

String URL = "http://feeds.feedburner.com/hatena/b/hotentry";
XML[] items;
String contents;
PrintWriter output;
String words;

import wordcram.*;
import org.atilika.kuromoji.Token;
import org.atilika.kuromoji.Tokenizer;
import java.util.*;

void setup() {
  size(1000, 600);
  colorMode(HSB);
  background(230);

  XML xml = loadXML(URL);
  items = xml.getChildren("item");
  for(int i = 0; i < items.length; i++){
    contents += items[i].getChild("title").getContent();
    contents += items[i].getChild("description").getContent();
  }

  Tokenizer tokenizer = Tokenizer.builder().build();
  List<Token> tokens = tokenizer.tokenize(contents);
  for (Token token : tokens) {
    String[] partOfSpeech = token.getPartOfSpeech().split(",");
    if (partOfSpeech[0].equals("名詞") && !token.getSurfaceForm().equals("こと")) {
      words += token.getSurfaceForm() + "\n";
    }
  }

  output = createWriter("words.txt");
  output.println(words);
  output.close();

  drawWordCloud();
}

void draw() {

}

void mousePressed() {
  drawWordCloud();
}

void drawWordCloud() {
  println("creating a new word clound");
  background(230);
  new WordCram(this)
    .fromTextFile("./words.txt")
    .withColors(color(30), color(110),
                color(random(255), 240, 200))
    .sizedByWeight(5, 120)
    .withFont("Copse")
    .drawAll();
}

結果はこんな感じ:

WordCram にかける前にある程度意味のない頻出単語をフィルタしたほうが良い。今回は Processing での正規表現がわからなかったので、!token.getSurfaceForm().equals("こと") のように kuromoji で弾くようにしている。