Keras で MNIST を分類
「Keras で iris を分類」 に続き、今回は Keras で畳み込みニューラルネットを使った MNIST の分類を試してみました。
- Keras 1.0.8
- Python 3.5.2
ソースは 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 であればビルドツールに sbt を使う方が簡単かもしれませんが、引き続き Gradle を使います。
- Gradle 3.0
- ScalaPB 0.5.40
今回作成したソースは 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 3.0
- protoc-jar 3.0.0
- protobuf-java 3.0.0
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_package
と java_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
を使って実行してみました。
- Spark Framework 2.5
- Groovy 2.4.7
今回のソースは 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.getHeaders
は Servlet 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 サンプルコードの全体像です。(OpenCL の API は try-finally 内で呼び出しています)
OpenCL 演算の入力値として data
変数の値を使用します。OpenCL のコードはファイルから読み込んで code
変数へ設定しています。
OpenCL API の clCreateXXX
で作成したリソースは 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() ); }
clCreateProgramWithSource
へ OpenCL のコードを渡す際に、ref-array
で作成した String の配列 StringArray
を使っています。
OpenCL 処理部分
番号 | 概要 | 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 を使って Windows と Linux の両方で動作確認します。
- 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 から OpenCL の API を呼び出してみましたが、これを Linux 上で行うために OpenCL の実行環境を Docker で構築してみました。
OpenCL の実行環境として pocl を使い、Linux は Fedora を使います。(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