無印吉澤

Site Reliability Engineering(SRE)、ソフトウェア開発、クラウドコンピューティングなどについて、吉澤が調べたり試したことを書いていくブログです。

機械学習初心者が『Python 機械学習プログラミング』(速習コース)を読んだメモ

f:id:muziyoshiz:20160924124311p:plain

きっかけ

機械学習の重要性は、それこそ「ビッグデータ」という言葉が出てきた頃からいろいろな人が訴えていますが、最近は特にツールが充実して、敷居が下がってきたように感じています。

そろそろ自分でも機械学習関係のツールを使えるようになりたいと思っていたのですが、そんなときに「具体的なコード例が多くて読みやすい」という本書の評判を聞いて、読み始めました。

Python機械学習プログラミング 達人データサイエンティストによる理論と実践 (impress top gear)

Python機械学習プログラミング 達人データサイエンティストによる理論と実践 (impress top gear)

まだ、ツールを使いこなせるレベルには全然届いていないのですが、試したことを記録しておかないと忘れてしまいそうなので、一旦メモをまとめておきます。

この記事を書いた人のレベル

私のレベルはこんな感じです。

  • 大学は情報科学科で、線形代数とか勉強したはずなのに、数学のことはほとんど忘れている
  • プログラミング言語の知識はあるが、Python のコードは書いたことがなく「ブロックをインデントで表現するやつ」程度の認識

今回の読書プラン

読み始める前に書評を色々検索して読んでたのですが(気後れしていたとも言う)、そのなかに、翻訳の福島さん自身が、本書の読み方をアドバイスする記事がありました。

thinkit.co.jp

この記事の3ページ目で、「速習コース」と「特訓コース」という2通りの読書プランを紹介されています。「全部読まなくて良い」というので少し気が楽になり、まずは速習コースに従って一通り読みました。

f:id:muziyoshiz:20160924124421p:plain:w600
速習コースのフロー
f:id:muziyoshiz:20160924124447p:plain:w600
特訓コースのフロー

Python 環境の構築

本書にコード例がふんだんに含まれており、すべて 本書の GitHub (rasbt/python-machine-learning-book) で公開されてます。しかも Jupyter Notebook の .ipynb ファイルで公開されているので、Web ブラウザ上からすぐに実行できます。今風ですね。

github.com

そこで、まずは Jupyter Notebook を動かすために必要なソフトをインストールします。Python 本体はもちろん、科学計算向けのライブラリもいくつかインストールする必要があります。

本書にはインストール方法はあまり書かれておらず、『Python機械学習プログラミング』学び方ガイド で紹介されている データサイエンティストを目指す人のpython環境構築 2016 - Qiita にも、Mac での手順はあまり詳しく書かれていませんでした。つまづいたポイントがいくつかあるので、自分の環境でのインストール手順をメモしておきます。

インストール先の環境

今回のインストール先は以下の通りです。次の MacBook Pro が出たら OS を乗り換えよう、と思っていたら、未だに Yosemite を使ってしまってる始末……。

  • OS X Yosemite 10.10.5
  • MacBook Pro (Mid 2014)

Anaconda (Python 3) のインストール

本書にも「科学計算に特に推奨したい Python ディストリビューションの1つに Anaconda がある」と書いてあったので、Anaconda をインストールしました。Mac に元々入ってる Python は使いません。

Mac上にデータ分析で使用するAnacondaをインストールし、IDEを起動するまで - Qiita を参考に、pyenv を入れてから、その上に Anaconda をインストールします。

Homebrew が入っていれば、以下のコマンドで pyenv がインストールされます。

% brew install pyenv

ただ、この状態で、

% pyenv install --list | grep anaconda

を実行したところ Anaconda 4.0.0 までしか選択できませんでした。Anaconda のダウンロードページ を確認したところ、その時点での最新版は Anaconda 4.1.1 だったのに……。

で、ここ最近 brew update を実行してなかったことに気づき、以下のコマンドを実行。

% brew update && brew upgrade pyenv

これで Anaconda 4.1.0 まで選択できるようになりました。最新ではないのが若干気になりますが、まあ Homebrew 対応に時間差があるのだろうと考えて、以下のようにインストール。デフォルトで Anaconda が使われるように設定しました。

% pyenv install anaconda3-4.1.0
% pyenv global anaconda3-4.1.0
% pyenv rehash

そして、Anaconda のなかでのパッケージ管理に使われる conda をアップデート。何かの記事を参考に実行したと思うのですが、これは必要なかったかもしれません(参考元ページを探し直したのですが見つからず)。

% conda update conda

科学計算に関するライブラリのインストール

本書 p.14 には、以下のパッケージが必要と書かれていました。

  • NumPy
  • SciPy
  • scikit-learn
  • matplotlib
  • pandas

これらを以下のコマンドでインストールします。元々 Anaconda に同梱されているものもありましたが、最新版にアップデートされました。

% conda install numpy scipy scikit-learn matplotlib pandas

サンプルの実行に必要なライブラリのインストール

本書を読みながらコード例を実行したところ、12〜13章のコードを実行するのに、以下のパッケージも必要になりました。

  • Theano
  • Keras

これらも conda install theano keras でインストールできると思いきや、インストールできませんでした。anaconda search -t conda theano を実行しろ、というメッセージが表示されますが、osx-64 向けに個人がビルドしたと思われる Threano が43個も見つかる始末。

最終的には Installing Theano — Theano 0.8.2 documentation の手順に従って、以下のコマンドを実行してインストールしました。

% pip install Theano

Theano のページには conda install pydot-ng も実行するように、と書いてあるのですが、私の環境では以下のエラーが出てインストールできませんでした。ただ、これをインストールしなくてもサンプルは動いたので、そのまま使っています。

% conda install pydot-ng
Using Anaconda Cloud api site https://api.anaconda.org
Fetching package metadata .......
Solving package specifications: ....

The following specifications were found to be in conflict:
  - pydot-ng
  - python 3.5*
Use "conda info <package>" to see the dependencies for each package.

Keras のほうは以下のコマンドでインストールできました。

% pip install keras

ただ、このインストール後にサンプルコードを実行すると、

from keras.utils import np_utils

の行で、ImportError: No module named 'tensorflow' というエラーが発生してしまいました。サンプルコードで TensorFlow は使ってないはずなんですが……。TensorFlow のダウンロードページ を参考に、以下のコマンドで TensorFlow をインストールしたところ、このエラーは出なくなりました。

% conda install -c conda-forge tensorflow

あと、サンプルの実行に必須ではないですが、notebook の先頭でソフトウェアのバージョン番号を表示したい場合は watermark のインストールも必要です。

% pip install watermark

サンプルの実行

サンプルは GitHub で公開されているので、git clone でコピーします。

% git clone https://github.com/rasbt/python-machine-learning-book.git

あとは jupyter notebook で Jupyter Notebook を起動し、code ディレクトリ以下にある ipynb ファイルを開けばコードを実行できます。

Jupyter Notebook の使い方については、本書の付録A「Jupyter Notebook の基本的な使用方法」と、Jupyter事始め - Qiita を参考にしました。メニューバーから Cell → Run All で動く、ということさえわかれば普通に使えると思います。私は、コードが書かれたセルを選んだ状態でないと Run Cells を押しても何も起こらない、ということに気づくのに結構時間がかかりました……。

それ以外の方法としては、jupyter console で CLI を開いて、ここにコードをコピーして実行するのも良いと思います。変数の中身を1個ずつ確認しながら実行するのは、GUI よりも CLI のほうが楽な気がしました。

例えば、サンプルコードを実行してると、これは Python 標準のオブジェクトなのか、科学計算ライブラリ固有のオブジェクト(例えば NumPy 固有の、多次元配列を表す ndarray など)なのか、知りたくなることがありました。そういうときは、コンソールで変数名の後に "?" を付けると、以下のようにオブジェクトの説明が表示されて便利です。

% jupyter console
Jupyter Console 4.1.1


In [1]: from sklearn import datasets

In [2]: iris = datasets.load_iris()

In [3]: X = iris.data[:, [2, 3]]

In [4]: X?

Type:            ndarray
String form:
[[ 1.4  0.2]
           [ 1.4  0.2]
           [ 1.3  0.2]
           [ 1.5  0.2]
           [ 1.4  0.2]
           [ 1.7  0.4]
           [ 1.4  0.3]
           [ 1.5 <...>  1.9]
           [ 5.9  2.3]
           [ 5.7  2.5]
           [ 5.2  2.3]
           [ 5.   1.9]
           [ 5.2  2. ]
           [ 5.4  2.3]
           [ 5.1  1.8]]
Length:          150
File:            ~/.pyenv/versions/anaconda3-4.1.0/lib/python3.5/site-packages/numpy/__init__.py
Docstring:       <no docstring>
Class docstring:
ndarray(shape, dtype=float, buffer=None, offset=0,
        strides=None, order=None)
(以下、ndarray の docstring)

サンプルコードを実行していて引っかかったところ

速習コースのサンプルコードを実行していて、ライブラリのインストール不足以外では、以下のポイントでエラーが出ました。DeprecationWarning もいくつか出ましたが、そちらについては今回は触れません。

3章

Notebook の冒頭で以下のエラーが出ました。

DistributionNotFound: The 'sklearn' distribution was not found and is required by the application

これはパッケージ名の記載ミスのようで、

%watermark -a 'Sebastian Raschka' -u -d -v -p numpy,pandas,matplotlib,sklearn

を以下に書き換えたところ、エラーが出なくなりました。ちなみに、他の章の notebook は元々こうなってました。

%watermark -a 'Sebastian Raschka' -u -d -v -p numpy,pandas,matplotlib,scikit-learn

12〜13章

以下の場所で FileNotFoundError が出ます。

X_train, y_train = load_mnist('mnist', kind='train')
print('Rows: %d, columns: %d' % (X_train.shape[0], X_train.shape[1]))

git clone でファイルをコピーしていれば、code/datasets/mnist ディレクトリに gz ファイルがあるので、このディレクトリを丸ごと code/ch12/mnist(または code/ch13/mnist)にコピーし、gunzip で解凍しておけば OK です。

Python の勉強

コードを一通り動かしてみたものの、コードを読んでいるうちに

  • このコードはどこまでが Python の標準機能で書かれているのか?
  • やたらたくさんライブラリを使っているが、どの部分がどのライブラリの機能なのか?
  • というかこのやたら出てくるコロンは何なの?
  • カギ括弧のなかに for だの in だの書いてあるのは何なの?

などなど、機械学習の理論よりもコード自体のほうが気になってくる始末(逃避とも言う)。で、本屋に行って Python の本をいくつか眺めてみて、読みやすそうだった「入門 Python 3」を買ってきました。

入門 Python 3

入門 Python 3

サンプルコードに関する疑問点は、これの1〜7章と、付録C「科学におけるPy」を一通り読み終わったあたりで、ある程度解決できました。例えば以下のようなあたり。

  • 多次元配列にカンマ区切りの a[1, 2] でアクセスできるのは、Python 標準のリストではなくて、NumPy の ndarray を使ってるから。標準のリストを使っていたら a[1][2] のようにアクセスする必要がある。
  • a[:] のようにコロンを使ってるのは Python 標準の「スライス」。list[start:end:step] という文法でリストの一部を取得できる。返り値に end の要素は含まれない。
  • カギ括弧のなかに for だの in だのあるのは Python 標準の「リスト内包表記」。for 文でイテレータを回した結果をリストで返す、ということを短縮形で書いているだけで、for 文の入れ子も書ける。

1〜7章だけなら読むのに1日もかからない分量なので、Python 初心者にはお薦めです。それと、付録Cに ipython3 コマンド(jupyter console コマンド)の便利機能の紹介があったのは、個人的には非常に助かりました。

あとは、本書に載っていた参考 URL(下記)も読まなきゃ、と思いつつ、こちらはまだほとんど読めてません。

速習コースを読んでみた感想

説明が平易に書かれており、図も豊富なので、数式を読み飛ばしても、何ができるのかはなんとなく理解できました。また、Jupyter Notebook でコードを動かしながら確認できるので、数式だけ追うよりは、だいぶ気楽に読めました。

Python は科学計算関係のライブラリが充実しているとは聞いていましたが、短いコードでこんなにたくさんのことができるのか、と驚きました。例えば、3章に、

y_pred = ppn.predict(X_test_std)
print('Misclassified samples: %d' % (y_test != y_pred).sum())

というコードがあって、この (y_test != y_pred).sum() ってなんだ?と思ったら、これは2つの行列を比較した結果を bool の行列として取得し、さらに True の要素数を数えて返してるんですね。短いコードでこんなにたくさんのことができるのか、と驚きました。

ただ、一通り読み終えたものの、理解が中途半端なので、現状はこんな感じです。

  • いろいろな手法が紹介されているが、どういうときにどの手法を使えばよいかが、まだよくわからない
  • 実際の業務に使うとしたら、数式の部分をどこまで理解していれば十分なのか不安

なにか、身近の現実的な問題にツールを適用して、ツールにもう少し慣れたいところです。あと、最近職場でこの本の読書会をやろうという話が出てるので、何度か読み返して理解を深めたいと思います。

とりあえずの目標は、scikit-learn のサイトにある Machine Learning Map の範囲を、ツールとして一応使えるレベルかな……。

f:id:muziyoshiz:20160926132822p:plain:w800

Rails 5 で艦これアーケードのプレイデータ管理ツール "Admiral Stats" を開発中

f:id:muziyoshiz:20160828002458p:plain:w600

開発のきっかけ

このブログでゲームのことは書いたことなかったと思いますが、個人的には、長い期間かけてチマチマやるゲームが好きで、ここ数年は Ingress と艦これをやってます。

今年の4月には、艦これのアーケード版(艦これアーケード)もリリースされて、これも週1くらいのペースでゲーセンに通ってプレイしてたりします。

この艦これアーケードは筐体がネットワーク接続されており、自分のプレイデータをあとから SEGA の公式サイト で閲覧できるようになってます。このサイトで結構細かいデータまで見られるのですが、見られるデータはアクセス時の最新状態のみです。

このプレイデータを過去の分まで記録して時系列データとして可視化したら面白そう

と思いついたのと、

どうせ作るなら6月末にリリースされた Ruby on Rails 5 でも使ってみようか

ということで、この夏休みを使ってプレイデータ管理ツールを作ってみました。今回の記事は、このツール "Admiral Stats" の開発中間報告です。

2016-09-03追記

この記事の公開後、9/3にサービスリリースしました。Twitter アカウントでログインして使えます。ぜひお試しください。

www.admiral-stats.com

艦これアーケードとは?

艦これを全く知らない人向けに説明すると、艦これアーケードとは、艦娘と呼ばれるキャラのカードを集めて、選りすぐりのデッキを作成し、ステージを攻略していくアクションゲームです。ステージをクリアするたびに、ランダムで新たなカードが排出されます。

どのステージでどのカードが出やすいか、などのカード排出に関する法則性は全く公開されていないため、「自分(たち)が試したらこうだった」という情報が Wiki などで頻繁にやりとりされています。この法則性をつかむために(あるいは自分の不運をなぐさめるために)プレイデータを記録している人も多いと思います。

ちなみに、サービス開始直後は何時間も待たないとプレイできないほど人気でしたが、最近は少し待てばプレイできる程度に空いてきています。秋にゲーム内イベントがあるらしいので、それまでは空いてるんじゃないでしょうか。

Admiral Stats とは?

今回開発した Admiral Stats は、この艦これアーケードのプレイデータを可視化するサイトです。SEGA 公式のプレイヤーズサイト が対応していない、時系列での可視化に対応しています。

Ingress を知っている人なら Agent Stats の艦これアーケード版」 という説明が一番分かりやすいと思います。実際、Agent Stats からの連想で Admiral Stats を作ることを思いつきましたし、名前も Agent Stats からの連想で付けました*1

f:id:muziyoshiz:20160828004335p:plain
Agent Stats の画面例

Admiral Stats の画面サンプル

サンプル 1:カードの入手履歴

Admiral Stats にまずログインすると、最近のプレイで入手したカードの一覧が表示されます。ずっとプレイしていてカードが増えてくると、「あれ、このカードって前にゲットしたっけ? 今日が初めてだっけ?」とわからなくなってくるのですが(自分はそうでした)、そういう場合を想定した機能です。

f:id:muziyoshiz:20160828003035p:plain

サンプル 2:カードの入手数・入手率のグラフ

カードの種類(ノーマル、レアなど)ごとの入手数、入手率のグラフです。Admiral Stats の内部に各カードのリリース時期のデータを登録してあるため、入手率は減少することもあります。

f:id:muziyoshiz:20160828003052p:plain

サンプル 3:レベル・経験値のグラフ

艦娘のレベル・経験値だけでなく、艦種(駆逐艦とか)や艦隊全体の累計レベル・経験値も表示できます。

f:id:muziyoshiz:20160828003101p:plain

サンプル 4:カード入手状況の一覧表示

公式サイトでも見られる情報なのですが、Admiral Stats では情報量を絞る代わりに、1ページにまとめて表示します。

f:id:muziyoshiz:20160828003114p:plain

データのアップロード方法

艦これアーケードの公式サイトは、残念ながら、プレイデータのダウンロード機能を提供していません。ただ、このサイトはとても綺麗に作られていて、プレイデータはすべて API 用の URL から JSON で取得し、Web ブラウザ側で画面を描画しています。

そのため、今回はこの JSON をそのままファイルに出力する admiral_stats_exporter というエクスポートツールを作りました。このツールが出力した JSON ファイルを Admiral Stats にアップロードすると、上記のサンプルのような画面が表示されます。

f:id:muziyoshiz:20160828002458p:plain:w600

Admiral Stats へのログイン方法

メールアドレスの管理をしたくなかったので、Twitter アカウントでログインする方法を採用しました。 Admiral Stats から SEGA のサイトに直接アクセスすることはないので、SEGA ID などの登録は必要ありません。

Admiral Stats の公開予定

実装は一通り終わりました。ローカルの仮想マシンで動かせば、自分1人で使う分には実用的に使えています。

ただ、どうせなら元ネタの Agent Stats の 「全ユーザとの比較」ページ のように統計情報を表示できると、もっと面白くなるんじゃないかと思ってます。Agent Stats ではレベルや経験値の分布、プレイ傾向がわかる指標(攻撃重視か構築重視か、など)の分布が公開されています。艦これの場合、レアカードの所有率の分布とかでしょうか。

そこで他のユーザのデータもアップロードしてもらえるように、Admiral Stats を設置したサイトを公開するための準備中です。ただ、以下のような作業がまだ残っていて、公開できるのは1〜2週間先になる見込みです。

  • サーバのレンタル
  • SSL 証明書導入(Let's Encrypt)と HTTPS 対応
  • デプロイ自動化スクリプトの作成(場当たり的に開発環境を作ったので、必要な手順や設定を整理できてなくて……)
  • production 設定での動作確認
  • 最低限のテスト

もし、Admiral Stats を使ってみたい方は、admiral_stats_exporter で事前にプレイデータをエクスポートしておいてください。ただ、こちらはあくまで非公式のツールなので、リンク先の説明を理解したうえで、利用は自己責任でお願いします。

あと、このエクスポータは突貫で実装したツールなので、使いづらいのはご容赦ください……。本当は Agent Stats のように、スマホだけでエクスポートからインポートまで完結できると良いと思うんですけどね。そこまで手が回りませんでした。

Admiral Stats についての紹介はここまでで、これ以降は Rails 5 での実装に関する細かい話です。

実装の詳細

最近は PHP や Java で Web アプリを作っていたので、rails でまともにアプリを作るのは、Ruby on Rails 2 以来だったりします。そのため、Rails を使い慣れている人には当たり前の話が多いかもしれません。

開発環境

コーディングはホストOS(Mac OS X Yosemite)、実行はゲストOS(Vagrant + VirtualBox + CentOS 7.2)で行いました。IDE は、最近 IntelliJ に慣れてきたので RubyMine にしました。

  • IDE: RubyMine 2016.2.1
  • Ruby: ruby 2.3.1p112
  • Ruby on Rails: Rails 5.0.0.1

プラグイン

画面は Bootstrap のデフォルトのデザインをほぼそのまま採用し、グラフは Highcharts、表は Datatables で作りました。いずれも gem でインストールできました。便利ですね。

自分で明示的に導入したプラグインと、導入方法、参考にしたページなどは以下の通りです。

bootstrap-sass (3.3.7)

  • twbs/bootstrap-sass: Official Sass port of Bootstrap 2 and 3.
  • rails new を実行した時点で、Gemfiles に gem 'sass-rails', '~> 5.0' が入っていた。そのため、追加したのは gem 'bootstrap-sass', '~> 3.3.6' のみ。
  • application.css のファイル名を application.scss に変更し、以下の行を追加。
@import "bootstrap-sprockets";
@import "bootstrap";
  • application.scss にした時点で、元の CSS ファイルにあった *= require_tree . の文法は使えなくなる。そのため、rails generate controller <controller_name> で自動生成される <controller_name>.scss は、自動的には読み込まれない。もし読み込みたければ、各ファイルを明示的に @import で指定するか、css - Proper SCSS Asset Structure in Rails - Stack Overflow の回答(日本語訳)にあるような手段を使う必要がある。
  • Sass 自体については、後述する書籍と、Sass + Railsの基礎 - Qiita を主に参考にした。
  • Bootstrap の使い方については、公式サイトの Getting Started の Examples と、Components および CSS を参考にした。

jquery-datatables-rails (3.4.0)

  • jquery-datatables-rails の "Twitter Bootstrap 3 Installation" の手順に従ってインストール。ただし、Sass 版の Bootstrap をインストールしたので、application.scss には以下のように記載する。
@import "dataTables/bootstrap/3/jquery.dataTables.bootstrap";

highcharts-rails (4.2.5)

//= require highcharts
//= require highcharts/highcharts-more

// チャート画像のダウンロード機能
//= require highcharts/modules/exporting
//= require highcharts/modules/offline-exporting

omniauth (1.3.1), omniauth-twitter (1.2.1)

google-analytics-rails (1.1.0)

Ruby on Rails 5 を使ってみた感想

Rails 2 時代の知識のアップデートするために、まずは本屋で Rails 4 の本をいくつか流し読みしてから、そのうちの1冊を買ってきて読みました。これは内容が網羅的で、かつ読みやすい良書でした。

Ruby on Rails 4 アプリケーションプログラミング

Ruby on Rails 4 アプリケーションプログラミング

また、Rails 5 に関するページをいくつか流し読みしました。主に参考にしたページはこのあたりです。

今回の開発の範囲では、基本的な機能しか使わなかったせいか、Rails 5 だからという理由でつまづくことは特にありませんでした。本当に何もなくて、拍子抜けしたくらいです。

Rails 5 からデフォルトの開発用 Web サーバが Webrick から Puma に変わったとのことですが、特に意識せずに使えました。また、プラグインも、Rails 5 だから動かない、というものはありませんでした。

強いて言えば、いままでは rake db:migrate のように rake で実行していたコマンドが、rails db:migrate で実行できるようになったので、新しいやり方に慣れるためになるべく rails の方を使っていました。まあ、rake の方も使えるので、無理に rails を使う必要はなさそうですけど。

今後、Admiral Stats に機能を追加する機会があれば、API mode など、Rails 5 の新機能をうまく入れ込んでみたいと思います。

*1:Ingress ではプレイヤーのことを Agent と呼び、艦これでは提督(Admiral)と呼ぶため。

Treasure Data Tech Talk 201607 レポート(古橋さんと成瀬さんの講演メモのみ)

f:id:muziyoshiz:20160724153127p:plain:w320

先週末に、Treasure Data Tech Talk に参加してきました。このイベントは毎回濃い話を聞けるので、行けるときはなるべく参加するようにしています。

今回は、古橋さんによる Digdag での YAML 利用の話と、成瀬さんによる PerfectQueue の話が特に面白かったです。以下、講演内容のメモと、公開済みのスライドです。

講演内容

DigdagはなぜYAMLなのか? (Sadayuki Furuhashi, @frsyuki)

  • Digdag とは何か?

    • Workflow automation system
    • Digdag で一番やりたいのはバッチデータ解析の自動化
  • Digdag の競合

    • OSS, Proprietary それぞれに競合がある
    • Workflow automation system は、ワークフローの定義方法によって3つに分類できる
      • プログラミング言語型:Luigi など
      • GUI型:Rundeck など
      • 定義ファイル+スクリプト型:Azkaban など
    • ワークフローの作りやすさと、カスタマイズの柔軟性のトレードオフ
  • Digdag

    • Digdag は定義ファイル+スクリプト型
    • 定義ファイル+スクリプト+俺たちのYAML
    • YAMLは便利だが、include できない、変数の埋め込みができない、(言語内DSLのように)プログラムが書けない、という欠点がある
    • Digdag では、YAML の仕様に従ったうえで、これらの欠点を克服した
  • include できる

    • YAML の仕様では、値(scalar)の前に "!" から始まる文字(タグと呼ばれる)を付与できる
    • 通常、YAMLパーサは、正規表現によるマッチでタグを決定して、自動的にタグを付与している
    • Digdag では "!include : filename" という表記を、ファイルインクルードの文法として使っている
    • !include の後ろに、" " を書く必要がある。このスペースが大事。このスペースのおかげで、通常の YAML パーサでも、キーが " "、値が filename のハッシュとして読み込める
    • ただし、この !include を複数書くと、キーが " " のハッシュが重複してしまう。Digdag の YAML パーサでは、複数の !include を書けるように、内部的にキーを UUID に書き換えている
  • 変数の埋め込みができる、プログラムが書ける

    • Java 8 は、Nashorn(ナスホーン)という JavaScript Engine を同梱している。これを使って ${} 内を評価している
    • だから Digdag は Java8 必須
  • Q) 何故YAMLをベースにした?

    • A) 比較的書きやすい、読みやすい。YAMLとして既存のプログラムから扱える。
  • Q) YAMLはグラフ構造を表現するのに適さないのでは?

    • A) YAMLはDAG、グラフを扱うわけではない。ツリーを扱っている。
  • Q) YAMLにコードを書けるとのことだが、悪さはできないのか?

    • A) 悪さできないように対策している。JavaScript はサンドボックス内でしか動作しない。そのためにJavaScriptを採用した。

PerfectQueueはいかにパーフェクトか、あるいはRubyとMySQLでジョブキューを作る試みについて (Yui Naruse, @nalsh)

  • Who is naruse

    • nkfメンテナ
    • Rubyコミッタ
    • Treasure DataではバックエンドのRubyを担当
  • そもそもジョブキューとは

    • FIFO
    • フロントエンドとバックエンドを疎結合化
  • PerfectQueue の特徴

    • MySQL で実装
    • At-least-once を優先(at-least-once と at-most-once はトレードオフの関係)
  • キューのデータ構造

CREATE TABLE `queue` (
    /* unique key (-> at most once) */
    id VARCHAR(255) NOT NULL,
    /* for FIFO's timeline */
    timeout INT NOT NULL,
    /* opaque data */
    data LONGBLOB NOT NULL,
    /* alive or finished */
    created_at INT,
    PRIMARY KEY (id)
)
  • タスクのライフサイクル

    • タスクの投入時に、timeout, created_at を現在時刻にする
    • タスクの取得時は、timeout が小さいものから優先的に取得し、timeout を 300 秒後に更新
    • タスクの実行中は、ハートビートとして、timeout を定期的に 300 秒後に更新
    • タスクの完了後は、created_at を NULL にして、一定時間保存するために timeout を 720 秒後に更新(タスクの重複を検出するため)
    • 前述の retention time を過ぎたら物理削除
  • タスクの取得、削除時の排他制御が大変

    • 排他処理のために、最初期は FOR UPDATE を使っていた
      • しかし、頻繁にデッドロックする
      • デッドロックを避けるには MySQL の気持ちになってクエリを書く必要がある
    • LOCK TABLES を使うと、テーブル全体をロックしてしまうので、SELECT にも影響
    • GET_LOCK が一番安全、でもクエリの書き方によってはデッドロックが発生した
      • ロックの delete クエリと acquire のクエリがバッティングしないように、タイミングの調整が必要(間隔を乱数で変える)
      • 調整しないと性能問題が起きた
      • さらに、ネットワーク遅延が発生すると影響大
        • GET_LOCK から RELEASE LOCK まで 3 RTT かかるので、影響大
    • queue テーブルに owner カラムを追加したうえで、FOR UPDATE を使うクエリに変更し、テーブルロックを不要にした
      • MySQL の気持ちになって書いたので安全
  • 結論

    • PerfectQueue はより完璧になった
  • Q) 何故 MySQL をキューに選んだのか?

    • A) 当時、Amazon RDS で PostgreSQL が使えなかった。RDBMS を選んだ理由は、Amazonで提供されている、フェイルオーバーがある、など。新しいジョブキューを開発したのは、ジョブキューにフェアスケジューリングの機能(Treasure Dataのサービスで必要)を付けたかったから。(古橋)

感想

Digdag での YAML の拡張(いや、標準の仕様に従っているので拡張と言うのは不適切か?)については、話としては面白かったんですが、そもそも YAML でプログラミングするのは辛そう、というのが第一の感想でした。Ansible もそんな感じのつらみがありますしね。AnsibleSpec みたいな、Digdag で書いたワークフローをテストするツールとかが、いずれ出てきたりするんでしょうか。

また、マークアップ言語として YAML にこだわる必要があるんだろうか、とも思ったのですが、じゃあ代替手段として何があるのか、と考えてみると、なかなか難しそうです。XML よりもマシな選択肢となると、いまは YAML なのかな……。HashiCorp の HCL のように、独自方式を作る方向もあったと思いますが、Digdag に独自方式を作るほどの要件はなかったんですかね。

手を動かす Spark MLlib & Word2Vec Part 2 (Wikipedia 英語版から Word2Vec モデルを作るまで)

f:id:muziyoshiz:20160626223709p:plain

このシリーズについて

実際に手を動かして Spark MLlib に慣れていこう、というシリーズです。

Spark を使うならそれなりに大きなデータを分散処理しないと面白くないと思い、Wikipedia のデータから Word2Vec のモデルを作るところまでやってみました。環境構築については Part 1 をご参照ください。

muziyoshiz.hatenablog.com

Part 2 の範囲

Wikipedia 英語版のデータから作成したコーパスを Amazon EC2 上の Spark Cluster で処理して、Word2Vec のモデルを作成するところまで。

Wikipedia 英語版のデータからコーパス作成

最終的にやったこと

Wikipedia:Database download から辿って、https://dumps.wikimedia.org/enwiki/ (HTTP) または Data dump torrents (BitTorrent) からダウンロードできます。

Wikipedia のデータは XML および SQL で公開されており、それぞれ色々ファイルがあります。今回は Wikipedia の本文からコーパスを作りたいので enwiki-latest-pages-articles.xml.bz2 をダウンロードしました。私がダウンロードした時の最新版は 2016-06-03 作成の 12.1 GB のファイルでした。

このファイルを解凍すると以下のような XML が入っています。<text> タブの中身が本文です。

  <page>
    <title>Anarchism</title>
    <ns>0</ns>
    <id>12</id>
    <revision>
      <id>721573764</id>
      <parentid>719202660</parentid>
      <timestamp>2016-05-22T19:25:12Z</timestamp>
      <contributor>
        <username>PBS-AWB</username>
        <id>11989454</id>
      </contributor>
      <comment>modification to template Cite SEP and possibly some gen fixes using [[Project:AWB|AWB]]</comment>
      <model>wikitext</model>
      <format>text/x-wiki</format>
      <text xml:space="preserve">{{Redirect2|Anarchist|Anarchists|the fictional character|Anarchist (comics)|other uses|Anarchists (disambiguation)}}
{{pp-move-indef}}
{{Use British English|date=January 2014}}
{{Anarchism sidebar}}
'''Anarchism''' is a [[political philosophy]] that advocates [[self-governance|self-governed]] societies based on voluntary institutions.
(中略)
[[Category:Far-left politics]]</text>
      <sha1>sfoc30irh5k1bj62ubt29wp1ygxark0</sha1>
    </revision>
  </page>

この XML から本文だけ取り出す方法を色々探してみたところ、wp2txt というツールがあったので、今回はこちらを使わせてもらいました。

github.com

wp2txt は Ruby で書かれており、関連する gem のインストールに苦労したという記事も見かけたのですが、以下の手順で問題なく動作しました。

% mkdir wp2txt
% cd wp2txt
% bundle init
% echo 'gem "wp2txt"' >> Gemfile
% bundle install
% mkdir output_dir
% bundle exec wp2txt -i enwiki-latest-pages-articles.xml.bz2 -o enwiki --no-heading --no-title --no-marker

今回は本文だけが必要なので、余分な出力をなるべく減らすためのオプション(--no-heading, --no-title, --no-marker)を付けました。それでも、MediaWiki のマークアップなどは残ってしまうのですが、それは Spark で除去することにします。

wp2txt の処理が終わると、enwiki ディレクトリに enwiki-latest-pages-articles.xml-<連番>.txt というファイルが作成されます。今回は合計 1,754 ファイル、18 GB でした。ちなみに、私の環境(MacBook Pro 2.2 GHz, 16GB DDR3)では、実行に19時間くらいかかりました。

つまづいたこと

Wikipedia のデータからコーパスを作る方法を色々探したのですが、英語圏も含めて、簡単にできる方法がなかなか見つかりませんでした。

wp2txt 以外で良さそうなものとしては wiki2vec というツールが、コーパスを作る機能を持っているようでした。ただ、時間の都合で今回は試せませんでした。ちなみに、この記事を書くために読み返していて気付きましたが、wiki2vec のパラメータとして minCount = 50, vectorSize = 500, windowSize = 10 という例が載っていました。次はこれで試してみるのもよいかもしれません。

github.com

その後、Word2Vec 関係の記事を探しまわるなかで、以下の記事から wp2txt の存在に気づき、今回は wp2txt を使うことにしました。

techblog.gmo-ap.jp

コーパスの、S3 へのアップロード

最終的にやったこと

Apache EC2 上に構築した Spark Cluster に Wikipedia のファイルを渡さなければいけないのですが、データサイズが大きいので、今回は S3 経由で渡しました。

Spark の textFile メソッドは gzip 圧縮されたファイルも読み込めるので、まずは先程のファイルを圧縮します。圧縮後のファイルは 6 GB になりました。

% gzip enwiki/*.txt

そして、これを S3 にアップロードします。aws s3 cp コマンドはワイルドカードが使えないので、一括アップロード時には、ディレクトリ名を指定して --recursive を指定する必要があります。

% aws s3 cp enwiki s3://my-bucket-name/ --recursive

aws コマンドがない場合は、Installing the AWS Command Line Interface - AWS Command Line Interface に従ってインストールしてください。Mac の場合は pip でインストールします。

つまづいたこと

最初は、以下のように1ファイルにまとめてアップロードしたのですが、これだとファイル読み込みが全く分散されませんでした。

% cat enwiki/*.txt > enwiki.txt
% gzip enwiki.txt
% aws s3 cp enwiki.txt.gz s3://my-bucket-name/

今回は wp2txt がファイルを複数に分割してくれていましたが、他の方法でコーパスを作った場合も、ファイルを分けてアップロードしたほうがいいですね。

spark-submit で使う jar の作成

最終的にやったこと

いままでは spark-shell で Word2Vec を実行していましたが、データ量が増えると実行時間が長くなって、EC2 インスタンスへの接続が切れてしまう可能性が出てきます。そのため、Word2Vec を実行する簡単な jar を作って、spark-submit で実行することにします。

以下がそのコードです。

Simple Word2Vec application

Feature Extraction and Transformation にあるサンプルに、以下の修正を加えています。

  • コマンドライン引数から、読み込むファイル、モデルの出力先、Word2Vecのパラメータを設定
  • repartition メソッドを使って、RDD を分割(これをしないと読み込み後の処理が分散されない)
  • split する際の区切り文字に、スペース以外の文字も含めることで、MediaWiki 記法の文字を除去
  • filter で長さが 1 の文字を除去
  • 処理の最後で、実行時間を出力

jar をビルドしたい場合は、以下のリポジトリを使ってください。

github.com

sbt assembly を実行すると target/scala-2.11 ディレクトリに word2vec-model-generator-assembly-0.0.1.jar ができます。この jar ファイルだけ master ノードに持っていけば実行できます。

つまづいたこと

最初は repartition メソッドを使わなかったのですが、その場合、ファイルの読み込みは分散しても、その後の処理が途中まで全く分散されませんでした。repartition メソッドに渡す引数は scala - Spark: Repartition strategy after reading text file - Stack Overflow を参考にしました。

MediaWiki記法を除去するルールは、ローカルマシンで enwiki-latest-pages-articles.xml-0001.txt を処理して手探りで決めたのですが、もっと良い方法がありそうです。例えば、先ほど紹介した wiki2vec は、うまく処理していそうです("Word2Vec Corpus" の節を参照)。

Amazon EC2 への Spark クラスタの構築(5台構成)

最終的にやったこと

Part 1 でもクラスタを構築しましたが、それは1回削除して、Slave の台数とスペックを増やしたクラスタを作り直しました。ちなみに、spark-ec2 destory spark-cluster でクラスタを削除できます。

大規模データで Spark MLlib を試すのは初めてなので、手間取っている間にマシンが無駄に動いている……という可能性があったので(というか実際そうなったので)、少しケチって以下のスペックで構築しました。

  • master: r3.large ($0.185/hour, 2 vCPU, 15 GB memory, 1 x 32 SSD) 1台
  • slave: m3.2xlarge ($0.616/hour, 8 vCPU, 30 GB memory, 2 x 80 SSD) 5台

構築時のコマンドは以下の通りです。

./spark-ec2 \
--key-pair=word2vec-key-pair \
--identity-file=/Users/myoshiz/.ssh/word2vec-key-pair.pem \
--region=us-west-1 \
--zone=us-west-1a \
--master-instance-type=r3.large \
--instance-type=m3.2xlarge \
--copy-aws-credentials \
--hadoop-major-version=yarn \
--slaves 5 \
launch spark-cluster > spark-submit.log 2>&1 &

構築が完了したら、前回同様に .bash_profile に環境変数 EC2_SPARK_MASTER を設定します。また、今回は環境変数 AWS_ACCESS_KEY_ID と AWS_SECRET_ACCESS_KEY も設定します。これは、Spark アプリケーションから S3 にアクセスするために必要な設定です。

export EC2_SPARK_MASTER=ec2-xxx-xxx-xxx-xxx.us-west-1.compute.amazonaws.com
export AWS_ACCESS_KEY_ID={{ IAM ユーザ "word2vec-user" のアクセスキーID }}
export AWS_SECRET_ACCESS_KEY={{ IAM ユーザ "word2vec-user" のシークレットアクセスキー}}

つまづいたこと

Part 1 でも書きましたが、--copy-aws-credential を指定しても、Spark に対してはアクセスキーID、シークレットアクセスキーが設定されません。環境変数を設定したところ、Spark(spark-shell, spark-submit)から S3 にアクセスできるようになったので、今回はこの方法で済ませました。

なお、AWS のインスタンスプロファイルを使ってアクセス権限を与えることもできると思いますが、今回は試していません。クラスタを作ったり壊したりを繰り返していたため、そのたびに AWS マネジメントコンソールをいじるのは面倒だったので……。

spark-submit の実行

最終的にやったこと

先ほど作った jar を、master にコピーします。

% scp -i ~/.ssh/word2vec-key-pair.pem word2vec-model-generator-assembly-0.0.1.jar root@${EC2_SPARK_MASTER}:/root/

そして、以下のようにコマンドを実行すると、Word2Vec アプリケーションが実行されます。ssh 接続が切れた場合のために、バックグラウンドで実行し、標準出力はファイルに出力させておきます。

$ ./spark/bin/spark-submit \
--master spark://${EC2_SPARK_MASTER}:7077 \
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer \
--conf spark.driver.memory=11g \
--conf spark.akka.frameSize=1024 \
--class jp.muziyoshiz.word2vec.Word2VecModelGenerator \
word2vec-model-generator-assembly-0.0.1.jar \
s3n://my-bucket-name/enwiki/*.txt.gz \
s3n://my-bucket-name/enwiki_model \
10 20 50 > spark-submit-wp8.log 2>&1 &

Spark から S3 にアクセスする際は、aws コマンドで指定するときの s3:// ではなくて、s3n:// を指定する必要がある点に注意です。

Word2Vec のパラメータを変えて、何度か実行してみたところ、実行時間は以下のようになりました。

Pattern No. numPartition numIteration minCount vectorSize 実行時間
1 10 1 5 10 75.0 min
2 10 1 20 20 82.6 min
3 10 1 20 50 130.6 min

最初のパターン1は、vectorSize をかなり減らしたにも関わらず時間がかかり、後述するようにモデルの精度もよくありませんでした。

そのため、パターン2では minCount(単語の最小出現回数)を大きくして、vocabSize(モデルに含まれる単語数)を減らしました。その結果、vectorSize をパターン1の2倍にしたにも関わらず、実行時間は1.1倍程度に収まり、モデルの精度も若干上がりました。

最後に、他のパラメータは同じままで vectorSize のみ50に増やしたところ、vectorSizeはパターン2の2.5倍で、実行時間は1.5倍になりました。

実行時間の内訳を Spark UI で確認したところ、処理の最後に slave 1台で実行するタスク(Locality Level NODE_LOCAL のタスク)があり、これが1〜2時間かかっていました。このタスクがボトルネックになっているということは、少なくとも Word2Vec については、Spark MLlib による分散処理のメリットって、もしかしてあまり無いとか……?

ただ、今回は numIteration を 1 で固定にしましたが、精度を上げるためには numPartition と同じ 10 まで上げたほうがよいはずです。numIteration を増やせば、分散処理されるタスクが占める割合も増えるので、Spark の恩恵が得られるのではないかと思います。それはまた今度試してみます。

つまづいたこと(1):ドライバのメモリ使用量を増やさないと落ちる

最初に実行したところ、以下のエラーが発生してジョブが止まりました。

16/06/25 09:39:46 ERROR TransportRequestHandler: Error while invoking RpcHandler#receive() for one-way message.
java.lang.IllegalStateException: unread block data

Spark UI の Environment タブで、Spark Properties を確認したところ、spark.executor.memory は m3.2xlarge(メモリ 30GB)に合わせて 26837m に指定されていたのですが、spark.driver.memory のほうは何も指定されていませんでした。spark.driver.memory のデフォルトは 1g です。

これを r3.large のメモリ 15GB から 4GB を引いた 11GB(11g)に指定したところ、このエラーは出なくなりました。また、Environment タブで、spark.driver.memory が指定されていることを確認できました。

つまづいたこと(2):vocabSize*vectorSize が大きすぎると落ちる

numPartition numIteration minCount vectorSize
10 1 5 100

Spark MLlib の vectorSize のデフォルト値は 100 なので、最初はこの値を使っていました。しかし、上記のパラメータの組み合わせで実行したところ、Stage 1 の処理の途中で以下のエラーが出て、タスクが止まってしまいました。

Exception in thread "main" java.lang.RuntimeException: Please increase minCount or decrease vectorSize in Word2Vec to avoid an OOM. You are highly recommended to make your vocabSize*vectorSize, which is 3856720*100 for now, less than `Int.MaxValue/8`.
        at org.apache.spark.mllib.feature.Word2Vec.fit(Word2Vec.scala:319)
(スタックトレースは省略)

Int.MaxValue/8 = 268435455 です。つまり、vocabSize(単語数)*vectorSize がこの上限を大幅に超えていることが原因のようです。Int の最大値の8分の1ってなんでまた……。

とにかく、単語数を減らすか、vectorSize を減らす必要があることがわかりました。そのため、これ以降のテストでは vectorSize を減らすパターン(パターン1)と、minCount を増やして単語数を減らすパターン(パターン2〜3)を試しました。

つまづいたこと(3):モデルのサイズが大きすぎると akka のフレームサイズ上限を超えて落ちる

Word2Vec.fit() の最後、生成された Word2Vec モデルを parquet 形式で出力するところで、以下のエラーが出て落ちました。

16/06/25 11:28:06 ERROR InsertIntoHadoopFsRelation: Aborting job.
org.apache.spark.SparkException: Job aborted due to stage failure: Serialized task 1793:0 was 213710572 bytes, which exceeds max allowed: spark.akka.frameSize (134217728 bytes) - reserved (204800 bytes). Consider increasing spark.akka.frameSize or using broadcast variables for large values.

spark.akka.frameSize のデフォルトは 128(単位は MB)で、タスクのサイズがこれを超えると落ちるようです。小さいデータセットでは出なかったエラーなのですが、Wikipedia 規模になると出るようです。1〜2時間待ったあとで、最後の最後にこのエラーで落ちると、非常に(精神的にも金銭的にも)痛いです……。

設定可能な上限値は調べても分かりませんでしたが、ひとまず --conf spark.akka.frameSize=1024 を指定して 1GB にしたところ、Word2Vec モデルの出力まで成功しました。

ローカルマシン上での Word2Vec モデルの利用

最終的にやったこと

先ほどの spark-submit の実行により、Word2Vec モデルが S3 にアップロードされました。このモデルをローカルマシンにダウンロードして使ってみます。

% aws s3 cp s3://my-bucket-name/enwiki_model ./enwiki_model --recursive

以下のコマンドで spark-shell を起動します。Part 1 で使ったオプションに加えて、--conf spark.kryoserializer.buffer.max=1g を指定しています。

% spark-shell --master local \
--conf spark.driver.memory=5g \
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer \
--conf spark.kryoserializer.buffer.max=1g

そして、spark-shell のプロンプトで以下を実行し、Word2Vec モデルをロードします。

import org.apache.spark._
import org.apache.spark.rdd._
import org.apache.spark.SparkContext._
import org.apache.spark.mllib.feature.{Word2Vec, Word2VecModel}

val model = Word2VecModel.load(sc, "enwiki_model")

また、今回は Apache Spark 入門に書かれたメソッドを使って、関係性を加味した推測を行ってみます。有名な例で言うと、「"king" に対する "kings" は、"queen" に対する何か?」という関係を、Word2Vecモデルから推測することができます。

以下は、Apache Spark 入門の p.224 から抜粋したコードです。これを spark-shell に貼り付けるか、ファイルに書いておいて :load <file name> でロードして使います。

import org.apache.spark.mllib.linalg.Vectors

def relationWords(w1: String, w2: String, target: String, model: Word2VecModel) :Array[(String, Double)] = {
    val b = breeze.linalg.Vector(model.getVectors(w1))
    val a = breeze.linalg.Vector(model.getVectors(w2))
    val c = breeze.linalg.Vector(model.getVectors(target))
    val x = c + (a - b)
    model.findSynonyms(Vectors.dense(x.toArray.map(_.toDouble)), 10)
}

パターン1:minCount = 5, vectorSize = 10

ベクトル数が小さいためか、精度はかなり悪いです。Tokyoという都市名、Japanという国名に対する類似語を求めても、上位10件にそれらしいものが現れません。

scala> model.findSynonyms("Tokyo", 10).foreach(println)
(Rebounder,5.026514312082014)
(Fivepenny,5.006271473525809)
(Riviera,4.9806280562664655)
(Pirmahal,4.977896311409738)
(A2217,4.973896329049228)
(Pestújhely,4.967955406306887)
(Tri,4.966647609406325)
(Cigarros,4.966214313196464)
(Seahorses,4.9657892250050715)
(Club,4.965424934604451)

scala> model.findSynonyms("Japan", 10).foreach(println)
(Prabda,3.8591253451462766)
(Skateabout,3.789246081518729)
(detailslink,3.756286768742609)
(Oceania,3.7439580152901946)
(Daeges,3.743037606956309)
(Equestrianism,3.73990681262581)
(Miegs,3.7392088293670396)
(Fleuth,3.735308547592705)
(KBID-LP,3.730579527776324)
(Powerlifting,3.717090309581691)

関係性を加味した推測も、以下のようにうまく行きませんでした。

scala> relationWords("king", "kings", "queen", model).foreach(println)
(satsuma-biwa,4.95347264322314)
(shoguns,4.93869343414127)
(mystics,4.931215483461304)
(Zelimxan,4.925167012454619)
(Christianized,4.922235458369835)
(veneration,4.921893688910249)
(Shi’i,4.921205040607001)
(Russified,4.917586471812209)
(pagan,4.912822109308089)
(revered,4.911351827558269)

scala> relationWords("prince", "king", "princess", model).foreach(println)
(Pandava,4.2101984410814834)
(Aegisthus,4.207452272387961)
(bandit,4.202362575975742)
(amanuensis,4.194580140364399)
(Aerope,4.188601884423512)
(tradesman,4.178661804898081)
(Candaules,4.177194064593601)
(princess,4.173209621638307)
(Shoulang,4.165125455530385)
(Seibei,4.163678291883964)

パターン2:minCount = 20, vectorSize = 20

単語数を減らし、ベクトル数を上げた結果、精度が若干向上しました。Tokyo に対する類義語として、日本の都市の Osaka、Sapporo が出てくるようになりました。一方で、Japan に対する類義語のほうは、あまり改善が見られません。

scala> model.findSynonyms("Tokyo", 10).foreach(println)
(Wrestle,7.689458080058069)
(Split,7.626499879518354)
(Osaka,7.620597049534027)
(Sapporo,7.556529623946273)
(Setagaya,7.513748270603075)
(Hiroshima,7.490792005499523)
(Shinjuku,7.45951304352636)
(Kanazawa,7.459122453399323)
(Expo,7.453010168798164)
(ESCOM,7.447874763780933)

scala> model.findSynonyms("Japan", 10).foreach(println)
(Tokyo,5.679376270328159)
(AXN,5.640570343734289)
(Wrestle,5.60396135079362)
(Expo,5.590382781259281)
(TV2,5.522196857434101)
(Hanoi,5.495135749493573)
(TV6,5.490184062697079)
(Kyoto,5.486577183328772)
(Skate,5.4760554670281065)
(Benelux,5.430530293625971)

関係性を加味した推測は、まだあまりうまくいきません。ただ、後者のほうは5位に正解の "queen" が出ているので、若干精度が向上しています。

scala> relationWords("king", "kings", "queen", model).foreach(println)
(pagan,6.667731329068959)
(garb,6.659426546093454)
(gods,6.648366573398432)
(symbolised,6.648168276539841)
(sacred,6.6085783714277975)
(personages,6.598811565877372)
(veneration,6.597536687593547)
(puranas,6.590383098194837)
(deities,6.588936982768422)
(beauties,6.588806331810932)

scala> relationWords("prince", "king", "princess", model).foreach(println)
(lord,6.574825899196509)
(princess,6.522661208674787)
(bride,6.521167177599623)
(lady,6.492377997870626)
(queen,6.479450084505509)
(first-born,6.466189456944019)
(king,6.441766970616445)
(blessed,6.441764119985444)
(beloved,6.4396910737789606)
(bridegroom,6.423838321417851)

パターン3:minCount = 20, vectorSize = 50

ベクトル数を大幅に増やした結果、かなり精度が向上しました。Tokyo に対して日本の都市、Japan に対してアジアの国名や首都が表示されるようになりました。

scala> model.findSynonyms("Tokyo", 10).foreach(println)
(Osaka,6.442472711716078)
(Fukuoka,6.3918200759436)
(Saitama,6.343209033208874)
(Setagaya,6.237343626467007)
(Japan,6.063812875793321)
(Sapporo,6.027676167552773)
(Nagano,5.955215285602899)
(Kobe,5.891646194480255)
(Yamagata,5.86912171881318)
(Shibuya,5.835765966270005)

scala> model.findSynonyms("Japan", 10).foreach(println)
(Tokyo,5.510337298616405)
(Korea,5.509610108188756)
(China,5.486622516556292)
(Fukuoka,5.378651363703807)
(Taiwan,5.377869828524535)
(Seoul,5.321357314331263)
(Shizuoka,5.31678565272272)
(Prefecture,5.297746506109964)
(Hamamatsu,5.159312705112953)
(Kanagawa,5.157422752148916)

関係性を加味した推測も、かなり精度が向上しました。前者は正解の "queens" が2位、後者も正解の "queen" が2位に表示されています。イテレーション数(numIteration)を増やすなど、更にパラメータを調整すれば、これらの単語が1位に上がることが期待できます。

scala> relationWords("king", "kings", "queen", model).foreach(println)
(realms,6.380416904839411)
(queens,6.292521776188793)
(knightly,6.2558567330155626)
(consorts,6.241017073100756)
(kings,6.200374546691251)
(kindreds,6.17249501613232)
(lamas,6.1721177720161915)
(monuments,6.147651372785442)
(patrilineal,6.1288029631730545)
(depictions,6.121416883901753)

scala> relationWords("prince", "king", "princess", model).foreach(println)
(princess,5.956775488416378)
(queen,5.9055082324742685)
(slaying,5.793197818446893)
(king’s,5.696965618712307)
(betrothal,5.59067630474941)
(goddess,5.58159904439838)
(apparition,5.554027664552106)
(martyrdom,5.534826668619817)
(Pelops,5.503355785910461)
(ancestress,5.4953139994512545)

つまづいたこと

spark-shell の起動時に --conf spark.kryoserializer.buffer.max=1g を指定しないと、Word2VecModel.load() の呼び出しで落ちました。

16/06/25 22:00:26 WARN TaskSetManager: Lost task 1.0 in stage 2.0 (TID 3, localhost): TaskKilled (killed intentionally)
org.apache.spark.SparkException: Job aborted due to stage failure: Task 0 in stage 2.0 failed 1 times, most recent failure: Lost task 0.0 in stage 2.0 (TID 2, localhost): org.apache.spark.SparkException: Kryo serialization failed: Buffer overflow. Available: 1, required: 4. To avoid this, increase spark.kryoserializer.buffer.max value.
    at org.apache.spark.serializer.KryoSerializerInstance.serialize(KryoSerializer.scala:299)
(スタックトレースは省略)

あと、余談ですが、zsh を使っていると --master local[*] の指定ができないようです。以下のようなエラーが出ます。bash なら指定できるので、spark-shell の実行時だけ bash に切り替えました。

% spark-shell --master local[*] \
--conf spark.driver.memory=5g \
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer \
--conf spark.kryoserializer.buffer.max=1g
zsh: no matches found: local[*]

Part 2 のまとめ

Wikipedia の大規模データを、Amazon EC2 上に構築した Spark Cluster で処理できました。また、実際にいろいろなパラメータを試してみて、大規模データを処理する際に注意すべき Spark のプロパティや、Word2Vec のパラメータを把握することができました。

今回生成した Word2Vec のモデルはまだあまり精度が良くありませんが、以下の方針で精度を上げることができそうです。

  • minCount を大きくして、単語数(vocabSize)を小さくする
  • vectorSize を増やす
  • numIteration を numPartition に近づける
  • 元データからノイズ(MediaWiki記法など)を除去する方法を改善する

実際に試してみて、Word2Vec の場合は slave 1台で実行される処理がボトルネックになっていることがわかりました。Amazon EC2 の高いマシンを借りているのに、slave 1台だけが頑張っていて、残りの4台は遊んでいる、というのはなかなか焦ります。

Slave 1台しか動かない時間を短くする方法としては、vocabSize*vectorSize を小さくするしかないのですかね? 実際のアプリで Word2Vec を使う際には、Spark MLlib での処理の前に、そのアプリで使わない単語を除去してしまって単語数を大幅に減らしておく、などの対策が必要かもしれません。

Part 2 の主な参考文献