2012-12-14

Machine Learning Advent Calendar 2012 の 14 日目の記事になります。気まぐれでこのカレンダー登録してみたものの、この日に至るまでの数々の記事のガチな高レベルさに登録してしまったことをかなり後悔している @komiya_atsushi です。

この記事では、オンライン学習アルゴリズムの AROW を Ruby で実装した (& gem として公開した) という、どこにでもよくある話を書いています。主に機械学習に馴染みのない or 取り組み始めたばかりの(自分を含めた)エンジニア向けの記事となっています。

AROW とオンライン学習

まずはじめに、この記事で取り上げている AROW とその前提となる知識について軽く説明していきます。

AROW (Adaptive Regularization of Weight Vectors) というのは、「オンライン学習」ができる「教師あり」の「クラス分類(二値分類)」アルゴリズムです。ではこの AROW を使って何ができるのか? というと、例えば「海賊船からかっぱらってきた金貨のようなものを、事前に明らかになっている数十枚のコインの真贋の情報をもとに、その体積と重さから真贋を見分ける」ことができるようになります。

AROW は 2009 年に提案されたアルゴリズムであるものの、後からよりよい手法が幾つか提案されているので state of the art な手法ではないですが、言い換えれば安定して使える「枯れた技術」として使うには十分ではないでしょうか。

オンライン学習

「オンライン学習」については、今回の Advent Calendar でも記事を書かれている kisa12012 さんの オンライン学習による線形識別器 などがありますので、詳しく知りたい方はこちらをご参照いただくのがいいかと思います。ここではざっくりな概要の説明に留めるとして、クラス分類での説明を簡単(雑?)にすると「次々と与えられるデータの『特徴』とそのデータに対するクラスを示す『ラベル』(Yes / No みたいなもの)をもとに、それらをラベルに従って分類してくれるモデルを作る・更新する(=学習する)」となるでしょう。

オンライン学習の特徴としては、以下に示すものがあります。

  • 大量のデータをもとにモデルを作る必要があっても、全データを同時にすべて取り扱う必要がない(主記憶装置にやさしい)。
  • モデルの更新に利用したデータを不要となれば破棄することもできる(二次記憶装置にやさしい)。
  • 1回1回のモデルの更新にかかる計算量が小さい(演算装置にやさしい)。

教師あり学習

次に「教師あり学習」について。教師あり学習というのは「正解の状態が明らかになっているデータ(訓練データ)をもとにモデルを作成する」学習になります。具体例でいうと今回取り上げている「クラス分類」がそれにあたります。対する用語として「教師なし学習」がありますが、こちらは「クラスタリング」などが例に挙げられます。

クラス分類

最後は「クラス分類」について。これは「データの『特徴』とそのデータに対するクラスを示す『ラベル』(Yes / No みたいなもの)をもとに、それらをラベルに従って分類してくれるモデルを作る・更新する。またラベルが未知のデータを予測して分類してくれる」お仕事と表現できるかと思います。

AROW の論文を読んでみる

続いて、AROW のアルゴリズムを実装する観点で、AROW の 論文 を読んでいきましょう。…とは言え、私自身、数式がそれなりにふんだんに使われた論文をすらすらと読めるほど数学リテラシーが高いわけではないので、重要な部分だけをピックアップして軽く説明します。

AROW を実装するに当たって一番重要なのは、p.4 のページ上にある Fig.1 です。(下図参照)

ここにある式を頑張って実装すればいいわけです。Σ が summation としてではなく共分散行列を表す変数として使われていることと、ベクトル x が列ベクトルであることに注意をすれば比較的容易に数式を読み解けると思います。

実際にこのコア部分を Ruby で実装すると、高々 50 行弱のコードになります。とってもシンプルですね! 1

なお既存の他言語での実装を見てみると、Σ の更新式が、論文中の (9) 式ではなく、(8) 式をもとにした実装がほとんどになっているようです。tsubosaka 先生の Java 実装 や C++ 実装の AROWPP などなど。なぜみんな (8) 式を利用しているのか理由が分からなかったのと、(9) 式の方がより効率よく計算できて精度も向上するようなので、今回は (9) 式での実装をしました。 2

gem 'arow'

今回は上記実装をもとに gem 化したものを RubyGems.org で公開 しています。ソースコードは github.com/komiya-atsushi/arow にて公開しています。突貫工事的に gem 化したので、テストケースはないしマニュアルはないし README からして適当と粗が目立ちますが、カジュアルに使えるクラス分類器を自分自身が欲していたので、(お仕事が落ち着いたら)もうちょっと使いやすくできるようにメンテナンスをしていく予定です。

利用例がないのもアレなので、先に挙げた Code IQ の問題に対する適用例を示します… としたかったのですが、12/14 現在、まだ挑戦者受付中とのことらしいので念の為にしばらくはコード公開を控えておきます。また後日で。 3

2012.12.21 追記:公開しました。

おわりに・感想

久々に数式たっぷりな論文を読んで、改めて己の数学力の低さを悔やみました。数学力を鍛え直したい…

この Machine Learning Advent Calendar の企画を開催してくださいました @naoya_t さん、ありがとうございました!


1 : 今回の実装では、μ (@means) と Σ (@covariances) は時間・空間計算量を減らすために行列の対角要素をとったもので表現しています。既存の各種実装をみても、いずれもこの対角要素を利用する方式をとっているようです。
2 : 理由を御存知の方がいらしたら是非、教えていただけると幸いです。
3 : Advent Calendar に登録した当初は 12/14 時点で受付終了している予定でしたが、終了が延長されてしまったため、このタイミングでは公開できず、と。ぐぬぬ…

2012-11-29

難しい手順ではないのだけれど、ちょっとはまったことがあったので未来の自分のためにメモを残しておきます。

手順

  1. $ play new で Play framework のアプリを作成する
  2. ./conf/dependencies.yml に Twitter4J を利用する旨を記述する
  3. $ play dependencies で依存ライブラリを ./lib に配置する
  4. $ play eclipsify で Eclipse 上で開発可能な状態にする
  5. javaee-api-5.0-x.jar をビルドパスから取り除く

「2. ./conf/dependencies.yml に Twitter4J を利用する旨を記述する」について

以下のように、Twitter4J に対する依存を記述すれば OK。Twitter4J の jar ファイルをダウンロードして特定ディレクトリに配置… みたいなことをする必要はありません。

# Application dependencies

require:
    - play
    - org.twitter4j -> twitter4j-core [3.0,)

「5. javaee-api-5.0-x.jar をビルドパスから取り除く」について

これをし忘れてちょっとはまりました。

Twitter4J を dependencies.yml に記述して $ play dependencies を実行すると、twitter4j-code-3.0.x.jar とともに javaee-api-5.0-x.jar というファイルも ./lib のディレクトリに配置されることになります。この jar ファイルをビルドパスに含めたまま Play アプリを起動してページアクセスすると、以下のエラーが発生することがあります。

play.exceptions.UnexpectedException: Unexpected Error
 at play.Invoker$Invocation.onException(Invoker.java:244)
 at play.Invoker$Invocation.run(Invoker.java:286)
 at Invocation.HTTP Request(Play!)
Caused by: java.lang.NoSuchMethodError: javax.persistence.EntityManager.setProperty(Ljava/lang/String;Ljava/lang/Object;)V
 at play.db.jpa.JPAPlugin.startTx(JPAPlugin.java:375)
 at play.db.jpa.JPAPlugin.beforeInvocation(JPAPlugin.java:345)
 at play.plugins.PluginCollection.beforeInvocation(PluginCollection.java:473)
 at play.Invoker$Invocation.before(Invoker.java:217)
 at play.Invoker$Invocation.run(Invoker.java:277)
 ... 1 more

どうやら javaee-api-5.0-x.jar に含まれる javax.persistence.EntityManager クラスの定義が Play が参照しているそれとかち合ってしまうようなので、思い切って javaee の方をビルドパスから除去してあげることで解消できます。

2012-11-17

2013.06.08 追記:JDBC 接続文字列で characterEncoding / connectionCollation を指定すると思った通りの挙動をしてくれないようなので、別の方法 (多分これが正しい方法) を記載しました。

Unicode における、こんな 感じの絵文字、いわゆる Unicode の 追加面 の文字、Java で言えばサロゲートペアでの表現が必要となる文字を、JDBC 経由で UTF-8 エンコーディングして MySQL のテーブル・カラムに格納しようとすると、以下の例外が発生することがあります。

Caused by: java.sql.SQLException: Incorrect string value: '\xF0\x9F\x98\x81 h...' for column 'col_name' at row 1
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1074)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4096)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4028)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2490)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2651)
    at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2734)
    at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:2155)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2458)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2375)
    at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2359)

Twitter API 叩いて得たツイートを MySQL に入れて蓄積したい、みたいなことをしようとして、毎回このスタックトレースと格闘しているので、対応方法を忘れないようにメモしておきます。

確認した環境は以下のとおり。

  • MySQL 5.5.25a
  • MySQL Connector/J 5.1.22
  • Java 1.7.0_07-b10

すべきこと

  • 強く推奨される対処方法
    1. my.cnf (Windows の場合は my.ini) の [mysqld] セクションで character-set-server に utf8mb4 を設定する
    2. 既存のデータベース / テーブル / カラムの CHARSET を確認する
  • いかんともし難い理由により my.cnf を変更できない場合の代替手段
    1. データベース / テーブルの CHARSET に utf8mb4 を指定する
    2. Java のプログラムから具体的な SQL を発行する前に SET NAMES utf8mb4 を発行する

ざっくりとまとめると上記のとおり、2 つの手段があります。以下でそれぞれ説明します。

強く推奨される対処方法

my.cnf (my.ini) を変更することができるならば、こちらの手段での対処を強くおすすめします。こちらの方法であれば、Java プログラム側は特別な対処をする必要がありません (JDBC 接続文字列での characterEncoding や connectionCollation の設定も不要です)。

1. my.cnf の [mysqld] セクションで character-set-server に utf8mb4 を設定する

以下のように、character-set-server の値を utf8mb4 に書き換えるだけです。

(略)

# SERVER SECTION
# ----------------------------------------------------------------------
#
# The following options will be read by the MySQL Server. Make sure that
# you have installed the server correctly (see above) so it reads this 
# file.
#
# server_type=3
[mysqld]

(略)

# The default character set that will be used when a new schema or table is
# created and no character set is defined
character-set-server=utf8mb4  # ← ここを utf8mb4 にする

2. 既存のテーブル / カラムの CHARSET を確認す

続いて、すでに存在するテーブルやカラムの CHARSET を以下のクエリで、テーブルごとに確認します。

SHOW CREATE TABLE テーブル名

このクエリを実行した結果、たとえば、

CREATE TABLE `tbl_name` (
  `col_name` varchar(8) CHARACTER SET utf8 DEFAULT NULL
) ENGINE=InnoDB DEFAUTL CHARSET=utf8bm4;

このような結果が得られた場合は、tbl_name CHARSET は utf8mb4 だけれども、col_name は utf8 であることが分かります。このテーブルのカラムをすべて utf8mb4 の CHARSET にしたい場合は、

ALTER TABLE tbl_name CONVERT TO CHARACTER SET utf8mb4;

というクエリを発行します。この確認 (と CHARSET 変更) を、追加面の文字が格納される可能性のあるすべてのテーブルに対して実施します (確認が面倒どうであれば、すべてのテーブルに対して無差別に ALTER TABLE ~ してしまってもいいかもしれませんね)。

いかんともし難い理由により my.cnf を変更できない場合の代替手段

こちらは my.cnf を変更出来ない場合の対処方法です。理由は後述しますが、こちらの対応方法はあまりおすすめできません。

1. データベース / テーブルの CHARSET に utf8mb4 を指定する

この対応方法はまず、データベース or テーブルで文字列を取り扱う際の CHARSET に utf8mb4 を指定することから始まります。

データベース全体の CHARSET を設定するのであれば、CREATE DATABASE でデータベースを作成する際に以下のように指定するとよいでしょう。

CREATE DATABASE
    db_name
  DEFAULT CHARSET
    utf8mb4;

また、テーブルを作成するときに、テーブル単位 or カラム単位で CHARSET を指定することもできます。追加面の文字が入るテーブル / カラムのみ変更したい場合は、こちらの指定がよいでしょう。

/* テーブル単位で指定する場合 */
CREATE TABLE tbl_name (
  col_name VARCHAR(100)
) ENGINE 
    InnoDB
  CHARSET
    utf8mb4;

/* カラム単位で指定する場合 */
CREATE TABLE tbl_name (
  col_name VARCHAR(100) CHARSET utf8mb4
);

2. Java のプログラムから具体的な SQL を発行する前に SET NAMES utf8mb4 を発行する

続いて Java プログラム側の話になります。

INSERT / UPDATE などの具体的な SQL を発行する前に、「コネクション単位で」SET NAMES utf8mb4 のクエリを発行するようにします。つまり以下のプログラムのように、java.sql.Connection オブジェクトを取得した直後に java.sql.PreparedStatement オブジェクトなどを利用して SET NAMES utf8mb4 を発行するようにします。

try (Connection conn = 
         DriverManager.getConnection("jdbc:mysql://localhost/dbname?user=u&password=p")) {
    try (PreparedStatement pstmt = conn.prepareStatement("SET NAMES utf8mb4");
         ResultSet rset = pstmt.executeQuery()) {
    }

    // この後に具体的な SQL を発行する...
}

SET NAMES utf8mb4 の発行結果はコネクションをまたいで引き継がれるわけではないので、コネクションを取得し直す度に毎回 発行し直す必要があります (逆を言えば、同一のコネクション内であれば、再発行する必要はありません)。

この対応方法がおすすめ出来ない理由

MySQL Connector/J のマニュアル 22.3.5.4. Using Character Sets and Unicode を読み進めて見ると分かりますが、

上記のキャプチャのように、「SET NAMES を JDBC のコネクション上で発行すること自体すべきでない」と明記されています。 加えて O/R マッパーを利用する場合、java.sql.Connection オブジェクトの管理が O/R マッパー任せとなるので、いつ SET NAMES utf8mb4 を発行すればよいか、そのタイミングを図るのが非常に難しくなります。

特別な理由がない限りは、my.cnf の設定を変更する方法を利用し、「どうしても my.cnf は触れない/触りたくないんだ…!」というときのみ、こちらの利用をするとよいかと思います。

2013.06.08 削除:以下の対処は不要で、文字数分の大きさの VARCHAR カラムをすれば十分でした。

2. Unicode 追加面 が格納される可能性のある VARCHAR のカラムを、文字数の4倍に設定する

Unicode 追加面の文字を VARCHAR のカラムに格納すると、同文字1文字あたり VARCHAR 4 文字分を消費するようです。そのため、例えば 100 文字格納できるカラムが合った場合に、すべての文字が Unicode 追加面の文字である 100 文字の文字列を格納することを想定して、VARCHAR のカラムの文字数を 400 に設定する必要があります。

つまり、

CREATE TABLE tbl_name (
  col_name VARCHAR(100)
)
のようなテーブルがあったとして、col_name のカラムに実際に 100 文字格納したいんだけど、Unicode 追加面の文字が混じる可能性があるのであれば、
CREATE TABLE tbl_name (
  col_name VARCHAR(400)
)
のようにしましょう、ということです。

2013.06.08 削除:この対処も不要で、特に接続文字列に何かを付け加える必要はありません。

3. JDBC の接続文字列で characterEncoding / connectionCollation を指定する

MySQL のサーバ側の設定などは上記までで、残すは JDBC 周りとなります。JDBC 周りの設定に手を入れない限りは、最初に示したスタックトレースとおさらばすることはできません。

JDBC 周りの設定内容は以下のとおりです。

  • characterEncoding ... UTF-8
  • connectionCollation ... utf8mb4_general_ci

JDBC の接続文字列での指定例は以下になります

jdbc:mysql://host:3306/db_name?characterEncoding=UTF-8&connectionCollation=utf8mb4_general_ci

以上。

2012-09-30

2012/9/30(日)に開催された 第3回 データ構造と情報検索と言語処理勉強会 DSIRNLP で、最近(個人的に)注目の圧縮ライブラリ、LZ4 に関してトークしてきました。

LT という時間枠でしたので、テーマを絞って LZ4 の速さの秘密について話しましたが、秘密が盛りだくさんで話し切れず。時間をオーバーしてご迷惑をおかけしてしまいました。すみません…

発表資料は以下になります。

私の発表はアレですが、LZ4 を知らない方が結構多い集まりでしたので、今回の発表を機に、もっと利用が広まるといいなあと思っています。

以下、個人的な反省点です。

  • LT で 90 ページ超とか無理があり過ぎるのでやめよう。
  • もっと簡潔に話そう。
  • テーマを絞り込もう。
  • いい加減、発表直前まで資料作成をするのはやめよう。

2012-08-29

Play 的に正しいと思われる解決方法が明らかになったのでメモ。Play framework 1.2.5 が対象です。また、フロントの Web サーバは nginx を想定しています。

リバースプロキシと SSL

Play framework を使って開発された Web アプリケーションを本番環境で運用する場合、Play アプリの AP サーバを直接外部に対して公開するのではなく、Apache や Nginx などの Web サーバを前方に立てて、その Web サーバからの HTTP リクエスト(リバースプロキシ)として、Play アプリの AP サーバに HTTP リクエストが到達するようなサーバ構成を取ることが一般的と思われます。

Play に限った話ではありませんが、このようなサーバ構成において SSL (HTTPS) による通信をサポートするとなると、フロントの Web サーバで SSL をデコードし、Web サーバと AP サーバの間を HTTP プロトコルで通信をすることがきっと多いことでしょう。

Play framework でのリダイレクトと HTTPS に関わる問題

ところで、Play framework の play.mvc.Controller クラス (MVC アーキテクチャでいうところのコントローラに相当するもの) には、HTTP ステータスコード 301/302 によりリダイレクトを行う Controller.redirect() メソッドが実装されています。このメソッドを使って redirect("Users.list"); とすると、次のような HTTP レスポンスがクライアントに返却され、その結果、http://localhost:9000/Users/list の URL へのリダイレクトが行われることになります。

HTTP/1.1 302 Found
Server: nginx/1.2.2
Date: Mon, 27 Aug 2012 16:30:14 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 0
Connection: close
Location: http://localhost:9000/Users/list
Cache-Control: no-cache

この Controller.redirect() によるリダイレクトですが、前述したリバースプロキシかつフロントの Web サーバで SSL デコードをする構成で行うと、リダイレクト先の URL が https で始まる URL ではなく、http で始まる URL となってしまいます。これにより、Web アプリケーションを利用している最中のリダイレクトにより、思いがけず HTTPS から HTTP に切り替わってしまうという悩ましい問題が発生し得ることになります。この問題は、Play アプリとフロントの Web サーバとの間のやりとりが HTTP ベースで行われていることによります。

問題の解決方法

この問題を解決する方法として、

  • フロント Web サーバの設定と Play アプリの application.conf の設定を変更する
  • Controller.redirect() メソッドを呼び出す直前で、request.isSecure = Boolean.TRUE とする
などの方法があります。後者の手段は、フロント Web サーバの設定を変更できない残念な状況の最終手段として残しておくとして、Play 的には前者の手段での対応がおすすめされているようです。そういうわけで、前者の方法について以下説明していきます。

application.conf の設定を変更する

リダイレクト先の URL を生成する Play framework の当該処理の実装を見てみるとわかりますが、スキームを http にするか https にするかの判断は、play.mvc.Http クラスの Http#parseXForwarded() メソッドHttp#isRequestSecure() メソッド の処理結果が強く関係してきます。特に、後者のメソッドが実行されて true を返すような処理となる場合に、リダイレクト先 URL のスキームが https となるので、つまりはそのような望ましい結果となるように条件を揃えてあげればいいことになります。

同式を true にするために変更すべき application.conf の設定は、以下の2つになります。

  • XForwardedSupport=all を追記する ... これにより、Play アプリに渡される HTTP リクエストのヘッダに X-Forwarded-For フィールドが存在すれば、無条件に Http#isRequestSecure() が実行されるようになります。
  • XForwardedProto=https とする ... これを設定することにより、X-Forwarded-Proto フィールドや X-Forwarded-Ssl フィールドの値によらず、X-Forwarded-For フィールドが定義された状況下での Play アプリに対するリクエストを HTTPS でリクエストされたものとして扱うようになります。Web アプリ全体を HTTPS で提供する場合に適した設定となります。
このあたりの設定は実は Play のドキュメント にも書かれています(私は、この内容から今回の目的に対する解決策を汲み取ることができませんでした…)。

nginx.conf の設定を変更する

nginx.conf の設定は簡単で、リバースプロキシの設定において proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; を仕込んであげれば OK です。

解決方法は以上となります。もっとこと細かに HTTP / HTTPS を制御したい、としても、基本は play.mvc.Http クラスの、特に前述した2つのメソッドの実装を眺めれば解決するものと思います。

2012-08-17

業務システムとかを開発していると、たまにこの手の処理がしたくなるのですが、

Date date = new Date();
for (int i = 0; i < 10; i++) {
  Calendar cal = Calendar.getInstance();
  cal.setTime(date);
  cal.add(Calendar.HOUR, 1);
  date = cal.getTime();

  // ここに date オブジェクトを利用した処理が書かれる...
}
こんなコードを書きたくない人向けの Iterator / Iterable クラスがこちらになります。


2012-07-02

先月末に前職を退職したばかりですが、本日より新宿・代々木にある、とある「分析力をコアとする情報最適化企業」でエンジニアとして働き始めることになりました。新しい仲間のみなさま、これからよろしくお願いします。また、および前職でお世話になったみなさま、これから『も』よろしくお願いいたします。

転職先・仕事について

初めての転職でもあり、ゆっくり時間をかけて、次の会社を探す・・・つもりでしたが、思いのほか早く、いまの自分のやりたいこと(のひとつ)である「ビジネスに近い立ち位置での分析・データマイニング」が実現できる会社を選ぶことができました。

入社後は、まずはエンジニアリングで会社に貢献していくつもりです。データサイエンティストの方々 が、よりよくデータ解析できるように、エンジニアリング面でのサポートもしていきたいですね。幸いなことに(?)、エンジニアリングの分野には楽しそうな課題やこの先取り組んでいきたい技術などが転がっているそうなので、これからの仕事が本当に楽しみです。

もちろんエンジニアリングだけでなく、データ分析の分野にもいろいろと楽しそうな案件があるそうなので、ゆくゆくは分析力をつけて、データサイエンティストな道も狙ってみたいなあ、と、ほんのりと野望を抱いていたりします。

お礼

とてもありがたいことに、今回の退職・転職にあたって、いくつかの企業の方からお話をいただくことがありました。また、知人経由でエージェントの方をご紹介していただくこともありました。その他、勉強会などの場で、転職についてのご相談をしていただくこともありました。

実際に各社のお話を伺わせていただき、それぞれの会社の魅力を知ることで、正直、どの道に進むべきか迷った時期もありましたが、結果として「ビジネスに近い立ち位置での分析・データマイニング」「B2C ・大規模な Web サービス」の二つのやりたいことのうち、前者に注力できるよい環境を持っている・整っている今の会社を選ぶに至りました。

お話をいただいていた企業の方々には、忙しい時間を割いてお話する機会をいただきまして感謝しております。特に某社の S さんにはかなり深いご相談をさせていただきまして、とても感謝しております。

あ、もちろん、入社のきっかけにもなった某オレオの人にも感謝してますよ・・・!

みなさま本当にありがとうございました。それではこれからも(これからが本番ですが)よろしくお願いします。

2012-06-23

実に 1 年 4,5 ヶ月ぶり? に TokyoWebmining でトークをしてきました。今回はソーシャルグラフ(social network)における「リンク予測」と、ソーシャルグラフを利用した「レコメンデーション」の2つのネタで話をしました。

ちなみに、発表資料に載せていたソーシャルグラフは TouchGraph を使って描いています。

以下、個人的な反省点です。

  • 発表時間は 80 分でも足りない。100 分ぐらいの時間枠を要求しよう。
  • Random Walk (with Restarts) ネタを振るときは、ちゃんと説明できる準備をしよう (地雷なテーマっぽいので避けてしまうかな…)。
  • 論文紹介をするなら、ちゃんと細かなところまで読んでおこう。
  • 資料作りは早めに取りかかろう…

2012-06-16

lz4-ruby という、native extensions (C 拡張) を含む Ruby gem を開発していて、Linux/Windows 両方に対応した gem パッケージを作るのに苦労したので、その内容を備忘録がてらにまとめます。

gem の開発環境構築と、実際の gem 開発に分けてエントリを書く予定です。このエントリは、前者の開発環境構築についてのエントリとなります。

構築する開発環境について

今回構築する開発環境は Linux をベースとし、jeweler により gem のスケルトンを作成、mingw32/rake-compiler で Windows 向けのプリコンパイルされた native extension を作る構成になります。

以下、開発を行う OS、パッケージ、Ruby 環境、gem パッケージのそれぞれについてまとめています。

OS

後述する mingw32 のコンパイル環境を整備する都合により、Ubuntu Desktop の利用を強くおすすめします。CentOS、Scientific Linux などの RHEL クローンな Linux ディストリビューションでも環境構築は不可能ではないと思いますが、おすすめできません(私は途中で挫けました)。以降の説明も Ubuntu Desktop を前提としています。

debian パッケージ

mingw32 のコンパイル環境を整えるために、 として mingw32 パッケージをインストールします。RHEL クローン OS の場合は mingw32-runtime やら mingw32-gcc やら、いろんなパッケージをインストールしなければならなかったり、そもそも CentOS 6.2 向けの mingw32 関連パッケージが揃っていなかったりと茨の道となっているので、RHEL クローン OS で頑張る人は相当の覚悟をもって臨んでください。

Ruby 環境

rvm を導入して、異なるバージョンの Ruby 環境を切り替えられるようにしておきます。インストールは、https://rvm.io/rvm/install/ を参考に、以下のようにします。

rvm をインストールした後は、本来なら実際の Ruby 実行環境の構築(インストール)を進めたいところですが、その前に一つやるべきことがあります。

rvm install ほげほげ で Ruby の実行環境を構築した場合に、標準でインストールされる gem パッケージを列挙・指定する ~/.rvm/gemsets/global.gems というファイルがあります。このファイルをテキストエディタなどで開いて、bundler が記述された行を削除・上書き保存し、bundler が標準インストールされないようにします。この作業は、jeweler が要求する bundler のバージョンと、標準インストールされる bundler のバージョンが合致しないために必要な作業になります。

global.gems のファイル編集が済んだところで、実際の Ruby 環境を構築します。以下のように、1.8 系と 1.9 系の最新バージョンをインストールします。

gem パッケージ

下記 2 つの gem パッケージを、gem コマンドを用いてインストールします。

  • bundler
    Gemfile を用いた、依存する gem のインストールや各種コマンドの実行に必要
  • jeweler
    gem のスケルトン作成に必要

前者の bundler は、1.8 系、1.9 系の両方にインストールします。インストールする bundler のバージョンは、1.0 系の最新版とします。

jeweler は 1.8 系、1.9 系の環境どちらにインストールしても構いません(開発の主体となる Ruby 環境にいいれるといいでしょう)。

rake-compiler については bundler 経由でのインストールとなるので、ここではまだインストールはしません。

以上で開発環境の構築は終わりです。次のエントリで、実際に gem を開発する作業について記述します。

2012-06-05

(主に社員宛て)。今月 6 月末をもちまして、いま勤めている会社を退職します。

退職エントリをポストするにはまだちょっと早いですが、直接、退職の意思を伝えたい社員にはほぼ伝えきれたことと、退職するまでの間に、他の社員には意思というか真意を伝える機会がなかなかないと感じたので、このタイミングでポストします。

現職での業務

いまの会社に新卒で入社し、6 年とちょっと、お世話になりました。

数値解析業務、コンシューマ向けパッケージ製品開発・販売業務など、いろいろ手広くビジネスをしている独立系の中小企業にて、業務システム開発を担当する SIer 的な部署に所属し、入社後はずっと、エンジニアリングを軸としたお仕事をしてきました。

同部署では主として、Web 系の業務システム開発にフロントエンドからサーバサイドまで携わってきました。その一方で、とある研究所の研究者の方とご一緒に仕事をさせていただき、レコメンデーションエンジンの研究開発のお手伝いをすることもありました(自分の中で、一番思い入れのある仕事でした)。近年は Web ベースの地図アプリケーションの開発に多く携わってきました。その他、リッチクライアント系だったり、家電組み込み系だったり、フィーチャーフォンアプリ開発だったり・・・

振り返ってみると、いままでやってきた仕事はバリエーションに富んでおり、また技術的に挑戦し甲斐のあるお仕事が多かったなあ、と気付かされます。もちろん、残念なお仕事もありましたが、それでも私は仕事に恵まれていたんだなあ、と思いました。

会社を辞めること

退職の理由は、会社のやり方、特に経営層の考え方・行動に対する不満が限度を超えた1、というわけではなく、過去の人事に対する不満2というわけでもありません。理由の一つは、「この先の SI 業界に対する懸念」、もう一つの理由は「自分の心境の変化」です。

前者の懸念には、新人PGの0円セール などにもあるように、現状においても単価競争にあえいでいる SI 業界の今後・行く末、そして、このまま SIer に勤めていては、実務に活かせる技術スキルが身につかない・伸び悩んでしまうのではないか、という思い・不安が挙げられます。 後者は、SI というお仕事に対する魅力3が薄れてしまったこと、現状の対企業的な仕事ではなく、より消費者に近い位置で仕事をしたい、などの気持ちの変化が挙げられます。

一方でいまの会社に対する不満は、なくはないにせよ、人間関係はとてもよく、いい上司・後輩に恵まれた職場で 8~9 割は楽しく仕事ができたわけで、退職の理由にはなりえませんでした。社員の皆様には本当にお世話になりました。

今後

「今月末で辞める」ことは決めたものの、転職先など、この先は未定です。

転職よりも退職を先に決めたことには、退路を断って自身を行動せざるを得ない状況に追い込むという意味があります。こういうことをしない限り、現状に甘んじてしまい、次の行動をとることができずに過ごしてしまう自分がいることを嫌というほど理解しているからです。

今後は、エンジニアとして仕事に関わっていくスタンスは崩さず、自分が関わっていきたいと考えている「ビジネスに近い立ち位置での分析・データマイニング」「B2C ・大規模な Web サービス」の分野にて、真に自分がやりたいことを実現でき、ハイレベルなエンジニアリングができる環境をじっくりと探すつもりです。

会社の外で、自分のエンジニアとしての能力がどこまで通用するか分かりませんが、挑戦はしていきたいと思っています。これからもよろしくお願いします。


1 : 不満はありますが、退職の理由ではありません。
2 : 納得いかないこともありましたが、退職の理由ではありません。
3 : さまざまな業種・仕事に関われるのが、SIer に勤めることの魅力だと思っています。

2012-05-31

簡潔データ構造を利用したイケてる trie 実装 marisa-trie を Node.js で扱えるようにするための、誰得感溢れるモジュールを作ってみました。ソースコードと npm は以下のとおりに公開しています。

とりあえずデモ的なことをしてみましょう

ひとまず、marisa-trie のsample.cc の JavaScript 版を実行することを目標としましょう。

まずは、marisa-trie のインストールをします。

環境によっては、/usr/local/lib にインストールされた共有ライブラリが参照されなかったりするので、その場合は /etc/ld.so.conf.d/ ディレクトリあたりに /usr/local/lib ディレクトリ配下の共有ライブラリを参照させるような .conf ファイルを配置してください。

あとは node-marisa-trie を npm でインストールし、デモプログラムを実行するだけ、です。

API 構成について

詳細はいずれ作成する予定のドキュメントに譲るとして、原則として C++ 版の marisa-trie の API に準拠しています。ただし、[] 演算子など、一部実装ができていない関数があります。

今後の予定

  • テストコードを書いて自動テストしたい。
  • 未実装の機能を実装したい。
  • 使い方・API のドキュメントを充実させたい。
  • ドキュメントを英語化したい。
  • node-gyp でビルドできるようにしたい (node v0.7.x 対応)。

参考にした情報

本モジュールを作成するにあたり、参考にした情報をポインタ的に列挙しておきます。

2012-04-23

とっても残念なことに、Heroku Toolbelt のインストーラが CentOS やら Scientific Linux などの RHEL クローンな OS に対応していない (Linux は Debian/Ubuntu のみサポート) らしいので、頑張って (中途半端に) 環境構築したメモを残しておきます。

ざっくり手順

gcc やら git、Ruby のビルドに必要なライブラリ (openssl-devel などなど)、ごく基本的な開発環境がすでに整っていることを前提とします。

  1. rvm をインストールする
  2. Ruby をインストールする
  3. foreman をインストールする
  4. heroku CLI をインストールする
  5. heroku コマンドにパスを通す


rvm をインストールする

rvm を入れなければならない、というわけではありませんが… rvm がお好みに合わないようであれば、rbenv でもいいでしょうし、漢らしくソースコードからビルドして make & sudo make install でもいいでしょう。

場合によっては、rvm のインストールにより ~/.bash_profile に追記された以下の1行を ~/.bashrc 側に移行する必要があるかもしれません。


Ruby をインストールする

rvm なあなたは としましょう。Ruby のバージョンはお好みで。


foreman をインストールする

プロセスなどを管理してくれるものらしい (よくわかっていない) foreman を、gem を使ってインストールします。 この gem は必須ではありませんが、インストールしておくとローカルの環境での動作確認がちょっと楽になります。


heroku CLI をインストールする

いよいよ heroku CLI をインストールします。tarball をダウンロード&展開し、任意のディレクトリに配置します。例えば以下のように。


heroku コマンドにパスを通す

~/.bashrc あたりを編集して、heroku コマンドにパスを通します。上記の例でいえば、以下のようになります。

以上で環境構築は完了です。これでどこでも heroku ができるようになりますね!

2012-02-16

Eclipse を使って開発した Play のアプリケーションを Eclipse 上でデバッグしようとすると、Play アプリケーションのプロセスとデバッガの連携に失敗する問題に遭遇することがあったりします。このエントリでは、その一つの原因と対処法について解説します。


現象

今回対象とする事象は、公式に書かれている内容 に従ってデバッグ手順を進めていて、Connect JPDA to プロジェクト名.launch をデバッグ実行しようとした際に、以下のようなエラーダイアログが表示される場合を想定しています。


原因

play のアプリケーションを、play eclipsify コマンドを用いて Eclipse プロジェクトに変換する際に、 というメッセージが表示されていませんでしたか?(※)

これはメッセージにあるとおり、デバッグ用に利用しようとしている既定のポート 8000 番を別のプログラムがすでに利用しているよ、ということを意味します。

このメッセージが表示されて eclipsify された Play アプリケーションは、Eclipse 上でのデバッグ実行時のデバッガ連携に利用する待ち受けポート番号が不定(アプリを立ち上げるたびにポート番号が変化する状態)となります。そのため、Play アプリケーションが待ち受けするデバッグ用ポート番号と、デバッガが期待するポート番号が食い違うために連携できず… という状態に陥ってしまい、結果としてデバッグできない状態となります。

※「play eclipsify したときの出力メッセージなんて憶えてないや…」という方は、実際に Play アプリケーションを Eclipse 上で実行したときのアプリケーションログで確認することもできます。 Play アプリを立ち上げると、最初に以下のようなログが出力されますが、この ~address: XXXX の部分の数字が既定のポート番号(8000)以外で、かつ Play アプリケーションを起動するたびに変化するようであれば、上述した状態となっていることがわかります。


対処法

この問題を解決するには、デバッグ用のポート番号を 8000 番以外の空いている任意の番号に変更(&固定)する必要があります。対処には以下の3つの作業が必要になります。

  1. デバッグに利用するポート番号を決める
  2. play eclipsify で生成されるファイル プロジェクト名.launch を編集する
  3. 同様に play eclipsify で生成されるファイル Connect JPDA to プロジェクト名.launch を編集する

順を追って、説明していきます。


1. デバッグに利用するポート番号を決める

開発 PC にインストールしているソフトウェアなどを確認し、デバッガと Play アプリケーションの連携に利用する任意の空きポート番号を決めます。


2. プロジェクト名.launch を編集する

1. で決めたデバッグ用のポート番号の値を、プロジェクト名.launch 内に記述されている Java VM 引数の文字列に設定します。

Java VM 引数の文字列は、key 属性が org.eclipse.jdt.launching.VM_ARGUMENTS である stringAttribute 要素に設定されています。 その文字列の ~,address=0 となっている部分の数字を、1. で決めたポート番号に設定しなおします。

以下の例では、ポート番号 8123 をデバッグ用のポートとして割り当てています。


3. Connect JPDA to プロジェクト名.launch を編集する

2. と同様に、デバッグ用のポート番号を Connect JPDA to プロジェクト名.launch 内に記述されている接続先ポート番号として設定します。

接続先ポート番号は、key 属性が port である mapEntry 要素の value 属性に設定されています。

以下は先ほどと同様に 8123 のポート番号を割り当てる例です。


補足

なお、application.conf にも JPDA のポート番号を指定する設定項目 jpda.port がありますが、Eclipse 上で実行する場合は起動時オプションの方が優先されるので、こちらを指定・変更する必要はありません。

2012-01-14

Play framework を扱う案件に最近携わっていて、いろいろとドキュメントにないノウハウが溜まりつつあるのでここらでメモ書きしておきます。


Play framework とトランザクション

Play framework では、HTTP リクエストはもちろんのこと、非同期ジョブにおいてもトランザクションが自動的に提供されます。通常の業務であれば、1つの HTTP リクエスト処理・非同期ジョブに1つのトランザクションがあればそれで十分ですが、ちょっと込み入ったことをやろうとすると、例えば

  • 監査用の操作ログ・クエリログをテーブルに出力したい
    →コミットされるタイミングは後でもいいんだけど、本流の処理で転けてロールバックかけた場合に、操作ログまでなかったことになるのは避けたい。
  • 非同期ジョブなどで、ジョブ管理テーブルを随時監視・更新したい
    →本流の処理のトランザクションはコミットしたくないけど、ジョブ管理テーブルへの変更は即座にコミットして反映させたい。
など、1つのトランザクションではちょっと実現が難しいこともあったりします。前者の例は、トランザクションを二つに分けたとして互いが重ならないように工夫できるでしょうが、後者はそう簡単にはいきません。互いのトランザクションが一部重なり、同時並列で存在しなければならない状況が発生し得ます。

ところで Play framework 的には、「一連の処理で複数のトランザクションを扱う」ことは「レールから外れた」行為である可能性が高く、このような行為を標準でサポートしているわけでもなく、当然ながら (私の知る限り) ドキュメント・リファレンスの類いに書かれているわけでもありません。


Play のエンティティと EntityManager

Play 標準では複数トランザクションを明示的にはサポートしていませんが、複数トランザクションを扱えないと困ることがあるのは事実なので、何とかしたいところです。何とかするには、まず Play がどのようにトランザクションを提供してくれているかを把握する必要があります。そこで、Play の実装、特にトランザクション周りをソースコードリーディングしていくことにします。

初めは、Play における永続化、特に トランザクションと関わりの深い EntityManager 周りの扱いを確認してみましょう。

EntityManager は、Play では play.db.jpa.JPA クラスフィールド entityManager で管理されています。JPA クラスのオブジェクト自体は、ThreadLocalクラスフィールド JPA.local で管理されています。ThreadLocal オブジェクトで管理されていることから、JPA オブジェクトそのもの、またそのフィールドにある EntityManager オブジェクトは、スレッド毎に用意されていることがわかります。

Play のエンティティは、この JPA.local クラスフィールドで管理されている、スレッド毎に用意された EntityManager を利用して永続化やクエリを実現しています。例えば、Model#save() メソッド から呼び出される play.db.jpa.JPABase クラス_save() メソッド を見てみましょう。JPABase.em() メソッドから始まり、呼び出し先を追っていくと JPA.em() → JPA.get().entityManager → JPA.local.get().entityManager と、ThreadLocal オブジェクトで管理されている JPA オブジェクトのフィールドにたどり着くことができます。Model#find() メソッド も同様で、その内部ではやはり JPA.local クラスフィールドで管理されている EntityManager を利用します。


トランザクションの開始・終了

トランザクションと関わりの深い EntityManager が、JPA.local にスレッド別になるよう管理されていることがわかりました。次に、トランザクションの開始と終了について確認してみましょう。

play.db.jpa.JPAPlugin クラス を見てみます。このクラスの startTx() クラスメソッド と、closeTx() クラスメソッド がそれぞれトランザクションの開始・終了処理を担当します。JPAPlugin.startTx() クラスメソッドの実装を見ると、その最後で JPA.createContext() クラスメソッド を呼び出しています。この JPA.createContext() クラスメソッドの実装を見て明らかなように、

  1. 既存の JPA オブジェクト、EntityManager オブジェクト (≒トランザクション) が存在する場合は、その EntityManager オブジェクトをクローズ(≠コミット)し、
  2. 新たな JPA オブジェクトを生成して、JPA.local クラスフィールドの ThreadLocal オブジェクトに設定する
という処理が、トランザクション開始時に行われます。つまり「新しいトランザクションを開始しようとすると、既存のトランザクションは破棄された上で新しいトランザクションが始まる」ということになります。

なお、JPAPlugin.startTx()/closeTx() 両クラスメソッドは、それぞれ HTTP リクエストに対応するコントローラ・アクションメソッドの呼び出し前後、非同期ジョブの実行前後に呼び出され、標準のトランザクションを構成します。


複数トランザクションを並列して扱う方法

上記のように、JPAPlugin.startTx() クラスメソッドを呼び出すことで新しいトランザクションの開始はできますが、古い・既存のトランザクションは破棄されて利用できなくなってしまいます。このままでは、複数のトランザクションを並列して扱うことはできません。それではどのようにして複数トランザクションを実現すればよいのでしょうか?

実は結構簡単で、新しいトランザクションを開始する前に、既存のトランザクション(JPA オブジェクト)を JPA.local より待避し、空っぽの状態にしてしまえばいいのです。

コードで表すと、以下のようになります

もとのトランザクションに復帰させたいときは、待避しておいた JPA オブジェクトを JPA.local に設定する(ThreadLocal#set())だけです。

実装がやや煩雑になってしまうのが難点ですが、待避した JPA オブジェクトを Stack で管理したり、または Map で管理するなどして、複数トランザクション管理機能をクラス化してしまえば多少は扱いやすくなるのではないでしょうか。


注意点

以上が Play で複数並列にトランザクションを扱う方法になりますが、1つ、注意しなければならないことがあります。

先に述べたとおり、JPAPlugin.startTx()/closeTx() クラスメソッドは HTTP リクエスト処理のアクションの前後に (暗黙的に) 呼び出されます。特に JPAPlugin.closeTx() については、アクションのメソッド内で例外が発生したとしても必ず呼ばれる実装になっています(例外が発生したら closeTx(true) でロールバック、例外発生がなければ closeTx(false) でコミットするように呼び出されます)。

しかし、この暗黙的な JPAPlugin.closeTx() 呼び出しでクローズされるのは、JPA.local に設定されている JPA オブジェクトのトランザクションだけであり、たとえば上記コードの 8 行目で待避した JPA オブジェクトについては、25 行目で再び JPA.local に戻されるまでの間に例外が発生すると、クローズされずに放置プレイ状態になってしまいます。

そのため、JPA.local から JPA オブジェクトを待避させる場合は例外発生に注意し、たとえ例外が発生したとしても、待避された JPA オブジェクトのトランザクションを確実に・明示的にクローズする実装が必要になるでしょう。