ML(flux) of julia

この時期、恒例のLLやらRuby会議が開催されるはずだけど、どうなのかな? 田舎へ引っ込んでからは、その方面はさっぱりになってしまいました。(HAMの祭典、Ham fearも同様)

元気のあるJulia業界も、開発会議が開かれるようで、そのたたき台として、事前アンケートが 実施されたようです。

その集計結果が、公表されてました。

Julia User & Developer Survey 2019

90ケ国以上から1844人の回答があったそうです。

人気は、スピード、パフォーマンスが一番に来て、以下、使い易さ、オープンソースと続くようです。不人気な点は、パッケージが十分じゃない、最初のプロっとを生成するのに時間がかかり過ぎる。まだ広まっていない。なる程、オイラーと同じ感想だな。

試した理由は、未来の言語のようだってのにも同意します。pythonが世間を席巻してるけど、ひと昔前のBASICじゃん。ちっとも面白みが無いと思うぞ。

人気のあるパッケージは、遅いと評判のplot,Dataframe。うんうん激同意。そしてML(マシンラーニング)とかも人気。もう、客層がもろに出てますなあ。

juliaをバイナリーで手に入れて使ってる人が70%越え、17%の人がソースから自前でコンパイルしてるってさ。

ソースからコンパイルする連中は、GPUを組み込みたいとか、速い行列パッケージと差し替えたいとか、スピード命って事だな。 頑張って、世の中のデータを食い荒らしてください。

上で未来の言語って期待する発言が有ったけど、冷静に考えれば、何も未来ではないと思うぞ。 むしろ古代からある言語を再復活させたって事だろう。昔の言語は、括弧の鎧をまとっていて、 とてもいかめしかった。みんなこの無骨さに恐れをなして敬遠。

で、Lisp発祥の地であるMITの人は考えた。Lispを現代に蘇えらそう。括弧は嫌われるから、止そうね。でもLispの魂は絶対に抜くな。抜いちゃうとpythonみたいに普通の言語になっちまう。で、マクロは温存。マクロ恐怖症を払拭する為、ソフトな文法を用意してあげよう。

現代の要請は、ひたすら速い事。ならばコンパイル(しかも自動で)は必須。みんなの石を寄せ集めて、チームコンピューティングが簡単に出来るようにしようよ。

かくして、隠れLispの再登場となりました。現代の若者もその根底にある思想を嗅ぎ取って、続々と集まっています。

ASTにおける型の伝搬

なんだか大仰なタイトルだけど、恐れるなかれ。前回やった、flispが提供するパーサーで、型はどう扱われるかの確認だ。

> (julia-parse "f(x) = 2x + 1")
(= (call f x) (block (line 1 none) (call + (call * 2 x) 1)))

> (julia-parse "f(x::Float64) = 2x + 1")
(= (call f (:: x Float64)) (block (line 1 none)
                                  (call + (call * 2 x) 1)))
> (julia-parse "convert(Vector{Float64}, vec(x))")
(call convert (curly Vector Float64)
      (call vec x))

> (julia-parse "convert(Vector{Float64}, x")
error: incomplete: premature end of input
#0 (error ("incomplete: premature end of input"))
 :

convertってベクター版のキャスト用途にも使える便利な逸品です。で、curlyって、華麗に? カーリーブラケット?が正解みたい。

間違った引数を与えるとflispレベルでも怒られるんだけど、juliaレベルでは、

julia> convert(Vector{Float64}, x)
ERROR: MethodError: Cannot `convert` an object of type Int64 to an object of type Array{Float64,1}

こんな怒られ方をした。立場が違うと、着目する対象も変わるって事です。

julia update (1.1.1 -> 1.2.0)

新しい版が出てた。debian機に試しに入れてみた。本体はVerNo付きのファイルになってるんで、PATHを切替るだけってラクチン操作です。

debian:~$ julia
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.2.0 (2019-08-20)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

そしたら、1.1.1時代に入れておいたパッケージがことごとく無い事になってた。バージョンで 切り分けしてるのかな?

パッケージの集積地、.julia内をみたら、compiledとenvironmentsが更新されてた。この中に それぞれ、v1.1とv1.2が出来上がっていた。ここで分離してるのね。後のpackagesとかは共通だ。なる程ね。了解しました。一生懸命にpkgを登録し直したよ。現在どんなpkgが登録されてるかは、.julia/environments/v1.2/Project.tomlとかに記録されてるのね。pkg.statusした時に報告されるのは、この内容なんだ。

じゃ、以前にやったように、Gnuplotだけシステムに組み込んじゃえ。

julia> PackageCompiler.compile_incremental( :Gnuplot, force = false)
[ Info:   Gnuplot version: 5.2.0
[ Info: Registered package Gnuplot, using already given UUID: dc211083-a33a-5b79
-959f-2ff34033469d
  Updating registry at `~/.julia/registries/General`
  :
Activating environment at `~/.julia/packages/PackageCompiler/CJQcs/packages/Gnup
lot/Project.toml`
ERROR: LoadError: `Gnuplot` is a direct dependency, but does not appear in the m
anifest. If you intend `Gnuplot` to be a direct dependency, run `Pkg.resolve()`
to populate the manifest. Otherwise, remove `Gnuplot` with `Pkg.rm("Gnuplot")`.
Finally, run `Pkg.instantiate()` again.

あろう事か、こんなエラーを喰らってしまったぞ。1.1.1の時は問題無かったのにね。仰せに従って、resolve()をしてみたけど、やはり同様のエラー発生。本体の挙動が変わったのかな?

深く追求するのは止そう。そのうちに追従してくれるかも知れないから。まあ、新しいバージョンは多分に人柱の要素を含んでいるからね。1.2.1ぐらいが出るまで待とう、ほととぎす。

時々、1.2.0にパッケージが追従してくれていないか、確認出来るようにしておこう。いわゆるjuliaのバージョン切り替え器ね。バージョンを切り替えると、パッケージ側も自動的に切り替わる配慮がされてるから、本体だけ切り替えるだけでOKのはず。

今まで、本体dirに付いていたバージョン文字は落としておいて、PATHに /usr/local/julia/binを入れてた。これはこのままにしておく。

そして、/usr/local/julia-1.1.1 と、/usr/local/julia-1.2.0 と言う具合に、単に展開したものにする。切り替えは、リンク先を何処にするかで決める。(rootの権限が必要だけど、そして、ユーザーが一人と言う幸運な者のみに許される。)

debian:~$ cd /usr/local
debian:local$ sudo ln -s julia-1.1.1 julia

これで、古い版になる。新しい版にするなら、上記でjulia-1.2.0を選ぶ。超原始的方法也。

debian:local$ ls -ld julia*
lrwxrwxrwx 1 root staff   12 Aug 25 13:48 julia -> julia-1.1.1//
drwxr-xr-x 7 1337  1337 4096 May 16 15:13 julia-1.1.1/
drwxr-xr-x 7 1337  1337 4096 Aug 20 10:50 julia-1.2.0/

ML業界

んな事で、オイラーも乗り出してみるか。結論を先に書いておくと、お前の使ってるような貧乏人用のパソコンじゃ、遅くてストレスを抱え込むだけ、ですが。

2017年の冬の時代に、オイラーにもAIの3次ブームが来ていた。で、某大学の 人工知能の学習理論 のを見ていたんだった。

機械学習を1ヵ月で実践レベルにする #5 (線形回帰 Octave 実装)

K-meansクラスタリングと主成分分析

やる夫で学ぶ機械学習 - 序章 -

色々あるな。デフォは、使い易いoctaveあたりでしょうか。

MATLAB–Python–Julia cheatsheet

そういう貴方には、MLで人気の言語を横断出来るチートシートがうってつけ。これさえあれば怖くないぞ。

ML by julia

色々紹介されてるな。

The Best Machine Learning Libraries in Julia

FluxとかKnetがjulia純粋の物とな。後は移植組ってか移民だな。多様性を求めるなら移民組のScikitLeanあたりかな。なんとかフローは取り合えず敬遠しましょ。

Flux

一番人気品を選んでみるか。

flux

なんか、動物園と称して、実例が展示されてるな。でも、日本語の記事が欲しいという贅沢な希望を持ってるんだ。

Flux.jl で XOR を学習させてみる

短いコードで紹介してくれてる。

深層学習フレームワークFlux.jlを使ってみる その1:基本編

深層学習フレームワークFlux.jlを使ってみる その2:線形回帰編

深層学習フレームワークFlux.jlを使ってみる その3:ニューラルネットとバッチ正規化編

何となく、分かったような分からないような。。。

MNIST

ML界のHello worldと称される、手書き文字の認識をやってみる。先人に倣って。

Julia 1.0 + FluxでMNIST学習

(base) cent:ML$ time julia mn.jl
Start to train
┌ Warning: ADAM(params) is deprecated; use ADAM(η::Float64) instead
│   caller = ip:0x0
└ @ Core :-1
┌ Warning: train!(loss, data, opt) is deprecated; use train!(loss, params, data, opt) instead
│   caller = ip:0x0
└ @ Core :-1
[ Info: Epoch 1
0.059
 :
0.948 
action for each epoch
(total_loss, total_acc) = (2.3708556f0 (tracked), 0.09152464177160226)
[ Info: Epoch 2
0.000
0.059
 :
action for each epoch
(total_loss, total_acc) = (2.3708556f0 (tracked), 0.09152464177160226)
[ Info: Epoch 10
0.000
0.059
 :
action for each epoch
(total_loss, total_acc) = (2.3708556f0 (tracked), 0.09152464177160226)
0.000
Finished to train
Start to evaluate testset
loading pretrained model
prepare dataset
accuracy(X, Y) = 0.0916
Done
Start to evaluate testset
loading pretrained model
prepare dataset
accuracy(X, Y) = 0.0916
Done

real    1m5.386s
user    1m32.849s
sys     0m37.051s

最終的な正解率は、9.166%でした。市場に出回っているものに比べてやけに低いな? ともかく6万枚の画像を学習して、その中から適当に選んでテストしてってのが、1分そこそこ。よく見れば、多数の石を動員してるな。

別の解法も提示されてた。 Julia 1.0 + Flux でMNIST学習(CNN版) これって、畳み込みニューラルネットワークの事だな。残念ながら途中で落っこちてしまって、オイラーの今の技量ではチンプンカンプンなのさ。

以下、テスト対象の画の諸元。 基本的な所で、Xは60000枚の入力画像。1枚は28x28ピクセルのグレースケール画。

julia> @show size(X)  size(Y);
size(X) = (784, 60000)
size(Y) = (10, 60000)

期待値はラベルを元に変形して、Yに下記のように入ってる。デコーダ出力みたいな形式だな。

julia> Y
10×60000 Flux.OneHotMatrix{Array{Flux.OneHotVector,1}}:
 false   true  false  false  false  …  false  false  false  false  false
 false  false  false   true  false     false  false  false  false  false
 false  false  false  false  false     false  false  false  false  false
 false  false  false  false  false     false   true  false  false  false
 false  false   true  false  false     false  false  false  false  false
  true  false  false  false  false  …  false  false   true  false  false
 false  false  false  false  false     false  false  false   true  false
 false  false  false  false  false     false  false  false  false  false
 false  false  false  false  false      true  false  false  false   true
 false  false  false  false   true     false  false  false  false  false

Recognising MNIST Digits

これ参考になりそうで、やはり落ちるなあ。困ったものだ。

改造

上で参考にしたコードは、ML界のハロワ。これで終わっては恥ずかしいぞ。と言う事で、身近なやつでやってみる。

その前に、コード中に出て来るバッチとかエポックって知らない事なので、お勉強。

ディープラーニングにおけるバッチサイズ、イテレーション数、エポック数の決め方

バッチサイズとエポック

朝に晩に血圧と脈拍を測って記録してる。既に7年を超えるデータ(5541件)が集まっている。ぽんとデータが提示された時に、それは朝のデータか晩のデータか占ってもらおう。

ハロワMLに合わせたデータを用意して、後はちょこっとモデルを変えれば済みそう。

(base) cent:tmp$ tail -2 current.csv
19082005,134,72,50
19082021,123,64,58

こんなテストデータを用意して、

using DelimitedFiles
using Flux: onehotbatch

bld = readdlm("current.csv", ',', Int, '\n');
x   = bld[:, 2:4]
X   = x' / 160
y   = [div(rem(h,100),10) for h in bld[:,1]]
Y   = onehotbatch(y, 0:2)

実験コードを書いた。Xが入力でYがラベルと言うか教師データになる。CSVを読み込んで、入力になるのを選別。それを転置して、最高血圧らしき値の160で除して、一応正規化する。

教師データは、測定日時の時間の2桁目をyに得る。それをFlux得意の関数で変換。

julia> X
3×10 Array{Float64,2}:
 0.7875   0.74375  0.9      0.80625  …  0.8375  0.75    0.8375  0.76875
 0.44375  0.39375  0.49375  0.38125     0.4625  0.4     0.45    0.4
 0.33125  0.35     0.34375  0.39375     0.3375  0.3625  0.3125  0.3625

julia> Y
3×10 Flux.OneHotMatrix{Array{Flux.OneHotVector,1}}:
  true  false   true  false   true  false   true  false   true  false
 false  false  false  false  false  false  false  false  false  false
 false   true  false   true  false   true  false   true  false   true

朝は05時代、夜は21時に測定なんで、それをマッピングしてるだけだ。

(base) cent:BML$ julia bml.jl
Start to train
[ Info: Epoch 1
  :
[ Info: Epoch 10
Finished to train
Start to evaluate testset
accuracy(X, Y) = 0.5002707092582567
Done

これじゃ全部のデータが朝(あるいは晩)に測ったものでしょうと言ってるのと同じだな。何か決定的な間違いを冒しているのだろうか?

セカンドオピニオン

上にやってたの、随分と認識率が低かった。そこでセカンドオピニオンですよ。五里霧中なんで、色々なルートを試すのが鉄則。早速見つけたのが下記。

Machine Learning with Julia

コメントをチラ見してたら、オイラーのCPUだと5分かかったなんて恐ろしい事が書いてあったんで、試行枚数を下記のように1000枚限定にした。

 imgs = MNIST.images(1000);
 X = hcat(float.(reshape.(imgs, :))...)
 labels = MNIST.labels(1000)
 Y = onehotbatch(labels, 0:9)

そして、実行。

julia> @time include("smpl-mn.jl")
loss(X, Y) = 2.273553f0 (tracked)
loss(X, Y) = 0.27941138f0 (tracked)
acc X, Y0.9443acc X, Y 0.9443
 15.566915 seconds (1.15 M allocations: 14.460 GiB, 9.37% gc time)

今度は、それらしい答えが返ってきた。それにしても随分とメモリーを喰ってるな。 60000枚をやったら、134秒かかった。正解率は92.2%だったよ。

修正

セカンドオピニオンのやつと、最初に試したやつを見比べていたら、違いを発見。オピニオンに倣って、下記のように修正。

    optimizer = ADAM()

    @epochs epochs Flux.train!(loss, params(model), train_dataset, optimizer, cb = eval_callback)

そしたら、起動時の警告が消えた。

[ Info: Epoch 1
(total_loss, total_acc) = (0.17522457f0 (tracked), 0.9481654363873209)
[ Info: Epoch 2
(total_loss, total_acc) = (0.136959f0 (tracked), 0.9610155232305687)
[ Info: Epoch 3
(total_loss, total_acc) = (0.127683f0 (tracked), 0.9636750976986539)
[ Info: Epoch 4
(total_loss, total_acc) = (0.12070274f0 (tracked), 0.9668333423795049)
[ Info: Epoch 5
(total_loss, total_acc) = (0.1203792f0 (tracked), 0.9676135746851933)
[ Info: Epoch 6
(total_loss, total_acc) = (0.11963306f0 (tracked), 0.9702086951801998)
[ Info: Epoch 7
(total_loss, total_acc) = (0.124816164f0 (tracked), 0.9692113547546679)
[ Info: Epoch 8
(total_loss, total_acc) = (0.11925055f0 (tracked), 0.9734178245766392)
[ Info: Epoch 9
(total_loss, total_acc) = (0.12464694f0 (tracked), 0.9734313938341294)
[ Info: Epoch 10
(total_loss, total_acc) = (0.12430419f0 (tracked), 0.9719862679114198)
Finished to train
Start to evaluate testset
accuracy(X, Y) = 0.9716
Done
Start to evaluate testset2
accuracy(X, Y) = 0.9716
Done
 46.655722 seconds (27.69 M allocations: 23.030 GiB, 13.04% gc time)

Epoch毎にロスが減ってるし、精度も向上してた。そして最終的な精度も、満足な結果となった。違いが有った部分の際立つ所は、trainの引数が足りなかった事。型チェックに引っかかって落ちてもよさそうなのに走ってしまうって、どゆ事? 型安全に頼れない現実が有るわけね。

まてまて、警告と思って無視するお前が絶対的に認識不足。警告にちゃんと対処して、それでもおかしいようなら、堂々と文句を付けるべし!

これに味を占めたオイラーは早速、朝晩を占ってみた。変更したのは、Epoch数を増やしたのと 、モデルの中間層の値を決めてる所を hidden=6 にしたぐらいかな。

[ Info: Epoch 22
(total_loss, total_acc) = (0.30759943f0 (tracked), 0.8933301033591731)
[ Info: Epoch 23
(total_loss, total_acc) = (0.2992826f0 (tracked), 0.8968023255813953)
[ Info: Epoch 24
(total_loss, total_acc) = (0.29181564f0 (tracked), 0.8968023255813953)
[ Info: Epoch 25
(total_loss, total_acc) = (0.28507486f0 (tracked), 0.8985384366925064)
[ Info: Epoch 26
(total_loss, total_acc) = (0.27892983f0 (tracked), 0.9020106589147286)
[ Info: Epoch 27
(total_loss, total_acc) = (0.27335957f0 (tracked), 0.9037467700258398)
[ Info: Epoch 28
(total_loss, total_acc) = (0.26823938f0 (tracked), 0.9037467700258398)
[ Info: Epoch 29
(total_loss, total_acc) = (0.26352647f0 (tracked), 0.9037467700258398)
[ Info: Epoch 30
(total_loss, total_acc) = (0.25917217f0 (tracked), 0.9054828811369509)
Finished to train
Start to evaluate testset
accuracy(X, Y) = 0.8946038621187511
Done
Start to evaluate testset2
accuracy(X, Y) = 0.8946038621187511
Done
  1.242674 seconds (3.39 M allocations: 193.251 MiB, 3.48% gc time)

試行の度に精度は、0.7から0.95ぐらいまで、結構ばらつく。正に占いっぽい。これじゃ、某リクなんとかみたいに金は取れないな。

そして更に占いは続きます。セカンドオピニオンに組み込んだ結果。

(base) cent:BML$ julia mine.jl
[ Info: Epoch 1
loss(X, Y) = 1.2215866f0 (tracked)
[ Info: Epoch 2
loss(X, Y) = 0.97985697f0 (tracked)
[ Info: Epoch 3
loss(X, Y) = 0.8772018f0 (tracked)
[ Info: Epoch 4
loss(X, Y) = 0.79091394f0 (tracked)
[ Info: Epoch 5
loss(X, Y) = 0.72251934f0 (tracked)
train acc X, Y 0.8041958041958042
test  acc X, Y 0.8214607754733995
(base) cent:BML$ cat mine.jl
 using Flux, DelimitedFiles, Statistics
 using Flux: onehotbatch, onecold, crossentropy, throttle, @epochs
 using Base.Iterators: repeated
 using Random

bld = readdlm("current.csv", ',', Int, '\n');
x   = bld[:, 2:4]
aX   = x' / 160
y   = [div(rem(h,100),10) for h in bld[:,1]]
aY   = onehotbatch(y, 0:2)

    shuffled_indices = shuffle(1:size(aY)[2])
    divide_idx = round(Int,0.8*length(shuffled_indices))
    train_indices = shuffled_indices[1:divide_idx]
    test_indices = shuffled_indices[divide_idx:end]
    X = aX[:,train_indices]
    Y = aY[:,train_indices]
    tX = aX[:,test_indices]
    tY = aY[:,test_indices]

 m = Chain(
        Dense(3, 6, relu),
        Dense(6, 3), softmax )
 loss(x, y) = crossentropy(m(x), (y))
 accuracy(x, y) = mean(onecold(m(x)) .== onecold(y))
 dataset = repeated((X, Y), 100)
 evalcb = () -> @show(loss(X, Y))
 opt = ADAM()
@epochs 5  Flux.train!(loss, params(m), dataset, opt, cb=throttle(evalcb, 10))

println("train acc X, Y ", accuracy(X, Y))
println("test  acc X, Y ", accuracy(tX, tY))

後日の自分の為のメモ。aX,aY は、読み込んだ全てのデータを、占い器の入力と出力に分離。 ちょっとインデントしてる部分は、トレーニング用とテスト用に分割。

トレーニング用の組(X,Y)を使ってトレーニングし、出来栄えを表示。続いてテストデータで最終占い。

なんか、ディープインパクトの子供だからきっと強いだろうってのと、同じ心理っぽいぞ。 ただ、こちらは数式を振り回して、正当化してるだけ、かな?

そういう輩はMLなんて勉強する必要無し。

それから、関数にしないでバラしてあると、こういう風に変数を容易に観察できるのね。

julia> varinfo()
 :
loss                 0 bytes typeof(loss)          
m                  960 bytes Chain{Tuple{Dense{typeof(relu),TrackedArray{…,Array{Float32,2}},TrackedArray{…,Array{Float32,1}}},Dense{typeof(identity),TrackedArray{…,Array{Float32,2}},TrackedArray{…,Array{Float32,1}}},typeof(softmax)}}
opt                2.016 KiB ADAM                                         
shuffled_indices  43.328 KiB 5541-element Array{Int64,1}                     
tX                26.031 KiB 3×1109 Array{Float64,2}           
tY                 8.719 KiB 3×1109 Flux.OneHotMatrix{Array{Flux.OneHotVector,1}}    
test_indices       8.703 KiB 1109-element Array{Int64,1}                
train_indices     34.672 KiB 4433-element Array{Int64,1}        
x                129.906 KiB 5541×3 Array{Int64,2}                    
y                 43.328 KiB 5541-element Array{Int64,1}

Regexで指定も出来るとな。

julia> varinfo(r"(X|Y)")
name        size summary
–––– ––––––––––– ––––––––––––––––––––––––––––––––––––––––––––––––––––
X    103.938 KiB 3×4433 Array{Float64,2}
Y     34.688 KiB 3×4433 Flux.OneHotMatrix{Array{Flux.OneHotVector,1}}
aX   129.906 KiB 3×5541 Array{Float64,2}
aY    43.344 KiB 3×5541 Flux.OneHotMatrix{Array{Flux.OneHotVector,1}}
tX    26.031 KiB 3×1109 Array{Float64,2}
tY     8.719 KiB 3×1109 Flux.OneHotMatrix{Array{Flux.OneHotVector,1}}

etc

セキュリティ・キャンプ全国大会2019 解析トラック