grbl1.1+Arduino CNCシールドV3.5+bCNCを使用中。
BluetoothモジュールおよびbCNCのPendant機能でスマホからもワイヤレス操作可能。
その他、電子工作・プログラミング、機械学習などもやっています。
MacとUbuntuを使用。

CNCマシン全般について:
国内レーザー加工機と中国製レーザー加工機の比較
中国製レーザーダイオードについて
CNCミリングマシンとCNCルーターマシンいろいろ
その他:
利用例や付加機能など:
CNCルーター関係:



*CNCマシンの制作記録は2016/04/10〜の投稿に書いてあります。


ラベル 機械学習 の投稿を表示しています。 すべての投稿を表示
ラベル 機械学習 の投稿を表示しています。 すべての投稿を表示

2018年11月20日火曜日

Kaggle Digital Recognizer(MNIST): Keras, fit_generator() + hyperopt

Kaggle Digital Recognizer(MNIST)の続きです。前回から少しだけ内容を変えてみたらベストスコアがでました。
改良点は以下です。

fit_generator():
前回は、通常の訓練model.fit()を使った後にData Augmentationとしてmodel.fit_generator()を追加して二段階で訓練しましたが、今回は最初からfit_generator()だけで訓練してみることにしました。

BatchNormalization:
CNNに関しては前回よりも層を少なくして、conv2dの後に必ずBatchNormalization()を入れ、プーリング層(学習なし)を使わずにstrides=2のconv2d()(学習あり)で1/2にダウンサンプリングすることにしました。

Hyperopt:
Hyperoptに関しては前回同様Dropout率だけを最適化しています(合計3箇所)。探索回数はとりあえず10回。

スコア:
結果はこれまでのベストスコアである0.99771(Top 5%)まで向上しました。
この辺りまで来るとスコアを0.001上げるのはかなり至難の技で、正直0.997以上になるとは期待していませんでした。しかし予想以上に満足できる結果が得られたので、Digital Decognizer(MNIST)に関してはひと段落ついたという感じです。

ただし、調べれば調べるほど興味深い項目が登場してきて、今後試してみたいのは:
・他のMNISTデータセットで今回のモデルの精度を確かめてみる(KaggleのMNISTデータセットにオーバーフィッティングしていないかどうかの検証)。
・今回はHyperoptによってDropout率を自動的に決定させましたが、Dropoutを一般化したDropconnectというのもあるらしく、それを使うとどうなるか(Keras Dropconnect Implementation)?

Kaggleで勝つデータ分析の技術
門脇 大輔 阪田 隆司 保坂 桂佑 平松 雄司
技術評論社
売り上げランキング: 363


追記:
その後、既存のkeras.datasets.mnistのデータセット(60000+10000)で検証してみると0.997以上の正解率となりました。おそらくKaggleのMNISTデータも多数含まれているので似たような結果となったのだと思います(しかし偶然KaggleのMNISTデータだけにオーバーフィットしすぎているというわけでもなさそう)。

2018年11月16日金曜日

Kaggle Digital Recognizer(MNIST): Hyperopt + Data Augmentation

引き続きKaggle Digital Recognaizer(MNIST)のスコア向上のため、今回はHyperoptData Augmentationを組み合わせてみました。
結果として、これまで0.995前後(Top 18%)のスコアがでていましたが、今回の方法で0.99671(Top 9%)まで向上しました。それでも28000個あるデータのうち92個が間違っているということになります。


今回の方法:

・畳み込み層を増やしてもう少し特徴量検出できるようにする
・あまり自動調節させるハイパーパラメータは増やさない
・バッチサイズやデータ分割などは一般的な値にしておく
・Dropout率でモデル全体の精度を調整

要は、Hyperoptによるハイパーパラメータ最適化はDropout率(合計5個)だけに絞り、その他は固定。
探索回数は20回(GTX1060で約2時間)。

前回までと異なるのは:
・ダブルの畳み込み層をもう一式追加
・Dense層出力ユニット数の増加
・最後にData Augmentationでの訓練を追加
ということになります。

Data Augmantation(訓練データの変形加工:水増し)するためにKerasのImageDataGeneratorを使用しました。
ImageDataGeneratorを使うには元データのshapeを(-1, 28, 28, 1)にしておく必要があるようです。これまではCNNモデル入力層でkeras.layers.Reshape()を使って(784, )から(-1, 28, 28, 1)に変換していましたが、ImageDataGeneratorの入力次元数とモデルの入力次元数が異なるためエラーがでてしまい、モデルに入力する前に(-1, 28, 28, 1)へ変換することにしました。

流れとしては:
・訓練画像を元にHyperoptで最適化
・最適化されたモデルをData Augmentationで追加訓練
という二段階の訓練です。
Data Augmentationの訓練は結構すぐに収束してしまいましたが、その訓練の差なのかほんのわずか向上しました。


まとめ:
個人的には、これまでスコア0.995が壁になっていて、手動でいろいろ試してみたけれどもなかなか超えられませんでした。今回のスコアは期待していた以上に良かったので、MNISTに関してはもうこの辺で充分かと思いますが、あと試してみたいのは以下。
・最初からImageDataGeneratorで訓練
・交差検証(KFold

基本的なMNIST分類問題ですが、こうやってスコア向上を目標に試してみると、いろんなテクニックが見つかりかなり勉強になります。基本的なCNNアルゴリズムだけでなく、その他の方法も組み合わせることでわずかながらでも向上するということが分かったのもよかったです。

関連:
Kaggle Digital Decognizer(MNIST): Keras, fit_generator() + hyperopt


直感 Deep Learning ―Python×Kerasでアイデアを形にするレシピ
Posted with Amakuri at 2018.12.21
Antonio Gulli, Sujit Pal
オライリージャパン
販売価格 ¥3,672

2018年11月14日水曜日

GPyOpt: Digital Recognizer(MNIST) CNN Keras ハイパーパラメータ最適化

引き続きハイパーパラメータ最適化として、今回はGPyOptを使ってみました。これまでHyperasHyperoptを試して見ましたが、ベイズ最適化でも採用しているアルゴリズムが微妙に違うようなので試してみたという感じです。
個人的にはHyperoptが一番使いやすく感じましたが、GPyOptは以前scikit-learnで試したベイス最適化に近いアルゴリズムだったのでもう少し理解を深めてみようかと。
まだ手探り段階なので、使い方に関しては後から追記するかもしれません。


使い方:

ハイパーパラメータの設定:
まずはMNISTモデルにおけるハイパーパラメータの設定からです。今回はやや少なめで。

最適化するハイパーパラメータ:
・各層のドロップアウト率:連続値
・Dense層出力ユニット数:離散値
・validation_splitの比率:連続値

GPyOptではハイパーパラメータを以下のようなフォーマットで書きます。
params = [
    {'name': 'Dropout_0',        'type': 'continuous',  'domain': (0.0, 0.5)},
    {'name': 'Dropout_1',        'type': 'continuous',  'domain': (0.0, 0.5)},
    {'name': 'Dropout_2',        'type': 'continuous',  'domain': (0.0, 0.5)},
    {'name': 'Dropout_3',        'type': 'continuous',  'domain': (0.0, 0.5)},
    {'name': 'Dense_0',          'type': 'discrete',    'domain': (128, 256, 512)},
    {'name': 'Dense_1',          'type': 'discrete',    'domain': (64,128, 256)},
    {'name': 'validation_split', 'type': 'continuous',  'domain': (0.1, 0.3)}
]

リスト化されたディクショナリーで(ここを参考に)、

・'name' : パラメータ名
・'type' : 'continuout'(連続値)、'discrete'(離散値)、'categorical'(分類値)
・'domain' : 適用範囲または選択肢を()で括る

となるようです。


CNNモデルの構築:
次にモデルを構築します。前回同様MNIST分類用のCNNを使います。このモデルからはベイズ最適化するための評価値となるlossかaccが求められればいいのですが、

・loss
・acc
・model
・history

の4種類を戻り値にしておきました。
model.fit()させてEarlyStoppingで打ち切りになった最後のval_lossとval_accの値を参照しています。
    loss = hist.history['val_loss'][-1]
    acc = hist.history['val_acc'][-1]
modelやhistoryは不要ですが、後から参照するかもしれないので一応入れておきました(使うかどうかは分からない)。
return loss, acc, model, hist

上記パイパーパラメータに対応する変数部分には、x[:, 0]などと引数にインデックス番号をつけるようですが、どれが何番目かはわかりにくいのでハイパーパラメータの'name'から参照できる関数をつくってみました。
model.add(Dropout(Param('Dropout_0'), seed=seed))
このように書き込めばx[:, 0]へ自動変換してくれます。Hyperoptなどでもディクショナリーのキーを使っていたので、このほうが個人的には使いやすいかと(リスト内容を変えた場合にインデックス番号だと、他の番号も変わってしまうのが面倒なので)。
注意点として、最初に書いたハイパーパラメータはリストであるのに対して、この変数は2次元のndarrayに変換されてから代入されるようです。この変換関数は以下(cnn_model関数内)。
    def Param(p_name):
        p_index = [p['name'] for p in params].index(p_name)
        p_type = params[p_index]['type']
        
        if type(x) is np.ndarray:
            if p_type == 'continuous':
                return float(x[:, p_index])
            else:
                return int(x[:, p_index])
        else: # list
            if p_type == 'continuous':
                return float(params[p_index]['domain'])
            else:
                return int(params[p_index]['domain'])
後で最適化されたハイパーパラメータリストを直接渡せるようにしてあります。引数がndarrayならx[:,0]のような2次元ndarray、listならlist内のスカラー値へ変換後代入。また今回の場合、離散値はすべて整数だったのでintかfloatかも振り分けています。


フィッティング関数:
上記CNNモデルを以後のベイズ最適化関数GPyOpt.methods.BayesianOptimization()に直接渡してもいいのですが、CNNモデルからは4種類の値を出力することにしたので、このf(x)関数を間にはさんで必要な評価値だけを渡せるようにしました。今回はaccを評価値として渡すことにし、最小化するためにマイナス反転して-accにしています。
前述のように引数のxは二次元のnumpy.ndarrayになるようです。今回は7種類のハイパーパラメータがあるので、x.shapeは(1,7)になります。最初に設定したハイパーパラメータはリストでありndarrayではないので、この辺をいじる場合は変換するなどの工夫が必要です(このサンプルを参照)。
実際は、
def f(x):
    x = np.atleast_2d(x)
    fs = np.zeros((x.shape[0],1))
    for i in range(x.shape[0]):
        loss, acc, model, hist = cnn_model(x)
        fs[i] += np.log(acc)*(-1)
    return fs
このように書いたほうがいいのかもしれませんが、戻り値は1次元のndarrayだったので、今回は省略して以下のようにしました。対数変換したほうがいいのかもしれませんが効果の違いは検証していません。
def f(x):
    loss, acc, model, hist = cnn_model(x)
    return -acc

ベイズ最適化関数:
GPyOpt.methods.BayesianOptimization()に先程のf(x)関数とハイパーパラメータリストparamsを渡し、その他初期探索値や獲得関数などを決めます。獲得関数はデフォルトではEIになっていますが'EI_MCMC'を選んでみました。'EI_MCMC'を選択する場合は、model_typeで'GP_MCMC'を選んでおかなければいけないようです。
initial_design_numdataは20に設定しましたが、これはどのくらいがいいのかは不明(デフォルト:5)。探索する前のランダムな開始点の数なのかもしれませんが、今回の7次元に対してどのくらいが適当なのか?探索点は徐々に追加されながらフィッティングしていくと思うのでデフォルトの5でもいいのかもしれません。入れた回数だけループするようです(20回で約1時間)。
こまかな設定がいくつかありますが、まだ使いながら試している段階です。

次に、run_optimization(max_iter=50)で最適化が始まります。イテレーションを50回に設定しました。7種類のハイパーパラメータに対してどのくらいが適当なのかはまだ不明(ハイパーハイパーパラメータ)。50回で約4時間かかりました。
ループが終了すれば最適なハイパーパラメータが見つかったことになります。設定した回数より早く終わることもあります。


最適化されたハイパーパラメータの取得:
以下で結果を取得することができます。
x_best = opt.x_opt
print([i for i in x_best])

y_best = opt.fx_opt
print(y_best)
そうすると、
[0.1732254530746627, 0.39555160207057505, 0.14877909656106353, 0.07323704794308367, 128.0, 128.0, 0.1471216716379693]
-0.9945388349514563
と値が出てきて、最初のリストが最適化された各ハイパーパラメータ。
下の値はそのときのロス値。今回はaccをマイナス反転してあるのでaccの値と同じ。精度0.994以上でているのでまあまあの結果です。


最適化されたハイパーパラメータをモデルに適用:
上記結果と同時にベストモデルやベストウェイトを直接取り出したいのですが、そのような方法がGPyOptにはないようなので、最適化されたハイパーパラメータをCNNモデルに入れ直して再度訓練させてみました。
一応、上記ハイパーパラメータリストを元々のディクショナリー型のリストへ移し替えてからCNNモデルに渡しています。CNNモデルの引数がlistの場合はスカラー値を各変数に代入するような関数にしています。
CNNモデルはEarlyStopping機能をつけているので15ループで収束してくれました(4分25秒)。
このモデルを利用して提出用データを予測します。


まとめと結果(スコア):
最終的にスコアは0.99457でした。まあまあいい結果です(それでも手動調整のベストスコアである0.99528には達していない)。約6時間でこの結果ですが、もっと回せば向上するかはわからないです。これ以上のスコアを出すには、data augmentationでデータを水増しするなど必要かもしれません。
GpyOptはHyperoptに比べるとやや使いにくいという印象でした(サンプルも少ない)。しかしやりたいことに応じて使いやすく改造すればいいのかもしれません。もともとのアルゴリズム自体は優れていると思うので、いくつかを同時に試して結果的にいい方を選ぶ感じでしょうか。時間的にもHyperoptのほうが速いかもしれませんが、どのライブラリであっても数時間はかかるので時間よりも精度がでるほうがいいと思います(仕事で使っているわけではないので)。
このほか気になるライブラリとして、SkoptKoptPyBOSpearMintなどありますが、とりあえずはもう十分かと。

これまでは機械学習理論やアルゴリズムの種類を覚えていくことが面白かったのですが、Kaggleをきっかけにスコア(精度)を少しでもあげようとすることにも興味を持てたのはよかったです。実際使ってみて、その結果から次にどうすればいいのかという具体的な疑問が次のモチベーションになるので、より理解も深まりつつ面白くなっていく感じです。

追記:
その後、4つのDropout率だけをハイパーパラメータとして最適化した結果スコア:0.99557まで向上(これまでのベストスコアは0.99524)。
その他のハイパーパラメータは以下のように固定。
validation data:test_size=0.15
Dense_0 output units: 256
Dense_1 output units: 128
batch_size=32

そして最適化においては以下の探索回数に設定。
initial_design_numdata=30(2h 29mins)
max_iter=100(stop at 52: 7h 47mins)
max_iterは最大100回に設定しましたが途中52回で収束し停止しました。
合計で10時間30分(GTX1060で)。

関連:
Kaggle Digital Recognizer(MNIST): Hyperopt + Data Augmentation
Kaggle Digital Decognizer(MNIST): Keras, fit_generator() + hyperopt



機械学習スタートアップシリーズ ベイズ推論による機械学習入門 (KS情報科学専門書)
Posted with Amakuri at 2018.12.21
須山 敦志
講談社
販売価格 ¥3,024(2018年12月21日20時40分時点の価格)

2018年11月10日土曜日

Hyperopt:Digital Recognizer(MNIST)のハイパーパラメータの最適化



今回は、HyperoptでMNISTのハイパーパラメータの最適化を行いました。Hyperoptは前回試したHyperasの元となっているライブラリです。Hyperasはシンプルに使える反面やや扱いにくい部分(慣れていないだけかもしれませんが)もあったため、大元のHyperoptで書き直してみました。

データは前回同様KaggleのDigital Recognizer(MNIST)で、最適化するハイパーパラメータは以下です。

・Dropout率
・Dense層出力ユニット数
・batch_size数
・validation_splitの比率

Hyperoptの使い方:

ハイパーパラメータのディクショナリー設定:
まずハイパーパラメータのディクショナリーを以下のようなフォーマットで用意します。

params = {
    'Dense_0':          hp.choice('Dense_0', [128, 256, 512]),
    'Dense_1':          hp.choice('Dense_1', [64, 128, 256]),
    'Dropout_0':        hp.uniform('Dropout_0', 0.0, 1.0),
    'Dropout_1':        hp.uniform('Dropout_1', 0.0, 1.0),
    'Dropout_2':        hp.uniform('Dropout_2', 0.0, 1.0),
    'Dropout_3':        hp.uniform('Dropout_3', 0.0, 1.0),
    'batch_size':       hp.choice('batch_size', [16, 32, 64]),
    'validation_split': hp.uniform('validation_split', 0.1, 0.3)
}

離散値の場合はhp.choice()、連続値の場合はhp.uniform()を使います。このあたりはHyperasと同じような感じです。このほか乱数用のhp.randint()や正規分布用のhp.normal()などいくつかあります(ここに書いてあります)。


ハイパーパラメータの挿入と戻り値の設定:
次はモデルの構築です。MNISTデータの前処理をしておいてから、CNNを用いてMNIST分類モデルを構築します。そして最適化したい変数の部分(以下の場合:CNN層内のドロップアウト率)に、

model.add(Dropout(params['Dropout_0'], seed=seed))

という感じで挿入しておきます。
model.compile()、model.fit()したあと model.evaluate()でlossとaccを求めて、その値を戻り値とします。サンプルなどではlossのかわりにaccを評価値として次のfmin()関数に渡していますが、どちらがいいのかは不明。または、hist=fit()のhistoryからhist.history['val_loss'][-1]で最後のロス値を取得する方法でもいいのかもしれません(あるいは'val_acc')。尚、accを渡す場合はマイナスをかけて最大値を最小値に反転させておく必要があります。
またモデルなどその他の値やオブジェクトを渡すときはディクショナリーにするといいようです。ディクショナリーにする場合は、次のfmin()関数に値を渡すために'loss'と'status'のキーが最低含まれていないといけないようです。今回は追加でモデルも含めたので以下のような戻り値としました。あとでベストmodelを参照する場合は追加しておくといいと思います。

return {'loss': -acc, 'status': STATUS_OK, 'model': model}


最適化:
最後に、best=fmin()で最適なパラメータを見つけます。fmin()へモデルとハイパーパラメータディクショナリーを渡し、探索回数などを指定して最適化します。探索回数は多いほどいいと思いますが、それなりに時間はかかります(数時間とか)。
trialsには探索結果の記録が保持されるので後で参照します。

trials = Trials()
best = fmin(fn=cnn_model, 
            space=params, 
            algo=tpe.suggest, 
            max_evals=20, 
            trials=trials,
            verbose=1,
            rstate=np.random.RandomState(seed))

fn:CNNモデル(前述の'loss', 'status', 'model'が戻り値)
space:パイパーパラメータのディクショナリー
algo:使用するアルゴリズム(TPEなのでこのまま)
max_evals:探索回数
trials:探索記録保持先
verbose:ログ出力
rstate:乱数固定


結果参照:
best=fmin()からは最適化されたパラメータのディクショナリーが出力されます。そのままだと、hp.choice()の場合リストのインデックス番号が返されるので、

space_eval(params, best)

で実際の値に変換出力してくれます(以下)。

{'Dense_0': 512,
 'Dense_1': 256,
 'Dropout_0': 0.19796353174591008,
 'Dropout_1': 0.30328292011950164,
 'Dropout_2': 0.7005074297830172,
 'Dropout_3': 0.3974900176858912,
 'batch_size': 64,
 'validation_split': 0.16617354953831512}

あらかじめtrials=Trials()と定義しておけば、trialsの中に全ての情報が記録されるので、必要に応じて値やモデルを参照することができます。

trials.best_trial['result']

で以下が出力されます(複数回探索した中でのベストの結果)。lossはaccをマイナス反転したものなのでaccのこと、modelはそのときのベストモデル、statusは処理が無事完了なら'ok'。

{'loss': -0.9935714285714285,
 'model': <keras.engine.sequential.Sequential at 0x7fc5c3da87f0>,
 'status': 'ok'}

ベストモデルは、

best_model = trials.best_trial['result']['model']

によって参照することができるので、このモデルを使ってpredict()することができます。

まとめ:
前回のHyperasよりも使い勝手はよさそうです。それほど面倒なコーディングをすることもないので、個人的にはHyperoptのほうが便利かと。要は、パイパーパラメータディクショナリーとモデルを最適化関数に入れれば答えがでてくるということです。
詳しいドキュメントがないので(ここくらい)、細かな使い方はわからないのですが(ソースを読み解くしかないかも)、いろいろ応用できそうです。
今回はmax_evals=20で20回探索(NVIDIA GTX1060で49分)した結果、スコアは0.99257でした。まあまあの結果でしたが、実際100回以上(数時間)は回したほうがいいのかもしれません。

こちらのサイトでは様々なベイズ最適化ライブラリーを比較しており、時間的にはHyperoptが一番速いようです。10次元以下の最適化であればPyBOが優れているようで、それ以上の次元ではどれも遅くなるようです。また20次元や40次元になるとほとんどのライブラリが最適化できなくなるようで、Spearmintが20次元でも機能していたようです。

追記1:
その後100回(約6時間)回してみましたがスコアは0.99185という結果。validationセットでのスコアは0.995だったので向上しましたが、オーバーフィッティング気味だったったのか結果的にはいまいち。いずれにせよ0.992前後が限界という感じ。CNNの層を少し改造するか、kerasのImageDataGeneratorでデータ水増しした方がいいのかもしれません。

追記2:
validation_splitを0.2に固定して、Dropout率とDense層出力ユニット数だけをハイパーパラメータとして10回ほど探索すると0.99442まで向上しました。普通に考えてvalidation dataは少ないほどval_accは上がってしまうと思うので固定にしたほうがよさそうです。

関連:
GPyOpt: Digital Recognizer(MNIST) CNN Keras ハイパーパラメータ最適化
Kaggle Digital Recognizer(MNIST): Hyperopt + Data Augmentation
Kaggle Digital Decognizer(MNIST): Keras, fit_generator() + hyperopt


2018年11月9日金曜日

HyperasでDigital Recognizer(MNIST)を試す


前回AutokerasでMNISTを自動化分類してみましたが、今回は特定のハイパーパラメータを最適化するHyperasHyperoptのkerasラッパー)を試してみました。最適化にはいろいろ種類があって、HyperasはTree-structured Parzen Estimators (TPE)というガウス過程の欠点を補った方法のようです。グリッドサーチよりも計算量が少なく、ベイズ最適化のように探索していくようですが、微妙に計算方法が異なるようです。


使い方:
Hyperasの使い方は非常に簡単で、
例えばドロップアウト率を0から1の範囲で最適化したい場合は、

Dropout({{uniform(0, 1)}})

複数の選択肢がある場合は、

optimizer={{choice(['adam', 'rmsprop', 'SGD'])}}

を通常の変数部分に入れて行くだけです。ディクショナリーを用意する必要もないので楽です。

ちなみに、今回最適化したハイパーパラメータは以下。
・Dropout率
・Dense層出力ユニット数
・batch_size数
・validation_splitの比率


ルール:
ただし少しだけルールがあるようです(このあたりがやや使いにくいかも)。
HyperasはHyperoptのラッパーで、一旦コーディングした内容をそのままHyperoptへ翻訳変換してしまうようです。そのためまずdata関数とmodel関数を作り(カプセル化するフォーマット)、その後dataとmodelをoptim.minimize()へ渡してモデル探索します。基本的には以下のようなフォーマット。

def data():
  データの読み込み
  データの前処理
  return X_train, Y_train, X_test, Y_test

def model(X_train, Y_train, X_test, Y_test):
  model = Sequential()
  ...
  model.compile()
  model.fit()
  ...
  return{'loss':-acc, 'status': STATUS_OK, 'model': model}

best_run, best_model = optim.minimize(model, data, ...)


それから、data関数とmodel関数外で定義したグローバル変数はdata関数とmodel関数のスコープに届かないようです(importしたモジュールなどは問題ない)。よってグローバル変数などは、新たにdata関数とmodel関数内に定義しないといけなさそうです。globalを使えばいいのかも。
それと引数や戻り値の変数名も一致させておかないとエラーがでました。
また、model関数内で{{uniform()}}や{{choice()}}を含んだ行を#でコメントアウトしても、Hyperoptへ変換される際には最適化する変数として読み込まれてしまうので消去する必要があります。

最初なぜエラーがでるのかわかりませんでしたが、Hyperoptへそのまま翻訳変換するためそういうルールのようです。そういうことから、やや複雑なことをするならHyperoptを使ったほうがいいかもしれません。

ということで、このルールに戸惑いましたが何とかコーディングしたものが以下。

最適化されたハイパーパラメータはbest_runによって出力されます。

{'Dense': 64,
 'Dense_1': 64,
 'Dropout': 0.21099660541107612,
 'Dropout_1': 0.29327102196615873,
 'Dropout_2': 0.7302305870589935,
 'Dropout_3': 0.258985915829989,
 'batch_size': 32,
 'validation_split': 0.10388179991112252}

今回のハイパーパラメータはこんな感じ。
デフォルトだとchoice型のハイパーパラメータはリストのindex番号として出力されるので(例えば上記の'Dense': 64がデフォルトだと'Dense': 0と表示される)、optim.minimize()内に

eval_space=True

を加えておけば、リスト内の実際の値として出力されます。
また今回は、max_evals=10としました。これは探索回数のことだと思うのですが、最低どのくらいが必要かは試してみないとわかりません(ハイパーパラメータのハイパーパラメータ)。探索回数が多いほど正確に予測できると思いますが、その分時間もかかってしまいます。以前ベイズ最適化を試した時は15回くらいの探索でブラックボックス関数にほぼ近似していましたが、Hyperas(Hyperopt)のTPEアルゴリズムにおいてはどうなのか?



まとめ:
まだ使い方に慣れていないためか、スコアは0.98371(max_evals=10)でいまいちでした。何回かやり直してみたので、それなりに時間がかかってしまいました(エラーや失敗も含めて)。そうなると、まだまだ手動調節(スコア:099528)のほうがMNISTの場合であれば早いという感じです。
ただ、手っ取り早く試すにはHyperasはいいのかもしれません。少し使い勝手が悪いので、流れがわかればHyperoptへ移行してもいいと思います。
このほかKeras用の最適化ライブラリでTalosというものも試してみました。Talosもシンプルで簡単ですが、グリッドサーチやランダムサーチをベースにしているのか、最適化範囲や選択肢が増えると探索にとてつもなく時間がかかってしまったので一旦中止。
次は、Hyperoptで実装しなおすか、GpyOptを試してみようかと思います。

追記その1:
その後、max_evals=20で探索すると0.99200までスコアがあがりました。

追記その2:
さらに、max_evals=100まで試すと0.99271まで向上。
以下が最適化されたハイパーパラメータ:

{'Dense': 256,
 'Dense_1': 128,
 'Dropout': 0.01772328721174527,
 'Dropout_1': 0.4978692970747428,
 'Dropout_2': 0.13740853439432676,
 'Dropout_3': 0.01305834864014449,
 'batch_size': 32,
 'validation_split': 0.12455270876039955}

結構まっとうな値です。たまにDropout:0.7以上がでるときもありますが、今回はどれも0.5以下。
Denseの出力ユニット数はそれぞれ選択肢の最大値256と128なので、もう一段階512と256へ上げてもいいかもしれません。
GPU:GTX1060(6GB)で数時間回せばこのような結果が出せるのでそれなりに使えそうです。探索回数であるmax_evalsは最低でも100、もしくは500や1000で1日中回すのがいいかもしれません。

2018年10月24日水曜日

Kaggle:その2(Titanic、Mnistなど)

前回のTitanicの続きです。
いろいろとハイパーパラメータを調節して目標としていたスコア:0.80(上位8%)を何とか超えることができましたが、どうも乱数固定が不安定で偶然出てきた結果という感じ。たぶんCUDAとともにインストールしたcuDNNのほうで乱数の固定ができていないような。まあ、それでもできるだけ固定することでわずかな誤差ですむようになってきました。以下が現在の乱数固定方法。このほかKerasのDense層のkernel_initializer、Dropoutにおいてもseedを固定しています。


スコアをあげるための決定的な解決策はまだ出てきていないのですが、今回はKerasのEarlyStopping機能(訓練ループを自動的に止める)を使ってみました。

EarlyStoppingだけでなく、自動的に学習率を下げるReduceLROnPlateauとModelCheckpointでベストなウェイトを保存させて、その結果から予測させています。要はできるだけ自動化という方向で。

提出結果のスコアを比較していくと、隠れ層を1層にした非常にシンプルなニューラルネットのほうがいい結果が出ました。Titanicの場合はデータ数が少ないので(訓練+テスト:1309サンプル)、優れた予測モデルを構築しにくいのかもしれません。乱数の違いでもかなり結果が変わってしまうのでその辺が難しそう。

基本的にはデータをみながらの工夫はせずに、数値化したデータをそのままニューラルネットに渡して自動的に解決する方法にしています。

それぞれのデータに関しては:

Pclass:そのまま
Name:含まれるTitle(Mr/Mrsなど)を抽出し数値化(0〜17)、正規化
Sex:数値化、male:0, female:1
Age:欠損値あり(後で穴埋め)、正規化
SibSp:正規化
Parch:正規化
Ticket:削除
Fare:欠損値あり(後で穴埋め)、正規化
Cabin:欠損値も含め数値化:nan:0, C:1, E:2, G:3, D:4, A:5, B:6, F:7, T:8に変換
Embarked:欠損値あり(後で穴埋め)、数値化:S:0, C:1, Q:2

何度かスコア:0.80を超えた(上位8%)のですが、あまり当てにならないので、再度仕切り直しで以下のコード(スコア:0.78947)。

表示されない場合はこちら

Digit Recognizer(Mnist):
Titanicはまだまだやり続けたいのですが、1日に10回までしか提出できないので、ビギナー用のDigit Recognizerも試してみました。これはサンプルでよく使われているMnist(手書き文字)。
基本的にCNNを通して10通りの数字を分類しますが、これまで精度を上げてみるということはしたことがなかったので、どの程度できるのか今回チャレンジ。

よくあるCNNでやってみてもスコア:0.99以上にはなりました。あとは0.001でもいいのでより精度をあげるにはどうしたらいいかという感じです。
結果としては、0.99528(上位18%)まで上げることができました。以下がコード。

表示されない場合はこちら

サンプルなどでよくあるCNNに対して層やユニット数を調整したり、BatchNormalizationやDropoutを加えてみました。最初は0.993くらいでしたが、その分やや向上しました。
この他、画像をリサイズしてKeras ApllicationsにあるXceptionやInceptionV3なども試してみましたが、それほど良い結果は得られなかったので、そんなに層を増やさなくてもよさそうです。
これもまだまだ精度をあげることはできそうなので、もう少しやり込みたいと思っています。


TGS Salt Identification Challenge:
この他、賞金ありのコンペにも試しに登録してみました。これは地質画像をもとに塩の埋蔵量を予測するコンペのようです。Kernelsには基本的なアルゴリズムがのっているので、そのままコピペしてベースラインのスコアは得られますが、そこからさらに精度をあげなければいけません。基本的に画像認識のコンペですが、セグメンテーションするためのU-net、intersection-over-union(IoU)、その離散値を連続値として計算可能にするLovasz Hinge Lossというテクニックが使われているようで難しそうです。
期限前までに完全理解することはできませんでしたが、Kernelsを読んでいるだけでも勉強になるので、難しそうでも一度参加してみて、できるところまでやってみると知見も広がってよさそうです。

Kaggleで勝つデータ分析の技術
門脇 大輔 阪田 隆司 保坂 桂佑 平松 雄司
技術評論社
売り上げランキング: 363

2018年9月29日土曜日

Kaggle:TitanicをKerasで試してみる

いつかはKaggle(機械学習コンペサイト)をやってみようと思っていましたが、今回ようやくチャレンジしてみました。練習もかねて、ビギナー向けの「Titanic」から。


「Titanic」は訓練データを元に、テストデータの乗客者の生存確率を予想するコンペです。ビギナー向けなので締め切りはないようです。現在は1日10回まで提出できるようです(結果のスコアが分かる)。
もうすでに10000チーム以上がエントリーしてあり、100%の予想方法もあるようですが、今回はあまり難しい方法は使わず、単純にKerasでニューラルネットを組んでどのくらいの確率になるか試してみることにしました。

Googleアカウントで登録可能で、コンペにエントリーすればデータのダウンロードができます。

提出用ファイルの書式サンプルもあるので、それに従ってcsvファイルを書き出せばいいようです。提出は以下の画面からドラックアンドドロップでも可能(Step 1)で、すぐに結果を知ることができます。Step 2は任意のメモ欄で、パラメータの設定値などをメモしています(後で編集可)。

結果を提出すれば、以下のようにすぐにScore(右端)が出てきます。
この提出結果は0.75119なので、いまいち。


Kernels:
KaggleにはKernelsと呼ばれる、解析手順のアイデアやアルゴリズムが参加者によって挙げられています。これを参考にみていくと、どのようにこのデータを扱っていけばいいのかわかります。

使うデータは:
train.csv
test.csv
の二つだけで、最終的にはtest.csvの乗客者リストの生死を0/1で予想します。

データを読み込むと、

PassengerId
Survived
Pclass
Name
Sex
Age
SibSp
Parch
Ticket
Fare
Cabin
Embarked

という項目にわかれて数値や文字列が出てきます。
直接生死に関係のないデータも含まれていますが、どの項目を重視し、あるいは捨ててしまうかはその人次第です。
しかし厄介なのは、たまにデータが抜け落ちており(欠損値)、それを捨てるか、それとも何かを手がかりに穴埋めするかも決めなくてはなりません。まずは一つずつチェックしていかなければならないのですが、Pandas(データ用ライブラリ)をつかえばこのような作業も比較的簡単にできます。

Kernelsを見ると、それぞれの項目の相関を表にしたり、事前にいろいろとデータの傾向を見ているようです。
データサイエンティストではないので、今回はこのような手続きはスキップしてKerasのニューラルネットで自動的に予測してみたいと思います。

そのためには、多少データを整理する必要があります。
・不必要と思われる項目を捨てる
・データに含まれる文字列を数値化
・必要に応じて数値を正規化/標準化
・欠損値を埋める

Pandasをそこまで使いこなしていないので、今回はPandasの勉強もかねてデータクリーニングするところから開始という感じです。

ベースライン:
一応ベースラインというものがあるようで、性別を根拠に求めると0.76555にはなるようです。その他の要素をつっこんでも0.77990が限界という人もいるようです。どうも0.80000を超えるのは難しいようで、なにかしらの工夫が必要なのかもしれません。ということで、とりあえずはベースライン以上を目指してみようかと。

Kaggleで勝つデータ分析の技術
門脇 大輔 阪田 隆司 保坂 桂佑 平松 雄司
技術評論社
売り上げランキング: 363


事前準備:
あとでAge, Embarked, Fareの欠損値を穴埋めするために、以下のような事前準備。
・train.csvとtest.csvを合体
・Ticketの項目を捨てる

文字列を数値へ変換:
・NameからTitle(MrやMissなど)を抜き出す
・Titleに番号を割り振る(0〜17)、合計18種類
・Sexをmale:0, female:1へ変換
・Cabinをnan:0、C:1, E:2, G:3, D:4, A:5, B:6, F:7, T:8へ変換(欠損値:0)
・Embarkedをnan:nan, S:0, C:1, Q:2へ変換(欠損値はあとで穴埋め)

これで、train.csvとtest.csvの両方を数値化(欠損値以外)完了。train.csvとtest.csvを合わせて合計1309人分のデータになります。

数値の正規化あるいは標準化:
特にAgeとFareは他の項目よりも数値が大きいので、場合によっては正規化あるいは標準化が必要かも。

データを欠損値の有無で分ける:
Age, Fare, Embarkedに欠損値があるので、これらを穴埋めするために分けておきます。そうすると、1309人中1043人分のデータが欠損値なしになります。

欠損値補完:
欠損値を穴埋めする際には
・0で埋める
・平均値で埋める
・頻度の高い数値で埋める
などいくつか方法があるようですが、今回は欠損値もニューラルネットで予想しようと思います。

ここまで準備するにも結構時間がかかりました。おかげでPandasの使い方にも慣れてきました。

Kerasで穴埋め用ニューラルネットモデルを構築:
とりあえず、層の数、ユニット数などは適当に決めて、あとから調整してみたいと思います。Embarked、Fare, Ageという欠損値の少ない順に求めてみました。


生存者予想:
nanを穴埋めしたデータを元に、train.csv(891人)からtest.csv(418人)の生存者を0/1で予想します。これもまたKerasのニューラルネットを使って予想します。

結果:
とりあえず一回目の結果としては0.71770でした。かなり低い。しかし、エラーはでないので一応アルゴリズムとしては間違ってはいなさそうなので、ここから改良していこうかと。


改良:
ニューラルネットの層をいろいろ変えてみると、層を増やしてもあまりいい結果がでないので浅くしてみました。少し改善されて0.74641。
batch_sizeをデフォルトの32から5に変えると0.75598

しかし、テストデータ418人中100人以上が間違っているということなので、ちょっとしたランダムな誤差でも数人分かわってしまいそう。このあたりになってくると、もはやゲームのハイスコア狙いのような感覚になってきます。

さらにBatchNomalizationやDropoutなども層に追加してみたり、いろいろ試してみました。しかし0.75前後という感じで0.80まではなかなか届きそうにありません。複雑にしたからといっても正解率があがるわけでもなく、精度の低いモデルにオーバーフィッティングしているだけなのかもしれません。

要素を減らして(Pclass, Sex, Age, Fare, Embarkedだけ)シンプルに計算してみると、0.77990まであがりました。ようやくベースライン。

その後、また要素は戻して、正規化で全ての数値を0.0〜1.0に変換し、比較的シンプルな層でやってみると今までのベストスコアとなる0.78947。約10000人中の3371位。半分より上に行けたのでよかったのですが、これも偶然という感じ。
random.seedによっても結果が変わりそうなので、

import tensorflow as tf
import random as rn
import os
os.environ['PYTHONHASHSEED'] = '0'
rn.seed(123)
np.random.seed(123)
session_conf = tf.ConfigProto(intra_op_parallelism_threads=1, inter_op_parallelism_threads=1)
from keras import backend as K
tf.set_random_seed(123)
sess = tf.Session(graph=tf.get_default_graph(), config=session_conf)
K.set_session(sess)

で乱数を固定してみました(Kerasの再現可能な結果)。
追記:
Dense()とDropout()のなかのkernel_initializerにも乱数の設定があるので、
kernel_initializer=keras.initializers.glorot_uniform(seed=123)
と固定してみましたが、GPUを使っているためかそれでも毎回微妙に値が異なってしまいます。cuDNNのほうも設定しないといけないのかもしれません。

まとめ:
Kaggle自体敷居が高そうですが登録や提出などは簡単で、何度も提出できるので気軽に参加できます。実際に具体的な目標(スコア)があるので工夫しがいがあります。ハマると、ゲーム感覚でハイスコアを狙うといったやりこみ癖が出てきそうです。そういう意味でも面白いかもしれません。勉強する際にサンプルコードを写経するだけでおしまいになることがありますが(動くかどうか確かめるだけ)、工夫によってスコアが変わるためいろいろ試しながら結果を比較向上していく部分がさらなる理解度を深めます。当然、データの事前処理も今まではあまりやったことはありませんでしたが、データのあり方から様々な傾向が見えてくるのも面白いと思います。

その後いろいろいじってみましたが、0.76前後をふらふらしており決定的な改善策が見当たりません。NameをTitleに変換し、さらにTitleをone-hotラベルに変換したりしましたがそれほど効果なし。欠損値をニューラルネットで予測し、その予測を元に最後の生死を予測しているので、最初の予測が間違っていれば意味がないという感じかもしれません。1日に10回まで提出することができるので何度も試しているところです。

以下が、現在のコード(Jupyter Notebook)。まだベースライン前後なので、もう少し改良が必要です。


Gistが表示されない場合はこちら

2018年9月15日土曜日

SCHOOL OF AI: MOVE 37(無料強化学習コース)


Siraj Raval氏のAI関係のYoutube動画はよく見るのですが、9/10から無料のコース「MOVE 37」が始まったので試しに登録してみました。内容は主に強化学習についてであり、10週で完了するコースです。

去年まではGANなどの生成的ネットワークにトピックが集中していたような気がしますが、最近は強化学習が流行ってきたのでしょうか?Pose Estimatorのようなアルゴリズムも公開され、ディープラーニングの応用法においても幅が広がってきたのかもしれません。


この「MOVE 37」というコースにおいては、以下を使って学ぶようです。

・Pytorch
・OpenAI Gym
・Google Collaborator
・TensorFlow

Google Colab上でコーディングし無料GPUで訓練できるので、どんなパソコンでも大丈夫という感じです。


コース内容は、1週間ごとに以下の項目を実施(合計10週)。

・Markov Decision Processes 0/7
・Dynamic Programming 0/7
・Monte Carlo Methods 0/6
・Model Free Learning 0/7
・RL in Continuous Spaces 0/7
・Deep Reinforcement Learning 0/7
・Policy Based Methods 0/7
・Policy Gradient Methods 0/7
・Actor Critic Methods 0/7
・Multi Agent RL 0/7

基本から始まり、一通り強化学習を順を追って勉強していくので、全体を網羅するにはいいのかもしれません。

1週目は「マルコフ決定過程」についてですが、
・強化学習の基礎知識(ビデオや文章)
・ミニテスト
・OpenAIのインストール
・Google Dopamine(Google最新の強化学習フレームワーク)の紹介
などです。

2週目(来週)から本格的にコーディングしていくという感じでしょうか。
無料コースなので、それほど親切丁寧に指導してくれるというわけではなさそうですが、Forumなどもあるので、聞きたいことがあれば誰かが答えてくれるといった環境はあります。そもそも強化学習に興味あるけど、どのような順番で何を勉強していけばいいかという手がかりは最低限得られるので、その後独学する手立てにもなると思います。

2018年9月10日月曜日

Google Colabの無料GPUで強化学習(Keras-RL)を試す

最近は、Google Colabの無料GPU(連続12時間まで)を使用して演算が可能なので、機械学習用GPUが搭載されていないパソコンでも充分機械学習の訓練などできそうです。使い方はオンラインのJupyter Notebookという感じです。


Colabスペック:
Ubuntu17.10(現在は18.04)
Intel(R) Xenon CPU @2.3GHz
13GB RAM
GPU NVIDIA Tesla K80

スペックはこんな感じ。アルゴリズムにもよりますがTesla K80がどのくらい高速なのかが気になります。自前のゲーミングノートはPascal世代のGTX1060ですが、K80はそれよりも2世代前のKepler世代のGPUのようです。
!nvidia-smiを打ち込むと以下。


このほか、機械学習に必要な基本的なライブラリはnumpy、pandas、matplotlib、Tensorflow、Kerasも含め既にインストールしてあるようです。Pytorchは入っていないのでインストールする必要があります(追記:その後Pytorchなどもインストール済みになったようです)。

ということで、前回のKeras-RL + Open AI Gym Atariの訓練(1750000ステップ)を比較してみようかと。自前のゲーミングノートGTX1060では約3時間かかりました。


インストール:
とりあえず、Keras-RLやGymなど必要なものをapt-get installやpipで前回同様にインストールする必要があります。
!pip install gym keras-rl pyglet==1.2.4
!apt-get install -y cmake zlib1g-dev libjpeg-dev xvfb ffmpeg xorg-dev python-opengl libboost-all-dev libsdl2-dev swig
!pip install 'gym[atari]'
最初に「!」マークをつけたあとpipやapt-getなどでインストールできます。今回は上記のものをインストールしました。前回Jupyterの際、pyglet1.3.2だとダメだったので、最新ではないpyglet1.2.4をインストールしています。
必要なライブラリなどはインストールできますが、再度ログインするとまたインストールし直さないといけないようです(Colab内は毎回クリアされる?)。


実行時間の比較:
以下が自前のGTX1060での訓練時のログ。これは前回Jupyter用に書き直したAtariブロック崩しの訓練です。
10000ステップで約60秒かかっています。訓練するには合計1750000ステップあるので、約3時間ということになります。

これに比べて、Google Colab(Tesla K80)の方は以下。
10000ステップで約108秒かかっています(意外と遅い)。
自前のゲーミングノート(GTX1060)のほうが1.8倍速いという計算になります。
とはいっても普通のMacBook(CPU演算)に比べればはるかに速いとは思います。

しかしながら、370000ステップの途中で突然ストップしてしまいました。
最後に、「Buffered data was truncated after reaching the output limit」という表示が出ています。何か出力限界に達したようで、途中停止しています。
検索してみると、Stack flowに似たような件について書いてありました。RAMかGPUのメモリをオーバーしてしまったようで、バッチサイズを少なくするか、小分けに途中経過(ウェイト)を保存しておいて再開するかなどの工夫が必要そうです。一応プログラム上ではチェックポイントを設けて250000ステップごとにウェイトを途中保存してはあります。
やはりある程度の規模になると、使用する上での工夫が必要そうです。今回の場合だと1750000ステップの訓練をさせるには概算すると5時間はかかりそうです。連続使用12時間以内で済みそうですが、それ以前にメモリなども気を遣わなければいけないということです。
ということで、処理速度を知りたかったので訓練は途中で終了。

動画表示:
そのかわりに、以前保存しておいたウェイトを読み込んでColab上に表示できるか試してみました。
尚、外部ファイルなどを読み込ませるには以下を書き込んでランさせるとファイル選択ダイアログがでてきて、パソコンの任意の場所からファイルを読み込むことができます。
from google.colab import files
uploaded = files.upload()
前回Jupyter上で表示可能であった方法を試してみましたが、画面のちらつき、動画としての更新速度が非常に遅く、いまいちという結果。

ということから一旦出力画像を非表示にして、matplotlibのArtistAnimationに変換後表示する方法を試してみました(以下)。
from rl.callbacks import Callback
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML
%matplotlib inline

ims = []

class Render(Callback):
    def on_step_end(self, step, logs={}):
        im = plt.imshow(env.render(mode='rgb_array'))
        ims.append([im])

weights_filename = 'dqn_{}_weights.h5f'.format(ENV_NAME)
dqn.load_weights(weights_filename)

callbacks = Render()
fig = plt.figure(figsize=(4,5))
plt.axis('off')
dqn.test(env, nb_episodes=1, visualize=False, callbacks=[callbacks])

ani = animation.ArtistAnimation(fig=fig, artists=ims, interval=10)
# ani.save("anim.gif", writer = "imagemagick")  # imagemagick for Ubuntu
plt.close()

HTML(ani.to_jshtml())         # JavascriptHTML output
#HTML(ani.to_html5_video())   # HTML5 Video output
これは前回もJupyter上で試した方法です(前半の訓練時のコードは省いてあります)。一旦画像は非表示にして、Keras-RLのCallback関数をオーバーライドして表示用サブクラスをつくっています(詳しくは前回参照)。Keras-RLはdqn.test()で別窓に動画表示してしまうので、それを非表示にしておき、Jupyter上で表示可能なデータに変換してから表示しています。
結果的には、アニメーションとして最後表示することができました。Jupyterも表示には手こずりましたが、Colabはオンラインでもあるためどうしてもリアルタイムだと遅くなってしまうようで、このようにアニメーションとして変換してしまえば問題なさそうです。アニメーションに変換する分、少しだけ(数十秒程度)余計に時間がかかりますが、特に目立った遅さではないのでこれでいけそうです。

アニメーションとして保存してあるので、オンラインによるタイムラグはなく普通に速く動きます。
Atariの場合はこのようにColab上に表示されますが、CartPoleのようなClassic controlの場合はレンダリングの仕組みが違うためかColab上のアニメーションだけでなく別窓にも表示されてしまいます。

追記:尚、このボタン付きアニメーションをWebページに埋め込むには以下。
Matplotlib Animation embed on web page:アニメーションのWebページ上への埋め込み


まとめ:
結果的には自前のゲーミングノートGTX1060のほうが速かったのですが、数時間かかりそうな訓練の場合にはGoogle Colabのほうで処理させておき、メインのパソコンではまた別の作業をという感じで使うこともできそうなので、それなりに便利かと。
今後はこのようなGPUのクラウドサービスが主流となってくれば、機械学習をするにはわざわざゲーミングパソコンを買うまでもないかと思います。ネットだけできればいいような5万円前後のGoogle Notebookでもいいのかもしれません。


2018年9月7日金曜日

Keras-RLで強化学習/DQN(Deep Q-Network)を試してみる

前回GANについて理解を深めてみましたが、その後もGANの発展型となるACGANやInfoGANについても引き続き勉強中です。しかしながら、今回はやや方向転換して強化学習(Reinforcement Learning)について試してみました。

環境:
Ubuntu 18.04
GTX 1060
CUDA 9.0
python 3.6
tensorflow 1.9
Keras 2.1.6
Keras-rl 0.4.2
Jupyter Notebook 5.6


アルゴリズム:
強化学習には独特のアルゴリズムが使われており、ディープラーニング以前にも

・Q-Learning
・SARSA
・モンテカルロ法

などが基本としてあるようです。
その後、AlphaGOで有名となった

・DQN(Deep Q-Network)

そして、さらに改良された

・Double DQN
・Dueling DQN
・AC3
・UNREAL
・PPO

などがあるようです。日々改良されているようですが、どれがいいのかは目的によっても異なるようです。とりあえず今となってはDQNあたりが基本かと。


OpenAI:
手っ取り早く強化学習を勉強するならOpenAIのGYMを利用するとよさそうです。GYMには倒立振子やATARIのビデオゲームなどの教材があり、強化学習アルゴリズムを書き足せばすぐに試すことができます。
このページのインストール方法に従って必要なライブラリなどをインストールしますが、ATARIのビデオゲームを使いたい場合はcmakeも必要となるので、Ubuntuであれば一通り以下のコマンドで全てインストールしておいたほうが良さそうです。
apt-get install -y python-numpy python-dev cmake zlib1g-dev libjpeg-dev xvfb ffmpeg xorg-dev python-opengl libboost-all-dev libsdl2-dev swig

動作チェック:
Getting Started with Gymにも書いてありますが、以下のコードで100ループ動きます(ランダムな動き)。ちなみにこのままだとJupyter Notebookでは表示(レンダリング)されないので、.pyファイルにして実行させないといけません。

import gym

env = gym.make('CartPole-v0')
env.reset()

for _ in range(100):
    env.render()
    env.step(env.action_space.sample())
env.close()

あっというまに表示が終わってしまうので以下のようにtime.sleep()でディレイを加えてみました。

import gym
import time

env = gym.make('CartPole-v0')
env.reset()

for _ in range(100):
    env.render()
    env.step(env.action_space.sample())
    time.sleep(0.02)
env.close()


Jupyter Notebookの場合:
アニメーションをJupyter Notebook上で表示するには少し工夫が必要です。stack overflowにもいくつか方法が書いてあります。
ATARIの場合であれば以下の方法で表示可能でした。


import gym
from IPython import display
import matplotlib.pyplot as plt
%matplotlib inline

env = gym.make('Breakout-v0')
env.reset()

img = plt.imshow(env.render(mode='rgb_array'))

for _ in range(100):
    img.set_data(env.render(mode='rgb_array'))
    display.display(plt.gcf())
    display.clear_output(wait=True)
    action = env.action_space.sample()
    env.step(action)
env.close()
しかし、CartPole-v0のようなClassic controlの場合だとエラーがでてしまうので、インストールしてあるpyglet1.3.2を一旦アンインストール(pip uninstall pyglet)して、pyglet1.2.4をインストール(pip install pyglet==1.2.4)し直すといいようです(こちらの方法)。ただこの方法だとJupyter上だけでなく別窓も開いてしまいます。そして、env.close()を最後に書き加えないと、別窓を閉じることができなくなるので要注意。

別窓だけの表示でいいのであれば(Jupyter上には表示させない)、pyglet1.2.4にダウングレードさえしておけば、以下の方法でも可能でした。
import gym
import time

env = gym.make('CartPole-v0')
env.reset()

for _ in range(100):
    env.render()
    env.step(env.action_space.sample())
    time.sleep(0.02)
env.close()
Jupyter Notebook上に表示させる方法をいろいろ探してみましたが、別窓での表示であれば簡単そうなので、以下のKeras-RLでも別窓表示にすることにしました。


Keras-RL:
とりあえずDQNで強化学習をしたいので、どの機械学習フレームワーク(Tensorflow、Keras、Pytorchなど)を使えばいいかということですが、Keras-RLというKeras向けの強化学習用のライブラリがあり、以下のようなアルゴリズム(ここに書いてある)が既に搭載されており、すぐに使うことができます。

NameImplementationObservation SpaceAction Space
DQNrl.agents.DQNAgentdiscrete or continuousdiscrete
DDPGrl.agents.DDPGAgentdiscrete or continuouscontinuous
NAFrl.agents.NAFAgentdiscrete or continuouscontinuous
CEMrl.agents.CEMAgentdiscrete or continuousdiscrete
SARSArl.agents.SARSAAgentdiscrete or continuousdiscrete
複雑なアルゴリズムをコーディングしなくても、既存の関数にパラメータを渡せば計算してくれますが、基本となるQ-Learningの仕組みはある程度理解しておいたほうがよさそうです。
Action Space欄に離散値か連続値かの違いがあるので、目的に応じて使い分けるといいと思います。


DQNを試してみる:

CartPoleのサンプル:
Keras-RLにはいくつかのサンプルコードがあるので、dqn_cartpole.pyを試してみることに。
Jupyter NotebookでRunさせる場合は、別窓としてアニメーションが表示されます。終了後別窓を閉じるために、最後の行にenv.close()を追加しておきます。


import numpy as np
import gym

from keras.models import Sequential
from keras.layers import Dense, Activation, Flatten
from keras.optimizers import Adam
from rl.agents.dqn import DQNAgent
from rl.policy import BoltzmannQPolicy
from rl.memory import SequentialMemory

ENV_NAME = 'CartPole-v0'

env = gym.make(ENV_NAME)
np.random.seed(123)
env.seed(123)
nb_actions = env.action_space.n

model = Sequential()
model.add(Flatten(input_shape=(1,) + env.observation_space.shape))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(nb_actions))
model.add(Activation('linear'))
print(model.summary())

memory = SequentialMemory(limit=50000, window_length=1)
policy = BoltzmannQPolicy()
dqn = DQNAgent(model=model, nb_actions=nb_actions, memory=memory, nb_steps_warmup=10,
               target_model_update=1e-2, policy=policy)
dqn.compile(Adam(lr=1e-3), metrics=['mae'])

dqn.fit(env, nb_steps=50000, visualize=True, verbose=2)
dqn.save_weights('dqn_{}_weights.h5f'.format(ENV_NAME), overwrite=True)

dqn.test(env, nb_episodes=5, visualize=True)
env.close()
GTX1060で、学習50000ステップ、約5分かかりました。
DQNAgentクラスに必要な項目を渡すだけなので、アルゴリズム的には超シンプルです。
CartPoleに関しては左か右に動かすだけなので、env.action_space.nは2になります。
最後のほうにあるdqn.save_weight()で学習したウェイトが外部保存されるので、次回このウェイトをつかってテストするには、以下のように書き換えることになります。
# 以下をコメントアウトして
# dqn.fit(env, nb_steps=50000, visualize=True, verbose=2)
# dqn.save_weights('dqn_{}_weights.h5f'.format(ENV_NAME), overwrite=True)

# かわりに保存したウェイトを読み込む
dqn.load_weights('dqn_{}_weights.h5f'.format(ENV_NAME))

dqn.test(env, nb_episodes=5, visualize=True)
env.close()
デフォルトでは、以下のように一回のepisodeでsteps: 200になっています。
例えばsteps: 500に変えるには、
env = gym.make("CartPole-v0")
env._max_episode_steps = 500
とすればいいようです(ここに書いてありました)。


ATARIブロック崩しをJupyter Notebook上に表示:
もうひとつは、ATARIのブロック崩しのサンプルです。これは (210, 160, 3) のRGB画像を入力としてCNNを通して学習していきます。画像から判断するので、どんなゲームでもいいということになります。サンプルにある通り1750000ステップ学習するために約3時間かかりました(GTX1060)。
もともとこのサンプルは.pyファイルですが、Jupyter Notebook上に表示できるように少し手を加えてみました。サンプルは最後の方にあるdqn.test()で結果表示されますが、既存コードを見るとenv.render(mode='human')が使用されており、Jupyter Notebook上に表示するにはenv.render(mode='rgb_array')に変換する必要がありそうです。
そのため、既存の結果表示はvisualize=Falseで非表示にし、かわりに自前のCallback関数を追加することで毎ステップ画像表示させることにしました。またargparseはJupyterでは使えないので消去し、そのかわりに各変数を用意しました。


from PIL import Image
import numpy as np
import gym

from keras.models import Sequential
from keras.layers import Dense, Activation, Flatten, Convolution2D, Permute
from keras.optimizers import Adam
import keras.backend as K

from rl.agents.dqn import DQNAgent
from rl.policy import LinearAnnealedPolicy, BoltzmannQPolicy, EpsGreedyQPolicy
from rl.memory import SequentialMemory
from rl.core import Processor
from rl.callbacks import FileLogger, ModelIntervalCheckpoint


INPUT_SHAPE = (84, 84)
WINDOW_LENGTH = 4

class AtariProcessor(Processor):
    def process_observation(self, observation):
        assert observation.ndim == 3
        img = Image.fromarray(observation)
        img = img.resize(INPUT_SHAPE).convert('L')
        processed_observation = np.array(img)
        assert processed_observation.shape == INPUT_SHAPE
        return processed_observation.astype('uint8')

    def process_state_batch(self, batch):
        processed_batch = batch.astype('float32') / 255.
        return processed_batch

    def process_reward(self, reward):
        return np.clip(reward, -1., 1.)

ENV_NAME = 'BreakoutDeterministic-v4'
env = gym.make(ENV_NAME)
np.random.seed(123)
env.seed(123)
nb_actions = env.action_space.n
input_shape = (WINDOW_LENGTH,) + INPUT_SHAPE

model = Sequential()

if K.image_dim_ordering() == 'tf':
    model.add(Permute((2, 3, 1), input_shape=input_shape))
elif K.image_dim_ordering() == 'th':
    model.add(Permute((1, 2, 3), input_shape=input_shape))
else:
    raise RuntimeError('Unknown image_dim_ordering.')

model.add(Convolution2D(32, (8, 8), strides=(4, 4)))
model.add(Activation('relu'))
model.add(Convolution2D(64, (4, 4), strides=(2, 2)))
model.add(Activation('relu'))
model.add(Convolution2D(64, (3, 3), strides=(1, 1)))
model.add(Activation('relu'))
model.add(Flatten())
model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dense(nb_actions))
model.add(Activation('linear'))
#print(model.summary())

memory = SequentialMemory(limit=1000000, window_length=WINDOW_LENGTH)
processor = AtariProcessor()

policy = LinearAnnealedPolicy(EpsGreedyQPolicy(), attr='eps', value_max=1., value_min=.1, value_test=.05,
                              nb_steps=1000000)

dqn = DQNAgent(model=model, nb_actions=nb_actions, policy=policy, memory=memory,
               processor=processor, nb_steps_warmup=50000, gamma=.99, target_model_update=10000,
               train_interval=4, delta_clip=1.)

dqn.compile(Adam(lr=.00025), metrics=['mae'])

# コールバックとJupyter表示用モジュールのインポート
from rl.callbacks import Callback
from IPython import display
import matplotlib.pyplot as plt
%matplotlib inline

# 表示用Renderサブクラス作成(keras-rlのCallbackクラス継承)
class Render(Callback):
    def on_step_end(self, step, logs={}):
        plt.clf()
        plt.imshow(env.render(mode='rgb_array'))
        display.display(plt.gcf())
        display.clear_output(wait=True)

MODE = 'train' # 'train' or 'test' 学習とテストのモード切替え

if MODE == 'train':
    weights_filename = 'dqn_{}_weights.h5f'.format(ENV_NAME)
    checkpoint_weights_filename = 'dqn_' + ENV_NAME + '_weights_{step}.h5f'
    log_filename = 'dqn_{}_log.json'.format(ENV_NAME)
    callbacks = [ModelIntervalCheckpoint(checkpoint_weights_filename, interval=250000)]
    callbacks += [FileLogger(log_filename, interval=100)]
    dqn.fit(env, callbacks=callbacks, nb_steps=1750000, log_interval=10000)

    dqn.save_weights(weights_filename, overwrite=True)
    # dqn.test(env, nb_episodes=10, visualize=False)
    
elif MODE == 'test':
    weights_filename = 'dqn_{}_weights.h5f'.format(ENV_NAME)
    dqn.load_weights(weights_filename)

    # 表示用コールバック関数を適用
    callbacks = Render()
    plt.figure(figsize=(6,8))
    dqn.test(env, nb_episodes=2, visualize=False, callbacks=[callbacks])

env.close()
元々のサンプルはargparseで'train'か'test'でモードの切り替えをしていましたが、かわりに変数MODEを用意して切り替えています(argparseの代わりにeasydictを使うといいようです)。
keras-rlのCallback関数をオーバーライドしJupyter用に表示用サブクラスをつくって、毎ステップごとにenv.render(mode='rgb_array')を呼び出して表示させています。keras-rlのCallbackクラスを見てみると、episodeやstepの前半後半のタイミングでコールバックできるようで、今回はstep後半のon_step_end()に表示機能を挿入しておきました。
この結果、一応Jupyter上には表示できるようになりましたが、plt.imshow()を使っているせいか動きが遅くなってしまいます。やはり別窓に表示させたほうがいいかもしれません。リアルタイムで表示させなくてもいいのであれば、以下の方法がいいかと。


Jupyter Notebook上にアニメーション表示とGIF動画保存:
matplotlibのArtistAnimationクラスで先程のブロック崩しを表示しつつ、GIF動画として保存する方法についてです。訓練後のテスト部分を少し変えて以下のようにしてみました。
from rl.callbacks import Callback
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML
%matplotlib inline

ims = []  # アニメーション用リスト

class Render(Callback):
    def on_step_end(self, step, logs={}):
        im = plt.imshow(env.render(mode='rgb_array'))
        ims.append([im])

weights_filename = 'dqn_{}_weights.h5f'.format(ENV_NAME)
dqn.load_weights(weights_filename)

callbacks = Render()
fig = plt.figure(figsize=(4,5)) # 出力画面サイズ調整
plt.axis('off')                 # 目盛り、枠線なし
dqn.test(env, nb_episodes=1, visualize=False, callbacks=[callbacks])

ani = animation.ArtistAnimation(fig=fig, artists=ims, interval=10)
# ani.save("anim.gif", writer = "imagemagick") # GIFアニメ保存する場合はコメントアウト
plt.close()

# Jupyter Notebook上にアニメーション表示
HTML(ani.to_jshtml())        # JavascriptHTML出力
#HTML(ani.to_html5_video())  # HTML5 Video出力(.mp4ファイルとしてダウンロード可)
サブクラスRender()内で予め用意しておいたアニメーション用リストに毎ステップ画像を追加していき、それをあとからArtistAnimationで動画にするという手順です。ArtistAnimationを使えばすぐにGIF動画として保存もできます。


Jupyter上のアニメーション表示としては2種類あり、JavascriptHTMLは動画速度を変えて再生も可能なので便利です(上画像)。またHTML5 Video出力のほうは表示画面から.mp4として動画をダウンロードできる機能がついています。
尚、matplotlibのArtistAnimationについては以前投稿したここを参照して下さい。

追記:尚、このボタン付きアニメーションをWebページに埋め込むには以下。
Matplotlib Animation embed on web page:アニメーションのWebページ上への埋め込み


まとめ:
OpenAIのGYMとKeras-RLを使うことで簡単にDQNを試すことができます。DQNに渡すパラメータについて理解しておけばいいという感じです。
このほか、二足歩行モデルがあるMuJoCo、ロボットアームやハンドマニピュレータがあるRoboticsのサンプルもあります。学習させるには結構時間かかりそうなので、まだ試してはいませんが、強化学習は生成モデルとはまた違ったアプローチをしている部分が興味深いという感じ。また、強化学習と生成モデルの組み合わせもできそうなので、アルゴリズム的に面白くなりそうです。
GPUがなくても、Google Colabを使えばこの程度の訓練であれば短時間でおわるかもしれません。

関連:
Google Colabの無料GPUで強化学習訓練を試す(Keras-RL)



人気の投稿