はじめに
近年、人工知能ブームにより、人工知能を使ったトレーディング手法が盛んである。そこで、今回は深層強化学習を用いたシステムトレーディングを実施した。
まず、基本的な深層強化学習を用いたトレーディングモデルである。agentの行動として、 BUY、HOLD、SELLの三つの内一つを選択する。環境の戻り値として、状態(今現在保有しているポジションの価格、市場価格、手持ちのキャッシュ)、報酬(手持ちのキャッシュの変化値(含む益も含む))、終了(取引の終了か否か)、情報(ターミナルにディスプレイする情報)を返す。
使用データについて
トレンド傾向の掴みやすさから、yahoo financeからGSPCの日足を使用した。
訓練データの期間:2015/1/1 - 2017/6/30
テストデータの期間:2017/7/1 - 2021/1/1
以下ソースコード
# PPO1
ppo(Proximal Policy Optimization Algorithms)の特徴は以下の通り
・ 更新前と更新後の方策関数の変化量を抑えるため、クリッピング操作を行う
・ TRPOより、実装が安易
・ 方策ベース手法により、条件付き確率で行動する
・ Actor-Criticモデル
PPOはagentの役割が大きく変容する。agentは経験を溜め、ミニバッチ法により、観測データを抽出し、勾配をParmeter Serverに蓄積させる。
損失関数
L^{CLIP+ VF + S}_t(\theta) = \hat{E_t}[L^{CLIP}_t(\theta) - c_1 L^{VF}_t(\theta) + c_2 S[ \pi_{\theta}] (s_t) ]
方策損失関数
clip手法を用いて、損失関数の更新を大きく変更されることを防いでいる。
L^{CLIP}_t(\theta) = \hat{E_t}[min(r_t(\theta) \hat{A_t},clip(r_t(\theta), 1 - \epsilon, 1 + \epsilon)\hat{A_t})] \\
r_t(\theta) = \frac{\pi_{\theta} (a|s)}{ \pi_{\theta_{old}}(a|s)}
状態価値損失関数
L_t^{\text{VF}} = (V_\theta(s_t) - V_t^{\text{targ}})^2 \\
V_t^{\text{targ}} = \sum_{l=0}^{ \infty } \gamma^l r_{t+1}
GAE2
Advantageを推定する方法として、PPOでは、GAE(HIGH-DIMENSIONAL CONTINUOUS CONTROL USING GENERALIZED ADVANTAGE ESTIMATION)が用いられる。学習を安定化させるために、λ を変更することで、推定量の分散とバイアスを調節をする。
A^{GAE}(s_t,a_t) = \sum_{l=0}^{ \infty } (\gamma \lambda)^l \delta_{t+1} \\
\delta_{t} = r_t + \gamma V(s_{t + 1}) - V(s_{t})
エントロピー損失関数
S[ \pi_{\theta}] (s_t) = - \sum_{a_t} \pi_{\theta} (a|s) log (\pi_{\theta}(a|s))
ε - greedy法
行動の初期に、ランダムな行動をし、報酬の局所的な最大化を防ぐ、方法を ε - greedy 法という。実行回数の増大化に伴い、 ε が減少し、ランダムな行動の回数が減り、その代わり、学習による行動が増える。
マルチスレッド
マルチスレッドの特徴は以下の通り
・平行処理
・GILロックを持っているスレッドのみ、実行可能、他のスレッドは待機する。
・cpuのコア数に依存しない。
マルチプロセスの特徴は以下の通り
・並列処理
・メモリーが共有されていないので、値の受け渡しが大変。
・tensowflowだと、modelが動かないことがある。
・cpuのコア一つにプロセスを割り当てする。
cpuバウンドとはcpu内で行なっている数値計算などの処理
ループ
class Task:
def __init__(self, name):
self.name = name
self.num = 0
self.sum = 0.0
def run(self):
while True:
sleep(1)
if self.num == 3:
break
print('roop_name = ' + self.name + ' :num = '+ str(self.num) + ' :sum = '+ str(self.sum))
self.num += 1
self.sum += random.random()
name = 'nomal-roop'
start = time.time()
for i in range(4):
w = Task('roop_point_'+str(i))
w.run()
end = time.time() - start
arr.append("name:" + name + " process_time:{0}".format(end) + "[s]")
roop_name = roop_point_0 :num = 0 :sum = 0.0
roop_name = roop_point_0 :num = 1 :sum = 0.642469181212962
roop_name = roop_point_0 :num = 2 :sum = 1.5964812171373977
roop_name = roop_point_1 :num = 0 :sum = 0.0
roop_name = roop_point_1 :num = 1 :sum = 0.8876820994429431
roop_name = roop_point_1 :num = 2 :sum = 1.627826300716026
roop_name = roop_point_2 :num = 0 :sum = 0.0
roop_name = roop_point_2 :num = 1 :sum = 0.03546302344611851
roop_name = roop_point_2 :num = 2 :sum = 1.0239282875765587
roop_name = roop_point_3 :num = 0 :sum = 0.0
roop_name = roop_point_3 :num = 1 :sum = 0.602393530385244
roop_name = roop_point_3 :num = 2 :sum = 1.555539488491399
マルチスレッド
class Task:
def __init__(self, name):
self.name = name
self.num = 0
self.sum = 0.0
def run(self):
while True:
sleep(1)
if self.num == 3:
break
print('roop_name = ' + self.name + ' :num = ' + str(self.num) + ' :sum = ' + str(self.sum))
self.num += 1
self.sum += random.random()
name = 'thread-pool'
start = time.time()
thread_num = 4
threads = []
for i in range(thread_num):
threads.append(Task(name=f'thread_{i}'))
datas = []
with ThreadPoolExecutor(max_workers = thread_num) as executor:
for task in threads:
job = lambda: task.run()
datas.append(executor.submit(job))
end = time.time() - start
arr.append("name:" + name + " process_time:{0}".format(end) + "[s]")
roop_name = thread_0 :num = 0 :sum = 0.0
roop_name = thread_1 :num = 0 :sum = 0.0
roop_name = thread_2 :num = 0 :sum = 0.0
roop_name = thread_3 :num = 0 :sum = 0.0
roop_name = thread_0 :num = 1 :sum = 0.7829927782861958
roop_name = thread_2 :num = 1 :sum = 0.7264674393557742
roop_name = thread_1 :num = 1 :sum = 0.4721450639806136
roop_name = thread_3 :num = 1 :sum = 0.2746835685320669
roop_name = thread_0 :num = 2 :sum = 0.8189509274906515
roop_name = thread_1 :num = 2 :sum = 0.7522106668563098
roop_name = thread_2 :num = 2 :sum = 1.3346477522815392
roop_name = thread_3 :num = 2 :sum = 0.33216049073474685
売買ルール
1.空売りは認めない
2.ポジションを持っている場合、追加注文を出せない。
3.最後のステップでポジションを全て売却する。
4.ポジションは全買い、全売り
5.所持金は1000000ドル
実装と結果
ソースコードはこちら
時系列に影響されないために、ReplyMemoryが、時系列データを扱う本論では、裏目に出ている。
ソースコードはこちら
謝意
コードの記述の際に以下のサイトを参考にしました。
大変参考になりました。
【強化学習】実装しながら学ぶPPO【CartPoleで棒立て:1ファイルで完結】
-
J. Schulman, F. Wolski, P. Dhariwal, A. Radford, O. Klimov "Proximal Policy Optimization Algorithms"(2017) In OpenAI. ↩
-
J. Schulman, P. Moritz, S. Levine, M. I. Jordan and P. Abbeel "HIGH-DIMENSIONAL CONTINUOUS CONTROL USING GENERALIZED ADVANTAGE ESTIMATION" (2016) Department of Electrical Engineering and Computer Science University of California, Berkeley ↩