Github项目:https://github.com/Ceruleanacg/Personae
前八期传送门:
【系列58】强化学习在Market Making上的应用
【系列57】为什么机器学习在投资领域并不是那么好用
【系列56】特征重要性在量化投资中的深度应用
【系列55】机器学习应用量化投资必须要踩的那些坑
【系列54】因子的有效性分析基于7种机器学习算法
【系列53】基于XGBoost的量化金融实战
【系列52】基于Python预测股价的那些人那些坑
正文
目前,在本项目中:
对于监督学习模型的数据集:
我们采用2008年1月1日到2018年1月1日这个区间内
这四只银行股在第T天的
作为输入数据,第T+1天的
作为输出数据,进行训练,其中,这个区间前70%的数据作为训练数据,后30%作为测试数据,目前没有设置验证集数据。
下图是目前的实验结果,就目前的实验结果来看,监督学习的表现要远好于强化学习。
图例 :蓝色的折线是测试数据集,其他颜色的折线是三种不同的监督学习模型在测试集上的预测。
接下来,我们将会依次对这三个监督学习的模型做一个简短的介绍。
Naive-LSTM (LSTM)
该模型是基于LSTM和Dense(全连接)的基本模型,输入是序列长度为5,即第T到第T+4天的OCHLV数据,输出是一个实数,代表了第T+5的预测收盘价格。
arXiv:1506.02078: https://arxiv.org/abs/1506.02078
计算图:
Naive-LSTM
以下是构建模型的核心代码:
def _init_nn(self):
self.rnn = self.add_rnn(1, self.hidden_size)
self.rnn_output, _ = tf.nn.dynamic_rnn(self.rnn, self.x, dtype=tf.float32)
self.rnn_output = self.rnn_output[:, -1]
self.rnn_output_dense = self.add_fc(self.rnn_output, 16)
self.y = self.add_fc(self.rnn_output_dense, self.y_space)
可以看出,第一行代码调用了项目中封装的用于构建LSTM层的API,第二行代码用于计算该层输出和状态序列,第四行和第五行构造了一个全连接层并计算最终的输出。
IJCAI 2017. Hybrid Neural Networks for Learning the Trend in Time Series,https://www.ijcai.org/proceedings/2017/0316.pdf
上述引用的论文提出了一种混合神经网络的结构,同时用RNN与CNN提取序列特征,然后将输出拼接作为全连接层的输入,最后输出最终的预测结果。
计算图:
Tre-Net
以下是构建模型的核心代码:
def _init_nn(self):
self.rnn = self.add_rnn(1, self.hidden_size)
self.rnn_output, _ = tf.nn.dynamic_rnn(self.rnn, self.rnn_x, dtype=tf.float32)
self.rnn_output = self.rnn_output[:, -1]
# self.cnn_x_input is a [-1, 5, 20, 1] tensor, after cnn, the shape will be [-1, 5, 20, 5].
self.cnn = self.add_cnn(self.cnn_x, filters=2, kernel_size=[2, 2], pooling_size=[2, 2])
self.cnn_output = tf.reshape(self.cnn, [-1, self.seq_length * self.x_space * 2])
self.y_concat = tf.concat([self.rnn_output, self.cnn_output], axis=1)
self.y_dense = self.add_fc(self.y_concat, 16)
self.y = self.add_fc(self.y_dense, self.y_space)
可以看出,第一到第三行构造了LSTM层并计算结果,第四到第五行用项目封装的构造CNN的API构造了CNN层并计算了结果。最后拼接了RNN和CNN的结果,作为全连接层的输入,然后得到最终的计算结果。
arXiv:1704.02971: A Dual-Stage Attention-Based Recurrent Neural Network for Time Series Prediction。https://arxiv.org/abs/1704.02971
上述引用的论文提出了一种基于注意力机制(Attention Based Model)的与Seq-to-Seq模型的网络结构,其创新点在于该模型连续两次使用注意力机制,在对原始序列使用注意力机制求权重后再次使用注意力机制对编码后的序列求权重,然后经解码与全连接层后输出结果。
计算图:
DA-RNN
以下是构建模型的核心代码:
def _init_nn(self):
# First Attn
with tf.variable_scope("1st_encoder"):
self.f_encoder_rnn = self.add_rnn(1, self.hidden_size)
self.f_encoder_outputs, _ = tf.nn.dynamic_rnn(self.f_encoder_rnn, self.x, dtype=tf.float32)
self.f_attn_inputs = self.add_fc(self.f_encoder_outputs, self.hidden_size, tf.tanh)
self.f_attn_outputs = tf.nn.softmax(self.f_attn_inputs)
with tf.variable_scope("1st_decoder"):
self.f_decoder_input = tf.multiply(self.f_encoder_outputs, self.f_attn_outputs)
self.f_decoder_rnn = self.add_rnn(1, self.hidden_size)
self.f_decoder_outputs, _ = tf.nn.dynamic_rnn(self.f_decoder_rnn, self.f_decoder_input, dtype=tf.float32)
# Second Attn
with tf.variable_scope("2nd_encoder"):
self.s_attn_input = self.add_fc(self.f_decoder_outputs, self.hidden_size, tf.tanh)
self.s_attn_outputs = tf.nn.softmax(self.s_attn_input)
with tf.variable_scope("2nd_decoder"):
self.s_decoder_input = tf.multiply(self.f_decoder_outputs, self.s_attn_outputs)
self.s_decoder_rnn = self.add_rnn(2, self.hidden_size)
self.f_decoder_outputs, _ = tf.nn.dynamic_rnn(self.s_decoder_rnn, self.s_decoder_input, dtype=tf.float32)
self.f_decoder_outputs_dense = self.add_fc(self.f_decoder_outputs[:, -1], 16)
self.y = self.add_fc(self.f_decoder_outputs_dense, self.y_space)
可以看出,分别对应于四个变量命名空间,具有2组4个编解码层,在每一个编解码层都运用了一次注意力机制,求出当前序列的权重,然后与序列相乘后进行下一步的编解码工作,这是一种Seq-to-Seq的机制,更广泛地用于自然语言处理。最终解码的输出结果作为全连接层的输入,然后计算最终的结果。
以上是关于项目中监督学习模型的简短介绍,其中,所有模型的具体实现可以在项目链接中看到。
接下来是关于3个强化学习模型的介绍,但是在介绍强化学习模型前,我们首先对强化学习的数据和环境一个简短的概述。
这个文件实现了三个核心类,分别是:
他们分别代表了市场、交易员、持仓信息,最终Market类作为Agent(强化学习模型)的Environment(环境),接受Agent的Action(动作),同时给出Next State(下一状态)和Reward(奖励),并进行迭代。
对于强化学习使用的数据,我们使用这四只银行股在第T天的
和交易员在第T天的
作为State(状态),使用交易指令
作为Agent(智能体)的Action(动作),其中,Reward Func(奖励函数)的计算代码如下:
def _update_reward(self, action_code, action_status, position):
if action_code == ActionCode.Buy:
if action_status == ActionStatus.Success:
if position.pro_value > position.cur_value:
self.reward += 70
else:
self.reward -= 50
else:
self.reward -= 100
elif action_code == ActionCode.Sell:
if action_status == ActionStatus.Success:
if position.pro_value > position.cur_value:
self.reward -= 70
else:
self.reward += 50
else:
self.reward -= 100
else:
if action_status == ActionStatus.Success:
if position.pro_value > position.cur_value:
self.reward += 70
else:
self.reward -= 50
else:
self.reward -= 100
接下来是关于实验结果与强化学习模型的介绍:
图例 - 横坐标是时间,纵坐标是利润,其中蓝色折线是基准线,其他颜色的折线是强化学习模型表现
可以看出,除了Policy Gradient可以跑赢基准收益外,其他强化学习模型的收益甚至不如基准,这里非常值得讨论,目前笔者也在尝试从参数、输入特征、输出特征、奖励函数等多个角度考虑解决该问题。
接下来是关于强化学习模型的介绍:
NIPS. Vol. 99. 1999: Policy gradient methods for reinforcement learning with function approximation。https://papers.nips.cc/paper/1713-policy-gradient-methods-for-reinforcement-learning-with-function-approximation.pdf
计算图:
Policy Gradient
Basic Policy Gradient的思想很朴素,对于一次采样的所有动作中,根据奖励函数值的正负决定梯度下降的方向,从而提高或者降低这些动作出现的概率。
Policy Gradient Algorithm
以下是构建模型的核心代码:
def _init_nn(self):
# Initialize predict actor and critic.
w_init, b_init = tf.random_normal_initializer(.0, .3), tf.constant_initializer(0.1)
with tf.variable_scope('nn'):
first_dense = tf.layers.dense(self.s,
50,
tf.nn.relu,
kernel_initializer=w_init,
bias_initializer=b_init)
second_dense = tf.layers.dense(first_dense,
50,
tf.nn.relu,
kernel_initializer=w_init,
bias_initializer=b_init)
action_prob = tf.layers.dense(second_dense,
self.a_space,
tf.nn.tanh,
kernel_initializer=w_init,
bias_initializer=b_init)
self.a_prob = action_prob
self.a_s_prob = tf.nn.softmax(action_prob)
def _init_op(self):
with tf.variable_scope('loss'):
# a_one_hot = tf.one_hot(self.a, self.a_space)
# negative_cross_entropy = -tf.reduce_sum(tf.log(self.a_prob) * a_one_hot)
negative_cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=self.a_prob, labels=self.a)
self.loss_fn = tf.reduce_mean(negative_cross_entropy * self.r)
with tf.variable_scope('train'):
self.train_op = tf.train.RMSPropOptimizer(self.learning_rate * 2).minimize(self.loss_fn)
self.session.run(tf.global_variables_initializer())
本实现简单地采用两次全连接后输出Softmax后各个动作的概率,最后期望最小化采样动作的概率与真实概率乘以奖励函数的值的交叉熵。
arXiv:1509.06461: Deep Reinforcement Learning with Double Q-learning。https://arxiv.org/abs/1509.06461
计算图:
Double-DQN
Double-DQN采用评估网络与目标网络相互制约,期望避免传统DQN中容易出现的过度估计问题。首先使用评估网络预测下一个状态的状态-动作函数值,然后选取取得最大值的动作,计做amax,接着用目标网络预测下一状态与采用amax的状态值计算标签,然后期望最小化标签与评估网络对当前状态的状态-动作函数和当前动作的Q的均方差。
Double-DQN
以下是构建模型的核心代码:
def _init_op(self):
self.loss = tf.reduce_mean(tf.squared_difference(self.q_next, self.q_eval))
self.train_op = tf.train.RMSPropOptimizer(self.learning_rate).minimize(self.loss)
self.e_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='q_eval')
self.t_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='q_target')
self.update_q_target_op = [tf.assign(t, e) for t, e in zip(self.t_params, self.e_params)]
self.session.run(tf.global_variables_initializer())
def train(self):
# 1. If buffer length is less than buffer size, return.
if self.buffer_length < self.buffer_size:
return
# 2. Update Q-Target if need.
if self.total_step % self.update_q_target_step == 0:
self.session.run(self.update_q_target_op)
# 3. Get transition batch.
s, a, r, s_next = self.get_transition_batch()
# 4. Calculate q_eval_next.
q_eval_next = self.session.run(self.q_eval, {self.s: s_next})
# 5. Get action indices and make batch indices.
a_indices = np.argmax(q_eval_next, axis=1)
b_indices = np.arange(self.batch_size, dtype=np.int)
# 6. Calculate q_target_next selected by actions.
q_target_next = self.session.run(self.q_target, {self.s_next: s_next})
q_target_next_with_a = q_target_next[b_indices, a_indices]
# 7. Calculate labels.
q_eval = self.session.run(self.q_eval, {self.s: s})
q_next = q_eval.copy()
q_next[b_indices, a.astype(np.int)] = r + self.gamma * q_target_next_with_a
# 8. Calculate loss.
_, self.critic_loss = self.session.run([self.train_op, self.loss], {self.s: s, self.q_next: q_next})
# 9. Increase total step.
self.total_step += 1
Double-DQN的实现代码中,注释已经非常详尽,在这里就不再过多赘述。
arXiv:1509.02971: Continuous control with deep reinforcement learning。https://arxiv.org/abs/1509.02971
计算图:
DDPG
DDPG用于连续动作空间,在本问题中,对于四只股票的买卖持有的动作被映射到区间[0, 11],其中,DDPG使用Actor-Critic Model,引入评估Actor,目标Actor模型与评估Critic,目标Critic模型两组四个网络,其中Actor模型用于预测动作,Critic模型用于评估当前状态与动作的分数(状态-动作值函数),该方法期望最小化:评估Critic与评估Actor对当前状态-动作函数值与目标Critic和目标Actor对下一状态-动作函数值的均方差(如算法图所示),依次迭代改进目标Critic和目标Actor。
DDPG
以下是构建模型的核心代码:
def _init_nn(self):
# Initialize predict actor and critic.
self.a_predict = self.__build_actor_nn(self.s, "predict/actor", trainable=True)
self.q_predict = self.__build_critic(self.s, self.a_predict, "predict/critic", trainable=True)
# Initialize target actor and critic.
self.a_next = self.__build_actor_nn(self.s_next, "target/actor", trainable=False)
self.q_next = self.__build_critic(self.s_next, self.a_next, "target/critic", trainable=False)
# Save scopes
self.scopes = ["predict/actor", "target/actor", "predict/critic", "target/critic"]
def _init_op(self):
# Get actor and critic parameters.
params = [tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope) for scope in self.scopes]
zipped_a_params, zipped_c_params = zip(params[0], params[1]), zip(params[2], params[3])
# Initialize update actor and critic op.
self.update_a = [tf.assign(t_a, (1 - self.tau) * t_a + self.tau * p_a) for p_a, t_a in zipped_a_params]
self.update_c = [tf.assign(t_c, (1 - self.tau) * t_c + self.tau * p_c) for p_c, t_c in zipped_c_params]
# Initialize actor loss and train op.
self.a_loss = -tf.reduce_mean(self.q_predict)
self.a_train_op = tf.train.RMSPropOptimizer(self.learning_rate).minimize(self.a_loss, var_list=params[0])
# Initialize critic loss and train op.
self.q_target = self.r + self.gamma * self.q_next
self.c_loss = tf.losses.mean_squared_error(self.q_target, self.q_predict)
self.c_train_op = tf.train.RMSPropOptimizer(self.learning_rate * 2).minimize(self.c_loss, var_list=params[2])
# Initialize variables.
self.session.run(tf.global_variables_initializer())
代码首先初始化两组四个网络,分别是评估Actor、目标Actor,评估Critic,目标Critic,然后根据DDPG的算法最小化评估Critic与评估Actor对当前状态-动作函数值与目标Critic和目标Actor对下一状态-动作函数值的均方差,依次迭代更新Actor与Critic直至收敛。
arXiv:1511.06581: Dueling Network Architectures for Deep Reinforcement Learning。https://arxiv.org/abs/1511.06581
计算图:
算法:
Dueling-DQN
相对于DQN直接输出状态-动作函数值,Dueling-DQN的状态-动作函数值由上式决定,从网络结构上可以看出,在输出状态-动作函数值前,Dueling-DQN的结构拆分了原DQN网络结构的最后一层,这样的思想很像Actor-Critic模型中的Baseline,因为并不是每个状态都是十分重要的,有些时候对于这些状态,采取那个动作都不会有很大的影响。
以下是构建模型的核心代码:
def __build_critic_nn(self, s, scope):
w_init, b_init = tf.random_normal_initializer(.0, .3), tf.constant_initializer(.1)
with tf.variable_scope(scope):
s_first_dense = tf.layers.dense(s,
16,
activation=tf.nn.relu,
kernel_initializer=w_init,
bias_initializer=b_init)
s_second_dense = tf.layers.dense(s_first_dense,
16,
tf.nn.relu,
kernel_initializer=w_init,
bias_initializer=b_init)
value = tf.layers.dense(s_second_dense,
1,
kernel_initializer=w_init,
bias_initializer=b_init)
advantage = tf.layers.dense(s_second_dense,
self.a_space,
kernel_initializer=w_init,
bias_initializer=b_init)
q = value + (advantage - tf.reduce_mean(advantage, axis=1, keep_dims=True))
return q
即最终对于某个状态-动作函数值而言,Advantage的在不同动作维度上的值一定意义上描述了这个动作对于这个状态的重要性,最后加上Q值,避免了过度估计。
以上是最近关于强化学习和监督学习在A股中的一些应用和相关论文方法的实现。
同时,项目中可能有Bug,欢迎各种Issue提出以及欢迎贡献各种代码 : )