# 序列模型
# 训练
import torchfrom torch import nnfrom d2l import torch as d2limport matplotlib. pyplot as pltT = 1000 time = torch. arange( 1 , T + 1 , dtype= torch. float32) x = torch. sin( 0.01 * time) + torch. normal( 0 , 0.2 , ( T, ) )
d2l. plot( time, [ x] , 'time' , 'x' , xlim= [ 1 , 1000 ] , figsize= ( 6 , 3 ) )
tau = 4 features = torch. zeros( T- tau, tau) """ features类似于这样 1 2 3 4 2 3 4 5 3 4 5 6 """ for i in range ( tau) : features[ : , i] = x[ i: T - tau + i] """ x[tau:]: 从原始序列 x 中提取从索引 tau (在这里是 4) 开始到末尾的所有元素。这些元素是紧跟在每个特征窗口之后的那个值。 .reshape((-1, 1)): 将提取出来的一维切片重塑(reshape)成一个列向量(二维张量,形状为 (N, 1))。 -1: 表示让 PyTorch 自动计算该维度的大小。因为原始切片长度为 T - tau,所以这里会自动变成 T - tau 行。 1: 表示结果张量应该有 1 列。 """ labels = x[ tau: ] . reshape( ( - 1 , 1 ) )
tensor([[ 0.1444, -0.2229, 0.1308, 0.0739],
[-0.2229, 0.1308, 0.0739, 0.3058],
[ 0.1308, 0.0739, 0.3058, 0.1263],
...,
[-0.6506, -0.6614, -0.6828, -0.0962],
[-0.6614, -0.6828, -0.0962, -0.3576],
[-0.6828, -0.0962, -0.3576, -0.6742]])
batch_size, n_train = 16 , 600 train_iter = d2l. load_array( ( features[ : n_train] , labels[ : n_train] ) , batch_size, is_train= True )
def init_weights ( m) : """ 如果 m 是一个线性层: - nn.init: PyTorch 中用于进行参数初始化的子模块。 - xavier_uniform_: 调用 Xavier (也称 Glorot) 均匀分布初始化方法。 这是一种常用的权重初始化策略,旨在帮助网络训练时信号(梯度)更好地传播, 使得各层激活值的方差大致保持不变。 - 下划线 `_` 表示这是一个 in-place(原地)操作,它会直接修改传入的张量。 - m.weight: 访问线性层 m 的权重参数(一个张量)。 注意:这里只初始化了权重 (weight),没有初始化偏置 (bias)。通常偏置会初始化为 0。 """ if type ( m) == nn. Linear: nn. init. xavier_uniform_( m. weight) def get_net ( ) : net = nn. Sequential( nn. Linear( 4 , 10 ) , nn. ReLU( ) , nn. Linear( 10 , 1 ) , ) """ 这是 nn.Module 提供的一个方法,它会递归地将函数 fn 应用到模型自身以及模型包含的每一个子模块(submodule)上。 - init_weights: 就是我们上面定义的那个权重初始化函数。 - 效果: 1. apply 将 init_weights 应用到 net 本身(但 net 不是 nn.Linear,所以没效果)。 2. apply 将 init_weights 应用到第一个 nn.Linear(4, 10) 层。这时 init_weights 内部的 if 条件满足,该层的权重被 Xavier 初始化。 3. apply 将 init_weights 应用到 nn.ReLU() 层。if 条件不满足,无操作。 4. apply 将 init_weights 应用到第二个 nn.Linear(10, 1) 层。if 条件满足,该层的权重被 Xavier 初始化。 # - 目的:确保网络中所有线性层的权重都按照我们定义的 init_weights 函数进行了初始化。 """ net. apply ( init_weights) return net """ # 创建一个损失函数的实例。 # - nn.MSELoss: PyTorch 提供的计算均方误差(Mean Squared Error)的类。 # 这是回归任务中最常用的损失函数之一。它计算的是模型预测值与真实目标值之间差值的平方的均值。 # 计算公式通常为:L = mean((prediction - target)^2) # - reduction='none': 这是 MSELoss 的一个重要参数,用于控制损失计算的输出方式。 # - 'mean' (默认值): 计算批次中所有样本损失的平均值,返回一个标量(scalar)。 # - 'sum': 计算批次中所有样本损失的总和,返回一个标量。 # - 'none': 不进行最终的求和或求平均操作。它会返回一个与输入/目标张量形状相同(或可广播)的张量,其中每个元素是对应样本的平方误差损失值。这样做通常是为了后续可能需要对每个样本的损失进行不同的处理(比如加权、分析等),或者在某些特定的训练流程中需要原始的、未聚合的损失值。如果需要得到最终的平均损失,可以在得到 `output_loss` 后再手动调用 `.mean()` 方法。 """ loss = nn. MSELoss( reduction= 'none' )
def train ( net, train_iter, loss, epochs, lr) : """ 创建一个优化器 (Optimizer) 实例。 - torch.optim.Adam: 使用 Adam 优化算法。Adam 是一种常用的、高效的梯度下降优化算法, 它能自适应地调整每个参数的学习率。 - net.parameters(): 获取模型 `net` 中所有需要训练的参数 (权重和偏置)。优化器需要知道要更新哪些参数。 - lr=lr: 将函数参数传入的学习率 `lr` 设置给 Adam 优化器。 """ trainer = torch. optim. Adam( net. parameters( ) , lr= lr) for epoch in range ( epochs) : for X, y in train_iter: trainer. zero_grad( ) """ # 执行核心计算: # 1. net(X): 前向传播 (Forward Pass)。将当前批次的特征 `X` 输入网络 `net`,得到模型的预测输出。 # 2. loss(predictions, targets): 计算损失。使用传入的 `loss` 函数,计算模型预测值 `net(X)` 与真实标签 `y` 之间的差异(损失)。 # 3. l = ...: 将计算得到的损失赋值给变量 `l`。 *重要提示*:因为我们之前定义的 `loss` 使用了 `reduction='none'`, # 所以这里的 `l` 不是一个单一的数值(标量),而是一个包含了 # 批次中每个样本单独损失值的张量。 """ l = loss( net( X) , y) """ 执行反向传播 (Backward Pass / Backpropagation)。 1. l.sum(): 由于 `l` 是包含每个样本损失的张量,我们需要一个标量值来启动整个计算图的反向传播。`.sum()` 将当前批次所有样本的损失加起来,得到一个标量值(代表批次总损失)。(如果 loss 使用的是 'mean' 或 'sum',则可以直接调用 l.backward()) 2. .backward(): PyTorch 的自动微分引擎会根据这个最终的标量损失值,自动计算损失相对于模型中所有可训练参数(`net.parameters()` 中`requires_grad=True` 的参数)的梯度。 """ l. sum ( ) . backward( ) """ # 更新模型参数。 # - 优化器 (`trakiner` / `optimizer`) 根据 `.backward()` 计算得到的梯度, # 以及它自身的更新规则 (Adam 算法) 和学习率 (`lr`),来调整模型 `net` 的权重和偏置。 """ trakiner. step( ) print ( 'epoch: %d, loss: %.3f' % ( epoch + 1 , d2l. evaluate_loss( net, train_iter, loss) ) ) net = get_net( ) train( net, train_iter, loss, 5 , 0.01 )
epoch: 1, loss: 0.073
epoch: 2, loss: 0.051
epoch: 3, loss: 0.052
epoch: 4, loss: 0.051
epoch: 5, loss: 0.051
# 预测
""" 张量feature: (T - tau, tau) 张量onestep_preds: (T - tau, 1)其中 onestep_preds[j] 是模型基于真实历史数据 [x[j], ..., x[j+tau-1]] 对下一个时间点 x[j+tau] 的预测值。 """ onestep_preds = net( features) d2l. plot( [ time, time[ tau: ] ] , """ - x.detach().numpy(): 原始时间序列数据 x,转换为 NumPy 数组用于绘图。 .detach() 用于从计算图中分离张量,防止梯度计算;.numpy() 将其转为 NumPy 格式。 onestep_preds.detach().numpy(): 单步预测结果,同样转换为 NumPy 数组。 """ [ x. detach( ) . numpy( ) , onestep_preds. detach( ) . numpy( ) ] , 'time' , 'x' , legend= [ 'data' , '1-step preds' ] , xlim= [ 1 , 1000 ] , figsize= ( 6 , 3 ) )
multistep_preds = torch. zeros( T) """ 多步预测通常是在训练数据之后进行的。为了启动预测过程,我们需要提供模型所需的初始历史数据。这里假设模型是在前 n_train 个点上训练的。 预测第 n_train + tau 个点的值,需要用到从 n_train 到 n_train + tau - 1 的数据。所以我们将真实数据 x 中直到 n_train + tau - 1 的部分(共 n_train + tau 个点)复制到 multistep_preds 中作为“种子”或初始条件。 """ multistep_preds[ : n_train + tau] = x[ : n_train + tau] for i in range ( n_train + tau, T) : multistep_preds[ i] = net( multistep_preds[ i- tau: i] . reshape( ( 1 , - 1 ) ) ) """ multistep_preds[i - tau : i]: 从 multistep_preds 张量中提取预测当前点 i所需要的历史数据,即从 i-tau 到 i-1 的 tau 个值。 *核心区别*:当 i > n_train + tau 时,这个切片中将包含至少一个先前由模型 *自己预测* 并存入 multistep_preds 的值,而不是真实的 x 值。 .reshape((1, -1)): 将提取出的 1D 切片(长度 tau)重塑为 2D 张量,形状为 (1, tau)。这是因为神经网络 net 通常期望输入是一个批次 (batch),即使批次大小仅为 1。-1 会自动推断出列数应为 tau。 逻辑:这是一个自回归(auto-regressive)的过程。模型预测未来的一个点,然后这个预测值又被用作未来预测的输入。这种方式模拟了真实的预测场景,即我们不知道未来的真实值,只能依赖模型的预测来滚动向前。这种方法的缺点是预测误差会累积。 """
d2l. plot( [ time, time[ tau: ] , time[ n_train + tau: ] ] , [ x. detach( ) . numpy( ) , onestep_preds. detach( ) . numpy( ) , multistep_preds[ n_train + tau: ] . detach( ) . numpy( ) ] , 'time' , 'x' , legend= [ 'data' , '1-step preds' , 'multistep pred' ] , xlim= [ 1 , 1000 ] , figsize= ( 6 , 3 ) )
max_steps = 64 """ 形状分析: (行数, 列数) 行数 = T - tau - max_steps + 1: 这个行数代表了我们可以从原始序列 x 中选取多少个不同的起始“窗口”。对于每个起始点 j(从 0 开始),我们需要 tau 个历史点(x[j] 到 x[j+tau-1])并且还需要能进行 max_steps 的预测。最后一步预测对应原始序列的 j + tau + max_steps - 1 这个时间点。这个时间点必须小于总长度 T,即 j + tau + max_steps - 1 < T,所以 j < T - tau - max_steps + 1。因此,有效的起始点 j 的数量就是 T - tau - max_steps + 1。 列数 = tau + max_steps: 这个列数表明,对于每个起始窗口(每一行),我们将存储 tau 个初始的历史真实值,并且存储接下来由模型生成的 max_steps 个预测值。 逻辑: 创建一个足够大的矩阵,用来存放所有起始窗口的初始历史数据以及它们各自对应的、直到 max_steps 步的未来预测序列。 """ features = torch. zeros( ( T - tau - max_steps + 1 , tau + max_steps) ) for i in range ( tau) : features[ : , i] = x[ i: T - tau - max_steps + i + 1 ] """ 这行代码是进行自回归预测并填充 features 矩阵后续的 max_steps 列。 features[:, i - tau : i]: 对于要计算的第 i 列(i >= tau),这会选取它前面 tau 列的所有行数据。这构成了预测第 i 时刻(相对于每个窗口起始点)所需的输入特征。例如,计算第 tau 列(即 1 步预测)时,它使用第 0 到 tau-1 列(真实历史);计算第 tau+1 列(即 2 步预测)时,它使用第 1 到 tau 列(其中第 tau 列是上一步的预测值)。 net(...): 将这些大小为 (行数, tau) 的特征数据块输入到神经网络 net 中。模型的输出形状将是 (行数, 1)。 .reshape(-1): 将网络输出从 (行数, 1) 变形为 (行数,),即一个一维张量。 features[:, i] = ...: 将这些预测结果赋值给 features 矩阵的第 i 列。 逻辑: 这个循环逐列(即逐个预测时间步)地生成预测。对于 features 矩阵中的每一行(代表一个起始窗口),它都在用模型基于该行之前的 tau 个值(无论是真实历史还是先前的预测)来预测下一个值,并将结果存储在该行的相应列中。这个过程对所有行并行执行,直到生成了 max_steps 步的预测。 """ for i in range ( tau, tau + max_steps) : features[ : , i] = net( features[ : , i- tau: i] ) . reshape( - 1 )
steps = ( 1 , 4 , 16 , 64 ) d2l. plot( [ time[ tau + i - 1 : T - max_steps + i] for i in steps] , [ features[ : , ( tau + i- 1 ) ] . detach( ) . numpy( ) for i in steps] , 'time' , 'x' , legend= [ f' { i} -step preds' for i in steps] , xlim= [ 5 , 1000 ] , figsize= ( 6 , 3 ) )