模型的训练过程就是不断调整权重的过程,准确一点还应该加上偏置,模型的训练过程就是不断调整权重和偏置的过程,调整的过程依赖反向传播、损失函数等等。
准备一些训练数据,可以用 CSV 格式的文件存储。
将数据集分割为训练集和测试集,常用的比例为 80% 训练集和 20% 测试集,确保模型能在未见过的数据上进行有效预测。
这里说的参数主要是指权重 (W) 和偏置 (b) 。这两个变量的初始值非常关键,因为它们可以影响网络的收敛速度,以及是否能够收敛到一个好的解。选择好的初始值可以避免一些问题,如梯度消失或梯度爆炸。我们来看一些常用的权重和偏置初始化方法。
在神经网络权重初始化时,通常会选用较小的随机数,这些随机值可从均匀分布或正态分布中抽取。例如,一种常见做法是从 “均值为 0、标准差为 1/√n(n 为当前层的输入节点数)” 的正态分布中采样 —— 这种初始化方式的具体名称,会根据所选分布的方差差异,被称为 He 初始化、Glorot 初始化或 Xavier 初始化。
以我们之前定义的 Transformer 模型为例,其解码器层默认采用的权重初始化方法就是 Glorot 初始化。
至于偏置的初始化,一般会将其设为 0,或取值极小的正数(比如 0.01)。这样设计的核心原因是:在模型训练初期,不希望偏置对输出结果产生过大干扰,而是让模型优先通过调整权重来学习数据中的特征与模式。
将所有权重或偏置设置为同一个常数,比如 0。不过我不太推荐这种方法,因为它会导致神经网络在训练初期每个神经元的行为都相同,这会阻碍有效的学习。特定
对于某些特定的网络架构或激活函数,可能需要特定的初始化方法。例如,使用 ReLU 激活函数的时候,He 初始化,也就是使用较大的方差来初始化权重,通常效果更好,因为它考虑到了 ReLU 在负值上的非激活特性。
在某些情况下,特别是在训练深层网络或循环神经网络(RNNs)的时候,使用正交初始化方法来初始化权重有助于减少梯度消失或爆炸的问题。正交初始化保证了权重矩阵的行或列是正交的,这有助于保持激活和梯度在不同层间的独立性。
我们可以查看源代码来了解各个层,如 Embedding 层、Linear 层、编码器、解码器层使用的初始化方法。使用下面的方法查看默认的参数值:
print(decoder_layer.self_attn.in_proj_weight)
在实际应用中,如果默认的初始化策略不满足特定的需求,你也可以用下面的代码,通过自定义函数并使用 .apply() 方法来对模型的所有参数进行自定义初始化。这种方法非常灵活,适用于复杂的模型结构。
decoder_layer = nn.TransformerDecoderLayer(d_model=512, nhead=8)
def custom_init(m): if isinstance(m, nn.Linear): torch.nn.init.xavier_uniform_(m.weight) if m.bias is not None: torch.nn.init.constant_(m.bias, 0.0)
decoder_layer.apply(custom_init)
在训练过程中,前向传播就是 Embedding 后的输入向量一层一层向后传递的过程,每一层都有权重和偏置,我们看一下 Linear 层的权重和参数是怎么赋值的。
self.fc = nn.Linear(embed_size, vocab_size)
def __init__(self, in_features: int, out_features: int, bias: bool = True, device=None, dtype=None) -> None: factory_kwargs = {'device': device, 'dtype': dtype} super().__init__() self.in_features = in_features self.out_features = out_features self.weight = Parameter(torch.empty((out_features, in_features), **factory_kwargs)) if bias: self.bias = Parameter(torch.empty(out_features, **factory_kwargs)) else: self.register_parameter('bias', None) self.reset_parameters()
权重(Weights):一个形状为 (embed_size, num_features) 的矩阵。
偏置(Bias):一个形状为 (embed_size,) 的向量。
当你通过这一层传递输入 input 时(input = self.fc1(input)),它实际上执行的操作是矩阵乘法加上偏置项,你可以看一下计算公式:output=input×weightT+bias
这里的 “T” 代表矩阵的转置操作,并非次方,这是线性代数里为调整矩阵维度、确保矩阵乘法合法执行的常用手段。
具体到公式场景中:
-
输入(input)是一个形状为
m×n 的矩阵,其中 m 代表批处理大小(也就是单次计算的样本数量),n 代表每个样本的特征数量;
-
权重(weight)是一个形状为
d×n 的矩阵,其中 d 代表输出层(或下一层)的神经元数量。
矩阵乘法有个核心规则:前一个矩阵的列数,必须与后一个矩阵的行数相等,乘法才能有效执行。但在神经网络的 nn.Linear 层中,权重矩阵默认是以 d×n 的形式存储的 —— 这个形状无法直接和 m×n 的输入矩阵相乘(输入列数 n 与权重行数 d 不匹配)。
因此,我们需要对权重矩阵做转置处理,将其从 d×n 转换为 n×d。此时,m×n 的输入矩阵与 n×d 的转置后权重矩阵满足乘法规则,相乘后会得到一个 m×d 的矩阵 —— 这个结果就对应着 m 个样本经过线性层后的输出。
其中 σ 是 Sigmoid 函数,一种常用的激活函数。最后将 A2 作为输入传入输出层,计算本次前向传播得到的输出值,用来计算损失。
损失是神经网络训练过程中非常重要的概念,描述本次前向传播结果和实际值的差异,一般来说越低越好,神经网络根据损失进行反向传播,找到更合适的权重和偏置,进而更新参数。对于我们举的二元分类问题,最常用的损失函数是二元交叉熵损失(Binary Cross-Entropy Loss)。当输出是一个概率值,并且标签是 0 或 1 的时候,这种方法非常合适。损失的计算公式如下:
其中 N 是样本数量,yi 是真实标签,y^i 是预测的概率。Python 中可以直接使用下面的函数。
import torch.nn as nn
criterion = nn.BCEWithLogitsLoss()
loss = criterion(output, target)
到损失值就可以开始反向传播了。当然,如果损失已经非常小,并到达训练目标了,那是可以停止训练的。
反向传播也是神经网络训练过程中的一个重要概念,用来根据损失推算合适的权重和偏置。为了理解反向传播的含义,我们还是举 y=kx+b 的例子。
当我们初始化 k 和 b 的值分别为 2 和 1 时,如果输入值 x=1,那么经过计算 y 的值为 3。如果我们期望的值是 4,那么此处就可以通过调用损失函数,把前向传播得到的值 3 和期望值 4 传入损失函数 L,计算出损失值。假设得到的损失值是 0.6,这个时候我们需要通过一定的计算公式反推出合适的 k 和 b,比如当输入 x=1 时,k+b=3,这个时候需要找到合适的 k 和 b。因为 k 和 b 有无数种组合,那到底该怎么推呢?我们看一下详细的过程。
反向传播的目的是计算 ∂w∂L(损失 L 对 w 的梯度)和 ∂b∂L(损失 L 对 b 的梯度),其中 w 和 b 是网络层的权重和偏置。我们可以应用链式法则,你看一下这 2 条公式。
我们在训练的时候,不用自己计算,调用如下代码就可以计算梯度了。
得到 ∂w∂L 和 ∂b∂L 后,就可以更新参数了。w 更新:w←w−η∂w∂Lb 更新:b←b−η∂b∂L其中,η 是学习率,一个小的正数,用来控制学习的步长。我们可以使用下面这行代码更新权重参数。
optimizer = optim.Adam(model.parameters(), lr=learning_rate)optimizer.step()
这里使用的是 optim.Adam 优化器,当然也可以使用其他优化器,比如 AdaGrad 和 RMSProp 等。接下来就是按照训练的策略,继续下一轮训练,直到达到目标或者训练轮数完成。