完整架构概述
在这篇文章中,我将创建一个预测股票价格变动的完整过程。我们将使用生成对抗网络(GAN)与LSTM(一种循环神经网络)作为生成器,使用卷积神经网络CNN作为鉴别器。我们使用LSTM的原因很明显,我们正在尝试预测时间序列数据。为什么我们使用GAN,特别是卷积神经网络(CNN)作为鉴别器呢?这是一个很好的问题:稍后会有特别的部分。
当然,我们将详细介绍每个步骤,但最困难的部分是GAN:成功训练GAN的非常棘手的部分是获得正确的超参数集。出于这个原因,我们将使用贝叶斯优化(还有高斯过程)和深度强化学习(DRL)来决定何时以及如何改变GAN的超参数。在创建强化学习时,我将使用该领域的最新进展,例如Rainbow和PPO。
我们将使用许多不同类型的输入数据。除了股票的历史交易数据和技术指标,我们将使用NLP的最新进展(使用“BERT,对NLP进行迁移学习)来创建情感分析(作为基本面分析的来源) ),用于提取整体趋势方向的傅里叶变换,用于识别其他高级特征的栈式自动编码器,用于查找相关资产的特征投资组合,差分整合移动平均自回归模型(ARIMA))对于股票函数
近似,以便捕获尽可能多的关于股票的信息,模式,依赖关系等。我们都知道,数据越多越好。预测股价走势是一项极其复杂的任务,所以我们对股票(从不同的角度)了解得越多,我们的系统就会越好。
为了创建所有神经网络,我们将使用MXNet和它的高级API - Gluon,并在多个GPU上训练它们。
注:尽管我试图深入探讨数学和几乎所有算法和技术背后的机制,但本文并没有明确地解释机器/深度学习或股票市场是如何运作的。其目的是展示我们如何使用不同的技术和算法来准确预测股票价格的变动,并给出每一步使用每种技术的原因和有用性背后的理论基础。
1.简介
准确预测股票市场是一项复杂的任务,因为有数百万种情况会影响它。因此,我们需要能够尽可能多地捕获这些前置条件。我们还需要做出几个重要的假设:1)市场不是100%随机,2)历史重复,3)市场遵循人们的理性行为,4)市场是“ 完美的 ”。
我们将尝试预测高盛(NYSE: GS)的价格走势。为此,我们将使用2010年1月1日至2018年12月31日的每日收盘价(七年数据用于训练,两年数据用于验证)。我们将交替使用“高盛”和“GS”这两个术语。
2.数据
我们需要尽可能多地合并信息(从不同方面和角度描绘股票)。我们将使用每日数据,1585天来训练各种算法(我们有70%的数据)并预测接下来的680天(测试数据),然后我们将把预测结果与测试数据进行比较。每种类型的数据(我们将其称为特征)将在后面的章节中进行更详细的解释,但是,作为一个高层次的概述,我们将使用的特征是:
接下来,有这么多特征,我们需要执行几个重要步骤:
作为数据准备的最后一步,我们还将使用主成分分析(PCA)创建Eigen投资组合,以减少自动编码器创建的特征的维数。
from utils import *
import time
import numpy as np
from mxnet import nd, autograd, gluon
from mxnet.gluon import nn, rnn
import mxnet as mx
import datetime
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
from sklearn.decomposition import PCA
import math
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import StandardScaler
import xgboost as xgb
from sklearn.metrics import accuracy_score
import warnings
warnings.filterwarnings("ignore")
context = mx.cpu(); model_ctx=mx.cpu()
mx.random.seed(1719)
def parser(x):
return datetime.datetime.strptime(x,'%Y-%m-%d')
dataset_ex_df = pd.read_csv('data/panel_data_close.csv', header=0, parse_dates=[0], date_parser=parser)
dataset_ex_df[['Date', 'GS']].head(3)
print('There are {} number of days in the dataset.'.format(dataset_ex_df.shape[0]))
output >>> There are 2265 number of days in the dataset.
让我们想象一下过去九年的股票。垂直虚线表示训练和测试数据之间的分离。
plt.figure(figsize=(14, 5), dpi=100)
plt.plot(dataset_ex_df['Date'], dataset_ex_df['GS'], label='Goldman Sachs stock')
plt.vlines
(datetime.date(2016,4, 20), 0, 270, linestyles='--', colors='gray', label='Train/Test data cut-off')
plt.xlabel('Date')
plt.ylabel('USD')
plt.title('Figure 2: Goldman Sachs stock price')
plt.legend()
plt.show()
num_training_days = int(dataset_ex_df.shape[0]*.7)
print('Number of training days: {}. Number of test days: {}.'.format(num_training_days, \
dataset_ex_df.shape[0]-num_training_days))
Number of training days: 1585. Number of test days: 680.
2.1、相关资产
如前所述,我们将使用其他资产作为特征,而不仅仅是GS。
那么,还有哪些资产会影响高盛的股价走势呢?对公司、业务线、竞争环境、依赖关系、供应商和客户类型等的良好理解对于选择正确的相关资产集非常重要:
总的来说,我们在数据集中有72个其他资产 - 每个资产的每日价格。
2.2、技术指标
我们已经讨论了什么是技术指标以及为什么使用它们,现在让我们直接跳到Python代码。我们将只为GS创建技术指标。
def get_technical_indicators(dataset):
# Create 7 and 21 days Moving Average
dataset['ma7'] = dataset['price'].rolling(window=7).mean()
dataset['ma21'] = dataset['price'].rolling(window=21).mean()
# Create MACD
dataset['26ema'] = pd.ewma(dataset['price'], span=26)
dataset['12ema'] = pd.ewma(dataset['price'], span=12)
dataset['MACD'] = (dataset['12ema']-dataset['26ema'])
# Create Bollinger Bands
dataset['20sd'] = pd.stats.moments.rolling_std(dataset['price'],20)
dataset['upper_band'] = dataset['ma21'] + (dataset['20sd']*2)
dataset['lower_band'] = dataset['ma21'] - (dataset['20sd']*2)
# Create Exponential moving average
dataset['ema'] = dataset['price'].ewm(com=0.5).mean()
# Create Momentum
dataset['momentum'] = dataset['price']-1
return dataset
dataset_TI_df = get_technical_indicators(dataset_ex_df[['GS']])
dataset_TI_df.head()
所以我们有每个交易日的技术指标(包括MACD、Bollinger bands等)。我们总共有12项技术指标。
让我们想象一下这些指标的最后400天。
def plot_technical_indicators(dataset, last_days):
plt.figure(figsize=(16, 10), dpi=100)
shape_0 = dataset.shape[0]
xmacd_ = shape_0-last_days
dataset = dataset.iloc[-last_days:, :]
x_ = range(3, dataset.shape[0])
x_ =list(dataset.index)
# Plot first subplot
plt.subplot(2, 1, 1)
plt.plot(dataset['ma7'],label='MA 7', color='g',linestyle='--')
plt.plot(dataset['price'],label='Closing Price', color='b')
plt.plot(dataset['ma21'],label='MA 21', color='r',linestyle='--')
plt.plot(dataset['upper_band'],label='Upper Band', color='c')
plt.plot(dataset['lower_band'],label='Lower Band', color='c')
plt.fill_between(x_, dataset['lower_band'], dataset['upper_band'], alpha=0.35)
plt.title('Technical indicators for Goldman Sachs - last {} days.'.format(last_days))
plt.ylabel('USD')
plt.legend()
# Plot second subplot
plt.subplot(2, 1, 2)
plt.title('MACD')
plt.plot(dataset['MACD'],label='MACD', linestyle='-.')
plt.hlines
(15, xmacd_, shape_0, colors='g', linestyles='--')
plt.hlines(-15, xmacd_, shape_0, colors='g', linestyles='--')
plt.plot(dataset['log_momentum'],label='Momentum', color='b',linestyle='-')
plt.legend()
plt.show()
plot_technical_indicators(dataset_TI_df, 400)
2.3、基本面分析
对于基本面分析,我们将对所有关于GS的每日新闻进行情感分析。最后使用sigmoid,结果将在0和1之间。分数越接近0 - 新闻越负面(接近1表示正面情感)。对于每一天,我们将创建平均每日得分(作为0到1之间的数字)并将其添加为特征。
2.3.1、 BERT
为了将新闻分类为正面或负面(或中性),我们将使用BERT,这是一种预训练的语言表示。
已经在MXNet / Gluon中提供预训练的BERT模型。我们只需要实例化它们并添加两个(任意数量)Dense层,softmax - 得分从0到1。
# just import bert
import bert
2.4、傅立叶变换
用于趋势分析
傅立叶变换采用函数并创建一系列正弦波(具有不同的幅度和帧)。组合时,这些正弦波接近原始函数。从数学上讲,变换看起来像这样:
我们将使用傅里叶变换来提取GS股票的整体和局部趋势,并对其进行降噪。我们来看看它是如何工作的。
data_FT = dataset_ex_df[['Date', 'GS']]
close_fft = np.fft.fft(np.asarray(data_FT['GS'].tolist()))
fft_df = pd.DataFrame({'fft':close_fft})
fft_df['absolute'] = fft_df['fft'].apply(lambda x: np.abs(x))
fft_df['angle'] = fft_df['fft'].apply(lambda x: np.angle(x))
plt.figure(figsize=(14, 7), dpi=100)
fft_list = np.asarray(fft_df['fft'].tolist())
for num_ in [3, 6, 9, 100]:
fft_list_m10= np.copy(fft_list); fft_list_m10[num_:-num_]=0
plt.plot(np.fft.ifft(fft_list_m10), label='Fourier transform with {} components'.format(num_))
plt.plot(data_FT['GS'], label='Real')
plt.xlabel('Days')
plt.ylabel('USD')
plt.title('Figure 3: Goldman Sachs (close) stock prices & Fourier transforms')
plt.legend()
plt.show()
正如您在图3中看到的,我们使用傅里叶变换的成分越多,逼近函数越接近实际股票价格(100个成分变换几乎与原始函数相同 - 红色和紫色线几乎重叠)。我们使用傅立叶变换来提取长期和短期趋势,因此我们将使用具有3,6和9个成分的变换。您可以推断出具有3个成分的转换是长期趋势。
用于去噪数据的另一种技术是调用小波。小波和傅里叶变换给出了类似的结果,因此我们只使用傅里叶变换。
from collections import deque
items = deque(np.asarray(fft_df['absolute'].tolist()))
items.rotate(int(np.floor(len(fft_df)/2)))
plt.figure(figsize=(10, 7), dpi=80)
plt.stem(items)
plt.title('Figure 4: Components of Fourier transforms')
plt.show()
2.5、ARIMA作为一项特征值
ARIMA是一种预测时间序列数据的技术。我们将展示如何使用它,虽然ARIMA不能作为我们的最终预测,但我们将使用它作为一种技术来稍微降低噪声,并(可能)提取一些新的模式或特征。
from statsmodels.tsa.arima_model import ARIMA
from pandas import DataFrame
from pandas import datetime
series = data_FT['GS']
model = ARIMA(series, order=(5, 1, 0))
model_fit = model.fit(disp=0)
print(model_fit.summary())
from pandas.tools.plotting import autocorrelation_plot
autocorrelation_plot(series)
plt.figure(figsize=(10, 7), dpi=80)
plt.show()
from pandas import read_csv
from pandas import datetime
from statsmodels.tsa.arima_model import ARIMA
from sklearn.metrics import mean_squared_error
X = series.values
size = int(len(X) * 0.66)
train, test = X[0:size], X[size:len(X)]
history = [x for x in train]
predictions = list()
for t in range(len(test)):
model = ARIMA(history, order=(5,1,0))
model_fit = model.fit(disp=0)
output = model_fit.forecast()
yhat = output[0]
predictions.append(yhat)
obs = test[t]
history.append(obs)
error = mean_squared_error(test, predictions)
print('Test MSE: %.3f' % error)
Test MSE: 10.151
# Plot the predicted (from ARIMA) and real prices
plt.figure(figsize=(12, 6), dpi=100)
plt.plot(test, label='Real')
plt.plot(predictions, color='red', label='Predicted')
plt.xlabel('Days')
plt.ylabel('USD')
plt.title('Figure 5: ARIMA model on GS stock')
plt.legend()
plt.show()
从图5中可以看出,ARIMA给出了一个非常接近实际股价的结果。我们将通过ARIMA使用预测价格作为LSTM的输入特征,因为正如我们前面提到的,我们希望尽可能多地捕获关于高盛的特征和模式。我们测试MSE(均方误差)为10.151,这本身并不是一个坏结果(考虑到我们有很多测试数据),但是我们仍然只将其作为LSTM中的一个特征。
2.6、统计检查
确保数据具有良好的质量对于机器学习模型非常重要。为了确保我们的数据拟合,我们将执行几个简单的检查,以确保我们实现和观察到的结果是真实的,而不是因为底层数据分布存在基本错误而受到损害。
2.6.1、异方差性,多重共线性,序列相关性
我们不会在这里进入代码,因为它很简单,我们的重点更多地放在深度学习部分,但数据是定性的。
2.7、特征工程
print('Total dataset has {} samples, and {} features.'.format(dataset_total_df.shape[0], \
dataset_total_df.shape[1]))
Total dataset has 2265 samples, and 112 features.
因此,在添加了所有类型的数据(相关资产、技术指标、基础分析、傅立叶和Arima)之后,我们在这2,265天中总共有112个特征(如前所述,训练数据只有1,585天)。
我们还将从自动编码器生成更多特征。
2.7.1、XGBoost的重要性
有这么多的特征,我们必须要考虑它们是否真的代表了走势。例如,我们在机器学习数据集中包含了以美元计价的LIBOR利率
,因为我们认为LIBOR的变化可能表明经济的变化,而经济的变化又可能表明GS的股票行为的变化。但我们需要测试。测试特征重要性的方法有很多,但是我们将使用XGBoost,因为它在分类和回归问题中都给出了最好的结果之一。
由于特征数据集非常大,出于演示目的,我们将仅使用技术指标。在真实特征重要性测试期间,所有选定的特征都证明有些重要,因此我们在训练GAN时不会排除任何内容。
def get_feature_importance_data(data_income):
data = data_income.copy()
y = data['price']
X = data.iloc[:, 1:]
train_samples = int(X.shape[0] * 0.65)
X_train = X.iloc[:train_samples]
X_test = X.iloc[train_samples:]
y_train = y.iloc[:train_samples]
y_test = y.iloc[train_samples:]
return (X_train, y_train), (X_test, y_test)
# Get training and test data
(X_train_FI, y_train_FI), (X_test_FI, y_test_FI) = get_feature_importance_data(dataset_TI_df)
regressor = xgb.XGBRegressor(gamma=0.0,n_estimators=150,base_score=0.7,colsample_bytree=1,learning_rate=0.05)
xgbModel = regressor.fit(X_train_FI,y_train_FI, \
eval_set = [(X_train_FI, y_train_FI), (X_test_FI, y_test_FI)], \
verbose=False)
eval_result = regressor.evals_result()
training_rounds = range(len(eval_result['validation_0']['rmse']))
让我们绘制训练和验证误差的曲线图,以便观察训练和检查过拟合(欠拟合)。
plt.scatter(x=training_rounds,y=eval_result['validation_0']['rmse'],label='Training Error')
plt.scatter
(x=training_rounds,y=eval_result['validation_1']['rmse'],label='Validation Error')
plt.xlabel('Iterations')
plt.ylabel('RMSE')
plt.title('Training Vs Validation Error')
plt.legend()
plt.show()
fig = plt.figure(figsize=(8,8))
plt.xticks(rotation='vertical')
plt.bar([i for i in range(len(xgbModel.feature_importances_))], xgbModel.feature_importances_.tolist(), tick_label=X_test_FI.columns)
plt.title('Figure 6: Feature importance of the technical indicators.')
plt.show()
毫不奇怪,MA7,MACD和BB是其中的重要特征。
我遵循相同的逻辑来对整个数据集执行特征重要性 - 与仅少数几个特征相比,训练花费的时间更长,结果更难以阅读。
2.8、使用Stacked Autoencoders提取高级特征
在我们进入自动编码器之前,我们将探索激活函数。
2.8.1、激活功函数- GELU(高斯误差
)
最近提出了GELU - Gaussian Error Linear Unites - link。在论文中,作者展示了使用ReLU作为激活的使用GELU的神经网络优于网络的几个实例。gelu也用于BERT,我们用于新闻情绪分析的NLP方法。
我们将使用GELU作为自动编码器。
注:下面的单元格展示了GELU数学背后的逻辑。它不是作为激活函数的实际实现。我必须在MXNet中实现GELU。如果您按照代码将act_type='relu'更改为act_type='gelu',那么它将不起作用,除非您更改MXNet的实现。对整个项目发出pull请求,以访问GELU的MXNet实现。
def gelu(x):
return 0.5 * x * (1 + math.tanh(math.sqrt(2 / math.pi
) * (x + 0.044715 * math.pow(x, 3))))
def relu(x):
return max(x, 0)
def lrelu(x):
return max(0.01*x, x)
让我们来看看GELU、ReLU和LeakyReLU(最后一个主要用于GAN)。
plt.figure(figsize=(15, 5))
plt.subplots_adjust(left=None, bottom=None, right=None, top=None, wspace=.5, hspace=None)
ranges_ = (-10, 3, .25)
plt.subplot(1, 2, 1)
plt.plot([i for i in np.arange(*ranges_)], [relu(i) for i in np.arange(*ranges_)], label='ReLU', marker='.')
plt.plot([i for i in np.arange(*ranges_)], [gelu(i) for i in np.arange(*ranges_)], label='GELU')
plt.hlines(0, -10, 3, colors='gray', linestyles='--', label='0')
plt.title('Figure 7: GELU as an activation function for autoencoders')
plt.ylabel('f(x) for GELU and ReLU')
plt.xlabel('x')
plt.legend()
plt.subplot(1, 2, 2)
plt.plot([i for i in np.arange(*ranges_)], [lrelu(i) for i in np.arange(*ranges_)], label='Leaky ReLU')
plt.hlines(0, -10, 3, colors='gray', linestyles='--', label='0')
plt.ylabel('f(x) for Leaky ReLU')
plt.xlabel('x')
plt.title('Figure 8: LeakyReLU')
plt.legend()
plt.show()
好的,回到自动编码器,如下所示(图像只是原理图,它不代表实际的层数,units等)
注意:通常,在自动编码器中编码器的数量==解码器的数量。但是,我们希望提取更高级别的特征(而不是创建相同的输入),因此我们可以跳过解码器中的最后一层。我们实现了这一点,在训练期间创建了具有相同层数的编码器和解码器。
batch_size = 64
n_batches = VAE_data.shape[0]/batch_size
VAE_data = VAE_data.values
train_iter = mx.io.NDArrayIter(data={'data': VAE_data[:num_training_days,:-1]}, \
label={'label': VAE_data[:num_training_days, -1]}, batch_size = batch_size)
test_iter = mx.io.NDArrayIter(data={'data': VAE_data[num_training_days:,:-1]}, \
label={'label': VAE_data[num_training_days:,-1]}, batch_size = batch_size)
model_ctx = mx.cpu()
class VAE(gluon.HybridBlock):
def __init__(self, n_hidden=400, n_latent=2, n_layers=1, n_output=784, \
batch_size=100, act_type='relu', **kwargs):
self.soft_zero = 1e-10
self.n_latent = n_latent
self.batch_size = batch_size
self.output = None
self.mu = None
super(VAE, self).__init__(**kwargs)
with self.name_scope():
self.encoder = nn.HybridSequential(prefix='encoder')
for i in range(n_layers):
self.encoder.add(nn.Dense(n_hidden, activation=act_type))
self.encoder.add(nn.Dense(n_latent*2, activation=None))
self.decoder = nn.HybridSequential(prefix='decoder')
for i in range(n_layers):
self.decoder.add(nn.Dense(n_hidden, activation=act_type))
self.decoder.add(nn.Dense(n_output, activation='sigmoid'))
def hybrid_forward(self, F, x):
h = self.encoder(x)
#print(h)
mu_lv = F.split(h, axis=1, num_outputs=2)
mu = mu_lv[0]
lv = mu_lv[1]
self.mu = mu
eps = F.random_normal(loc=0, scale=1, shape=(self.batch_size, self.n_latent), ctx=model_ctx)
z = mu + F.exp(0.5*lv)*eps
y = self.decoder(z)
self.output = y
KL = 0.5*F.sum(1+lv-mu*mu-F.exp(lv),axis=1)
logloss = F.sum(x*F.log(y+self.soft_zero)+ (1-x)*F.log(1-y+self.soft_zero), axis=1)
loss = -logloss-KL
return loss
n_hidden=400 # neurons in each layer
n_latent=2
n_layers=3 # num of dense layers in encoder and decoder respectively
n_output=VAE_data.shape[1]-1
net = VAE(n_hidden=n_hidden, n_latent=n_latent, n_layers=n_layers, n_output=n_output, batch_size=batch_size, act_type='gelu')
net.collect_params().initialize(mx.init.Xavier(), ctx=mx.cpu())
net.hybridize
()
trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate': .01})
print(net)
所以我们在编码器和解码器中都有3层(每个层有400个神经元)。
n_epoch = 150
print_period = n_epoch // 10
start = time.time()
training_loss = []
validation_loss = []
for epoch in range(n_epoch):
epoch_loss = 0
epoch_val_loss = 0
train_iter.reset()
test_iter.reset()
n_batch_train = 0
for batch in train_iter:
n_batch_train +=1
data = batch.data[0].as_in_context(mx.cpu())
with autograd.record():
loss = net(data)
loss.backward()
trainer.step(data.shape[0])
epoch_loss += nd.mean(loss).asscalar()
n_batch_val = 0
for batch in test_iter:
n_batch_val +=1
data = batch.data[0].as_in_context(mx.cpu())
loss = net(data)
epoch_val_loss += nd.mean(loss).asscalar()
epoch_loss /= n_batch_train
epoch_val_loss /= n_batch_val
training_loss.append(epoch_loss)
validation_loss.append(epoch_val_loss)
"""if epoch % max(print_period, 1) == 0:
print('Epoch {}, Training loss {:.2f}, Validation loss {:.2f}'.\
format(epoch, epoch_loss, epoch_val_loss))"""
end = time.time()
print('Training completed in {} seconds.'.format(int(end-start)))
Training completed in 62 seconds.
dataset_total_df['Date'] = dataset_ex_df['Date']
vae_added_df = mx.nd.array(dataset_total_df.iloc[:, :-1].values)
print('The shape of the newly created (from the autoencoder) features is {}.'.format(vae_added_df.shape))
The shape of the newly created (from the autoencoder) features is (2265, 112).
我们从自动编码器中创建了112个更多特征。由于我们只想拥有高级特征(整体模式),我们将使用主成分分析(PCA)在新创建的112个特征上创建特征投资组合。这将减少数据的维度(列数)。Eigen组合的描述能力将与原始的112个特征相同。
注意再一次,这纯粹是实验性的。我并非100%确定描述的逻辑将成立。作为人工智能和深度学习的其他一切,这是艺术和需要实验。
2.8.2、主成分分析的特征组合
# We want the PCA to create the new components to explain 80% of the variance
pca = PCA(n_components=.8)
x_pca = StandardScaler().fit_transform(vae_added_df)
principalComponents = pca.fit_transform(x_pca)
principalComponents.n_components_
84
所以,为了解释80%的方差我们需要84个(112个)特征。这仍然是一个很大的问题。因此,目前我们不包括自动编码器创建的特征。我将致力于创建autoencoder架构,在该架构中,我们从中间层(而不是最后一层)获得输出,并使用30个神经元将其连接到另一个Dense 层。因此,我们将1)只提取更高级别的特征,2)提供更少的列数。
3.生成性对抗网络(GAN)
GAN的体系结构
GAN如何运作?
如前所述,本文的目的不是详细解释深度学习背后的数学,而是展示其应用。当然,在我看来,彻底和非常坚实的理解从基础到最小的细节,是极其必要的。因此,我们将尝试平衡并给出一个关于GAN如何工作的高级概述,以便读者充分理解使用GAN预测股价走势背后的原理。
GAN网络由两个模型组成 - Generator(G)和Discriminator (D)。训练GAN的步骤如下:
当组合在一起时,D和G作为一种playing minmax游戏(Generator试图欺骗Discriminator ,使其增加假例子的概率,即最小化z〜pz(z)[log(1-D(G) z)))]。Discriminator 想要通过最大化x~pr(x)[logD(x)]来分离来自发生器D(G(z))的数据。但是,具有分离的损失函数,它是不清楚两者如何汇合在一起(这就是为什么我们使用普通GAN的一些进步,例如Wasserstein GAN)。总的来说,组合损失函数看起来像:
3.1、为什么GAN用于股市预测
生成对抗网络(GAN)最近主要用于创建逼真的图像,绘画和视频剪辑。在我们的案例中,没有多少GAN用于预测时间序列数据。然而,主要想法应该是相同的 - 我们希望预测未来的股票变动。在未来,GS股票的模式和行为应该或多或少相同(除非它开始以完全不同的方式运作,或者经济急剧变化)。因此,我们希望“生成”未来的数据,这些数据将具有与我们已有的相似(当然不完全相同)的分布 - 历史交易数据。所以,从理论上讲,它应该有效。
在我们的例子中,我们将使用LSTM作为时间序列生成器,并使用CNN作为Discriminator。
3.2、Metropolis-Hastings GAN和Wasserstein GAN
I. Metropolis-Hastings GAN
Uber的工程团队最近对传统GAN进行了改进,称为Metropolis-Hastings GAN(MHGAN)。优步的方法背后的想法(正如他们所述)与谷歌和加州大学伯克利分校创建的另一种方法有点类似,称为DRS。基本上,当我们训练GAN时,我们使用Discriminator(D)的唯一目的是更好地训练Generator(G)。通常,在训练GAN之后我们不再使用D. 然而,MHGAN和DRS尝试使用D来选择由G生成的接近实际数据分布的样本(MHGAN之间的微小差异是使用马尔可夫链蒙特卡罗(MCMC)进行采样)。
MHGAN采用从G生成的K个样本(从独立的噪声输入到下图中的G-z0到zK)。然后它依次运行K个输出(x'0到x'K)并遵循接受规则(从Discriminator创建)决定是接受当前样本还是保留最后接受的样本。最后保留的输出是被认为是G的实际输出的输出。
注意:MHGAN最初由优步在pytorch中实现。我只把它转移到MXNet / Gluon。
图10:MHGAN的视觉表示
II。Wasserstein GAN
训练GAN非常困难。模型可能永远不会收敛,模式崩溃很容易发生。我们将使用名为Wasserstein GAN - WGAN的GAN修改。
最值得注意的要点是:
3.4、Generator - One layer RNN
3.4.1、LSTM或GRU
如前所述,Generator是LSTM网络的一种循环神经网络(RNN)。RNN用于时间序列数据,因为它们跟踪所有先前的数据点,并且可以捕获随时间发展的模式。由于它们的性质,RNN很多时候都会受到梯度消失的影响 - 也就是说,在训练期间权重变化的变化变得如此之小,以至于它们不会改变,使得网络无法收敛到最小的损失(相反的问题也可以有时会观察到 - 当梯度变得太大时。这称为梯度爆炸,但解决方法很简单。两个方法解决了这个问题 - 门控循环单元(GRU)和长短期记忆(LSTM)。两者之间最大的区别是:1)GRU有2个门(update 和reset),LSTM有4个(update, input, forget, 和output),2)LSTM保持内部存储器状态,而GRU没有, 3)LSTM在输出门之前应用非线性(sigmoid),GRU不应用。
在大多数情况下,LSTM和GRU在准确性方面给出了类似的结果,但GRU的计算密集程度要低得多,因为GRU的可训练参数更少。然而,LSTM使用得更多。
严格地说,LSTM cell (gates)背后的数学是:
LSTM cell背后的数学
其中⊙是一个逐元素的乘法运算符,并且,对于所有x = [x1,x2,...,xk]⊤∈R^ k,激活函数:
3.4.2LSTM架构
LSTM架构非常简单 - LSTM一层有112个输入单元(因为我们在数据集中有112个特征)和500个隐藏单元,Dense层有1个输出 - 每天的价格。初始化器是Xavier,我们将使用L1损失(这是L1正则化的平均绝对误差损失)。
注意 - 在Python代码中,您可以看到我们使用Adam(使用learning rate.01)作为优化器。
gan_num_features = dataset_total_df.shape[1]
sequence_length = 17
class RNNModel(gluon.Block):
def __init__(self, num_embed, num_hidden, num_layers, bidirectional=False, \
sequence_length=sequence_length, **kwargs):
super(RNNModel, self).__init__(**kwargs)
self.num_hidden = num_hidden
with self.name_scope():
self.rnn = rnn.LSTM(num_hidden, num_layers, input_size=num_embed, \
bidirectional=bidirectional, layout='TNC')
self.decoder = nn.Dense(1, in_units=num_hidden)
def forward(self, inputs, hidden):
output, hidden = self.rnn(inputs, hidden)
decoded = self.decoder(output.reshape((-1, self.num_hidden)))
return decoded, hidden
def begin_state(self, *args, **kwargs):
return self.rnn.begin_state(*args, **kwargs)
lstm_model = RNNModel(num_embed=gan_num_features, num_hidden=500, num_layers=1)
lstm_model.collect_params().initialize(mx.init.Xavier(), ctx=mx.cpu())
trainer = gluon.Trainer(lstm_model.collect_params(), 'adam', {'learning_rate': .01})
loss = gluon.loss.L1Loss()
我们将在LSTM层中使用500个神经元并使用Xavier初始化。为了正则化,我们将使用L1。让我们来看看LSTMMXNet打印的内容。
print(lstm_model)
正如我们所看到的,LSTM的输入是112个特征(dataset_total_df.shape[1])然后进入LSTM层中的500个神经元,然后转换为单个输出 - 股票价格值。
LSTM背后的逻辑是:我们取17天(sequence_length)的数据(同样,这些数据是GS股票每天的股价+当天的所有其他特性——相关资产、情绪等),并尝试预测第18天。
3.4.3、学习率调度程序
最重要的超参数之一是学习率。在训练神经网络时,为几乎所有优化器(例如SGD,Adam或RMSProp)设置学习速率至关重要,因为它既能控制收敛速度,又能控制网络的最终性能。最简单的学习率策略之一是在整个训练过程中具有固定的学习率。选择较小的学习速率可以使优化器找到好的解决方案,但这是以限制初始收敛速度为代价的。随着时间的推移改变学习率可以克服这种权衡。
让我们绘制我们将在每个epoch使用的学习率。
class TriangularSchedule():
def __init__(self, min_lr, max_lr, cycle_length, inc_fraction=0.5):
self.min_lr = min_lr
self.max_lr = max_lr
self.cycle_length = cycle_length
self.inc_fraction = inc_fraction
def __call__(self, iteration):
if iteration <= self.cycle_length*self.inc_fraction:
unit_cycle = iteration * 1 / (self.cycle_length * self.inc_fraction)
elif iteration <= self.cycle_length:
unit_cycle = (self.cycle_length - iteration) * 1 / (self.cycle_length * (1 - self.inc_fraction))
else:
unit_cycle = 0
adjusted_cycle = (unit_cycle * (self.max_lr - self.min_lr)) + self.min_lr
return adjusted_cycle
class CyclicalSchedule():
def __init__(self, schedule_class, cycle_length, cycle_length_decay=1, cycle_magnitude_decay=1, **kwargs):
self.schedule_class = schedule_class
self.length = cycle_length
self.length_decay = cycle_length_decay
self.magnitude_decay = cycle_magnitude_decay
self.kwargs = kwargs
def __call__(self, iteration):
cycle_idx = 0
cycle_length = self.length
idx = self.length
while idx <= iteration:
cycle_length = math.ceil(cycle_length * self.length_decay)
cycle_idx += 1
idx += cycle_length
cycle_offset = iteration - idx + cycle_length
schedule = self.schedule_class(cycle_length=cycle_length, **self.kwargs)
return schedule(cycle_offset) * self.magnitude_decay**cycle_idx
schedule = CyclicalSchedule(TriangularSchedule, min_lr=0.5, max_lr=2, cycle_length=500)
iterations=1500
plt.plot([i+1 for i in range(iterations)],[schedule(i) for i in range(iterations)])
plt.title('Learning rate for each epoch')
plt.xlabel("Epoch")
plt.ylabel("Learning Rate")
plt.show()
3.4.4、如何防止过度拟合和偏差 - 方差权衡
有很多特征和神经网络,我们需要确保我们避免过拟合,并注意总损失。
我们使用几种技术来防止过度拟合(不仅在LSTM中,并且在CNN和自动编码器中):
构建复杂神经网络时的另一个重要考虑因素是偏差 - 方差权衡。基本上,我们在训练网络时得到的误差是偏差,方差和不可减少误差的函数 - σ(由噪声和随机性引起的误差)。权衡的最简单公式是:Error=bias^2+variance+σ.
3.5、Discriminator
为什么CNN作为Discriminator ?
我们通常将CNN用于与图像相关的工作(分类,上下文提取等)。例如,在狗的图像中,第一个卷积层将检测边缘,第二个将开始检测圆圈,第三个将检测到鼻子。在我们的例子中,数据点形成小趋势,小趋势形成更大,趋势形成模式。CNN检测特征的能力可用于提取有关GS股票价格变动模式的信息。
使用CNN的另一个原因是CNN在空间数据上运行良好 - 这意味着彼此更接近的数据点彼此之间的相关性更高,而不是数据点。对于时间序列数据,这应该适用。在我们的例子中,每个数据点(对于每个特征)是连续的每一天。很自然地假设彼此距离越近,彼此之间的相关性就越大。需要考虑的一件事(虽然没有涉及这项工作)是季节性以及它如何改变(如果有的话)CNN的工作。
3.5.1。CNN架构
建议的CNN模型的架构
GAN中CNN的Python代码是这样的:
num_fc = 512
# ... other parts of the GAN
cnn_net = gluon.nn.Sequential()
with net.name_scope():
# Add the 1D Convolutional layers
cnn_net.add(gluon.nn.Conv1D(32, kernel_size=5, strides=2))
cnn_net.add(nn.LeakyReLU(0.01))
cnn_net.add(gluon.nn.Conv1D(64, kernel_size=5, strides=2))
cnn_net.add(nn.LeakyReLU(0.01))
cnn_net.add(nn.BatchNorm())
cnn_net.add(gluon.nn.Conv1D(128, kernel_size=5, strides=2))
cnn_net.add(nn.LeakyReLU(0.01))
cnn_net.add(nn.BatchNorm())
# Add the two Fully Connected layers
cnn_net.add(nn.Dense(220, use_bias=False), nn.BatchNorm(), nn.LeakyReLU(0.01))
cnn_net.add(nn.Dense(220, use_bias=False), nn.Activation(activation='relu'))
cnn_net.add(nn.Dense(1))
# ... other parts of the GAN
让我们打印CNN。
print(cnn_net)
3.6、超参数
我们将跟踪和优化的超参数是:
我们将训练超过200个epochs。
4.超参数优化
在200个epochs的GAN训练之后,它将记录MAE(这是LSTM中的误差函数,GG)并将其作为奖励值传递给强化学习,该学习决定是否改变继续训练的超参数。
如果RL决定它将更新超参数,它将调用贝叶斯优化(下面讨论)库,它将提供下一个最佳预期的超级参数集
4.1、超参数优化的强化学习
为什么我们在超参数优化中使用强化学习?股市一直在变化。即使我们设法训练我们的GAN和LSTM以创建非常准确的结果,结果可能仅在一段时间内有效。意思是,我们需要不断优化整个过程。为了优化流程,我们可以:
4.1.1、强化学习理论
在不解释RL的基础知识的情况下,我们将详细介绍我们在此实现的具体方法。我们将使用model-free RL算法,原因很明显我们不了解整个环境,因此没有明确的环境工作模型 - 如果我们不需要预测股票价格变动 - 它们只会按照该模型。我们将使用model-free RL的两个细分 - 策略优化和Q学习。
构建RL算法的一个关键方面是准确设置奖励。它必须捕获环境的所有方面以及代理与环境的交互。我们将奖励R定义为:
Reward=2∗lossG+lossD+accuracyG,
其中lossG,accuracyG和lossD分别是Generator的损失和准确性,以及Discriminator的损失。环境是GAN和LSTM训练的结果。不同代理可以采取的动作是如何更改GAN的D和G网络的超参数。
4.1.1.1、Rainbow
Rainbow(link)是一种基于Q学习的非策略深度强化学习算法,它将七种算法结合在一起:
4.1.1.2、PPO
近端策略优化(PPO)是一种无策略优化模型的强化学习。实现其他算法要简单得多,效果非常好。
我们为什么要使用PPO?PPO的一个优点是它直接学习策略,而不是间接地通过值(Q学习使用Q值来学习策略的方式)。它可以在连续动作空间中很好地工作,这在我们的使用案例中是合适的,并且可以(通过平均值和标准偏差)学习分布概率(如果将softmax作为输出添加)。
政策梯度方法的问题在于它们对步长选择极其敏感 - 如果它很小,则进度需要太长时间(很可能主要是由于需要二阶导数矩阵); 如果它很大,会有很多噪音会显著降低性能。由于政策的变化(以及奖励和观察变化的分布),输入数据是非平稳的。与监督学习相比,选择不当的步骤可能会更具破坏性,因为它会影响下次访问的整个分布。PPO可以解决这些问题。更重要的是,与其他一些方法相比,PPO:
注意:出于练习的目的,我们不会过多地研究和优化RL方法,PPO和其他方法。相反,我们将采用可用的方法,并尝试适应我们的GAN,LSTM和CNN模型的超参数优化过程。我们将重用和自定义的Python代码由OpenAI创建,可在此处获得(https://github.com/openai/baselines)。
4.1.2、进一步加强学习的工作
进一步探索强化学习的一些想法:
4.2、贝叶斯优化
我们将使用贝叶斯优化来代替网格搜索,这可能需要花费大量时间来找到超参数的最佳组合。我们将使用的库已经实现 - 链接(https://github.com/fmfn/BayesianOptimization)。
4.2.1、高斯过程
# Initialize the optimizer
from bayes_opt import BayesianOptimization
from bayes_opt import UtilityFunction
utility = UtilityFunction(kind="ucb", kappa=2.5, xi=0.0)
4.2.2、高斯过程结果
5.结果
最后,当在过程的不同阶段之后将看不见的(测试)数据用作输入时,我们将比较LSTM的输出。
from utils import plot_prediction
plot_prediction('Predicted and Real price - after first epoch.')
2.绘制50个epochs后的图像。
plot_prediction('Predicted and Real price - after first 50 epochs.')
plot_prediction('Predicted and Real price - after first 200 epochs.')
RL运行en episodes(我们将eposide定义为200个epochs上的一个完整GAN训练)。
plot_prediction('Final result.')