时间序列数据通常出现在不同的领域,如经济、商业、工程和许多其他领域,并且可以有不同的应用。使用机器学习进行时间序列建模已经成为新的发展趋势。主要的任务目标包括异常检测和回归预测。递归神经网络(RNNs)和卷积神经网络(CNNs)在时间序列建模问题上取得了良好结果。
CNN应用于时间序列的代表即TCN,又被称为因果卷积。TCN与RNNs相比,具有如下的优势:
(1)并行性。与 RNN 中后继时间步长的预测必须等待之前时间步完成预测不同,卷积可以并行完成,因为每一层都使用相同的滤波器。因此,在训练和评估中,TCN 可以处理一整个较长的输入序列,而不是像 RNN 中那样顺序处理。
(2)灵活的感受野大小。TCN 有多种方式更改其感受野大小。其中目前表现最好的即WaveNet使用的扩大卷积(dilated convolution)。
(3)梯度稳定。TCN的反向传播与RNN相比明显不同,而且通过引入残差连接和跳跃连接,能够有效缓解和避免梯度爆炸和梯度消失现象。
了解WaveNet,必须了解如下的三个概念(1)扩大因果卷积(2)PixelCNN门激活(3)跳跃连接和残差连接
扩大因果卷积
图1 因果卷积示意图
因果卷积确保了模型输出不会违反数据的顺序:模型在 t 时刻输出的预测不会依赖任何一个未来时刻的数据,但是直接采用上述的因果卷积,为了使得模型能够捕捉长期依赖,模型必须拥有足够大的感受野,只能通过增加模型的深度和卷积核的大小,然而这将带来参数数量的迅速增加。WaveNet使用堆叠式扩大卷积层,这种卷积只需要几层就能获得很大的感受野,同时保留了输入分辨率和计算效率。
扩大因果卷积层示意图
上图描述了WaveNet中的扩大因果卷积,dilation的值为:
扩大卷积即每卷积核在找下一个卷积位置的时候会隔dilation-1个位置,这样设计能够使得感受野成指数级别的增长。
class CausalConv1d(nn.Module):
"""
Input and output sizes will be the same.
"""
def __init__(self, in_size, out_size, kernel_size, dilation=1):
super(CausalConv1d, self).__init__()
self.pad = (kernel_size - 1) * dilation
self.conv1 = nn.Conv1d(in_size, out_size, kernel_size, padding=pad, dilation=dilation)
def forward(self, x):
x = self.conv1(x)
x = x[..., :-self.pad]
return x
一维卷积输出的纬度为
,输出纬度为
。且
满足:
扩大因果卷积padding示意图
为了实现因果卷积, 需要对输入进行padding。上图演示了kernel_size 为1时左侧的padding,但是pytorch的padding是在两边同时进行的(默认两边相同大小padding)。可以看到图中单侧的padding大小为(kernel_size - 1) * dilation为,但是这样pytorch的输出结果会比原来大(kernel_size - 1) * dilation,最后多出来的数据其实是涉及到了未来时刻,直接舍弃这些值即可。这样就能在保证因果关系的同时,使得输入和输出的大小一致。
残差链接、PixelCNN、跳跃链接
RESIDUAL AND SKIP CONNECTIONS
在因果卷积后链接的是pixelCNN中的门控单元:
*代表卷积操作,
代表点乘操作。
,PixelCNN单元门之后的1*1卷积是有点迷惑的,根据我查阅github上的多个wavenet的实现,这里实际上是两个不同的1*1卷积,一个是为skip connection服务,一个为residual connection服务。
考虑到残差和跳跃连接层会多次出现,使用一个类来实现:
class ResidualLayer(nn.Module):
def __init__(self, residual_size, skip_size, dilation):
super(ResidualLayer, self).__init__()
self.conv_filter = CausalConv1d(residual_size, residual_size,
kernel_size=2, dilation=dilation)
self.conv_gate = CausalConv1d(residual_size, residual_size,
kernel_size=2, dilation=dilation)
self.resconv1_1 = nn.Conv1d(residual_size, residual_size, kernel_size=1)
self.skipconv1_1 = nn.Conv1d(residual_size, skip_size, kernel_size=1)
def forward(self, x):
conv_filter = self.conv_filter(x)
conv_gate = self.conv_gate(x)
fx = F.tanh(conv_filter) * F.sigmoid(conv_gate)
fx = self.resconv1_1(fx)
skip = self.skipconv1_1(fx)
residual = fx + x
#residual=[batch,residual_size,seq_len] skip=[batch,skip_size,seq_len]
return skip, residual
为了实现
这样的扩大卷积,只需要把它拆分为两个
即可,
这里采用DilatedStack来表示一个
堆叠层。
class DilatedStack(nn.Module):
def __init__(self, residual_size, skip_size, dilation_depth):
super(DilatedStack, self).__init__()
residual_stack = [ResidualLayer(residual_size, skip_size, 2**layer)
for layer in range(dilation_depth)]
self.residual_stack = nn.ModuleList(residual_stack)
def forward(self, x):
skips = []
for layer in self.residual_stack:
skip, x = layer(x)
skips.append(skip.unsqueeze(0))
#skip =[1,batch,skip_size,seq_len]
return torch.cat(skips, dim=0), x # [layers,batch,skip_size,seq_len]
WaveNet的组装
在pytorch中,输入时间序列数据纬度为
为了匹conv1d在最后一个纬度即序列长度方向进行卷积,首先需要交换输入的纬度为
,按照waveNet原文一开始就需要一个因果卷积。
依次经过两层
的卷积,每层的skip都会输出用于后面的计算,最后把skip加和作为结果输出,此后即可见进入decoder层,可以根据需要进行设计。
class WaveNet(nn.Module):
def __init__(self,input_size,out_size, residual_size, skip_size, dilation_cycles, dilation_depth):
super(WaveNet, self).__init__()
self.input_conv = CausalConv1d(in_put_size,residual_szie, kernel_size=2)
self.dilated_stacks = nn.ModuleList(
[DilatedStack(residual_size, skip_size, dilation_depth)
for cycle in range(dilation_cycles)]
)
self.convout_1 = nn.Conv1d(skip_size, out_size, kernel_size=1)
self.convout_2 = nn.Conv1d(skip_size, out_size, kernel_size=1)
def forward(self, x):
x = x.permute(0,2,1)# [batch,input_feature_dim, seq_len]
x = self.input_conv(x) # [batch,residual_size, seq_len]
skip_connections = []
for cycle in self.dilated_stacks:
skips, x = cycle(x)
skip_connections.append(skips)
## skip_connection=[total_layers,batch,skip_size,seq_len]
skip_connections = torch.cat(skip_connections, dim=0)
# gather all output skip connections to generate output, discard last residual output
out = skip_connections.sum(dim=0) # [batch,skip_size,seq_len]
out = F.relu(out)
out = self.convout_1(out) # [batch,out_size,seq_len]
out = F.relu(out)
out=self.convout_2(out)
out=out.permute(0,2,1)
#[bacth,seq_len,out_size]
return out