JDI でオブジェクトの世代(Young・Old)を判別する2
前回 の処理を sun.jvm.hotspot.oops.ObjectHeap
を使って高速化してみたいと思います。(世代の判別方法などは前回と同じ)
使用した環境は前回と同じです。
- Groovy 2.4.6
- Java SE 8u92 64bit版 (JDK)
ソースは http://github.com/fits/try_samples/tree/master/blog/20160506/
ObjectHeap で Oop を取得
ObjectReference の代わりに、sun.jvm.hotspot.oops.ObjectHeap
の iterate(HeapVisitor)
メソッドを使えば Oop を取得できます。
今回のような方法では、以下の理由で iterate メソッドの引数へ SAJDIClassLoader がロードした sun.jvm.hotspot.oops.HeapVisitor
インターフェースの実装オブジェクトを与える必要があります。
- JDI の内部で管理している Serviceability Agent API は
sun.jvm.hotspot.jdi.SAJDIClassLoader
によってロードされている
下記サンプルでは SAJDIClassLoader がロードした HeapVisitor を入手し、asType
を使って実装オブジェクトを作成しています。
また、HeapVisitor の doObj
で false を返すと処理を継続し、true を返すと中止 ※ するようです。
※ 厳密には、 対象としている Address 範囲の while ループを break するだけで、 その外側の(liveRegions に対する)for ループは継続するようです (ObjectHeap の iterateLiveRegions メソッドのソース参照)
なお、ObjectHeap は sun.jvm.hotspot.jdi.VirtualMachineImpl
から saObjectHeap()
で取得するか、sun.jvm.hotspot.runtime.VM
から取得します。
check_gen2.groovy
import com.sun.jdi.Bootstrap def pid = args[0] def prefix = (args.length > 1)? args[1]: '' def manager = Bootstrap.virtualMachineManager() def connector = manager.attachingConnectors().find { it.name() == 'sun.jvm.hotspot.jdi.SAPIDAttachingConnector' } def params = connector.defaultArguments() params.get('pid').setValue(pid) def vm = connector.attach(params) // 世代の判定処理を返す generation = { heap -> def hasYoungGen = heap.metaClass.getMetaMethod('youngGen') != null [ young: hasYoungGen? heap.youngGen(): heap.getGen(0), old: hasYoungGen? heap.oldGen(): heap.getGen(1) ] } try { def uv = vm.saVM.universe def gen = generation(uv.heap()) def youngGen = gen.young def oldGen = gen.old println "*** youngGen=${youngGen}, oldGen=${oldGen}" println '' def objHeap = vm.saObjectHeap() // 以下でも可 //def objHeap = vm.saVM.objectHeap // SAJDIClassLoader がロードした HeapVisitor インターフェースを取得 def heapVisitorCls = uv.class.classLoader.loadClass('sun.jvm.hotspot.oops.HeapVisitor') // SAJDIClassLoader がロードした HeapVisitor インターフェースを実装 def heapVisitor = [ prologue: { size -> }, epilogue: {}, doObj: { oop -> def clsName = oop.klass.name.asString() if (clsName.startsWith(prefix)) { def age = oop.mark.age() // 世代の判別 def inYoung = youngGen.isIn(oop.handle) def inOld = oldGen.isIn(oop.handle) def identityHash = '' try { identityHash = Long.toHexString(oop.identityHash()) } catch (e) { } println "class=${clsName}, hash=${identityHash}, handle=${oop.handle}, age=${age}, inYoung=${inYoung}, inOld=${inOld}" } // 処理を継続する場合は false を返す false } ].asType(heapVisitorCls) objHeap.iterate(heapVisitor) } finally { vm.dispose() }
動作確認
前回と同じように、実行中の apache-tomcat-9.0.0.M4 へ適用してみました。
前回と異なり、クラス名が '/' で区切られている点に注意
実行例1 (Windows の場合)
> jps 3604 Bootstrap 4516 Jps
> groovy -cp %JAVA_HOME%/lib/sa-jdi.jar check_gen2.groovy 3604 org/apache/catalina/core/StandardContext *** youngGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSYoungGen@0x0000000002149ab0, oldGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSOldGen@0x0000000002149b40 class=org/apache/catalina/core/StandardContextValve, hash=0, handle=0x00000000c3a577d0, age=1, inYoung=false, inOld=true class=org/apache/catalina/core/StandardContext$NoPluggabilityServletContext, hash=0, handle=0x00000000c3a633d8, age=0, inYoung=false, inOld=true class=org/apache/catalina/core/StandardContext$ContextFilterMaps, hash=0, handle=0x00000000c3a63ef0, age=1, inYoung=false, inOld=true class=org/apache/catalina/core/StandardContext$NoPluggabilityServletContext, hash=0, handle=0x00000000ebc46da0, age=0, inYoung=true, inOld=false class=org/apache/catalina/core/StandardContext, hash=6f2d2815, handle=0x00000000eddfeaa0, age=1, inYoung=true, inOld=false class=org/apache/catalina/core/StandardContext, hash=21f2e66b, handle=0x00000000eddff238, age=3, inYoung=true, inOld=false ・・・
実行例2 (Linux の場合)
$ jps 2778 Jps 2766 Bootstrap
$ groovy -cp $JAVA_HOME/lib/sa-jdi.jar check_gen2.groovy 2766 org/apache/catalina/core/StandardContext *** youngGen=sun.jvm.hotspot.memory.DefNewGeneration@0x00007f0760019cb0, oldGen=sun.jvm.hotspot.memory.TenuredGeneration@0x00007f076001bfc0 class=org/apache/catalina/core/StandardContext, hash=497fe2c4, handle=0x00000000f821bf90, age=0, inYoung=true, inOld=false class=org/apache/catalina/core/StandardContext$ContextFilterMaps, hash=0, handle=0x00000000f821c5d8, age=0, inYoung=true, inOld=false class=org/apache/catalina/core/StandardContextValve, hash=0, handle=0x00000000f821ca60, age=0, inYoung=true, inOld=false ・・・ class=org/apache/catalina/core/StandardContext, hash=5478de1a, handle=0x00000000fb12b310, age=1, inYoung=false, inOld=true class=org/apache/catalina/core/StandardContext$NoPluggabilityServletContext, hash=0, handle=0x00000000fb12f6b0, age=0, inYoung=false, inOld=true class=org/apache/catalina/core/StandardContext$ContextFilterMaps, hash=0, handle=0x00000000fb131a80, age=0, inYoung=false, inOld=true class=org/apache/catalina/core/StandardContextValve, hash=0, handle=0x00000000fb1398b0, age=0, inYoung=false, inOld=true
JDI でオブジェクトの世代(Young・Old)を判別する
前回、オブジェクトの age を取得しましたが、同様の方法で今回はオブジェクトが Young 世代(New 領域)と Old 世代(Old 領域) のどちらに割り当てられているかを判別してみたいと思います。 (ただし、結果の正否は確認できていません)
使用した環境は前回と同じです。
- Groovy 2.4.6
- Java SE 8u92 64bit版 (JDK)
ソースは http://github.com/fits/try_samples/tree/master/blog/20160430/
Young・Old 世代の判別
さて、Young・Old の判別方法ですが。
Serviceability Agent API を見てみると sun.jvm.hotspot.gc_implementation.parallelScavenge
パッケージに PSYoungGen
と PSOldGen
というクラスがあり、isIn(Address)
メソッドで判定できそうです。
更に PSYoungGen と PSOldGen は sun.jvm.hotspot.gc_implementation.parallelScavenge.ParallelScavengeHeap
から取得できます。
Address (sun.jvm.hotspot.debugger
パッケージ所属) は sun.jvm.hotspot.oops.Oop
の getHandle()
か getMark().getAddress()
で取得できるので (下記サンプルでは getHandle を使用)、ParallelScavengeHeap を取得すれば何とかなりそうです。
実際に試してみたところ、ParallelScavengeHeap を取得できたのは Windows 環境で、Linux 環境では GenCollectedHeap を使った別の方法 (getGen
メソッドを使う) が必要でした。 (GC の設定等によって更に変わるかもしれません)
世代の判定クラス
実行環境 | ヒープクラス ※ | Young 世代の判定クラス | Old 世代の判定クラス |
---|---|---|---|
Windows | ParallelScavengeHeap | PSYoungGen | PSOldGen |
Linux | GenCollectedHeap | DefNewGeneration | TenuredGeneration |
※ Universe の heap() メソッド戻り値の実際の型 CollectedHeap のサブクラス
上記を踏まえて、前回の処理をベースに以下を追加してみました。
- (1)
sun.jvm.hotspot.jdi.VirtualMachineImpl
からsun.jvm.hotspot.runtime.VM
オブジェクトを取り出す ※1 - (2) VM オブジェクトから
sun.jvm.hotspot.memory.Universe
オブジェクトを取得 - (3) Universe オブジェクトから CollectedHeap (のサブクラス) を取得 ※2
- (4) (3) の結果から世代を判定するオブジェクトをそれぞれ取得
(4) で妥当な条件分岐の仕方が分からなかったので、とりあえず youngGen メソッドが無ければ GenCollectedHeap として処理するようにしました。
※1 private フィールドの saVM か、package メソッドの saVM() で取得 ※2 今回のやり方では、Windows は ParallelScavengeHeap、 Linux は GenCollectedHeap でした
JDI の SAPIDAttachingConnector で attach した結果が VirtualMachineImpl となります。
また、SAPIDAttachingConnector でデバッグ接続した場合 (読み取り専用のデバッグ接続)、デバッグ対象オブジェクトのメソッド (hashCode や toString 等) を呼び出せないようなので、オブジェクトを識別するための情報を得るため identityHash
を使ってみました。 (ただし、戻り値が 0 になるものが多数ありました)
check_gen.groovy
import com.sun.jdi.Bootstrap def pid = args[0] def prefix = args[1] def manager = Bootstrap.virtualMachineManager() def connector = manager.attachingConnectors().find { it.name() == 'sun.jvm.hotspot.jdi.SAPIDAttachingConnector' } def params = connector.defaultArguments() params.get('pid').setValue(pid) def vm = connector.attach(params) // (4) 世代を判定するためのオブジェクトを取得 generation = { heap -> def hasYoungGen = heap.metaClass.getMetaMethod('youngGen') != null [ // Young 世代の判定オブジェクト(PSYoungGen or DefNewGeneration) young: hasYoungGen? heap.youngGen(): heap.getGen(0), // Old 世代の判定オブジェクト(PSOldGen or TenuredGeneration) old: hasYoungGen? heap.oldGen(): heap.getGen(1) ] } try { if (vm.canGetInstanceInfo()) { // (1) (2) def uv = vm.saVM.universe // (3) def gen = generation(uv.heap()) def youngGen = gen.young def oldGen = gen.old println "*** youngGen=${youngGen}, oldGen=${oldGen}" println '' vm.allClasses().findAll { it.name().startsWith(prefix) }.each { cls -> println cls.name() cls.instances(0).each { inst -> def oop = inst.ref() def age = oop.mark.age() // 世代の判別 def inYoung = youngGen.isIn(oop.handle) def inOld = oldGen.isIn(oop.handle) def identityHash = '' try { identityHash = Long.toHexString(oop.identityHash()) } catch (e) { } println " hash=${identityHash}, handle=${oop.handle}, age=${age}, inYoung=${inYoung}, inOld=${inOld}" } } } } finally { vm.dispose() }
動作確認
前回と同じように、実行中の apache-tomcat-9.0.0.M4 へ適用してみました。
実行例1 (Windows の場合)
> jps 2836 Bootstrap 5944 Jps
> groovy -cp %JAVA_HOME%/lib/sa-jdi.jar check_gen.groovy 2836 org.apache.catalina.core.StandardContext *** youngGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSYoungGen@0x0000000002049ad0, oldGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSOldGen@0x0000000002049b60 org.apache.catalina.core.StandardContext hash=66dfd722, handle=0x00000000c394a990, age=0, inYoung=false, inOld=true hash=39504d4e, handle=0x00000000edea7cf8, age=3, inYoung=true, inOld=false hash=194311fa, handle=0x00000000edea8e90, age=1, inYoung=true, inOld=false hash=2b28e016, handle=0x00000000edf0c130, age=2, inYoung=true, inOld=false hash=578787b8, handle=0x00000000edf457c0, age=1, inYoung=true, inOld=false org.apache.catalina.core.StandardContext$ContextFilterMaps hash=0, handle=0x00000000c394e7d0, age=0, inYoung=false, inOld=true hash=0, handle=0x00000000c396ec90, age=2, inYoung=false, inOld=true hash=0, handle=0x00000000c3988eb0, age=1, inYoung=false, inOld=true hash=0, handle=0x00000000edf04320, age=1, inYoung=true, inOld=false hash=0, handle=0x00000000edf70988, age=1, inYoung=true, inOld=false ・・・
> groovy -cp %JAVA_HOME%/lib/sa-jdi.jar check_gen.groovy 2836 org.apache.catalina.LifecycleEvent *** youngGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSYoungGen@0x0000000002049ad0, oldGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSOldGen@0x0000000002049b60 org.apache.catalina.LifecycleEvent hash=0, handle=0x00000000c37459c0, age=0, inYoung=false, inOld=true hash=0, handle=0x00000000c374ed40, age=1, inYoung=false, inOld=true hash=0, handle=0x00000000c39ff950, age=0, inYoung=false, inOld=true hash=0, handle=0x00000000ebb8ef90, age=0, inYoung=true, inOld=false hash=0, handle=0x00000000ebb90490, age=0, inYoung=true, inOld=false hash=0, handle=0x00000000ebb904c0, age=0, inYoung=true, inOld=false ・・・
実行例2 (Linux の場合)
$ jps 2801 Jps 2790 Bootstrap
$ groovy -cp $JAVA_HOME/lib/sa-jdi.jar check_gen.groovy 2790 org.apache.catalina.core.StandardContext *** youngGen=sun.jvm.hotspot.memory.DefNewGeneration@0x00007fca50019cb0, oldGen=sun.jvm.hotspot.memory.TenuredGeneration@0x00007fca5001bfc0 org.apache.catalina.core.StandardContext hash=27055bff, handle=0x00000000fb025d38, age=1, inYoung=false, inOld=true hash=5638a30f, handle=0x00000000fb1270a8, age=1, inYoung=false, inOld=true hash=15fad243, handle=0x00000000fb296730, age=1, inYoung=false, inOld=true hash=36c4d4a0, handle=0x00000000fb2f3cf0, age=1, inYoung=false, inOld=true hash=33309557, handle=0x00000000fb2f3ef8, age=1, inYoung=false, inOld=true org.apache.catalina.core.StandardContextValve hash=0, handle=0x00000000fb045ad8, age=0, inYoung=false, inOld=true hash=0, handle=0x00000000fb135300, age=0, inYoung=false, inOld=true hash=0, handle=0x00000000fb2ad568, age=1, inYoung=false, inOld=true hash=0, handle=0x00000000fb3022b0, age=1, inYoung=false, inOld=true hash=0, handle=0x00000000fb3050e8, age=1, inYoung=false, inOld=true ・・・
$ groovy -cp $JAVA_HOME/lib/sa-jdi.jar check_gen.groovy 2790 org.apache.catalina.LifecycleEvent *** youngGen=sun.jvm.hotspot.memory.DefNewGeneration@0x00007fca50019cb0, oldGen=sun.jvm.hotspot.memory.TenuredGeneration@0x00007fca5001bfc0 org.apache.catalina.LifecycleEvent hash=0, handle=0x00000000f82079a8, age=0, inYoung=true, inOld=false hash=0, handle=0x00000000f8207a00, age=0, inYoung=true, inOld=false hash=0, handle=0x00000000f8210470, age=0, inYoung=true, inOld=false ・・・ hash=0, handle=0x00000000f8506568, age=0, inYoung=true, inOld=false hash=0, handle=0x00000000fb003370, age=0, inYoung=false, inOld=true
この結果の正否はともかく、一応は判別できているように見えます。
ちなみに、前回と同様に処理が遅い(重い)点に関しては、Oop を Serviceability Agent API の sun.jvm.hotspot.oops.ObjectHeap
で取得するように変更すれば改善できます。
注意点
今回のように JDI の内部で管理している Serviceability Agent API を取り出して使う場合の注意点は以下の通りです。
- JDI 内部の Serviceability Agent API のクラス(インターフェースも含む)は
sun.jvm.hotspot.jdi.SAJDIClassLoader
クラスローダーによってロードされる
同じ名称のクラスでもロードするクラスローダーが異なれば別物となりますので、Java で今回のような処理を実装しようとすると、クラスのキャストができずリフレクション等を多用する事になると思います。
また、Groovy でも HeapVisitor 等を使う場合に多少の工夫が必要になります。
JDI でオブジェクトの年齢(age)を取得
HotSpot VM の世代別 GC において、オブジェクト(インスタンス)には年齢 (age) が設定されており、Minor GC が適用される度にカウントアップされ、長命オブジェクトかどうかの判定に使われるとされています。
そこで今回は、Groovy で JDI (Java Debug Interface) と Serviceability Agent API を使って、実行中の Java アプリケーションへアタッチし、オブジェクトの年齢を取得してみたいと思います。
使用した環境は以下の通りです。
- Groovy 2.4.6
- Java SE 8u92 64bit版 (JDK)
ソースは http://github.com/fits/try_samples/tree/master/blog/20160425/
はじめに
今回は、JDI の SA PID コネクタ (sun.jvm.hotspot.jdi.SAPIDAttachingConnector
) を使ってデバッグ接続する事にします。
SAPIDAttachingConnector の特徴は以下のようになります。
SAPIDAttachingConnector の特徴
利点 | 欠点 |
---|---|
デバッグ対象アプリケーションの実行時に -agentlib:jdwp のようなオプション指定が不要 |
読み取り専用で VM へアタッチするため、実行可能な API が限定される。デバッグ接続中は対象のプロセスが中断する ※ |
※ 全スレッドの処理が中断した状態となり、 ステップ実行のような状態を変化させる API は使えません。 (VMCannotBeModifiedException が throw される API は使えない)
デバッグ接続中は全ての処理が完全に中断するので、運用中のサーバーアプリケーションなどへの適用には不向きだと思います。
(a) JDI でクラスの一覧を取得
まずは SAPIDAttachingConnector の動作確認も兼ねて、実行中のアプリケーションへデバッグ接続して、クラスの一覧を出力してみます。
SAPIDAttachingConnector を使ったデバッグ接続手順は以下のようになります。
- (1) Bootstrap から
com.sun.jdi.VirtualMachineManager
を取得 - (2) VirtualMachineManager から利用可能な JDI コネクタの一覧を
attachingConnectors
メソッドで取得し、SAPIDAttachingConnector を抽出 - (3) デバッグ対象アプリケーションのプロセスIDをパラメータへ設定しデバッグ接続
SAPIDAttachingConnector を使うには、JDI を使用するアプリケーション(下記スクリプト)の実行時のクラスパスへ lib/sa-jdi.jar を含めておかなければならない点に注意が必要です ※。
そうしないと attachingConnectors の結果に SAPIDAttachingConnector が含まれず、下記スクリプトでは NullPointerException となります。 (find の結果が null になるので)
※ JDI を使うには lib/tools.jar が必要ですが、 Groovy の場合は groovy-starter.conf のデフォルト設定で tools.jar をロードするようになっています
class_list.groovy
import com.sun.jdi.Bootstrap def pid = args[0] // (1) def manager = Bootstrap.virtualMachineManager() // (2) SAPIDAttachingConnector を抽出 def connector = manager.attachingConnectors().find { it.name() == 'sun.jvm.hotspot.jdi.SAPIDAttachingConnector' } // パラメータの設定 def params = connector.defaultArguments() params.get('pid').setValue(pid) // (3) デバッグ接続 def vm = connector.attach(params) try { // クラス情報 com.sun.jdi.ReferenceType の取得 vm.allClasses().each { cls -> println cls.name() } } finally { vm.dispose() }
動作確認
今回は apache-tomcat-9.0.0.M4 を起動しておき、上記スクリプトでデバッグ接続してクラスの情報を取得してみます。
実行例1 (Windows の場合)
まずはデバッグ対象の Tomcat を実行しておきます。(設定等はデフォルトのまま)
Tomcat 起動
> startup
jps 等でプロセス ID を調べて上記スクリプトを実行します。 JDK の lib/sa-jdi.jar をクラスパスへ指定する必要があります。
class_list.groovy の実行
> jps 804 Bootstrap 5244 Jps > groovy -cp %JAVA_HOME%/lib/sa-jdi.jar class_list.groovy 804 sun.management.HotSpotDiagnostic java.security.CodeSigner java.security.CodeSigner[] java.lang.Character java.lang.Character[] ・・・ short[] short[][] long[] float[] double[]
実行例2 (Linux の場合)
Linux でも同じです。
Tomcat 起動
$ ./startup.sh
class_list.groovy の実行
$ jps 2388 Jps 2363 Bootstrap $ groovy -cp $JAVA_HOME/lib/sa-jdi.jar class_list.groovy 2363 sun.nio.ch.SelectorProviderImpl java.util.jar.JarInputStream org.apache.catalina.mapper.Mapper$ContextVersion org.apache.catalina.mapper.Mapper$ContextVersion[] java.nio.channels.SeekableByteChannel ・・・ short[] short[][] long[] float[] double[]
SAPIDAttachingConnector により読み取り専用でデバッグ接続するため、デバッグ接続中に Tomcat へアクセスしても応答が返ってこなくなる点にご注意下さい。
(b) JDI でオブジェクトの年齢を取得
それでは、本題の年齢を取得してみます。
クラス情報 ReferenceType を取得するまでは上記 (a) と同じで、その後は以下のように JDI の API から Serviceability Agent API を取り出せばオブジェクトの年齢を取得できます。
- (1) ReferenceType の
instances
メソッドでインスタンス情報 ObjectReference を取得 (引数を 0 にすると全インスタンスを取得) - (2) (1) の実体が
sun.jvm.hotspot.jdi.ObjectReferenceImpl
なので、protected メソッドのref()
を呼び出してsun.jvm.hotspot.oops.Oop
(Serviceability Agent API) を取得 - (3) Oop の
getMark()
メソッドでsun.jvm.hotspot.oops.Mark
を取得し、age()
メソッドで年齢を取得
instances メソッドは非常に重い処理だったので、今回は対象クラスをコマンドライン引数 (第2引数) で指定した名称で始まるクラス名に限定するようにしました。 (下記 findAll の箇所)
また、Oop は他の方法 (sun.jvm.hotspot.oops.ObjectHeap
を使用) でも取得できるようです。
age_list.groovy
import com.sun.jdi.Bootstrap def pid = args[0] def prefix = args[1] def manager = Bootstrap.virtualMachineManager() def connector = manager.attachingConnectors().find { it.name() == 'sun.jvm.hotspot.jdi.SAPIDAttachingConnector' } def params = connector.defaultArguments() params.get('pid').setValue(pid) def vm = connector.attach(params) try { if (vm.canGetInstanceInfo()) { vm.allClasses().findAll { it.name().startsWith(prefix) }.each { cls -> println cls.name() // (1) インスタンス情報 ObjectReference の取得 cls.instances(0).each { inst -> // (2) Oop の取得 def oop = inst.ref() // (3) Mark を取得して年齢を取得 def age = oop.mark.age() println " handle=${oop.handle}, age=${age}" } } } } finally { vm.dispose() }
ここで、HotSpot VM のソース share/vm/oops/oop.inline.hpp の oopDesc::age()
を見ると、has_displaced_mark()
が true の時は displaced_mark
の age を取得しているのですが、同じように実装(下記)すると hasDisplacedMarkHelper が true の場合に sun.jvm.hotspot.debugger.UnalignedAddressException
が発生したため (Windows・Linux の両方で発生)、とりあえず displacedMarkHelper は今回無視するようにしました。
hasDisplacedMarkHelper が true の場合に UnalignedAddressException が発生したコード例
def mark = oop.mark def age = mark.hasDisplacedMarkHelper()? mark.displacedMarkHelper().age(): mark.age()
動作確認
先程と同じように実行中の apache-tomcat-9.0.0.M4 に対して適用してみます。
全クラスを対象にすると時間がかかり過ぎるので、クラス名が org.apache.catalina.core.ApplicationContext で始まるものに限定してみました。
実行例1 (Windows の場合)
> groovy -cp %JAVA_HOME%/lib/sa-jdi.jar age_list.groovy 804 org.apache.catalina.core.ApplicationContext org.apache.catalina.core.ApplicationContextFacade handle=0x00000000c39602f0, age=0 handle=0x00000000c3962460, age=1 handle=0x00000000c3971718, age=0 handle=0x00000000ecf913b0, age=3 handle=0x00000000ecf99950, age=1 org.apache.catalina.core.ApplicationContext handle=0x00000000c39607e0, age=0 handle=0x00000000c3966960, age=1 handle=0x00000000c3971c08, age=0 handle=0x00000000ecf918a0, age=3 handle=0x00000000ecf99680, age=1
実行例2 (Linux の場合)
$ groovy -cp $JAVA_HOME/lib/sa-jdi.jar age_list.groovy 2363 org.apache.catalina.core.ApplicationContext org.apache.catalina.core.ApplicationContextFacade handle=0x00000000fb041d08, age=1 handle=0x00000000fb1288d8, age=0 handle=0x00000000fb2b1df0, age=1 handle=0x00000000fb307c98, age=1 handle=0x00000000fb318aa8, age=1 org.apache.catalina.core.ApplicationContext handle=0x00000000fb0338c0, age=1 handle=0x00000000fb12ed50, age=0 handle=0x00000000fb2a56a0, age=1 handle=0x00000000fb2f8da8, age=1 handle=0x00000000fb312a80, age=1
Deeplearning4J で iris を分類
Deeplearning4J (DL4J) を使って 「ConvNetJS で iris を分類」 と同様に iris を分類してみました。
今回は Groovy を使って実行します。
ソースは http://github.com/fits/try_samples/tree/master/blog/20160412/
準備
iris データセット
iris のデータセットは org.deeplearning4j.datasets.DataSets.iris()
メソッドで取得できるため、ConvNetJS の時のようにダウンロードする必要はありません。
OpenBlas のインストール(Windows)
Deeplearning4J が利用する ND4J を効果的に使うには OpenBLAS などのネイティブの BLAS ライブラリが必要です。
「MNIST データセットのパース」 でも少し書きましたが、OpenBLAS を Windows 環境へインストールするには以下のようにします。
- http://nd4j.org/getstarted.html#open のリンクから ND4J_Win64_OpenBLAS-v0.2.14.zip をダウンロード・解凍し、環境変数 PATH へ設定
また、JniLoader が netlib-native_system-win-x86_64.dll
を %TEMP% ディレクトリへダウンロードするのを防止するには、この dll も環境変数 PATH へ設定した場所へ配置しておきます。 (dll は Maven の Central Repository からダウンロードできます)
今回は、ND4J_Win64_OpenBLAS-v0.2.14.zip の解凍先へ netlib-native_system-win-x86_64.dll も配置しました。
- C:\ND4J_Win64_OpenBLAS-v0.2.14
環境変数 PATH 設定例
> set PATH=C:\ND4J_Win64_OpenBLAS-v0.2.14;%PATH%
共通処理の作成
Deeplearning4J には学習モデルを JSON 化するような処理は用意されていないようです。
ただ、MultiLayerNetwork
自体が Serializable
なので、今回は Java のシリアライズ機能を使って保存等を行いました。
1. 学習モデルのセーブ・ロード処理
ObjectInputStream・ObjectOutputStream を使って MultiLayerNetwork
を保存・復元する処理です。
ModelManager.groovy
@Grab('org.deeplearning4j:deeplearning4j-core:0.4-rc3.8') @Grab('org.nd4j:nd4j-x86:0.4-rc3.8') import org.deeplearning4j.nn.conf.MultiLayerConfiguration import org.deeplearning4j.nn.multilayer.MultiLayerNetwork abstract class ModelManager extends Script { // MultiLayerNetwork の復元 def loadModel(String fileName) { new File(fileName).withObjectInputStream(this.class.classLoader) { it.readObject() as MultiLayerNetwork } } // MultiLayerNetwork の生成と保存 def saveModel(String fileName, MultiLayerConfiguration conf) { // MultiLayerNetwork の生成と初期構築 def model = new MultiLayerNetwork(conf) model.init() new File(fileName).withObjectOutputStream { it.writeObject model } } }
2. 学習・評価処理
以前 ConvNetJS で実装したものと同じような出力結果となるように実装してみました。
今回使用した MultiLayerNetwork
のメソッドは以下の通りです。
メソッド | 備考 |
---|---|
fit | 学習の実施(誤差の算出、重みの調整等) |
score | 誤差の取得(引数次第で誤差の算出も実施) |
output | ニューラルネットの処理結果を取得 |
output
は正解率の算出に使うだけなので、第 2引数を false にして評価モード(TrainingMode.TEST)で実行します。
今回は、学習時と評価時の処理を共通化するため score
メソッドで誤差を算出しましたが、setListeners
メソッドで ScoreIterationListener
を設定すれば誤差をログ出力できます。
Evaluation
の eval
メソッドへ正解のラベルデータとニューラルネットの処理結果を渡すと正解率などを算出してくれ、accuracy
メソッドで正解率を取得できます。 (stats
メソッドを使えば結果を文字列で取得する事もできます)
org.nd4j.linalg.dataset.api.DataSet
は splitTestAndTrain
メソッドで学習用と評価用にデータを分割 ※、batchBy
メソッドでミニバッチへ分割できます。
※ getTrain で学習用、getTest で評価用のデータセットを取得できます
iris_train.groovy
@Grab('org.deeplearning4j:deeplearning4j-core:0.4-rc3.8') @Grab('org.nd4j:nd4j-x86:0.4-rc3.8') import org.deeplearning4j.datasets.DataSets import org.deeplearning4j.eval.Evaluation import org.nd4j.linalg.dataset.SplitTestAndTrain import groovy.transform.BaseScript @BaseScript ModelManager baseScript def epoch = args[0] as int def trainRate = 0.7 def batchSize = 1 def modelFile = args[1] // 誤差・正解率の算出 class SimpleEvaluator { private def model private def ev = new Evaluation(3) // iris の品種の数 3 を設定 private def lossList = [] SimpleEvaluator(model) { this.model = model } def eval(d) { // 誤差の算出とリストへの追加 lossList << model.score(d) // 正解率などの算出 ev.eval(d.labels, model.output(d.featureMatrix, false)) } // 誤差(平均値) def loss() { lossList.sum() / lossList.size() } // 正解率 def accuracy() { ev.accuracy() } } // 学習モデル (MultiLayerNetwork) のロード def model = loadModel(modelFile) // iris データセットの取得 def data = DataSets.iris() (0..<epoch).each { def ev = [ train: new SimpleEvaluator(model), test: new SimpleEvaluator(model) ] data.shuffle() // 学習用とテスト用にデータセットを分割 def testAndTrain = data.splitTestAndTrain(trainRate) // 学習用データセットをミニバッチへ分割 testAndTrain.train.batchBy(batchSize).each { ev.train.eval(it) // 学習 model.fit(it) } // テスト用データセットを評価 ev.test.eval(testAndTrain.test) // 結果の出力 println([ ev.train.loss(), ev.train.accuracy(), ev.test.loss(), ev.test.accuracy() ].join(',')) }
また、デフォルトではログを標準出力するので、今回は設定ファイルを用意して無効化しました。
logback.xml
<configuration> <root level="OFF"></root> </configuration>
(a) 単純な構成 (入力層 - 出力層)
入力層と出力層だけの単純なニューラルネットを試します。
iris データセットは、4つの変数を使って 3つの品種に分類する事になりますので、OutputLayer.Builder
へ以下のように設定します。
nIn
へ変数の数 4 を設定nOut
へ分類の数 3 を設定
ソフトマックスと 3種類以上の交差エントロピー(Cross Entropy)を実施するには、以下のように設定すれば良いみたいです。
activation
へ softmax を設定OutputLayer.Builder
でLossFunctions.LossFunction.MCXENT
(Multiclass Cross Entropy) を設定
また、list には設定するレイヤーの数 (以下では 1) を設定します ※。
※ ただし、最新のソースで list(int) は Deprecated となっており、 代わりにレイヤー数を指定しなくても済む list() を使うようです また、list(int) は list() を呼ぶだけの実装へ変わっていました
create_iris_hnn1.groovy
@Grab('org.deeplearning4j:deeplearning4j-core:0.4-rc3.8') @Grab('org.nd4j:nd4j-x86:0.4-rc3.8') import org.deeplearning4j.nn.conf.NeuralNetConfiguration import org.deeplearning4j.nn.conf.Updater import org.deeplearning4j.nn.conf.layers.OutputLayer import org.nd4j.linalg.lossfunctions.LossFunctions import groovy.transform.BaseScript @BaseScript ModelManager baseScript def learningRate = args[0] as double def updateMethod = Updater.valueOf(args[1].toUpperCase()) def destFile = args[2] def conf = new NeuralNetConfiguration.Builder() .iterations(1) // 最適化の繰り返し回数(デフォルト設定は 5) .updater(updateMethod) .learningRate(learningRate) .list(1) .layer(0, new OutputLayer.Builder(LossFunctions.LossFunction.MCXENT) .nIn(4) .nOut(3) .activation('softmax') .build() ) .build() // 保存 saveModel(destFile, conf)
学習・評価
更新方法 (updateMethod) だけを変えたモデルを作って学習・評価を実施してみました。
(a-1) learningRate = 0.01, updateMethod = adam, epoch = 50
実行例
> groovy create_iris_hnn1.groovy 0.01 adam models/a-1_adam.ser ・・・ > groovy iris_train.groovy 50 models/a-1_adam.ser > results/a-1.csv ・・・
(a-2) learningRate = 0.01, updateMethod = adadelta, epoch = 50
実行例
> groovy create_iris_hnn1.groovy 0.01 adadelta models/a-2_adadelta.ser ・・・ > groovy iris_train.groovy 50 models/a-2_adadelta.ser > results/a-2.csv ・・・
(a-3) learningRate = 0.01, updateMethod = sgd, epoch = 50
実行例
> groovy create_iris_hnn1.groovy 0.01 sgd models/a-3_sgd.ser ・・・ > groovy iris_train.groovy 50 models/a-3_sgd.ser > results/a-3.csv ・・・
(b) 隠れ層を追加 (入力層 - 隠れ層 - 出力層)
次に、隠れ層を追加してみます。
DenseLayer
を追加して nIn
と nOut
を調整します。
create_iris_hnn2.groovy
@Grab('org.deeplearning4j:deeplearning4j-core:0.4-rc3.8') @Grab('org.nd4j:nd4j-x86:0.4-rc3.8') import org.deeplearning4j.nn.conf.NeuralNetConfiguration import org.deeplearning4j.nn.conf.Updater import org.deeplearning4j.nn.conf.layers.DenseLayer import org.deeplearning4j.nn.conf.layers.OutputLayer import org.nd4j.linalg.lossfunctions.LossFunctions import groovy.transform.BaseScript @BaseScript ModelManager baseScript def learningRate = args[0] as double def updateMethod = Updater.valueOf(args[1].toUpperCase()) // 隠れ層のニューロン数 def fcNeuNum = args[2] as int // 隠れ層の活性化関数 def fcAct = args[3] def destFile = args[4] def conf = new NeuralNetConfiguration.Builder() .iterations(1) .updater(updateMethod) .learningRate(learningRate) .list(2) // 隠れ層 .layer(0, new DenseLayer.Builder() .nIn(4) .nOut(fcNeuNum) .activation(fcAct) .build() ) .layer(1, new OutputLayer.Builder(LossFunctions.LossFunction.MCXENT) .nIn(fcNeuNum) .nOut(3) .activation('softmax') .build() ) .build() // 保存 saveModel(destFile, conf)
学習・評価
隠れ層の活性化関数 (fcAct) だけを変えたモデルを作って学習・評価を実施してみました。
(b-1) fcNeuNum = 8, fcAct = relu, learningRate = 0.01, updateMethod = adam, epoch = 50
実行例
> groovy create_iris_hnn2.groovy 0.01 adam 8 relu models/b-1_adam_relu.ser ・・・ > groovy iris_train.groovy 50 models/b-1_adam_relu.ser > results/b-1.csv ・・・
(b-2) fcNeuNum = 8, fcAct = sigmoid, learningRate = 0.01, updateMethod = adam, epoch = 50
実行例
> groovy create_iris_hnn2.groovy 0.01 adam 8 sigmoid models/b-2_adam_sigmoid.ser ・・・ > groovy iris_train.groovy 50 models/b-2_adam_sigmoid.ser > results/b-2.csv ・・・
ConvNetJS で MNIST を分類2 - 畳み込みニューラルネット
前回 の続きです。 今回は畳み込みニューラルネットを使って MNIST の手書き数字を分類してみます。
- Node.js 5.8.0
- ConvNetJS 0.3.0
ソースは http://github.com/fits/try_samples/tree/master/blog/20160328/
準備
誤差・正解率のグラフ化と畳み込みフィルタの画像化を行うため、前回の構成へ d3
等を追加しています。
package.json
{ "name": "convnetjs_mnist_conv_sample", "version": "1.0.0", "description": "", "main": "index.js", "dependencies": { "basic-csv": "0.0.2", "bluebird": "^3.3.4", "convnetjs": "^0.3.0", "d3": "^3.5.16", "jsdom": "^8.1.0", "shuffle-array": "^0.1.2" } }
インストール例
> npm install
(b) 畳み込みニューラルネット
畳み込みニューラルネットでは畳み込み層とプーリング層を組み合わせてレイヤーを構築します。 (実際は全結合層も使います)
名称 | 処理 | ConvNetJS の layer_type |
---|---|---|
畳み込み層 | 入力画像へフィルターを適用し特徴量を抽出 | conv |
プーリング層 | 入力画像へプーリング演算(フィルター内)を適用 | pool |
ConvNetJS の畳み込み層・プーリング層は以下のように設定します。
sx
とsy
でフィルターのサイズを指定 (sy
を省略するとsx
と同じ値を適用)pad
で入力画像の周囲にゼロパディング(0埋め)する数を指定stride
でフィルタの適用位置を縦横に移動する数を指定 (1 の場合は縦横に 1画素ずつずらしてフィルターを適用)
畳み込み層では filters
で適用するフィルターの数を指定します。
プーリング層では 「最大プーリング」 (フィルターの値の最大値を採用) を行うようになっており、今回試したバージョンでは (「平均プーリング」等へ) プーリング方法を変更する機能は無さそうでした。
今回は、以下のように畳み込み層・プーリング層が 2回続くような構成にしてみました。
create_layer_conv.js (畳み込みニューラルネットのモデル構築と保存処理)
'use strict'; // 畳み込み層の活性化関数 const act = process.argv[2]; // 出力ファイル名 const jsonDestFile = process.argv[3]; require('./save_model').saveModel( [ { type: 'input', out_sx: 28, out_sy: 28, out_depth: 1 }, // 1つ目の畳み込み層 { type: 'conv', sx: 5, filters: 8, stride: 1, pad: 2, activation: act }, // 1つ目のプーリング層 { type: 'pool', sx: 2, stride: 2 }, // 2つ目の畳み込み層 { type: 'conv', sx: 5, filters: 16, stride: 1, pad: 2, activation: act }, // 2つ目のプーリング層 { type: 'pool', sx: 3, stride: 3 }, { type: 'softmax', num_classes: 10 } ], jsonDestFile );
活性化関数へ relu を指定した場合の内部的なレイヤー構成は以下のようになりました。
学習モデルの内部的なレイヤー構成例
input -> conv -> relu -> pool -> conv -> relu -> pool -> fc -> softmax
各レイヤーの出力サイズは以下の通りです。
layer_type | out_sx | out_sy | out_depth |
---|---|---|---|
input | 28 | 28 | 1 |
conv | 28 | 28 | 8 |
relu | 28 | 28 | 8 |
pool | 14 | 14 | 8 |
conv | 14 | 14 | 16 |
relu | 14 | 14 | 16 |
pool | 4 | 4 | 16 |
fc | 1 | 1 | 10 |
softmax | 1 | 1 | 10 |
学習と評価
前回 作成した共通処理 (learn_mnist.js 等) を使って学習と評価を実施します。
学習回数を前回と同じ 15回にすると相当時間がかかってしまうので、今回は以下の 4種類で学習・評価を試してみました。
- 活性化関数 = relu, 学習回数 = 5
- 活性化関数 = relu, 学習回数 = 10
- 活性化関数 = sigmoid, 学習回数 = 5
- 活性化関数 = sigmoid, 学習回数 = 10
学習回数以外は前回と同じパラメータを使います。
- 学習回数 = 15
- バッチサイズ = 100
- 学習係数 = 0.001
- 学習係数の決定方法 = adadelta
処理時間は学習回数 5回で 1.5時間、10回で 3時間程度でした。
PC の性能にも依存すると思いますが、1つの CPU で処理するので比較的遅めだと思います。
1. 活性化関数 = relu, 学習回数 = 5 (バッチサイズ = 100, 学習係数 = 0.001, adadelta)
> node create_layer_conv.js relu models/conv_relu.json > node learn_mnist.js 5 100 0.001 adadelta models/conv_relu.json results/b-1_conv_relu.json > logs/b-1_conv_relu.log > node validate_mnist.js results/b-1_conv_relu.json data size: 10000 accuracy: 0.9785
学習時の誤差と正解率
学習後のフィルター
2. 活性化関数 = relu, 学習回数 = 10 (バッチサイズ = 100, 学習係数 = 0.001, adadelta)
> node learn_mnist.js 10 100 0.001 adadelta models/conv_relu.json results/b-2_conv_relu.json > logs/b-2_conv_relu.log > node validate_mnist.js results/b-2_conv_relu.json data size: 10000 accuracy: 0.9786
学習時の誤差と正解率
学習後のフィルター
3. 活性化関数 = sigmoid, 学習回数 = 5 (バッチサイズ = 100, 学習係数 = 0.001, adadelta)
> node create_layer_conv.js sigmoid models/conv_sigmoid.json > node learn_mnist.js 5 100 0.001 adadelta models/conv_sigmoid.json results/b-3_conv_sigmoid.json > logs/b-3_conv_sigmoid.log > node validate_mnist.js results/b-3_conv_sigmoid.json data size: 10000 accuracy: 0.9812
学習時の誤差と正解率
学習後のフィルター
4. 活性化関数 = sigmoid, 学習回数 = 10 (バッチサイズ = 100, 学習係数 = 0.001, adadelta)
> node learn_mnist.js 10 100 0.001 adadelta models/conv_sigmoid.json results/b-4_conv_sigmoid.json > logs/b-4_conv_sigmoid.log > node validate_mnist.js results/b-4_conv_sigmoid.json data size: 10000 accuracy: 0.9862
学習時の誤差と正解率
学習後のフィルター
結果のまとめ
番号 | 活性化関数 | 学習回数 | 正解率 |
---|---|---|---|
1 | relu | 5 | 0.9785 |
2 | relu | 10 | 0.9786 |
3 | sigmoid | 5 | 0.9812 |
4 | sigmoid | 10 | 0.9862 |
前回の結果よりも高い正解率となりました。
補足
(1) 誤差と正解率のグラフ化
誤差と正解率のログは以下のようなスクリプトでグラフ化しました。
誤差の値が Infinity
となる事があったので ※、その場合はとりあえず固定値(以下では 1000)で置換するようにしています。
※ ただし、Infinity となったのは前回の階層型ニューラルネットの結果で、 畳み込みニューラルネットの結果では発生していません
line_chart.js
'use strict'; const Promise = require('bluebird'); const d3 = require('d3'); const jsdom = require('jsdom').jsdom; const readCSV = Promise.promisify(require('basic-csv').readCSV); const w = 300; const h = 300; const margin = { top: 20, bottom: 50, left: 50, right: 20 }; const xLabels = ['バッチ回数', 'バッチ回数']; const yLabels = ['誤差', '正解率']; readCSV(process.argv[2]).then( ds => { const document = jsdom(); const chartLayout = (xnum, w, h, margin) => { const borderWidth = w + margin.left + margin.right; const borderHeight = h + margin.top + margin.bottom; const svg = d3.select(document.body).append('svg') .attr('xmlns', 'http://www.w3.org/2000/svg') .attr('width', xnum * borderWidth) .attr('height', borderHeight); return Array(xnum).fill(0).map( (n, i) => svg.append('g') .attr('transform', `translate(${i * borderWidth + margin.left}, ${margin.top})`) ); }; const xDomain = [0, ds.length]; const yDomain = [1, 0]; // スケールの定義 const x = d3.scale.linear().range([0, w]).domain(xDomain); const y = d3.scale.linear().range([0, h]).domain(yDomain); // 軸の定義 const xAxis = d3.svg.axis().scale(x).orient('bottom').ticks(5); const yAxis = d3.svg.axis().scale(y).orient('left'); // 折れ線の作成 const createLine = d3.svg.line() .x((d, i) => x(i + 1)) .y(d => { // Infinity の際に固定値を設定 if (d == 'Infinity') { d = 1000; } return y(d); }); // 折れ線の描画 const drawLine = (g, data, colIndex, color) => { g.append('path') .attr('d', createLine(data.map(d => d[colIndex]))) .attr('stroke', color) .attr('fill', 'none'); }; const gs = chartLayout(2, w, h, margin); // X・Y軸の描画 gs.forEach( (g, i) => { g.append('g') .attr('transform', `translate(0, ${h})`) .call(xAxis) .append('text') .attr('x', w / 2) .attr('y', 35) .style('font-family', 'Sans') .text(xLabels[i]); g.append('g') .call(yAxis) .append('text') .attr('x', -h / 2) .attr('y', -35) .attr('transform', 'rotate(-90)') .style('font-family', 'Sans') .text(yLabels[i]); }); drawLine(gs[0], ds, 2, 'blue'); drawLine(gs[1], ds, 3, 'blue'); return document.body.innerHTML; }).then( html => console.log(html) );
このスクリプトを使って svg ファイルへ出力し、ImageMagick で png ファイルへ変換しました。
実行例
> node line_chart.js logs/b-1_conv_relu.log > img/b-1.svg > convert img/b-1.svg img/b-1.png
(2) 畳み込み層のフィルターを svg 化
学習後の畳み込み層のフィルターを以下のようなスクリプトで画像化(svg)してみました。
実際のフィルターサイズは 5x5 と小さくて分かり難いので、d3.scale
を使って 50x50 へ拡大しています。
また、フィルターを可視化するため、d3.scale
でフィルターの値が 0 ~ 255 となるように変換しています。
conv_filter_svg.js
'use strict'; const Promise = require('bluebird'); const convnetjs = require('convnetjs'); const d3 = require('d3'); const jsdom = require('jsdom').jsdom; const readFile = Promise.promisify(require('fs').readFile); const size = 50; const margin = 5; const modelJsonFile = process.argv[2]; // フィルター内の最小値と最大値を抽出 const valueRange = fs => fs.reduce( (acc, f) => [ Math.min(acc[0], Math.min.apply(null, f.w)), Math.max(acc[1], Math.max.apply(null, f.w)) ], [0, 0] ); readFile(modelJsonFile).then( json => { const net = new convnetjs.Net(); net.fromJSON(JSON.parse(json)); return net.layers; }).then( layers => layers.reduce( (acc, v) => { // 畳み込み層のフィルターを抽出 if (v.layer_type == 'conv' && v.filters) { acc.push( v.filters ); } return acc; }, []) ).then( filtersList => { const document = jsdom(); const svg = d3.select(document.body) .append('svg') .attr('xmlns', 'http://www.w3.org/2000/svg'); filtersList.forEach( (fs, j) => { const yPos = (size + margin) * j; // フィルターの数値を 0 ~ 255 の値へ変換 const pixelScale = d3.scale.linear() .range([0, 255]).domain(valueRange(fs)); fs.forEach( (f, i) => { const xPos = (size + margin) * i; const g = svg.append('g') .attr('transform', `translate(${xPos}, ${yPos})`); const xScale = d3.scale.linear() .range([0, size]).domain([0, f.sx]); const yScale = d3.scale.linear() .range([0, size]).domain([0, f.sy]); for (let y = 0; y < f.sy; y++) { for (let x = 0; x < f.sx; x++) { const p = pixelScale( f.get(x, y, 0) ); g.append('rect') .attr('x', xScale(x)) .attr('y', yScale(y)) .attr('width', xScale(1)) .attr('height', yScale(1)) .attr('fill', d3.rgb(p, p, p)); } } }); }); return document.body.innerHTML; }).then( svg => console.log(svg) ).catch( e => console.error(e) );
こちらも、svg ファイルとして出力し、ImageMagick で png ファイルへ変換しました。
実行例
> node conv_filter_svg.js results/b-1_conv_relu.json > img/b-1_filters.svg > convert img/b-1_filters.svg img/b-1_filters.png
ConvNetJS で MNIST を分類1 - 階層型ニューラルネット
Node.js で ConvNetJS を使って MNIST の手書き数字を分類してみます。
今回は階層型ニューラルネット、次回は畳み込みニューラルネットを試す予定です。
- Node.js 5.8.0
- ConvNetJS 0.3.0
ソースは http://github.com/fits/try_samples/tree/master/blog/20160322/
準備
npm で convnetjs 等のモジュールをインストールします。(概ね「ConvNetJS で iris を分類 」と同じ構成です)
package.json
{ "name": "convnetjs_mnist_sample", "version": "1.0.0", "description": "", "main": "index.js", "dependencies": { "bluebird": "^3.3.4", "convnetjs": "^0.3.0", "shuffle-array": "^0.1.2" } }
インストール例
> npm install
共通処理
ConvNetJS には、ニューラルネットの学習モデルを JSON で入出力する機能(fromJSON
と toJSON
)がありますので、今回はこの機能を使って学習前と後のモデルを JSON ファイルで扱う事にします。
MNIST には学習用のデータセット 6万件と評価用のデータセット 1万件がそれぞれ用意されていますので、今回は 6万件の学習データセットを全て学習に使い、1万件の評価データセットで評価する事にしました。
今回と次回で共通に使う処理として以下のようなスクリプトを作成しました。
(1) MNIST データセットのロード
MNIST の学習・評価データセットをロードする処理です。(処理内容に関しては 前回 を参照)
load_mnist.js
'use strict'; const Promise = require('bluebird'); const convnetjs = require('convnetjs'); const fs = require('fs'); const readFile = Promise.promisify(fs.readFile); const readToBuffer = file => readFile(file).then(r => new Buffer(r, 'binary')); const loadImages = file => readToBuffer(file) .then(buf => { const magicNum = buf.readInt32BE(0); const num = buf.readInt32BE(4); const rowNum = buf.readInt32BE(8); const colNum = buf.readInt32BE(12); const dataBuf = buf.slice(16); const res = Array(num); let offset = 0; for (let i = 0; i < num; i++) { const data = new convnetjs.Vol(colNum, rowNum, 1, 0); for (let y = 0; y < rowNum; y++) { for (let x = 0; x < colNum; x++) { const value = dataBuf.readUInt8(offset++); data.set(x, y, 0, value); } } res[i] = data; } return res; }); const loadLabels = file => readToBuffer(file) .then(buf => { const magicNum = buf.readInt32BE(0); const num = buf.readInt32BE(4); const dataBuf = buf.slice(8); const res = Array(num); for (let i = 0; i < num; i++) { res[i] = dataBuf.readUInt8(i); } return res; }); module.exports.loadMnist = (imgFile, labelFile) => Promise.all([ loadImages(imgFile), loadLabels(labelFile) ]).spread( (r1, r2) => r2.map((label, i) => new Object({ values: r1[i], label: label })) );
(2) 学習モデルの保存
ニューラルネットの学習モデルを JSON ファイルへ保存する処理です。
save_model.js
'use strict'; const Promise = require('bluebird'); const convnetjs = require('convnetjs'); const writeFile = Promise.promisify(require('fs').writeFile); module.exports.saveModel = (layers, destFile) => { const net = new convnetjs.Net(); // 内部的なレイヤーの構築 net.makeLayers(layers); // JSON 化してファイルへ保存 writeFile(destFile, JSON.stringify(net.toJSON())) .catch( e => console.error(e) ); };
(3) 学習
指定の学習モデル (JSON) を MNIST の学習データセットで学習する処理です。
処理の進行状況を確認できるように batchSize 毎に誤差の平均と正解率を出力するようにしました。 (ただし、以下の方法では batchSize 次第で Trainer によるパラメータの更新タイミングと合わなくなります)
また、MNIST データセットの配列を直接シャッフルする代わりに、0 ~ 59999 のインデックス値から成る配列を用意して、それをシャッフルするようにしています。
learn_mnist.js
'use strict'; const Promise = require('bluebird'); const fs = require('fs'); const shuffle = require('shuffle-array'); const convnetjs = require('convnetjs'); const readFile = Promise.promisify(fs.readFile); const writeFile = Promise.promisify(fs.writeFile); const mnist = require('./load_mnist'); const epoch = parseInt(process.argv[2]); const batchSize = parseInt(process.argv[3]); const learningRate = parseFloat(process.argv[4]); const trainMethod = process.argv[5]; const modelJsonFile = process.argv[6]; const modelJsonDestFile = process.argv[7]; // 0 ~ n - 1 を要素とする配列作成 const range = n => { const res = Array(n); for (let i = 0; i < n; i++) { res[i] = i; } return res; }; // 指定サイズ毎に誤差の平均と正解率を出力する処理 const createLogger = (logSize, logFunc) => { let list = []; let counter = 0; return (loss, accuracy) => { list.push({loss: loss, accuracy: accuracy}); const size = list.length; if (size >= logSize) { const res = list.reduce( (acc, d) => { acc.loss += d.loss; acc.accuracy += d.accuracy; return acc; }, { loss: 0.0, accuracy: 0 } ); // 出力処理の実行 logFunc( res.loss / size, res.accuracy / size, counter++ ); list = []; } }; }; Promise.all([ readFile(modelJsonFile), mnist.loadMnist('train-images.idx3-ubyte', 'train-labels.idx1-ubyte') ]).spread( (json, data) => { const net = new convnetjs.Net(); // JSON から学習モデルを復元 net.fromJSON(JSON.parse(json)); const trainer = new convnetjs.Trainer(net, { method: trainMethod, batch_size: batchSize, learning_rate: learningRate }); range(epoch).forEach(ep => { // ログ出力処理の作成 const log = createLogger(batchSize, (loss, acc, counter) => console.log( [ep, counter, loss, acc].join(',') ) ); // インデックス値の配列を作成しシャッフル shuffle(range(data.length)).forEach(i => { // 該当するデータを取得 const d = data[i]; // 学習 const stats = trainer.train(d.values, d.label); log( stats.loss, (net.getPrediction() == d.label)? 1: 0 ); }); }); return net; }).then( net => // 学習モデルの保存 writeFile(modelJsonDestFile, JSON.stringify(net.toJSON())) ).catch( e => console.error(e) );
(4) 評価(テスト)
指定の学習モデル (JSON) で MNIST の評価データセットを処理し、正解率を出力する処理です。
validate_mnist.js
'use strict'; const Promise = require('bluebird'); const convnetjs = require('convnetjs'); const readFile = Promise.promisify(require('fs').readFile); const mnist = require('./load_mnist'); const modelJsonFile = process.argv[2]; Promise.all([ readFile(modelJsonFile), mnist.loadMnist('t10k-images.idx3-ubyte', 't10k-labels.idx1-ubyte') ]).spread( (json, data) => { const net = new convnetjs.Net(); // JSON から学習モデルを復元 net.fromJSON(JSON.parse(json)); const accuCount = data.reduce((acc, d) => { net.forward(d.values); // 正解数のカウント return acc + (d.label == net.getPrediction()? 1: 0); }, 0); console.log(`data size: ${data.length}`); // 正解率の出力 console.log(`accuracy: ${accuCount / data.length}`); });
(a) 階層型ニューラルネット
MNIST の画像サイズは 28x28 のため、入力層 (type = input) の out_sx
と out_sy
へそれぞれ 28 を設定し、画素値は 1バイトのため out_depth
へ 1 を設定します。
出力層 (type = output)は 0 ~ 9 の分類 (10種類) となるため、type
を softmax
にして num_classes
へ 10 を設定します。
今回は、隠れ層を 1層にして活性化関数とニューロン数をコマンドライン引数で指定できるようにしました。
create_layer_hnn.js
'use strict'; const act = process.argv[2]; const numNeurons = parseInt(process.argv[3]); const jsonDestFile = process.argv[4]; require('./save_model').saveModel( [ { type: 'input', out_sx: 28, out_sy: 28, out_depth: 1 }, { type: 'fc', activation: act, num_neurons: numNeurons }, { type: 'softmax', num_classes: 10 } ], jsonDestFile );
例えば、上記の活性化関数へ relu
を指定した場合の makeLayers
の結果は下記のようになります。
学習モデルの内部的なレイヤー構成例
input -> fc -> relu -> fc -> softmax
type
へ softmax を指定した場合、fc 層が差し込まれるようになっています。
学習と評価
今回は以下の 4種類で学習・評価を試してみました。
- 活性化関数 = relu, ニューロン数 = 50
- 活性化関数 = relu, ニューロン数 = 300
- 活性化関数 = sigmoid, ニューロン数 = 50
- 活性化関数 = sigmoid, ニューロン数 = 300
学習回数などのパラメータはとりあえず下記で実行します。
- 学習回数 = 15
- バッチサイズ = 100
- 学習係数 = 0.001
- 学習係数の決定方法 = adadelta
バッチサイズを 100 とする事で、学習データ 100件毎にパラメータ(重み)の更新が実施されます。
なお、処理時間はニューロン数や学習回数・バッチサイズなどに影響されます。(今回、ニューロン数 50 では 10分程度、300 では 50分程度でした)
1. 活性化関数 = relu, ニューロン数 = 50 (学習回数 = 15, バッチサイズ = 100, 学習係数 = 0.001, adadelta)
> node create_layer_hnn.js relu 50 models/relu_50.json > node learn_mnist.js 15 100 0.001 adadelta models/relu_50.json results/a-1_relu_50.json > logs/a-1_relu_50.log > node validate_mnist.js results/a-1_relu_50.json data size: 10000 accuracy: 0.9455
学習時の誤差と正解率
2. 活性化関数 = relu, ニューロン数 = 300 (学習回数 = 15, バッチサイズ = 100, 学習係数 = 0.001, adadelta)
> node create_layer_hnn.js relu 300 models/relu_300.json > node learn_mnist.js 15 100 0.001 adadelta models/relu_300.json results/a-2_relu_300.json > logs/a-2_relu_300.log > node validate_mnist.js results/a-2_relu_300.json data size: 10000 accuracy: 0.965
学習時の誤差と正解率
3. 活性化関数 = sigmoid, ニューロン数 = 50 (学習回数 = 15, バッチサイズ = 100, 学習係数 = 0.001, adadelta)
> node create_layer_hnn.js sigmoid 50 models/sigmoid_50.json > node learn_mnist.js 15 100 0.001 adadelta models/sigmoid_50.json results/a-3_sigmoid_50.json > logs/a-3_sigmoid_50.log > node validate_mnist.js results/a-3_sigmoid_50.json data size: 10000 accuracy: 0.9368
学習時の誤差と正解率
4. 活性化関数 = sigmoid, ニューロン数 = 300 (学習回数 = 15, バッチサイズ = 100, 学習係数 = 0.001, adadelta)
> node create_layer_hnn.js sigmoid 300 models/sigmoid_300.json > node learn_mnist.js 15 100 0.001 adadelta models/sigmoid_300.json results/a-4_sigmoid_300.json > logs/a-4_sigmoid_300.log > node validate_mnist.js results/a-4_sigmoid_300.json data size: 10000 accuracy: 0.9631
学習時の誤差と正解率
結果のまとめ
番号 | 活性化関数(隠れ層) | ニューロン数(隠れ層) | 正解率 |
---|---|---|---|
1 | relu | 50 | 0.9455 |
2 | relu | 300 | 0.965 |
3 | sigmoid | 50 | 0.9368 |
4 | sigmoid | 300 | 0.9631 |
MNIST データセットをパースする
MNIST データセットは、THE MNIST DATABASE of handwritten digits からダウンロード可能な手書き数字のデータです。
機械学習ライブラリ等に標準で用意されてたりしますが、 今回は Node.js と Java でパースしてみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20160307/
概要
MNIST は以下のようなデータです。
- 0 ~ 9 の手書き数字のグレースケール画像
- 1つの画像は 28x28 のサイズ
- 学習用に 6万件、テスト用に 1万件が用意されている
- 画像データとラベルデータのファイルが対になっている
下記のように 4種類のファイルが用意されています。
画像データファイル | ラベルデータファイル | |
---|---|---|
学習用 | train-images.idx3-ubyte | train-labels.idx1-ubyte |
テスト用 | t10k-images.idx3-ubyte | t10k-labels.idx1-ubyte |
ファイルフォーマットは以下の通りです。
画像データのファイルフォーマット
オフセット | タイプ | 値 | 内容 |
---|---|---|---|
0 | 32bit integer | 2051 | マジックナンバー |
4 | 32bit integer | 60000 or 10000 | 画像の数 |
8 | 32bit integer | 28 | 行の数 |
12 | 32bit integer | 28 | 列の数 |
16~ | unsigned byte | 0~255 ※ | 1バイトずつピクセル値が連続 |
先頭 16バイトがヘッダー部分、それ以降に 1画像 28 x 28 = 784 バイトのデータが 6万件もしくは 1万件続きます。
※ 0 が白、255 が黒
ラベルデータのファイルフォーマット
オフセット | タイプ | 値 | 内容 |
---|---|---|---|
0 | 32bit integer | 2049 | マジックナンバー |
4 | 32bit integer | 60000 or 10000 | ラベルの数 |
8~ | unsigned byte | 0~9 | 1バイトずつラベル値が連続 |
先頭 8バイトがヘッダー部分、それ以降にラベルデータが 6万件もしくは 1万件続きます。
(a) ConvNetJS 用に変換 (Node.js)
それでは、Node.js を使ってパースしてみます。
今回は ConvNetJS で使えるように、画像データを convnetjs.Vol
として作成するようにしました。
Buffer
を使ってバイナリデータを処理します。
MNIST のデータはビッグエンディアンのようなので 32bit integer を読み込む際は readInt32BE
を使います。
なお、マジックナンバーに関してはチェックしていません。
load_mnist.js
var Promise = require('bluebird'); var convnetjs = require('convnetjs'); var fs = require('fs'); var readFile = Promise.promisify(fs.readFile); // ファイル内容から Buffer 作成 var readToBuffer = file => readFile(file).then(r => new Buffer(r, 'binary')); // 画像データのロード var loadImages = file => readToBuffer(file) .then(buf => { var magicNum = buf.readInt32BE(0); var num = buf.readInt32BE(4); var rowNum = buf.readInt32BE(8); var colNum = buf.readInt32BE(12); // 画像データ部分を分離(ヘッダー部分を除外) var dataBuf = buf.slice(16); var res = Array(num); var offset = 0; for (var i = 0; i < num; i++) { var data = new convnetjs.Vol(colNum, rowNum, 1, 0); for (var y = 0; y < rowNum; y++) { for (var x = 0; x < colNum; x++) { var value = dataBuf.readUInt8(offset++); data.set(x, y, 0, value); } } res[i] = data; } return res; }); // ラベルデータのロード var loadLabels = file => readToBuffer(file) .then(buf => { var magicNum = buf.readInt32BE(0); var num = buf.readInt32BE(4); // ラベルデータ部分を分離(ヘッダー部分を除外) var dataBuf = buf.slice(8); var res = Array(num); for (var i = 0; i < num; i++) { res[i] = dataBuf.readUInt8(i); } return res; }); // 画像・ラベルデータをロード module.exports.loadMnist = (imgFile, labelFile) => Promise.all([ loadImages(imgFile), loadLabels(labelFile) ]).spread( (r1, r2) => r2.map((label, i) => { return { values: r1[i], label: label }; }) );
bluebird
と convnetjs
パッケージを使っています。
package.json
{ "name": "mnist_parse_sample", "version": "1.0.0", "description": "", "dependencies": { "bluebird": "^3.3.3", "convnetjs": "^0.3.0" } }
動作確認
以下のテストコードを使って簡単な動作確認を行います。
手書き数字の大まかな形状を確認できるように、ピクセル値が 0 より大きければ #
へ、それ以外は へ変換し出力してみました。
test_load_mnist.js
var mnist = require('./load_mnist'); var printData = d => { console.log(`***** number = ${d.label} *****`); var v = d.values; for (var y = 0; y < v.sy; y++) { var r = Array(v.sx); for (var x = 0; x < v.sx; x++) { // ピクセル値が 0より大きいと '#'、それ以外は ' ' r[x] = v.get(x, y, 0) > 0 ? '#' : ' '; } // 文字で表現した画像を出力 console.log(r.join('')); } // ピクセル値を出力 console.log(d.values.w.join(',')); }; mnist.loadMnist(process.argv[2], process.argv[3]) .then(ds => { console.log(`size: ${ds.length}`); printData(ds[0]); console.log('----------'); printData(ds[1]); });
MNIST 学習用データを使った実行結果は以下の通りです。
実行結果
> node test_load_mnist.js train-images.idx3-ubyte train-labels.idx1-ubyte size: 60000 ***** number = 5 ***** ############ ################ ################ ########### ####### ## ##### #### #### ###### ###### ###### ##### #### ####### ######## ######### ########## ########## ########## ######## 0,・・・,3,18,18,18,126,136,175,26,166,255,・・・,0 ---------- ***** number = 0 ***** ##### ###### ######### ########### ########### ############ ######### ### ###### ### ####### ### #### ### ### ### #### ### #### ##### ### ##### ### #### ### #### ############# ########### ######### ####### 0,・・・,51,159,253,159,50,・・・,0
(b) Deeplearning4J 用に変換 (Java)
次は Deeplearning4J で使えるように Java で org.nd4j.linalg.dataset.DataSet
へ変換します。
画像データとラベルデータをそれぞれ org.nd4j.linalg.api.ndarray.INDArray
として作成し、最後に DataSet へまとめます。
1件の画像データは 784 (= 28 x 28) 要素のフラットな INDArray として作成します ※。
※ Deeplearning4J では、ConvolutionLayerSetup を使って 画像の高さ・幅を指定できるため、フラットなデータにして問題ありません
ラベルの値は FeatureUtil.toOutcomeVector()
メソッドで作成します。
このメソッドによって、ラベルの種類と同じ数 (今回は 10) の要素を持ち、該当するインデックスの値だけが 1 で他は 0 になった配列 (INDArray) を得られます。
このように、変換後のデータセットの構造は (a) のケースとは異なります。
また、Java には unsigned byte
という型がなく、ピクセル値を byte として読み込むと -128 ~ 127 になってしまいます。そこで & 0xff
して正しい値 (0 ~ 255) となるように変換しています。
ちなみに、Deeplearning4J の org.deeplearning4j.datasets.DataSets.mnist()
メソッドを使えば MNIST データセットを取得できますが、その場合はピクセル値が正規化 ※ されて 0 か 1 の値となります。 (シャッフルも実施される)
※ ピクセル値 > 30 の場合は 1、それ以外は 0 になる
src/main/java/MnistLoader.java
import org.nd4j.linalg.api.ndarray.INDArray; import org.nd4j.linalg.dataset.DataSet; import org.nd4j.linalg.factory.Nd4j; import org.nd4j.linalg.util.FeatureUtil; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Paths; import java.util.concurrent.CompletableFuture; public class MnistLoader { private final static int LABELS_NUM = 10; public static CompletableFuture<DataSet> loadMnist(String imageFileName, String labelFileName) { return CompletableFuture.supplyAsync(() -> loadImages(imageFileName)) .thenCombineAsync( CompletableFuture.supplyAsync(() -> loadLabels(labelFileName)), DataSet::new ); } private static INDArray loadImages(String fileName) { try (FileChannel fc = FileChannel.open(Paths.get(fileName))) { ByteBuffer headerBuf = ByteBuffer.allocateDirect(16); // ヘッダー部分の読み込み fc.read(headerBuf); headerBuf.rewind(); int magicNum = headerBuf.getInt(); int num = headerBuf.getInt(); int rowNum = headerBuf.getInt(); int colNum = headerBuf.getInt(); ByteBuffer buf = ByteBuffer.allocateDirect(num * rowNum * colNum); // 画像データ部分の読み込み fc.read(buf); buf.rewind(); int dataSize = rowNum * colNum; INDArray res = Nd4j.create(num, dataSize); for (int n = 0; n < num; n++) { INDArray d = Nd4j.create(1, dataSize); for (int i = 0; i < dataSize; i++) { // & 0xff する事で unsigned の値へ変換 d.putScalar(i, buf.get() & 0xff); } res.putRow(n, d); } return res; } catch(IOException ex) { throw new RuntimeException(ex); } } private static INDArray loadLabels(String fileName) { try (FileChannel fc = FileChannel.open(Paths.get(fileName))) { ByteBuffer headerBuf = ByteBuffer.allocateDirect(8); // ヘッダー部分の読み込み fc.read(headerBuf); headerBuf.rewind(); int magicNum = headerBuf.getInt(); int num = headerBuf.getInt(); ByteBuffer buf = ByteBuffer.allocateDirect(num); // ラベルデータ部分の読み込み fc.read(buf); buf.rewind(); INDArray res = Nd4j.create(num, LABELS_NUM); for (int i = 0; i < num; i++) { res.putRow(i, FeatureUtil.toOutcomeVector(buf.get(), LABELS_NUM)); } return res; } catch(IOException ex) { throw new RuntimeException(ex); } } }
動作確認
Gradle で以下のテストコードを実行し簡単な動作確認を行います。
src/main/java/TestMnistLoader.java
import org.nd4j.linalg.api.ndarray.INDArray; import org.nd4j.linalg.dataset.DataSet; import java.util.stream.IntStream; public class TestMnistLoader { public static void main(String... args) { MnistLoader.loadMnist(args[0], args[1]) .thenAccept(ds -> { System.out.println("size: " + ds.numExamples()); printData(ds.get(0)); System.out.println("----------"); printData(ds.get(1)); }) .join(); } private static void printData(DataSet d) { System.out.println("***** labels = " + d.getLabels()); INDArray v = d.getFeatures(); IntStream.range(0, 28).forEach( y -> { IntStream.range(0, 28).forEach ( x -> { System.out.print( v.getInt(x + y * 28) > 0 ? "#" : " " ); }); System.out.println(); }); System.out.println(d.getFeatures()); } }
build.gradle
apply plugin: 'application' tasks.withType(AbstractCompile)*.options*.encoding = 'UTF-8' mainClassName = 'TestMnistLoader' repositories { jcenter() } dependencies { compile 'org.nd4j:nd4j-x86:0.4-rc3.8' runtime 'org.slf4j:slf4j-nop:1.7.18' } run { if (project.hasProperty('args')) { args project.args.split(' ') } }
MNIST 学習用データを使った実行結果は以下の通りです。
実行結果
> gradle run -Pargs="train-images.idx3-ubyte train-labels.idx1-ubyte" ・・・ :run 3 06, 2016 10:22:09 午後 com.github.fommil.netlib.BLAS <clinit> 警告: Failed to load implementation from: com.github.fommil.netlib.NativeSystemBLAS 3 06, 2016 10:22:10 午後 com.github.fommil.jni.JniLoader liberalLoad 情報: successfully loaded ・・・\Temp\jniloader211398199777152626netlib-native_ref-win-x86_64.dll **************************************************************** WARNING: COULD NOT LOAD NATIVE SYSTEM BLAS ND4J performance WILL be reduced Please install native BLAS library such as OpenBLAS or IntelMKL See http://nd4j.org/getstarted.html#open for further details **************************************************************** size: 60000 ***** labels = [ 0.00, 0.00, 0.00, 0.00, 0.00, 1.00, 0.00, 0.00, 0.00, 0.00] ############ ################ ################ ########### ####### ## ##### #### #### ###### ###### ###### ##### #### ####### ######## ######### ########## ########## ########## ######## [ 0.00, ・・・, 3.00, 18.00, 18.00, 18.00, 126.00, 136.00, 175.00, 26.00, 166.00, 255.00, ・・・, 0.00] ---------- ***** labels = [ 1.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00] ##### ###### ######### ########### ########### ############ ######### ### ###### ### ####### ### #### ### ### ### #### ### #### ##### ### ##### ### #### ### #### ############# ########### ######### ####### [ 0.00, ・・・, 51.00, 159.00, 253.00, 159.00, 50.00, ・・・, 0.00]
今回は DataSet を作っているだけなので WARNING を気にする必要は特にありませんが、WARNING を消して正しい状態で実行するには OpenBlas 等の BLAS ライブラリをインストールしてから実行します。
Windows 環境であれば、http://nd4j.org/getstarted.html#open のリンクから ND4J_Win64_OpenBLAS-v0.2.14.zip をダウンロード・解凍し、環境変数 PATH へ設定してから実行するのが簡単だと思います。
また、JniLoader が TEMP ディレクトリへ netlib-native_system-win-x86_64.dll
をダウンロードするのを防止するには、この dll も環境変数 PATH へ設定した場所へ配置しておきます。 (dll は Maven の Central Repository からダウンロードできます ※)
※ ダウンロードしたファイル名では都合が悪いようなので、 バージョン番号を除いた netlib-native_system-win-x86_64.dll という ファイル名へ変更します
OpenBlas 設定後の実行結果
> set PATH=C:\ND4J_Win64_OpenBLAS-v0.2.14;%PATH% > gradle run -Pargs="train-images.idx3-ubyte train-labels.idx1-ubyte" ・・・ :run 3 06, 2016 11:31:22 午後 com.github.fommil.jni.JniLoader liberalLoad 情報: successfully loaded ・・・\netlib-native_system-win-x86_64.dll size: 60000 ***** labels = [ 0.00, 0.00, 0.00, 0.00, 0.00, 1.00, 0.00, 0.00, 0.00, 0.00] ・・・