Keras で MNIST を分類

Keras で iris を分類」 に続き、今回は Keras で畳み込みニューラルネットを使った MNIST の分類を試してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20160920/

準備

Docker で実行するための Docker イメージを作成します。

Docker イメージ作成

/vagrant/work/keras/Dockerfile
FROM python

RUN apt-get update && apt-get upgrade -y

RUN pip install --upgrade pip

RUN pip install keras
RUN pip install h5py

RUN apt-get clean

h5py は Keras の save_model 関数を使うために必要でした。

今回のバージョンでは keras をインストールしても h5py は自動的にインストールされなかったので、別途インストールするようにしています。

上記を docker build して Docker イメージを作成しておきます。

Docker ビルド
$ cd /vagrant/work/keras
$ docker build --no-cache -t sample/py-keras:0.2 .

(1) MNIST データセットの取得

keras.datasets の mnist を使うと MNIST データセットを取得できます。(S3 からダウンロードするようになっている)

load_data 関数で取得したデータセット[[<学習用画像データ>, <学習用ラベルデータ>], [<評価用画像データ>, <評価用ラベルデータ>]] のような内容となっていましたが(画素の値は 0 ~ 255)、そのままでは今回の用途に使えなかったので numpy を使って変換しています。

/vagrant/work/mnist_helper.py
import numpy as np
from keras.datasets import mnist
from keras.utils import np_utils

# 学習用のデータセット取得
def train_mnist():
    return convert_mnist(mnist.load_data()[0])

# 評価用のデータセット取得
def test_mnist():
    return convert_mnist(mnist.load_data()[1])

def convert_mnist(tpl):
    # 画像データの加工
    features = tpl[0].reshape(tpl[0].shape[0], 1, 28, 28).astype(np.float32)
    features /= 255

    # ラベルデータの加工 (10種類の分類)
    labels = np_utils.to_categorical(tpl[1], 10)

    return (features, labels)

(2) 畳み込みニューラルネットモデル

畳み込みニューラルネットのモデルを作成してバイナリファイルとして保存する処理です。

畳み込みのレイヤー構成は 「ConvNetJS で MNIST を分類2」 と同じ様にしてみました。 (活性化関数は relu を使用)

/vagrant/work/create_layer_conv.py
import sys

from keras.models import Sequential, save_model
from keras.layers.core import Dense, Activation, Flatten
from keras.layers import Convolution2D, MaxPooling2D

model_dest_file = sys.argv[1]

model = Sequential()

# 1つ目の畳み込み層(5x5 で 8個出力)
model.add(Convolution2D(8, 5, 5, input_shape = (1, 28, 28)))
model.add(Activation('relu'))

# 1つ目のプーリング層 (最大プーリング)
model.add(MaxPooling2D(pool_size = (2, 2), strides = (2, 2)))

# 2つ目の畳み込み層(5x5 で 16個出力)
model.add(Convolution2D(16, 5, 5))
model.add(Activation('relu'))

# 2つ目のプーリング層 (最大プーリング)
model.add(MaxPooling2D(pool_size = (3, 3), strides = (3, 3)))

model.add(Flatten())

model.add(Dense(10))
model.add(Activation('softmax'))

model.compile(loss = 'categorical_crossentropy', optimizer = 'adam', metrics = ['accuracy'])

# モデルの保存
save_model(model, model_dest_file)

(3) 学習処理

学習処理は以下の通りです。

学習用の MNIST データセットを使って fit を実行します。

/vagrant/work/learn_mnist.py
import sys

from keras.models import save_model, load_model
from mnist_helper import train_mnist

epoch = int(sys.argv[1])
mini_batch = int(sys.argv[2])

model_file = sys.argv[3]
model_dest_file = sys.argv[4]

# モデルの読み込み
model = load_model(model_file)

# 学習用 MNIST データセット取得
(x_train, y_train) = train_mnist()

# 学習
model.fit(x_train, y_train, nb_epoch = epoch, batch_size = mini_batch)

# 学習後のモデルを保存
save_model(model, model_dest_file)

(4) 評価処理

評価処理は以下の通りです。

評価用の MNIST データセットを使って evaluate を実行します。 verbose を 0 にすれば途中経過を出力しなくなるようです。

/vagrant/work/eval_mnist.py
import sys

from keras.models import load_model
from mnist_helper import test_mnist

model_file = sys.argv[1]

model = load_model(model_file)

# 評価用 MNIST データセット取得
(x_test, y_test) = test_mnist()

# 評価
(loss, acc) = model.evaluate(x_test, y_test, verbose = 0)

print("loss = %f, accuracy = %f" % (loss, acc))

実行

まずは、作成した Docker イメージ(py-keras)を使って Docker コンテナを起動します。

Docker コンテナ起動
$ docker run --rm -it -v /vagrant/work:/work sample/py-keras:0.2 bash

# cd /work

起動した Docker コンテナで、畳み込みニューラルネットのモデルを作成してファイルへ保存します。

1. モデル作成
# python create_layer_conv.py 1.model

Using Theano backend.

保存したファイルを使って学習を行います。 今回はミニバッチサイズ 200 で 3 回繰り返してみます。

初回実行時は MNIST データセットのダウンロードが行われます。

2. 学習
# python learn_mnist.py 3 200 1.model 1a.model

Using Theano backend.
Downloading data from https://s3.amazonaws.com/img-datasets/mnist.pkl.gz
・・・
Epoch 1/3
60000/60000 [==============================] - 116s - loss: 0.7228 - acc: 0.7931
Epoch 2/3
60000/60000 [==============================] - 116s - loss: 0.1855 - acc: 0.9458
Epoch 3/3
60000/60000 [==============================] - 115s - loss: 0.1352 - acc: 0.9591

最後に、学習後のモデルを使って評価用のデータセットを評価します。

3. 評価
# python eval_mnist.py 1a.model

Using Theano backend.
loss = 0.102997, accuracy = 0.968600

Gradle で ScalaPB を使う

前回と同様の処理を ScalaPB で行ってみました。

ScalaPB であればビルドツールに sbt を使う方が簡単かもしれませんが、引き続き Gradle を使います。

今回作成したソースは http://github.com/fits/try_samples/tree/master/blog/20160905/

proto ファイル

前回と同じファイルですが、ファイル名に - を含むと都合が悪いようなので ※ ファイル名だけ変えています。

※ ScalaPB 0.5.40 では、デフォルトで proto ファイル名が
   そのままパッケージ名の一部となりました
   (パッケージ名は <java_package オプションの値>.<protoファイル名> )

ちなみに、java_outer_classname のオプション設定は無視されるようです。

proto/addressbook.proto (.proto ファイル)
syntax = "proto3";

package sample;

option java_package = "sample.model";
option java_outer_classname = "AddressBookProtos";

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phone = 4;
}

message AddressBook {
  repeated Person person = 1;
}

Gradle ビルド定義

基本的な構成は前回と同じですが、ScalaPBC を実行してソースを生成する等、Scala 用に変えています。

build.gradle
apply plugin: 'scala'
apply plugin: 'application'

// protoc によるソースの自動生成先
def protoDestDir = 'src/main/protoc-generated'
// proto ファイル名
def protoFile = 'proto/addressbook.proto'

mainClassName = 'SampleApp'

repositories {
    jcenter()
}

configurations {
    scalapbc
}

dependencies {
    scalapbc 'com.trueaccord.scalapb:scalapbc_2.11:0.5.40'

    compile 'org.scala-lang:scala-library:2.11.8'
    compile 'com.trueaccord.scalapb:scalapb-runtime_2.11:0.5.40'
}

task scalapbc << {
    mkdir(protoDestDir)

    javaexec {
        main = 'com.trueaccord.scalapb.ScalaPBC'
        classpath = configurations.scalapbc
        args = [ protoFile, "--scala_out=${protoDestDir}" ]
    }
}

compileScala {
    dependsOn scalapbc
    source protoDestDir
}

clean {
    delete protoDestDir
}

サンプルアプリケーション

こちらも前回と同じ処理内容ですが、ScalaPB 用の実装となっています。

src/main/scala/SampleApp.scala
import java.io.ByteArrayOutputStream

import sample.model.addressbook.Person
import Person.PhoneNumber
import Person.PhoneType._

object SampleApp extends App {

    val phone = PhoneNumber("000-1234-5678", HOME)
    val person = Person(name = "sample1", phone = Seq(phone))

    println(person)

    val output = new ByteArrayOutputStream()

    try {
        person.writeTo(output)

        println("----------")

        val restoredPerson = Person.parseFrom(output.toByteArray)

        println(restoredPerson)

    } finally {
        output.close
    }
}

ビルドと実行

ビルドと実行の結果は以下の通りです。

前回と違って今回のビルド(scalapbc の実施)には python コマンドが必要でした。※

※ python コマンドを呼び出せるように環境変数 PATH 等を設定しておきます
   今回は Python 2.7 を使用しました
実行結果
> gradle run

・・・
:scalapbc
protoc-jar: protoc version: 300, detected platform: windows 10/amd64
protoc-jar: executing: [・・・\Temp\protoc8428481850206377506.exe, --plugin=protoc-gen-scala=・・・\Temp\protocbridge9000836851429371052.bat, proto/addressbook.proto, --scala_out=src/main/protoc-generated]
:compileScala
・・・
:run
name: "sample1"
phone {
  number: "000-1234-5678"
  type: HOME
}

----------
name: "sample1"
phone {
  number: "000-1234-5678"
  type: HOME
}


BUILD SUCCESSFUL

scalapbc タスクの実行によって以下のようなソースが生成されました。

  • src/main/protoc-generated/sample/model/addressbook/AddressBook.scala
  • src/main/protoc-generated/sample/model/addressbook/AddressbookProto.scala
  • src/main/protoc-generated/sample/model/addressbook/Person.scala

java_package オプションの設定値は反映されていますが、java_outer_classname オプション設定の方は無視されているようです。

Gradle で Protocol Buffers を使う - Java

Gradle を使って Protocol Buffers の protoc で Java ソースコードを生成し、ビルドしてみます。

Gradle から protoc コマンドを呼び出す方法もありますが、今回は protoc-jar を使いました。

protoc-jar を使うと、プラットフォームの環境に応じた protoc コマンドを TEMP ディレクトリへ一時的に生成して実行してくれます。

今回作成したソースは http://github.com/fits/try_samples/tree/master/blog/20160829/

proto ファイル

今回は以下の proto ファイル(version 3)を使います。

Protocol Buffers では、proto ファイルを protoc コマンドで処理する事で任意のプログラム言語のソースコードを自動生成します。

java_packagejava_outer_classname オプションで Java ソースコードを生成した際のパッケージ名とクラス名をそれぞれ指定できます。

proto/address-book.proto (.proto ファイル)
syntax = "proto3";

package sample;

option java_package = "sample.model";
option java_outer_classname = "AddressBookProtos";

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phone = 4;
}

message AddressBook {
  repeated Person person = 1;
}

Gradle ビルド定義

compileJava タスクの実行前に com.github.os72.protocjar.Protoc を実行して src/main/protoc-generated へソースを自動生成する protoc タスクを定義しました。

protoc-jar モジュールをクラスパスへ指定するため protoc 用の configurations を定義しています。

なお、今回は Java 用のソースコードを生成するため --java_out オプションを使っています。

build.gradle
apply plugin: 'application'

// protoc によるソースの自動生成先
def protoDestDir = 'src/main/protoc-generated'
// proto ファイル名
def protoFile = 'proto/address-book.proto'

mainClassName = 'SampleApp'

repositories {
    jcenter()
}

configurations {
    protoc
}

dependencies {
    protoc 'com.github.os72:protoc-jar:3.0.0'

    compileOnly 'org.projectlombok:lombok:1.16.10'

    compile 'com.google.protobuf:protobuf-java:3.0.0'
}
// protoc の実行タスク
task protoc << {
    mkdir(protoDestDir)

    // protoc の実行
    javaexec {
        main = 'com.github.os72.protocjar.Protoc'
        classpath = configurations.protoc
        args = [ protoFile, "--java_out=${protoDestDir}" ]
    }
}

compileJava {
    dependsOn protoc
    source protoDestDir
}

clean {
    delete protoDestDir
}

サンプルアプリケーション

protoc で自動生成したクラスを動作確認するための簡単なサンプルを用意しました。

src/main/java/SampleApp.java
import static sample.model.AddressBookProtos.Person.PhoneType.*;

import lombok.val;

import java.io.ByteArrayOutputStream;

import sample.model.AddressBookProtos.Person;
import sample.model.AddressBookProtos.Person.PhoneNumber;

class SampleApp {
    public static void main(String... args) throws Exception {

        val phone = PhoneNumber.newBuilder()
                        .setNumber("000-1234-5678")
                        .setType(HOME)
                        .build();

        val person = Person.newBuilder()
                        .setName("sample1")
                        .addPhone(phone)
                        .build();

        System.out.println(person);

        try (val output = new ByteArrayOutputStream()) {
            // シリアライズ処理(バイト配列化)
            person.writeTo(output);

            System.out.println("----------");

            // デシリアライズ処理(バイト配列から復元)
            val restoredPerson = Person.newBuilder()
                                    .mergeFrom(output.toByteArray())
                                    .build();

            System.out.println(restoredPerson);
        }
    }
}

ビルドと実行

ビルドと実行の結果は以下の通りです。

実行結果
> gradle run

:protoc
protoc-jar: protoc version: 300, detected platform: windows 10/amd64
protoc-jar: executing: [・・・\Local\Temp\protoc3178938487369694690.exe, proto/address-book.proto, --java_out=src/main/protoc-generated]
:compileJava
・・・
:run
name: "sample1"
phone {
  number: "000-1234-5678"
  type: HOME
}

----------
name: "sample1"
phone {
  number: "000-1234-5678"
  type: HOME
}


BUILD SUCCESSFUL

なお、protoc で以下のようなコードが生成されました。

src/main/protoc-generated/sample/model/AddressBookProtos.java
package sample.model;

public final class AddressBookProtos {
  private AddressBookProtos() {}
  ・・・
  public  static final class Person extends
      com.google.protobuf.GeneratedMessageV3 implements
      // @@protoc_insertion_point(message_implements:sample.Person)
      PersonOrBuilder {
    ・・・
    public enum PhoneType
        implements com.google.protobuf.ProtocolMessageEnum {
      /**
       * <code>MOBILE = 0;</code>
       */
      MOBILE(0),
      /**
       * <code>HOME = 1;</code>
       */
      HOME(1),
      /**
       * <code>WORK = 2;</code>
       */
      WORK(2),
      UNRECOGNIZED(-1),
      ;
      ・・・
    }
    ・・・
    public  static final class PhoneNumber extends
        com.google.protobuf.GeneratedMessageV3 implements
        // @@protoc_insertion_point(message_implements:sample.Person.PhoneNumber)
        PhoneNumberOrBuilder {
      ・・・
    }
    ・・・
  }
  ・・・
}

Axon Framework でイベントソーシング

Axon Fraework のイベントソーシング機能を軽く試してみました。

今回のソースは http://github.com/fits/try_samples/tree/master/blog/20160815/

はじめに

今回は以下のような Gradle 用ビルド定義を使います。

lombok は必須ではありませんが、便利なので使っています。 compileOnlyコンパイル時にのみ使用するモジュールを指定できます。

build.gradle
apply plugin: 'application'

repositories {
    jcenter()
}

dependencies {
    compileOnly 'org.projectlombok:lombok:1.16.10'

    compile 'org.axonframework:axon-core:3.0-M3'

    runtime 'org.slf4j:slf4j-simple:1.7.21'
}

mainClassName = 'SampleApp'

Axon Framework によるイベントソーシング

DDD や CQRS におけるイベントソーシングでは、永続化したイベント群を順次適用して集約の状態を復元します。

Axon Framework では以下のように処理すれば、イベントソーシングを実現できるようです。

  • (a) CommandHandler でコマンドからイベントを生成し適用
  • (b) EventSourcingHandler でイベントの内容からモデルの状態を変更

なお、これらのハンドラはアノテーションで指定できるようになっています。

コマンドの作成

まずは、在庫作成のコマンドを実装します。

処理対象の識別子を設定するフィールドに @TargetAggregateIdentifier を付けますが、CreateInventoryItem には特に付けなくても問題無さそうでした。(次の CheckInItemsToInventory では必須)

なお、lombok.Value を使っているため、コンストラクタや getter メソッドが自動的に生成されます。

src/main/java/sample/commands/CreateInventoryItem.java
package sample.commands;

import org.axonframework.commandhandling.TargetAggregateIdentifier;
import lombok.Value;

// 在庫作成コマンド
@Value
public class CreateInventoryItem {

    @TargetAggregateIdentifier // このアノテーションは必須では無さそう
    private String id;

    private String name;
}

次に、在庫数を加えるためのコマンドです。

src/main/java/sample/commands/CheckInItemsToInventory.java
package sample.commands;

import org.axonframework.commandhandling.TargetAggregateIdentifier;
import lombok.Value;

// 在庫数追加コマンド
@Value
public class CheckInItemsToInventory {

    @TargetAggregateIdentifier // このアノテーションは必須
    private String id;

    private int count;
}

イベントの作成

次は、コマンドによって生じるイベントを作成します。

今回は、CreateInventoryItem コマンドから 2つのイベント (InventoryItemCreated と InventoryItemRenamed) が生じるような仕様で考えてみました。

src/main/java/sample/events/InventoryItemCreated.java
package sample.events;

import lombok.Value;

// 在庫作成イベント
@Value
public class InventoryItemCreated {
    private String id;
}
src/main/java/sample/events/InventoryItemRenamed.java
package sample.events;

import lombok.Value;

// 名前変更イベント
@Value
public class InventoryItemRenamed {
    private String newName;
}
src/main/java/sample/events/ItemsCheckedInToInventory.java
package sample.events;

import lombok.Value;

// 在庫数追加イベント
@Value
public class ItemsCheckedInToInventory {
    private int count;
}

エンティティの作成

在庫エンティティを作成します。

一意の識別子を設定するフィールドへ @AggregateIdentifier を付与します。

コマンドを処理するコンストラクタ ※ やメソッドへ @CommandHandler を付与し、その処理内でイベントを作成して AggregateLifecycle.apply メソッドへ渡せば、@EventSourcingHandler を付与した該当メソッドが呼び出されます。

イベントの内容に合わせてエンティティの内部状態を更新する事で、イベントソーシングを実現できます。

 ※ 新規作成のコマンドを処理する場合に、コンストラクタを使います

なお、引数なしのデフォルトコンストラクタは必須なようです。

src/main/java/sample/models/InventoryItem.java
package sample.models;

import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.commandhandling.model.AggregateIdentifier;
import org.axonframework.commandhandling.model.AggregateLifecycle;
import org.axonframework.commandhandling.model.ApplyMore;

import org.axonframework.eventsourcing.EventSourcingHandler;

import lombok.Getter;
import lombok.ToString;

import sample.commands.CreateInventoryItem;
import sample.commands.CheckInItemsToInventory;
import sample.events.InventoryItemCreated;
import sample.events.InventoryItemRenamed;
import sample.events.ItemsCheckedInToInventory;

@ToString
public class InventoryItem {

    @AggregateIdentifier
    @Getter
    private String id;

    @Getter
    private String name;

    @Getter
    private int count;

    // デフォルトコンストラクタは必須
    public InventoryItem() {
    }

    // 在庫作成コマンド処理
    @CommandHandler
    public InventoryItem(CreateInventoryItem cmd) {
        System.out.println("C call new: " + cmd);
        // 在庫作成イベントの作成と適用
        AggregateLifecycle.apply(new InventoryItemCreated(cmd.getId()));
        // 名前変更イベントの作成と適用
        AggregateLifecycle.apply(new InventoryItemRenamed(cmd.getName()));
    }

    // 在庫数追加コマンド処理
    @CommandHandler
    private ApplyMore updateCount(CheckInItemsToInventory cmd) {
        System.out.println("C call updateCount: " + cmd);
        // 在庫数追加イベントの作成と適用
        return AggregateLifecycle.apply(new ItemsCheckedInToInventory(cmd.getCount()));
    }

    // 在庫作成イベントの適用処理
    @EventSourcingHandler
    private void applyCreated(InventoryItemCreated event) {
        System.out.println("E call applyCreated: " + event);

        this.id = event.getId();
    }

    // 名前変更イベントの適用処理
    @EventSourcingHandler
    private void applyRenamed(InventoryItemRenamed event) {
        System.out.println("E call applyRenamed: " + event);

        this.name = event.getNewName();
    }

    // 在庫数追加イベントの適用処理
    @EventSourcingHandler
    private void applyCheckedIn(ItemsCheckedInToInventory event) {
        System.out.println("E call applyCheckedIn: " + event);

        this.count += event.getCount();
    }
}

実行クラスの作成

動作確認のための実行クラスを作成します。

CommandGateway へコマンドを send すれば処理が流れるように CommandBus・Repository・EventStore 等を組み合わせます。

イベントソーシングには EventSourcingRepository を使用します。 アノテーションを使ったコマンドハンドラを適用するには AggregateAnnotationCommandHandler を使用します。

また、今回はインメモリでイベントを保持する InMemoryEventStorageEngine を使っています。

src/main/java/SampleApp.java
import org.axonframework.commandhandling.AggregateAnnotationCommandHandler;
import org.axonframework.commandhandling.SimpleCommandBus;
import org.axonframework.commandhandling.gateway.DefaultCommandGateway;

import org.axonframework.eventsourcing.EventSourcedAggregate;
import org.axonframework.eventsourcing.EventSourcingRepository;
import org.axonframework.eventsourcing.eventstore.EmbeddedEventStore;
import org.axonframework.eventsourcing.eventstore.inmemory.InMemoryEventStorageEngine;

import lombok.val;

import sample.commands.CreateInventoryItem;
import sample.commands.CheckInItemsToInventory;
import sample.models.InventoryItem;

public class SampleApp {
    public static void main(String... args) {

        val cmdBus = new SimpleCommandBus();
        val gateway = new DefaultCommandGateway(cmdBus);

        val es = new EmbeddedEventStore(new InMemoryEventStorageEngine());

        // イベントソーシング用の Repository
        val repository = new EventSourcingRepository<>(InventoryItem.class, es);

        // アノテーションによるコマンドハンドラを適用
        new AggregateAnnotationCommandHandler<>(InventoryItem.class, 
                                                repository).subscribe(cmdBus);

        String r1 = gateway.sendAndWait(new CreateInventoryItem("s1", "sample1"));
        System.out.println("id: " + r1);

        System.out.println("----------");

        EventSourcedAggregate<InventoryItem> r2 = 
            gateway.sendAndWait(new CheckInItemsToInventory("s1", 5));

        printAggregate(r2);

        System.out.println("----------");

        EventSourcedAggregate<InventoryItem> r3 = 
            gateway.sendAndWait(new CheckInItemsToInventory("s1", 3));

        printAggregate(r3);
    }

    private static void printAggregate(EventSourcedAggregate<InventoryItem> esag) {
        System.out.println(esag.getAggregateRoot());
    }
}

なお、CreateInventoryItem を send した後に、同じ ID (今回は "s1")を使って再度 CreateInventoryItem を send してみたところ、特に重複チェックなどが実施されるわけではなく、普通にイベント (InventoryItemCreated と InventoryItemRenamed) が追加されました。

実行

実行結果は以下の通りです。

コマンドを送信する度に、これまでのイベントを適用してエンティティの状態を復元した後、新しいコマンドを処理している動作を確認できました。

> gradle run

・・・
:run
C call new: CreateInventoryItem(id=s1, name=sample1)
E call applyCreated: InventoryItemCreated(id=s1)
E call applyRenamed: InventoryItemRenamed(newName=sample1)
id: s1
----------
E call applyCreated: InventoryItemCreated(id=s1)
E call applyRenamed: InventoryItemRenamed(newName=sample1)
C call updateCount: CheckInItemsToInventory(id=s1, count=5)
E call applyCheckedIn: ItemsCheckedInToInventory(count=5)
InventoryItem(id=s1, name=sample1, count=5)
----------
E call applyCreated: InventoryItemCreated(id=s1)
E call applyRenamed: InventoryItemRenamed(newName=sample1)
E call applyCheckedIn: ItemsCheckedInToInventory(count=5)
C call updateCount: CheckInItemsToInventory(id=s1, count=3)
E call applyCheckedIn: ItemsCheckedInToInventory(count=3)
InventoryItem(id=s1, name=sample1, count=8)

備考 - スナップショット

イベント数が多くなると、イベントを毎回初めから適用して状態を復元するのはパフォーマンス的に厳しくなるため、スナップショットを使います。

Axon Framework にもスナップショットの機能が用意されており、EventCountSnapshotterTrigger 等を EventSourcingRepository へ設定すれば使えるようです。

ただし、今回のサンプル (SampleApp.java) では EventCountSnapshotterTrigger を機能させられませんでした。

というのも、EventCountSnapshotterTrigger では decorateForAppend メソッドの実行時にスナップショット化を行うようですが ※、今回のサンプルでは decorateForAppend は一度も実行せず decorateForRead メソッドのみが実行されるようでした。

 ※ ソースをざっと見た限りでは、カウンターのカウントアップ等も
    decorateForAppend の中でのみ実施しているようだった

Groovy の @Grab で Spark Framework を実行

Spark Framework - A tiny Java web framework を Groovy の @Grab を使って実行してみました。

今回のソースは http://github.com/fits/try_samples/tree/master/blog/20160801/

はじめに

以下のように @Grab を使った Spark の Groovy スクリプトを groovy コマンドで実行し、Web クライアントでアクセスしてみると、java.lang.NoSuchMethodError: javax.servlet.http.HttpServletResponse.getHeaders エラーが発生してしまいました。

now.groovy
@Grab('com.sparkjava:spark-core:2.5')
@Grab('org.slf4j:slf4j-simple:1.7.21')
import static spark.Spark.*

get('/now') { req, res -> new Date().format('yyyy/MM/dd HH:mm:ss') }

groovy コマンドで上記スクリプトを実行。

実行例
> groovy now.groovy

・・・
[Thread-1] INFO org.eclipse.jetty.server.Server - Started @3213ms

/now へアクセスすると、以下のようなエラーが発生。

エラー例(クライアント側)
$ curl http://localhost:4567/now

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/>
<title>Error 500 </title>
</head>
<body>
<h2>HTTP ERROR: 500</h2>
<p>Problem accessing /now. Reason:
<pre>    java.lang.NoSuchMethodError: javax.servlet.http.HttpServletResponse.getHeaders(Ljava/lang/String;)Ljava/util/Collection;</pre></p>
<hr /><a href="http://eclipse.org/jetty">Powered by Jetty:// 9.3.6.v20151106</a><hr/>
</body>
</html>
エラー例(サーバー側)
> groovy now.groovy

・・・
java.lang.NoSuchMethodError: javax.servlet.http.HttpServletResponse.getHeaders(Ljava/lang/String;)Ljava/util/Collection;
        at spark.utils.GzipUtils.checkAndWrap(GzipUtils.java:67)
        at spark.http.matching.Body.serializeTo(Body.java:69)
        at spark.http.matching.MatcherFilter.doFilter(MatcherFilter.java:158)
        at spark.embeddedserver.jetty.JettyHandler.doHandle(JettyHandler.java:50)
        at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:189)
        ・・・

このエラーは、groovy コマンドの実行時に $GROOVY_HOME/lib/servlet-api-2.4.jar (Servlet 2.4) を先にロードする事が原因で発生しているようです。

HttpServletResponse.getHeadersServlet 3.0 から追加されたメソッドのため、Servlet 2.4 の API が適用されていると該当メソッドが存在せず NoSuchMethodError になります。

回避策

エラー原因は、groovy コマンドが $GROOVY_HOME/lib/servlet-api-2.4.jar をロードする事なので、Gradle 等で実行すれば上記のようなエラーは発生しません。

しかし、今回は groovy コマンドで実行する場合の回避策をいくつか検討してみました。

  • (a) -cp オプションを使用
  • (b) Groovy 設定ファイル (groovy-starter.conf) を編集
  • (c) $GROOVY_HOME/lib/servlet-api-2.4.jar を削除

(a) と (b) は Servlet 2.4 より先に Servlet 3.1 を適用させる方法で、(c) は servlet-api-2.4.jar をロードさせない方法です。

(a) -cp オプションを使用

Servlet 3.1 の JAR (下記では javax.servlet-api-3.1.0.jar)を入手し、groovy コマンドの -cp オプションでその JAR を指定して実行します。

こうする事でエラーは出なくなりました。

実行例(サーバー)
> groovy -cp lib_a/javax.servlet-api-3.1.0.jar now.groovy
・・・
実行例(クライアント)
$ curl http://localhost:4567/now
2016/07/31 20:46:42

(b) Groovy 設定ファイル (groovy-starter.conf) を編集

$GROOVY_HOME/lib/servlet-api-2.4.jar は groovy-starter.conf の設定 (load !{groovy.home}/lib/*.jar) によりロードされています。

つまり、$GROOVY_HOME/lib/*.jar よりも先に、別のディレクトリ内の JAR をロードするように groovy-starter.conf を書き換え、そのディレクトリへ Servlet 3.1 の JAR を配置すれば、(a) と同様に回避できるはずです。

groovy-starter.conf 変更例
# 以下を追加
load lib_a/*.jar

# load required libraries
load !{groovy.home}/lib/*.jar

・・・

lib_a/javax.servlet-api-3.1.0.jar を配置して実行すると、エラーは出なくなりました。

実行例(サーバー)
> groovy now.groovy
・・・
実行例(クライアント)
$ curl http://localhost:4567/now
2016/07/31 20:48:05

備考. Groovy 設定ファイル (groovy-starter.conf) の指定方法

groovy-starter.conf を直接書き換えるのはイマイチなので、任意の Groovy 設定ファイルを使いたいところです。

startGroovy スクリプトの内容を見ると GROOVY_CONF 環境変数で指定できそうです。

ただし、startGroovy.bat の方は今のところ GROOVY_CONF 環境変数を考慮しておらず、Windows 環境 (groovy.bat を使う場合) では使えません。

そこで今回は、下記 postinit.bat を用意し、Windows 環境で GROOVY_CONF に対応してみました。

%USERPROFILE%/.groovy/postinit.bat の例
if not "%GROOVY_CONF%" == "" (
    set GROOVY_OPTS=%GROOVY_OPTS% -Dgroovy.starter.conf="%GROOVY_CONF%"
    set STARTER_CONF=%GROOVY_CONF%
)

上記を配置した後、以下のように実行します。

GROOVY_CONF 環境変数の利用例 (Windows
> set GROOVY_CONF=groovy-starter_custom.conf

> groovy now.groovy
・・・

(c) $GROOVY_HOME/lib/servlet-api-2.4.jar を削除

$GROOVY_HOME/lib/servlet-api-2.4.jar を一時的に削除(拡張子を変える等)して実行します。

備考. Gradle で実行する場合

Gradle で実行する場合は、src/main/groovy/now.groovy を配置して(@Grab の箇所は削除しておく)、以下のような build.gradle を使います。

build.gradle 例
apply plugin: 'groovy'
apply plugin: 'application'

repositories {
    jcenter()
}

dependencies {
    compile 'com.sparkjava:spark-core:2.5'
    compile 'org.codehaus.groovy:groovy:2.4.7'
    runtime 'org.slf4j:slf4j-simple:1.7.21'
}

mainClassName = 'now'
実行例
> gradle run
・・・
:run
・・・
[Thread-1] INFO org.eclipse.jetty.server.Server - Started @1035ms

node-ffi で OpenCL を使う2 - 演算の実行

node-ffi で OpenCL を使う」 に続き、Node.js を使って OpenCL の演算を実施してみます。

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20160725/

はじめに

演算の実行には ref-array モジュールを使った方が便利なため、node-ffi をインストールした環境へ追加でインストールしておきます。

ref-array インストール例
> npm install ref-array

OpenCL の演算実行サンプル

今回は配列の要素を 3乗する OpenCL のコード(以下)を Node.js から実行する事にします。

cube.cl
__kernel void cube(
    __global float* input,
    __global float* output,
    const unsigned int count)
{
    int i = get_global_id(0);

    if (i < count) {
        output[i] = input[i] * input[i] * input[i];
    }
}

サンプルコード概要

上記 cube.cl を実行する Node.js サンプルコードの全体像です。(OpenCLAPI は try-finally 内で呼び出しています)

OpenCL 演算の入力値として data 変数の値を使用します。OpenCL のコードはファイルから読み込んで code 変数へ設定しています。

OpenCL APIclCreateXXX で作成したリソースは clReleaseXXX で解放するようなので、解放処理を都度 releaseList へ追加しておき、finally で実行するようにしています。

なお、OpenCL API のエラーコード取得には以下の 2通りがあります。(使用する関数による)

  • 関数の戻り値でエラーコードを取得
  • 関数の引数(ポインタ)でエラーコードを取得
calc.js (全体)
'use strict';

const fs = require('fs');
const ref = require('ref');
const ArrayType = require('ref-array');
const ffi = require('ffi');

const CL_DEVICE_TYPE_DEFAULT = 1;

const CL_MEM_READ_WRITE = (1 << 0);
const CL_MEM_WRITE_ONLY = (1 << 1);
const CL_MEM_READ_ONLY = (1 << 2);
const CL_MEM_USE_HOST_PTR = (1 << 3);
const CL_MEM_ALLOC_HOST_PTR = (1 << 4);
const CL_MEM_COPY_HOST_PTR = (1 << 5);

const intPtr = ref.refType(ref.types.int32);
const uintPtr = ref.refType(ref.types.uint32);
const sizeTPtr = ref.refType('size_t');
const StringArray = ArrayType('string');

const clLib = (process.platform == 'win32') ? 'OpenCL' : 'libOpenCL';

// 使用する OpenCL の関数定義
const openCl = ffi.Library(clLib, {
    'clGetPlatformIDs': ['int', ['uint', sizeTPtr, uintPtr]],
    'clGetDeviceIDs': ['int', ['size_t', 'ulong', 'uint', sizeTPtr, uintPtr]],
    'clCreateContext': ['pointer', ['pointer', 'uint', sizeTPtr, 'pointer', 'pointer', intPtr]],
    'clReleaseContext': ['int', ['pointer']],
    'clCreateProgramWithSource': ['pointer', ['pointer', 'uint', StringArray, sizeTPtr, intPtr]],
    'clBuildProgram': ['int', ['pointer', 'uint', sizeTPtr, 'string', 'pointer', 'pointer']],
    'clReleaseProgram': ['int', ['pointer']],
    'clCreateKernel': ['pointer', ['pointer', 'string', intPtr]],
    'clReleaseKernel': ['int', ['pointer']],
    'clCreateBuffer': ['pointer', ['pointer', 'ulong', 'size_t', 'pointer', intPtr]],
    'clReleaseMemObject': ['int', ['pointer']],
    'clSetKernelArg': ['int', ['pointer', 'uint', 'size_t', 'pointer']],
    'clCreateCommandQueue': ['pointer', ['pointer', 'size_t', 'ulong', intPtr]],
    'clReleaseCommandQueue': ['int', ['pointer']],
    'clEnqueueReadBuffer': ['int', ['pointer', 'pointer', 'bool', 'size_t', 'size_t', 'pointer', 'uint', 'pointer', 'pointer']],
    'clEnqueueNDRangeKernel': ['int', ['pointer', 'pointer', 'uint', sizeTPtr, sizeTPtr, sizeTPtr, 'uint', 'pointer', 'pointer']]
});

// エラーチェック
const checkError = (err, title = '') => {
    if (err instanceof Buffer) {
        // ポインタの場合はエラーコードを取り出す
        err = intPtr.get(err);
    }

    if (err != 0) {
        throw new Error(`${title} Error: ${err}`);
    }
};

// 演算対象データ
const data = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9];

const functionName = process.argv[2]
// OpenCL コードの読み込み(ファイルから)
const code = fs.readFileSync(process.argv[3]);

const releaseList = [];

try {
    ・・・ OpenCL API の呼び出し処理 ・・・

} finally {
    // リソースの解放
    releaseList.reverse().forEach( f => f() );
}

clCreateProgramWithSourceOpenCL のコードを渡す際に、ref-array で作成した String の配列 StringArray を使っています。

OpenCL 処理部分

今回は以下のような OpenCL API を使っています。

番号 概要 OpenCL 関数名
(1) プラットフォーム取得 clGetPlatformIDs
(2) バイス取得 clGetDeviceIDs
(3) コンテキスト作成 clCreateContext
(4) コマンドキュー作成 clCreateCommandQueue
(5) プログラム作成 clCreateProgramWithSource
(6) プログラムのビルド clBuildProgram
(7) カーネル作成 clCreateKernel
(8) 引数用のバッファ作成 clCreateBuffer
(9) 引数の設定 clSetKernelArg
(10) 処理の実行 clEnqueueNDRangeKernel
(11) 結果の取得 clEnqueueReadBuffer

OpenCL のコードを実行するには (6) のように API を使ってビルドする必要があります。

Node.js と OpenCL 間で配列データ等をやりとりするには (8) で作ったバッファを使います。(入力値をバッファへ書き込んで、出力値をバッファから読み出す)

また、今回は clEnqueueNDRangeKernel を使って実行しましたが、clEnqueueTask を使って実行する方法もあります。

calc.js (OpenCL 処理部分)
・・・
try {
    const platformIdsPtr = ref.alloc(sizeTPtr);
    // (1) プラットフォーム取得
    let res = openCl.clGetPlatformIDs(1, platformIdsPtr, null);

    checkError(res, 'clGetPlatformIDs');

    const platformId = sizeTPtr.get(platformIdsPtr);

    const deviceIdsPtr = ref.alloc(sizeTPtr);
    // (2) デバイス取得 (デフォルトをとりあえず使用)
    res = openCl.clGetDeviceIDs(platformId, CL_DEVICE_TYPE_DEFAULT, 1, deviceIdsPtr, null);

    checkError(res, 'clGetDeviceIDs');

    const deviceId = sizeTPtr.get(deviceIdsPtr);

    const errPtr = ref.alloc(intPtr);
    // (3) コンテキスト作成
    const ctx = openCl.clCreateContext(null, 1, deviceIdsPtr, null, null, errPtr);

    checkError(errPtr, 'clCreateContext');
    releaseList.push( () => openCl.clReleaseContext(ctx) );
    // (4) コマンドキュー作成
    const queue = openCl.clCreateCommandQueue(ctx, deviceId, 0, errPtr);

    checkError(errPtr, 'clCreateCommandQueue');
    releaseList.push( () => openCl.clReleaseCommandQueue(queue) );

    const codeArray = new StringArray([code.toString()]);
    // (5) プログラム作成
    const program = openCl.clCreateProgramWithSource(ctx, 1, codeArray, null, errPtr);

    checkError(errPtr, 'clCreateProgramWithSource');
    releaseList.push( () => openCl.clReleaseProgram(program) );
    // (6) プログラムのビルド
    res = openCl.clBuildProgram(program, 1, deviceIdsPtr, null, null, null)

    checkError(res, 'clBuildProgram');
    // (7) カーネル作成
    const kernel = openCl.clCreateKernel(program, functionName, errPtr);

    checkError(errPtr, 'clCreateKernel');
    releaseList.push( () => openCl.clReleaseKernel(kernel) );

    const FixedFloatArray = ArrayType('float', data.length);
    // 入力データ
    const inputData = new FixedFloatArray(data);

    const bufSize = inputData.buffer.length;
    // (8) 引数用のバッファ作成(入力用)し inputData の内容を書き込む
    const inClBuf = openCl.clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, bufSize, inputData.buffer, errPtr);

    checkError(errPtr, 'clCreateBuffer In');
    releaseList.push( () => openCl.clReleaseMemObject(inClBuf) );

    // (8) 引数用のバッファ作成(出力用)
    const outClBuf = openCl.clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, bufSize, null, errPtr);

    checkError(errPtr, 'clCreateBuffer Out');
    releaseList.push( () => openCl.clReleaseMemObject(outClBuf) );

    const inClBufRef = inClBuf.ref();
    // (9) 引数の設定
    res = openCl.clSetKernelArg(kernel, 0, inClBufRef.length, inClBufRef);

    checkError(res, 'clSetKernelArg 0');

    const outClBufRef = outClBuf.ref();
    // (9) 引数の設定
    res = openCl.clSetKernelArg(kernel, 1, outClBufRef.length, outClBufRef);

    checkError(res, 'clSetKernelArg 1');

    const ct = ref.alloc(ref.types.uint32, data.length);

    // (9) 引数の設定
    res = openCl.clSetKernelArg(kernel, 2, ct.length, ct);

    checkError(res, 'clSetKernelArg 2');

    const globalPtr = ref.alloc(sizeTPtr);
    sizeTPtr.set(globalPtr, 0, data.length);
    // (10) 処理の実行
    res = openCl.clEnqueueNDRangeKernel(queue, kernel, 1, null, globalPtr, null, 0, null, null);

    checkError(res, 'clEnqueueNDRangeKernel');

    const resData = new FixedFloatArray();

    // (11) 結果の取得 (outClBuf の内容を resData へ)
    res = openCl.clEnqueueReadBuffer(queue, outClBuf, true, 0, resData.buffer.length, resData.buffer, 0, null, null);

    checkError(res, 'clEnqueueReadBuffer');

    // 結果出力
    for (let i = 0; i < resData.length; i++) {
        console.log(resData[i]);
    }

} finally {
    // リソースの解放
    releaseList.reverse().forEach( f => f() );
}

動作確認

今回は以下の Node.js を使って WindowsLinux の両方で動作確認します。

  • Node.js v6.3.1

(a) Windows で実行

node-ffi で OpenCL を使う」 で構築した環境へ ref-array をインストールして実行しました。

実行結果 (Windows
> node calc.js cube cube.cl

1.3310000896453857
10.648000717163086
35.93699645996094
85.18400573730469
166.375
287.4959716796875
456.532958984375
681.4720458984375
970.2988891601562

(b) Linux で実行

前回 の Docker イメージを使って実行します。

calc.js と cube.cl を /vagrant/work へ配置し、Docker コンテナからは /work でアクセスできるようにマッピングしました。

Docker コンテナ実行
$ docker run --rm -it -v /vagrant/work:/work sample/opencl:0.1 bash
Node.js と必要なモジュールのインストール
# curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.2/install.sh | bash
・・・
# source ~/.bashrc

# nvm install v6.3.1
・・・
# npm install -g node-gyp
・・・
# cd /work
# npm install ffi ref-array
・・・

/work 内で実行します。

実行結果 (Linux
# node calc.js cube cube.cl

1.3310000896453857
10.648000717163086
35.93699645996094
85.18400573730469
166.375
287.4959716796875
456.532958984375
681.4720458984375
970.2988891601562

Docker で OpenCL の実行環境を構築 - pocl

前回の 「node-ffi で OpenCL を使う」 では Windows 上で Node.js から OpenCLAPI を呼び出してみましたが、これを Linux 上で行うために OpenCL の実行環境を Docker で構築してみました。

OpenCL の実行環境として pocl を使い、LinuxFedora を使います。(Fedora にしたのは pocl のインストールが簡単だったから)

サンプルのソースは http://github.com/fits/try_samples/tree/master/blog/20160711/

Docker イメージ作成

Docker の公式 fedora イメージを使って OpenCL の Docker イメージを作成します。

最近の Fedora ではパッケージ管理に DNF (yum の後継) を使うようなので、dnf コマンドを使った Dockerfile を作成しました。

Dockerfile
FROM fedora

RUN dnf update -y
RUN dnf install -y clinfo pocl pocl-devel

RUN ln -s /lib64/libpoclu.so /lib64/libOpenCL.so

RUN dnf install -y tar findutils make python2

RUN dnf clean all

OpenCL を実行するだけなら pocl のインストールだけで良さそうですが、ついでに clinfo と pocl-devel もインストールしています。 (clinfo で OpenCL の環境情報を参照できます)

なお、OpenCL の標準的なライブラリ名は libOpenCL.so のようですが、pocl をインストールしても libOpenCL.so を作ってくれなかったので、libpoclu.soシンボリックリンクとして作成するようにしています。※

 ※ node-ffi で指定するライブラリ名を libpoclu とするなら、
    libOpenCL.so は無くてもよい

また、tar と findutils は nvm で、make と python2 は node-gyp で必要となるため、ついでにインストールしています。

Docker イメージは docker build コマンドで作成します。

ビルド例 (Docker イメージ作成)
$ docker build --no-cache -t sample/opencl:0.1 .

OpenCL 動作確認

まずは、今回の環境で動作するように 前回 のサンプルを少し書き換えます。 ライブラリ名をプラットフォームに合わせて切り替えるようにします。

/vagrant/work/platform_info.js (変更点)
・・・
const clLib = (process.platform == 'win32') ? 'OpenCL' : 'libOpenCL';

const openCl = ffi.Library(clLib, {
    ・・・
});
・・・

先程作成した Docker イメージを使ってコンテナを起動します。

Docker コンテナ実行
$ docker run --rm -it -v /vagrant/work:/work sample/opencl:0.1 bash

ここからは Docker コンテナ内での操作となります。

dnf (もしくは yum) だと古いバージョンの Node.js をインストールしてしまうので、nvm を使って Node.js v6.2.2 をインストールします。

Node.js のインストール後、node-gyp と node-ffi を npm でそれぞれインストールします。

Node.js と必要なモジュールのインストール
# curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.2/install.sh | bash
・・・
# source ~/.bashrc

# nvm install v6.2.2
・・・
# npm install -g node-gyp
・・・
# cd /work
# npm install ffi
・・・

最後に platform_info.js を実行すると問題なく動作しました。

platform_info.js 実行
# node platform_info.js

FULL_PROFILE
OpenCL 2.0 pocl 0.13, LLVM 3.8.0
Portable Computing Language