wide and deep

WindowsからWSL内のPython仮想環境にスクリプトを実行させる

今回したこと

タイトルの通り,WindowsからWSL内のPython仮想環境にスクリプトを実行させました.
この記事に需要があるかどうかは謎ですが,WSLで環境を作成し,Windowsからその環境でスクリプトを実行したいようなときに役立つと思います.

test.py(py37)からevaluate.py(py27)を呼び出したいような状況が発生したので調べました.

Windows{ Python3.7 }   --->  WSL{ venv{ Python2.7 } }
           └ test.py                       └ evaluate.py

答え

wsl のあとに引数としてwslで動かしたいコマンドを打てばいいみたい.

> wsl { Python仮想環境の絶対パス } { スクリプトの絶対パス }

今回はPythonスクリプトからWSL環境で別スクリプトを起動したかったので,
諸々をバッチ化してからそれを呼び出すようにしました.
各パスは変更するの面倒なのでそのまま貼ります.
環境はvenvで作成したものです.

@echo off
set root_path=/mnt/c/Users/Milano/Desktop/wind-turbine_design_optimization
set python_path=%root_path%/EC2019/jpnsecCompetition2019/bin/python
set eval_script=%root_path%/evaluation/windturbine_SOP.py

wsl %python_path% %eval_script%

関係ないですけど.batのシンタックスハイライトは dosbatch なんですね
上記バッチ(evaluation.bat)をPythonスクリプト内から呼び出す.

import os
os.system('evaluation.bat')

終わりに

あまり役立つ機会があるとは思えないですが,
例えば下記のようなコンペに参加するときなどに覚えておくと幸せになるかもしれないです.
評価モジュールががWSL上Python2.7系なので
www.jpnsec.org

自分が取り組んだコード
コンペ自体はまだ開催中ですが,提出する気はないので貼っておきます.
github.com

スマートフォンのGPS情報を取得し地図上に表示するWEBアプリを作成

今回作ったもの

URLにアクセスしたスマートフォンの位置情報を取得し,地図上に表示するWEBアプリを作成した.
建前上の用途は,バスなどにスマートフォンを積ませ,停留所で待つ乗客が位置を確認できるなど...
ソースはここに置いている
github.com

実際の動き

スマートフォンからアクセスした画面はこんな感じ
緯度経度と選択された経路が表示されている(
バスの経路をイメージ).
https://user-images.githubusercontent.com/37448236/66890234-59ef8580-f020-11e9-836c-4c4648df0028.jpg

表示される地図はこんな感じ
指定座標にピンを立て,infoには経路情報を表示している.
https://user-images.githubusercontent.com/37448236/66890059-a6869100-f01f-11e9-9360-03a708781e43.png

構成

サーバ側はpython(flask),クライアント側は大体Javascriptでどうにかしている.

  • python(flask)
    • サーバを建てる
    • 位置情報を保存
    • 地図を描画しピンを立てる(flask_googlemaps)
  • Javascript
    • 位置情報を取得
    • 位置情報をサーバに送信

新しく知ったこと

GPS情報はJavascriptで取得できる

標準のnavigator.geolocation.watchPositionを使って各種情報を取得できるらしい.かんたん.
今回は緯度経度・精度しか使っていないが,他にもspeedとか色々あるらしい.
developer.mozilla.org

// GPS値が変化したら実行される
navigator.geolocation.watchPosition((position) => {
    var lat = position.coords.latitude;
    var lng = position.coords.longitude;
    var acc = position.coords.accuracy;
    sendLocation(lat, lng, acc, route_id);
}, (error) => {
    alert('GPS情報が取得できません.権限を確認してください')
}, {
    enableHighAccuracy: true
});

GPS情報はhttps接続でなければ取得できない

参考↓
www.flying-h.co.jp

httpsでサーバを建てるためにオレオレ証明書を発行し,flaskに読み込ませた.
まずssl証明書を/certに作成

$ sudo apt install openssl
$ sudo apt install python3-openssl
$ mkdir cert
$ cd cert
$ openssl genrsa 2048 > server.key
$ openssl req -new -key server.key > server.csr
$ openssl x509 -days 365 -req -signkey server.key < server.csr > server.crt

作成した証明書をflaskに読み込ませる.

from flask import Flask
import ssl

app = Flask(__name__)
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.load_cert_chain('cert/server.crt', 'cert/server.key')
...
if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True, ssl_context=context)

GoogleMapはflask_googlemapsから描画できる

JavascriptでGoogleMapを描画できるAPIpythonラッパーが公開されていた.
github.com

使い方は直感的でわかりやすい.
Mapオブジェクトに色々設定したあと(ここでは変数mymapに格納),それをrender_templateでmymap=mymapとして渡してやる.

from flask import Flask, render_template
from flask_googlemaps import GoogleMaps
from flask_googlemaps import Map

app = Flask(__name__)
GoogleMaps(app, key="GOOGLE_MAP_API_KEY")

def mapview():
    df = pd.read_pickle('./df.pkl')
    subset = df[['lat', 'lng', 'info']]
    locations = [tuple(x) for x in subset.values]
    # マップを作成
    mymap = Map(
        identifier = "view",
        lat = 31.581319,
        lng = 130.544519,
        markers = [(loc[0], loc[1], loc[2]) for loc in locations],
        fit_markers_to_bounds = len(locations) > 1,
        style = "height:800px; width:80%; margin:auto; text-align:center;",
        region = "JPN"
    )
    return render_template('map.html', mymap=mymap)

map.html側では,ヘッダで{{mymap.js}}を受け取り,body(表示したい場所)で{{mymap.html}}を受け取る.

<!DOCTYPE html>
<html>
<head>
    {{mymap.js}}
</head>

<body>
    <h1 style="text-align: center;">Flask Google Maps</h1>
    {{mymap.html}}
</body>
</html>

とても簡単に使えるので今後使っていきたい.
あとGOOGLE_MAP_API_KEYを取得する必要がある.
このあたりを参考に
nendeb.com

クライアント → サーバのデータ送信

サーバ側にPOST用のページを作成しておく.
ここでは緯度経度,精度,経路情報を受け取るようにしてある.
受け取るデータはjson.loads(request.data.decode('utf-8'))のようにしなければならない.
request.data.decode('utf-8')自体はString形式を返すらしいのでちゃんとJSON形式に変換してやる

# POSTされた情報を受け取るページ
@app.route('/send-location', methods=['POST'])
def send():
    data = json.loads(request.data.decode('utf-8'))
    addr = request.remote_addr
    lat = data["lat"]
    lng = data["lng"]
    acc = data["acc"]
    route_id = data["route_id"]
    route_name = data["route_name"]
    ...
    return ''

クライアント側のデータ送信部分
上記/send-locationに各情報をまとめた辞書をJSON化して(JSON.stringify(data)))POSTする

// 取得したデータをサーバへ送信
function sendLocation(lat, lng, acc, route_id, route_name) {
    var data = {
        "lat": lat,
        "lng": lng,
        "acc": acc,
        "route_id": route_id,
        "route_name": route_name
    }
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "/send-location");
    xhr.send(JSON.stringify(data));
}

終わりに

今回はスマートフォンGPS情報を取得し地図上に表示するWEBアプリを作成した.
大学院の特別研究と称された,外部研究室での研究として取り組んだが,とても興味を引くことをやらせていただいた.
今までの知識も活用することができ,早く簡単に作成することができた.

kerasで自作レイヤーを含むモデルをload_modelするときのエラー処理

エラーと原因

  • 自作レイヤーを読み込む際に,初期定義された重みの形状とは異なることに関するエラー.

ValueError: Layer #0 (named "custom_conv" in the current model) was found to correspond to layer custom_conv in the save file. However the new layer custom_conv expects 3 weights, but the saved weights have 4 elements.

tensorflow.python.framework.errors_impl.InvalidArgumentError: Dimension 3 in both shapes must be equal, but are 16 and 1. Shapes are [3,3,3,16] and [3,3,3,1]. for 'Assign_64' (op: 'Assign') with input shapes: [3,3,3,16], [3,3,3,1].

  • 原因の箇所

モデルの学習自体は下記CuntomConvレイヤーにおいてoutput_chs=[16,16,16,16]で実行した.
保存されたモデルのCuntomConvが持つ重みと自作レイヤーCuntomConvが初期状態で持つ重みの形状が異なることが原因だと思う.

# conv2Dを並列に複数行いconcatするようなレイヤー
# output_chsで各convの出力チャンネル数を指定している
class CuntomConv(Layer):
    def __init__(self, kernel_initializer='he_normal', output_chs=[1,1,1,1],
                 kernel_regularizer=None, kernel_constraint=None, **kwargs):
    ...

pretrained_model = load_model(pretrained_model_path, compile=False, custom_objects={'CuntomConv': CuntomConv})

解決策

custom_objectsに与える自作レイヤーの初期重み形状を,保存したモデルの自作レイヤーと同じものにする.
具体的には,自作レイヤーを継承しoutput_chsを変更したレイヤーを作成し,custom_objectsに与える.

output_chs = [16,16,16,16]
class _CunsomConv(CunsomConv):
    def __init__(self, kernel_initializer='he_normal', output_chs=output_chs, kernel_regularizer=None, kernel_constraint=None, **kwargs):
        super().__init__(kernel_initializer='he_normal', output_chs=output_chs, kernel_regularizer=None, kernel_constraint=None, **kwargs)
pretrained_model = load_model(pretrained_model_path, compile=False, custom_objects={'CunsomConv': _CunsomConv})


試行錯誤で対処したので間違っているかもしれないが,参考になる情報が転がっていなかったのでここに残しておく.

keras/tensorflowでoptimizersの学習率を層ごとに決定する

一昔前のDL論文を読んでいると,層ごとに違う学習率が設定されていることがある.
自分にはあまり馴染みがなかったが,Caffeでは簡単にその設定ができたために使われていたらしい.

keras(tensorflow)でこの設定をしようとしたとき,

keras.optimizers.SGD(lr=0.01, momentum=0.0, decay=0.0, nesterov=False)

lr: 0以上の浮動小数点数.学習率.
最適化 - Keras Documentation

上記のようにあり,learning_rateは整数しか取らず,全体としての学習率しか設定できない.
keras/optimizers.py at master · keras-team/keras · GitHub

solution

探しまくると下記記事に当たった.
ksaluja15.github.io
keras.optimizers.SGDに追加の引数multipliersを取らせ,対象のlayerの学習率を定数倍するというもの.
このあたりがポイントみたい.

matched_layer = [x for x in self.lr_multipliers.keys() if x in p.name]
if matched_layer:
    new_lr = lr * self.lr_multipliers[matched_layer[0]]
else:
    new_lr = lr

こんな感じで使う.
これは全結合層だけ10^-3,それ以外は10^-4といったところ.

from LR_SGD import LR_SGD
...
LR_mult_dict = {}
LR_mult_dict['fc1'] = 10
LR_mult_dict['fc2'] = 10
LR_mult_dict['predictions'] = 10
optimizer = LR_SGD(lr=10e-4, multipliers=LR_mult_dict)

なお,model.save()load_model()したときは,読み込む際にエラーが出るので

  File ".\train.py", line 69, in main
    base_model = load_model('./dst/model.h5')
ValueError: Unknown optimizer: LR_SGD

このようにすること

model = load_model('./dst/model.h5', compile=False)
# model.compile(loss=categorical_crossentropy, optimizer=optimizer, metrics=['accuracy'])


辞書を受け取って学習率更新というポイントをAdamに適用した有志もいる.
erikbrorson.github.io

公式で対応してくれ~~

keras/tensorflowでdilated convolutionをstrides!=1で使う

keras(tensorflow backend)にはdilated convolutionが実装されている.
Convolutionalレイヤー - Keras Documentation

keras.layers.Conv2D(filters, kernel_size, strides=(1, 1), padding='valid', data_format=None, dilation_rate=(1, 1), activation=None, use_bias=...)

Conv2Dにdilation_rateとして引数を渡せばいいのだが,

dilation_rate: 整数か2つの整数からなるタプル/リストで,dilated convolutionで使われる膨張率を指定します. 現在,dilation_rate value != 1 とすると,strides value != 1を指定することはできません.
dilation_rate: an integer or tuple/list of 2 integers, specifying the dilation rate to use for dilated convolution. Can be a single integer to specify the same value for all spatial dimensions. Currently, specifying any dilation_rate value != 1 is incompatible with specifying any stride value != 1.

とあるようにdilated convを使うときにはstridesを1以外にできない.

inputs = Input(shape=(7*224, 7*224, 3))
x = Conv2D(64, (3, 3), strides=(7, 7), padding='same', activation='relu', dilation_rate=(2, 2),
            kernel_initializer='he_normal')(inputs)
...

上記のようにムリヤリ使おうとしても,もちろんエラーが出る.

  File ".\train.py", line 151, in <module>
    main()
  File ".\train.py", line 77, in main
    kernel_initializer='he_normal')(inputs)
  File "C:\Python36\lib\site-packages\keras\engine\base_layer.py", line 457, in __call__
    output = self.call(inputs, **kwargs)
  File "C:\Python36\lib\site-packages\keras\layers\convolutional.py", line 168, in call
    dilation_rate=self.dilation_rate)
  File "C:\Python36\lib\site-packages\keras\backend\tensorflow_backend.py", line 3565, in conv2d
    data_format=tf_data_format)
  File "C:\Python36\lib\site-packages\tensorflow\python\ops\nn_ops.py", line 779, in convolution
    data_format=data_format)
  File "C:\Python36\lib\site-packages\tensorflow\python\ops\nn_ops.py", line 842, in __init__
    num_spatial_dims, strides, dilation_rate)
  File "C:\Python36\lib\site-packages\tensorflow\python\ops\nn_ops.py", line 641, in _get_strides_and_dilation_rate
    "strides > 1 not supported in conjunction with dilation_rate > 1")

エラーを見て"C:\Python36\lib\site-packages\tensorflow\python\ops\nn_ops.py"(パスは人による:仮想環境を使うべき...)の
関数_get_strides_and_dilation_rate()内,下記の箇所を書き換えValueErrorが出ないようにした.

  if np.any(strides > 1) and np.any(dilation_rate > 1):
    # raise ValueError(
    #     "strides > 1 not supported in conjunction with dilation_rate > 1")
    print("strides > 1 not supported in conjunction with dilation_rate > 1")

これで上記のdilation_rate!=1, strides!=1のconv層を作成できるようになった.
あまり使い所はないかもしれないが,どうしても等間隔でdilated convをしたいときにどうぞ.

dilated conv自体は下記記事を参考にさせて頂いた.
joisino.hatenablog.com

matplotlibでTcl_AsyncDelete: async handler deleted by the wrong threadが出たときのトラブルシューティング

発生したエラー

前回記事で紹介したコールバックを使用すると学習途中に下記エラーが発生した.
catdance124.hatenablog.jp

Epoch 4/50
138/590 [======>.......................] - ETA: 8:02 - loss: 0.8965 - acc: 0.7292Exception ignored in: <bound method Image.__del__ of <tkinter.PhotoImage object at 0x00000235FB7D5198>>
Traceback (most recent call last):
  File "C:\Python36\lib\tkinter\__init__.py", line 3504, in __del__
    self.tk.call('image', 'delete', self.name)
RuntimeError: main thread is not in main loop
Exception ignored in: <bound method Image.__del__ of <tkinter.PhotoImage object at 0x00000235FB8F4F28>>
Traceback (most recent call last):
  File "C:\Python36\lib\tkinter\__init__.py", line 3504, in __del__
    self.tk.call('image', 'delete', self.name)
RuntimeError: main thread is not in main loop
Exception ignored in: <bound method Image.__del__ of <tkinter.PhotoImage object at 0x00000235FBAF2C18>>
Traceback (most recent call last):
  File "C:\Python36\lib\tkinter\__init__.py", line 3504, in __del__
    self.tk.call('image', 'delete', self.name)
RuntimeError: main thread is not in main loop
Tcl_AsyncDelete: async handler deleted by the wrong thread

解決策

下記の対応で解決する.

import matplotlib  # <--追記
matplotlib.use('Agg')  # <--追記
from matplotlib import pyplot as plt
...

なお,公開したgistは修正済み.

詳細

コールバックは繰り返し呼び出されるためfigureは多く作成され,下記warningが発生していた.

RuntimeWarning: More than 20 figures have been opened. Figures created through the pyplot interface 
(matplotlib.pyplot.figure) are retained until explicitly closed and may consume too much memory.
 (To control this warning, see the rcParam figure.max_open_warning).

そのため,下記記事を参考にplt.close()をコードの最後に仕込んでいた.
xartaky.hatenablog.jp
しかし,plt.close()を追記すると表題のエラーが発生した.
plt.show()をせずに(plt.savefig()とか)closeしようとするとこのエラーが発生するらしい.

pyplot maintains references to the opened figures to make show work, but this will cause memory leaks unless the figures are properly closed

matplotlib.org

エポック終了時に学習曲線図を保存するコールバックを作成(keras)

今回したこと

エポック終了時にそれまでの学習曲線を図として保存するコールバックを作成した.
通常はkeras.callbacks.History()を使用し,学習が終わってから1度のみhistoryを取得するが,途中で学習を止めた際にはhistoryが取得できないのでコールバックを自作した.

作成したコールバックは下記gistで公開している.
https://gist.github.com/catdance124/0976c5dbacdaeeaa7a6ac852c1f59cff

使う際には下記のように

from plot_history import PlotHistory
dir_name = './dst'
title = f'{model_name}_{optimizer_name}'
ph = PlotHistory(save_interval=5, dir_name=dir_name, csv_output=True, title=title)
cbs = [ph]
model.fit_generator(
    generator=train_generator,
    steps_per_epoch=train_datagen.steps_per_epoch,
    epochs=args.epochs,
    callbacks=cbs
)

上記だと
./dst/配下に下記のファイルが5エポックごとに保存される.

f:id:catdance124:20190919193530p:plain
実際に作成された学習曲線図
がっつり過学習しているが今回はその話はしない

コード説明

大まかに2つの要素からなる

  • 渡されたhistory(dic)をmatplotlibで画像化するplot_history関数
  • コールバック用のPlotHistoryクラス

plot_history関数

この関数はコールバック専用ではなく,通常の学習で得られるhistoryを渡しても描画できるように作成した.
辞書形式のhistoryを受け取り,trainのacc/loss,valがあればval_acc/val_lossをplotする.
オプションとして受け取ったhistorycsvアウトプットできる.後からhistoryを見たいときに便利.
下記は工夫点のみに触れるので全体のコードは載せていない.

def plot_history(history, begin_epoch=1, dir_name=None, csv_output=True, title='learning_curve'):
    # plot init settings
    val_exist = 'val_acc' in history.keys()
    plt.figure(figsize=(18, 7))
    plt.suptitle(title, fontsize=16)

受け取ったhistoryにvalについての情報があるかをval_existに格納しておく.
今後val_existでval_acc/val_lossを描画するかどうかを判断する.
plt.suptitle()を用いて全体としてのグラフタイトルを表示する.

    # plot accuracy settings
    plt.subplot(121)
    plt.title(f'model accuracy')
    plt.xlabel('epoch')
    plt.ylabel('accuracy')
    plt.gca().get_xaxis().set_major_locator(ticker.MaxNLocator(integer=True))
    # plot accuracy
    plt.plot(list(range(begin_epoch+1, len(history['acc'][begin_epoch:])+1)), history['acc'][begin_epoch:])
    if val_exist:
        plt.plot(list(range(begin_epoch+1, len(history['val_acc'][begin_epoch:])+1)), history['val_acc'][begin_epoch:])
        plt.legend(['acc', 'val_acc'], loc='lower right')
    else:
        plt.legend(['acc'], loc='lower right')
    
    # plot loss settings
    plt.subplot(122)
    plt.title(f'model loss')
    plt.xlabel('epoch')
    plt.ylabel('loss')
    plt.gca().get_xaxis().set_major_locator(ticker.MaxNLocator(integer=True))
    # plot loss
    plt.plot(list(range(begin_epoch+1, len(history['loss'][begin_epoch:])+1)), history['loss'][begin_epoch:])
    if val_exist:
        plt.plot(list(range(begin_epoch+1, len(history['val_loss'][begin_epoch:])+1)), history['val_loss'][begin_epoch:])
        plt.legend(['loss', 'val_loss'], loc='upper right')
    else:
        plt.legend(['loss'], loc='upper right')

ここはacc/lossでほとんど同じコード.valがあればvalも描画する.
plt.gca().get_xaxis().set_major_locator(ticker.MaxNLocator(integer=True))の部分でepoch軸は整数のみを取るようにしている.

    # show or save?
    if dir_name is None:
        plt.show()
    else:
        plt.savefig(f'{dir_name}/learning_curve.png')
        if csv_output:
            values = []
            for key in history.keys():
                values.append(history[key])
            values = np.array(values)
            with open(f'./{dir_name}/history.csv', 'w') as f_handle:
                writer = csv.writer(f_handle, lineterminator="\n")
                writer.writerows([history.keys()])  # header
                np.savetxt(f_handle, values.T, fmt="%.6f", delimiter=',')
    plt.close()

出力ディレクトリが指定されていれば画像として出力する.
更にcsv_outputの指定があればcsvも出力する.

PlotHistoryクラス

keras.callbacks.Callbackを継承し,epoch_endにplot_history()を呼び出すようにする.
下記も工夫点のみに触れ,全体のコードは載せていない.

class PlotHistory(Callback):
    def __init__(self, save_interval=1, dir_name='./', csv_output=False, title=''):

    def on_train_begin(self, logs=None):
        self.history = {}
        self.history['loss'] = []
        self.history['acc'] = []
        self.do_validation = self.params['do_validation']
        if self.do_validation:
            self.history['val_loss'] = []
            self.history['val_acc'] = []

学習開始時にloss/acc | val_loss/val_accの空リストを持っておく.
また,valが渡されるかどうかはself.params['do_validation']で判断することができる.

    def on_epoch_end(self, epoch, logs=None):
        self.history['loss'].append(logs.get('loss'))
        self.history['acc'].append(logs.get('acc'))
        if self.do_validation:
            self.history['val_loss'].append(logs.get('val_loss'))
            self.history['val_acc'].append(logs.get('val_acc'))
        if (epoch-1) % self.interval == 0:
            plot_history(history=self.history, dir_name=self.dir_name, csv_output=self.csv_output, title=self.title)

    def on_train_end(self, logs=None):
        plot_history(history=self.history, dir_name=self.dir_name, csv_output=self.csv_output, title=self.title)

毎エポックの終わりon_epoch_endでそのエポックのloss/ accをリストにappendしていく.
指定されたintervalでplot_history()を呼び出すようにした.

終わりに

今回はエポック終了時に学習曲線図を保存するコールバックを作成した.
keras.callbacks.Callbackを継承すればエポック終わり・バッチ学習終わりなど好きなタイミングで処理を行うことができる.
コールバックとして実装しておけば学習モデル・タスクを問わず使い回せることが多いのでとても便利だと思う.
実装したコードはここに
https://gist.github.com/catdance124/0976c5dbacdaeeaa7a6ac852c1f59cff

参考にしたサイト

qiita.com
minus9d.hatenablog.com
keras.io