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
実際の動き
スマートフォンからアクセスした画面はこんな感じ
緯度経度と選択された経路が表示されている(
バスの経路をイメージ).
構成
サーバ側は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接続でなければ取得できない
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を描画できるAPIのpythonラッパーが公開されていた.
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)); }
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
エポック終了時に学習曲線図を保存するコールバックを作成(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エポックごとに保存される.
がっつり過学習しているが今回はその話はしない
コード説明
大まかに2つの要素からなる
plot_history関数
この関数はコールバック専用ではなく,通常の学習で得られるhistoryを渡しても描画できるように作成した.
辞書形式のhistoryを受け取り,trainのacc/loss,valがあればval_acc/val_lossをplotする.
オプションとして受け取ったhistoryをcsvアウトプットできる.後から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()
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