20201227のPythonに関する記事は30件です。

【Python】配列生成(初期化)時間まとめ!!!

まとめ!!!

最初に結果をまとめておこう.いわゆる「時間のない人のための」というやつである.かなり見やすくまとまっていると自負している.

1d配列

Code Mean Stdev. Runs Loops
np.empty(N) 1.76 µs 32 ns 7 runs 1000000
np.zeros(N) 11.3 ms 453 µs 7 runs 100
np.ones(N) 14.9 ms 182 µs 7 runs 100
np.zeros_like(np.empty(N)) 19.9 ms 320 µs 7 runs 100
np.ones_like(np.empty(N)) 20.8 ms 931 µs 7 runs 10
[None] * N 35.9 ms 418 µs 7 runs 10
[0] * N 35.2 ms 489 µs 7 runs 10
[None for i in range(N)] 417 ms 77.7 ms 7 runs 1
[0 for i in range(N)] 375 ms 15.1 ms 7 runs 1

2d配列

Code Mean Stdev. Runs Loops
np.empty([M,M]) 3.58 µs 157 ns 7 runs 100000
np.zeros([M,M]) 3.66 µs 51.5 ns 7 runs 100000
[[None] * M] * M 37.4 µs 1.08 µs 7 runs 10000
[[0] * M] * M 37.4 µs 388 ns 7 runs 10000
np.ones([M,M]) 378 ms 5.46 ms 7 runs 1
np.zeros_like(np.empty([M,M])) 375 ms 3.22 ms 7 runs 1
np.ones_like(np.empty([M,M])) 384 ms 7.62 ms 7 runs 1
[[None for j in range(M)] for i in range(M)] 3.83 s 37.5 ms 7 runs 1
[[0 for j in range(M)] for i in range(M)] 3.86 s 61.6 ms 7 runs 1

さて,これらの詳細を見ていこう.

0 準備

型の確認.

type(None), type(0)
(NoneType, int)

適切なパラメータを設定する.

N = int(1e7)
M = int(1e4)

インポートも一応計測する.

%%timeit
import numpy as np
#112 ns ± 0.579 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

これはナノ秒なので考慮しなくて良い.

1 実行

1d配列

%timeit np.empty(N)
%timeit np.zeros(N)
%timeit np.ones(N)
%timeit np.zeros_like(np.empty(N))
%timeit np.ones_like(np.empty(N))
%timeit [None] * N
%timeit [0] * N
%timeit [None for i in range(N)]
%timeit [0 for i in range(N)]
'''
1.76 µs ± 32 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
11.3 ms ± 453 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
14.9 ms ± 182 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
19.9 ms ± 320 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
20.8 ms ± 931 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
35.9 ms ± 418 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
35.2 ms ± 489 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
417 ms ± 77.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
375 ms ± 15.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
'''

np.empty(N)が激烈に速い.ただし何が出力されるかその時々で異なるので速度は一定しないことに注意.100 μs ほどになることもあるが,それでも速い.
また,0 と None は実行する順序を入れ替えたり%%timeitで独立のセルで実行したりすると,速度が逆転することがある.ということはどちらを使ってもほぼ同じ速度と見て差し支えない.

2d配列

%timeit np.empty([M,M])
%timeit np.zeros([M,M])
%timeit np.ones([M,M])
%timeit np.zeros_like(np.empty([M,M]))
%timeit np.ones_like(np.empty([M,M]))
%timeit [[None] * M] * M
%timeit [[0] * M] * M
%timeit [[None for j in range(M)] for i in range(M)]
%timeit [[0 for j in range(M)] for i in range(M)]
'''
3.58 µs ± 157 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
3.66 µs ± 51.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
378 ms ± 5.46 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
375 ms ± 3.22 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
384 ms ± 7.62 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
37.4 µs ± 1.08 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
37.4 µs ± 388 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
3.83 s ± 37.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
3.86 s ± 61.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
'''

やはりnp.empty([M,M])が速いがnp.zeros([M,M])も負けていない.意外に掛け算も善戦している.for 文は言わずもがな遅く,書き方の工夫もまだできるかもしれないが,初期化には使えない.

2 結果(表にまとめる)

1d配列

Code Mean Stdev. Runs Loops
np.empty(N) 1.76 µs 32 ns 7 runs 1000000
np.zeros(N) 11.3 ms 453 µs 7 runs 100
np.ones(N) 14.9 ms 182 µs 7 runs 100
np.zeros_like(np.empty(N)) 19.9 ms 320 µs 7 runs 100
np.ones_like(np.empty(N)) 20.8 ms 931 µs 7 runs 10
[None] * N 35.9 ms 418 µs 7 runs 10
[0] * N 35.2 ms 489 µs 7 runs 10
[None for i in range(N)] 417 ms 77.7 ms 7 runs 1
[0 for i in range(N)] 375 ms 15.1 ms 7 runs 1

2d配列

Code Mean Stdev. Runs Loops
np.empty([M,M]) 3.58 µs 157 ns 7 runs 100000
np.zeros([M,M]) 3.66 µs 51.5 ns 7 runs 100000
[[None] * M] * M 37.4 µs 1.08 µs 7 runs 10000
[[0] * M] * M 37.4 µs 388 ns 7 runs 10000
np.ones([M,M]) 378 ms 5.46 ms 7 runs 1
np.zeros_like(np.empty([M,M])) 375 ms 3.22 ms 7 runs 1
np.ones_like(np.empty([M,M])) 384 ms 7.62 ms 7 runs 1
[[None for j in range(M)] for i in range(M)] 3.83 s 37.5 ms 7 runs 1
[[0 for j in range(M)] for i in range(M)] 3.86 s 61.6 ms 7 runs 1

3 結論

配列生成だけならnp.empty(),初期化するならnp.zeros()が速い.

4 おまけ

np.empty_like も速い.

%timeit np.empty_like(np.empty(N))
%timeit np.empty_like(np.empty([M,M]))
'''
3.24 µs ± 59.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
7.1 µs ± 108 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
'''

参考記事

numpy.empty — NumPy v1.19 Manual
Notes: empty, unlike zeros, does not set the array values to zero, and may therefore be marginally faster. On the other hand, it requires the user to manually set all the values in the array, and should be used with caution.
とある.
【NumPy入門 np.empty】要素を初期化せずに新しい配列を作る | 侍エンジニア塾ブログ(Samurai Blog) - プログラミング入門者向けサイト
【NumPy入門】配列の生成方法(1次元、2次元、高速化など) | 西住工房
NumPyで全要素を同じ値で初期化した配列ndarrayを生成 | note.nkmk.me
NumPyで空の配列ndarrayを生成するemptyとempty_like | note.nkmk.me
未初期化の配列を生成するnumpy.empty関数の使い方 - DeepAge

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

macOS Big SurでPythonをはじめてみる

  • はじめる人は、Unix系OSの基本的な知識はある
  • Pythonははじめてで、知識ゼロ
  • 環境はAppleのiMac(mid 2020、x86_64の最後のiMac??)、macOS Big Surにアップデート済み

現状確認

iMacにはおそらくPythonはプリインストールされていると思うので、ターミナルを起動して現状確認してみる。

% which python
/usr/bin/python

Pythonのコマンドがあった。
どう使うのかな?

% man python

PYTHON(1)                                                                                                                                                    PYTHON(1)

NAME
       python - an interpreted, interactive, object-oriented programming language

SYNOPSIS
       python [ -B ] [ -d ] [ -E ] [ -h ] [ -i ] [ -m module-name ]
              [ -O ] [ -OO ] [ -R ] [ -Q argument ] [ -s ] [ -S ] [ -t ] [ -u ]
              [ -v ] [ -V ] [ -W argument ] [ -x ] [ -3 ] [ -?  ]
              [ -c command | script | - ] [ arguments ]

Vオプションで、バージョンが確認できるみたいだ。

% python -V
Python 2.7.16

インストールされているPythonは、2.7.16みたいだ。
今の最新バージョンはいくつなのかな?
manで下の方を見ると、Webサイトがいくつか書いてある。

INTERNET RESOURCES
       Main website:  https://www.python.org/

Pythonのメイン・ウェブサイトを見てみると、2020/12/21にバージョン3.8.7がでているみたい。
Bic Surにはずいぶんと古いPythonがインストールされているみたいだ。
Pythonのメイン・ウェブサイトを眺めていると、Pythonのバージョン2系とPythonバージョン3系には、大きな?違いがあるようだ。
2系から3系へのポーティング・ガイドもあるので、おそらくは言語仕様の非互換を伴うバージョンアップらしい。

現状確認ができたので、今日はここまで。
次は以下を満たす参考書を探そうかな。

  • Pythonのバージョン2系とバージョン3系の解説があること
  • バージョン3へのアップグレードについて記述があること
  • プログラム経験はあるけど、Pythonは初心者という人向けであること
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[エラー]IOPub message rate exceeded. The notebook server will temporarily stop sending output to the client in order to avoid crashing it.

エラー

IOPub message rate exceeded.
The notebook server will temporarily stop sending output
to the client in order to avoid crashing it.
To change this limit, set the config variable
`--NotebookApp.iopub_msg_rate_limit`.

原因

iopun_data_rate_limit の値がとても小さく設定されているため。

解決策

設定されている値を変更する

jupyter notebook --NotebookApp.iopub_data_rate_limit=10000000000

追記

最新版では修正されている。

参考

https://github.com/jupyter/notebook/issues/2287

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

E資格問題集を解いてみた[第1章5問目]

初投稿です。

JDLA(日本ディープラーニング協会)が主催するE資格の勉強をしています。
対策には『徹底攻略ディープラーニングE資格エンジニア問題集』を使っています。
しかし問題を解いていると、誤植などが散見されます。
E資格の対策本としては唯一の書籍なので、これから勉強する人のためにも、自分が気付いた誤りを1記事当たり1問で書いていこうと思います。
間違いがあればご指摘ください。

まずはテキストの問題文と解答を見ていきます。
次に自分が解いてみた結果と投稿主の解答を掲載します。

第1章 5問目

テキストの問題文

次の数式の(ア)~(エ)に当てはまる選択肢をそれぞれ1つずつ選べ。ただし、同じ選択肢が2ヵ所以上に当てはまることもある。

行列

A=
\begin{pmatrix}
1 & 0 & 0\\
0 & 1 & 2
\end{pmatrix}

を特異値分解し、$A=U \Sigma V^T$の形で表すとき、以下のようになる。

\Sigma =
\begin{pmatrix}
(ア) & 0 & 0\\
0 & (イ) & 0
\end{pmatrix}
\\
U=
\begin{pmatrix}
0 & 1\\
(ウ) & 0
\end{pmatrix}
\\
V=
\begin{pmatrix}
0 & 1 & 0\\
\frac{1}{\sqrt{5}} & 0 & -\frac{2}{\sqrt{5}}\\
\frac{2}{\sqrt{5}} & 0 & (エ)
\end{pmatrix}

A. -1
B. 0
C. 1
D. 2
E. -$\sqrt{5}$
F. $\sqrt{5}$
G. $-\frac{1}{\sqrt{5}}$
H. $\frac{1}{\sqrt{5}}$

テキストの解答

(ア)C、(イ)F、(ウ)C、(エ)H

まず、m×n長方行列$A$の特異値、すなわち$\Sigma$の$(i,i)$成分(i=1,...,min{m,n})には$A ^T A$の固有値の正の平方根が降順(大きい順)に並びます。

A^T A=
\begin{pmatrix}
1 & 0\\
0 & 5
\end{pmatrix}

なので、その固有値は、

det(\lambda I - A ^T A) = det
\begin{pmatrix}
\lambda -1 & 0\\
0 & \lambda -5
\end{pmatrix}
= 0

を解いて、$\lambda = 1, 5$です。$A$の特異値は$\sigma = 1, \sqrt{5}$なので、これを降順に並べれば(ア)は1、(イ)は$\sqrt{5}$です(ア=C、イ=F)。
ここで、$U$の第一列は$A^T A$の固有値5に対応する固有ベクトル$u=(()u_x, u_y)^T$のうち、大きさが1のものです。したがって、

\begin{pmatrix}
1 & 0\\
0 & 5
\end{pmatrix}
\begin{pmatrix}
u_x\\
u_y
\end{pmatrix}
=5
\begin{pmatrix}
u_x\\
u_y
\end{pmatrix}

を解いて、$u_x = 0$、$u_y$は任意の実数という解が得られます。ここで、$u$の大きさが1となるためには、$u_y = \pm1$でなくてはならないことがわかります。
また、$V$の各列は$A^T A$の固有ベクトルですが、これらは正規直交性を満たす必要があります。したがって、$v_1 = (0, 1/\sqrt{5}, 2/\sqrt{5})^T$、$v_3 = (0, -2/\sqrt{5}, v_z)^T$と置けば、それらの直交性から$v_1^T \cdot v_3 = 0$になります。よって、

0 \cdot 0 + \frac{1}{\sqrt{5}} \cdot \left( -\frac{2}{\sqrt{5}} \right) + \frac{2}{\sqrt{5}} v_z = 0

となり、(エ)$v_z = 1/\sqrt{5}$が得られます(エ=H)。
最後に、(ウ)に当てはまる値が1か-1かを代入によって確かめ、1であると特定すればよいでしょう。(ウ=C)。

自分で解いてみた

この問題を自分で解いてみたところ、テキストの解答と選択肢が違いました。
自分の計算が間違っている可能性も十分にあるため、テキストの解答にある選択肢をそれぞれ$\Sigma$、$U$、$V$に代入して$U \Sigma V^T$を計算してみたところ、以下のようになりました。

\begin{eqnarray}
U \Sigma V^T
&=&
\begin{pmatrix}
0 & 1\\
1 & 0
\end{pmatrix}
\begin{pmatrix}
1 & 0 & 0\\
0 & \sqrt{5} & 0
\end{pmatrix}
\begin{pmatrix}
0 & \frac{1}{\sqrt{5}} & \frac{2}{\sqrt{5}}\\
1 & 0 & 0\\
0 & -\frac{2}{\sqrt{5}} & \frac{1}{\sqrt{5}}
\end{pmatrix}\\
&=&
\begin{pmatrix}
0 & \sqrt{5} & 0\\
1 & 0 & 0
\end{pmatrix}
\begin{pmatrix}
0 & \frac{1}{\sqrt{5}} & \frac{2}{\sqrt{5}}\\
1 & 0 & 0\\
0 & -\frac{2}{\sqrt{5}} & \frac{1}{\sqrt{5}}
\end{pmatrix}\\
&=&
\begin{pmatrix}
\sqrt{5} & 0 & 0\\
0 & -\frac{1}{\sqrt{5}} & \frac{2}{\sqrt{5}}
\end{pmatrix}
\end{eqnarray}

これは$A$とは成分が異なります。
解答の中で$A$の特異値を求めていますが、「(ア)は1、(イ)は$\sqrt{5}$」としたことが原因でしょう。

改めて、一旦$U \Sigma V^T$を未知数が存在する状態で計算してみましょう。
(ア)をw、(イ)をx、(ウ)をy、(エ)をzとおくことにします。すると、

\begin{eqnarray}
U \Sigma V^T
&=&
\begin{pmatrix}
0 & 1\\
y & 0
\end{pmatrix}
\begin{pmatrix}
w & 0 & 0\\
0 & x & 0
\end{pmatrix}
\begin{pmatrix}
0 & \frac{1}{\sqrt{5}} & \frac{2}{\sqrt{5}}\\
1 & 0 & 0\\
0 & -\frac{2}{\sqrt{5}} & z
\end{pmatrix}\\
&=&
\begin{pmatrix}
0 & x & 0\\
wy & 0 & 0
\end{pmatrix}
\begin{pmatrix}
0 & \frac{1}{\sqrt{5}} & \frac{2}{\sqrt{5}}\\
1 & 0 & 0\\
0 & -\frac{2}{\sqrt{5}} & z
\end{pmatrix}\\
&=&
\begin{pmatrix}
x & 0 & 0\\
0 & \frac{wy}{\sqrt{5}} & \frac{2wy}{\sqrt{5}}
\end{pmatrix}
\end{eqnarray}

これと$A$の$(1,1)$成分を見比べると、$x=1$であることが分かります。$A$の特異値は1と$\sqrt{5}$だったので、$w=\sqrt{5}$であることが分かります。$(2,2)$成分の比較により、$y=1$であることが分かります。特異値分解の性質より$V$は正規直交行列なので、1列目のベクトル$v_1$と3列目のベクトル$v_3$の内積を取ると、

v_1 \cdot v_3
=
\begin{pmatrix}
0\\
\frac{1}{\sqrt{5}}\\
\frac{2}{\sqrt{5}}
\end{pmatrix}
\cdot
\begin{pmatrix}
0\\
-\frac{2}{\sqrt{5}}\\
z
\end{pmatrix}
=
0 \cdot 0 - \frac{1}{\sqrt{5}}\ \cdot \frac{2}{\sqrt{5}} + \frac{2}{\sqrt{5}} z
=
-\frac{2}{5} + \frac{2}{\sqrt{5}} z
=
0

これを解いて、$z = 1/\sqrt{5}$が分かります。以上より、投稿主の解答は以下の通りです。

投稿主の解答

(ア)F、(イ)C、(ウ)C、(エ)H

余談ですが、多分$u=(()u_x, u_y)^T$もただの誤植で、正しくは$u=(u_x, u_y)^T$だと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

複数のバージョンのCUDAを同一環境で扱う方法

大学の研究で機械学習をやっていて、同一環境で複数のバージョンのCUDAを扱うにはどうすればよいのか困ったことがあったのでそれについて書いていく。

前提

Windows10環境においてPyTorchをGPUで動かす。
PyTorchのバージョンによって必要なCUDAのバージョンが異なる。
これに伴って、複数のバージョンのCUDAをインストールした。

対象者

PyTorchをGPUで動かしたことがあり、cuDNNやCUDAのインストール方法やパスの通し方は大体知っているが、複数のバージョンの扱い方がわからない方。

方法

結論から言うと、ユーザーは何もしなくてよい。
パスさえ通っていれば、PyTorchのどのバージョンを動かすかによって必要なCUDAを自動で認識してくれる。

where nvccとコマンドプロンプトに入力した時に

C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.2\bin\nvcc.exe
C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.0\bin\nvcc.exe
C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.0\bin\nvcc.exe

というように、使うバージョンのCUDAのパスがあれば問題ない。
もしない場合は、(こんなことはまずないはずだが、もし自分でわからずつついている内に消してしまうことはあるかも?)システム環境変数のPathに以下のようなパスを追加すればよい。(バージョン10.2の場合)

C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.2\bin
C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.2\libnvvp

ちなみに、CUDAをインストールしたフォルダはこのようになっている。
CUDAFolder.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画像内のA4プリントを切り出す

画像内のA4プリントを切り出す

写真に存在するプリントを真上視点から切り抜いた画像に変換する。
image.png
上の写真を下の写真に変換できる。
image.png

コードに関する注意

  • 縦長のA4ファイルに変換するので横長の場合はxとyを書き換えること。
  • xが短辺の長さになっているので適度に書き換えること。

コード

cutting_a4.py
#カラー画像を受け取って紙を切り取って返す
def cutting_paper(img):
    x = 2000 #切り取り後のx座標
    y = int(x*1.415)
    img_gray = gray(img)
    #2値化
    ret,img_th1  = cv2.threshold(img_gray,220,255,cv2.THRESH_TOZERO_INV)
    img_not = cv2.bitwise_not(img_th1)
    ret,img_th2  = cv2.threshold(img_not,0,255, cv2.THRESH_BINARY | cv2.THRESH_OTSU )
    #輪郭抽出
    contours, hierarchy = cv2.findContours(img_th2, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

    #2番目にでかい四角が紙の輪郭なのでpaper_tapを取り出す
    pic_tap = None #画像の輪郭
    paper_tap = None #紙の輪郭
    for i, con in enumerate(contours):
        size = cv2.contourArea(con)
        if pic_tap is None:
            pic_tap = (con,size)
            continue
        if pic_tap[1] <= size:
            pic_tap = (con,size)
            continue
        elif paper_tap is None:
            paper_tap = (con,size)
        elif paper_tap[1] <= size:
            paper_tap = (con,size)

    #直線近似して描画
    epsilon = 0.1*cv2.arcLength(paper_tap[0],True)
    paper_corners= cv2.approxPolyDP(paper_tap[0],epsilon,True)#紙の頂点座標
    fix_con = np.array([[[0,0]],[[x,0]],[[x,y]],[[0,y]]], dtype="int32")#整形後のサイズ

    M = cv2.getPerspectiveTransform(np.float32(paper_corners),np.float32(fix_con))#変換行列の生成
    img_trans = cv2.warpPerspective(img,M,(x,y))#変換
    return img_trans

img = cv2.imread('input.png',-1)
cv2.imwrite("output.png", img)
plt.imshow(plt.imread("output.png"))
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webカメラから映像取得・画像撮影

はじめに

ラズパイ用に、USB接続のWebカメラをもらった。
今回はその動作確認とプログラムのメモ。

カメラ

使用するカメラはこちら。
Logicool Brio Ultra HD Pro Webcam

接続の確認

$ lsusb
Bus 002 Device006: ID 046d:085e Logitech, Inc.
$ ls /dev/video*
/dev/video0  /dev/video1  /dev/video2  /dev/video3

が追加された。

動作確認

1. guvcview

まずインストールしたのは guvcview というソフト。

$ sudo apt-get install guvcview

以下のコマンドで実行。

$ guvcview &

実行されたはいいものの、最初の画像取得後動かなかった。
動画もとってみたが上手く保存されず。

2. fswebcam

調べたが解決しないため諦めて、次は fswebcam というものをインストール。

$ sudo apt-get install fswebcam

実行方法は

$ fswebcam img.jpg

撮影した画像が /home/[ユーザー名]/img.jpg として保存される。

python上で動かす

以下のコードを実行。

import numpy as np
import cv2

cap = cv2.VideoCapture(0)

while(True):
    ret, frame = cap.read()
    cv2.imshow('frame',frame)

    key = cv2.waitKey(1)

    if key == ord('q'):
        break
    if key == ord('s'):
        path = "/home/kono/画像/カメラ画像/photo.jpg"
        cv2.imwrite(path,frame)

cap.release()
cv2.destroyAllWindows()

キーボードの s を押すと撮影、 q を押すと終了する。
撮影した画像は指定された場所に photo.jpg として保存される。

参考サイトはこちら。
【Raspberry Pi】webカメラを接続する方法

まとめ

ソフトウェア上よりも自分で書いたプログラムのほうが動作が安定するなんて。
硬貨識別の課題がんばる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】Selenium3から4へ上げたらWarningが出た【WebDriver】

はじめに

以前投稿したSeleniumの記事を書くための調査で私用PCのSeleniumを3から4に上げたんですが、それに伴ってcronで定期実行していた既存のSelenium3のログに大量のWarningが出るようになりました。

Warningなのでコード自体は正常終了するのですが、ログが無駄に長くなるのは嫌なのでSelenium4のソースコードを分析して自分のコードを修正し、全てのWarningを消すことに成功したのでここに備忘録を残しておきます。

DeprecationWarning: executable_path has been deprecated, please pass in a Service object

Selenium3では、ブラウザのドライバにPATHを通さない場合は以下のようにドライバを起動していました。

from selenium import webdriver

driver = webdriver.Firefox(executable_path="/usr/local/bin/geckodriver")
driver.get('https://www.google.com/')

ですが、Selenium4で同じことをしたいはドライバ起動時に直接executable_pathを渡すのではなく、以下のようにServiceオブジェクトにexecutable_pathを渡し、そのServiceオブジェクトを渡す必要があります。

from selenium import webdriver
from selenium.webdriver.firefox import service as fs

firefox_servie = fs.Service(executable_path="/usr/local/bin/geckodriver")
driver = webdriver.Firefox(service=firefox_servie)
driver.get('https://www.google.com/')

DeprecationWarning: service_log_path has been deprecated, please pass in a Service object

ログの出力先を指定する場合、executable_pathと同様service_log_pathServiceオブジェクトに渡す仕様になりました。ただ以下の2点に気をつけてください。

①キーワードの名前がservice_log_pathではなくlog_pathになる
②ブラウザドライバがPATHに通っていてもServiceオブジェクトにexecutable_pathを渡す必要がある。

なのでこの場合、上の4行目は以下のようになります。

firefox_servie = fs.Service(executable_path="/usr/local/bin/geckodriver", log_path='/my/log/path')

UserWarning: find_element_by_* commands are deprecated. Please use find_element() instead

メッセージの通りですが、find_element_by_*という名前の関数はdeprecatedになり、by_*の部分はfind_element()のキーワード変数byで指定するようになりました。

例えば、Selenium3の以下のコードがあったとします。
(ドライバの実行ファイルにはPATHが通っていることとします)

from selenium import webdriver

driver = webdriver.Firefox()
driver.get('https://www.google.com/')
element = driver.find_element_by_xpath("//input[@type='text']")
element.send_keys('Qiita')
element.submit()

これをSelenium4仕様にすると以下のようになります。

from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Firefox()
driver.get('https://www.google.com/')
element = driver.find_element(by=By.XPATH, value="//input[@type='text']")
element.send_keys('Qiita')
element.submit()

まとめ

find_elementの書き換えはかなり面倒なので、テストコードが大量にある場合はスクリプトを書いていい感じに自動化しないと大変そうですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】 配列内で一番近い値を取る index を返す関数

意外と詰まったし良いまとめがないので書く.

1 一番近い値の重複を考えない場合

1.1 最もシンプルなバージョン

探したい値が一つで,返したい index も一つの場合.これが最も簡単.探索されるデータは1次元配列を想定.

import numpy as np

def idx_of_the_nearest(data, value):
    idx = np.argmin(np.abs(np.array(data) - value))
    return idx

1.2 具体例

data = [1, 1 ,1 ,0.5 ,2 ,3 ,-1]
value =  0.8
n = idx_of_the_nearest(data, value)
n
#0

1.3 探したい値が複数ある場合

探したい値をリストで指定できるようにしたい.そのための関数が以下.

import numpy as np

def idx_of_the_nearest(data, value):
    if type(value) == float:
        print('value:', type(value))
        idx = np.argmin(np.abs(np.array(data) - value))
        #print(np.abs(np.array(data) - value))
        return idx
    if type(value) == list:
        print('value:', type(value))
        idx = [None]*len(value)
        for i in range(len(value)):
            idx[i] = np.argmin(np.abs(np.array(data) - value[i]))
            #idx[i] = [value[i], np.argmin(np.abs(np.array(data) - value[i]))] #としてもよい
            #print(np.abs(np.array(data) - value[i]))
        return idx

1.4 具体例

data = [1, 1 ,1 ,0.5 ,2 ,3 ,-1]
value =  [0.8,0.7]
n = idx_of_the_nearest(data, value)
n
'''Result
value: <class 'list'>
[0, 3]
'''

1.5 多次元に拡張

探索される対象のデータが多次元配列の場合に,多次元配列の index を返したい人はこちら.出力はタプルのリスト.np.unravel_index()なるものを使う.

import numpy as np

def idx_of_the_nearest(data, value):
    if type(value) == float:
        print('value:', type(value))
        idx = np.argmin(np.abs(np.array(data) - value))
        #print(np.abs(np.array(data) - value))
        return idx
    if type(value) == list:
        print('value:', type(value))
        idx = [None]*len(value)
        for i in range(len(value)):
            idx[i] = np.unravel_index(np.argmin(np.abs(np.array(data) - value[i])) , np.array(data).shape)
            #print(np.abs(np.array(data) - value[i]))
        return idx

1.6 具体例

data = [[1, 1 ,1 ,0.5] ,[2 ,3 ,-1,0]]
value = [0.8, 0.7, 2]
idx_of_the_nearest(data, value)
'''
value: <class 'list'>
[(0, 0), (0, 3), (1, 0)]
'''

2 最も近い値が複数ある場合

最もなのに「複数」とはどういうことだ,とは言わないでいただきたい.そういうことも数学ではままあるのである(今までもそういう具体例をわざと作っておいた.).

2.1 シンプルなバージョン

最もシンプルに添字を複数返したい方はこちら.1次元配列で探したい値が1つだけある場合.

import numpy as np

def indices_of_the_nearest(data, value):
    distance = np.abs(np.array(data) - value)
    indices = np.where(distance == np.min(distance))[0]
    return indices

この np.where() なるものが曲者で,意外と使いにくい.

ただ,シンプルなバージョンでは,具体例で見るようにこれが最適解である.[0]をつけることで arrayが返ってくる.

2.2 具体例

data = [1, 1 ,1 ,0.5 ,2 ,3 ,-1]
value = 0.8
indices_of_the_nearest(data, value)
'''
array([0, 1, 2])
'''

2.3 探したい値が複数ある場合(多次元でも使えるが改良の余地あり)

探す値も複数にしたいという「欲張り」な人はこちら.ただし改良の余地がある.

def indices_of_the_nearest(data, value):
    if type(value) == float:
        print('value:', type(value))
        distance = np.abs(np.array(data) - value)
        indices = np.where(distance == np.min(distance))
        #print(np.abs(np.array(data) - value))
        return indices
    if type(value) == list:
        print('value:', type(value))
        indices = [None]*len(value)
        for i in range(len(value)):
            distance = np.abs(np.array(data) - value[i])
            indices[i] = np.where(distance == np.min(distance))
            #print(np.abs(np.array(data) - value[i]))
        return indices

1.3 節を応用しただけ.

2.4 具体例

data = [[1, 1 ,1 ,0.5] ,[2 ,3 ,-1,0]]
value = [0.8,0.7]
indices_of_the_nearest(data, value)
'''
value: <class 'list'>
[(array([0, 0, 0]), array([0, 1, 2])), (array([0]), array([3]))]
'''

行番号と列番号が分離して出力されてわかりにくい(1つ目の要素の意味は[0][0]と[0][1]と[0][2]に一番近い値がありますよ,という意味).

どうしてもという人はこれを使えばよいが,今回の目的からしてあまり使う意味もない.data が1次元の場合は問題なかったのだが,これが更に3次元となってくるともっと見にくい.

data = [[[1, 1] ,[1 ,0.5]] ,[[2 ,3] ,[-1,0]]]
value = [0.8,0.7]
indices_of_the_nearest(data, value)
'''
value: <class 'list'>
[(array([0, 0, 0]), array([0, 0, 1]), array([0, 1, 0])),
 (array([0]), array([1]), array([1]))]
'''

リストの中の1つ目のタプルは[0][0][0], [0][0][1], [0][1][0] に一番近い値がありますよという意味.

これは array の中をそのまま読んでいるのでなくて,array([0, 0, 0])の i (i = 0, 1, 2, 3) 番目とarray([0, 0, 1])の i 番目とarray([0, 1, 0])の i 番目をつなげて読んでいる(リストの中の2つ目のタプルを合わせて見れば意味がわかる).

見にくい!

2.5 多次元に拡張(改良版)

というわけで改良しよう.

import numpy as np

def indices_of_the_nearest(data, value):
    if type(value) == float:
        print('value:', type(value))
        distance = np.abs(np.array(data) - value)
        indices = np.where(distance == np.min(distance))
        #print(np.abs(np.array(data) - value))
        return indices
    if type(value) == list:
        print('value:', type(value))
        indices = [None]*len(value)
        for i in range(len(value)):
            distance = np.abs(np.array(data) - value[i])
            indices[i] = np.array((np.where(distance == np.min(distance)))).T #Transpose
            #print(np.abs(np.array(data) - value[i]))
        return indices

何をやっているかというと,
indices[i] = np.array((np.where(distance == np.min(distance)))).T
と書き換えただけ.

2.6 具体例

data = [[1, 1 ,1 ,0.5] ,[2 ,3 ,-1,0]]
value = [0.8,0.7]
indices_of_the_nearest(data, value)
'''
value: <class 'list'>
[array([[0, 0],
        [0, 1],
        [0, 2]]),
 array([[0, 3]])]
'''
data = [[[1, 1] ,[1 ,0.5]] ,[[2 ,3] ,[-1,0]]]
value = [0.8,0.7]
indices_of_the_nearest(data, value)
'''
value: <class 'list'>
[array([[0, 0, 0],
        [0, 0, 1],
        [0, 1, 0]]),
 array([[0, 1, 1]])]
'''

見やすい!(出力から要素を取り出したい場合は 内包表記で for 文を回すなどすればできると思う)

3 まとめ

添字取り出し,意外と大変だったけど,どこかの誰かの役に立てれば.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】 配列内で一番近い値を取る index を返す関数まとめ

意外と詰まったし良いまとめがないので書く.

1 一番近い値の重複を考えない場合

1.1 最もシンプルなバージョン

探したい値が一つで,返したい index も一つの場合.これが最も簡単.探索されるデータは1次元配列を想定.

import numpy as np

def idx_of_the_nearest(data, value):
    idx = np.argmin(np.abs(np.array(data) - value))
    return idx

1.2 具体例

data = [1, 1 ,1 ,0.5 ,2 ,3 ,-1]
value =  0.8
n = idx_of_the_nearest(data, value)
n
#0

1.3 探したい値が複数ある場合

探したい値をリストで指定できるようにしたい.そのための関数が以下.

import numpy as np

def idx_of_the_nearest(data, value):
    print('value:', type(value))
    if type(value) == float:
        idx = np.argmin(np.abs(np.array(data) - value))
        #print(np.abs(np.array(data) - value))
        return idx
    if type(value) == list:
        idx = [None]*len(value)
        for i in range(len(value)):
            idx[i] = np.argmin(np.abs(np.array(data) - value[i]))
            #idx[i] = [value[i], np.argmin(np.abs(np.array(data) - value[i]))] #としてもよい
            #print(np.abs(np.array(data) - value[i]))
        return idx

1.4 具体例

data = [1, 1 ,1 ,0.5 ,2 ,3 ,-1]
value =  [0.8,0.7]
n = idx_of_the_nearest(data, value)
n
'''Result
value: <class 'list'>
[0, 3]
'''

1.5 多次元に拡張

探索される対象のデータが多次元配列の場合に,多次元配列の index を返したい人はこちら.出力はタプルのリスト.np.unravel_index()なるものを使う.

import numpy as np

def idx_of_the_nearest(data, value):
    print('value:', type(value))
    if type(value) == float:
        idx = np.argmin(np.abs(np.array(data) - value))
        #print(np.abs(np.array(data) - value))
        return idx
    if type(value) == list:
        idx = [None]*len(value)
        for i in range(len(value)):
            idx[i] = np.unravel_index(np.argmin(np.abs(np.array(data) - value[i])) , np.array(data).shape)
            #print(np.abs(np.array(data) - value[i]))
        return idx

1.6 具体例

data = [[1, 1 ,1 ,0.5] ,[2 ,3 ,-1,0]]
value = [0.8, 0.7, 2]
idx_of_the_nearest(data, value)
'''
value: <class 'list'>
[(0, 0), (0, 3), (1, 0)]
'''

2 最も近い値が複数ある場合

最もなのに「複数」とはどういうことだ,とは言わないでいただきたい.そういうことも数学ではままあるのである(今までもそういう具体例をわざと作っておいた.).

2.1 シンプルなバージョン

最もシンプルに添字を複数返したい方はこちら.1次元配列で探したい値が1つだけある場合.

import numpy as np

def indices_of_the_nearest(data, value):
    distance = np.abs(np.array(data) - value)
    indices = np.where(distance == np.min(distance))[0]
    return indices

この np.where() なるものが曲者で,意外と使いにくい.

ただ,シンプルなバージョンでは,具体例で見るようにこれが最適解である.[0]をつけることで arrayが返ってくる.

2.2 具体例

data = [1, 1 ,1 ,0.5 ,2 ,3 ,-1]
value = 0.8
indices_of_the_nearest(data, value)
'''
array([0, 1, 2])
'''

2.3 探したい値が複数ある場合(多次元でも使えるが改良の余地あり)

探す値も複数にしたいという「欲張り」な人はこちら.ただし改良の余地がある.

import numpy as np

def indices_of_the_nearest(data, value):
    print('value:', type(value))
    if type(value) == float:
        distance = np.abs(np.array(data) - value)
        indices = np.where(distance == np.min(distance))
        #print(np.abs(np.array(data) - value))
        return indices
    if type(value) == list:
        indices = [None]*len(value)
        for i in range(len(value)):
            distance = np.abs(np.array(data) - value[i])
            indices[i] = np.where(distance == np.min(distance))
            #print(np.abs(np.array(data) - value[i]))
        return indices

1.3 節を応用しただけ.

2.4 具体例

data = [[1, 1 ,1 ,0.5] ,[2 ,3 ,-1,0]]
value = [0.8,0.7]
indices_of_the_nearest(data, value)
'''
value: <class 'list'>
[(array([0, 0, 0]), array([0, 1, 2])), (array([0]), array([3]))]
'''

行番号と列番号が分離して出力されてわかりにくい(1つ目の要素の意味は[0][0]と[0][1]と[0][2]に一番近い値がありますよ,という意味).

どうしてもという人はこれを使えばよいが,今回の目的からしてあまり使う意味もない.data が1次元の場合は問題なかったのだが,これが更に3次元となってくるともっと見にくい.

data = [[[1, 1] ,[1 ,0.5]] ,[[2 ,3] ,[-1,0]]]
value = [0.8,0.7]
indices_of_the_nearest(data, value)
'''
value: <class 'list'>
[(array([0, 0, 0]), array([0, 0, 1]), array([0, 1, 0])),
 (array([0]), array([1]), array([1]))]
'''

リストの中の1つ目のタプルは[0][0][0], [0][0][1], [0][1][0] に一番近い値がありますよという意味.

これは array の中をそのまま読んでいるのでなくて,array([0, 0, 0])の i (i = 0, 1, 2, 3) 番目とarray([0, 0, 1])の i 番目とarray([0, 1, 0])の i 番目をつなげて読んでいる(リストの中の2つ目のタプルを合わせて見れば意味がわかる).

見にくい!

2.5 多次元に拡張(改良版)

というわけで改良しよう.

import numpy as np

def indices_of_the_nearest(data, value):
    print('value:', type(value))
    if type(value) == float:
        distance = np.abs(np.array(data) - value)
        indices = np.where(distance == np.min(distance))
        #print(np.abs(np.array(data) - value))
        return indices
    if type(value) == list:
        indices = [None]*len(value)
        for i in range(len(value)):
            distance = np.abs(np.array(data) - value[i])
            indices[i] = np.array((np.where(distance == np.min(distance)))).T #Transpose
            #print(np.abs(np.array(data) - value[i]))
        return indices

何をやっているかというと,
indices[i] = np.array((np.where(distance == np.min(distance)))).T
と書き換えただけ.

2.6 具体例

data = [[1, 1 ,1 ,0.5] ,[2 ,3 ,-1,0]]
value = [0.8,0.7]
indices_of_the_nearest(data, value)
'''
value: <class 'list'>
[array([[0, 0],
        [0, 1],
        [0, 2]]),
 array([[0, 3]])]
'''
data = [[[1, 1] ,[1 ,0.5]] ,[[2 ,3] ,[-1,0]]]
value = [0.8,0.7]
indices_of_the_nearest(data, value)
'''
value: <class 'list'>
[array([[0, 0, 0],
        [0, 0, 1],
        [0, 1, 0]]),
 array([[0, 1, 1]])]
'''

見やすい!(出力から要素を取り出したい場合は 内包表記で for 文を回すなどすればできると思う)

3 まとめ

添字取り出し,意外と大変だったけど,いつかどこかの誰かの役に立てれば.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DjangoにおけるCookie管理の仕方

こんな感じらしいです。
後で読む―

公式ドキュメント
https://docs.djangoproject.com/ja/2.1/topics/http/sessions/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Djangoで認証、認可処理を作ろうとしてる

まず、SpringSecurityと違う所

SpringSecurityはメソッドチェーンを使ったマッチャーでアクセスURLを制御しますが、
Djangoにはフィルター制御がないようです。

なので、認証、認可処理を独自に実装するか、ライブラリを使う必要があります。

Djangoの認可方式

取りあえずこの方が紹介してる方法だと
リクエストに、ユーザーオブジェクトがもう入っちゃってます。

https://www.slideshare.net/hirokiky/django-pyconjp2017

つまり、Viewでいったんアクセスを受け付けて、「正しいユーザーオブジェクト、ロールじゃない、アクセスを拒否るよー」
という形式ですね

よさそな形式

個人的にはトークンを発行した方がいいかもしれないですね。

1.認証トークンとロールをCookieに保存する
2.トランザクションテーブルに保存したトークンとCookieを比較(キャッシュ方式はデバッグしづらい><)
3.不許可ページなら、エラーメッセージとか表示

みたいな認可手順が良さそうです。

個人的にSpringSecurityのコードと比較した時、
Viewごとにコードが書く、Djangoの認証の方が保守性が高いかな、とは思いました。

こんな感じの設計

設計はこんな感じ。
あとは実装できそうか調べるかなー、という所

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オフライン環境でpyenv、pip、pythonを使用できるまでの完全ガイドブック

対象者

前提知識無しで、ネットワークに接続させたくないマシンに、様々なライブラリを導入し、pythonファイルを自由に実行できるようにしたい方へ

はじめに

この記事を書こうと思った理由は、僕のように前提知識がほぼ皆無の方に向けられた、0からオフライン環境で自由にpythonファイルを実行するまでまとめられた資料が無いように感じ、(部分的なものは数多くあります)
オンライン環境ではかなり簡単に環境構築できたものの、オフライン環境ではオンライン環境程、単純ではなく、osの理解やその他諸々の理解が必要で、環境構築するまで2~3日も掛かかってしまった為です。

また分からない箇所は、その都度調べ、理解しながら環境を構築したので、余分な処理が紛れ込んでる可能性がありますが、その際はコメントして頂けるととても嬉しいです。

必要なもの

・オンライン環境下にあるマシン
・オフライン環境のマシン

マシンのOSの状況

僕のマシンでは、

・オンライン環境下にあるマシン(macOS)
・オフライン環境のマシン(CentOS Linux)

読者へ

もしこの流れでもエラーが生じてしまった場合、コメントにエラー内容を残しておいて下さい。
確認できる時に回答させて頂きます。

前提条件

オンライン環境下にあるマシンとオフライン環境のマシンとはSSHで接続してある事

目次

・オンライン環境での事前環境作成(pyenvまで)
・オフライン環境での環境作成(pyenvまで)(僕が結構、詰まった箇所)
・オンライン環境でのpip作成
・オフライン環境でのpip作成
・オンライン環境での必要なライブラリのダウンロード(僕が結構、詰まった箇所)
・オフライン環境での必要なライブラリのダウンロード(僕が結構、詰まった箇所)

オンライン環境での事前環境作成(pyenvまで)

本作業はオンライン環境下でのマシンで行ってください。

以下の例では、python3.6.5を入れます。

まずはどこでも良いので、.pyenvフォルダを格納する為のフォルダを作成します。
今回は、desktop/pyenvに保存する事にします。

ターミナル

$ mkdir ~/desktop/pyenv
$ cd ~/desktop/pyenv

.pyenvフォルダをclone

ターミナル

$ git clone git://github.com/yyuu/pyenv.git .pyenv

python3.6.5用のプラグインファイルを確認

このファイルを参照してリモートから取得するので対象ファイル名を確認しておきましょう

$ cat .pyenv/plugins/python-build/share/python-build/3.6.5
#require_gcc
install_package "openssl-1.0.2k" "https://www.openssl.org/source/old/1.0.2/openssl-1.0.2k.tar.gz#6b3977c61f2aedf0f96367dcfb5c6e578cf37e7b8d913b4ecb6643c3cb88d8c0" mac_openssl --if has_broken_mac_openssl
install_package "readline-8.0" "https://ftpmirror.gnu.org/readline/readline-8.0.tar.gz#e339f51971478d369f8a053a330a190781acb9864cf4c541060f12078948e461" mac_readline --if has_broken_mac_readline
if has_tar_xz_support; then
  install_package "Python-3.6.5" "https://www.python.org/ftp/python/3.6.5/Python-3.6.5.tar.xz#f434053ba1b5c8a5cc597e966ead3c5143012af827fd3f0697d21450bb8d87a6" ldflags_dirs standard verify_py36 copy_python_gdb ensurepip
else
  install_package "Python-3.6.5" "https://www.python.org/ftp/python/3.6.5/Python-3.6.5.tgz#53a3e17d77cd15c5230192b6a8c1e031c07cd9f34a2f089a731c6f6bd343d5c6" ldflags_dirs standard verify_py36 copy_python_gdb ensurepip
fi

※brewを使用してしまうと、上記コマンドは打てません。

関連ファイルダウンロード

wgetを使用して、対象ファイルをリモートから取得します。
wgetコマンドが使えない方はこちらを参照して、wgetコマンドを使用できるようにするか、
対象ファイルを別の形でダウンロードできた状態にして下さい。
wgetコマンド用いる際は下記のようにダウンロードします。

$ wget https://www.openssl.org/source/old/1.0.2/openssl-1.0.2k.tar.gz
$ wget https://ftpmirror.gnu.org/readline/readline-8.0.tar.gz
$ wget https://www.python.org/ftp/python/3.6.5/Python-3.6.5.tar.xz
$ wget https://www.python.org/ftp/python/3.6.5/Python-3.6.5.tgz

チェックサムを確認

md5sumを用いて行います。
md5sumコマンドが入力できない方は、md5sha1sumをインストールし、md5sumコマンドを使用できるようにして下さい。
brewコマンドを使用できるようにしておくと便利です。

$ md5sum *
f965fc0bf01bf882b31314b61391ae63  openssl-1.0.2k.tar.gz
????????????????????????????????  readline-8.0.tar.gz
????????????????????????????????  Python-3.6.5.tar.xz
????????????????????????????????  Python-3.6.5.tgz

上記のような感じで、確認できます。

pluginファイル修正

①httpsをhttpに変更
②URLを「http://localhost/ 」に変更
③チェックサムをダウンロードしてきたファイルの値に変更

$ cp .pyenv/plugins/python-build/share/python-build/3.6.5 .
$ vi 3.6.5
#require_gcc
install_package "openssl-1.0.2k" "http://localhost/openssl-1.0.2k.tar.gz#f965fc0bf01bf882b31314b61391ae63" mac_openssl --if has_broken_mac_openssl
install_package "readline-8.0" "http://localhost/readline-8.0.tar.gz#????????????????????????????????" mac_readline --if has_broken_mac_readline
if has_tar_xz_support; then
  install_package "Python-3.6.5" "http://localhost/Python-3.6.5.tar.xz#????????????????????????????????" ldflags_dirs standard verify_py36 copy_python_gdb ensurepip
else
  install_package "Python-3.6.5" "http://localhost/Python-3.6.5.tgz#????????????????????????????????" ldflags_dirs standard verify_py36 copy_python_gdb ensurepip
fi

アーカイブ化

$ tar cvfz pyenv_files.tar.gz readline-8.0.tar.gz openssl-1.0.2k.tar.gz Python-3.6.5.tar.xz Python-3.6.5.tgz 3.6.5
$ tar cvfz pyenv_local.tar.gz .pyenv

まとめたアーカイブをオフライン環境に転送

pyenv_files.tar.gz
pyenv_local.tar.gz
の2つをscpなどでオフライン環境下のhomeに転送して下さい。

ここで「オンライン環境での事前環境作成(pyenvまで)」は終了です。

オフライン環境での環境作成(pyenvまで)

アーカイブファイルの展開

※転送した箇所に移動して下さい。

homeに転送した場合は、一応、下記コマンドを打ちます。

$ cd ~/

先ほど固めたファイルをここで展開

$ tar xvfz pyenv_local.tar.gz
$ tar xvfz pyenv_files.tar.gz 

設定ファイルの記述

$ echo $SHELL

と入力し、

①実行結果が /bin/bash の場合

$ echo 'export PATH="$HOME/.pyenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
$ . ~/.bash_profile

②実行結果が /bin/zsh の場合

$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
$ echo 'eval "$(pyenv init -)"' >> ~/.zshrc
$ source ~/.zshrc

何をしたのかと言うと、「.bash_profile」もしくは「.zshrc」というファイルに設定用のコードを追加しました。

何が起きているのかを厳密に知りたい方は、こちらを参照して下さい。

修正済みプラグインの配布

mv 3.6.5 .pyenv/plugins/python-build/share/python-build/

git 構築

オフライン環境下のマシンでgitコマンドを使用できない方は、こちらを参照してインストールして下さい。
コマンド一つです。

$ cd ~
$ git init 
$ git add Python-3.6.5.tgz openssl-1.0.2k.tar.gz readline-8.0.tar.gz Python-3.6.5.tar.xz
$ git commit -m'init'

httpサーバー起動

bzip2-develをインストールし、使用できるようになると思います。

使用できない方

こちらからインストールできます
僕の場合は、オフライン環境では、Red Hat 系のcentOS linuxなので、
$ sudo yum install bzip2-devel
こちらのコマンド行けました。
linuxはosの種類によって、パッケージ管理ツールが変わってくるので、注意です。

使用できる方

※80番ポートで起動する場合、root権限が必要

$ sudo python -m SimpleHTTPServer 80 &

もしエラーが出た場合は、こちらを確認し、プロセスを切って再度行って下さい。

pyenvをインストール

$ pyenv install 3.6.5
$ pyenv versions
* system (set by /home/hoge/.pyenv/version)
  3.6.5
$ pyenv global 3.6.5
$  pyenv versions
  system
* 3.6.5 (set by /home.hoge.pyenv/version)
$ which python; python -V
~/.pyenv/shims/python
Python 3.6.5

これでpython 3.6.5がオフライン上で使用できるようになりました。
もしここまででエラーなどが生じてしまっていたら、コメントに残しておいて下さい。

オンライン環境でのpip作成

こちらからご自身が導入したいpipのバージョンのファイルをダウンロードして下さい。
僕は、pip-20.3.3-py2.py3-none-any.whl をダウンロードしました。

このファイルをscpでオフライン環境のマシンに持っていきます。
僕は、自身で作成したフォルダの~/.local/libフォルダに転送しましたが、お好きな位置で大丈夫です。

オフライン環境でのpip作成

.bash_profileでpathの設定をしているので、pipコマンドを入力する階層はどこでも大丈夫です。

ここからpip-20.3.3-py2.py3-none-any.whlをinstallするのですが、

①pipのupdateで行う(参照する際のおすすめ記事
②一旦pipをuninstallしてから再installする(参照する際のおすすめ記事
③パスを指定して普通にinstallする

どの方法でもおそらく行えます。
今回は僕が実行できた「③パスを指定して普通にinstallする」を記述します。

③について

pip-20.3.3-py2.py3-none-any.whl をダウンロードした階層に移動します

僕の場合は、pip-20.3.3-py2.py3-none-any.whlが~/.local/libに存在するので、下記コマンドを行います。

$ pip install --no-deps ~/.local/lib/pip-20.3.3-py2.py3-none-any.whl

結果として、
僕の場合は、pip-20.3.3-py2.py3-none-any.whl をダウンロードしたので、

$ pip list
pip 20.3.3 from ~(python 3.6)

となっていれば成功です。
要は、pipのバージョンがダウンロードしたものに変更されていれば成功です。

オンライン環境での必要なライブラリのダウンロード

ここでは、numpyやpandasなど使用したいライブラリをオフライン環境下で使用できるようなダウンロード方法の記述になります。

ダウンロードしてきたライブラリを格納するフォルダの作成

僕は、desktopにpip-cacheというフォルダ名で作成しましました。

$ cd ~/desktop
$ mkdir pip-cache

任意のライブラリをダウンロード

ここでpip downloadを行うのですが、オプションの記述が重要です。

一般的なオプションを付けない記述では、下記のようになります。

$ pip download -d ./pip-cache numpy #numpyをダウンロードする場合

これでpip-cacheフォルダにnumpyがダウンロードされるのですが、このnumpyをオフライン環境下のマシンに持っていき、pip install numpyというようなコマンドを打つとエラーが生じます。

理由は単純で、platformの違いです。
無知な僕はここに気付くのに少し時間がかかりました。

もっと具体的に記述すると、上記のpip downloadはオンライン環境下のマシンで行いました。
そして、僕のオンライン環境下のマシンのosの種類はmacOSです。
しかし、僕のオフライン環境下のマシンのosの種類はcentOS linuxです。

--platformのオプションを付けないでdownloadすると、macOS用のnumpyがインストールされるので、その理由で、オフライン環境下のマシンのcentOS linuxでは動かなかったという事です。

従って、下記のように、platformをcentOS linux用に宣言してあげます。

$ pip download --platform manylinux1_x86_64 -d ./pip-cache numpy #numpyをダウンロードする場合

これでオフライン環境下のマシンで動くようになりました。

requirements.txtでダウンロード

ここも少し詰まった場面になります。
僕自身がこの時、本来行いたかったのは、こちらのrequirements.txtのダウンロードの方で、一般的な記述は、

requirements.txtをdesktopに置いたので、

$ pip download -d ./pip-cache -r ~/desktop/requirements.txt  #requirements.txt をダウンロードする場合

先程の--platformオプションを付けて、

$ pip download --platform manylinux1_x86_64 -d ./pip-cache -r ~/desktop/requirements.txt  #requirements.txt をダウンロードする場合

しかし、この記述ではエラーが吐かれるんですね。
この理由も分かってしまえば至って、シンプルなのですが、ライブラリの中にはplatformを記述する必要がないものがあるんですね。(互換性があるものとないものがある)

例えば、numpyの場合、
スクリーンショット 2020-12-27 16.52.59.png

次に、astorの場合、
スクリーンショット 2020-12-27 16.53.43.png

このように,numpyではplatformの記載がありますが、astorではplatformの記載がないです。
これが原因で、僕のrequirement.txtには、互換性があるものと無いものが入り乱れていました。

何か良いオプションは無いものかとエラー内容から確認した所、あるではありませんか、パッケージの依存関係をインストールしないオプションが、そう、--no-depsを使用します。

$ pip download --no-deps --platform manylinux1_x86_64 -d ./pip-cache -r ~/desktop/requirements.txt  #requirements.txt をダウンロードする場合

これで無事にできました。

オフライン環境下のマシンにダウンロードしたライブラリをまとめて転送

そして、これまで同様、今回は、pip-cacheに必要なライブラリがダウンロードできたので、それをtarでアーカイブ化して、scpで転送します。この時、requirements.txtも送っておいた方が良いです。
pipの時と同様、僕は、自身で作成した.local/libに転送しました

オフライン環境での必要なライブラリのダウンロード

それでは、転送した任意の階層に移動して、まとめて送られてきたアーカイブを解凍していきましょう。
僕の場合は、.local/libに転送したので、

$ cd ~/.local/lib
$ tar xf pip-cache.tar

最後にpip installを行います。
僕はrequirement.txtはhomeに転送していたので、下記の記述となります。

pip install --no-index --find-links=~/.local/lib/pip-cache -r ~/requirements.txt

ちなみに、--no-index --find-linksというオプションは、通常の参照先(PyPI)を検索しないで、ローカルにあるディレクトリの中身だけ(僕の場合は、~/.local/lib/pip-cacheの中身だけ)でインストールを実行します。

--no-index --find-linksが無いと、オフラインの場合はエラーが生じると思われますので注意が必要です。

以上で、オフライン環境下で必要なライブラリのインストールが完了しました。

まとめ

この記事では、オフライン環境にpyenv、pip、pythonをインストールできたので、好きなようにpythonを動かせるようになりました。

また、この部分はもっと簡単にできるなど、色々ご意見ありましたら、コメントに残して頂けると、とても嬉しいです。

ここまで読んで頂き、ありがとうございました。
社内のネットワークに接続しないマシンでpythonを動かす事になった方や、研究のセキュリティの都合上、ネットワークに接続しないgpuなどのマシンを使用する方など、参考になっていれば幸いです。

おまけ

pyenv+virtualenvで仮想環境を実現

こちらのサイトが非常に分かりやすいです。
但し、オンラインのマシン用にはなります。
しかし、ここまで理解して読まれた方は、下記サイトの確認だけで簡単にオフライン環境でも実現できますので、是非試してください。
https://qiita.com/overflowfl/items/9bc7c9504655f6c17f4e

参考URL

https://pip.pypa.io/en/stable/reference/pip_download/
https://webkaru.net/dev/mac-wget-command-install/
https://qiita.com/panda11/private/67d1e09ec8261e4c19f4
https://git-scm.com/book/ja/v2/%E4%BD%BF%E3%81%84%E5%A7%8B%E3%82%81%E3%82%8B-Git%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB
http://www.s-yata.jp/docs/libbzip2/
http://edo.blog.jp/archives/1819066.html
https://tech-diary.net/python-library-offline-install/
https://tnamao.hatenablog.com/entry/2017/06/29/182050

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker + VSCode + Remote Containerで作る Jupyter Lab(Python)分析環境

この記事はどのような内容?

Docker + VSCode + Remote Containerで作るデータ分析環境構築の手順が書いてあります。

想定読者

Jupyter Lab? それともGoogle Colaboratoryなどのクラウドサービス?...なんか色々あって、Pythonのデータ分析環境わからん!簡単に導入できて、かつそれなりに快適な分析環境を整えたい!」
という方に向けて書きました。

サンプルリポジトリはGitHubにPushしていますので、cloneしてコンテナを立ち上げ、READMEの手順に沿ってリモート接続していただければ、10分程度で環境構築ができます。

https://github.com/hatahata7757/sample-analytical-env

この記事を読んだ方のデータ分析環境の選択に、少しでも貢献できましたら幸いです。

はじめに

最近、Python でデータ分析する機会があり、Jupyterが動く環境を構築する必要がありました。

ローカルにJupyter Lab環境を直接構築しても良いのですが、なるべくローカル環境は汚したくないですし、Jupyter LabではVSCodeと違って入力補完等のサポートが弱いので、ちょっと使いにくい。

色々と調べていると、VSCodeのOctober-2019-releaseで、Microsoft公式の Python 拡張機能が、.ipynb(Ipython notebook専用ファイル)をサポートしていたことを知りました。よって、コーディング環境はJupyter Labではなくインテリセンスなどが効くVSCodeを選択。

仮想環境も、Jupyter LabのDockerイメージがDocker Hubにpushされていることを知ったので、pullしてこれば手軽に環境構築ができそうです。

ということで、

  • 仮想環境は、Jupyter LabDockerイメージを使う

  • Remote Containerを使って、起動したコンテナにリモート接続

  • Remote Containerで接続したワークスペース内で、入力補完やLinterを効かせながらコーディング

上記のような環境をデータ分析環境として選択しました。
イメージはこのような感じです。
Screenshot 2020-12-26 15.17.11.png

Docker + VSCode + Remote Containerで作るJupyter Lab(Python)分析環境構築

以下の内容は、自分で環境構築をされる方用に、少し細かく説明したものになります。

この記事の上部に載せたリポジトリには、必要拡張機能や各種設定を.vscode/settings.json.devcontainer/devcontainer.jsonに記述 & 最低限の手順をREADMEに記載しておりますので、以下手順を読む必要がないようにしてあります。

Docker

他の方が書かれた記事:【Docker】3分でjupyterLab(python)環境を作る!を参考に...というか丸パクリなのですが、docker-compose.ymlに、Jupyter Lab環境構築に必要な設定を記述します。

docker-compose.yml
version: '3'
services:
  notebook:
    image: jupyter/datascience-notebook
    ports:
      - '8888:8888'
    environment:
      - JUPYTER_ENABLE_LAB=yes
    volumes:
      - ./work:/home/jovyan/work
    command: start-notebook.sh --NotebookApp.token=''

volumesにhome/ 以下にjovyan という名前でディレクトリを切っておりますが、jovyanは Jupyter Notebookを使用する人々全員 という意味があるそうです。

参考:

これで、Jupyter Labが動く環境が用意できました。

Remote Container

このままdocker-compose up -dでコンテナを立ち上げれば、http://localhost:8888 にアクセスしてJupyter Labでコーディングできます。

別にそれでも良いのですが、VSCodeの入力補完などの強力な拡張機能の恩恵を受けたいため、Remote Container立ち上げたコンテナにリモート接続し、VSCodeでコーディングしたいと思います。
Remote Containerに関しては、調べると他の方が詳しく説明をしてくださっていますので、そちらをご参照ください。

参考:VSCode Remote Containerが良い

Remote Containerの設定

VSCode [設定]→[拡張機能]→[Remote Container]:インストール。

インストールすると、「Reopen in Container」というポップアップが表示されるので、選択。

Screenshot 2020-12-27 17.10.34.png

すると、プロジェクトのdocker-compose.ymlの設定に基づいたコンテナを自動的にbuild & 起動してくれ、コンテナの中でVSCodeのワークスペースが開かれます。

(二回目以降は、VSCode左下の「><」みたいなマーク→reopen in Containerで接続できます。)
Screenshot 2020-12-27 17.09.37.png

ワークスペース内では、ターミナルもコンテナのシェルと接続されているため、コマンドなどを流す際にいちいちdocker-compose ~(もしくはエイリアス)をつける必要はありません。

ワークスペースを開くと、「No Python interpreter is selected...」といったポップアップが表示されます。これは、Pythonの実行環境を聞いてきているので、Jupyter Labの実行環境であるconda環境「Python 3.xx 64-bit('conda': virtualenv)」を選択。
Screenshot 2020-12-27 17.14.50.png

VSCode

起動したコンテナにリモート接続し、コンテナの中でVSCodeが開ける状態になりました。
ただ、このまま.ipynbを開いてもIPython Notebook用の表示にならないため、ワークスペースに拡張機能を追加します。

ワークスペース内で[拡張機能]→[python(ms-python.python)]を探し、Dev Container: [プロジェクト名にインストールする]を選択。これで、IPython Notebook用の表示になります。
Screenshot 2020-12-26 15.10.05.png

他にも、LinterやIntelliSense系の各種拡張機能を、適宜ワークスペースにインストールしてください。.devcontainer/devcontainer.json内のextensionsに拡張機能を記載しておけば、ワークスペース起動時に自動的に読み込んでくれるので、プロジェクトの配布時などに便利です。

(※「はじめに」に載せたリポジトリには、python (ms-python.python)と自動インポート機能・型チェック機能をサポートしているpylance (ms-python.vscode-pylance)が読み込まれるように設定しています)

最後に。
新しい.ipynbを開く度、そのままでは権限が付与されておらず、編集ができない状態になっています。おそらく「A notebook could execute harmful code when opened.」というポップアップが表示されるので、「Trust」を選択してください。

(ちょっとここが面倒です。ALL Trustを選択しても、一度コンテナを再起動すると設定が初期化されます、、解決策ありましたらご教授ください:qiitan-cry:

環境構築完了!

これでDocker + VSCode + Remote Containerで、入力補完などの強力な各ツールを効かせつつ、データ分析ができる環境が用意できました!

なお、ワークスペースを閉じてもコンテナは起動しっぱなしなので、コンテナは都度停止するのを忘れないようにしてくださいね。

終わりに

Docker + VSCode + Remote Containerで、Jupyter Lab(Python)分析環境を作成しました。
入力補完も効きますし、自分的には快適だなぁと思っているのですが、Pythonでの分析をし始めたばかりなのでこれが正解かわかっておりません。

Google Colaboratory やAzure Notebookなどの便利なクラウドサービスもありますが、時間制限があったり、ローカルで分析した方が計算が早かった、という場合があったので、今回はローカルの環境構築方法にフォーカスしました。

内容の間違いや、もっと良い方法がありましたら、ご指摘・ご教授をお願いいたします。

では、快適なコーディング環境を!

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker + VSCode + Remote Containerで作る快適Jupyter Lab(Python)分析環境

この記事はどのような内容?

Docker + VSCode + Remote Containerで作るデータ分析環境構築の手順が書いてあります。

想定読者

Jupyter Lab? それともGoogle Colaboratoryなどのクラウドサービス?...なんか色々あって、Pythonのデータ分析環境わからん!簡単に導入できて、かつそれなりに快適な分析環境を整えたい!」
という方に向けて書きました。

サンプルリポジトリはGitHubにPushしていますので、cloneしてコンテナを立ち上げ、READMEの手順に沿ってリモート接続していただければ、10分程度で環境構築ができます。

https://github.com/hatahata7757/sample-analytical-env

この記事を読んだ方のデータ分析環境の選択に、少しでも貢献できましたら幸いです。

はじめに

最近、Python でデータ分析する機会があり、Jupyterが動く環境を構築する必要がありました。

ローカルにJupyter Lab環境を直接構築しても良いのですが、なるべくローカル環境は汚したくないですし、Jupyter LabではVSCodeと違って入力補完等のサポートが弱いので、ちょっと使いにくい。

色々と調べていると、VSCodeのOctober-2019-releaseで、Microsoft公式の Python 拡張機能が、.ipynb(Ipython notebook専用ファイル)をサポートしていたことを知りました。よって、コーディング環境はJupyter Labではなくインテリセンスなどが効くVSCodeを選択。

仮想環境も、Jupyter LabのDockerイメージがDocker Hubにpushされていることを知ったので、pullしてこれば手軽に環境構築ができそうです。

ということで、

  • 仮想環境は、Jupyter LabDockerイメージを使う

  • Remote Containerを使って、起動したコンテナにリモート接続

  • Remote Containerで接続したワークスペース内で、入力補完やLinterを効かせながらコーディング

上記のような環境をデータ分析環境として選択しました。
イメージはこのような感じです。
Screenshot 2020-12-26 15.17.11.png

Docker + VSCode + Remote Containerで作るJupyter Lab(Python)分析環境構築

以下の内容は、自分で環境構築をされる方用に、少し細かく説明したものになります。

この記事の上部に載せたリポジトリには、必要拡張機能や各種設定を.vscode/settings.json.devcontainer/devcontainer.jsonに記述 & 最低限の手順をREADMEに記載しておりますので、以下手順を読む必要がないようにしてあります。

Docker

他の方が書かれた記事:【Docker】3分でjupyterLab(python)環境を作る!を参考に...というか丸パクリなのですが、docker-compose.ymlに、Jupyter Lab環境構築に必要な設定を記述します。

docker-compose.yml
version: '3'
services:
  notebook:
    image: jupyter/datascience-notebook
    ports:
      - '8888:8888'
    environment:
      - JUPYTER_ENABLE_LAB=yes
    volumes:
      - ./work:/home/jovyan/work
    command: start-notebook.sh --NotebookApp.token=''

volumesにhome/ 以下にjovyan という名前でディレクトリを切っておりますが、jovyanは Jupyter Notebookを使用する人々全員 という意味があるそうです。

参考:

これで、Jupyter Labが動く環境が用意できました。

Remote Container

このままdocker-compose up -dでコンテナを立ち上げれば、http://localhost:8888 にアクセスしてJupyter Labでコーディングできます。

別にそれでも良いのですが、VSCodeの入力補完などの強力な拡張機能の恩恵を受けたいため、Remote Container立ち上げたコンテナにリモート接続し、VSCodeでコーディングしたいと思います。
Remote Containerに関しては、調べると他の方が詳しく説明をしてくださっていますので、そちらをご参照ください。

参考:VSCode Remote Containerが良い

Remote Containerの設定

VSCode [設定]→[拡張機能]→[Remote Container]:インストール。

インストールすると、「Reopen in Container」というポップアップが表示されるので、選択。

Screenshot 2020-12-27 17.10.34.png

すると、プロジェクトのdocker-compose.ymlの設定に基づいたコンテナを自動的にbuild & 起動してくれ、コンテナの中でVSCodeのワークスペースが開かれます。

(二回目以降は、VSCode左下の「><」みたいなマーク→reopen in Containerで接続できます。)
Screenshot 2020-12-27 17.09.37.png

ワークスペース内では、ターミナルもコンテナのシェルと接続されているため、コマンドなどを流す際にいちいちdocker-compose ~(もしくはエイリアス)をつける必要はありません。

ワークスペースを開くと、「No Python interpreter is selected...」といったポップアップが表示されます。これは、Pythonの実行環境を聞いてきているので、Jupyter Labの実行環境であるconda環境「Python 3.xx 64-bit('conda': virtualenv)」を選択。
Screenshot 2020-12-27 17.14.50.png

VSCode

起動したコンテナにリモート接続し、コンテナの中でVSCodeが開ける状態になりました。
ただ、このまま.ipynbを開いてもIPython Notebook用の表示にならないため、ワークスペースに拡張機能を追加します。

ワークスペース内で[拡張機能]→[python(ms-python.python)]を探し、Dev Container: [プロジェクト名にインストールする]を選択。これで、IPython Notebook用の表示になります。
Screenshot 2020-12-26 15.10.05.png

他にも、LinterやIntelliSense系の各種拡張機能を、適宜ワークスペースにインストールしてください。.devcontainer/devcontainer.json内のextensionsに拡張機能を記載しておけば、ワークスペース起動時に自動的に読み込んでくれるので、プロジェクトの配布時などに便利です。

(※「はじめに」に載せたリポジトリには、python (ms-python.python)と自動インポート機能・型チェック機能をサポートしているpylance (ms-python.vscode-pylance)が読み込まれるように設定しています)

最後に。
新しい.ipynbを開く度、そのままでは権限が付与されておらず、編集ができない状態になっています。おそらく「A notebook could execute harmful code when opened.」というポップアップが表示されるので、「Trust」を選択してください。

(ちょっとここが面倒です。ALL Trustを選択しても、一度コンテナを再起動すると設定が初期化されます、、解決策ありましたらご教授ください:qiitan-cry:

環境構築完了!

これでDocker + VSCode + Remote Containerで、入力補完などの強力な各ツールを効かせつつ、データ分析ができる環境が用意できました!

なお、ワークスペースを閉じてもコンテナは起動しっぱなしなので、コンテナは都度停止するのを忘れないようにしてくださいね。

終わりに

Docker + VSCode + Remote Containerで、Jupyter Lab(Python)分析環境を作成しました。
入力補完も効きますし、自分的には快適だなぁと思っているのですが、Pythonでの分析をし始めたばかりなのでこれが正解かわかっておりません。

Google Colaboratory やAzure Notebookなどの便利なクラウドサービスもありますが、時間制限があったり、ローカルで分析した方が計算が早かった、という場合があったので、今回はローカルの環境構築方法にフォーカスしました。

内容の間違いや、もっと良い方法がありましたら、ご指摘・ご教授をお願いいたします。

では、快適なコーディング環境を!

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「あまり強い言葉を遣うなよ」~BLEACHで始める感情分析

「・・・あまり強い言葉を遣うなよ」
「弱く見えるぞ」
F3E6714B-0440-434F-817D-40F7B9542686.jpeg
(Bleach 20巻より)

愛染様の貫禄を読者に強烈に刻み付ける名シーンでした。
日本語の慣用句として広辞苑に登録される日も近いかもしれません(嘘)。

さて、ここで気になるのが「強い言葉」の定義です。
具体的にどのラインを超えたら「強い言葉」なのでしょうか。

「言葉の強さは主観的で定量化できない」と思われるでしょうか?
一つの方法として、感情分析を利用すると「言葉の強さ」をmaginitudeのスコア、すなわち定量的な値として算出できます。

そこで今回は実際に、PythonでGoogle Cloud API Natural Languageを使用して、BLEACHの名言に対して感情分析を行います。

環境

・Google Colaboratory(Google Chrome上で使用します)

自然言語処理とは?

自然言語処理は、コンピュータに自然言語を処理させるということです。
「自然言語」とは何かというと、「人間が普段使っている言語」です。
「人工的に作られた言語であるプログラミング言語」と対比する意味で、自然発生した言語という意味で自然言語と呼ぶんですね。
人工言語の例) Python, JavaScript, Ruby等
自然言語の例) 日本語、英語、中国語等

人間が日常生活で使っている言語は曖昧です。一方で、コンピュータはプログラミング言語のように一意に解釈できる言語で命令しなければいけません。コンピュータが人間の言葉を扱うのは苦手なことなのです。

感情分析とは?

感情分析は自然言語処理の代表的な技術の一つです。
文字通り、テキストデータの感情をプログラミングを用いて分析します。
感情分析には
1.極性辞書と照らし合わせる
2.機械学習モデルを構築する
3.APIを使用する
と三つのやり方がありますが、比較的簡単に実装でき、かつ高精度も期待できるので、今回は「3.APIを使用する」で行います。
感情分析のAPIも多様な選択肢がありますが、今回はGoogle Cloud API Natural Language(以下GoogleNLPと記載)を使用します。

実装してみた

今回はGoogle Colaboratory上というオンラインの環境で実装します。

実際に、Google Cloud APIを使用してテキストを感情分析できるか試してみましょう。

import requests 
APIkey = "XXXXXXXXX"
text = """
あまり強い言葉を遣うなよ 弱く見えるぞ
"""
url = 'https://language.googleapis.com/v1/documents:analyzeSentiment?key=' + APIkey

header = {'Content-Type': 'application/json'}
body = {
    "document": {
        "type": "PLAIN_TEXT",
        "language": "EN",
        "content": text
    },
    "encodingType": "UTF8"
}

response = requests.post(url, headers=header, json=body).json()

print("総合magnitude:",response["documentSentiment"]["magnitude"])
print("総合score:",response["documentSentiment"]["score"])

上記コードを実行すると、下記の出力結果が得られました!無事、感情分析できたようです。

総合magnitude: 0.2
総合score: -0.2
あまり強い言葉を遣うなよ 弱く見えるぞ magnitude: 0.2 , score: -0.2

magunitude?score?耳慣れない用語が出てきましたね。
まずscoreはテキストがネガティブかポジティブかどうかを-1.0~1.0の値で表しています(正式には「極性」と言います)。
次にmagnitudeが、テキストの感情の重さを0~∞の値で表しています。
イメージとしては、scoreが感情のベクトル、maginitudeが感情の絶対値と思ってください。

さて、改めて出力結果を見てみましょう。
少しネガ寄りのニュートラルで、強い言葉ではないようです。

いよいよ、愛染が「強い言葉」と表現した、日番谷君のセリフを感情分析にかけてみましょう。
text= "藍染、オレはテメーを殺す"としてコードを実行します。

総合magnitude: 0.1 総合score: 0.1
藍染、オレはテメーを殺す。 magnitude: 0.1 , score: 0.1

おや、magnitudeが先ほどより低いです。実は強い言葉を使っていたのは愛染様の方だったということになります。

「愛染」という人物名詞が余計なノイズになってしまっているようです。
「愛染、」を取り除いて再度実行します。

総合magnitude: 0
総合score: 0
オレはテメーを殺す magnitude: 0 , score: 0

望んだような結果が得られていません。

原因としては、下記のような理由が考えられます。
- 「愛染」という人物名詞が余計なノイズになってしまっている
- テキストが短すぎて、上手く分析できていないようです。
- 日本語だと精度が出にくい(日本語の自然言語処理は難しいです。逆に、英語の自然言語処理は最も研究が進んでいます)

そこで色々と試行錯誤したところ、精度が向上して行きました。
特に、「テキストを英語に翻訳してから感情分析する」というのが最も効果的でした。

ひとまずの結論としては、おそらくmagnitude0.7以上で「強い言葉」のようです。
皆さんも、今後宿敵やライバルと対峙するときは、magnitude0.7以下のセリフをチョイスすることを心がけましょう。死亡フラグを回避できます。
今後、BLEACHの勝敗と感情分析の相関関係等も調べてみたいですね。

参考

Python 1年生 体験してわかる!会話でまなべる!プログラミングのしくみ
初心者の最初の一冊としてベストです。

機械学習・深層学習による自然言語処理入門
ある程度Pythonの基礎は理解していることが前提ですが、Python×自然言語処理では最もお勧めです。

Bleach カラー版(Kindle)

また、久保帯人先生待望の新作で、Bleachと世界観を共有するBURN THE WITCH
Prime Videoで全話無料配信中です!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「あまり強い言葉を遣うなよ」を定量化する(BLEACHで始める感情分析)

「・・・あまり強い言葉を遣うなよ」
「弱く見えるぞ」
F3E6714B-0440-434F-817D-40F7B9542686.jpeg
(Bleach 20巻より)

愛染様の貫禄を読者に強烈に刻み付ける名シーンでした。
日本語の慣用句として広辞苑に登録される日も近いかもしれません(嘘)。

さて、ここで気になるのが「強い言葉」の定義です。
具体的にどのラインを超えたら「強い言葉」なのでしょうか。

「言葉の強さは主観的で定量化できない」と思われるでしょうか?
一つの方法として、感情分析を利用すると「言葉の強さ」をmaginitudeのスコア、すなわち定量的な値として算出できます。

そこで今回は実際に、PythonでGoogle Cloud API Natural Languageを使用して、BLEACHの名言に対して感情分析を行います。

環境

・Google Colaboratory(Google Chrome上で使用します)

自然言語処理とは?

自然言語処理は、コンピュータに自然言語を処理させるということです。
「自然言語」とは何かというと、「人間が普段使っている言語」です。
「人工的に作られた言語であるプログラミング言語」と対比する意味で、自然発生した言語という意味で自然言語と呼ぶんですね。
人工言語の例) Python, JavaScript, Ruby等
自然言語の例) 日本語、英語、中国語等

人間が日常生活で使っている言語は曖昧です。一方で、コンピュータはプログラミング言語のように一意に解釈できる言語で命令しなければいけません。コンピュータが人間の言葉を扱うのは苦手なことなのです。

感情分析とは?

感情分析は自然言語処理の代表的な技術の一つです。
文字通り、テキストデータの感情をプログラミングを用いて分析します。
感情分析には
1.極性辞書と照らし合わせる
2.機械学習モデルを構築する
3.APIを使用する
と三つのやり方がありますが、比較的簡単に実装でき、かつ高精度も期待できるので、今回は「3.APIを使用する」で行います。
感情分析のAPIも多様な選択肢がありますが、今回はGoogle Cloud API Natural Language(以下GoogleNLPと記載)を使用します。

実装してみた

今回はGoogle Colaboratory上というオンラインの環境で実装します。

実際に、Google Cloud APIを使用してテキストを感情分析できるか試してみましょう。

import requests 
APIkey = "XXXXXXXXX"
text = """
あまり強い言葉を遣うなよ 弱く見えるぞ
"""
url = 'https://language.googleapis.com/v1/documents:analyzeSentiment?key=' + APIkey

header = {'Content-Type': 'application/json'}
body = {
    "document": {
        "type": "PLAIN_TEXT",
        "language": "EN",
        "content": text
    },
    "encodingType": "UTF8"
}

response = requests.post(url, headers=header, json=body).json()

print("総合magnitude:",response["documentSentiment"]["magnitude"])
print("総合score:",response["documentSentiment"]["score"])

上記コードを実行すると、下記の出力結果が得られました!無事、感情分析できたようです。

総合magnitude: 0.2
総合score: -0.2
あまり強い言葉を遣うなよ 弱く見えるぞ magnitude: 0.2 , score: -0.2

magunitude?score?耳慣れない用語が出てきましたね。
まずscoreはテキストがネガティブかポジティブかどうかを-1.0~1.0の値で表しています(正式には「極性」と言います)。
次にmagnitudeが、テキストの感情の重さを0~∞の値で表しています。
イメージとしては、scoreが感情のベクトル、maginitudeが感情の絶対値と思ってください。

さて、改めて出力結果を見てみましょう。
少しネガ寄りのニュートラルで、強い言葉ではないようです。

いよいよ、愛染が「強い言葉」と表現した、日番谷君のセリフを感情分析にかけてみましょう。
text= "藍染、オレはテメーを殺す"としてコードを実行します。

総合magnitude: 0.1 総合score: 0.1
藍染、オレはテメーを殺す。 magnitude: 0.1 , score: 0.1

おや、magnitudeが先ほどより低いです。実は強い言葉を使っていたのは愛染様の方だったということになります。

「愛染」という人物名詞が余計なノイズになってしまっているようです。
「愛染、」を取り除いて再度実行します。

総合magnitude: 0
総合score: 0
オレはテメーを殺す magnitude: 0 , score: 0

望んだような結果が得られていません。

原因としては、下記のような理由が考えられます。
- 「愛染」という人物名詞が余計なノイズになってしまっている
- テキストが短すぎて、上手く分析できていないようです。
- 日本語だと精度が出にくい(日本語の自然言語処理は難しいです。逆に、英語の自然言語処理は最も研究が進んでいます)

そこで色々と試行錯誤したところ、精度が向上して行きました。
特に、「テキストを英語に翻訳してから感情分析する」というのが最も効果的でした。

ひとまずの結論としては、おそらくmagnitude0.7以上で「強い言葉」のようです。
皆さんも、今後宿敵やライバルと対峙するときは、magnitude0.7以下のセリフをチョイスすることを心がけましょう。死亡フラグを回避できます。
今後、BLEACHの勝敗と感情分析の相関関係等も調べてみたいですね。

参考

Python 1年生 体験してわかる!会話でまなべる!プログラミングのしくみ
初心者の最初の一冊としてベストです。

機械学習・深層学習による自然言語処理入門
ある程度Pythonの基礎は理解していることが前提ですが、Python×自然言語処理では最もお勧めです。

Bleach カラー版(Kindle)

また、久保帯人先生待望の新作で、Bleachと世界観を共有するBURN THE WITCH
Prime Videoで全話無料配信中です!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画像処理エンジニア検定(エキスパート)合格攻略法

1. 概要

画像処理エンジニア検定とはCG-ARTS(公益財団法人 画像情報教育振興協会)が実施しているCG-ARTS検定の1つになります。

活用分野は、医療、半導体、コンピュータ周辺機器、自動車、プラント、印刷等数えればきりがないほど多岐に渡ります。これから画像処理エンジニアの需要は伸びてくるため、技術者であれば持っていると自分の進路選択の幅が広がる資格だと思います。

CG-ARTSの試験には、問題の難易度を変えたベーシックとエキスパートの2種類有ります。
合格率は、ベーシック約7割、エキスパート3〜4割となっておりエキスパートはやや難しい試験です。
ですが、どちらも問題のおよそ70%正解すれば受かる絶対評価の試験だと思うので、最終的にはエキスパートをとることをおすすめします!

今回はその画像処理エンジニア検定・エキスパートを受験しました。
勉強時間や、参考書、試験での注意点など受験生が知っていると役立つ内容にまとめたので紹介していきます。

2. 勉強時間

およそ2ヶ月間かけて学習しました。試験日が11月末でしたので、9月末にちょっとずつ勉強を始めました。(試験は7月と11月の年2回行われています)
このときは、画像処理に対する知識はほとんど持っていませんでした。プログラミングは大学院生の頃研究でC言語やFortranを使ったり、今は趣味でPythonの少しかじっていました。あとは統計と深層学習にある程度知見があるくらいでした。
平日は仕事があったので、仕事が終わってからもくもくと勉強していました。
(残業続きだと23時勉強開始となってまあまあしんどかったです笑)
平日は多くて2時間、短くて30分程度を毎日継続していました。
休日はメリハリを付けて、午前中はぐっすり寝ていたり、部屋の掃除をしたりしてのんびりしてました。そしてお昼ごはんを食べてから、夕方くらいまで集中して取り組みました。1日4時間位やっていたと思います。
平日1日1時間、休日1日4時間とすると、1h×45day+4h×17day=113h
なので画像処理エンジニア検定に費やした時間は合計で100時間強でした。

3. 参考書

参考書ですが、この2冊を勉強しました。
①ディジタル画像処理
②画像処理エンジニア検定エキスパート・ベーシック公式問題集
この2冊しか使っていませんが、擦り切れるほど読みました。
闇雲にいろいろな参考書に手を出さないでこの2冊に絞ってやりましょう。
検定を主催しているCG-ARTSが編集しているので、資格試験に直結する内容です。

勉強の進め方はこんな感じがセオリーになるかと思います。
1. ①を一通り読んで全体像をなんとなく理解する。
2. ②のベーシックの練習問題を解く(間違えた箇所や気になった箇所は①を使って復習をする)
3. ②のエキスパートの練習問題を解く(間違えた箇所は①を使って復習をする)
4. ②の問題集で、間違えた問題、理解の薄い問題をやり直して全て解けるようにする。
5. 最後にWebサイトに載っている過去問題を、時間を測り本番を意識して解く。

「余談」
実は、、、試験前日の夜になって過去問がWEB公開されていることを知って、大慌てで試験日の午前中にちょろちょろっと過去問を確認しました...(汗)確認大事です。
問題構成は②の問題集とほとんど変わらないですが、量をこなすという点からも過去問は、取り組んでおいて損はないので時間の許す限りやりましょう。

4. 試験について

私は千葉県の受験会場で受けました。
試験日が11月末だったのでひんやりとした空気の中、試験会場に足を運びました。
とっても綺麗なホテルでびっくりしたのを覚えています。

試験時間は単願(1科目のみ)だと80分、併願(2科目)だと150分です。なぜか併願すると10分短くなっています。
※CG-ARTS検定は画像処理エンジニア検定の他に、
CGクリエイター検定
CGエンジニア検定
Webデザイナー検定
マルチメディア検定
があり、この5科目の中の2科目を同時に受けることが出来ます。(もちろん受験申込時に「併願」として申し込む必要が有ります。)

私は画像処理エンジニア検定(エキスパート)のみの単願受験だったので、試験時間は13:00〜14:20の80分間でした。

受験した感想としては、、、
ボリュームは多すぎず少なすぎずと言った感じで、80分あれば十分解ききれる量です。
私は60分で一通り解き終えて、残りの20分で見直しをしました。

なので、ペースメーカーとして...
・大問1〜5までを30分
・大問6〜10までを30分
・残り20分を見直し&解けなかった問題に当てる

を目安にするのがおすすめです!

5. 注意点

「4.試験について」にまとめて注意点を書こうか迷ったのですが、実際に試験を受けて緊張していると勘違いしそうだなと思ったことがあったので、別章を設けて「5.注意点」にまとめました。

・問題用紙について
問題用紙は、画像処理エンジニア検定だけではなく、CGクリエイター検定、Webデザイナー検定、CGエンジニア検定、画像処理エンジニア検定、マルチメディア検定の全てが一緒になった冊子となっています(2020年度後期現在)。
加えて、検定の種類に関わらず共通問題となる大問1(著作権)は前半のページに記載されています。
試験の種類が違っても似通った問題があるので、自分の解く試験のページはしっかり確認してから問題を解きましょう。

・解答用紙について
解答用紙はマークシートになっています。
解答用紙も著作権の大問1は共通のマーク欄で、大問2以降は各検定で別々のマーク欄があるので自分の回答するマーク欄は必ず確認しましょう。

6. 結果

無事合格出来ました!
試験日から5日後に試験問題と解答速報がWEBにアップロードされるので結果はほぼわかっていたのですが、やっぱり合格した受験番号を確認すると安堵と嬉しさがこみ上げてきますね。

大問1の著作権で4問中1問のみ正解と散々な結果でしたが他の問題で稼いでくれて80%程取れていました。

皆さんの資格試験取得への一助になれば幸いです。
最後まで読んでいただきありがとうございました!

画像処理エンジニア検定で学んだ手法をPythonで書いてみた記事も上がっていますのでこちらも是非見てください!

【Python】対象物の画素値RGBの平均値を計算する
【Python】サポートベクタマシン(SVM)を使って画素値からりんごと梨を分類
【画像処理】ポスタリゼーション

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python】Pyxelでゲームを作る - エディターを使う -

前回の記事
【Python】Pyxelでゲームを作るための最初の一歩

はじめに

Pyxelにはエディタ(Pyxel Editor)が用意されており、ドット絵を描いたりできます。
ここでは、エディタの使い方を紹介します。

エディタを起動する

以下のコマンドを実行します。

pyxeleditor [Pyxelリソースファイル]

リソースファイルは.pyxresという形式のファイルです。
省略した場合は新たに作成されます。

絵を描く

エディターはこんな画面です。

右側の枠が絵を描ける領域(イメージバンク)全体です。
イメージバンクは3枚用意されています。番号は0から2で、右下の枠で切り替えられます。
左側の枠はイメージバンク全体のうち、選択された16×16の領域の拡大図です。
左下の枠では、描画する色等を選択できます。
したがって、まず右枠で、どの範囲に絵を描くか選択し、左枠で実際に書き込んでいく、という流れになります。
とりあえず適当に絵を描いて保存します。

絵を表示させる

エディターで描いた絵をゲーム画面に表示させるためには、リソースの読み込みと描画が必要です。
以下のようにコードを書きます。

import pyxel

class App:
    def __init__(self):
        pyxel.init(100, 100)

        # リソースファイルの読み込み
        pyxel.load("sample.pyxres")

    def run(self):
        pyxel.run(self.update, self.draw)

    def update(self):
        pass

    def draw(self):
        pyxel.cls(0)

        # イメージの描画:(x, y, img, u, v, w, h, [colkey])
        # xy:コピー先の座標、img:イメージバンクの番号
        # uv:コピー元の座標、wh:コピー範囲、colkey:透明色
        pyxel.blt(0, 0, 0, 0, 0, 16, 16, 0)

App().run()


bltの引数がたくさんありますね。
まず、x, yは、ゲーム画面のどの場所にイメージをコピーするか、です。今回は(0,0)としたので、左上にコピーされています。
imgは何枚目のイメージバンクを使うか、です。0にすると1枚目です。
u, vは、イメージバンク上の座標です。どこを基準にコピーするかを選択します。
w, hは、コピー範囲の幅と高さです。今回は(u,v,w,h)(0,0,16,16)にしたので、左上から16×16のサイズがコピーされています。結果として、左上に描いた白い丸が描画されました。
引数の値を変えてみるとわかりやすいと思います。

import pyxel

class App:
    def __init__(self):
        pyxel.init(100, 100)

        pyxel.load("sample.pyxres")

    def run(self):
        pyxel.run(self.update, self.draw)

    def update(self):
        pass

    def draw(self):
        pyxel.cls(0)

        pyxel.blt(0, 0, 0, 0, 0, 16, 16, 0)
        pyxel.blt(32, 48, 0, 16, 0, 16, 16, 0)
        pyxel.blt(64, 16, 0, 32, 16, 16, 16, 0)

App().run()

背景を描く

背景はタイルマップエディタを使って作成します。

背景は、イメージエディタで作成した画像を貼り付けていくことで作成します。
まず、右下の枠には、イメージバンクの内容が表示されます。先程描いた絵が表示されていますね。ここで、コピーしたい範囲を選択します。今は左上8×8が選択されています。
次に、右上の枠には、背景を描画できる領域全体(タイルマップ)が表示されています。全体のサイズは256×256です。ここから、編集する領域を選択します。
左側の枠に、タイルマップのうち選択した範囲16×16が表示されます。
タイルマップ1マスに、イメージバンクの領域8×8が入ります。
なお、タイルマップは8枚用意されており、番号は0〜7です。左下の「TILEMAP」というところで切り替えられます。
初期状態ではイメージバンク左上の絵で埋め尽くされているので、適当に変えておきます。

背景を表示させる

import pyxel

class App:
    def __init__(self):
        # 画面サイズをちょっと変更
        pyxel.init(128, 128)

        pyxel.load("sample.pyxres")

    def run(self):
        pyxel.run(self.update, self.draw)

    def update(self):
        pass

    def draw(self):
        pyxel.cls(0)

        # 背景の描画:(x, y, tm, u, v, w, h, colkey)
        # xy:コピー先の座標、tm:タイルマップの番号
        # uv:コピー元の座標、wh:コピー範囲、col透明色
        pyxel.bltm(0, 0, 0, 0, 0, 16, 16, 0)

        pyxel.blt(0, 0, 0, 0, 0, 16, 16, 0)
        pyxel.blt(32, 48, 0, 16, 0, 16, 16, 0)
        pyxel.blt(64, 16, 0, 32, 16, 16, 16, 0)

App().run()

bltmを使って背景を描画します。
引数はbltと似ていますが、(u, v, w, h)について、タイルマップエディタにおける座標を指定することに注意です。ドットは128×128個ですが、タイルマップの領域としては16×16です。

背景を表示できました。でもちょっと気持ち悪いですね。

参考

https://github.com/kitao/pyxel/blob/master/README.ja.md
Pyxelでパックマンぽいゲームを作る 前編
Pyxelで倉庫番ゲームを作る(前編)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LeetCodeに毎日挑戦してみた 141. Linked List Cycle(Python、Go)

Leetcodeとは

leetcode.com
ソフトウェア開発職のコーディング面接の練習といえばこれらしいです。
合計1500問以上のコーデイング問題が投稿されていて、実際の面接でも同じ問題が出されることは多いらしいとのことです。

golang入門+アルゴリズム脳の強化のためにgoとPythonで解いていこうと思います。(Pythonは弱弱だが経験あり)

32問目(問題141)

141. Linked List Cycle

問題内容

Given head, the head of a linked list, determine if the linked list has a cycle in it.

There is a cycle in a linked list if there is some node in the list that can be reached again by continuously following the next pointer. Internally, pos is used to denote the index of the node that tail's next pointer is connected to. Note that pos is not passed as a parameter.

Return true if there is a cycle in the linked list. Otherwise, return false.

日本語訳

headリンクリストの先頭であるが与えられた場合、リンクリストにサイクルが含まれているかどうかを判断します。

next ポインタを継続的にたどることによって再び到達できるノードがリストにある場合、リンクリストにサイクルがあり ます。内部的にpos は、テールのnext ポインタが接続されているノードのインデックスを示すために使用 されます。 これpos はパラメータとして渡されないことに注意し てください

trueリンクリストにサイクルがある場合に戻り ます。それ以外の場合は、を返しfalseます。

Example 1:

img

Input: head = [3,2,0,-4], pos = 1
Output: true
Explanation: There is a cycle in the linked list, where the tail connects to the 1st node (0-indexed).

Example 2:

img

Input: head = [1,2], pos = 0
Output: true
Explanation: There is a cycle in the linked list, where the tail connects to the 0th node.

Example 3:

img

Input: head = [1], pos = -1
Output: false
Explanation: There is no cycle in the linked list.

考え方

  1. 一度に2つノードを進めるfastと1つしか進めないslowを用意します。
  2. ループ処理をかけて、fastがslowに追いつくか、値がなくなるかまで処理を行います
  3. 追いついたらTrue,nilになったらFalseです

解答コード

def hasCycle(self, head):
    slow = fast = head
    while fast and fast.next:
        fast = fast.next.next
        slow = slow.next
        if slow == fast:
            return True
    return False
  • Goでも書いてみます!
func hasCycle(head *ListNode) bool {
    if head == nil || head.Next == nil {
        return false
    }
    p1, p2 := head, head.Next
    for p1 != p2 {
        if p2 == nil || p2.Next == nil {
            return false
        }
        p1 = p1.Next
        p2 = p2.Next.Next
    }
    return true
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

データをインタラクティブに可視化する方法

データをインタラクティブに可視化できるソフト

 調べたので、メモ代わりのQiita投稿です。

Dash(Plotly)

 DashというのはPlotlyというライブラリを使ったWebのプラットフォームです。Plotlyに関しては[Python] Plotlyでぐりぐり動かせるグラフを作るを参照ください。

 Dashに関する記事は以下参照ください。

Jupyter上でDashを使えるjupyter_dash

なぜデータをインタラクティブに可視化したいのか?

DXの思ったところ

PlotlyとDashの本を共著で執筆しました(事後報告)

 Google Colabでも動かすことができます。以下が @OgawaHideyuki さん作成のノートブックへのリンクです。

qiita_jupyterdash_example.ipynb

PandasGUI

 PandasをGUIでぐりぐりいじれるソフト。

 まだ試せていませんが、リポジトリのREADMEの動画とかみるとかなりよさそうな感じです。

他の方からのフィードバック

 試したらまたちゃんと追記します。

まとめ

 簡単な自分のためのメモです。他にも良さそうなもの知っている人あれば、コメントや編集リクエストいただけるとうれしいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ふるさと納税 ワンストップ特例制度の寄付金税額控除に係る申告特例申請書をPythonで記入する

ふるさと納税 ワンストップ特例制度の寄付金税額控除に係る申告特例申請書をPythonで記入する

ふるさと納税ワンストップ特例制度を使う場合の申請書は1/10必着です。
手書きで記入するのが大変だったので、配布されているpdfに必要項目を記入するスクリプトを作成しました。

google Colaboratory で動かしています。

ライブラリの準備

# pdf編集に必要なライブラリ
!pip install PyPDF2 reportlab
# 日本語をpdfに書き込むためのフォント
!apt-get -y -q install fonts-ipaexfont

import io
from PyPDF2 import PdfFileWriter
from reportlab.lib.colors import Color
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas
from reportlab.lib.units import mm
from reportlab.lib.pagesizes import A4, portrait

# フォントを登録
pdfmetrics.registerFont(
    TTFont(
        'IPAexPMincho',
        '/usr/share/fonts/opentype/ipaexfont-mincho/ipaexm.ttf'
    )
)

# 書き込む情報
address = ["東京都〇〇区〇〇町", "123-123", "コーポ303"]
tel = "03-0000-0000"
hurigana = "ヤマダ タロウ"
name = "山田 太郎"
number = "000000000000"
sex = "male"
birth = ("令", "1", "1" ,"1")
wareki = "2"

data = [
    ("〇〇市", 2, 12, 1,100000),
    ("△△町", 2, 12, 1,50000),
]

文字を書き込む処理

def write_common(can):
    font_size = 10

    left = 49*mm
    right = 113*mm
    can.setFont('IPAexPMincho', font_size)
    can.setFillColor(Color(0, 0, 0, alpha=1))

    # 住所
    for i, row in enumerate(address):
        can.drawString(left, (37*mm + (font_size + 5) * i), row)

    # 電話番号
    can.drawString(left, 59*mm, tel)

    # フリガナ
    can.drawString(right, 33*mm, hurigana)

    # 氏名
    can.drawString(right, 39.5*mm, name)

    # 個人番号
    for i, num in enumerate(number):
        can.drawString(right - 2.1*mm + (i * 4.1)*mm, 47*mm, num)

    # 性別
    if sex == "male":
        maru = right + 13*mm
    else:
        maru = right + 27.4*mm
    can.drawString(maru, 52.7*mm, "○")

    # 生年月日
    if birth[0] == "明":
        nengou = (right - 1.5*mm, 58*mm)
    elif birth[0] == "大":
        nengou = (right + 2.7*mm, 58*mm)
    elif birth[0] == "昭":
        nengou = (right + 6.6*mm, 58*mm)
    elif birth[0] == "平":
        nengou = (right + 1.5*mm, 61*mm)
    elif birth[0] == "令":
        nengou = (right + 2.7*mm, 61*mm)
    can.drawString(*nengou, "○")
    birth_height = 60*mm
    can.drawString(right + 16*mm, birth_height, birth[1])
    can.drawString(right + 27*mm, birth_height, birth[2])
    can.drawString(right + 37*mm, birth_height, birth[3])

    # 和暦
    can.drawString(45*mm, 14*mm, wareki)

    # チェック
    can.drawString(151*mm, 161*mm, "✓")
    can.drawString(151*mm, 200*mm, "✓")

    # 記入日
    can.setFont('IPAexPMincho', 8)
    date_height = 26*mm
    can.drawString(40*mm, date_height, "2")
    can.drawString(51*mm, date_height, "12")
    can.drawString(63*mm, date_height, "31")
def write_kifu(can, kifu_data):
    can.setFont('IPAexPMincho', 10)
    can.drawRightString(68*mm, 29*mm, kifu_data[0] + "長")
    kifu_height = 134.5*mm
    can.drawString(51*mm, kifu_height, str(kifu_data[1]))
    can.drawString(62*mm, kifu_height, str(kifu_data[2]))
    can.drawString(74*mm, kifu_height, str(kifu_data[3]))
    can.drawRightString(139*mm, kifu_height, f"{kifu_data[4]:,}")

ファイルに書き込み

from PyPDF2 import PdfFileReader
# 配布されている申請書のpdfを読み込む
pdf_path = "./onestop_myNumber_form.pdf"

width, height = portrait(A4)
output = PdfFileWriter()

for row in data:
    packet = io.BytesIO()
    can = canvas.Canvas(packet, pagesize=(width, height), bottomup=False)
    write_common(can)
    write_kifu(can, row)
    can.showPage()
    can.save()

    packet.seek(0)
    new_pdf = PdfFileReader(packet)

    pdf = PdfFileReader(pdf_path)
    page = pdf.getPage(0)

    page.mergePage(new_pdf.getPage(0))
    output.addPage(page)


with open(f"./kifu.pdf", "wb") as fout:
    output.write(fout)

完成!

kifu_〇〇市.png

参考になった記事

https://buildersbox.corp-sansan.com/entry/2020/06/09/110000
https://news.mynavi.jp/article/zeropython-70/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Python] pytest-mock使い方メモ

インストール

poetryを使っていれば簡単です。

poetry add -D pytest-mock

使っていなくても簡単です。けれど、poetryのほうがおすすめです。

pip install pytest-mock

テストの書き方

テストメソッドの引数としてmockerを指定します。

class MyClassTests:
    def test_something(self, mocker):
        pass

モックの作り方

モック - Mockを作るには、mocker.Mockを使ってインスタンスを生成するやり方と、mocker.patchを使ってダイナミックにMockインスタンスに置き換えていくやり方があります。

題材として、このようなモジュールがあったとします。

exmock
├── __init__.py
├── bar.py
├── foo.py
└── target.py
exmock/target.py
from .foo import FooClass
from .bar import BarClass


class TargetClass:
    def do_something(self, foo: FooClass) -> None:
        """ do_foo()の結果をdo_bar()に渡します。 """
        bar = BarClass()
        bar.do_bar(foo.do_foo())

クラスのモックを作る

foo_mock = mocker.Mock(spec=FooClass)
# foo_mock.do_foo もモックになっています。

specとしてクラスを指定することで、クラスに実在する属性のみが生えるようにします。
生えてきた属性もMockのインスタンスです。

コンストラクタの戻り値をモックにする

コンストラクタは値を返さないのです表現としておかしいですが、説明の都合上このようにしています。

bar_mock = mocker.Mock(spec=BarClass)
mocker.patch('exmock.target.BarClass', return_value=bar_mock)

テスト対象ファイルexmock/target.pyにあるfrom .bar import BarClassにあわせて、exmock.target.BarClassをパッチしています。

メソッドをモック化する

do_bar_mock = mocker.patch.object(BarClass, 'do_bar')

do_barメソッドの実装がモック化し、なにもしなくなります。

do_barメソッドの動作を変更するには、戻り値のモックを使います。

モックの動作を変更する

生成したモックは、テストにあわせて任意の動きをさせます。

戻り値を設定する

生成したモックのreturn_value属性に値をセットします。

mymock.return_value = 戻り値

呼び出されたときに例外を発生させる

生成したモックのside_effect属性に例外をセットします。

mymock.side_effect = Exception('エラーです')

まるっと置き換える

モック対象の実装をまるっと置き換えます。

def side_effect():
    pass

mymock.side_effect = side_effect

モックのテスト

モックに対して、何らかの操作が行われたことをテストします。

1回呼び出されたことをテストする

mymock.assert_called_once()

複数回呼び出されたことをテストする

# pytestを使っている場合です。
assert mymock.call_count == 回数

まったく呼び出されないことをテストする

mymock.assert_not_called()

特定引数で呼び出されたことをテストする

mymock.assert_called_with(特定引数1, 特定引数2, ……)
mymock.assert_called_once_with(特定引数1, 特定引数2, ……) # 1回呼び出される場合

複雑な引数をテストする

args, kwargs = mymock.call_args
# argsやkwargsをテストする

call_argsに、直近の呼び出し引数が入ってます。
argsは、キーワードなし引数が入っているタプルです。kwargsは、キーワード付き引数が入っているディクショナリです。

検証環境

この記事を書くにあたって使った環境です。

  • Python 3.8.2
  • Poetry 1.1.4
  • pytest-mock 3.4.0

使ったファイルはこちらに置きました。
https://github.com/sengokyu/python-pytest-mock-usage-example

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Azure Machine Learningを使って実験管理とモデル管理をしつつ機械学習モデル開発をする話

はじめに

Azure Machine Learning という機械学習周りのかなり広い範囲をカバーするサービスがあります。

Azure Machine Learning があれば、データセット管理からモデルの開発、実験と検証、デプロイまで、機械学習モデルの開発に必要なほぼほぼ全ての工程をこなすことができます。

しかし、使い慣れてる上に魔改造を繰り返して自分に最適化した開発環境が既にあるというのに、わざわざ新しい環境に一気に切り替えるのもあまり良い気持ちがしません。

今回は Azure Machine Learning の SDK を使い、その機能の一部、実験管理とモデル管理部分のみを既存の機械学習モデル開発環境に組み入れてみようと思います。

環境

これまで

機械学習用に作ったGPU搭載のLinux機にJupyterLab環境を用意し、メインPCから接続して使っていました。

コード管理こそある程度Githubを活用していますが、恥ずかしながら、実験管理については面倒でやっておらず、結局ハイパラコメント&ノートブックコピー&ファイル名へのハイパラ挿入で実験管理をしておりました。できあがったモデルの管理も杜撰なもので、「model_adam_lr_0001_lstm_3_layer_e_512_h_1024.model」みたいなヤバい名前をつけておりました……。

そんなやり方なので当然「あれ、あの時のハイパラどうだったっけ」とか「あ、間違えて既存のファイル上書きしちった」とか「出力した辞書なくした」とかポカが発生していました。

これから

既存の環境に実験管理、モデル管理を行えるツールを追加しようと思います。

実験管理ツールとして広く使われているツールといえば MLflow だと思います。使い方を調べてみると、Jupyter 同様にサーバー的な使い方をするようですが、実験やモデルまで機械学習環境と共倒れになるような状態は好ましくありません。別にサーバーを建てるのも管理が面倒です。

マネージドサービスとしての MLflow があったらいいなぁと思って探していると、現状 Databricks を使うか Azure Machine Learning の MLflow 互換 API を使用するかという2択が存在するようでした。

Databricks は個人的に結構好きでかなり心動きましたが、最終的に実験管理からモデルを活用する MLOps まで考えるなら各種コンテナ系サービスとの連携機能を備えてデプロイやログまで面倒を見てくれる Azure Machine Learning の方が良いと思ったので、お試しがてら今回はこちらを採用してみます。

サービス理解

Azure Machine Learning

azure-machine-learning-taxonomy.png

ドキュメント1にあったこの画像がわかりやすいのでこちらを起点に理解していきます。

最上位リソースとして Workspace が存在し、その配下に実験管理の Experiment 、モデル管理の Registerd models 、 デプロイの Deployment endpoints 等の機能が存在しているようです。

MLFlow

ドキュメント2によれば、 MLflow は以下4つのコンポーネントで構成されているようです。

  • MLflow Tracking
  • MLflow Projects
  • MLflow Models
  • MLflow Registry

Tracking はパラメーターやメトリックの記録や実行履歴の管理を担っているので、今回行いたい「実験管理」というのは Tracking の領分ということになりそうです。

Projcts が学習処理をどう行うかという環境情報を記録し、 Models が学習済みモデルの管理を担当しているので、この2つのコンポーネントが「モデル管理」を担うことになりそうです。

Azure Machine Learning と MLflow の関係

Experiment が Azure Machine Learning では実験管理を担当するコンポーネントですが、このコンポーネントが MLflow Tracking Server としても機能するように作られています。

MLflow Projects が担う環境情報の管理もまたこの Experiment の領分です。

MLflow ではあるモデルの実験全体を表す Experiment という単位に、1回の実験実行に相当する Run を実験回数分収めてます。この構造は Azure Machine Learning でも同じです。

Artifact というのはモデル実行で得られた各ファイルのことを指しているようです。Artifact は Run の一部として記録されています。

僕の場合だとよくモデル本体以外にもログのバックアップやグラフを画像出力したものを作っているので、こういうものも管理できるのはありがたいです。

MLflow Models が担うモデル管理は MLflow 互換のモデル定義に対応している Registerd models が担います。

試してみた

事前にやること

GPU 付きのインスタンスを使いたい場合、事前にサポートリクエストからクォータ (使用できるリソースの枠) 申請をする必要がある点に注意が必要です。また、通常の仮想マシンと Azure Machine Learning 配下のインスタンスでは異なる枠が設定されているようだったので、今回本記事では使用しませんが Azure Machine Learning で GPU インスタンスを使いたい場合はそちらのクォータ申請も必要なようです。

(Data Science VM のデプロイ)

使っている GPU 搭載機の調子が非常に悪く、いい機会なので Azure 上に GPU 付きの VM をデプロイしてそちらに一時的に移行しようと思います。オンプレ GPU マシンはロマンなので再構築したらまた戻ろうとは思っていますが、データを失うと本当に辛いので今回はクラウド上のJupyterLab環境を使っていきます。

Data Science Virtual Machine というイメージを使用すれば、JupyterHub や JupyterLab の設定等が一通り事前に行われていて楽なので、こちらを使用します。

image.png

Tesla V100 を搭載しているインスタンスである NC v3 系インスタンスを使用しています。

注意点として、可用性オプションがデフォルトではゾーン1に設定されていましたが、Data Science Virtual Machine は可用性オプションに対応してないようで、どのゾーンを指定してもエラーが出たので「インフラストラクチャ冗長は必要ありません」に変更しています。コードは GitHub に、実験記録やモデルについては Azure Machine Learning に任せる予定なので、計算環境は最悪死んでも構いません。

また、ログインに JupyterHub を使う関係上アカウントはユーザー名&パスワードの方が都合が良いので認証の種類を「パスワード」に変更しています。

ディスクやネットワークの設定はデフォルトのままでいきます。ディスク容量については扱うデータやモデルの規模次第では拡張する必要があるかもしれません。

間違って VM をつけっぱなしにすると非常に財布が辛いことになるので、自動シャットダウンは有効にしました。

https://\<VM IP\>:8000

で JupyterHub に接続できるので、オレオレ証明書特有のエラー画面を抜けて設定した管理者アカウントでログインすると見慣れた Jupyter Notebook 環境が表示されます。

https://\<VM IP\>:8000/user/\<username\>/tree?

のような URL が表示されていますが、ここで

https://\<VM IP\>:8000/user/\<username\>/lab

とすると JupyterLab 環境に移ります。デフォルトでこの辺りを変えるには JupyterHub の設定書き換えが必要と記憶していますが、面倒なので今回はこのままいきます。

image.png

Azure Machine Learning のデプロイ

Azure Machine Learning のワークスペースを用意します。

image.png

設定を変える必要は特になく、リソースグループとワークスペース名を決めたらそのまま作成に移ります。

デプロイが完了してリソースの画面へ移り、「config.json をダウンロード」をクリックします。これは SDK 越しに作ったAzure Machine Learning ワークスペースに接続するための情報が記載されたファイルです。中身はサブスクリプション ID とリソースグループ名、ワークスペース名です。手動入力でも十分対応できますが、今回はこちらを使用します。

image.png

「スタジオの起動」をクリックすると Azure Machine Learning Studio という GUI ベースで色々できる統合環境を使うことができます。

image.png

ここから色々と操作できるようです。(今回はほとんど SDK から操作するので使いませんが)

きれいな UI がやるとやる気が出るタイプなので俄然元気になってきました。

Azure Machine Learning Workspace への接続

Data Science Virtual Machine の JupyterLab 環境に移って、 azureml_py36_pytorch というカーネルを使用してノートブックを作ります。(自分で conda の仮想環境を作る場合、azureml と MLflow のインストールさえすれば特に問題はなさそうです)

まずはワークスペースへの接続を行います。先程ダウンロードした config.json をノートブックと同じディレクトリ内にアップロードする必要があります。

MLflow Tracking 互換のサーバーとして利用するので、 MLflow とワークスペースの紐付けもついでに行います。

import mlflow
from azureml.core import Workspace

ws = Workspace.from_config()

mlflow.set_tracking_uri(ws.get_mlflow_tracking_uri())

認証しろと出ているので提示された URL から認証を済ませます。

image.png

別ウィンドウでの認証を済ませて戻ってくるとセルの実行が完了して認証済みとなっていました。

image.png

実験の用意

ボストンの住宅価格データセットを使用して、 PyTorch でニューラルネットワークを組んで価格を予測するモデルを用意してみました。

なお、今回なんとなく最適化手法として AdaBelief を使っています。使ってみたかっただけです。

conda activate azureml_py36_pytorch
pip install adabelief-pytorch==0.2.0

としてパッケージをインストールするか、パッケージインストールが面倒なら Optimizer の部分を

optimizer = torch.optim.Adam(nn_model.parameters(), lr=0.001)

とでもして Adam に置換すれば動きます。

import torch
from torch import nn
from torch.nn import functional as F
import numpy as np
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
import torch.utils.data as Data
from adabelief_pytorch import AdaBelief
import tqdm
from matplotlib import pyplot as plt
from sklearn.metrics import mean_squared_error

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

# ハイパーパラメーター

hidden_1 = 64
hidden_2 = 16
batch_size = 16
n_epochs = 20

# データセット 

boston = load_boston()
X_train, X_test = train_test_split(boston.data)
y_train, y_test = train_test_split(boston.target)

class BostonData(Data.Dataset):
    def __init__(self, X, y):
        self.targets = X.astype(np.float32)
        self.labels = y.astype(np.float32)

    def __getitem__(self, i):
        return self.targets[i, :], self.labels[i]

    def __len__(self):
        return len(self.targets)

train_dataset = BostonData(X_train, y_train)
test_dataset = BostonData(X_test, y_test)

train_loaded = Data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loaded = Data.DataLoader(test_dataset, batch_size=batch_size, shuffle=True)

# モデル

class Model(nn.Module):
    def __init__(self, n_features, hidden_1, hidden_2):
        super(Model, self).__init__()
        self.linear_1 = nn.Linear(n_features, hidden_1)
        self.linear_2 = nn.Linear(hidden_1, hidden_2)
        self.linear_3 = nn.Linear(hidden_2, 1)

    def forward(self, x):
        y = F.relu(self.linear_1(x))
        y = F.relu(self.linear_2(y))
        y = self.linear_3(y)
        return y

n_features = X_train.shape[1]
nn_model = Model(n_features, hidden_1, hidden_2)

# 最適化手法と損失関数

criterion = nn.MSELoss(size_average=False)
optimizer = AdaBelief(
    nn_model.parameters(),
    lr=1e-3,
    eps=1e-16,
    betas=(0.9,0.999),
    weight_decouple = True,
    rectify = False,
    print_change_log = False
)

# 学習

losses = []
for epoch in range(n_epochs):
    progress_bar = tqdm.notebook.tqdm(train_loaded, leave=False)
    losses = []
    total = 0
    for inputs, target in progress_bar:
        inputs.to(device)
        target.to(device)
        optimizer.zero_grad()

        y_pred = nn_model(inputs)
        loss = criterion(y_pred, torch.unsqueeze(target,dim=1))

        loss.backward()

        optimizer.step()

        losses.append(loss.item())
        total += 1

    epoch_loss = sum(losses) / total
    losses.append(epoch_loss)

    mess = f"Epoch #{epoch+1} Loss: {losses[-1]}"
    tqdm.tqdm.write(mess)

plt.plot(losses)

MLflow Tracking + Azure Machine Learning による実験管理

MLFlow Tracking のサーバーとしてAzure Machine Learning の Experiment コンポーネントを使用しますが、 API 自体は MLflow のものを使用します。

1つの実験を定義します。

experiment_name = 'boston_nn_experiment'
mlflow.set_experiment(experiment_name)

メトリックを追跡する場合、下記のようにwith mlflow.start_run():の内側でmlflow.log_metricを使って記録していきます。

with mlflow.start_run():
    mlflow.log_metric('Loss', 0.03)

mlflow.log_metrics関数を使えば辞書型で一括で記録することもできます。

ハイパーパラメーターの記録はmlflow.log_parammlflow.log_paramsを使います。単数形だと1個ずつ、複数形だと辞書で一括なのはメトリックの記録と同様です。

params = {
    "hidden_1":hidden_1,
    "hidden_2":hidden_2,
    "batch_size":batch_size,
    "n_epochs":n_epochs
        }
with mlflow.start_run():
    mlflow.log_metrics(params)

実際に実験を実行して記録するときは先程までのコードの下に実行部分を書いていくか、mlflow.start_run()mlflow.end_run()で実行部分を挟む必要があります。

この程度であれば既存コードも簡単に書き直せそうでいいですね。

生成したファイルの記録はmlflow.log_artifactで行います。今まではディレクトリに直接書き出していましたが、以後はファイルの管理も Azure Machine Learning に任せるので、一時ディレクトリを生成して使う方式にしてみました。

fig = plt.figure()
plt.plot(losses)
with tempfile.TemporaryDirectory() as d:
    filename = 'plot.png'
    artifact_path = pathlib.Path(d) / filename
    print(artifact_path)
    fig.savefig(str(artifact_path))
    mlflow.log_artifact(str(artifact_path))

学習したモデルの登録はmlflow.pytorch.log_modelを使用します。artifact_path はモデルとその関連ファイルが収められる、管理ディレクトリの最上位フォルダの名称となります。第1引数で指定しているのはモデルのオブジェクトで、自前で pickle やら torch.save をしなくても、勝手にバイナリ形式に変換してくれるみたいです。

mlflow.pytorch.log_model(nn_model,artifact_path="model")

関数名を見ると明白ですが、mlflow.pytorch.log_model以外にも色々とあります。3

log_model と log_artifact で記録したファイルは1まとめに管理されます。もし MLflow が対応していないライブラリを使う場合でも artifact として登録すればとりあえず追跡できますね。

ここまでの mlflow の色々を組み込んで、学習部分のコードを下記のように書き換えました。

with mlflow.start_run():
    mlflow.log_params(params)

    losses = []
    for epoch in range(n_epochs):
        progress_bar = tqdm.notebook.tqdm(train_loaded, leave=False)
        losses = []
        total = 0
        for inputs, target in progress_bar:
            inputs.to(device)
            target.to(device)
            optimizer.zero_grad()

            y_pred = nn_model(inputs)
            loss = criterion(y_pred, torch.unsqueeze(target,dim=1))

            loss.backward()

            optimizer.step()

            losses.append(loss.item())
            total += 1

        epoch_loss = sum(losses) / total



        losses.append(epoch_loss)

        mess = f"Epoch #{epoch+1} Loss: {losses[-1]}"
        tqdm.tqdm.write(mess)

    mlflow.log_metric("Loss",losses[-1])
    mlflow.pytorch.log_model(nn_model,artifact_path="model")

    fig = plt.figure()
    plt.plot(losses)
    with tempfile.TemporaryDirectory() as d:
        filename = 'plot.png'
        artifact_path = pathlib.Path(d) / filename
        print(artifact_path)
        fig.savefig(str(artifact_path))
        mlflow.log_artifact(str(artifact_path))

Azure Machine Learning Studio から実験を開いてみると、実験が増えています。

image.png

1回1回の Run も記録されています。

image.png

メトリックも記録されています。

image.png

画像もきちんと記録されています。

image.png

パラメーターが見当たりませんが、生JSONを開くと中に記録されていました。この状態だと GUI からは見づらいですが、しかし MLflow の Python API から叩く分には特に問題がないのでひとまずよしとします。

ひとまずこれで実験管理と部分的ではありますがモデル管理ができるようになりました。

MLflow Projects + MLflow Tracking + Azure Machine Learning による実験管理

実験管理の一連の工程は全てノートブック上での実験を前提としています。デバッグ等を済ませたら train.py のようなスクリプトファイルにまとめることになるかと思いますが、こうなると MLflow Projects の出番です。

MLflow Projects を使用することでローカルでの実行のみならずリモート (クラウド上の他の強力なリソース等) での実行すら可能になります。僕個人であればオンプレ GPU マシンに戻るときなどに重宝しそうですが、チームで開発を行っている場合には他人の実験の再現性を確保できるようになることが重要そうです。

雰囲気としてはソースコードとビルドの設定ファイルを使ってビルドを行うことに似ています。

先程使ったコードと実験の記録時に artifact として出力された conda.yml と公式サンプル4を参考に Project に必要な3つのファイル

  • train.py
  • conda.yml
  • MLproject

を作成します。

train.py
import torch
from torch import nn
from torch.nn import functional as F
import numpy as np
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
import torch.utils.data as Data
from adabelief_pytorch import AdaBelief
import tqdm
from matplotlib import pyplot as plt
from sklearn.metrics import mean_squared_error
import tempfile
import pathlib
import sys
import mlflow

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

# ハイパーパラメーター

hidden_1 = int(sys.argv[1]) if len(sys.argv) > 1 else 64
hidden_2 = int(sys.argv[2]) if len(sys.argv) > 1 else 16
batch_size = int(sys.argv[3]) if len(sys.argv) > 1 else 16
n_epochs = int(sys.argv[4]) if len(sys.argv) > 1 else 20

params = {
    "hidden_1":hidden_1,
    "hidden_2":hidden_2,
    "batch_size":batch_size,
    "n_epochs":n_epochs
}

# データセット 

boston = load_boston()
X_train, X_test = train_test_split(boston.data)
y_train, y_test = train_test_split(boston.target)

class BostonData(Data.Dataset):
    def __init__(self, X, y):
        self.targets = X.astype(np.float32)
        self.labels = y.astype(np.float32)

    def __getitem__(self, i):
        return self.targets[i, :], self.labels[i]

    def __len__(self):
        return len(self.targets)

train_dataset = BostonData(X_train, y_train)
test_dataset = BostonData(X_test, y_test)

train_loaded = Data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loaded = Data.DataLoader(test_dataset, batch_size=batch_size, shuffle=True)

# モデル

class Model(nn.Module):
    def __init__(self, n_features, hidden_1, hidden_2):
        super(Model, self).__init__()
        self.linear_1 = nn.Linear(n_features, hidden_1)
        self.linear_2 = nn.Linear(hidden_1, hidden_2)
        self.linear_3 = nn.Linear(hidden_2, 1)

    def forward(self, x):
        y = F.relu(self.linear_1(x))
        y = F.relu(self.linear_2(y))
        y = self.linear_3(y)
        return y

n_features = X_train.shape[1]
nn_model = Model(n_features, hidden_1, hidden_2)

# 最適化手法と損失関数

criterion = nn.MSELoss(size_average=False)
optimizer = AdaBelief(
    nn_model.parameters(),
    lr=1e-3,
    eps=1e-16,
    betas=(0.9,0.999),
    weight_decouple = True,
    rectify = False,
    print_change_log = False
)

# 学習

with mlflow.start_run():
    mlflow.log_params(params)

    losses = []
    for epoch in tqdm.tqdm(range(n_epochs)):
        losses = []
        total = 0
        for inputs, target in train_loaded:
            inputs.to(device)
            target.to(device)
            optimizer.zero_grad()

            y_pred = nn_model(inputs)
            loss = criterion(y_pred, torch.unsqueeze(target,dim=1))

            loss.backward()

            optimizer.step()

            losses.append(loss.item())
            total += 1

        epoch_loss = sum(losses) / total
        losses.append(epoch_loss)
        mess = f"Epoch #{epoch+1} Loss: {losses[-1]}"

    mlflow.log_metric("Loss",losses[-1])
    mlflow.pytorch.log_model(nn_model,artifact_path="model")

    fig = plt.figure()
    plt.plot(losses)
    with tempfile.TemporaryDirectory() as d:
        filename = 'plot.png'
        artifact_path = pathlib.Path(d) / filename
        print(artifact_path)
        fig.savefig(str(artifact_path))
        mlflow.log_artifact(str(artifact_path))

ノートブックで実行していたときとの違いはハイパーパラメーターをスクリプトの外から与えられるようになったことです。

conda.yml
channels:
- defaults
- conda-forge
- pytorch
dependencies:
- python=3.6.9
- pytorch=1.4.0
- torchvision=0.5.0
- pip
- pip:
  - mlflow
  - cloudpickle==1.6.0
name: mlflow-env

こちらは Azure Machine Learning の Experiment に自動で生成されていたファイルを持ってきています。

MLproject
name: mlflow-env

conda_env: conda.yaml

entry_points:
  main:
    parameters:
      hidden_1: {type: int, default: 64}
      hidden_2: {type: int, default: 16}
      batch_size: {type: int, default: 16}
      n_epochs: {type: int, default: 20}
    command: "python train.py {hidden_1} {hidden_2} {batch_size} {n_epochs}"

MLproject にはパラメーターを渡すためのブロックを追加しています。

entory_points は実行が複数段階、例えば前処理、学習1、学習2のように分かれる場合などに使いますが、今回は複数段階の実行はないのでデフォルトの main のままにしておきます。

以上のファイルを同一ディレクトリ内に収めます。

実験を行う際には以下のように Azure Machine Learning のワークスペースへの接続、実験名の定義、パラメーターの定義、ちょっとした設定を書くだけで実験が実行可能になります。

import mlflow
from azureml.core import Workspace
import os

ws = Workspace.from_config()
mlflow.set_tracking_uri(ws.get_mlflow_tracking_uri())

experiment_name = "pytorch-boston-project"
mlflow.set_experiment(experiment_name)

backend_config={"USE_CONDA": False}
params = {
    "hidden_1":32,
    "hidden_2":8,
    "batch_size":16,
    "n_epochs": 10
         }

local_env_run = mlflow.projects.run(uri=os.getcwd(), 
                                    parameters=params,
                                    backend = "azureml",
                                    use_conda=False,
                                    backend_config = backend_config)

backend_config はリモートで実行する際に変更します。

例えば Azure Machine Learning 配下にある cpu-cluster という名称のクラスターで実行する場合、下記のように変更します。5

backend_config = {"COMPUTE": "cpu-cluster", "USE_CONDA": False}

モデル管理

既に現時点でモデルの記録はできていますが、あくまでも artifact の1つとしての記録です。モデルは実験の管理コンポーネントである Experiment とは別に、 Registerd models でも記録が可能で、こちらにモデルを記録しているとコンテナ化してデプロイまでが容易になります。

Registerd models へのモデルの記録はmlflow.register_model で可能です。

with mlflow.start_run() as run:
    mlflow.log_params(params)

## 中略

    mlflow.log_metric("Loss",losses[-1])
    mlflow.pytorch.log_model(nn_model,artifact_path="model")

    model_uri = "runs:/{}/model".format(run.info.run_id)
    mlflow.register_model(model_uri, "PyTorchModel")

ですが、このやり方だと Azure Machine Learning 上では実験の記録との関連付けが切れていてあまり気持ちよくありません。そこで、azureml.mlflow の register_model を使用します。

import azureml.mlflow

with mlflow.start_run() as run:
    mlflow.log_params(params)

## 中略

    mlflow.log_metric("Loss",losses[-1])
    mlflow.pytorch.log_model(nn_model,artifact_path="model")

    #model_uri = "runs:/{}/model".format(run.info.run_id)
    #mlflow.register_model(model_uri, "PyTorchModel")

    azureml.mlflow.register_model(run, name="PyTorchModel", path="model")

実験の記録とも関連付けられた状態でモデルの登録でできている様子が確認できます。

image.png

デプロイ (準備中)

実験管理やモデル管理とは関係ありませんが、せっかくなのでAPIデプロイまでやってみようとおもいます。デプロイに時間がかかってデバッグが苦しいので、後で更新します。

作った機械学習モデルをデプロイする場合、もう1手間必要です。

エントリースクリプトと呼ばれるモデルのロードと予測値の出力を行うためのスクリプトを書く必要があります。6

終わりに

Azure Machine Learning を MLflow のバックエンドとして使用して、機械学習の実験管理とモデル管理ができました。

これでもう「model_adam_lr_0001_lstm_3_layer_e_512_h_1024.model」とはおさらばです。間違えてファイルを上書きしてしまって学習やり直しになったり、パラメーター設定を記録し忘れて真顔になったりしなくてよくなりました。

大学院時代にこれを知っていれば……。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Azure Machine Learning + MLflowで実験管理とモデル管理をしつつ機械学習モデル開発をする話

はじめに

Azure Machine Learning という機械学習周りのかなり広い範囲をカバーするサービスがあります。

Azure Machine Learning があれば、データセット管理からモデルの開発、実験と検証、デプロイまで、機械学習モデルの開発に必要なほぼほぼ全ての工程をこなすことができます。

しかし、使い慣れてる上に魔改造を繰り返して自分に最適化した開発環境が既にあるというのに、わざわざ新しい環境に一気に切り替えるのもあまり良い気持ちがしません。

今回は Azure Machine Learning の SDK を使い、その機能の一部、実験管理とモデル管理部分のみを既存の機械学習モデル開発環境に組み入れてみようと思います。

ただ Azure Machine Learning で実験やモデルの管理をするのではなく、MLflow のバックエンドとして Azure Machine Learning を取り扱うやり方で進めていこうと思います。

環境

これまで

機械学習用に作ったGPU搭載のLinux機にJupyterLab環境を用意し、メインPCから接続して使っていました。

コード管理こそある程度Githubを活用していますが、恥ずかしながら、実験管理については面倒でやっておらず、結局ハイパラコメント&ノートブックコピー&ファイル名へのハイパラ挿入で実験管理をしておりました。できあがったモデルの管理も杜撰なもので、「model_adam_lr_0001_lstm_3_layer_e_512_h_1024.model」みたいなヤバい名前をつけておりました……。

そんなやり方なので当然「あれ、あの時のハイパラどうだったっけ」とか「あ、間違えて既存のファイル上書きしちった」とか「出力した辞書なくした」とかポカが発生していました。

これから

既存の環境に実験管理、モデル管理を行えるツールを追加しようと思います。

実験管理ツールとして広く使われているツールといえば MLflow だと思います。使い方を調べてみると、Jupyter 同様にサーバー的な使い方をするようですが、実験やモデルまで機械学習環境と共倒れになるような状態は好ましくありません。別にサーバーを建てるのも管理が面倒です。

マネージドサービスとしての MLflow があったらいいなぁと思って探していると、現状 Databricks を使うか Azure Machine Learning の MLflow 互換 API を使用するかという2択が存在するようでした。

Databricks は個人的に結構好きでかなり心動きましたが、最終的に実験管理からモデルを活用する MLOps まで考えるなら各種コンテナ系サービスとの連携機能を備えてデプロイやログまで面倒を見てくれる Azure Machine Learning の方が良いと思ったので、お試しがてら今回はこちらを採用してみます。

サービス理解

Azure Machine Learning

azure-machine-learning-taxonomy.png

ドキュメント1にあったこの画像がわかりやすいのでこちらを起点に理解していきます。

最上位リソースとして Workspace が存在し、その配下に実験管理の Experiment 、モデル管理の Registerd models 、 デプロイの Deployment endpoints 等の機能が存在しているようです。

MLFlow

ドキュメント2によれば、 MLflow は以下4つのコンポーネントで構成されているようです。

  • MLflow Tracking
  • MLflow Projects
  • MLflow Models
  • MLflow Registry

Tracking はパラメーターやメトリックの記録や実行履歴の管理を担っているので、今回行いたい「実験管理」というのは Tracking の領分ということになりそうです。

Projcts が学習処理をどう行うかという環境情報を記録し、 Models が学習済みモデルの管理を担当しているので、この2つのコンポーネントが「モデル管理」を担うことになりそうです。

Azure Machine Learning と MLflow の関係

Experiment が Azure Machine Learning では実験管理を担当するコンポーネントですが、このコンポーネントが MLflow Tracking Server としても機能するように作られています。

MLflow Projects が担う環境情報の管理もまたこの Experiment の領分です。

MLflow ではあるモデルの実験全体を表す Experiment という単位に、1回の実験実行に相当する Run を実験回数分収めてます。この構造は Azure Machine Learning でも同じです。

Artifact というのはモデル実行で得られた各ファイルのことを指しているようです。Artifact は Run の一部として記録されています。

僕の場合だとよくモデル本体以外にもログのバックアップやグラフを画像出力したものを作っているので、こういうものも管理できるのはありがたいです。

MLflow Models が担うモデル管理は MLflow 互換のモデル定義に対応している Registerd models が担います。

試してみた

事前にやること

GPU 付きのインスタンスを使いたい場合、事前にサポートリクエストからクォータ (使用できるリソースの枠) 申請をする必要がある点に注意が必要です。また、通常の仮想マシンと Azure Machine Learning 配下のインスタンスでは異なる枠が設定されているようだったので、今回本記事では使用しませんが Azure Machine Learning で GPU インスタンスを使いたい場合はそちらのクォータ申請も必要なようです。

(Data Science VM のデプロイ)

使っている GPU 搭載機の調子が非常に悪く、いい機会なので Azure 上に GPU 付きの VM をデプロイしてそちらに一時的に移行しようと思います。オンプレ GPU マシンはロマンなので再構築したらまた戻ろうとは思っていますが、データを失うと本当に辛いので今回はクラウド上のJupyterLab環境を使っていきます。

Data Science Virtual Machine というイメージを使用すれば、JupyterHub や JupyterLab の設定等が一通り事前に行われていて楽なので、こちらを使用します。

image.png

Tesla V100 を搭載しているインスタンスである NC v3 系インスタンスを使用しています。

注意点として、可用性オプションがデフォルトではゾーン1に設定されていましたが、Data Science Virtual Machine は可用性オプションに対応してないようで、どのゾーンを指定してもエラーが出たので「インフラストラクチャ冗長は必要ありません」に変更しています。コードは GitHub に、実験記録やモデルについては Azure Machine Learning に任せる予定なので、計算環境は最悪死んでも構いません。

また、ログインに JupyterHub を使う関係上アカウントはユーザー名&パスワードの方が都合が良いので認証の種類を「パスワード」に変更しています。

ディスクやネットワークの設定はデフォルトのままでいきます。ディスク容量については扱うデータやモデルの規模次第では拡張する必要があるかもしれません。

間違って VM をつけっぱなしにすると非常に財布が辛いことになるので、自動シャットダウンは有効にしました。

https://\<VM IP\>:8000

で JupyterHub に接続できるので、オレオレ証明書特有のエラー画面を抜けて設定した管理者アカウントでログインすると見慣れた Jupyter Notebook 環境が表示されます。

https://\<VM IP\>:8000/user/\<username\>/tree?

のような URL が表示されていますが、ここで

https://\<VM IP\>:8000/user/\<username\>/lab

とすると JupyterLab 環境に移ります。デフォルトでこの辺りを変えるには JupyterHub の設定書き換えが必要と記憶していますが、面倒なので今回はこのままいきます。

image.png

Azure Machine Learning のデプロイ

Azure Machine Learning のワークスペースを用意します。

image.png

設定を変える必要は特になく、リソースグループとワークスペース名を決めたらそのまま作成に移ります。

デプロイが完了してリソースの画面へ移り、「config.json をダウンロード」をクリックします。これは SDK 越しに作ったAzure Machine Learning ワークスペースに接続するための情報が記載されたファイルです。中身はサブスクリプション ID とリソースグループ名、ワークスペース名です。手動入力でも十分対応できますが、今回はこちらを使用します。

image.png

「スタジオの起動」をクリックすると Azure Machine Learning Studio という GUI ベースで色々できる統合環境を使うことができます。

image.png

ここから色々と操作できるようです。(今回はほとんど SDK から操作するので使いませんが)

きれいな UI がやるとやる気が出るタイプなので俄然元気になってきました。

Azure Machine Learning Workspace への接続

Data Science Virtual Machine の JupyterLab 環境に移って、 azureml_py36_pytorch というカーネルを使用してノートブックを作ります。(自分で conda の仮想環境を作る場合、azureml と MLflow のインストールさえすれば特に問題はなさそうです)

まずはワークスペースへの接続を行います。先程ダウンロードした config.json をノートブックと同じディレクトリ内にアップロードする必要があります。

MLflow Tracking 互換のサーバーとして利用するので、 MLflow とワークスペースの紐付けもついでに行います。

import mlflow
from azureml.core import Workspace

ws = Workspace.from_config()

mlflow.set_tracking_uri(ws.get_mlflow_tracking_uri())

認証しろと出ているので提示された URL から認証を済ませます。

image.png

別ウィンドウでの認証を済ませて戻ってくるとセルの実行が完了して認証済みとなっていました。

image.png

実験の用意

ボストンの住宅価格データセットを使用して、 PyTorch でニューラルネットワークを組んで価格を予測するモデルを用意してみました。

なお、今回なんとなく最適化手法として AdaBelief を使っています。使ってみたかっただけです。

conda activate azureml_py36_pytorch
pip install adabelief-pytorch==0.2.0

としてパッケージをインストールするか、パッケージインストールが面倒なら Optimizer の部分を

optimizer = torch.optim.Adam(nn_model.parameters(), lr=0.001)

とでもして Adam に置換すれば動きます。

import torch
from torch import nn
from torch.nn import functional as F
import numpy as np
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
import torch.utils.data as Data
from adabelief_pytorch import AdaBelief
import tqdm
from matplotlib import pyplot as plt
from sklearn.metrics import mean_squared_error

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

# ハイパーパラメーター

hidden_1 = 64
hidden_2 = 16
batch_size = 16
n_epochs = 20

# データセット 

boston = load_boston()
X_train, X_test = train_test_split(boston.data)
y_train, y_test = train_test_split(boston.target)

class BostonData(Data.Dataset):
    def __init__(self, X, y):
        self.targets = X.astype(np.float32)
        self.labels = y.astype(np.float32)

    def __getitem__(self, i):
        return self.targets[i, :], self.labels[i]

    def __len__(self):
        return len(self.targets)

train_dataset = BostonData(X_train, y_train)
test_dataset = BostonData(X_test, y_test)

train_loaded = Data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loaded = Data.DataLoader(test_dataset, batch_size=batch_size, shuffle=True)

# モデル

class Model(nn.Module):
    def __init__(self, n_features, hidden_1, hidden_2):
        super(Model, self).__init__()
        self.linear_1 = nn.Linear(n_features, hidden_1)
        self.linear_2 = nn.Linear(hidden_1, hidden_2)
        self.linear_3 = nn.Linear(hidden_2, 1)

    def forward(self, x):
        y = F.relu(self.linear_1(x))
        y = F.relu(self.linear_2(y))
        y = self.linear_3(y)
        return y

n_features = X_train.shape[1]
nn_model = Model(n_features, hidden_1, hidden_2)

# 最適化手法と損失関数

criterion = nn.MSELoss(size_average=False)
optimizer = AdaBelief(
    nn_model.parameters(),
    lr=1e-3,
    eps=1e-16,
    betas=(0.9,0.999),
    weight_decouple = True,
    rectify = False,
    print_change_log = False
)

# 学習

losses = []
for epoch in range(n_epochs):
    progress_bar = tqdm.notebook.tqdm(train_loaded, leave=False)
    losses = []
    total = 0
    for inputs, target in progress_bar:
        inputs.to(device)
        target.to(device)
        optimizer.zero_grad()

        y_pred = nn_model(inputs)
        loss = criterion(y_pred, torch.unsqueeze(target,dim=1))

        loss.backward()

        optimizer.step()

        losses.append(loss.item())
        total += 1

    epoch_loss = sum(losses) / total
    losses.append(epoch_loss)

    mess = f"Epoch #{epoch+1} Loss: {losses[-1]}"
    tqdm.tqdm.write(mess)

plt.plot(losses)

MLflow Tracking + Azure Machine Learning による実験管理

MLFlow Tracking のサーバーとしてAzure Machine Learning の Experiment コンポーネントを使用しますが、 API 自体は MLflow のものを使用します。

1つの実験を定義します。

experiment_name = 'boston_nn_experiment'
mlflow.set_experiment(experiment_name)

メトリックを追跡する場合、下記のようにwith mlflow.start_run():の内側でmlflow.log_metricを使って記録していきます。

with mlflow.start_run():
    mlflow.log_metric('Loss', 0.03)

mlflow.log_metrics関数を使えば辞書型で一括で記録することもできます。

ハイパーパラメーターの記録はmlflow.log_parammlflow.log_paramsを使います。単数形だと1個ずつ、複数形だと辞書で一括なのはメトリックの記録と同様です。

params = {
    "hidden_1":hidden_1,
    "hidden_2":hidden_2,
    "batch_size":batch_size,
    "n_epochs":n_epochs
        }
with mlflow.start_run():
    mlflow.log_metrics(params)

実際に実験を実行して記録するときは先程までのコードの下に実行部分を書いていくか、mlflow.start_run()mlflow.end_run()で実行部分を挟む必要があります。

この程度であれば既存コードも簡単に書き直せそうでいいですね。

生成したファイルの記録はmlflow.log_artifactで行います。今まではディレクトリに直接書き出していましたが、以後はファイルの管理も Azure Machine Learning に任せるので、一時ディレクトリを生成して使う方式にしてみました。

fig = plt.figure()
plt.plot(losses)
with tempfile.TemporaryDirectory() as d:
    filename = 'plot.png'
    artifact_path = pathlib.Path(d) / filename
    print(artifact_path)
    fig.savefig(str(artifact_path))
    mlflow.log_artifact(str(artifact_path))

学習したモデルの登録はmlflow.pytorch.log_modelを使用します。artifact_path はモデルとその関連ファイルが収められる、管理ディレクトリの最上位フォルダの名称となります。第1引数で指定しているのはモデルのオブジェクトで、自前で pickle やら torch.save をしなくても、勝手にバイナリ形式に変換してくれるみたいです。

mlflow.pytorch.log_model(nn_model,artifact_path="model")

関数名を見ると明白ですが、mlflow.pytorch.log_model以外にも色々とあります。3

log_model と log_artifact で記録したファイルは1まとめに管理されます。もし MLflow が対応していないライブラリを使う場合でも artifact として登録すればとりあえず追跡できますね。

ここまでの mlflow の色々を組み込んで、学習部分のコードを下記のように書き換えました。

with mlflow.start_run():
    mlflow.log_params(params)

    losses = []
    for epoch in range(n_epochs):
        progress_bar = tqdm.notebook.tqdm(train_loaded, leave=False)
        losses = []
        total = 0
        for inputs, target in progress_bar:
            inputs.to(device)
            target.to(device)
            optimizer.zero_grad()

            y_pred = nn_model(inputs)
            loss = criterion(y_pred, torch.unsqueeze(target,dim=1))

            loss.backward()

            optimizer.step()

            losses.append(loss.item())
            total += 1

        epoch_loss = sum(losses) / total



        losses.append(epoch_loss)

        mess = f"Epoch #{epoch+1} Loss: {losses[-1]}"
        tqdm.tqdm.write(mess)

    mlflow.log_metric("Loss",losses[-1])
    mlflow.pytorch.log_model(nn_model,artifact_path="model")

    fig = plt.figure()
    plt.plot(losses)
    with tempfile.TemporaryDirectory() as d:
        filename = 'plot.png'
        artifact_path = pathlib.Path(d) / filename
        print(artifact_path)
        fig.savefig(str(artifact_path))
        mlflow.log_artifact(str(artifact_path))

Azure Machine Learning Studio から実験を開いてみると、実験が増えています。

image.png

1回1回の Run も記録されています。

image.png

メトリックも記録されています。

image.png

画像もきちんと記録されています。

image.png

パラメーターが見当たりませんが、生JSONを開くと中に記録されていました。この状態だと GUI からは見づらいですが、しかし MLflow の Python API から叩く分には特に問題がないのでひとまずよしとします。

ひとまずこれで実験管理と部分的ではありますがモデル管理ができるようになりました。

MLflow Projects + MLflow Tracking + Azure Machine Learning による実験管理

実験管理の一連の工程は全てノートブック上での実験を前提としています。デバッグ等を済ませたら train.py のようなスクリプトファイルにまとめることになるかと思いますが、こうなると MLflow Projects の出番です。

MLflow Projects を使用することでローカルでの実行のみならずリモート (クラウド上の他の強力なリソース等) での実行すら可能になります。僕個人であればオンプレ GPU マシンに戻るときなどに重宝しそうですが、チームで開発を行っている場合には他人の実験の再現性を確保できるようになることが重要そうです。

雰囲気としてはソースコードとビルドの設定ファイルを使ってビルドを行うことに似ています。

先程使ったコードと実験の記録時に artifact として出力された conda.yml と公式サンプル4を参考に Project に必要な3つのファイル

  • train.py
  • conda.yml
  • MLproject

を作成します。

train.py
import torch
from torch import nn
from torch.nn import functional as F
import numpy as np
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
import torch.utils.data as Data
from adabelief_pytorch import AdaBelief
import tqdm
from matplotlib import pyplot as plt
from sklearn.metrics import mean_squared_error
import tempfile
import pathlib
import sys
import mlflow

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

# ハイパーパラメーター

hidden_1 = int(sys.argv[1]) if len(sys.argv) > 1 else 64
hidden_2 = int(sys.argv[2]) if len(sys.argv) > 1 else 16
batch_size = int(sys.argv[3]) if len(sys.argv) > 1 else 16
n_epochs = int(sys.argv[4]) if len(sys.argv) > 1 else 20

params = {
    "hidden_1":hidden_1,
    "hidden_2":hidden_2,
    "batch_size":batch_size,
    "n_epochs":n_epochs
}

# データセット 

boston = load_boston()
X_train, X_test = train_test_split(boston.data)
y_train, y_test = train_test_split(boston.target)

class BostonData(Data.Dataset):
    def __init__(self, X, y):
        self.targets = X.astype(np.float32)
        self.labels = y.astype(np.float32)

    def __getitem__(self, i):
        return self.targets[i, :], self.labels[i]

    def __len__(self):
        return len(self.targets)

train_dataset = BostonData(X_train, y_train)
test_dataset = BostonData(X_test, y_test)

train_loaded = Data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loaded = Data.DataLoader(test_dataset, batch_size=batch_size, shuffle=True)

# モデル

class Model(nn.Module):
    def __init__(self, n_features, hidden_1, hidden_2):
        super(Model, self).__init__()
        self.linear_1 = nn.Linear(n_features, hidden_1)
        self.linear_2 = nn.Linear(hidden_1, hidden_2)
        self.linear_3 = nn.Linear(hidden_2, 1)

    def forward(self, x):
        y = F.relu(self.linear_1(x))
        y = F.relu(self.linear_2(y))
        y = self.linear_3(y)
        return y

n_features = X_train.shape[1]
nn_model = Model(n_features, hidden_1, hidden_2)

# 最適化手法と損失関数

criterion = nn.MSELoss(size_average=False)
optimizer = AdaBelief(
    nn_model.parameters(),
    lr=1e-3,
    eps=1e-16,
    betas=(0.9,0.999),
    weight_decouple = True,
    rectify = False,
    print_change_log = False
)

# 学習

with mlflow.start_run():
    mlflow.log_params(params)

    losses = []
    for epoch in tqdm.tqdm(range(n_epochs)):
        losses = []
        total = 0
        for inputs, target in train_loaded:
            inputs.to(device)
            target.to(device)
            optimizer.zero_grad()

            y_pred = nn_model(inputs)
            loss = criterion(y_pred, torch.unsqueeze(target,dim=1))

            loss.backward()

            optimizer.step()

            losses.append(loss.item())
            total += 1

        epoch_loss = sum(losses) / total
        losses.append(epoch_loss)
        mess = f"Epoch #{epoch+1} Loss: {losses[-1]}"

    mlflow.log_metric("Loss",losses[-1])
    mlflow.pytorch.log_model(nn_model,artifact_path="model")

    fig = plt.figure()
    plt.plot(losses)
    with tempfile.TemporaryDirectory() as d:
        filename = 'plot.png'
        artifact_path = pathlib.Path(d) / filename
        print(artifact_path)
        fig.savefig(str(artifact_path))
        mlflow.log_artifact(str(artifact_path))

ノートブックで実行していたときとの違いはハイパーパラメーターをスクリプトの外から与えられるようになったことです。

conda.yml
channels:
- defaults
- conda-forge
- pytorch
dependencies:
- python=3.6.9
- pytorch=1.4.0
- torchvision=0.5.0
- pip
- pip:
  - mlflow
  - cloudpickle==1.6.0
name: mlflow-env

こちらは Azure Machine Learning の Experiment に自動で生成されていたファイルを持ってきています。

MLproject
name: mlflow-env

conda_env: conda.yaml

entry_points:
  main:
    parameters:
      hidden_1: {type: int, default: 64}
      hidden_2: {type: int, default: 16}
      batch_size: {type: int, default: 16}
      n_epochs: {type: int, default: 20}
    command: "python train.py {hidden_1} {hidden_2} {batch_size} {n_epochs}"

MLproject にはパラメーターを渡すためのブロックを追加しています。

entory_points は実行が複数段階、例えば前処理、学習1、学習2のように分かれる場合などに使いますが、今回は複数段階の実行はないのでデフォルトの main のままにしておきます。

以上のファイルを同一ディレクトリ内に収めます。

実験を行う際には以下のように Azure Machine Learning のワークスペースへの接続、実験名の定義、パラメーターの定義、ちょっとした設定を書くだけで実験が実行可能になります。

import mlflow
from azureml.core import Workspace
import os

ws = Workspace.from_config()
mlflow.set_tracking_uri(ws.get_mlflow_tracking_uri())

experiment_name = "pytorch-boston-project"
mlflow.set_experiment(experiment_name)

backend_config={"USE_CONDA": False}
params = {
    "hidden_1":32,
    "hidden_2":8,
    "batch_size":16,
    "n_epochs": 10
         }

local_env_run = mlflow.projects.run(uri=os.getcwd(), 
                                    parameters=params,
                                    backend = "azureml",
                                    use_conda=False,
                                    backend_config = backend_config)

backend_config はリモートで実行する際に変更します。

例えば Azure Machine Learning 配下にある cpu-cluster という名称のクラスターで実行する場合、下記のように変更します。5

backend_config = {"COMPUTE": "cpu-cluster", "USE_CONDA": False}

モデル管理

既に現時点でモデルの記録はできていますが、あくまでも artifact の1つとしての記録です。モデルは実験の管理コンポーネントである Experiment とは別に、 Registerd models でも記録が可能で、こちらにモデルを記録しているとコンテナ化してデプロイまでが容易になります。

Registerd models へのモデルの記録はmlflow.register_model で可能です。

with mlflow.start_run() as run:
    mlflow.log_params(params)

## 中略

    mlflow.log_metric("Loss",losses[-1])
    mlflow.pytorch.log_model(nn_model,artifact_path="model")

    model_uri = "runs:/{}/model".format(run.info.run_id)
    mlflow.register_model(model_uri, "PyTorchModel")

ですが、このやり方だと Azure Machine Learning 上では実験の記録との関連付けが切れていてあまり気持ちよくありません。そこで、azureml.mlflow の register_model を使用します。

import azureml.mlflow

with mlflow.start_run() as run:
    mlflow.log_params(params)

## 中略

    mlflow.log_metric("Loss",losses[-1])
    mlflow.pytorch.log_model(nn_model,artifact_path="model")

    #model_uri = "runs:/{}/model".format(run.info.run_id)
    #mlflow.register_model(model_uri, "PyTorchModel")

    azureml.mlflow.register_model(run, name="PyTorchModel", path="model")

実験の記録とも関連付けられた状態でモデルの登録でできている様子が確認できます。

image.png

デプロイ (準備中)

実験管理やモデル管理とは関係ありませんが、せっかくなのでAPIデプロイまでやってみようとおもいます。デプロイに時間がかかってデバッグが苦しいので、後で更新します。

作った機械学習モデルをデプロイする場合、もう1手間必要です。

エントリースクリプトと呼ばれるモデルのロードと予測値の出力を行うためのスクリプトを書く必要があります。6

終わりに

Azure Machine Learning を MLflow のバックエンドとして使用して、機械学習の実験管理とモデル管理ができました。

これでもう「model_adam_lr_0001_lstm_3_layer_e_512_h_1024.model」とはおさらばです。間違えてファイルを上書きしてしまって学習やり直しになったり、パラメーター設定を記録し忘れて真顔になったりしなくてよくなりました。

大学院時代にこれを知っていれば……。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

python テキスト読み上げ (pyttsx3)

pyttsx3の理解を深める。

pyttsx3 はpythonのテキスト読み上げ変換ライブラリ。
題材はボディービルの掛け声にして勉強。
興味があるものを題材にした方が意欲がわく。

参考

https://pypi.org/project/pyttsx3/
https://coolfitness.jp/bodybuilding_kakegoe/

目次

1.install と import
2.使用法
3.調整
4.使ってみる(ボディービルの掛け声)

1.install と import

install import
pip install pyttsx3
import pyttsx3

2.使用法

基本形
engine = pyttsx3.init()
engine.say("切れてるよ")     #出力したい言葉
engine.runAndWait()

3.調整(速度:rate と音量:volume)

調整
engine = pyttsx3.init()


#rate デフォルト値は200
rate = engine.getProperty('rate')
engine.setProperty('rate',200)

#volume デフォルト値は1.0、設定は0.0~1.0
volume = engine.getProperty('volume')
engine.setProperty('volume',1.0)


engine.say("切れてるよ")
engine.runAndWait()

4.やってみる(ボディービルの掛け声)

やってみる
import pyttsx3
engine = pyttsx3.init()

#参照した言葉
words = ["腹筋、板チョコ!","ナイスバルク!","でかいよ!、他が見えない!",
        "土台が違うよ、土台が!","もうでかい!","切れてるよ!","バリバリ",
        "仕上がってるよ!","三角チョコパイ!","腹筋グレネード!",
        "腹筋ちぎりパン!","腹斜筋で大根をおろしたい!","脚が歩いている!",
        "グレートケツプリ!","カーフでかいよ!","胸がケツみたい!",
         "胸がはち切れる!","背中に羽が生えてる!","空も飛べるはず!",
        "背中に鬼が宿ってる!","背中にクリスマスツリー!",
         "上腕二頭筋ナイス!チョモランマ!","さんとうもいいね〜",
        "肩メロン!","肩にちっちゃいジープ乗せてんのかい!"]

rate = engine.getProperty("rate")
engine.setProperty("rate",200)

volume = engine.getProperty('volume')
engine.setProperty('volume',1.0)

#参照した言葉の出力
for word in words:
    engine.say(word)

engine.runAndWait()

漢字が正確に読めてないのはありますが、日本語が上手くない方が話している風が出ています。
個人的には満足です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

OpenCVの基本コード(python)

はじめに

pythonのopencvコード一覧。まだまだ書き足すつもりです。

ライブラリの読み込み

import cv2 # opencv
import numpy as np # numpy

画像の読み込み

img = cv2.imread('/home/kono/画像/image01.jpg') # imgに入れる

画像の書き出し

cv2.imwrite("/home/kono/画像/output_img.jpg", img)

画像の表示(不確定)

cv2.namedWindow('img', cv2.WINDOW_NORMAL) # ウインドウの大きさ設定
cv2.imshow('img', img)
cv2.waitKey(2000) # 画像の表示時間
cv2.waitKey(1)
cv2.destroyAllWindows() # ウインドウをすべて閉じる
cv2.waitKey(1)

ガウシアンフィルタ

gaus = cv2.GaussianBlur(img, ksize=(3, 3), sigmaX=10)

カーネルサイズと標準偏差x, yを指定

HSVでマスク処理

hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # HSV変換
lower_color = np.array([10, 100, 100]) # HSV下の値
upper_color = np.array([20, 240, 160]) # HSV上の値
hsv_mask = cv2.inRange(hsv, lower_color, upper_color) # 与えられた範囲で二値化
output = cv2.bitwise_and(img, img, mask = hsv_mask) # マスク
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pixel4のカメラで学ぶDepth map-4(pre-process最終回)

はじめに

この記事はNoteで連載している記事で扱っているCode部分のみを切り出しています。技術背景等にご興味がある方はNoteの記事の方をご参照ください。
 今回は前処理部分の最終回となります。今までDark shading補正及び画像修正処理で下準備ができたので、これらを統合し、Pre-process部分を完成させます。

Enum定義

 Global定数として使用する値を先に定義しておきます。画像サイズやプロジェクトのフォルダー関連情報を一箇所に集めておき管理しやすくするのが目的です。

parameter.py
class ProjectFolder(Enum):
    """ Define project folders
    TOP
     project top folder
    DARK
     folder of dark shading
    PGM
     folder for pgm, the folder should include left and right raw image data
    JPG
     folder for saving JPEG images
    DISPARITY
      folder for saving disparity images as a result of depth estimation
    """
    TOP = #Projectのトップレベルの絶対パス
    DARK = 'dark/'
    PGM = 'pgm/'
    JPG = 'jpg/'
    DISPARITY = 'disparity/'


class ImageSize(Enum):
    """ Define image size
    WIDTH
     image width
    HEIGHT
     image height
    """
    HEIGHT = 1512
    WIDTH = 2016

Main関数定義

 全ての準備は揃いました。それではMain関数を定義します。

main.py
import parameters
import dark
import dualpixel
import helper
import matplotlib.pyplot as plt


if __name__ == "__main__":
    # path setting
    DARK_PATH = parameters.ProjectFolder.TOP.value + parameters.ProjectFolder.DARK.value
    PGM_PATH = parameters.ProjectFolder.TOP.value + parameters.ProjectFolder.PGM.value
    JPG_PATH = parameters.ProjectFolder.TOP.value + parameters.ProjectFolder.JPG.value

    # image size setting
    width = int(parameters.ImageSize.WIDTH.value)
    height = int(parameters.ImageSize.HEIGHT.value)

    # initialize dark image
    print('--- Start dark shading correction ---')
    dk_sh = dark.Dark(DARK_PATH, 'pgm', dsize=(width, height))

    # calculate gain map for dark shading correction
    # left_gain_map     :use this map to correct left-PD image
    # right_gain_map    :use this map to correct right-PD image
    left_gain_map, right_gain_map = dk_sh.get_gain_map(kernal_size=32, analog_gain=[0.6, 0.6, 0.6], \
        left_offset=(0, 150), right_offset=(-80, 150))

    # initialize dualpd class
    print('--- Start raw data cooking ---')
    dualpd = dualpixel.DualPixel(PGM_PATH, 'pgm', dsize=(width, height))

    # set left and right gain map
    dualpd.set_dksh_gain_map(left_gain_map, left=True)
    dualpd.set_dksh_gain_map(right_gain_map, left=False)

    # run process
    raw_data_file_list, proc_imgs = dualpd.run_process(bi_kernel_size=5, bi_disp=75, \
        unsharp_sigma=2, eq_grid_size=2, eq_sigma=32, eq_mean_value=128)

    # write images
    print('--- Outputing the processed images to folder ---')
    helper.write_img_to_path(JPG_PATH, raw_data_file_list, proc_imgs)

    print('--- Finished !! ---')

今まで定義したものを処理順に並べて流すだけです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerでデータサイエンス環境構築

環境

OS: macOS Big Sur 11.1
Docker: 20.10.0
docker-compose: 1.27.4

注意点

コマンドの実行は管理者権限で行なってください。また、Docker、docker-composeはすでにインストールされているものとして進めていきます。

Docker imageはDockerfileを基に構築されます。Docker imageを一度buildした際はキャッシュが作成されます。キャッシュとは簡単に言えば、2回目以降の読込を高速化するものです。このキャッシュがあると、buildの際に優先的に用いられてしまいます。そのため、Dockerfileを更新した際はdocker-compose build --no-cacheでキャッシュを使用しないbuildをしましょう。

キャッシュを使用しないbuildですので、ものによってはかなり時間が掛かることもありますので、ご注意ください。

Dockerとは

Dockerとは、簡単にいえば仮想マシンのようなものです。ただ厳密には異なり、サーバーのカーネルを利用してプロセスやユーザーをサーバーごとに隔離し、あたかも別のマシンが動いているように動かすことができます。そのため、仮想化よりも軽量で高速に動きます。Dockerを使用することで簡単に環境構築をすることができ、OSの違いを考慮することなく簡単に自分の環境を共有できます。

事前準備

注意点にも記載していますが、すでにDocker、docker-composeはインストールされているものとします。その上で、ターミナル上で以下のコマンドを打ち込みます。

$ docker pull jupyter/datascience-notebook:latest

このコマンドで、DockerイメージをDocker Hubからローカルに引っ張ってきます。このとき使用するDockerイメージが決まっていれば、ご自分のお好きなものを選択しても良いです。Dockerイメージの選択にはJupyterの公式サイトをご確認いただき、選択してみてください。迷ったら上記のdatascience-notebookを使用すれば大丈夫です。

この段階で、ご自分のJupyterにパスワードを設定することが可能です。ローカルの環境でのみ作業をする場合は特に設定する必要はありませんが、もし必要であればコチラの記事が分かりやすかったので、参考にしてみてください。

その後、ご自分の任意のディレクトリに以下のDockerfileを保存してください。

Dockerfile

FROM jupyter/datascience-notebook

RUN pip install --upgrade pip
RUN pip install jupyterlab
RUN jupyter serverextension enable --py jupyterlab

### Jupyterlab 拡張機能
# Variable Inspector
RUN jupyter labextension install @lckr/jupyterlab_variableinspector
# Table of Contents
RUN jupyter labextension install @jupyterlab/toc

### settings
## prepare settings ※ ここのディレクトリはjupyterlabのSettingsのAdvanced Setting Editorを参照する
RUN mkdir -p /home/jovyan/.jupyter/lab/user-settings/@jupyterlab/apputils-extension
RUN mkdir -p /home/jovyan/.jupyter/lab/user-settings/@jupyterlab/notebook-extension
## user-settings ※ prepare settingsでディレクトリを生成していないとerror
# 黒背景
RUN echo '{"theme":"JupyterLab Dark"}' > \
  /home/jovyan/.jupyter/lab/user-settings/@jupyterlab/apputils-extension/themes.jupyterlab-settings
# 行番号表示
RUN echo '{"codeCellConfig": {"lineNumbers": true}}' > \
  /home/jovyan/.jupyter/lab/user-settings/@jupyterlab/notebook-extension/tracker.jupyterlab-settings

詳しく中身について解説はしませんが、このDockerfileでデータサイエンス環境Jupyterlabの設定を行なっています。コメント文を読めば何の目的に書いたコマンドなのかが分かるかと思いますので、興味のある方はぜひ詳しく見てみてください。

特にsetting以下はコマンドとして記載しておくことで、デフォルトのJupyterlabを黒背景、そして行番号を表示してくれるようになるので、個人的には記載必須です。

次にdocker-composeです。

docker-compose とは

docker-composeは、複数のコンテナで構成されるアプリケーションについて、Dockerイメージのビルドや各コンテナの起動・停止などをより簡単に行えるようにするものです。

docker-compose.yml

version: "3"
services:
  jupyterlab:
    build:
      context: .
      dockerfile: "Dockerfile"
    user: root
    container_name: con_jupyterlab
    image: jupyterlab
    ports:
      - "8888:8888"
    volumes:
      - "/Users/[user]/Documents/DataScience/jupyter:/home/jovyan/work"
    environment:
      GRANT_SUDO: "yes"
      TZ: Asia/Tokyo
    command: start.sh jupyter lab --NotebookApp.token=""

こちらについても詳しい解説はしません。適宜Dockerイメージ名、コンテナ名はご自身で作成したものを記載してください。volumesについては、コロン(:)前までのパスは、ご自身のDockerfileの存在するパスを指定してください。この部分が間違っていると、コンテナを停止する度にデータサイエンス環境で作業した内容が消去されてしまいます。必ず作業を行う前にデータが保持されているかの確認を忘れないようにしておいてください。

使用方法

ターミナルのcdコマンドを用いて、Dockerfile(docker-compose.yml)が存在しているディレクトリまで移動します。そこで、下記コマンドを実行します。

$ docker-compose up -d

実行した後、少々待ったら http://localhost:8888 にアクセスします。もし「ページを開けませんでした」とか「サーバに接続できません」と出たら、少し時間を開けた後に再度ページリロードを行なってみてください。それでもダメな場合はDockerfileかdocker-composeに間違いがありますので、ターミナル上のエラーメッセージを確認してみてください。

エラーが出ていなかったら、以上で作業は終わりです。お疲れさまでした。

おわりに

Dockerというものは、自分の環境を汚すことなく、開発環境を構築することができ、他人にも自分の環境を共有することが可能になります。これによって、環境による開発者同士の齟齬が起きなくなります。他にも軽量で高速といった特徴があるので、ぜひ勉強してみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BitMEXから入手したBTCのTradeデータを、PyTorchを使用してLSTMで予測を試みるも、勾配消失問題?によると思われるNaN問題等に挫折。気を取り直して、別売買ストラテジでバックテストを試してみた!

表題

BitMEXから入手したBTCのTradeデータを、PyTorchを使用してLSTMで予測を試みるも、勾配消失問題?によると思われるNaN問題等に挫折。気を取り直して、別売買ストラテジでバックテストを試してみた!

背景

機械学習及び、深層学習等を活用した、システムまたはソリューションを開発するスキル習得のために、まずはPythonを修学しました。
その集大成として、昨今話題の仮想通貨の価格変動のデータを取得し、分析してストラテジを検証するまでの流れを一通りやってみた結果を纏めた記事です。

何故PyTorch?何故BTC?

今後DeepLearningのフレームワークを活用していく中で、TnsorFlowは若干枯れている感がありました。しかし、最近PyTorchが話題になることが多かった事と、全く触ったことがなかったため、腰を据えていじって見たいと思いました。
BTC関連はこれから技術革新が進むと思った事と、APIが充実していることもあり、色々と試したりデータを入手し易いと思い選択しました。

結論、残項目

  1. 教師強制は難しく、今のモデルだと推論が収束しない
  2. 正弦波のようなパターンであればモデルがつくれるが、昨今の右肩上がりのパターンんだと「勾配消失問題」だと思われるNaNが発生してしまう。
  3. 簡易LSTMモデルだとバックテストの成績が良くない

流れ

  1. bitmexのデータ取得
  2. データの前処理
  3. LSTMのモデル構築
  4. 学習
  5. 推論
  6. 評価
  7. 考察

  8. 別のLSTMモデル構築

  9. バックテスト

  10. 必要なモジュール

  11. ソースコード1

  12. 実行結果ログ1

  13. 実行結果ログ2

  14. ソースコード2

  15. 実行結果ログ3

  16. 参考文献

BitMEXのデータ取得

最近は、日本から取引が出来ないためすっかり下火になっているようですが、APIは活用できるので活用させて頂きました。
ただ実施は、https://public.bitmex.com/?prefix=data/trade/ にTickのデータが置いてあるので、ファイルで撮ってきて活用しています。

データの前処理

VWAP計算

1分間隔で ボリューム加重平均価格(VWAP)を計算
事前定義された時間間隔で取引をグループ化します。何故なら、市場の取引は時間によって取引活性が変化するため
btc_prediction_by_lstm_pytorch.py1-df_vwap.plot.png

train/val/testデータ作成

ボリューム加重平均価格(VWAP)のデータを、モデル学習用データ0.65、モデル評価用データ0.08、推論用データ0.27に分ける

スケーリング

LSTMモデルをより速く収束させるに、データをスケーリングする
入力の値が大きいと、学習が遅くなる可能性がある
sklearnライブラリのStandardScalerを使用
スケーラーはトレーニングセットに適合し、検証およびテストセットで見えない取引データを変換するために使用
すべてのデータにスカラーを適合させると、モデルは過学習してしまい、このデータで良好な結果が得られるが、実際のデータでパフォーマンスが低下する

タイムバーデータ変換

スケーリング後、LSTMでのモデリングに適した形式にデータを変換
データの長いシーケンスを、単一のタイムバーだけシフトされる多くの短いシーケンス(シーケンスごとに100タイムバー)に変換
以下のプロットは、トレーニングセットの最初と2番目、3番目のシーケンス
両方のシーケンスの長さは100タイムバー
両方のシーケンスのターゲットが機能とほぼ同じであり、違いは最初と最後のタイムバーにあること
LSTMはトレーニングフェーズでシーケンスをどのように使用するか?
まず、最初のシーケンスに焦点を当てる
モデルは、インデックス0のタイムバーの特徴を取り、インデックス1のタイムバーのターゲットを予測しようとする
次に、インデックス1のタイムバーの特徴を取り、タイムバーのターゲットを予測しようとする
インデックス2などで。2番目のシーケンスの特徴は1番目のシーケンスの特徴から1タイムバーだけシフトされ、3番目のシーケンスの特徴は2番目のシーケンスから1タイムバーだけシフトされる
この手順では、多くの短いシーケンスが得られ、単一のタイムバーだけシフトされる
分類または回帰タスクでは、通常予測しようとしている一連の機能とターゲットがあることに注意
LSTMを使用したこの例では、フィーチャとターゲットは同じシーケンスからのもので、唯一の違いはターゲットが1タイムバーだけシフトされている
btc_prediction_by_lstm_pytorch.py2-plot_sequence.png

モデル構築と学習

LSTMのトレーニング
21個の隠れユニットを用いてLSTMを学習
単位数を少なくすることで、LSTMが完全に記憶する可能性が低くなるようにしている
学習には平均二乗誤差損失関数とAdamオプティマイザを用いる
学習率は0.001に設定し,5エポックごとに減衰する
バッチごとに100個のシーケンスを15エポックで学習
プロットから、学習損失と検証損失が6エポック目で収束
btc_prediction_by_lstm_pytorch.py3-optimization_1.plot_losses.png

モデル評価

テストセット

テストセットでモデルを評価
futureパラメータは5に設定
モデルが次の5つの時間帯(この例では5分)にあると考えられるVWAPを出力する
これにより、価格の変化が発生する数時間前に目に見えるようになる
プロットでは、予測値が実際のVWAPの値と密接に一致していることがわかる
しかし、将来のパラメータは5に設定されており、オレンジ色のラインはスパイクをカバーするのではなく、発生する前に反応しなければならない。
btc_prediction_by_lstm_pytorch.py4-df_result_1.plot.png

ズームイン

スパイクにズームインすると(開始時と終了時の1つともう1つの時系列)、予測値が実際の値を模倣していることがわかる
実際の値が方向を変えると、予測値が追従しますが、これでは役にたたない
未来のパラメータを増やしても同じことが起こる(予測線には影響しない)
btc_prediction_by_lstm_pytorch.py5-df_result_1.iloc.plot.png

モデルによる推論

モデルを使用して最初のテストシーケンスについて1000本のタイムバーを生成し、予測値、生成値、実際のVWAPを比較
モデルが予測値を出力している間は、実際の値に近いことが観察
しかし、値を生成し始めると、出力はほとんど正弦波に似ている
ある期間の後、値は9600に収束する
この動作は、モデルが実際の入力でのみ訓練され、生成された入力では訓練されなかったために起こっていると考えられる
モデルは生成された入力から出力を生成すると、次の値を生成するのが下手になる
教師強制でこの問題の是正を試みる
btc_prediction_by_lstm_pytorch.py6-generate_sequence.png

教師強制

教師強制は、
前の時間ステップの出力を入力とするリカレントニューラルネットワークを訓練する方法
RNNを訓練する際に、前の出力を現在の入力として使うことでシーケンスを生成することができる
訓練時にも同様の処理を行うことができるが、モデルが不安定になったり、収束しなかったりすることがある
教師強制は、訓練中にこれらの問題に対処するためのアプローチ
言語モデルでは一般的に使われている

今回は、Scheduled samplingと呼ばれるTeacher forcingの拡張機能を使用する
モデルは、学習中に一定の確率で、その生成された出力を入力として使用
最初は、モデルがその生成された出力を見る確率は小さく、訓練中に徐々に増加する
この例では、訓練中には増加しないランダムな確率を使用していることに注意

前と同じパラメータで、教師強制を有効にしたモデルを訓練
7エポック後、学習と検証の損失は収束
btc_prediction_by_lstm_pytorch.py7-optimization_2.plot_losses.png

モデル評価

以前と同様の予測されたシーケンスを観察
スパイクを拡大すると、予測値が実際の値を模倣しているようなモデルの挙動が観察できる
教師の強制では問題が解決しなかった。。。
btc_prediction_by_lstm_pytorch.py8-df_result_2.plot.png
btc_prediction_by_lstm_pytorch.py9-df_result_2.iloc.plot.png

モデルによる推論

教師強制で学習したモデルを用いて、最初のテストシーケンスの1000本のタイムバーを生成
btc_prediction_by_lstm_pytorch.py10-generate_sequence.png

考察

生成されたシーケンスに関する考察は,教師強制で訓練されたモデルから生成された値は,収束するまでに時間がかかるということ
もう一つは、シーケンスが増加しているとき、それはある点まで増加し続け、その後、減少し始め、シーケンスが収束するまでパターンが繰り返され、このパターンは、振幅が減少する正弦波のように見える

結論

検証の結果、モデルの予測がシーケンスの実際の値を模倣していることがわかる
第1のモデルと第2のモデルは、価格の変化を発生前に検出しない
別の特徴(ボリュームのような)を追加すると、モデルが発生する前に価格変化を検出するのに役立つかもしれないが、その場合、モデルは次のステップでそれらの出力を入力として使用するために2つの特徴を生成する必要があり、モデルが複雑になる
上のプロットで見られるように、モデルはVWAP時系列を予測する能力を持っているので、より複雑なモデル(複数のLSTMCellを使用し、隠れユニットの数を増やす)を使用しても役に立たないかもしれない
より高度な教師強制の方法で、モデルのシーケンス生成スキルを向上させる可能性はあるかもしれない。。。

別のLSTMモデル構築

  • 定数の設定
  • 教師データの作成
  • 価格の正規化
  • データの分割、TorchのTensorに変換
  • LSTMの学習モデル構築
  • まずはtrainデータのindexをランダムに入れ替える。最初のtime_steps分は使わない。
  • batch size毎にperm_idxの対象のindexを取得
  • LSTM入力用の時系列データの準備
  • pytorch LSTMの学習実施
  • validationデータの評価
  • validationの評価が良ければモデルを保存
  • bestモデルで予測する。
  • 簡易なストラテジでバックテストを行う

バックテスト

スコア

Start                     2020-11-24 13:20:00
End                       2020-12-23 23:50:00
Duration                     29 days 10:30:00
Exposure Time [%]                     10.2358
Equity Final [$]                       110980
Equity Peak [$]                        111948
Return [%]                            10.9804
Buy & Hold Return [%]                 20.5915
Return (Ann.) [%]                     255.219
Volatility (Ann.) [%]                 52.4708
Sharpe Ratio                          4.86403
Sortino Ratio                         30.6017
Calmar Ratio                          127.727
Max. Drawdown [%]                    -1.99816
Avg. Drawdown [%]                   -0.526232
Max. Drawdown Duration        7 days 22:30:00
Avg. Drawdown Duration        0 days 16:50:00
# Trades                                   26
Win Rate [%]                          73.0769
Best Trade [%]                        1.00127
Worst Trade [%]                      -1.00668
Avg. Trade [%]                       0.453453
Max. Trade Duration           0 days 10:40:00
Avg. Trade Duration           0 days 02:37:00
Profit Factor                         2.69042
Expectancy [%]                       0.457399
SQN                                   2.53193
_strategy                    myCustomStrategy
_equity_curve                             ...
_trades                       Size  EntryB...

プロット

btc_prediction_and_backtest_by_pytorch1.jpg
btc_prediction_and_backtest_by_pytorch6.jpg

必要なモジュール(下記のモジュールをpipでインストール)

pip install numpy pandas matplotlib dateutil pprint sklearn torch skorch backtesting bitmex

ソースコード1(PyTorchのLSTMで、データのスケーリングや教師強制にチャレンジしたコード)

btc_prediction_by_lstm_pytorch.py
# -*- coding: utf-8 -*-
'''
btc_prediction_by_lstm_pytorch.py

Copyright (C) 2020 HIROSE Ken-ichi (hirosenokensan@gmail.com) 
                                                 All rights reserved.
 This is free software with ABSOLUTELY NO WARRANTY.

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 02111-1307, USA
'''

import glob
import warnings

import os
import math
import time
import random
# import pprint
from dateutil import parser
from datetime import timedelta, datetime

import numpy as np
import pandas as pd
# import pandas_datareader.data as web

import matplotlib
import matplotlib.pyplot as plt

# import sklearn
from sklearn.preprocessing import StandardScaler

import torch
import torch.nn as nn
import torch.optim as optim
from torch.autograd import Variable
# import skorch

from backtesting import Backtest, Strategy
from backtesting.lib import plot_heatmaps

# import bitmex

class Model(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Model, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.lstm = nn.LSTMCell(self.input_size, self.hidden_size)
        self.linear = nn.Linear(self.hidden_size, self.output_size)

    def forward(self, input, future=0, y=None):
        outputs = []

        # h_t = torch.zeros(input.size(0), self.hidden_size, dtype=torch.float32)
        h_t = torch.zeros(input.size(0), self.hidden_size, dtype=torch.float32, device=cuda_device)
        # c_t = torch.zeros(input.size(0), self.hidden_size, dtype=torch.float32)
        c_t = torch.zeros(input.size(0), self.hidden_size, dtype=torch.float32, device=cuda_device)

        for i, input_t in enumerate(input.chunk(input.size(1), dim=1)):
            h_t, c_t = self.lstm(input_t, (h_t, c_t))
            # print("c_t:{}".format(c_t)) # NaNに変化している。
            # print("h_t:{}".format(h_t)) # NaNに変化している。
            output = self.linear(h_t)
            # print("output:{}".format(output)) # NaNに変化している。
            outputs += [output]
            # print("e-outputs:{}".format(outputs)) # NaNに変化している。

        for i in range(future):
            if y is not None and random.random() > 0.5:
                output = y[:, [i]]  # teacher forcing
            h_t, c_t = self.lstm(output, (h_t, c_t))
            output = self.linear(h_t)
            outputs += [output]
        outputs = torch.stack(outputs, 1).squeeze(2)
        # print("outputs:{}".format(outputs)) # NaNに変化している。
        return outputs

class Optimization:
    def __init__(self, model, loss_fn, optimizer, scheduler):
        self.model = model
        self.loss_fn = loss_fn
        self.optimizer = optimizer
        self.scheduler = scheduler
        self.train_losses = []
        self.val_losses = []
        self.futures = []

    @staticmethod
    def generate_batch_data(x, y, batch_size):
        for batch, i in enumerate(range(0, len(x) - batch_size, batch_size)):
            x_batch = x[i : i + batch_size]
            y_batch = y[i : i + batch_size]
            yield x_batch, y_batch, batch

    def train(
        self,
        x_train,
        y_train,
        x_val=None,
        y_val=None,
        batch_size=100,
        n_epochs=15,
        do_teacher_forcing=None,
    ):
        seq_len = x_train.shape[1]
        for epoch in range(n_epochs):
            startup = time.time()
            self.futures = []

            # with torch.autograd.detect_anomaly():
            train_loss = 0
            for x_batch, y_batch, batch in self.generate_batch_data(x_train, y_train, batch_size):
                y_pred = self._predict(x_batch, y_batch, seq_len, do_teacher_forcing)
                self.optimizer.zero_grad()
                loss = self.loss_fn(y_pred, y_batch)
                # print("tloss:{}".format(loss)) # NaNに変化している。
                loss.backward()
                # nn.utils.clip_grad_norm_(self.model.parameters(), 0.25) # https://pytorch.org/docs/stable/_modules/torch/nn/utils/clip_grad.html
                self.optimizer.step()
                train_loss += loss.item()
            self.scheduler.step()
            train_loss /= batch
            self.train_losses.append(train_loss)

            self._validation(x_val, y_val, batch_size)

            elapsed = time.time() - startup
            print(
                "Epoch %d Train loss: %.2f. Validation loss: %.2f. Avg future: %.2f. Elapsed time: %.2fs."
                 % (epoch + 1, train_loss, self.val_losses[-1], np.average(self.futures), elapsed)
            )

    def _predict(self, x_batch, y_batch, seq_len, do_teacher_forcing):
        # print("x_batch:{}".format(x_batch)) 
        if do_teacher_forcing:
            future = random.randint(1, int(seq_len) / 2)
            limit = x_batch.size(1) - future
            y_pred = self.model(x_batch[:, :limit], future=future, y=y_batch[:, limit:])
            # print("if-y_pred:{}".format(y_pred)) 
        else:
            # print("x_batch:{}".format(x_batch)) # NaNに変化している。
            future = 0
            y_pred = self.model(x_batch)
            # print("else-y_pred:{}".format(y_pred)) # NaNに変化している。
        self.futures.append(future)
        return y_pred

    def _validation(self, x_val, y_val, batch_size):
        if x_val is None or y_val is None:
            return
        with torch.no_grad():
            val_loss = 0
            batch = 1
            for x_batch, y_batch, batch in self.generate_batch_data(x_val, y_val, batch_size):
                y_pred = self.model(x_batch)
                loss = self.loss_fn(y_pred, y_batch)
                # print("vloss:{}".format(loss)) # NaNに変化している。 
                val_loss += loss.item()
            val_loss /= batch
            self.val_losses.append(val_loss)

    def evaluate(self, x_test, y_test, batch_size, future=1):
        with torch.no_grad():
            test_loss = 0
            actual, predicted = [], []
            for x_batch, y_batch, batch in self.generate_batch_data(x_test, y_test, batch_size):
                y_pred = self.model(x_batch, future=future)
                y_pred = (
                    y_pred[:, -len(y_batch) :] if y_pred.shape[1] > y_batch.shape[1] else y_pred
                )
                loss = self.loss_fn(y_pred, y_batch)
                # print("eloss:{}".format(loss)) # NaNに変化している。 
                test_loss += loss.item()
                actual += torch.squeeze(y_batch[:, -1]).data.cpu().numpy().tolist()
                predicted += torch.squeeze(y_pred[:, -1]).data.cpu().numpy().tolist()
            test_loss /= batch
            return actual, predicted, test_loss

    def plot_losses(self):
        plt.plot(self.train_losses, lw=1, label="Training loss")
        plt.plot(self.val_losses, lw=1, label="Validation loss")
        plt.legend()
        plt.title("Losses")

def transform_data(arr, seq_len):
    x, y = [], []
    for i in range(len(arr) - seq_len):
        x_i = arr[i : i + seq_len]
        y_i = arr[i + 1 : i + seq_len + 1]
        x.append(x_i)
        y.append(y_i)
    x_arr = np.array(x).reshape(-1, seq_len)
    y_arr = np.array(y).reshape(-1, seq_len)
    x_var = Variable(torch.from_numpy(x_arr).float().to(cuda_device))
    y_var = Variable(torch.from_numpy(y_arr).float().to(cuda_device))
    return x_var, y_var

def plot_sequence(axes, i, x_train, y_train):
    axes[i].set_title("%d. Sequence" % (i + 1))
    axes[i].set_xlabel("Time bars")
    axes[i].set_ylabel("Scaled VWAP")
    axes[i].plot(range(seq_len), x_train[i].cpu().numpy(), color="r", lw=1, label="Feature")
    axes[i].plot(range(1, seq_len + 1), y_train[i].cpu().numpy(), color="b", lw=1, label="Target")
    axes[i].legend()

def generate_sequence(scaler, model, x_sample, future=1000):
    y_pred_tensor = model(x_sample, future=future)
    y_pred = y_pred_tensor.cpu().tolist()
    y_pred = scaler.inverse_transform(y_pred)
    return y_pred

def to_dataframe(actual, predicted):
    return pd.DataFrame({"actual": actual, "predicted": predicted})

def inverse_transform(scalar, df, columns):
    for col in columns:
        df[col] = scaler.inverse_transform(df[col])
    return df

def minutes_of_new_data(symbol, kline_size, data):
    if len(data) > 0:
        old = parser.parse(data["timestamp"].iloc[-1])
    else:
        old = bitmex_client.Trade.Trade_getBucketed(symbol=symbol, 
                binSize=kline_size, count=1, reverse=False).result()[0][0]['timestamp']
    new = bitmex_client.Trade.Trade_getBucketed(symbol=symbol, 
                binSize=kline_size, count=1, reverse=True).result()[0][0]['timestamp']
    return old, new

def get_all_bitmex(symbol, kline_size, save = False):
    filename = 'data/%s-%s-data.csv' % (symbol, kline_size)
    if os.path.isfile(filename):
        data_df = pd.read_csv(filename)
    else:
        data_df = pd.DataFrame()
    oldest_point, newest_point = minutes_of_new_data(symbol, kline_size, data_df)
    delta_min = (newest_point - oldest_point).total_seconds()/60
    available_data = math.ceil(delta_min/binsizes[kline_size])
    rounds = math.ceil(available_data / batch_size)
    if rounds > 0:
        for round_num in range(rounds):
            time.sleep(1)
            new_time = (oldest_point + timedelta(minutes = round_num * batch_size * binsizes[kline_size]))
            data = bitmex_client.Trade.Trade_getBucketed(symbol=symbol, 
                    binSize=kline_size, count=batch_size, startTime = new_time).result()[0]
            temp_df = pd.DataFrame(data)
            data_df = data_df.append(temp_df)
    data_df.set_index('timestamp', inplace=True)
    if save and rounds > 0:
        data_df.to_csv(filename)
    return data_df

if __name__ == '__main__':
    os.chdir(os.path.dirname(os.path.abspath(__file__)))
    ownprefix = os.path.basename(__file__)

    warnings.simplefilter('ignore')
    pd.set_option('display.max_columns', 100)
    np.set_printoptions(precision=3, suppress=True, formatter={'float': '{: 0.2f}'.format}) #桁を揃える

    start_time = time.perf_counter()
    print("start time: ", datetime.now().strftime("%H:%M:%S"))

    print("pandas==%s" % pd.__version__)
    print("numpy==%s" % np.__version__)
    print("torch==%s" % torch.__version__)
    print("matplotlib==%s" % matplotlib.__version__)

    cuda_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print("cuda_device:",cuda_device)
    if cuda_device != "cpu": 
        print("devicde_name:",torch.cuda.get_device_name(torch.cuda.current_device()))
        torch.cuda.manual_seed(1)
    np.random.seed(1)
    random.seed(1)
    torch.manual_seed(1)

    if os.path.exists('{}.pickle'.format(ownprefix)):
        print("read_pickle:")
        df = pd.read_pickle('{}.pickle'.format(ownprefix))
    else:
        print("get_from_bitmex:")
        ## bitmex API
        # bitmex_api_key = ''    #Enter your own API-key here
        # bitmex_api_secret = '' #Enter your own API-secret here
        # binsizes = {"1m": 1, "5m": 5, "1h": 60, "1d": 1440}
        # batch_size = 750
        # bitmex_client = bitmex(test=False, api_key=bitmex_api_key, api_secret=bitmex_api_secret)
        # df = get_all_bitmex("XBTUSD","5m",save=True)
        ##
        # https://public.bitmex.com/?prefix=data/trade/
        files = sorted(glob.glob('./data/2019*.csv.gz'))
        # files = sorted(glob.glob('./data/2020*.csv.gz'))
        print("files:",files)
        df = pd.concat(map(pd.read_csv, files))
        df = df[df.symbol == 'XBTUSD']
        df.timestamp = pd.to_datetime(df.timestamp.str.replace('D', 'T'))
        df = df.sort_values('timestamp')
        df.set_index('timestamp', inplace=True)
        df.to_pickle('{}.pickle'.format(ownprefix))
        df.to_csv('{}.csv'.format(ownprefix))

    print("df.shape:",df.shape)
    print("df.tail:",df.tail(-5))


    '''
    1分間隔で ボリューム加重平均価格(VWAP)を計算
    事前定義された時間間隔で取引をグループ化します。何故なら、市場の取引は時間によって取引活性が変化するため
    '''
    df_vwap = df.groupby(pd.Grouper(freq="1Min")).apply(
                            lambda row: pd.np.sum(row.price * row.foreignNotional) 
                                        / pd.np.sum(row.foreignNotional))
    print("df_vwap.shape:",df_vwap.shape)

    df_vwap.plot(figsize=(14, 7))    
    plt.show()
    plt.savefig('{}1-df_vwap.plot.png'.format(ownprefix), dpi=175, constrained_layout=True, tight_layout=True)
    plt.close()

    '''
    ボリューム加重平均価格(VWAP)のデータを、モデル学習用データ0.65、モデル評価用データ0.08、推論用データ0.27に分ける
    '''
    train_len = round(len(df_vwap)*0.65)
    val_len = round(len(df_vwap)*0.08)

    df_train = df_vwap[:train_len].to_frame(name="vwap")
    print("df_train.shape:",df_train.shape)
    print("df_train.tail:",df_train.tail(-5))

    df_val = df_vwap[train_len:(train_len + val_len)].to_frame(name="vwap")
    print("df_val.shape:",df_val.shape)
    print("df_val.tail:",df_val.tail(-5))

    df_test = df_vwap[(train_len + val_len):].to_frame(name='vwap')
    print("df_test.shape:",df_test.shape)
    print("df_test.tail:",df_test.tail(-5))

    '''
    LSTMモデルをより速く収束させるに、データをスケーリングする
    入力の値が大きいと、学習が遅くなる可能性がある
    sklearnライブラリのStandardScalerを使用
    スケーラーはトレーニングセットに適合し、検証およびテストセットで見えない取引データを変換するために使用
    すべてのデータにスカラーを適合させると、モデルは過学習してしまい、このデータで良好な結果が得られるが、実際のデータでパフォーマンスが低下する 
    '''
    scaler = StandardScaler()
    print("scaler type:",type(scaler),"\n",scaler)

    train_arr = scaler.fit_transform(df_train)
    print("train_arr.shape:",train_arr.shape,"train_arr type:",type(train_arr),"\n",train_arr)

    val_arr = scaler.transform(df_val)
    print("val_arr.shape:",val_arr.shape,"val_arr type:",type(val_arr),"\n",val_arr)

    test_arr = scaler.transform(df_test)
    print("test_arr.shape:",test_arr.shape,"test_arr type:",type(test_arr),"\n",test_arr)

    '''
    スケーリング後、LSTMでのモデリングに適した形式にデータを変換
    データの長いシーケンスを、単一のタイムバーだけシフトされる多くの短いシーケンス(シーケンスごとに100タイムバー)に変換
    以下のプロットは、トレーニングセットの最初と2番目、3番目のシーケンス
    両方のシーケンスの長さは100タイムバー
    両方のシーケンスのターゲットが機能とほぼ同じであり、違いは最初と最後のタイムバーにあること
    LSTMはトレーニングフェーズでシーケンスをどのように使用するか?
    まず、最初のシーケンスに焦点を当てる
    モデルは、インデックス0のタイムバーの特徴を取り、インデックス1のタイムバーのターゲットを予測しようとする
    次に、インデックス1のタイムバーの特徴を取り、タイムバーのターゲットを予測しようとする
    インデックス2などで。2番目のシーケンスの特徴は1番目のシーケンスの特徴から1タイムバーだけシフトされ、3番目のシーケンスの特徴は2番目のシーケンスから1タイムバーだけシフトされる
    この手順では、多くの短いシーケンスが得られ、単一のタイムバーだけシフトされる
    分類または回帰タスクでは、通常予測しようとしている一連の機能とターゲットがあることに注意
    LSTMを使用したこの例では、フィーチャとターゲットは同じシーケンスからのもので、唯一の違いはターゲットが1タイムバーだけシフトされている
    '''
    seq_len = 100
    x_train, y_train = transform_data(train_arr, seq_len)
    print("x_train.shape:",x_train.shape,"x_train type:",type(x_train),"\n",x_train)
    print("y_train.shape:",y_train.shape,"y_train type:",type(y_train),"\n",y_train)

    x_val, y_val = transform_data(val_arr, seq_len)
    print("x_val.shape:",x_val.shape,"x_val type:",type(x_val),"\n",x_val)
    print("y_val.shape:",y_val.shape,"y_val type:",type(y_val),"\n",y_val)

    x_test, y_test = transform_data(test_arr, seq_len)
    print("x_test.shape:",x_test.shape,"x_test type:",type(x_test),"\n",x_test)
    print("y_test.shape:",y_test.shape,"y_test type:",type(y_test),"\n",y_test)

    fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(14, 7))
    plot_sequence(axes, 0, x_train, y_train)
    plot_sequence(axes, 1, x_train, y_train)    
    plot_sequence(axes, 2, x_train, y_train)    
    plt.show()
    plt.savefig('{}2-plot_sequence.png'.format(ownprefix), dpi=175, constrained_layout=True, tight_layout=True)
    plt.close()

    '''
    LSTMのトレーニング
    21個の隠れユニットを用いてLSTMを学習
    単位数を少なくすることで、LSTMが完全に記憶する可能性が低くなるようにしている
    学習には平均二乗誤差損失関数とAdamオプティマイザを用いる
    学習率は0.001に設定し,5エポックごとに減衰する
    バッチごとに100個のシーケンスを15エポックで学習
    プロットから、学習損失と検証損失が6エポック目で収束
    '''
    # model_1 = Model(input_size=1, hidden_size=21, output_size=1)
    model_1 = Model(input_size=1, hidden_size=21, output_size=1).to(cuda_device)
    print("model_1 type:",type(model_1),"\n",model_1)

    loss_fn_1 = nn.MSELoss()
    # loss_fn_1 = nn.BCELoss()
    # loss_fn_1 = nn.BCEWithLogitsLoss()
    print("loss_fn_1 type:",type(loss_fn_1),"\n",loss_fn_1)

    optimizer_1 = optim.Adam(model_1.parameters(), lr=1e-4)
    print("optimizer_1 type:",type(optimizer_1),"\n",optimizer_1)

    scheduler_1 = optim.lr_scheduler.StepLR(optimizer_1, step_size=5, gamma=0.1)
    # scheduler_1 = torch.optim.lr_scheduler.MultiStepLR(optimizer_1, milestones=[2, 6], gamma=0.1)
    print("scheduler_1 type:",type(scheduler_1),"\n",scheduler_1)

    optimization_1 = Optimization(model_1, loss_fn_1, optimizer_1, scheduler_1)
    print("optimization_1 type:",type(optimization_1),"\n",optimization_1)

    optimization_1.train(x_train, y_train, x_val, y_val, do_teacher_forcing=False)
    print("optimization_1 type:",type(optimization_1),"\n",optimization_1)

    optimization_1.plot_losses()    
    plt.show()
    plt.savefig('{}3-optimization_1.plot_losses.png'.format(ownprefix), dpi=175, constrained_layout=True, tight_layout=True)
    plt.close()

    '''
    テストセットでモデルを評価
    futureパラメータは5に設定
    モデルが次の5つの時間帯(この例では5分)にあると考えられるVWAPを出力する
    これにより、価格の変化が発生する数時間前に目に見えるようになる
    プロットでは、予測値が実際のVWAPの値と密接に一致していることがわかる
    しかし、将来のパラメータは5に設定されており、オレンジ色のラインはスパイクをカバーするのではなく、発生する前に反応しなければならない。
    '''
    actual_1, predicted_1, test_loss_1 = optimization_1.evaluate(x_test, y_test, batch_size=100, future=5)
    print("Test loss %.4f" % test_loss_1)
    df_result_1 = to_dataframe(actual_1, predicted_1) 
    df_result_1 = inverse_transform(scaler, df_result_1, ['actual', 'predicted'])

    df_result_1.plot(figsize=(14*2, 7), lw=0.3)    
    plt.show()
    plt.savefig('{}4-df_result_1.plot.png'.format(ownprefix), dpi=175, constrained_layout=True, tight_layout=True)
    plt.close()

    '''
    スパイクにズームインすると(開始時と終了時の1つともう1つの時系列)、予測値が実際の値を模倣していることがわかる
    実際の値が方向を変えると、予測値が追従しますが、これでは役にたたない
    未来のパラメータを増やしても同じことが起こる(予測線には影響しない)
    '''
    fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(14, 7))
    df_result_1.iloc[2350:2450].plot(ax=axes[0], figsize=(14, 7), lw=0.3)
    df_result_1.iloc[16000:17500].plot(ax=axes[1], figsize=(14, 7), lw=0.3)    
    plt.show()
    plt.savefig('{}5-df_result_1.iloc.plot.png'.format(ownprefix), dpi=175, constrained_layout=True, tight_layout=True)
    plt.close()

    '''
    モデルを使用して最初のテストシーケンスについて1000本のタイムバーを生成し、予測値、生成値、実際のVWAPを比較
    モデルが予測値を出力している間は、実際の値に近いことが観察
    しかし、値を生成し始めると、出力はほとんど正弦波に似ている
    ある期間の後、値は9600に収束する
    '''
    x_sample = x_test[0].reshape(1, -1)
    y_sample = df_test.vwap[:1100]     
    y_pred1 = generate_sequence(scaler, optimization_1.model, x_sample)

    plt.figure(figsize=(14, 7))
    plt.plot(range(100), y_pred1[0][:100], color="blue", lw=1, label="Predicted VWAP")
    plt.plot(range(100, 1100), y_pred1[0][100:], "--", color="blue", lw=1, label="Generated VWAP")
    plt.plot(range(0, 1100), y_sample, color="red", lw=1, label="Actual VWAP")
    plt.legend()    
    plt.show()
    plt.savefig('{}6-generate_sequence.png'.format(ownprefix), dpi=175, constrained_layout=True, tight_layout=True)
    plt.close()

    '''
    この動作は、モデルが実際の入力でのみ訓練され、生成された入力では訓練されなかったために起こっていると考えられる
    モデルは生成された入力から出力を生成すると、次の値を生成するのが下手になる
    教師強制でこの問題の是正を試みる

    [教師強制](https://machinelearningmastery.com/teacher-forcing-for-recurrent-neural-networks/)は、
    前の時間ステップの出力を入力とするリカレントニューラルネットワークを訓練する方法
    RNNを訓練する際に、前の出力を現在の入力として使うことでシーケンスを生成することができる
    訓練時にも同様の処理を行うことができるが、モデルが不安定になったり、収束しなかったりすることがある
    教師強制は、訓練中にこれらの問題に対処するためのアプローチ
    言語モデルでは一般的に使われている

    今回は、[Scheduled sampling](https://arxiv.org/abs/1506.03099)と呼ばれるTeacher forcingの拡張機能を使用する
    モデルは、学習中に一定の確率で、その生成された出力を入力として使用
    最初は、モデルがその生成された出力を見る確率は小さく、訓練中に徐々に増加する
    この例では、訓練中には増加しないランダムな確率を使用していることに注意

    前と同じパラメータで、教師強制を有効にしたモデルを訓練
    7エポック後、学習と検証の損失は収束
    '''
    # model_2 = Model(input_size=1, hidden_size=21, output_size=1)
    model_2 = Model(input_size=1, hidden_size=21, output_size=1).to(cuda_device)
    loss_fn_2 = nn.MSELoss()
    optimizer_2 = optim.Adam(model_2.parameters(), lr=1e-4)
    scheduler_2 = optim.lr_scheduler.StepLR(optimizer_2, step_size=5, gamma=0.1)
    optimization_2 = Optimization(model_2, loss_fn_2,  optimizer_2, scheduler_2)
    optimization_2.train(x_train, y_train, x_val, y_val, do_teacher_forcing=True)

    optimization_2.plot_losses()
    plt.show()
    plt.savefig('{}7-optimization_2.plot_losses.png'.format(ownprefix), dpi=175, constrained_layout=True, tight_layout=True)
    plt.close()


    '''
    以前と同様の予測されたシーケンスを観察
    スパイクを拡大すると、予測値が実際の値を模倣しているようなモデルの挙動が観察できる
    教師の強制では問題が解決しなかった。。。
    '''
    actual_2, predicted_2, test_loss_2 = optimization_2.evaluate(x_test, y_test, batch_size=100, future=5)
    print("Test loss %.4f" % test_loss_2)
    df_result_2 = to_dataframe(actual_2, predicted_2)
    df_result_2 = inverse_transform(scaler, df_result_2, ["actual", "predicted"])

    df_result_2.plot(figsize=(14*2, 7), lw=0.3)
    plt.show()
    plt.savefig('{}8-df_result_2.plot.png'.format(ownprefix), dpi=175, constrained_layout=True, tight_layout=True)
    plt.close()

    fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(14, 7))
    df_result_2.iloc[2350:2450].plot(ax=axes[0], figsize=(14, 7), lw=0.3)
    df_result_2.iloc[16000:17500].plot(ax=axes[1], figsize=(14, 7), lw=0.3)    
    plt.show()
    plt.savefig('{}9-df_result_2.iloc.plot.png'.format(ownprefix), dpi=175, constrained_layout=True, tight_layout=True)
    plt.close()


    '''
    教師強制で学習したモデルを用いて、最初のテストシーケンスの1000本のタイムバーを生成
    '''
    y_pred2 = generate_sequence(scaler, optimization_2.model, x_sample)

    plt.figure(figsize=(14, 7))
    plt.plot(range(100), y_pred2[0][:100], color="blue", lw=1, label="Predicted VWAP")
    plt.plot(range(100, 1100), y_pred2[0][100:], "--", color="blue", lw=1, label="Generated VWAP")
    plt.plot(range(0, 1100), y_sample, color="red", lw=1, label="Actual VWAP")
    plt.legend()
    plt.show()
    plt.savefig('{}10-generate_sequence.png'.format(ownprefix), dpi=175, constrained_layout=True, tight_layout=True)
    plt.close()


    end_time = time.perf_counter()
    print("end time: ", datetime.now().strftime("%H:%M:%S"))

    time = end_time - start_time
    print("end_time - start_time:%f" % (time))

    '''
    生成されたシーケンスに関する興味深い考察は,教師強制で訓練されたモデルから生成された値は,収束するまでに時間がかかるということ
    もう一つの考察は、シーケンスが増加しているとき、それはある点まで増加し続け、その後、減少し始め、シーケンスが収束するまでパターンが繰り返される
    このパターンは、振幅が減少する正弦波のように見える

    ## 結論
    検証の結果、モデルの予測がシーケンスの実際の値を模倣していることがわかる
    第1のモデルと第2のモデルは、価格の変化を発生前に検出しない
    別の特徴(ボリュームのような)を追加すると、モデルが発生する前に価格変化を検出するのに役立つかもしれないが、
    その場合、モデルは次のステップでそれらの出力を入力として使用するために2つの特徴を生成する必要があり、モデルが複雑になる
    上のプロットで見られるように、モデルはVWAP時系列を予測する能力を持っているので、
    より複雑なモデル(複数のLSTMCellを使用し、隠れユニットの数を増やす)を使用しても役に立たないかもしれない
    より高度な教師強制の方法で、モデルのシーケンス生成スキルを向上させる可能性はあるかもしれない。。。

    ## 参考文献
     - [時系列予測](https://github.com/pytorch/examples/tree/master/time_sequence_prediction)
     - [LSTMネットワークを理解する](https://colah.github.io/posts/2015-08-Understanding-LSTMs/)
     - [リカレントニューラルネットワークのための教師強制とは何か](https://machinelearningmastery.com/teacher-forcing-for-recurrent-neural-networks/)
     - [リカレントニューラルネットワークを用いた配列予測のためのスケジューリング](https://arxiv.org/abs/1506.03099)
    '''

実行結果ログ1(ソースコード1に2019年のBTCのデータを読み込ませた際のログ)

$ python3 btc_prediction_by_lstm_pytorch.py
start time:  19:56:49
pandas==1.1.4
numpy==1.19.2
torch==1.5.0
matplotlib==3.3.3
cuda_device: cuda
devicde_name: GeForce RTX 2070
get_from_bitmex:
files: ['./data/20190801.csv.gz', './data/20190802.csv.gz', './data/20190803.csv.gz', './data/20190804.csv.gz', './data/20190805.csv.gz', './data/20190806.csv.gz', './data/20190807.csv.gz', './data/20190808.csv.gz', './data/20190809.csv.gz', './data/20190810.csv.gz', './data/20190811.csv.gz', './data/20190812.csv.gz', './data/20190813.csv.gz', './data/20190814.csv.gz', './data/20190815.csv.gz', './data/20190816.csv.gz', './data/20190817.csv.gz', './data/20190818.csv.gz', './data/20190819.csv.gz', './data/20190820.csv.gz', './data/20190821.csv.gz', './data/20190822.csv.gz', './data/20190823.csv.gz', './data/20190824.csv.gz', './data/20190825.csv.gz', './data/20190826.csv.gz', './data/20190827.csv.gz', './data/20190828.csv.gz', './data/20190829.csv.gz', './data/20190830.csv.gz', './data/20190831.csv.gz', './data/20190901.csv.gz', './data/20190902.csv.gz', './data/20190903.csv.gz', './data/20190904.csv.gz', './data/20190905.csv.gz', './data/20190906.csv.gz', './data/20190907.csv.gz', './data/20190908.csv.gz', './data/20190909.csv.gz', './data/20190910.csv.gz', './data/20190911.csv.gz', './data/20190912.csv.gz', './data/20190913.csv.gz', './data/20190914.csv.gz', './data/20190915.csv.gz', './data/20190916.csv.gz', './data/20190917.csv.gz']
df.shape: (36708098, 9)
df.tail:                             symbol  side   size    price  tickDirection  \
timestamp
2019-08-01 00:00:03.950526  XBTUSD   Buy     35  10089.0   ZeroPlusTick
2019-08-01 00:00:03.950526  XBTUSD   Buy     35  10089.0   ZeroPlusTick
2019-08-01 00:00:03.950526  XBTUSD   Buy     40  10089.0   ZeroPlusTick
2019-08-01 00:00:03.950526  XBTUSD   Buy   3117  10089.0   ZeroPlusTick
2019-08-01 00:00:03.956035  XBTUSD   Buy  18670  10089.0   ZeroPlusTick
...                            ...   ...    ...      ...            ...
2019-09-17 23:59:59.189310  XBTUSD  Sell   2000  10184.5  ZeroMinusTick
2019-09-17 23:59:59.189310  XBTUSD  Sell  15000  10184.5  ZeroMinusTick
2019-09-17 23:59:59.189310  XBTUSD  Sell  45383  10184.5  ZeroMinusTick
2019-09-17 23:59:59.517938  XBTUSD  Sell  10000  10184.5  ZeroMinusTick
2019-09-17 23:59:59.531223  XBTUSD  Sell      1  10184.5  ZeroMinusTick

                                                      trdMatchID  grossValue  \
timestamp
2019-08-01 00:00:03.950526  cec26c94-563c-7fcb-6194-d03ae2f41b92      346920
2019-08-01 00:00:03.950526  607b403e-4211-abda-8392-4003ad9f9ad0      346920
2019-08-01 00:00:03.950526  4d5cff30-eb11-43fe-caf4-1e150bc0bc18      396480
2019-08-01 00:00:03.950526  ab4806f1-2fac-6949-a5bc-07b4da4048f0    30895704
2019-08-01 00:00:03.956035  63205fcd-5165-3ac9-35d8-a39c632a5e60   185057040
...                                                          ...         ...
2019-09-17 23:59:59.189310  f2385097-5527-3075-3498-0b660dd39e4c    19638000
2019-09-17 23:59:59.189310  af7e0ceb-f670-b863-abb3-0feb76066711   147285000
2019-09-17 23:59:59.189310  b954d196-c63a-f86c-2599-cb86b409bbb6   445615677
2019-09-17 23:59:59.517938  69391f3f-65bc-c3bc-548d-963ffe5501df    98190000
2019-09-17 23:59:59.531223  6926bf37-72b7-3ac8-2c8a-3cdf22688ec0        9819

                            homeNotional  foreignNotional
timestamp
2019-08-01 00:00:03.950526      0.003469             35.0
2019-08-01 00:00:03.950526      0.003469             35.0
2019-08-01 00:00:03.950526      0.003965             40.0
2019-08-01 00:00:03.950526      0.308957           3117.0
2019-08-01 00:00:03.956035      1.850570          18670.0
...                                  ...              ...
2019-09-17 23:59:59.189310      0.196380           2000.0
2019-09-17 23:59:59.189310      1.472850          15000.0
2019-09-17 23:59:59.189310      4.456157          45383.0
2019-09-17 23:59:59.517938      0.981900          10000.0
2019-09-17 23:59:59.531223      0.000098              1.0

[36708093 rows x 9 columns]
df_vwap.shape: (69120,)
df_train.shape: (44928, 1)
df_train.tail:                              vwap
timestamp
2019-08-01 00:05:00  10108.710704
2019-08-01 00:06:00  10122.709866
2019-08-01 00:07:00  10122.340347
2019-08-01 00:08:00  10126.617881
2019-08-01 00:09:00  10147.380407
...                           ...
2019-09-01 04:43:00   9625.001311
2019-09-01 04:44:00   9625.045365
2019-09-01 04:45:00   9625.063765
2019-09-01 04:46:00   9626.437824
2019-09-01 04:47:00   9630.077496

[44923 rows x 1 columns]
df_val.shape: (5530, 1)
df_val.tail:                              vwap
timestamp
2019-09-01 04:53:00   9630.231584
2019-09-01 04:54:00   9629.291041
2019-09-01 04:55:00   9626.054305
2019-09-01 04:56:00   9626.067363
2019-09-01 04:57:00   9626.485287
...                           ...
2019-09-05 00:53:00  10545.065878
2019-09-05 00:54:00  10545.195416
2019-09-05 00:55:00  10542.311323
2019-09-05 00:56:00  10538.363399
2019-09-05 00:57:00  10537.195479

[5525 rows x 1 columns]
df_test.shape: (18662, 1)
df_test.tail:                              vwap
timestamp
2019-09-05 01:03:00  10522.959503
2019-09-05 01:04:00  10521.220404
2019-09-05 01:05:00  10517.852199
2019-09-05 01:06:00  10520.261375
2019-09-05 01:07:00  10520.428804
...                           ...
2019-09-17 23:55:00  10191.031001
2019-09-17 23:56:00  10194.615079
2019-09-17 23:57:00  10193.758451
2019-09-17 23:58:00  10187.193670
2019-09-17 23:59:00  10184.758720

[18657 rows x 1 columns]
scaler type: <class 'sklearn.preprocessing._data.StandardScaler'>
 StandardScaler()
train_arr.shape: (44928, 1) train_arr type: <class 'numpy.ndarray'>
 [[-0.71]
 [-0.69]
 [-0.71]
 ...
 [-1.37]
 [-1.36]
 [-1.36]]
val_arr.shape: (5530, 1) val_arr type: <class 'numpy.ndarray'>
 [[-1.36]
 [-1.36]
 [-1.36]
 ...
 [-0.09]
 [-0.10]
 [-0.10]]
test_arr.shape: (18662, 1) test_arr type: <class 'numpy.ndarray'>
 [[-0.10]
 [-0.10]
 [-0.11]
 ...
 [-0.58]
 [-0.59]
 [-0.59]]
x_train.shape: torch.Size([44828, 100]) x_train type: <class 'torch.Tensor'>
 tensor([[-0.7070, -0.6901, -0.7066,  ..., -0.8349, -0.8319, -0.8261],
        [-0.6901, -0.7066, -0.7005,  ..., -0.8319, -0.8261, -0.8263],
        [-0.7066, -0.7005, -0.6831,  ..., -0.8261, -0.8263, -0.8259],
        ...,
        [-1.3670, -1.3665, -1.3665,  ..., -1.3646, -1.3651, -1.3651],
        [-1.3665, -1.3665, -1.3640,  ..., -1.3651, -1.3651, -1.3651],
        [-1.3665, -1.3640, -1.3614,  ..., -1.3651, -1.3651, -1.3631]],
       device='cuda:0')
y_train.shape: torch.Size([44828, 100]) y_train type: <class 'torch.Tensor'>
 tensor([[-0.6901, -0.7066, -0.7005,  ..., -0.8319, -0.8261, -0.8263],
        [-0.7066, -0.7005, -0.6831,  ..., -0.8261, -0.8263, -0.8259],
        [-0.7005, -0.6831, -0.6940,  ..., -0.8263, -0.8259, -0.8228],
        ...,
        [-1.3665, -1.3665, -1.3640,  ..., -1.3651, -1.3651, -1.3651],
        [-1.3665, -1.3640, -1.3614,  ..., -1.3651, -1.3651, -1.3631],
        [-1.3640, -1.3614, -1.3612,  ..., -1.3651, -1.3631, -1.3581]],
       device='cuda:0')
x_val.shape: torch.Size([5430, 100]) x_val type: <class 'torch.Tensor'>
 tensor([[-1.3576, -1.3577, -1.3575,  ..., -1.3784, -1.3783, -1.3783],
        [-1.3577, -1.3575, -1.3579,  ..., -1.3783, -1.3783, -1.3783],
        [-1.3575, -1.3579, -1.3580,  ..., -1.3783, -1.3783, -1.3773],
        ...,
        [-0.0342, -0.0332, -0.0311,  ..., -0.0853, -0.0886, -0.0884],
        [-0.0332, -0.0311, -0.0231,  ..., -0.0886, -0.0884, -0.0924],
        [-0.0311, -0.0231, -0.0397,  ..., -0.0884, -0.0924, -0.0979]],
       device='cuda:0')
y_val.shape: torch.Size([5430, 100]) y_val type: <class 'torch.Tensor'>
 tensor([[-1.3577, -1.3575, -1.3579,  ..., -1.3783, -1.3783, -1.3783],
        [-1.3575, -1.3579, -1.3580,  ..., -1.3783, -1.3783, -1.3773],
        [-1.3579, -1.3580, -1.3579,  ..., -1.3783, -1.3773, -1.3774],
        ...,
        [-0.0332, -0.0311, -0.0231,  ..., -0.0886, -0.0884, -0.0924],
        [-0.0311, -0.0231, -0.0397,  ..., -0.0884, -0.0924, -0.0979],
        [-0.0231, -0.0397, -0.0419,  ..., -0.0924, -0.0979, -0.0995]],
       device='cuda:0')
x_test.shape: torch.Size([18562, 100]) x_test type: <class 'torch.Tensor'>
 tensor([[-0.1027, -0.1037, -0.1098,  ..., -0.0839, -0.0890, -0.0888],
        [-0.1037, -0.1098, -0.1119,  ..., -0.0890, -0.0888, -0.0886],
        [-0.1098, -0.1119, -0.1121,  ..., -0.0888, -0.0886, -0.0886],
        ...,
        [-0.5124, -0.5140, -0.5179,  ..., -0.5837, -0.5798, -0.5748],
        [-0.5140, -0.5179, -0.5202,  ..., -0.5798, -0.5748, -0.5760],
        [-0.5179, -0.5202, -0.5229,  ..., -0.5748, -0.5760, -0.5851]],
       device='cuda:0')
y_test.shape: torch.Size([18562, 100]) y_test type: <class 'torch.Tensor'>
 tensor([[-0.1037, -0.1098, -0.1119,  ..., -0.0890, -0.0888, -0.0886],
        [-0.1098, -0.1119, -0.1121,  ..., -0.0888, -0.0886, -0.0886],
        [-0.1119, -0.1121, -0.1192,  ..., -0.0886, -0.0886, -0.0886],
        ...,
        [-0.5140, -0.5179, -0.5202,  ..., -0.5798, -0.5748, -0.5760],
        [-0.5179, -0.5202, -0.5229,  ..., -0.5748, -0.5760, -0.5851],
        [-0.5202, -0.5229, -0.5224,  ..., -0.5760, -0.5851, -0.5885]],
       device='cuda:0')
model_1 type: <class '__main__.Model'>
 Model(
  (lstm): LSTMCell(1, 21)
  (linear): Linear(in_features=21, out_features=1, bias=True)
)
loss_fn_1 type: <class 'torch.nn.modules.loss.MSELoss'>
 MSELoss()
optimizer_1 type: <class 'torch.optim.adam.Adam'>
 Adam (
Parameter Group 0
    amsgrad: False
    betas: (0.9, 0.999)
    eps: 1e-08
    lr: 0.0001
    weight_decay: 0
)
scheduler_1 type: <class 'torch.optim.lr_scheduler.StepLR'>
 <torch.optim.lr_scheduler.StepLR object at 0x7fb3c80daac8>
optimization_1 type: <class '__main__.Optimization'>
 <__main__.Optimization object at 0x7fb3c80daa58>
Epoch 1 Train loss: 1.03. Validation loss: 0.71. Avg future: 0.00. Elapsed time: 14.63s.
Epoch 2 Train loss: 0.66. Validation loss: 0.22. Avg future: 0.00. Elapsed time: 14.95s.
Epoch 3 Train loss: 0.29. Validation loss: 0.16. Avg future: 0.00. Elapsed time: 14.95s.
Epoch 4 Train loss: 0.14. Validation loss: 0.08. Avg future: 0.00. Elapsed time: 14.70s.
Epoch 5 Train loss: 0.08. Validation loss: 0.05. Avg future: 0.00. Elapsed time: 14.71s.
Epoch 6 Train loss: 0.08. Validation loss: 0.03. Avg future: 0.00. Elapsed time: 15.09s.
Epoch 7 Train loss: 0.06. Validation loss: 0.03. Avg future: 0.00. Elapsed time: 15.07s.
Epoch 8 Train loss: 0.06. Validation loss: 0.03. Avg future: 0.00. Elapsed time: 14.74s.
Epoch 9 Train loss: 0.06. Validation loss: 0.03. Avg future: 0.00. Elapsed time: 15.76s.
Epoch 10 Train loss: 0.05. Validation loss: 0.03. Avg future: 0.00. Elapsed time: 15.07s.
Epoch 11 Train loss: 0.05. Validation loss: 0.03. Avg future: 0.00. Elapsed time: 14.75s.
Epoch 12 Train loss: 0.05. Validation loss: 0.03. Avg future: 0.00. Elapsed time: 14.30s.
Epoch 13 Train loss: 0.05. Validation loss: 0.03. Avg future: 0.00. Elapsed time: 15.01s.
Epoch 14 Train loss: 0.05. Validation loss: 0.03. Avg future: 0.00. Elapsed time: 15.27s.
Epoch 15 Train loss: 0.05. Validation loss: 0.03. Avg future: 0.00. Elapsed time: 15.07s.
optimization_1 type: <class '__main__.Optimization'>
 <__main__.Optimization object at 0x7fb3c80daa58>
Test loss 0.0038
Epoch 1 Train loss: 0.89. Validation loss: 0.46. Avg future: 25.75. Elapsed time: 14.54s.
Epoch 2 Train loss: 0.51. Validation loss: 0.17. Avg future: 25.30. Elapsed time: 15.22s.
Epoch 3 Train loss: 0.15. Validation loss: 0.08. Avg future: 24.66. Elapsed time: 15.31s.
Epoch 4 Train loss: 0.08. Validation loss: 0.05. Avg future: 24.25. Elapsed time: 15.34s.
Epoch 5 Train loss: 0.06. Validation loss: 0.04. Avg future: 25.27. Elapsed time: 15.14s.
Epoch 6 Train loss: 0.07. Validation loss: 0.02. Avg future: 24.95. Elapsed time: 15.41s.
Epoch 7 Train loss: 0.06. Validation loss: 0.02. Avg future: 24.85. Elapsed time: 15.41s.
Epoch 8 Train loss: 0.05. Validation loss: 0.02. Avg future: 25.62. Elapsed time: 14.70s.
Epoch 9 Train loss: 0.05. Validation loss: 0.02. Avg future: 24.58. Elapsed time: 14.90s.
Epoch 10 Train loss: 0.05. Validation loss: 0.02. Avg future: 26.30. Elapsed time: 14.72s.
Epoch 11 Train loss: 0.05. Validation loss: 0.02. Avg future: 25.90. Elapsed time: 14.56s.
Epoch 12 Train loss: 0.05. Validation loss: 0.02. Avg future: 26.14. Elapsed time: 15.48s.
Epoch 13 Train loss: 0.04. Validation loss: 0.02. Avg future: 25.35. Elapsed time: 15.84s.
Epoch 14 Train loss: 0.04. Validation loss: 0.02. Avg future: 25.82. Elapsed time: 15.05s.
Epoch 15 Train loss: 0.04. Validation loss: 0.02. Avg future: 24.90. Elapsed time: 15.24s.
Test loss 0.0024
end time:  20:10:52
end_time - start_time:843.225909

実行結果ログ2(ソースコード1に2020年のBTCのデータを読み込ませてLossがNaNになってしまうログ)

$ python3 btc_prediction_by_lstm_pytorch.py
start time:  20:14:13
pandas==1.1.4
numpy==1.19.2
torch==1.5.0
matplotlib==3.3.3
cuda_device: cuda
devicde_name: GeForce RTX 2070
get_from_bitmex:
files: ['./data/20201031.csv.gz', './data/20201101.csv.gz', './data/20201102.csv.gz', './data/20201103.csv.gz', './data/20201104.csv.gz', './data/20201105.csv.gz', './data/20201106.csv.gz', './data/20201107.csv.gz', './data/20201108.csv.gz', './data/20201109.csv.gz', './data/20201110.csv.gz', './data/20201111.csv.gz', './data/20201112.csv.gz', './data/20201113.csv.gz', './data/20201114.csv.gz', './data/20201115.csv.gz', './data/20201116.csv.gz', './data/20201117.csv.gz', './data/20201118.csv.gz', './data/20201119.csv.gz', './data/20201120.csv.gz', './data/20201121.csv.gz', './data/20201122.csv.gz', './data/20201123.csv.gz', './data/20201124.csv.gz', './data/20201125.csv.gz', './data/20201126.csv.gz', './data/20201127.csv.gz', './data/20201128.csv.gz', './data/20201129.csv.gz', './data/20201130.csv.gz', './data/20201201.csv.gz', './data/20201202.csv.gz', './data/20201203.csv.gz', './data/20201204.csv.gz', './data/20201205.csv.gz', './data/20201206.csv.gz', './data/20201207.csv.gz', './data/20201208.csv.gz', './data/20201209.csv.gz', './data/20201210.csv.gz', './data/20201211.csv.gz', './data/20201212.csv.gz', './data/20201213.csv.gz', './data/20201214.csv.gz', './data/20201215.csv.gz', './data/20201216.csv.gz', './data/20201217.csv.gz', './data/20201218.csv.gz', './data/20201219.csv.gz', './data/20201220.csv.gz', './data/20201221.csv.gz', './data/20201222.csv.gz', './data/20201223.csv.gz']
df.shape: (18271747, 9)
df.tail:                             symbol  side   size    price tickDirection  \
timestamp
2020-10-31 00:00:02.626781  XBTUSD   Buy      1  13559.0      PlusTick
2020-10-31 00:00:02.748137  XBTUSD   Buy  24061  13560.5  ZeroPlusTick
2020-10-31 00:00:02.748137  XBTUSD   Buy   1414  13560.5  ZeroPlusTick
2020-10-31 00:00:02.748137  XBTUSD   Buy    150  13560.5  ZeroPlusTick
2020-10-31 00:00:02.748137  XBTUSD   Buy    100  13560.5      PlusTick
...                            ...   ...    ...      ...           ...
2020-12-23 23:59:58.571018  XBTUSD  Sell    420  23245.0     MinusTick
2020-12-23 23:59:58.580506  XBTUSD   Buy     13  23244.5     MinusTick
2020-12-23 23:59:58.593966  XBTUSD   Buy     10  23243.5     MinusTick
2020-12-23 23:59:58.597077  XBTUSD  Sell    447  23243.0     MinusTick
2020-12-23 23:59:58.646200  XBTUSD   Buy     11  23241.0     MinusTick

                                                      trdMatchID  grossValue  \
timestamp
2020-10-31 00:00:02.626781  eac8256e-bbe9-ad3c-9e63-17719295a974        7375
2020-10-31 00:00:02.748137  5581c2ae-b0ad-5121-858a-ce9c44d74943   177425814
2020-10-31 00:00:02.748137  e8a864fb-aa79-35e1-84af-8cbcaa127694    10426836
2020-10-31 00:00:02.748137  813afd63-06a0-6f79-f1b5-ee7643ed1eec     1106100
2020-10-31 00:00:02.748137  80a4d7f2-7a83-e7a9-4e9f-bb287c408b44      737400
...                                                          ...         ...
2020-12-23 23:59:58.571018  a12bbffc-d9d7-083c-cbd5-dbadb88cb0a0     1806840
2020-12-23 23:59:58.580506  f1e97f20-6739-4145-715a-fe0914393f20       55926
2020-12-23 23:59:58.593966  b57033c9-4dd2-9c96-04e5-7bf702e36806       43020
2020-12-23 23:59:58.597077  35a0a31a-4a9d-34e9-208f-5af533a576c5     1922994
2020-12-23 23:59:58.646200  b3cb9ba2-cebc-cdf5-528c-ce7c7b79269b       47333

                            homeNotional  foreignNotional
timestamp
2020-10-31 00:00:02.626781      0.000074              1.0
2020-10-31 00:00:02.748137      1.774258          24061.0
2020-10-31 00:00:02.748137      0.104268           1414.0
2020-10-31 00:00:02.748137      0.011061            150.0
2020-10-31 00:00:02.748137      0.007374            100.0
...                                  ...              ...
2020-12-23 23:59:58.571018      0.018068            420.0
2020-12-23 23:59:58.580506      0.000559             13.0
2020-12-23 23:59:58.593966      0.000430             10.0
2020-12-23 23:59:58.597077      0.019230            447.0
2020-12-23 23:59:58.646200      0.000473             11.0

[18271742 rows x 9 columns]
df_vwap.shape: (77760,)
df_train.shape: (50544, 1)
df_train.tail:                              vwap
timestamp
2020-10-31 00:05:00  13554.231586
2020-10-31 00:06:00  13557.871331
2020-10-31 00:07:00  13559.282798
2020-10-31 00:08:00  13557.649806
2020-10-31 00:09:00  13560.874972
...                           ...
2020-12-05 02:19:00  18741.511471
2020-12-05 02:20:00  18730.342480
2020-12-05 02:21:00  18724.025946
2020-12-05 02:22:00  18720.223295
2020-12-05 02:23:00  18721.599737

[50539 rows x 1 columns]
df_val.shape: (6221, 1)
df_val.tail:                              vwap
timestamp
2020-12-05 02:29:00  18745.846928
2020-12-05 02:30:00  18758.132585
2020-12-05 02:31:00  18765.631351
2020-12-05 02:32:00  18774.092713
2020-12-05 02:33:00  18788.076590
...                           ...
2020-12-09 10:00:00  18047.134980
2020-12-09 10:01:00  18026.450682
2020-12-09 10:02:00  18011.715896
2020-12-09 10:03:00  17995.496554
2020-12-09 10:04:00  18005.457240

[6216 rows x 1 columns]
df_test.shape: (20995, 1)
df_test.tail:                              vwap
timestamp
2020-12-09 10:10:00  17984.900355
2020-12-09 10:11:00  17991.922489
2020-12-09 10:12:00  17983.009223
2020-12-09 10:13:00  17979.464915
2020-12-09 10:14:00  17977.227506
...                           ...
2020-12-23 23:55:00  23248.030879
2020-12-23 23:56:00  23206.786654
2020-12-23 23:57:00  23264.388328
2020-12-23 23:58:00  23264.377437
2020-12-23 23:59:00  23268.652009

[20990 rows x 1 columns]
scaler type: <class 'sklearn.preprocessing._data.StandardScaler'>
 StandardScaler()
train_arr.shape: (50544, 1) train_arr type: <class 'numpy.ndarray'>
 [[-1.72]
 [-1.72]
 [-1.72]
 ...
 [ 1.06]
 [ 1.06]
 [ 1.06]]
val_arr.shape: (6221, 1) val_arr type: <class 'numpy.ndarray'>
 [[ 1.06]
 [ 1.07]
 [ 1.07]
 ...
 [ 0.67]
 [ 0.67]
 [ 0.67]]
test_arr.shape: (20995, 1) test_arr type: <class 'numpy.ndarray'>
 [[ 0.67]
 [ 0.66]
 [ 0.67]
 ...
 [ 3.51]
 [ 3.51]
 [ 3.51]]
x_train.shape: torch.Size([50444, 100]) x_train type: <class 'torch.Tensor'>
 tensor([[-1.7223, -1.7214, -1.7227,  ..., -1.6951, -1.6975, -1.6995],
        [-1.7214, -1.7227, -1.7272,  ..., -1.6975, -1.6995, -1.7009],
        [-1.7227, -1.7272, -1.7283,  ..., -1.6995, -1.7009, -1.6961],
        ...,
        [ 1.0484,  1.0509,  1.0464,  ...,  1.0668,  1.0681,  1.0621],
        [ 1.0509,  1.0464,  1.0463,  ...,  1.0681,  1.0621,  1.0587],
        [ 1.0464,  1.0463,  1.0454,  ...,  1.0621,  1.0587,  1.0566]],
       device='cuda:0')
y_train.shape: torch.Size([50444, 100]) y_train type: <class 'torch.Tensor'>
 tensor([[-1.7214, -1.7227, -1.7272,  ..., -1.6975, -1.6995, -1.7009],
        [-1.7227, -1.7272, -1.7283,  ..., -1.6995, -1.7009, -1.6961],
        [-1.7272, -1.7283, -1.7272,  ..., -1.7009, -1.6961, -1.6963],
        ...,
        [ 1.0509,  1.0464,  1.0463,  ...,  1.0681,  1.0621,  1.0587],
        [ 1.0464,  1.0463,  1.0454,  ...,  1.0621,  1.0587,  1.0566],
        [ 1.0463,  1.0454,  1.0409,  ...,  1.0587,  1.0566,  1.0574]],
       device='cuda:0')
x_val.shape: torch.Size([6121, 100]) x_val type: <class 'torch.Tensor'>
 tensor([[1.0619, 1.0737, 1.0740,  ..., 1.1188, 1.1161, 1.1076],
        [1.0737, 1.0740, 1.0740,  ..., 1.1161, 1.1076, 1.1094],
        [1.0740, 1.0740, 1.0698,  ..., 1.1076, 1.1094, 1.1188],
        ...,
        [0.5756, 0.5710, 0.5978,  ..., 0.6945, 0.6939, 0.6828],
        [0.5710, 0.5978, 0.5860,  ..., 0.6939, 0.6828, 0.6748],
        [0.5978, 0.5860, 0.5710,  ..., 0.6828, 0.6748, 0.6661]],
       device='cuda:0')
y_val.shape: torch.Size([6121, 100]) y_val type: <class 'torch.Tensor'>
 tensor([[1.0737, 1.0740, 1.0740,  ..., 1.1161, 1.1076, 1.1094],
        [1.0740, 1.0740, 1.0698,  ..., 1.1076, 1.1094, 1.1188],
        [1.0740, 1.0698, 1.0705,  ..., 1.1094, 1.1188, 1.1192],
        ...,
        [0.5710, 0.5978, 0.5860,  ..., 0.6939, 0.6828, 0.6748],
        [0.5978, 0.5860, 0.5710,  ..., 0.6828, 0.6748, 0.6661],
        [0.5860, 0.5710, 0.5541,  ..., 0.6748, 0.6661, 0.6715]],
       device='cuda:0')
x_test.shape: torch.Size([20895, 100]) x_test type: <class 'torch.Tensor'>
 tensor([[0.6657, 0.6632, 0.6724,  ..., 0.8106, 0.8074, 0.8068],
        [0.6632, 0.6724, 0.6659,  ..., 0.8074, 0.8068, 0.8062],
        [0.6724, 0.6659, 0.6675,  ..., 0.8068, 0.8062, 0.8041],
        ...,
        [3.3850, 3.3734, 3.3574,  ..., 3.5053, 3.4966, 3.4744],
        [3.3734, 3.3574, 3.3327,  ..., 3.4966, 3.4744, 3.5054],
        [3.3574, 3.3327, 3.2934,  ..., 3.4744, 3.5054, 3.5054]],
       device='cuda:0')
y_test.shape: torch.Size([20895, 100]) y_test type: <class 'torch.Tensor'>
 tensor([[0.6632, 0.6724, 0.6659,  ..., 0.8074, 0.8068, 0.8062],
        [0.6724, 0.6659, 0.6675,  ..., 0.8068, 0.8062, 0.8041],
        [0.6659, 0.6675, 0.6604,  ..., 0.8062, 0.8041, 0.8065],
        ...,
        [3.3734, 3.3574, 3.3327,  ..., 3.4966, 3.4744, 3.5054],
        [3.3574, 3.3327, 3.2934,  ..., 3.4744, 3.5054, 3.5054],
        [3.3327, 3.2934, 3.2572,  ..., 3.5054, 3.5054, 3.5077]],
       device='cuda:0')
model_1 type: <class '__main__.Model'>
 Model(
  (lstm): LSTMCell(1, 21)
  (linear): Linear(in_features=21, out_features=1, bias=True)
)
loss_fn_1 type: <class 'torch.nn.modules.loss.MSELoss'>
 MSELoss()
optimizer_1 type: <class 'torch.optim.adam.Adam'>
 Adam (
Parameter Group 0
    amsgrad: False
    betas: (0.9, 0.999)
    eps: 1e-08
    lr: 0.0001
    weight_decay: 0
)
scheduler_1 type: <class 'torch.optim.lr_scheduler.StepLR'>
 <torch.optim.lr_scheduler.StepLR object at 0x7fae7cf75828>
optimization_1 type: <class '__main__.Optimization'>
 <__main__.Optimization object at 0x7faead82ebe0>
Epoch 1 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 18.00s.
Epoch 2 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 18.02s.
Epoch 3 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 18.04s.
Epoch 4 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 18.03s.
Epoch 5 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 18.00s.
Epoch 6 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 17.75s.
Epoch 7 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 17.87s.
Epoch 8 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 17.92s.
Epoch 9 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 17.73s.
Epoch 10 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 17.97s.
Epoch 11 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 17.93s.
Epoch 12 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 17.84s.
Epoch 13 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 17.92s.
Epoch 14 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 18.05s.
Epoch 15 Train loss: nan. Validation loss: nan. Avg future: 0.00. Elapsed time: 17.68s.
optimization_1 type: <class '__main__.Optimization'>
 <__main__.Optimization object at 0x7faead82ebe0>
Test loss nan
Epoch 1 Train loss: nan. Validation loss: nan. Avg future: 25.69. Elapsed time: 18.89s.
Epoch 2 Train loss: nan. Validation loss: nan. Avg future: 25.42. Elapsed time: 19.03s.
Epoch 3 Train loss: nan. Validation loss: nan. Avg future: 23.72. Elapsed time: 18.80s.
Epoch 4 Train loss: nan. Validation loss: nan. Avg future: 24.97. Elapsed time: 18.47s.
Epoch 5 Train loss: nan. Validation loss: nan. Avg future: 25.43. Elapsed time: 18.58s.
Epoch 6 Train loss: nan. Validation loss: nan. Avg future: 24.85. Elapsed time: 18.69s.
Epoch 7 Train loss: nan. Validation loss: nan. Avg future: 25.38. Elapsed time: 18.40s.
Epoch 8 Train loss: nan. Validation loss: nan. Avg future: 24.75. Elapsed time: 18.42s.
Epoch 9 Train loss: nan. Validation loss: nan. Avg future: 26.19. Elapsed time: 18.63s.
Epoch 10 Train loss: nan. Validation loss: nan. Avg future: 25.97. Elapsed time: 18.22s.
Epoch 11 Train loss: nan. Validation loss: nan. Avg future: 25.67. Elapsed time: 17.62s.
Epoch 12 Train loss: nan. Validation loss: nan. Avg future: 25.30. Elapsed time: 17.29s.
Epoch 13 Train loss: nan. Validation loss: nan. Avg future: 26.18. Elapsed time: 16.94s.
Epoch 14 Train loss: nan. Validation loss: nan. Avg future: 25.29. Elapsed time: 17.39s.
Epoch 15 Train loss: nan. Validation loss: nan. Avg future: 24.92. Elapsed time: 16.77s.
Test loss nan
end time:  20:26:48
end_time - start_time:754.422876
$

ソースコード2(PyTorchのLSTMの簡便なモデリングで、バックテストまで行うコード)

btc_prediction_and_backtest_by_pytorch.py
# -*- coding: utf-8 -*-
'''
btc_prediction_and_backtest_by_pytorch.py

Copyright (C) 2020 HIROSE Ken-ichi (hirosenokensan@gmail.com) 
                                                 All rights reserved.
 This is free software with ABSOLUTELY NO WARRANTY.

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 02111-1307, USA
'''

import glob
import warnings

import os
# import math
import time
import random
# import pprint
# from dateutil import parser
from datetime import timedelta, datetime

import numpy as np
import pandas as pd
# import pandas_datareader.data as web

import matplotlib
import matplotlib.pyplot as plt

import sklearn
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score

import torch
import torch.nn as nn
import torch.optim as optim
from torch.autograd import Variable
import skorch

from backtesting import Backtest, Strategy
from backtesting.lib import plot_heatmaps

# import bitmex

class LSTMClassifier(nn.Module):
    def __init__(self, lstm_input_dim, lstm_hidden_dim, target_dim):
        super(LSTMClassifier, self).__init__()
        self.input_dim = lstm_input_dim
        self.hidden_dim = lstm_hidden_dim
        self.lstm = nn.LSTM(input_size=lstm_input_dim, 
                            hidden_size=lstm_hidden_dim,
                            num_layers=1, #default
                            #dropout=0.2,
                            batch_first=True
                            )
        self.dense = nn.Linear(lstm_hidden_dim, target_dim)

    def forward(self, X_input):
        _, lstm_out = self.lstm(X_input)
        linear_out = self.dense(lstm_out[0].view(X_input.size(0), -1))
        return torch.sigmoid(linear_out)

def prep_feature_data(batch_idx, time_steps, X_data, feature_num, cuda_device):  
    feats = torch.zeros((len(batch_idx), time_steps, feature_num), dtype=torch.float, device=cuda_device)
    for b_i, b_idx in enumerate(batch_idx):
        b_slc = slice(b_idx + 1 - time_steps ,b_idx + 1) # 過去のN足分をtime stepのデータとして格納する。
        feats[b_i, :, :] = X_data[b_slc, :]        
    return feats

def plot_losses(train_losses, val_losses):
    plt.plot(train_losses, lw=1, label="Training loss")
    plt.plot(val_losses, lw=1, label="Validation loss")
    plt.legend()
    plt.title("Losses")

def get_all_bitmex(symbol, kline_size, save = False):
    filename = 'data/%s-%s-data.csv' % (symbol, kline_size)
    if os.path.isfile(filename):
        data_df = pd.read_csv(filename)
    else:
        data_df = pd.DataFrame()
    oldest_point, newest_point = minutes_of_new_data(symbol, kline_size, data_df)
    delta_min = (newest_point - oldest_point).total_seconds()/60
    available_data = math.ceil(delta_min/binsizes[kline_size])
    rounds = math.ceil(available_data / batch_size)
    if rounds > 0:
        for round_num in range(rounds):
            time.sleep(1)
            new_time = (oldest_point + timedelta(minutes = round_num * batch_size * binsizes[kline_size]))
            data = bitmex_client.Trade.Trade_getBucketed(symbol=symbol, 
                    binSize=kline_size, count=batch_size, startTime = new_time).result()[0]
            temp_df = pd.DataFrame(data)
            data_df = data_df.append(temp_df)
    data_df.set_index('timestamp', inplace=True)
    if save and rounds > 0:
        data_df.to_csv(filename)
    return data_df

class myCustomStrategy(Strategy):
    def init(self):
        self.model = LSTMClassifier(feature_num, lstm_hidden_dim, target_dim).to(cuda_device) # LSTMの学習済みモデルの読み込み
        self.model.load_state_dict(torch.load('{}.mdl'.format(ownprefix), map_location=torch.device(cuda_device))) # load model

    def next(self): 
        # 過去500ステップ分のデータが貯まるまではスキップ
        # 1日に1回のみ取引するため、hour & minuteが0の時のみ処理するようにする。
        if len(self.data) >= moving_average_num + time_steps and len(self.data) % future_num == 0:
            # 2. 推測用データの用意
            x_array = self.prepare_data()
            x_tensor = torch.tensor(x_array, dtype=torch.float, device=cuda_device)
            # 3. 予測の実行
            with torch.no_grad():
                y_pred = self.predict(x_tensor.view(1, time_steps, feature_num))

            # 4. 予測が買い(1)であればbuy()、それ以外はsell()
            if y_pred == 1:
                self.buy(sl=self.data.Close[-1]*0.99, 
                         tp=self.data.Close[-1]*1.01)
            else:
                self.sell(sl=self.data.Close[-1]*1.01, 
                         tp=self.data.Close[-1]*0.99)

    def prepare_data(self):
        # いったんPandasのデータフレームに変換
        tmp_df = pd.concat([
                    self.data.Open.to_series(), 
                    self.data.High.to_series(), 
                    self.data.Low.to_series(), 
                    self.data.Close.to_series(), 
                    self.data.Volume.to_series(), 
                    ], axis=1)

        # 500足の移動平均に対する割合とする。
        cols = tmp_df.columns
        for col in cols:
            tmp_df['Roll_' + col] = tmp_df[col].rolling(window=moving_average_num, min_periods=moving_average_num).mean()
            tmp_df[col] = tmp_df[col] / tmp_df['Roll_' + col] - 1

        #最後のtime_steps分のみの値を返す
        return tmp_df.tail(time_steps)[cols].values

    def predict(self, x_array):
        y_score = self.model(x_array) 
        return np.round(y_score.view(-1).to('cpu').numpy())[0]

class mySimpleStrategy(Strategy):
    def init(self):
        pass

    def next(self): 
        self.buy if self.data.Close[-1]> self.data.Open[-1] else self.sell()


if __name__ == '__main__':
    os.chdir(os.path.dirname(os.path.abspath(__file__)))
    ownprefix = os.path.basename(__file__)

    warnings.simplefilter('ignore')
    pd.set_option('display.max_columns', 100)
    np.set_printoptions(precision=3, suppress=True, formatter={'float': '{: 0.2f}'.format}) #桁を揃える

    start_time = time.perf_counter()
    print("start time: ", datetime.now().strftime("%H:%M:%S"))

    print("pandas==%s" % pd.__version__)
    print("numpy==%s" % np.__version__)
    print("torch==%s" % torch.__version__)
    print("matplotlib==%s" % matplotlib.__version__)

    cuda_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print("cuda_device:",cuda_device)
    if cuda_device != "cpu": 
        print("devicde_name:",torch.cuda.get_device_name(torch.cuda.current_device()))
        torch.cuda.manual_seed(1)
    np.random.seed(1)
    random.seed(1)
    torch.manual_seed(1)

    if os.path.exists('{}.pickle'.format(ownprefix)):
        print("read_pickle:")
        df = pd.read_pickle('{}.pickle'.format(ownprefix))
    else:
        print("get_from_bitmex:")
        ## bitmex API
        # bitmex_api_key = ''    #Enter your own API-key here
        # bitmex_api_secret = '' #Enter your own API-secret here
        # binsizes = {"1m": 1, "5m": 5, "1h": 60, "1d": 1440}
        # batch_size = 750
        # bitmex_client = bitmex(test=False, api_key=bitmex_api_key, api_secret=bitmex_api_secret)
        # df = get_all_bitmex("XBTUSD","5m",save=True)
        ##
        # https://public.bitmex.com/?prefix=data/trade/
        # files = sorted(glob.glob('./data/2019*.csv.gz'))
        files = sorted(glob.glob('./data/2020*.csv.gz'))
        print("files:",files)
        df = pd.concat(map(pd.read_csv, files))
        df = df[df.symbol == 'XBTUSD']
        df.timestamp = pd.to_datetime(df.timestamp.str.replace('D', 'T'))
        df = df.sort_values('timestamp')
        df.set_index('timestamp', inplace=True)
        df.to_pickle('{}.pickle'.format(ownprefix))
        df.to_csv('{}.csv'.format(ownprefix))

    print("df.shape:",df.shape)
    print("df.tail:",df.tail(-5))

    # resample()の頻度コードをH(時間)、T(分)、S(秒)、B(月 - 金)、W(週)
    df_ohlcv = df['price'].resample('10T', label='left', closed='left').ohlc().assign(
                                    volume=df['foreignNotional'].resample('10T').sum().values)
    df_ohlcv.rename(columns={'timestamp':'Datetime','open':'Open','high':'High',
                    'low':'Low','close':'Close','volume':'Volume'}, inplace=True)
    print("df_ohlcv.shape:",df_ohlcv.shape,"df_ohlcv type:",type(df_ohlcv),"\n",df_ohlcv)

    '''
    # 1. 定数の設定
    '''
    future_num = 144 #何足先を予測するか
    feature_num = 5 # open,high,low,close,volume の5項目
    batch_size = 64 # batch_size = 128
    time_steps = 50 # lstmのtimesteps
    moving_average_num = 500 # 移動平均を取るCandle数
    n_epocs = 30 

    lstm_hidden_dim = 16
    target_dim = 1

    '''
    # 2. 教師データの作成
    '''
    future_price = df_ohlcv.iloc[future_num:]['Close'].values
    curr_price = df_ohlcv.iloc[:-future_num]['Close'].values
    y_data_tmp = future_price - curr_price
    print("future_price:",future_price,"\ncurr_price:",curr_price,"\ny_data_tmp:",y_data_tmp)

    y_data = np.zeros_like(y_data_tmp)
    y_data[y_data_tmp > 0] = 1
    y_data = y_data[moving_average_num:]
    print("y_data.shape:",y_data.shape,"y_data type:",type(y_data),"\n",y_data)

    '''
    # 3. 価格の正規化
    '''
    cols = df_ohlcv.columns # cols = df.columns
    for col in cols:
        df_ohlcv['Roll_' + col] = df_ohlcv[col].rolling(window=moving_average_num, min_periods=moving_average_num).mean()
        df_ohlcv[col] = df_ohlcv[col] / df_ohlcv['Roll_' + col] - 1    
    print("df_ohlcv.shape:",df_ohlcv.shape,"df_ohlcv type:",type(df_ohlcv))
    print("df_ohlcv.tail:",df_ohlcv.tail(-5))

    X_data = df_ohlcv.iloc[moving_average_num:-future_num][cols].values #最初の500足分は移動平均データがないため除く。後半の144足分は予測データがないため除く
    print("X_data.shape:",X_data.shape,"X_data type:",type(X_data),"\n",X_data)

    '''
    # 4. データの分割、TorchのTensorに変換
    '''
    val_idx_from = round(len(df_ohlcv)*0.65) #データをtrain, testに分割するIndex
    test_idx_from = round(len(df_ohlcv)*0.65) + round(len(df_ohlcv)*0.08)
    print("val_idx_from:",val_idx_from,"test_idx_from:",test_idx_from)

    X_train = torch.tensor(X_data[:val_idx_from], dtype=torch.float, device=cuda_device) #学習用データ
    y_train = torch.tensor(y_data[:val_idx_from], dtype=torch.float, device=cuda_device)
    print("X_train.shape:",X_train.shape,"X_train type:",type(X_train),"\n",X_train)
    print("y_train.shape:",y_train.shape,"y_train type:",type(y_train),"\n",y_train)

    X_val   = torch.tensor(X_data[val_idx_from:test_idx_from], dtype=torch.float, device=cuda_device) #評価用データ
    y_val   = y_data[val_idx_from:test_idx_from]
    print("X_val.shape:",X_val.shape,"X_val type:",type(X_val),"\n",X_val)
    print("y_val.shape:",y_val.shape,"y_val type:",type(y_val),"\n",y_val)

    X_test  = torch.tensor(X_data[test_idx_from:], dtype=torch.float, device=cuda_device) #テスト用データ
    y_test  = y_data[test_idx_from:]
    print("X_test.shape:",X_test.shape,"X_test type:",type(X_test),"\n",X_test)
    print("y_test.shape:",y_test.shape,"y_test type:",type(y_test),"\n",y_test)

    '''
    # 5. LSTMの学習モデル構築
    '''
    model = LSTMClassifier(feature_num, lstm_hidden_dim, target_dim).to(cuda_device)
    print("model type:",type(model),"\n",model)

    loss_function = nn.BCELoss()
    print("loss_function type:",type(loss_function),"\n",loss_function)

    optimizer= optim.Adam(model.parameters(), lr=1e-4)
    print("optimizer type:",type(optimizer),"\n",optimizer)


    train_size = X_train.size(0)
    print("train_size:",train_size)

    best_acc_score = 0
    for epoch in range(n_epocs):
        '''
        # 1. まずはtrainデータのindexをランダムに入れ替える。最初のtime_steps分は使わない。
        '''
        perm_idx = np.random.permutation(np.arange(time_steps, train_size))
        # print("perm_idx.shape:",perm_idx.shape,"perm_idx type:",type(perm_idx),"\n",perm_idx)
        '''
        # 2. batch size毎にperm_idxの対象のindexを取得
        '''
        for t_i in range(0, len(perm_idx), batch_size):
            batch_idx = perm_idx[t_i:(t_i + batch_size)]
            '''
            # 3. LSTM入力用の時系列データの準備
            '''
            feats = prep_feature_data(batch_idx, time_steps, X_train, feature_num, cuda_device)
            y_target = y_train[batch_idx]
            '''
            # 4. pytorch LSTMの学習実施
            '''
            model.zero_grad()
            train_scores = model(feats) # batch size x time steps x feature_num
            loss = loss_function(train_scores, y_target.view(-1, 1))
            loss.backward()
            optimizer.step()

        '''
        # 5. validationデータの評価
        '''
        with torch.no_grad():
            feats_val = prep_feature_data(np.arange(time_steps, X_val.size(0)), time_steps, X_val, feature_num, cuda_device)
            val_scores = model(feats_val)
            tmp_scores = val_scores.view(-1).to('cpu').numpy()
            bi_scores = np.round(tmp_scores)
            acc_score = accuracy_score(y_val[time_steps:], bi_scores)
            roc_score = roc_auc_score(y_val[time_steps:], tmp_scores)
            print('EPOCH:',str(epoch),'loss:',loss.item(),'Val ACC Score:',acc_score,'ROC AUC Score:',roc_score)

        '''
        # 6. validationの評価が良ければモデルを保存
        '''
        if acc_score > best_acc_score:
            best_acc_score = acc_score
            torch.save(model.state_dict(),'{}.mdl'.format(ownprefix))
            print('best score updated, Pytorch model was saved!!', )

    '''
    # 7. bestモデルで予測する。
    '''
    model.load_state_dict(torch.load('{}.mdl'.format(ownprefix)))
    with torch.no_grad():
        feats_test = prep_feature_data(np.arange(time_steps, X_test.size(0)), time_steps, X_test, feature_num, cuda_device)
        val_scores = model(feats_test)
        tmp_scores = val_scores.view(-1).to('cpu').numpy()
        bi_scores = np.round(tmp_scores)
        acc_score = accuracy_score(y_test[time_steps:], bi_scores)
        roc_score = roc_auc_score(y_test[time_steps:], tmp_scores)
        print('Test ACC Score:',acc_score,'ROC AUC Score:',roc_score)

    '''
    # 8. 簡易なストラテジでバックテストを行う
    '''
    # resample()の頻度コードをH(時間)、T(分)、S(秒)、B(月 - 金)、W(週)
    df_ohlcv = df['price'].resample('10T', label='left', closed='left').ohlc().assign(
                                    volume=df['foreignNotional'].resample('10T').sum().values)
    df_ohlcv.rename(columns={'timestamp':'Datetime','open':'Open','high':'High',
                    'low':'Low','close':'Close','volume':'Volume'}, inplace=True)
    print("df_ohlcv.shape:",df_ohlcv.shape,"df_ohlcv type:",type(df_ohlcv),"\n",df_ohlcv)

    bt = Backtest(df_ohlcv[8000:], myCustomStrategy, cash=100000, commission=.00004)
    print(bt.run())
    bt.plot(filename='{}'.format(ownprefix), open_browser=False)

実行結果ログ3(ソースコード2に2020年のBTCのデータを読み込ませたログ)

$ python3 btc_prediction_and_backtest_by_pytorch.py
start time:  23:52:58
pandas==1.1.4
numpy==1.19.2
torch==1.5.0
matplotlib==3.3.3
cuda_device: cuda
devicde_name: GeForce RTX 2070
get_from_bitmex:
files: ['./data/20200930.csv.gz', './data/20201001.csv.gz', './data/20201002.csv.gz', './data/20201003.csv.gz', './data/20201004.csv.gz', './data/20201005.csv.gz', './data/20201006.csv.gz', './data/20201007.csv.gz', './data/20201008.csv.gz', './data/20201009.csv.gz', './data/20201010.csv.gz', './data/20201011.csv.gz', './data/20201012.csv.gz', './data/20201013.csv.gz', './data/20201014.csv.gz', './data/20201015.csv.gz', './data/20201016.csv.gz', './data/20201017.csv.gz', './data/20201018.csv.gz', './data/20201019.csv.gz', './data/20201020.csv.gz', './data/20201021.csv.gz', './data/20201022.csv.gz', './data/20201023.csv.gz', './data/20201024.csv.gz', './data/20201025.csv.gz', './data/20201026.csv.gz', './data/20201027.csv.gz', './data/20201028.csv.gz', './data/20201029.csv.gz', './data/20201030.csv.gz', './data/20201031.csv.gz', './data/20201101.csv.gz', './data/20201102.csv.gz', './data/20201103.csv.gz', './data/20201104.csv.gz', './data/20201105.csv.gz', './data/20201106.csv.gz', './data/20201107.csv.gz', './data/20201108.csv.gz', './data/20201109.csv.gz', './data/20201110.csv.gz', './data/20201111.csv.gz', './data/20201112.csv.gz', './data/20201113.csv.gz', './data/20201114.csv.gz', './data/20201115.csv.gz', './data/20201116.csv.gz', './data/20201117.csv.gz', './data/20201118.csv.gz', './data/20201119.csv.gz', './data/20201120.csv.gz', './data/20201121.csv.gz', './data/20201122.csv.gz', './data/20201123.csv.gz', './data/20201124.csv.gz', './data/20201125.csv.gz', './data/20201126.csv.gz', './data/20201127.csv.gz', './data/20201128.csv.gz', './data/20201129.csv.gz', './data/20201130.csv.gz', './data/20201201.csv.gz', './data/20201202.csv.gz', './data/20201203.csv.gz', './data/20201204.csv.gz', './data/20201205.csv.gz', './data/20201206.csv.gz', './data/20201207.csv.gz', './data/20201208.csv.gz', './data/20201209.csv.gz', './data/20201210.csv.gz', './data/20201211.csv.gz', './data/20201212.csv.gz', './data/20201213.csv.gz', './data/20201214.csv.gz', './data/20201215.csv.gz', './data/20201216.csv.gz', './data/20201217.csv.gz', './data/20201218.csv.gz', './data/20201219.csv.gz', './data/20201220.csv.gz', './data/20201221.csv.gz', './data/20201222.csv.gz', './data/20201223.csv.gz']
df.shape: (25878209, 9)
df.tail:                             symbol  side   size    price tickDirection  \
timestamp
2020-09-30 00:00:02.771822  XBTUSD   Buy  12055  10839.5  ZeroPlusTick
2020-09-30 00:00:02.885748  XBTUSD  Sell   4500  10839.0     MinusTick
2020-09-30 00:00:02.989378  XBTUSD   Buy   3499  10839.5      PlusTick
2020-09-30 00:00:02.992595  XBTUSD   Buy     87  10839.5  ZeroPlusTick
2020-09-30 00:00:02.998145  XBTUSD   Buy   2383  10839.5  ZeroPlusTick
...                            ...   ...    ...      ...           ...
2020-12-23 23:59:58.571018  XBTUSD  Sell    420  23245.0     MinusTick
2020-12-23 23:59:58.580506  XBTUSD   Buy     13  23244.5     MinusTick
2020-12-23 23:59:58.593966  XBTUSD   Buy     10  23243.5     MinusTick
2020-12-23 23:59:58.597077  XBTUSD  Sell    447  23243.0     MinusTick
2020-12-23 23:59:58.646200  XBTUSD   Buy     11  23241.0     MinusTick

                                                      trdMatchID  grossValue  \
timestamp
2020-09-30 00:00:02.771822  ddc9e2a6-40b5-b5bf-715b-60cf18ab847a   111219430
2020-09-30 00:00:02.885748  938ba483-0bd9-b498-c5fb-162c2cc72acb    41517000
2020-09-30 00:00:02.989378  be1bab97-78a9-b0db-1ecd-be70eb7bdb99    32281774
2020-09-30 00:00:02.992595  29f68ab4-cc4f-291d-5cfb-a96baacac448      802662
2020-09-30 00:00:02.998145  be2e5e02-b1c8-88da-262a-1d23ffc62b32    21985558
...                                                          ...         ...
2020-12-23 23:59:58.571018  a12bbffc-d9d7-083c-cbd5-dbadb88cb0a0     1806840
2020-12-23 23:59:58.580506  f1e97f20-6739-4145-715a-fe0914393f20       55926
2020-12-23 23:59:58.593966  b57033c9-4dd2-9c96-04e5-7bf702e36806       43020
2020-12-23 23:59:58.597077  35a0a31a-4a9d-34e9-208f-5af533a576c5     1922994
2020-12-23 23:59:58.646200  b3cb9ba2-cebc-cdf5-528c-ce7c7b79269b       47333

                            homeNotional  foreignNotional
timestamp
2020-09-30 00:00:02.771822      1.112194          12055.0
2020-09-30 00:00:02.885748      0.415170           4500.0
2020-09-30 00:00:02.989378      0.322818           3499.0
2020-09-30 00:00:02.992595      0.008027             87.0
2020-09-30 00:00:02.998145      0.219856           2383.0
...                                  ...              ...
2020-12-23 23:59:58.571018      0.018068            420.0
2020-12-23 23:59:58.580506      0.000559             13.0
2020-12-23 23:59:58.593966      0.000430             10.0
2020-12-23 23:59:58.597077      0.019230            447.0
2020-12-23 23:59:58.646200      0.000473             11.0

[25878204 rows x 9 columns]
df_ohlcv.shape: (12240, 5) df_ohlcv type: <class 'pandas.core.frame.DataFrame'>
                         Open     High      Low    Close      Volume
timestamp
2020-09-30 00:00:00  10839.5  10842.0  10827.5  10828.0  13500945.0
2020-09-30 00:10:00  10828.5  10829.0  10822.0  10829.0   4477779.0
2020-09-30 00:20:00  10829.0  10829.0  10816.5  10819.5   3589041.0
2020-09-30 00:30:00  10819.5  10820.0  10814.5  10814.5   4523661.0
2020-09-30 00:40:00  10815.0  10820.0  10812.0  10820.0   3463389.0
...                      ...      ...      ...      ...         ...
2020-12-23 23:10:00  23307.0  23400.0  23264.0  23315.5  14089639.0
2020-12-23 23:20:00  23315.5  23485.5  23315.0  23382.0  40956253.0
2020-12-23 23:30:00  23382.0  23420.0  23333.0  23365.0  15243013.0
2020-12-23 23:40:00  23365.5  23376.0  23264.0  23281.0  13256178.0
2020-12-23 23:50:00  23281.5  23288.0  23190.0  23241.0  16298938.0

[12240 rows x 5 columns]
future_price: [ 10817.50  10799.50  10798.50 ...  23365.00  23281.00  23241.00]
curr_price: [ 10828.00  10829.00  10819.50 ...  23736.00  23757.50  23835.00]
y_data_tmp: [-10.50 -29.50 -21.00 ... -371.00 -476.50 -594.00]
y_data.shape: (11596,) y_data type: <class 'numpy.ndarray'>
 [ 1.00  1.00  1.00 ...  0.00  0.00  0.00]
df_ohlcv.shape: (12240, 10) df_ohlcv type: <class 'pandas.core.frame.DataFrame'>
df_ohlcv.tail:                          Open      High       Low     Close    Volume  \
timestamp
2020-09-30 00:50:00       NaN       NaN       NaN       NaN       NaN
2020-09-30 01:00:00       NaN       NaN       NaN       NaN       NaN
2020-09-30 01:10:00       NaN       NaN       NaN       NaN       NaN
2020-09-30 01:20:00       NaN       NaN       NaN       NaN       NaN
2020-09-30 01:30:00       NaN       NaN       NaN       NaN       NaN
...                       ...       ...       ...       ...       ...
2020-12-23 23:10:00 -0.002501 -0.000952 -0.001648 -0.002115 -0.256627
2020-12-23 23:20:00 -0.002115  0.002709  0.000556  0.000742  1.154715
2020-12-23 23:30:00  0.000742 -0.000075  0.001342  0.000026 -0.198753
2020-12-23 23:40:00  0.000048 -0.001942 -0.001603 -0.003553 -0.303641
2020-12-23 23:50:00 -0.003531 -0.005679 -0.004760 -0.005249 -0.144335

                     Roll_Open  Roll_High   Roll_Low  Roll_Close   Roll_Volume
timestamp
2020-09-30 00:50:00        NaN        NaN        NaN         NaN           NaN
2020-09-30 01:00:00        NaN        NaN        NaN         NaN           NaN
2020-09-30 01:10:00        NaN        NaN        NaN         NaN           NaN
2020-09-30 01:20:00        NaN        NaN        NaN         NaN           NaN
2020-09-30 01:30:00        NaN        NaN        NaN         NaN           NaN
...                        ...        ...        ...         ...           ...
2020-12-23 23:10:00  23365.435  23422.301  23302.411   23364.911  1.895367e+07
2020-12-23 23:20:00  23364.912  23422.039  23302.050   23364.672  1.900774e+07
2020-12-23 23:30:00  23364.673  23421.745  23301.725   23364.382  1.902411e+07
2020-12-23 23:40:00  23364.384  23421.476  23301.362   23364.003  1.903641e+07
2020-12-23 23:50:00  23364.007  23421.018  23300.902   23363.645  1.904826e+07

[12235 rows x 10 columns]
X_data.shape: (11596, 5) X_data type: <class 'numpy.ndarray'>
 [[-0.01 -0.01 -0.01 -0.01  0.11]
 [-0.01 -0.01 -0.01 -0.01 -0.54]
 [-0.01 -0.01 -0.01 -0.01 -0.69]
 ...
 [ 0.01  0.02  0.02  0.02  0.02]
 [ 0.02  0.02  0.02  0.02 -0.51]
 [ 0.02  0.02  0.02  0.02  0.18]]
val_idx_from: 7956 test_idx_from: 8935
X_train.shape: torch.Size([7956, 5]) X_train type: <class 'torch.Tensor'>
 tensor([[-0.0103, -0.0091, -0.0107, -0.0114,  0.1141],
        [-0.0113, -0.0116, -0.0109, -0.0117, -0.5357],
        [-0.0116, -0.0115, -0.0108, -0.0110, -0.6896],
        ...,
        [-0.0780, -0.0754, -0.0748, -0.0734, -0.2527],
        [-0.0734, -0.0760, -0.0737, -0.0763, -0.6506],
        [-0.0763, -0.0767, -0.0739, -0.0763, -0.3977]], device='cuda:0')
y_train.shape: torch.Size([7956]) y_train type: <class 'torch.Tensor'>
 tensor([1., 1., 1.,  ..., 1., 1., 1.], device='cuda:0')
X_val.shape: torch.Size([979, 5]) X_val type: <class 'torch.Tensor'>
 tensor([[-0.0763, -0.0761, -0.0738, -0.0761, -0.5517],
        [-0.0760, -0.0784, -0.0737, -0.0767, -0.7224],
        [-0.0767, -0.0780, -0.0767, -0.0791, -0.4797],
        ...,
        [-0.0186, -0.0175, -0.0194, -0.0182,  3.2428],
        [-0.0183, -0.0203, -0.0228, -0.0219,  2.7683],
        [-0.0219, -0.0187, -0.0194, -0.0167,  1.5095]], device='cuda:0')
y_val.shape: (979,) y_val type: <class 'numpy.ndarray'>
 [ 1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  0.00  1.00  1.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  1.00  0.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  1.00  1.00
  0.00  0.00  0.00  0.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  0.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  0.00
  0.00  0.00  0.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  0.00  0.00
  0.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  1.00  0.00  0.00  1.00  1.00  1.00  1.00  1.00
  1.00  1.00  1.00  1.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00
  0.00  0.00  0.00  0.00  0.00  1.00  1.00  1.00  1.00  1.00  0.00  0.00
  1.00  0.00  1.00  1.00  1.00  1.00  1.00]
X_test.shape: torch.Size([2661, 5]) X_test type: <class 'torch.Tensor'>
 tensor([[-0.0168, -0.0148, -0.0143, -0.0127,  0.9034],
        [-0.0128, -0.0124, -0.0111, -0.0121,  0.2539],
        [-0.0121, -0.0102, -0.0112, -0.0080,  0.7918],
        ...,
        [ 0.0135,  0.0165,  0.0155,  0.0151,  0.0248],
        [ 0.0151,  0.0152,  0.0175,  0.0159, -0.5096],
        [ 0.0159,  0.0181,  0.0175,  0.0192,  0.1800]], device='cuda:0')
y_test.shape: (2661,) y_test type: <class 'numpy.ndarray'>
 [ 1.00  1.00  1.00 ...  0.00  0.00  0.00]
model type: <class '__main__.LSTMClassifier'>
 LSTMClassifier(
  (lstm): LSTM(5, 16, batch_first=True)
  (dense): Linear(in_features=16, out_features=1, bias=True)
)
loss_function type: <class 'torch.nn.modules.loss.BCELoss'>
 BCELoss()
optimizer type: <class 'torch.optim.adam.Adam'>
 Adam (
Parameter Group 0
    amsgrad: False
    betas: (0.9, 0.999)
    eps: 1e-08
    lr: 0.0001
    weight_decay: 0
)
train_size: 7956
EPOCH: 0 loss: 0.691195547580719 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.5107399425287357
best score updated, Pytorch model was saved!!
EPOCH: 1 loss: 0.6914775967597961 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.5501719006568144
EPOCH: 2 loss: 0.6868444085121155 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.5867456896551724
EPOCH: 3 loss: 0.6009563207626343 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.6160483374384237
EPOCH: 4 loss: 0.6663740277290344 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.6489326765188834
EPOCH: 5 loss: 0.6851885318756104 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.682473830049261
EPOCH: 6 loss: 0.6095741391181946 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.703132697044335
EPOCH: 7 loss: 0.7387176156044006 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.7015419745484401
EPOCH: 8 loss: 0.690645694732666 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.6566810344827586
EPOCH: 9 loss: 0.6829000115394592 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.622172619047619
EPOCH: 10 loss: 0.738982617855072 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.5912587233169129
EPOCH: 11 loss: 0.6995638608932495 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.5838156814449917
EPOCH: 12 loss: 0.7947514057159424 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.5787972085385878
EPOCH: 13 loss: 0.6817842125892639 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.5798465722495894
EPOCH: 14 loss: 0.6924517154693604 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.5760827175697866
EPOCH: 15 loss: 0.5952319502830505 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.5766369047619048
EPOCH: 16 loss: 0.6980494260787964 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.5808010057471265
EPOCH: 17 loss: 0.6621570587158203 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.5822839696223316
EPOCH: 18 loss: 0.6212792992591858 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.586889367816092
EPOCH: 19 loss: 0.6528978943824768 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.5857194170771757
EPOCH: 20 loss: 0.7154173254966736 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.5881773399014778
EPOCH: 21 loss: 0.7460910677909851 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.5952534893267653
EPOCH: 22 loss: 0.6252413988113403 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.6035714285714285
EPOCH: 23 loss: 0.6823109984397888 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.6014983579638752
EPOCH: 24 loss: 0.6286243200302124 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.6065219622331691
EPOCH: 25 loss: 0.6132022142410278 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.6133697660098522
EPOCH: 26 loss: 0.5485300421714783 Val ACC Score: 0.6555435952637244 ROC AUC Score: 0.6092980295566501
EPOCH: 27 loss: 0.5204343795776367 Val ACC Score: 0.6609257265877287 ROC AUC Score: 0.6157327586206897
best score updated, Pytorch model was saved!!
EPOCH: 28 loss: 0.6037634611129761 Val ACC Score: 0.6749192680301399 ROC AUC Score: 0.6112941297208538
best score updated, Pytorch model was saved!!
EPOCH: 29 loss: 0.6126266717910767 Val ACC Score: 0.7158234660925726 ROC AUC Score: 0.6105398193760263
best score updated, Pytorch model was saved!!
Test ACC Score: 0.5947912677135198 ROC AUC Score: 0.45556905274916193
df_ohlcv.shape: (12240, 5) df_ohlcv type: <class 'pandas.core.frame.DataFrame'>
                         Open     High      Low    Close      Volume
timestamp
2020-09-30 00:00:00  10839.5  10842.0  10827.5  10828.0  13500945.0
2020-09-30 00:10:00  10828.5  10829.0  10822.0  10829.0   4477779.0
2020-09-30 00:20:00  10829.0  10829.0  10816.5  10819.5   3589041.0
2020-09-30 00:30:00  10819.5  10820.0  10814.5  10814.5   4523661.0
2020-09-30 00:40:00  10815.0  10820.0  10812.0  10820.0   3463389.0
...                      ...      ...      ...      ...         ...
2020-12-23 23:10:00  23307.0  23400.0  23264.0  23315.5  14089639.0
2020-12-23 23:20:00  23315.5  23485.5  23315.0  23382.0  40956253.0
2020-12-23 23:30:00  23382.0  23420.0  23333.0  23365.0  15243013.0
2020-12-23 23:40:00  23365.5  23376.0  23264.0  23281.0  13256178.0
2020-12-23 23:50:00  23281.5  23288.0  23190.0  23241.0  16298938.0

[12240 rows x 5 columns]
Start                     2020-11-24 13:20:00
End                       2020-12-23 23:50:00
Duration                     29 days 10:30:00
Exposure Time [%]                     10.2358
Equity Final [$]                       110980
Equity Peak [$]                        111948
Return [%]                            10.9804
Buy & Hold Return [%]                 20.5915
Return (Ann.) [%]                     255.219
Volatility (Ann.) [%]                 52.4708
Sharpe Ratio                          4.86403
Sortino Ratio                         30.6017
Calmar Ratio                          127.727
Max. Drawdown [%]                    -1.99816
Avg. Drawdown [%]                   -0.526232
Max. Drawdown Duration        7 days 22:30:00
Avg. Drawdown Duration        0 days 16:50:00
# Trades                                   26
Win Rate [%]                          73.0769
Best Trade [%]                        1.00127
Worst Trade [%]                      -1.00668
Avg. Trade [%]                       0.453453
Max. Trade Duration           0 days 10:40:00
Avg. Trade Duration           0 days 02:37:00
Profit Factor                         2.69042
Expectancy [%]                       0.457399
SQN                                   2.53193
_strategy                    myCustomStrategy
_equity_curve                             ...
_trades                       Size  EntryB...
dtype: object
$

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む