每次迭代,我们用一个样本进行训练,这种方式称为随机梯度下降(Stochastic Gradient Descent,SGD)。虽然这种方式简单,但是无法充分利用 NumPy 的矢量并行计算能力。实际上,我们可以一次将多个样本送入网络模型进行训练。这种方式称为批处理(Batch Processing),是现代深度学习的标准做法。
按照每次使用的样本数量,梯度下降可分为三类:
随机梯度下降(SGD):每次把一个样本送入迭代;
全批量梯度下降(Batch Gradient Descent,Batch GD):每次把全部样本送入迭代。实践中很少使用全批量梯度下降进行训练,因为需要大量的内存,运算速度会非常缓慢;
小批量梯度下降(Mini-batch Gradient Descent,Mini-batch GD):每次把数个样本送入迭代。小批量梯度下降充分地利用了 NumPy(或者其他计算库)的矢量并行计算能力,是目前常规的模型训练方式。
收敛¶
收敛(Convergence)指模型训练的过程中,网络模型通过不断的迭代,其损失值不再显著下降,模型参数趋于稳定。
随机梯度下降:每次迭代只能从一个样本进行学习,因此梯度变化随机性较大,收敛方向容易左右摇摆,所以收敛速度较慢;
全批量梯度下降:每次迭代都可以从全部数据进行学习,因此可以相对保持正确的收敛方向,所以收敛速度较快;
小批量梯度下降:折中方案,兼顾效率和稳定性。
泛化¶
泛化(Generalization)是评价模型训练效果的一个重要指标,即网络模型对新数据的处理能力。简单地讲,就是一个在训练集上表现良好的网络模型,是否可以在测试集上同样有好的表现。
通常来讲,泛化能力来自于对多样化的数据进行学习。因此:
随机梯度下降:每次一个样本的训练方式,通常可以带来更好的模型泛化能力;
全批量梯度下降:每次训练的数据一样,缺少变化,会影响泛化能力的提高;
小批量梯度下降:折中方案,兼顾多样性和稳定性。
拟合¶
拟合(Fitting)是指网络模型在训练集上的表现,即网络模型学习数据规律的程度。
泛化能力差一般就是因为拟合不理想,通常有两种情况:
欠拟合(Underfitting):网络模型在训练集上表现就很差,在测试集上表现也很差。通常是因为训练不足,或者网络模型过于简单。
过拟合(Overfitting):网络模型在训练集上接近优异,但在测试集上表现糟糕。通常是因为网络模型过于复杂,学习到了多余的、没有代表性的细节。
💡 提示:小批量梯度下降同时具有较好的收敛速度和泛化能力,因此在实际工作中是最常用的训练方式。
import numpy as np张量¶
class Tensor:
def __init__(self, data):
self.data = np.array(data)
self.grad = np.zeros_like(self.data)
self.gradient_fn = None
self.parents = set()
def backward(self):
if self.gradient_fn is not None:
self.gradient_fn()
for p in self.parents:
p.backward()
def __str__(self):
return f'Tensor({self.data})'数据集¶
我们给数据集增加了一个初始化参数:batch_size(批大小)。
通过设置不同的批大小,我们可以控制以何种方式进行训练:
随机梯度下降:这是数据集的缺省方式(
batch_size=1)。对于我们的微型数据集,__getitem__()每次返回一个样本,而__len__()将返回4;全批量梯度下降:直接使用
all()就可以一次性获得全部数据样本;小批量梯度下降:设置
batch_size为任何大于 1 的数值。比如我们设置batch_size=2,那么__getitem__()将每次返回两个样本,而__len__()将返回2。
需要特别提出的是:增加了批处理功能的数据集,输出的数据格式也从之前的 1 维张量,格式为 (特征数量,),改变为 2 维张量,格式为 (批大小, 特征数量)。
class Dataset:
def __init__(self, batch_size=1):
self.batch_size = batch_size
self.load()
self.train()
def load(self):
self.train_data = ([[22.5, 72.0],
[31.4, 45.0],
[19.8, 85.0],
[27.6, 63.0]],
[[95],
[210],
[70],
[155]])
self.test_data = ([[28.1, 58.0]],
[[165]])
def train(self):
self.data = self.train_data
def eval(self):
self.data = self.test_data
def all(self):
x, y = self.data
return Tensor(x), Tensor(y)
# 批次数量
def __len__(self):
x, *_ = self.data
return len(x) // self.batch_size
# 单个批次
def __getitem__(self, index):
s = slice(index * self.batch_size, (index + 1) * self.batch_size)
x, y = self.data
return Tensor(x[s]), Tensor(y[s])模型¶
数据集实现批处理后,__getitem__() 每次返回的样本将不再是 1 维张量,而是 2 维张量,格式为:(批大小, 特征数量)。因此推理函数的输出也将是 2 维张量,格式为:(批大小, 标签数量)。
梯度函数在计算权重的梯度时,也需要采用适用于 2 维张量的计算公式,即从乘法 p.grad * x.data 改成矩阵乘法 p.grad.T @ x.data。而在计算偏置的梯度时,也需要特别指明按照 0 轴(批次)求和 np.sum(p.grad, axis=0)。
这样修改后,可以保证一次运算,对同一批次中的每个样本都分别计算出正确的梯度。
class Linear:
def __init__(self, in_size, out_size):
self.weight = Tensor(np.ones((out_size, in_size)) / in_size)
self.bias = Tensor(np.zeros(out_size))
def __call__(self, x: Tensor):
return self.forward(x)
def forward(self, x: Tensor):
p = Tensor(x.data @ self.weight.data.T + self.bias.data)
def gradient_fn():
self.weight.grad += p.grad.T @ x.data
self.bias.grad += np.sum(p.grad, axis=0)
p.gradient_fn = gradient_fn
return p
@property
def parameters(self):
return [self.weight, self.bias]损失函数(均方误差)¶
梯度函数需要把误差项除以批大小。这样做的目的,是避免将一个批次的数个样本的误差项叠加到一起,将梯度放大数倍。
class MSELoss:
def __call__(self, p: Tensor, y: Tensor):
return self.loss(p, y)
def loss(self, p: Tensor, y: Tensor):
mse = Tensor(np.mean(np.square(y.data - p.data)))
def gradient_fn():
p.grad += -2 * (y.data - p.data) / y.data.size
mse.gradient_fn = gradient_fn
mse.parents = [p]
return mse优化器(随机梯度下降)¶
class SGDOptimizer:
def __init__(self, parameters, lr):
self.parameters = parameters
self.lr = lr
def zero_grad(self):
for p in self.parameters:
p.grad = np.zeros_like(p.data)
def step(self):
for p in self.parameters:
p.data -= p.grad * self.lrLEARNING_RATE = 0.00001批大小¶
作为我们的第二个超参数,批大小(Batch Size)定义了每个批次所使用的样本数量。
我们将采用小批量梯度下降算法。但是通过调整批大小,同样可以实现随机梯度下降和全批量梯度下降。
💡 提示:实践中,批大小通常选择 2 的幂次方(如 16、32、64、128 等)。这不是数学要求,而是因为这样设置能最大化硬件的运行效率。
BATCH_SIZE = 2建模¶
创建数据集时,需要提供批大小作为参数。
dataset = Dataset(BATCH_SIZE)
model = Linear(2, 1)
loss_fn = MSELoss()
optimizer = SGDOptimizer(model.parameters, lr=LEARNING_RATE)训练¶
采用小批量梯度下降后,训练集中的 4 个样本,只需要 2 次迭代。
for i in range(len(dataset)):
feature, label = dataset[i]
optimizer.zero_grad()
prediction = model(feature)
loss = loss_fn(prediction, label)
loss.backward()
optimizer.step()推理¶
模型推理的时候,我们采用了全批量的做法。
dataset.eval()
feature, label = dataset.all()
prediction = model(feature)
print(f'prediction:\t{prediction}')prediction: Tensor([[56.19176426]])
评估¶
loss = loss_fn(prediction, label)
print(f'loss:\t{loss}')loss: Tensor(11839.232164432306)