Pythonと機械学習

Pythonも機械学習も初心者ですが、頑張ってこのブログで勉強してこうと思います。

再帰型強化学習

目次

はじめに

こちらのブログで紹介されている、再帰型強化学習を使ってFXトレードをしてみるという記事が面白そうだったので試してみることにしました。

再帰型強化学習について知識はゼロですが、取り敢えずやってみるというのが大事じゃないかと思います。

強化学習とは機械学習の一種で、エージェントがアクションを起こして、そのアクションによって報酬をもらえるというような仕組みみたいで、各アクションでもらえる報酬を最大化するように学習を実施するという物らしいです。

更に再帰型なので、なんかよくわからなくてすごそうです。英語(Recurrent Reinforcement Learning)の頭文字をとってRRLと呼ばれてるみたいです。

上記のブログで参考にしてた論文がリンクされていたので、取り敢えずそちらを見ながら数式を追ってみることにしました。

再帰型強化学習のアルゴリズム

先ず、エージェント(ボットに相当します)のアクションですが、時刻{t}におけるアクションを{F_t}で表しlongかshortかneutral(それ以外)の3通りで、それぞれ数字の1、-1、0を割り当てます。要するに買うか、売るか、何もしないかの3通りです。

{\displaystyle
F_t \in \left[-1, 0 ,1 \right] \tag{1}
}

時刻{t}におけるアクション{F_t}は、{t}より前の{M}個の各時刻の価格変動{r_t}と、一つ前の時刻のアクション{F_{t-1}}に適当な重み{\mathbf{w}}をかけて足し合わせた後、ハイパブリックタンジェントの出力として表します。{p_t}は時刻{t}の価格です。ちょっとパーセプトロンっぽいです。

{\displaystyle
r_t = p_t - p_{t-1} \tag{2}
}


{\displaystyle
\mathbf{x_t} = \left[1,~ r_t,~ \cdots ,~r_{t-M},~ F_{t-1} \right] \tag{3}
}


{\displaystyle
F_t = \tanh \left( \mathbf{w}^{T} \mathbf{x_t} \right) \tag{4}
}

(3)式の一番左の1はしきい値を考慮したものです。M個の価格変動値{r_t}とひとつ前のアクション {F_{t-1}} と合わせて、{\mathbf{x_t}}は、全部でM+2個の成分からなるベクトルになります。

必然的に重み{\mathbf{w}}もM+2個の成分を持つベクトルになります。

現在のアクション{F_t}を決定するために、一つ前の時間のアクション{F_{t-1}}が必要なところが再帰型というわけですね。

時刻{t}においてもらえる報酬{R_t}は、取引量{\mu}とスプレッド{\sigma}を使って以下のようにかけます。

{\displaystyle
R_t =\mu \left(F_{t-1} r_t - \sigma |F_t - F_{t-1}| \right) \tag{5}
}

基本的には、エージェントは各時刻で報酬{R_t}が大きくなるようなアクション{F_t}をとるように学習すればいいと思いますが、上記の論文では、ある学習期間{T}内で定義されるシャープレシオ{S_T}というものを考え、それを最大化するように学習するみたいです。

シャープレシオ{S_T}は、期間{T}内での{R_t}の平均と標準偏差の比で表されます。

標準偏差は、2乗平均{B}と平均{A}の2乗の差で表すことができるので以下の様に書くことができます。


{\displaystyle
A =  \frac{1}{T} \sum^{T}_{t=1}R_t  \tag{6}
}

{\displaystyle
B =  \frac{1}{T} \sum^{T}_{t=1}R_t^2  \tag{7}
}

{\displaystyle
S_T =  \frac{A}{\sqrt{B - A^2}}  \tag{8}
}

確かに一回あたりの取引でもらえる報酬の平均が大きいほど、更にそのバラツキである標準偏差が小さいほど確実な利益を確保できるので、このシャープレシオという指標を最大化することは理にかなっています。

重みを変化させることで各時刻のアクション{F_t}が変化し、その結果シャープレシオが変化するので、シャープレシオの重みに対する勾配{\frac{dS_T}{d\mathbf{w}}}を算出し、その勾配方向に向かって重みを少しずつ変化させてやることでシャープレシオを最大化させる事ができます。

コスト関数がシャープレシオになり、勾配降下法が勾配上昇法になってますが、学習方法はADALINEなんかとにてますね。

シャープレシオの重みに対する勾配を求める為に、シャープレシオを重みで微分しなければいけません。これがまた大変ですね。。

シャープレシオ{S_T}は、平均{A}と2乗平均{B}の関数で、更に{A}{B}{R_t}の関数で、更に{R_t}{F_t}の関数で、{F_t}がようやく重み{\mathbf{w}}の関数なので、シャープレシオに対し随分深いところに重み{\mathbf{w}}がいます。

こういった関数の中に関数があって更にその中に関数があって、、という場合の微分はチェーンルールという方法を使うと簡単(でもないですが。。)に展開する事ができます。


{\displaystyle
\frac{dS_T}{d\mathbf{w}} = \sum^{T}_{t=1} \left\{ \frac{dS_T}{dA} \frac{dA}{dR_t} +\frac{dS_T}{dB} \frac{dB}{dR_t} \right\} \cdot \left\{ \frac{dR_t}{dF_t} \frac{dF_t}{d\mathbf{w}} + \frac{dR_t}{dF_{t-1}} \frac{dF_{t-1}}{d\mathbf{w}} \right\} \tag{9}
}

各成分について具体的に求めていきます。


{\displaystyle
\frac{dS_T}{dA} = \frac{S_T \left(1 + S_T^{2} \right) }{A} \tag{10}
}

{\displaystyle
\frac{dA}{dR_t} = \frac{1}{T} \tag{11}
}

{\displaystyle
\frac{dS_T}{dB} = \frac{S_T^{3}}{2A^2} \tag{12}
}

{\displaystyle
\frac{dA}{dR_t} = \frac{2R_t}{T} \tag{13}
}

{\frac{dR_t}{dF_t}}{\frac{dR_t}{dF_{t-1}}}は、符号関数{sgn()}を使って以下の様に書けます。

{
\displaystyle
\begin{align}

\frac{dR_t}{dF_t} &= \frac{d}{dF_t} \left\{ \mu \left(F_{t-1} r_t - \sigma |F_t - F_{t-1}| \right) \right\}  \\
\\

&= \frac{d}{dF_t} \left\{- \mu \sigma |F_t - F_{t-1}| \right\} \\
\\

&= \begin{cases}
- \mu \sigma & (F_t - F_{t-1}>0) \\
  \mu \sigma & (F_t - F_{t-1}<0)
\end{cases} \\
\\

&= \mu \sigma \cdot sgn(F_t - F_{t-1}) \tag{14}


\end{align}
}
{
\displaystyle
\begin{align}

\frac{dR_t}{dF_{t-1}} &= \frac{d}{dF_{t-1}} \left\{ \mu \left(F_{t-1} r_t - \sigma |F_t - F_{t-1}| \right) \right\}  \\
\\

&= \mu r_t -  \frac{d}{dF_t} \left\{\mu \sigma |F_t - F_{t-1}| \right\} \\
\\

&= \begin{cases}
\mu r_t + \mu \sigma & (F_t - F_{t-1}>0) \\
\mu r_t - \mu \sigma & (F_t - F_{t-1}<0)
\end{cases} \\
\\

&= \mu r_t + \mu \sigma \cdot sgn(F_t - F_{t-1}) \tag{15}


\end{align}
}

続いて{\frac{dF_t}{d\mathbf{w}}}{\frac{dF_{t-1}}{d\mathbf{w}}}を求めてみましょう。

{
\displaystyle
\begin{align}

\frac{dF_t}{d\mathbf{w}} &= \frac{d}{d\mathbf{w}} \tanh \left( \mathbf{w}^{T} \mathbf{x_t} \right)  \\
\\

&= \left\{1 - \tanh \left( \mathbf{w}^{T} \mathbf{x_t} \right) ^2 \right\} \frac{d\left( \mathbf{w}^{T} \mathbf{x_t} \right)}{d\mathbf{w}} \\
\\

&= \left\{1 - \tanh \left( \mathbf{w}^{T} \mathbf{x_t} \right) ^2 \right\} \left\{\mathbf{x_t} + w_{M+2} \frac{dF_{t-1}}{d\mathbf{w}} \right\} \\
\\

&= \left(1 - F_t^2 \right) \left\{\mathbf{x_t} + w_{M+2} \frac{dF_{t-1}}{d\mathbf{w}} \right\} \tag{16}


\end{align}
}

(16)式より、{\frac{dF_{t-1}}{d\mathbf{w}}}は、

{
\displaystyle

\frac{dF_{t-1}}{d\mathbf{w}}=\left(1 - F_{t-1}^2 \right) \left\{\mathbf{x_{t-1}} + w_{M+2} \frac{dF_{t-2}}{d\mathbf{w}} \right\} \tag{17}
}

(10)式から(17)式を全部計算し(9)式に代入することで、{\frac{dS_T}{d\mathbf{w}}}の値を計算し、学習率{\rho}をかけて、足しこむことで重みをアップデートしていきます。

{
\displaystyle
\mathbf{w}:=\mathbf{w} + \rho \frac{dS_T}{d\mathbf{w}} \tag{18}
}

{R_t}の算出式(5)では、{F_t}が1と-1と0に量子化された値として見てますが、論文中の実際の計算では{F_t}は実数として扱っているようです。

{F_t}量子化された値として考えると、量子化関数のクォンタライザーも{\mathbf{w}}微分しなけらばならず、よく分からないことになるからだと思います。

また{F_t}の算出式(4)や、{\frac{dF_t}{d\mathbf{w}}}{\frac{dF_{t-1}}{d\mathbf{w}}}の算出式(16)、(17)では再帰的に前の値に依存しているので、実際の計算では初期値が必要になってきます。

{F_t}の初期値を{F_0}を0と置き、{\frac{dF_t}{d\mathbf{w}}}の初期値{\frac{dF_0}{d\mathbf{w}}}を0ベクトルと置いてやればいいです。

{
\displaystyle
F_0=0 \tag{19}
}
{
\displaystyle
\begin{eqnarray}
\frac{dF_{0}}{d\mathbf{w}}=\left[
\begin{array}{c}
0 \\
0 \\
\vdots \\
0
\end{array}
\right]
\end{eqnarray} \tag{20}
}

{\frac{dF_t}{d\mathbf{w}}}{\frac{dF_{t-1}}{d\mathbf{w}}}は、重み{\mathbf{w}}と同じ個数(M+2個)の成分を持つベクトルになります。

必然的に{\frac{dS_T}{d\mathbf{w}}}もM+2個の成分を持つベクトルです。

Pythonで実装してみる

論文中では、IBMの日足データを使って、過去のある期間{T}シャープレシオを最大化するよう重み{\mathbf{w}}を学習させ、そこから未来の期間{T}で最適化した重み{\mathbf{w}}を使って取引したらどうなるか?という検証をしています。

f:id:darden:20170306210958p:plain

IBMの日足データでは、どうやら効果があるみたいです。

私は出来ればFXで実装したかったので、ブログに合わせてUSDJPYの30分足で検証してみることにしました。

MT4で出力したデータをGistにアップ(USDJPY30.csv)しています。

USDJPY30.csvは、日付、時間、始値、高値、安値、終値出来高の順でデータが並んでいます。

スクリプト中では、まずUSDJPY30.csvのデータを全部読み込んで、日付と終値all_tall_pという変数に保存します。

過去になる程インデックスが大きくなるように設定しています。(単にそうした方が私にとって扱い安いので。) スクリプト中では、tより過去はt+1で表現しているので注意してください。

13行目のinit_tで現在の時間インデックスを指定し、そこから過去の期間T(15行目のT=1000)の価格データをall_tall_pから切り出して学習を実施します。

学習後は、最適化された重みを使って未来の期間Tの価格データを再びall_tall_pから切り出し、取引シミュレーションするという流れです。

学習前の重みの初期値は、取り敢えず全部1にしておきました。

検証結果

学習では全部で10000回のエポックを回しており、100エポック毎にシャープレシオと経過時間を出力する様にしています。

Epoch loop start. Initial sharp's ratio is -0.0135567256889.
Epoch: 100/10000. Shape's ratio: 0.0343913902529. Elapsed time: 13.3904920599 sec.
Epoch: 200/10000. Shape's ratio: 0.0554093411079. Elapsed time: 26.6309506066 sec.
...
...
...
Epoch: 9900/10000. Shape's ratio: 0.166130877648. Elapsed time: 1308.91061026 sec.
Epoch: 10000/10000. Shape's ratio: 0.166027379068. Elapsed time: 1321.8811445 sec.
Epoch loop end. Optimized sharp's ratio is 0.166027379068.
Epoch: 10000/10000. Shape's ratio: 0.166027379068. Elapsed time: 1321.88194105 sec.

結構時間かかりますね。。

シャープレシオの推移

以下シャープレシオの推移です。学習率{\rho}は1で実施しました。

f:id:darden:20170306234326p:plain

途中で少し値が飛んでますが、エポック数が増えるに従いちゃんとシャープレシオが増えてますね。取りあえずは問題ないようです。

過去の期間Tでの学習結果

以下が過去の期間Tでの学習結果です。上から順にUSDJPYの価格、アクション{F_t}、報酬{R_t}の累積和です。

青い線が学習前の重み(全部1)でトレードを実施した結果で、赤い線が学習後の最適化された重みでトレードを実施した結果です。

f:id:darden:20170306234832p:plain

学習後は、報酬{R_t}の累積和が右肩上がりで増えてますね。

うまく学習できているみたいです。

未来の期間Tでの取引シミュレーション結果

ではこの学習した重みを使って、未来の期間Tでトレードしてみたらどうなるか検証してみましょう。

こちらも上から順にUSDJPYの価格、アクション{F_t}、報酬{R_t}の累積和です。

青い線が全部1の重みでトレードを実施した結果で、赤い線が先ほどの過去の期間Tで学習した重みを使ってトレードを実施した結果です。

f:id:darden:20170306235203p:plain

?。報酬{R_t}の累積和をみると、学習した重みで実施したトレードの方が悪い結果になってますね。。ロバスト性はあまりないようです。

取り敢えずパラメータである期間{T}と重みにかける過去データの個数{M}の値を最適化してみたい所ですが、私の環境では10000回のエポックループを回すのに1300秒もかかってしまいます。

先ずは学習スピードを何とかして速くするのが先決ですね。愈々Cythonを使ってみようかな。