(本文基本是对Jansen 的《Machine Learning for Algorithmic Trading》第二版的第21章进行翻译、学习和梳理,此项为课程作业,以此形式进行记录和自我学习)
利用GAN生成合成时间序列数据,所面临的挑战要大于利用GAN生成合成图片。除了要学习每个给定点分布(如某个时间戳上股票价格的分布),GAN还需要学习时间动力学——这种动力学是两个序列相关性背后的驱动力。
Yoon等人在2019年12月介绍了一种新颖的时间序列生成对抗网络(TimeGAN)框架,这种框架同时结合了监督学习和无监督学习,以解释时间上的相关性。这个模型在学习时间序列的内在空间(a time-series embedding space)时,同时对监督学习和对抗训练这两个目标进行优化。这能够使模型能够在对历史数据采样的同时,也能观测到其中的动力学(dynamics observed)。模型的作者将许多时间序列(如股票价格)用于测试模型,结果发现合成数据的质量要好于许多其他的数据。
本文在于展示TimeGAN的工作方式以及关键的实施步骤。
在Yoon等人的研究发表之前,将GAN作为生成序列数据的方法的研究,都没有对“时间上的动力学”(temporal dynamics,即序列的自相关性)给予足够的重视,而一般的作为解决预测问题的监督学习算法,则天然具有确定性(inherently deterministic)。因此对两种框架取其精华弃其糟粕,则是Yoon等人创新的地方。在文章中,Yoon等人结合了非监督学习的范式灵活的特点和监督学习对于训练过程控制力强的性质,让TimeGAN模型能够把“时间上的动力学”考虑进模型中。
具体而言,序列相关性作为时间序列数据的一种内在性质,对以生成时间序列为目的生成模型提出了一个很大的挑战。一个好的生成模型不仅应该学习到每个时间戳上的特征分布(distributiions of features within each time point),而且还应该学习到不同时点之间变量间的潜在复杂关系(potentially complex dynamics of those variables across time)。用数学的语言表达,就是说如果现在需要对一个序列数据 X 1 : T = ( X 1 , ⋯ , X T ) \textbf{X}_{1:T} = (\textbf{X}_1,\cdots,\textbf{X}_T) X1:T=(X1,⋯,XT)进行建模,那么我们希望模型能够准确地学习到 p ( X t ∣ X 1 : t − 1 ) p(\textbf{X}_t|\textbf{X}_{1:t-1}) p(Xt∣X1:t−1)这一条件分布,而且随着时间的流逝,模型也应该保持优秀的学习能力。
一方面,有大量的工作专注于使用自回归模型去拟合“时间上的动力学”。虽说自回归模型明确地将序列的分布划入了条件分布的乘积( ∏ t p ( X t ∣ X 1 : T ) \prod_tp(X_t|X_{1:T}) ∏tp(Xt∣X1:T))中,而且在解决预测问题上,自相关模型也算有用。但是,归根到底,自相关模型是天然确定性(fundamentally deterministic)的,而且自相关模型并不具备生成性,因为自相关模型的输入是有条件的,它的输入必须是有意义的真实数据。而一个好的生成模型,必须要能够做到自己动手丰衣足食,即使输入是随机噪声,生成模型也能够生成高质量的合成数据。
另一方面,许多研究致力于将GAN的框架直接用于序列数据的生成,这些研究的想法多是以RGANs作为生成器和判别器。然而,这些GAN的对抗训练,仅仅在于直接对 p ( X 1 : T ) p(\textbf{X}_{1:T}) p(X1:T)建模,而不考虑“时间上的动力学”。总之,RGANs的做法是每个时点的特征分布建模,相应的,其loss也仅仅是简单的将每个时点的loss相加,这种训练方法是不足以捕获序列相关性的。
在文章中,Yoon等人结合了上述两条技术路线的优点,并在此基础上提出了一个包含“时间上的动力学”的生成模型——TimeGAN。这个模型的特点:
在定性上,这一模型利用t-SNE和PCA分析,将生成数据对原始数据分布的模拟程度进行可视化,用以在定性上评价模型的好坏。在定量上,作者利用post-hoc(事后检验)对合成数据和真实数据进行区分以验证模型好坏。文章的最后,作者还用利用TSTR(train on synthetic,test on real)的框架,用以评价生成数据的质量。结论是,TimeGAN在生成时间序列数据这一领域达到了顶级水平。
TimeGAN的关键组成部分是生成器和内在网络,两者都直接关系到loss的最小化过程。而loss则用以衡量模型在学习动力学关系上达到了多“好”的程度。因此,模型会以促进生成器学习序列相关性为条件去学习潜在空间(latent space,也就是真实数据背后的驱动力)。
上图阐明了TimeGAN的两个网络及其所包含的四个部分
TimeGAN的训练是独特的,因为它是一种联合训练,即依靠三个不同的loss function对自编码器和对抗网络进行训练。Reconstruction Loss用于自编码器参数的优化,Unsupervised Loss用于对抗网络参数的优化,而Supervised Loss则是针对于生成器对“时间动力学”的学习。具体来说,自编码器的embedding network致力于创造出潜在空间,而对抗网络则在潜在空间里进行操作,最后Supervised Loss则要让合成数据的“时间上的动力学”尽可能地向真实数据的“时间上的动力学”靠拢。
自编码器实现的是特征空间和潜在空间之间的可逆映射,它的作用就是让对抗网络能够在更小维度的空间中学习“时间上的动力学”。
而TimeGAN中对抗网络不同于DCGANs的地方有两个
具体来说,三个loss的具体工作是
因此训练的进行要有三个阶段
构建和训练TimeGAN需要以下几个步骤:
Original code author: Jinsung Yoon
初始阶段
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler
import tensorflow as tf
from pathlib import Path #导入路径处理库
from tqdm import tqdm
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import GRU, Dense, RNN, GRUCell, Input
from tensorflow.keras.losses import BinaryCrossentropy, MeanSquaredError
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import TensorBoard
from tensorflow.keras.utils import plot_model
import matplotlib.pyplot as plt
import seaborn as sns
gpu_devices = tf.config.experimental.list_physical_devices('GPU')
if gpu_devices:
print('Using GPU')
tf.config.experimental.set_memory_growth(gpu_devices[0], True)
else:
print('Using CPU')
sns.set_style('white')
#试验路径
results_path = Path('time_gan')
if not results_path.exists():
results_path.mkdir()
experiment = 0
log_dir = results_path / f'experiment_{experiment:02}'
if not log_dir.exists():
log_dir.mkdir(parents=True)
hdf_store = results_path / 'TimeSeriesGAN.h5'
#数据准备
#参数设置
seq_len = 24
n_seq = 6
batch_size = 128
tickers = ['BA', 'CAT', 'DIS', 'GE', 'IBM', 'KO']
#此处数据来源于Quandl community,在2018年已经停用了,所以这里实际上使用的是历史数据
def select_data():
df = (pd.read_hdf('../data/assets.h5', 'quandl/wiki/prices')
.adj_close #经过调整的价格
.unstack('ticker')
.loc['2000':, tickers]
.dropna())
df.to_hdf(hdf_store, 'data/real')
select_data()
#画图
df = pd.read_hdf(hdf_store, 'data/real')
axes = df.div(df.iloc[0]).plot(subplots=True,
figsize=(14, 6),
layout=(3, 2),
title=tickers,
legend=False,
rot=0,
lw=1,
color='k')
for ax in axes.flatten():
ax.set_xlabel('')
plt.suptitle('Normalized Price Series')
plt.gcf().tight_layout()
sns.despine();
#将数据进行标准化处理
scaler = MinMaxScaler()
scaled_data = scaler.fit_transform(df).astype(np.float32)
#创建滚动窗口序列
data = []
for i in range(len(df) - seq_len):
data.append(scaled_data[i:i + seq_len])
n_windows = len(data)
#创建tf.data.Dataset
real_series = (tf.data.Dataset
.from_tensor_slices(data)
.shuffle(buffer_size=n_windows)
.batch(batch_size))
real_series_iter = iter(real_series.repeat())
#设置随机序列生成器
def make_random_data():
while True:
yield np.random.uniform(low=0, high=1, size=(seq_len, n_seq))
#我们使用 Python 生成器来提供一个 tf.data.Dataset,它会根据需要继续调用随机数生成器并生成所需的批量大小。
random_series = iter(tf.data.Dataset
.from_generator(make_random_data, output_types=tf.float32)
.batch(batch_size)
.repeat())
TimeGAN模型的组成部分
#网络参数
hidden_dim = 24
num_layers = 3
在这段代码里将其设计的非常简单,并且对四个组成部分都使用了一个非常相似的架构(看下方代码);但在实际的应用中,这些架构设计应当根据数据的实际情况进行定制修改。
def make_rnn(n_layers, hidden_units, output_units, name):
return Sequential([GRU(units=hidden_units,
return_sequences=True,
name=f'GRU_{i + 1}') for i in range(n_layers)] +
[Dense(units=output_units,
activation='sigmoid',
name='OUT')], name=name)
#自编码器:内在和复现网络(Embedder & Recovery)
embedder = make_rnn(n_layers=3,
hidden_units=hidden_dim,
output_units=hidden_dim,
name='Embedder')
recovery = make_rnn(n_layers=3,
hidden_units=hidden_dim,
output_units=n_seq,
name='Recovery')
#生成器和判别器(Generator & Discriminator)
generator = make_rnn(n_layers=3,
hidden_units=hidden_dim,
output_units=hidden_dim,
name='Generator')
discriminator = make_rnn(n_layers=3,
hidden_units=hidden_dim,
output_units=1,
name='Discriminator')
supervisor = make_rnn(n_layers=2,
hidden_units=hidden_dim,
output_units=hidden_dim,
name='Supervisor')
TimeGAN的训练
#设置
train_steps = 10000
gamma = 1
#通用损失函数
mse = MeanSquaredError()
bce = BinaryCrossentropy()
一、自编码器的训练
H = embedder(X)
X_tilde = recovery(H)
autoencoder = Model(inputs=X,
outputs=X_tilde,
name='Autoencoder')
#自编码器训练步骤
@tf.function
def train_autoencoder_init(x):
with tf.GradientTape() as tape:
x_tilde = autoencoder(x)
embedding_loss_t0 = mse(x, x_tilde)
e_loss_0 = 10 * tf.sqrt(embedding_loss_t0)
var_list = embedder.trainable_variables + recovery.trainable_variables
gradients = tape.gradient(e_loss_0, var_list)
autoencoder_optimizer.apply_gradients(zip(gradients, var_list))
return tf.sqrt(embedding_loss_t0)
#自编码器训练循环
for step in tqdm(range(train_steps)):
X_ = next(real_series_iter)
step_e_loss_t0 = train_autoencoder_init(X_)
with writer.as_default():
tf.summary.scalar('Loss Autoencoder Init', step_e_loss_t0, step=step)
二、监督训练
#训练步骤
@tf.function
def train_supervisor(x):
with tf.GradientTape() as tape:
h = embedder(x)
h_hat_supervised = supervisor(h)
g_loss_s = mse(h[:, 1:, :], h_hat_supervised[:, :-1, :])
var_list = supervisor.trainable_variables
gradients = tape.gradient(g_loss_s, var_list)
supervisor_optimizer.apply_gradients(zip(gradients, var_list))
return g_loss_s
#训练循环
for step in tqdm(range(train_steps)):
X_ = next(real_series_iter)
step_g_loss_s = train_supervisor(X_)
with writer.as_default():
tf.summary.scalar('Loss Generator Supervised Init', step_g_loss_s, step=step)
三、联合训练(Joint Training)
#对抗性架构--监督(supervised)
E_hat = generator(Z)
H_hat = supervisor(E_hat)
Y_fake = discriminator(H_hat)
adversarial_supervised = Model(inputs=Z,
outputs=Y_fake,
name='AdversarialNetSupervised')
#在潜在空间(latent space)的对抗性架构
Y_fake_e = discriminator(E_hat)
adversarial_emb = Model(inputs=Z,
outputs=Y_fake_e,
name='AdversarialNet')
#均值-方差损失
X_hat = recovery(H_hat)
synthetic_data = Model(inputs=Z,
outputs=X_hat,
name='SyntheticData')
#架构--实际的数据
Y_real = discriminator(H)
discriminator_model = Model(inputs=X,
outputs=Y_real,
name='DiscriminatorReal')
#生成器的训练步骤
@tf.function
def train_generator(x, z):
with tf.GradientTape() as tape:
y_fake = adversarial_supervised(z)
generator_loss_unsupervised = bce(y_true=tf.ones_like(y_fake),
y_pred=y_fake)
y_fake_e = adversarial_emb(z)
generator_loss_unsupervised_e = bce(y_true=tf.ones_like(y_fake_e),
y_pred=y_fake_e)
h = embedder(x)
h_hat_supervised = supervisor(h)
generator_loss_supervised = mse(h[:, 1:, :], h_hat_supervised[:, 1:, :])
x_hat = synthetic_data(z)
generator_moment_loss = get_generator_moment_loss(x, x_hat)
generator_loss = (generator_loss_unsupervised +
generator_loss_unsupervised_e +
100 * tf.sqrt(generator_loss_supervised) +
100 * generator_moment_loss)
var_list = generator.trainable_variables + supervisor.trainable_variables
gradients = tape.gradient(generator_loss, var_list)
generator_optimizer.apply_gradients(zip(gradients, var_list))
return generator_loss_unsupervised, generator_loss_supervised, generator_moment_loss
#内在网络(embedding)的训练步骤
@tf.function
def train_embedder(x):
with tf.GradientTape() as tape:
h = embedder(x)
h_hat_supervised = supervisor(h)
generator_loss_supervised = mse(h[:, 1:, :], h_hat_supervised[:, 1:, :])
x_tilde = autoencoder(x)
embedding_loss_t0 = mse(x, x_tilde)
e_loss = 10 * tf.sqrt(embedding_loss_t0) + 0.1 * generator_loss_supervised
var_list = embedder.trainable_variables + recovery.trainable_variables
gradients = tape.gradient(e_loss, var_list)
embedding_optimizer.apply_gradients(zip(gradients, var_list))
return tf.sqrt(embedding_loss_t0)
#判别器的训练步骤
@tf.function
def get_discriminator_loss(x, z):
y_real = discriminator_model(x)
discriminator_loss_real = bce(y_true=tf.ones_like(y_real),
y_pred=y_real)
y_fake = adversarial_supervised(z)
discriminator_loss_fake = bce(y_true=tf.zeros_like(y_fake),
y_pred=y_fake)
y_fake_e = adversarial_emb(z)
discriminator_loss_fake_e = bce(y_true=tf.zeros_like(y_fake_e),
y_pred=y_fake_e)
return (discriminator_loss_real +
discriminator_loss_fake +
gamma * discriminator_loss_fake_e)
@tf.function
def train_discriminator(x, z):
with tf.GradientTape() as tape:
discriminator_loss = get_discriminator_loss(x, z)
var_list = discriminator.trainable_variables
gradients = tape.gradient(discriminator_loss, var_list)
discriminator_optimizer.apply_gradients(zip(gradients, var_list))
return discriminator_loss
#训练循环
step_g_loss_u = step_g_loss_s = step_g_loss_v = step_e_loss_t0 = step_d_loss = 0
for step in range(train_steps):
# Train generator (twice as often as discriminator)
for kk in range(2):
X_ = next(real_series_iter)
Z_ = next(random_series)
# Train generator
step_g_loss_u, step_g_loss_s, step_g_loss_v = train_generator(X_, Z_)
# Train embedder
step_e_loss_t0 = train_embedder(X_)
X_ = next(real_series_iter)
Z_ = next(random_series)
step_d_loss = get_discriminator_loss(X_, Z_)
if step_d_loss > 0.15:
step_d_loss = train_discriminator(X_, Z_)
if step % 1000 == 0:
print(f'{step:6,.0f} | d_loss: {step_d_loss:6.4f} | g_loss_u: {step_g_loss_u:6.4f} | '
f'g_loss_s: {step_g_loss_s:6.4f} | g_loss_v: {step_g_loss_v:6.4f} | e_loss_t0: {step_e_loss_t0:6.4f}')
with writer.as_default():
tf.summary.scalar('G Loss S', step_g_loss_s, step=step)
tf.summary.scalar('G Loss U', step_g_loss_u, step=step)
tf.summary.scalar('G Loss V', step_g_loss_v, step=step)
tf.summary.scalar('E Loss T0', step_e_loss_t0, step=step)
tf.summary.scalar('D Loss', step_d_loss, step=step)
生成合成时间序列数据
generated_data = []
for i in range(int(n_windows / batch_size)):
Z_ = next(random_series)
d = synthetic_data(Z_)
generated_data.append(d)
#绘制样本序列
fig, axes = plt.subplots(nrows=3, ncols=2, figsize=(14, 7))
axes = axes.flatten()
index = list(range(1, 25))
synthetic = generated_data[np.random.randint(n_windows)]
idx = np.random.randint(len(df) - seq_len)
real = df.iloc[idx: idx + seq_len]
for j, ticker in enumerate(tickers):
(pd.DataFrame({'Real': real.iloc[:, j].values,
'Synthetic': synthetic[:, j]})
.plot(ax=axes[j],
title=ticker,
secondary_y='Synthetic', style=['-', '--'],
lw=1))
sns.despine()
fig.tight_layout()