目录
前言
本文用于记录在我在初学时间序列预测的网络时,对一些基础的网络的理解,并且主要从代码出发,根据代码的结构来解释网络中进行的具体流程。在我初学这里的每一个网络时,自认为比较重要且复杂的问题就是张量维度和通道数在网络中的变化问题,所以选择在本文中详细说明此类问题。
文中所记录的网络的代码在使用前需要自行初始化一个类,其中需要包含每一个网络中初始化参数 configs 中的值,并且需要保证进入网络的前向传播的代码需要满足Tansor[batch_size,seq_in,c_in] 的要求,部分细节可能需要根据自己的数据集进行修改。
一、CNN
卷积神经网络(Convolutional Neural Network, CNN),利用卷积来有效地提取输入数据的空间特征。具体步骤如下:
输入层 ==> 卷积层 ==> 池化层 ==> 全连接层 ==> 输出层
1.1 网络代码
class CNN(nn.Module):
def __init__(self, configs):
super(CNN, self).__init__()
self.relu = nn.ReLU(inplace=True)
self.configs = configs
# Conv layers with dynamic configurations
self.conv1 = nn.Sequential(
nn.Conv1d(in_channels=configs.c_out, out_channels=configs.d_model, kernel_size=2),
# output: (batch_size, d_model, seq_len-1)
nn.ReLU(),
nn.MaxPool1d(kernel_size=2, stride=1), # output: (batch_size, d_model, (seq_len-1)-1)
)
self.conv2 = nn.Sequential(
nn.Conv1d(in_channels=configs.d_model, out_channels=configs.d_model * 2, kernel_size=2),
# output: (batch_size, d_model*2, (seq_len-2)-1)
nn.ReLU(),
nn.MaxPool1d(kernel_size=2, stride=1), # output: (batch_size, d_model*2, (seq_len-3)-1)
)
# Compute the output size after conv layers
conv_output_size = self.configs.d_model * 2 * (self.configs.seq_len - 4) # This needs to be calculated based on conv/pool operations
# Fully connected layers
self.Linear1 = nn.Linear(conv_output_size, 50) # Flattened output size as input
self.Linear2 = nn.Linear(50, configs.c_out) # Output shape: (batch_size, pre_len, c_out)
def forward(self, x):
# Input shape: (batch_size, seq_len, c_in) => (batch_size, c_in, seq_len)
x = x.permute(0, 2, 1)
# Pass through conv layers
x = self.conv1(x) # Shape: (batch_size, d_model, seq_len-2)
x = self.conv2(x) # Shape: (batch_size, d_model*2, seq_len-4)
# Flatten the tensor for the linear layer
x = x.view(x.size(0), -1) # Shape: (batch_size, d_model * 2 * (seq_len-4))
# Fully connected layers
x = self.Linear1(x)
x = self.relu(x)
x = self.Linear2(x)
# Output shape: (batch_size, c_out) => (batch_size, pre_len, c_out)
x = x.view(x.shape[0], 1, -1)
return x
1.2 代码解读
① self.conv1
卷积层 ==> 激活层 ==> 池化层
self.conv1 = nn.Sequential(
nn.Conv1d(in_channels=configs.c_out, out_channels=configs.d_model, kernel_size=2),
nn.ReLU(),
nn.MaxPool1d(kernel_size=2, stride=1),
)
② nn.Conv1d(卷积层)
nn.Conv1d(in_channels=configs.c_out, out_channels=configs.d_model, kernel_size=2)
需要注意的是,卷积运算默认是在张量的最后一个维度上进行的,但是我们的输入序列一般是 [batch_size,seq_in,c_in] 的形式,而卷积运算的目标是为 seq_in 所在的维度上提取目标序列的特征,所以在前行传播的过程中会加入 x = x.permute(0, 2, 1) 来调换最后两维度的位置。
张量在经过 nn.Conv1d 后的维度变化如下:
[batch_size,c_in,seq_in] ==> [batch_size,d_model,seq_out]
其中seq_out的计算如下:(seq_in + 2 * padding - kernel_size) / stride + 1
关于维度转换转换的形象画法可以看下面这篇博客:
nn.Conv1d(in_channels=c_in, out_channels=d_model, kernel_size=3)的维度转换理解
③ nn.MaxPool1d(池化层)
池化相当于在空间范围内做了维度约减,从而使模型可以抽取更加广范围的特征。同时减小了下一层的输入大小,进而减少计算量和参数个数,并且在一定程度上可以以防止过拟合,更方便优化。
nn.MaxPool1d ( kernel_size = 2, stride = 1 )
卷积核大小(kernel_size )为 1*3 , 行上每次滑动(stride)两步,列上每次滑动一步。
输出结果为每一个卷积核内的最大值。其中 输出张量的维度变化与卷积层类似
[batch_size,c_in,seq_in] ==> [batch_size,d_model,seq_out]
其中seq_out的计算如下:(seq_in + 2 * padding - kernel_size) / stride + 1
④ nn.Linear(全连接层)
汇总卷积层和池化层得到的序列的底层特征和信息
nn.Linear ( in_feature = conv_output_size , out_feature = 50 )
nn.Linear 默认也是在输入张量的最后一维上进行,也就是in_feature和out_feature都应该是张量最后一维的通道数。而经过卷积和池化操作后的张量中,d_model 和 seq_out 所在的维度都包含了序列的信息,所以在全连接层之前通过 x = x.view(x.size(0), -1) 的操作将最后一维的通道数转变为 d_model * 2 * seq_out 。
nn.Linear(全连接层)的本质其实就是一个矩阵运算,例如我输入的张量为 Tansor[1,4],
全连接层定义为 nn.Linear ( in_feature = 4 , out_feature = 2 ) ,则矩阵运算如下图所示:
二、TCN
TCN模型通过膨胀卷积和残差块来捕捉序列中的依赖关系,能够捕捉序列中的长期依赖关系,适用于时间序列数据。
Input ==> [Conv1D] ==> [Residual Block] * N ==> [Output Layer]
输入层 ==> 一维卷积层 ==> 残差块 ==> 输出层
2.1 网络代码
① 总体框架
class TCN(nn.Module):
def __init__(self, configs):
super(TCN, self).__init__()
layers = []
num_inputs = configs.c_out
outputs = configs.c_out
self.output_size = configs.c_out
self.pre_len = configs.pred_len
num_channels = [configs.d_ff]
kernel_size = 2
dropout = 0.2
num_levels = len(num_channels)
for i in range(num_levels):
dilation_size = 2 ** i
in_channels = num_inputs if i == 0 else num_channels[i-1]
out_channels = num_channels[i]
layers += [TemporalBlock(in_channels, out_channels, kernel_size, stride=1, dilation=dilation_size,
padding=(kernel_size-1) * dilation_size, dropout=dropout)]
self.network = nn.Sequential(*layers)
self.linear = nn.Linear(num_channels[-1], outputs)
def forward(self, x):
x = x.permute(0, 2, 1) # 从 (batch_size, seq_len, c_in) 转换为 (batch_size, c_in, seq_len)
x = self.network(x)
x = x.permute(0, 2, 1) # 转换回 (batch_size, seq_len, num_channels[-1])
x = self.linear(x) # 线性层调整输出维度为 (batch_size, seq_len, outputs)
return x[:, -self.pre_len:, :] # 选择最后的 pre_len 个时间步
② 单层Block
class TemporalBlock(nn.Module):
def __init__(self, n_inputs, n_outputs, kernel_size, stride, dilation, padding, dropout=0.2):
super(TemporalBlock, self).__init__()
self.conv1 = weight_norm(nn.Conv1d(n_inputs, n_outputs, kernel_size,
stride=stride, padding=padding, dilation=dilation))
self.chomp1 = Chomp1d(padding)
self.relu1 = nn.ReLU()
self.dropout1 = nn.Dropout(dropout)
self.conv2 = weight_norm(nn.Conv1d(n_outputs, n_outputs, kernel_size,
stride=stride, padding=padding, dilation=dilation))
self.chomp2 = Chomp1d(padding)
self.relu2 = nn.ReLU()
self.dropout2 = nn.Dropout(dropout)
self.net = nn.Sequential(self.conv1, self.chomp1, self.relu1, self.dropout1,
self.conv2, self.chomp2, self.relu2, self.dropout2)
self.downsample = nn.Conv1d(n_inputs, n_outputs, 1) if n_inputs != n_outputs else None
self.relu = nn.ReLU()
self.init_weights()
def init_weights(self):
self.conv1.weight.data.normal_(0, 0.01)
self.conv2.weight.data.normal_(0, 0.01)
if self.downsample is not None:
self.downsample.weight.data.normal_(0, 0.01)
def forward(self, x):
out = self.net(x)
res = x if self.downsample is None else self.downsample(x)
return self.relu(out + res)
2.2 代码解读
① self.net
self.net = nn.Sequential(self.conv1, self.chomp1, self.relu1,
self.dropout1,self.conv2, self.chomp2, self.relu2, self.dropout2)
表示为每一层 Residual Block 中的左半边部分:
(Dilated Causal Conv ==> WeightNorm ==> ReLU ==> Dropout)* 2
其中的四个部分分别为:膨胀因果卷积、权重归一化、激活函数、Dropout
② self.conv1
self.conv1 = weight_norm(nn.Conv1d(n_inputs, n_outputs,
kernel_size, stride=stride, padding=padding, dilation=dilation))
这段初始化表示的过程为:先用 nn.Conv1d 对输入张量进行膨胀因果卷积,再利用 weight_norm 对卷积层的权重进行归一化处理。
其中 nn.Conv1d 中的最后一个参数 dilation 表示为卷积核的膨胀系数(一般膨胀系数是2的指数次方,即1,2,4,8,16,32…),用于控制感受野大小。
以下动图以 kernel_size=2 ,dilations=[1,2,4,8] 的因果膨胀卷积为例,展示计算过程:
③ self.chomp1
self.chomp1 = Chomp1d ( padding )
class Chomp1d(nn.Module):
def __init__(self, chomp_size):
super(Chomp1d, self).__init__()
self.chomp_size = chomp_sizedef forward(self, x):
return x[:, :, :-self.chomp_size].contiguous( )
为了使输出序列能够完全覆盖输入序列,也就是输出序列的的第一位值能够与输入序列的第一位对应,我们需要制定合适的 padding 值填充在输入序列前后。
padding = (kernel_size-1) * dilation_size
由于在输入序列的某位填补了 padding ,所以经过膨胀因果卷积处理后的通道数 seq_out 也会同样地增减相应的 padding ,其具体的计算方式如下:
seq_out = seq_in + (kernel_size - 1) * dilation_size
注:以上的情况是基于 stride 的取值为 1 ,当面对 stride 的取值不为 1 的一般情况时,seq_out 的值可以进一步地表示为:(“//”表示将除法结果向下取整)
seq_out = (seq_in + (kernel_size - 1) * dilation_size - 1) // stride + 1
所以为了确保膨胀因果卷积后的序列与初始序列的尺寸一致,我们利用 Chomp1d 来对输出的序列进行裁剪,而裁剪的尺寸即为 padding 。
三、LSTM
LSTM采用了门控输出的方式,即三门(输入门、遗忘门、输出门)两态(Cell State长时、Hidden State短时)。其核心即 Cell State,指用于信息传播的Cell的状态。
关于 LSTM 网络的详细流程步骤可以看下面这篇博客,其中有三个门的动画和流程的解释:
3.1 网络代码
① 总体框架
class LSTM(nn.Module):
"""
使用LSTM进行回归
参数:
- input_size: feature size (c_in)
- hidden_size: number of hidden units
- output_size: number of output features (c_in)
- num_layers: layers of LSTM to stack
- pre_len: length of the output sequence
"""
def __init__(self, configs):
super().__init__()
input_size = configs.c_out
hidden_size = configs.d_ff
output_size = configs.c_out
num_layers = configs.e_layers
pre_len = configs.pred_len
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
self.pre_len = pre_len
def forward(self, _x):
batch_size, seq_len, _ = _x.shape
# LSTM expects input shape: (batch_size, seq_len, input_size)
x, _ = self.lstm(_x) # _x shape: (batch_size, seq_len, input_size), output shape: (batch_size, seq_len, hidden_size)
x = self.fc(x) # Map hidden state to output feature space (batch_size, seq_len, output_size)
# Adjust the output sequence length to pre_len
if seq_len != self.pre_len:
if seq_len > self.pre_len:
x = x[:, -self.pre_len:, :] # Truncate sequence to last pre_len steps
else:
# Optionally, you could pad or repeat the sequence to match pre_len
padding = self.pre_len - seq_len
x = torch.cat([x, x[:, -1:, :].repeat(1, padding, 1)], dim=1) # Repeat last output to match pre_len
return x
3.2 代码解读
① self.lstm
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
由于pytorch中自带了LSTM的网络框架,所以可以直接用代码调用,其中输入输出张量的维度变化如下:
input_Tansor : [batch_size,seq_in,c_in]
==> output_Tansor : [batch_size,seq_in,hidden_size]
因为输出张量的数据(也就是h),对应的是每一位输入张量进入LSTM后分别对应的输出值,所以实际我们所需要的预测值,只需要取输出序列的最后一位即可,代码表示如下:
if seq_len != self.pre_len: if seq_len > self.pre_len: x = x[:, -self.pre_len:, :] #取输出序列的最后 self.pre_len 个值 else: padding = self.pre_len - seq_len x = torch.cat([x, x[:, -1:, :].repeat(1, padding, 1)], dim=1) #输入序列长度小于输出序列时,重复最后一个值补齐输出序列 return x
② num_layers
需要注意的是,nn.LSTM(input_size, hidden_size, num_layers, batch_first=True) 中的第三个参数 num_layers 所表示的并非 LSTM 中横向的模块链中模块的个数,而是纵向的模块链的个数,如下图所示,
也就是说对于多层的LSTM而言,需要把第一层的每个时间步的输出作为第二层的时间步的输入。
总结
作为一个学习嵌入式的电子信息本科生,初次接触神经网络虽感觉到陌生但是饶有兴趣,复现了许多代码后也有些许收获,之后也会继续学习。本文作为学习神经网络后写的第一篇博客,或许存在许多不足,希望得到大家的指点和交流,愿共勉。
巨人之肩:
卷积神经网络(CNN)详细介绍及其原理详解
关于nn.Conv1d、nn.MaxPool1d的工作原理(是否需要变换最后两个维度)
【学习日志】【TCN】时间序列卷积神经网络(1)
时间卷积网络(TCN):结构+pytorch代码
TCN(Temporal Convolutional Networks)详解
利用LSTM实现预测时间序列(股票预测)
LSTM从入门到精通(形象的图解,详细的代码和注释,完美的数学推导过程)