之——跟着佬一步步来
目录
杂谈
2023年10月8日22:59:10
逻辑很简单,但值得学习的是代码的严密框架结构与可视化的功能。
- 定义softmax
- 定义网络结构
- 定义损失函数
- 定义精度计算
- 定义一个batch的反向传播计算
- 定义整套多个epoch训练流程
- 选择优化器开始训练
- 可视化优化
正文
都在码里。
1.模型结构与参数
下面代码展示了模型的基础模块与初始化:
import torch
from IPython import display
#上面的函数已经整合在d2l里了
from d2l import torch as d2l
#一个batch
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size,resize=None)
#对于图像最好的方法当然是卷积神经网络,但在这里做softmax的实验所以简单地拉成一条向量
num_inputs = 28*28
num_outputs = 10
#定义权重和偏差,单层神经网络
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)
def softmax(X):
#对于输出层的结果[1,0.1,0.1……*10]、[1,0.1,0.1……*10]
#每个元素
X_exp = torch.exp(X)
partition = X_exp.sum(1, keepdim=True)
return X_exp / partition # 这里应用了广播机制,第i行除以第i行求和,那么最后返回的就是一个batchsize的列向量
#定义网络结构
def net(X):
#每一个batch的X reshape成为批量大小*W第一维度大小的向量,其实就是num_inputs,这种写法强调了网络参数的关联性,得出来是一个
y=softmax(torch.matmul(X.reshape(-1,W.shape[0]),W)+b) #最后结果是256*10 也就是一个batch256个样本的10类物品预测概率,加b是依靠广播机制
return y
#定义交叉熵损失函数,将y_hat下标对应的真实值取出来计算损失函数
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])
2.精度计算工具
这里自行定义了单一epoch的精度计算与多epoch的累计精度计算:
#对于一个batch的分类精度计算
def accuracy(y_hat, y):
"""计算预测正确的数量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
#取列最大的下标
y_hat = y_hat.argmax(axis=1)
#判断是否相等,转为bool类型
cmp = y_hat.type(y.dtype) == y
#返回正确个数
return float(cmp.type(y.dtype).sum())
#对于所有batch的正确率累计
def evaluate_accuracy(net, data_iter): #@save
"""计算在指定数据集上模型的精度"""
if isinstance(net, torch.nn.Module):
net.eval() # 将模型设置为评估模式,这是一个很好的习惯,因为它如果是nnmodule的话通常会在训练中调用dropout和批量归一化,评估模式则不会
print("evaled")
metric = Accumulator(2) # 正确预测数、预测总数
with torch.no_grad():
for X, y in data_iter:
#每一次的正确个数与总个数
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
# 创建一个名为Accumulator的类,用于在n个变量上进行累加操作
class Accumulator:
"""在n个变量上累加"""
# 初始化累加器对象,其中n是要存储的变量数量
def __init__(self, n):
# 创建一个名为data的列表,其中包含n个元素,每个元素都初始化为0.0
self.data = [0.0] * n
# 将一组值添加到累加器中
def add(self, *args):
# 使用列表推导式将每个参数的值与累加器中相应位置的元素相加,并将结果存储回data列表,zip方法使得args可以不用限制数量,最终会以最短的迭代对象为准
self.data = [a + float(b) for a, b in zip(self.data, args)]
# 重置累加器,将data列表中的所有元素重新设置为0.0
def reset(self):
self.data = [0.0] * len(self.data)
# 通过索引访问累加器中的元素
def __getitem__(self, idx):
return self.data[idx]
值得注意的是其中的评估模式的设置,这里我们自己定义的net所以用不到,但养成习惯以后就会用到,这是训练过程中评估模型的必要条件。
当使用深度学习框架(如PyTorch)训练和使用神经网络时,有两个主要模式:训练模式和评估模式。这两种模式之间的差异在于如何处理一些特定的操作,例如dropout和批量归一化。让我更详细地介绍一下为什么需要将神经网络切换到评估模式以及代码的作用:
训练模式(Training Mode):
- 在训练模型时,神经网络需要学习权重和调整模型参数以最小化损失函数。在这个过程中,通常会使用技巧如dropout(随机失活)和批量归一化(Batch Normalization)来帮助模型收敛。
- Dropout是一种正则化技术,它在训练期间随机丢弃神经元的输出,以减少过拟合。
- 批量归一化是一种用于规范神经网络中输入数据分布的技术,有助于提高训练的稳定性和速度。
评估模式(Evaluation Mode):
- 在模型部署和进行推断(inference)时,我们通常不需要或不希望应用dropout或批量归一化等训练中的技巧。
- 在评估模式下,模型会保持不变,不会执行dropout操作,而且批量归一化层的统计信息(例如均值和方差)通常是固定的,而不是根据每个小批次的数据动态计算。
- 这样做是为了确保在推断时模型的输出是一致的,可重复的,不会受到训练时的随机性影响。
其次,Accumulator类也是很好的封装,起到储存器的作用:
__init__(self, n)
:初始化累加器对象,其中n
是累加器中要存储的变量数量。在初始化时,创建一个长度为n
的列表,所有元素都被初始化为0.0。
add(self, *args)
:这个方法用于将一组值添加到累加器中。你可以传递不定数量的参数给这个方法,然后它将每个参数的值与累加器中相应位置的元素相加。这里使用了zip
函数来同时迭代参数和累加器的元素,确保它们能够按位置一一对应。
reset(self)
:这个方法用于重置累加器,将所有的元素重新设置为0.0。这可以在你需要重新开始累加操作时使用。
__getitem__(self, idx)
:这是一个特殊方法,允许你通过索引访问累加器中的元素。传递一个索引idx
,它将返回累加器中相应位置的元素值。这个累加器类的主要作用是方便地在多个变量上执行累加操作,将多个值按位置相加,并提供了重置和访问元素的功能。这在某些统计或累积值的应用中可能非常有用,例如计算某些统计量或跟踪多个变量的累积总和。
__getitem__
方法的定义允许你在外部使用索引来访问对象的元素。当你定义了这个方法后,你的对象就变成可索引的,就像列表或其他内置序列类型一样。
add
方法的设计允许你传递不定数量的参数,而不仅仅是3个参数。这是因为*args
在Python中表示可变数量的参数,你可以传递任意数量的参数给这个方法,它会自动处理。无论你传递多少个参数,
zip
函数都会将这些参数与累加器对象中的元素按位置一一对应地相加。这使得累加器更加灵活,可以应对不同数量的变量。
zip
函数本身不能避免参数数量不匹配的错误。它只是将多个可迭代对象按位置一一对应地组合在一起,但不会检查这些可迭代对象的长度是否相同。因此,如果你尝试将不同长度的可迭代对象传递给zip
函数,它将以最短的可迭代对象为准,忽略多余的部分
3.单次迭代反向传播
一个epoch的计算:
def train_epoch_ch3(net, train_iter, loss, updater): #@save
"""训练模型一个迭代周期(定义见第3章)"""
# 将模型设置为训练模式
if isinstance(net, torch.nn.Module):
net.train()
# 训练损失总和、训练准确度总和、样本数
metric = Accumulator(3)
for X, y in train_iter:
# 计算梯度并更新参数
y_hat = net(X)
l = loss(y_hat, y)
if isinstance(updater, torch.optim.Optimizer):
# 使用PyTorch内置的优化器和损失函数
updater.zero_grad()
l.mean().backward()
updater.step()
else:
# 使用定制的优化器和损失函数,反向传播计算梯度
l.sum().backward()
#优化器,传入batchsize
updater(X.shape[0])
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# 返回训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2]
4.整体训练函数
#训练函数
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):
"""训练模型(定义见第3章)"""
animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)
animator.add(epoch + 1, train_metrics + (test_acc,))
train_loss, train_acc = train_metrics
print(train_loss, train_acc)
#最终问题警告断言
assert train_loss < 0.5, train_loss
assert train_acc <= 1 and train_acc > 0.7, train_acc
assert test_acc <= 1 and test_acc > 0.7, test_acc
在 Python 中,
assert
关键字用于设置断言,这是一种调试辅助手段。当某个条件为False
时,assert
会触发一个AssertionError
异常。断言常用于在开发过程中捕获预期之外的条件,确保代码的某些部分按预期运行。断言并不应该用作程序的正常异常处理,因为它主要用于开发和测试,不应在生产环境中使用。在生产代码中,断言可以被全局地关闭。
其中用到的动画展示:
#动画展示类别
class Animator:
"""在动画中绘制数据"""
def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
figsize=(3.5, 2.5)):
# 增量地绘制多条线
if legend is None:
legend = []
d2l.use_svg_display()
self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)
if nrows * ncols == 1:
self.axes = [self.axes, ]
# 使用lambda函数捕获参数
self.config_axes = lambda: d2l.set_axes(
self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
self.X, self.Y, self.fmts = None, None, fmts
def add(self, x, y):
# 向图表中添加多个数据点
if not hasattr(y, "__len__"):
y = [y]
n = len(y)
if not hasattr(x, "__len__"):
x = [x] * n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
self.axes[0].cla()
for x, y, fmt in zip(self.X, self.Y, self.fmts):
self.axes[0].plot(x, y, fmt)
self.config_axes()
display.display(self.fig)
display.clear_output(wait=True)
5.执行训练
#定义updater
lr = 0.1
def updater(batch_size):
return d2l.sgd([W, b], lr, batch_size)
num_epochs = 20
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
其中一次的结果 :
6.简化版本
简化了参数的定义,net改为用模块搭建;
简化了损失函数与优化算法的定义;
其他还是使用的上述自定义函数,但都使用的是其与官方匹配的代码分支。
#简洁实现
import torch
from torch import nn
from d2l import torch as d2l
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
#初始化权重参数,m是当前layer
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight,mean=0, std=0.01)
#应用到net,也即每一层都用一下
net.apply(init_weights);
#交叉熵损失函数
loss = nn.CrossEntropyLoss(reduction='none') #将 reduction 设置为 'none',意味着不对损失进行任何汇总或平均操作,而是为每个样本返回一个独立的损失值。
#优化算法
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
#开启训练
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
nn.CrossEntropyLoss
是一个用于多类别分类任务的损失函数,通常用于深度学习中的神经网络训练。在多类别分类任务中,模型的输出是一个概率分布,表示每个类别的概率,而目标是选择具有最高概率的类别作为预测结果。
reduction
参数用于控制损失函数的计算方式。在你的代码中,将reduction
设置为'none'
,意味着不对损失进行任何汇总或平均操作,而是为每个样本返回一个独立的损失值。这意味着,如果你有一个批次(batch)中的多个样本,对于每个样本
nn.CrossEntropyLoss
都会计算一个损失值,并将这些损失值存储在一个张量中。这对于某些特定的任务或需要进一步处理每个样本的情况很有用。
最终结果类似:
补充
1.softlabel策略
对于one-hot分类编码方式的一种改进,不在用1而是用0.9表示正确,用0.1而非0表示不正确,这样可以使得指数拟合过程中避免出现指数无限大和无限接近0的情况,防止出现数字极端灾难。
"Soft label" 训练策略是一种在深度学习中用于改进模型性能和训练稳定性的技巧。它涉及到使用比传统的 "one-hot" 类别标签更平滑的标签分布进行训练。这种策略的主要目标是降低模型对训练数据的过拟合,提高泛化能力,并改善模型在困难样本上的表现。
以下是一些关于使用 soft label 训练策略的重要点和方法:
Softmax 激活函数的输出:传统的分类任务中,使用 one-hot 编码的标签,例如
[1, 0, 0]
表示类别 1。而在 soft label 训练中,可以使用类别概率分布,例如[0.9, 0.05, 0.05]
,这表示模型对类别 1 有 90% 的置信度,同时对其他类别也有一些小的置信度。温度参数 (Temperature Scaling):在 soft label 训练中,通常会使用一个称为 "温度参数" 的超参数来控制输出概率的平滑程度。较高的温度值会导致更平滑的概率分布,而较低的温度值则会更加尖锐。通过调整温度参数,可以平衡模型的训练和泛化性能。
交叉熵损失函数 (Cross-Entropy Loss):通常,你会使用交叉熵损失函数来训练 soft label 模型。与传统的 one-hot 编码标签一起使用时,这个损失函数可以度量模型输出的概率分布与真实标签之间的差距。
数据生成:生成 soft label 数据通常需要一些额外的处理步骤。你可以使用平滑函数,如 softmax 函数,来生成 soft label。还可以通过集成技术,如投票集成或平均集成,来汇总多个模型的输出,生成平滑的标签分布。
正则化效果:soft label 训练通常具有正则化效果,有助于减少模型的过拟合。这是因为 soft label 鼓励模型学习更平滑的决策边界,而不是过于尖锐的边界。
应用领域:soft label 训练策略在许多领域都有应用,包括图像分类、自然语言处理、推荐系统等。它特别适用于那些标签噪声较大或类别不平衡的任务。
总之,soft label 训练策略是一种有助于改进模型性能和泛化能力的技术,特别适用于复杂的分类任务,其中样本的标签分布可能不够明确或存在噪声。通过平滑标签分布,模型更容易学习到数据的潜在模式,提高了其性能和稳定性。
2.再一次理解交叉熵
#定义交叉熵损失函数,将y_hat下标对应的真实值取出来计算损失函数,因为y为1就不用再前面乘了
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])
# 比如四个样本真实标签分别是[0,0,2,2],而他们每一个样本对于三类结果的输出概率分别为
# [0.6,0.2,0.2]、[0.7,0.2,0.1]、[0.1,0.1,0.6]、[0.2,0.2,0.6],
# 这个函数就会根据真实类别将那个对应类的预测概率取出来做-log,也就是0.6、0.7、0.6、0.6
由于softmax函数在前面计算中已经融入了所有类别维度的输出信息(分母是所有输出节点的求和),独热编码与交叉熵的配合使用就可以让我们在计算损失时候只关注当前状态在对标真实类别时候的损失。
此外,交叉熵只考虑当前类别的预测情况,并不关心别的类的结果,所以也一定程度上避免了类别不平衡。
3.logistic回归
由于我们的softmax回归就是logistic回归从二分类到多分类的一个推广,logistic回归就是softmax的一个特例,softmax函数完成了概率归一化与多分类扩展,所以,没有做详细介绍。下面用对比的方式小计一下。
方法 | logistic | softmax |
激活函数 | sigmoid(WX+b) | softmax(WX+b) |
分类 | 0/1二分类 | one-hot多分类 |
损失函数 | 交叉熵(二分类) | 交叉熵 |
4.理解似然函数
似然函数是指当前状态下模型空间中找到合理模型的概率,所以可以理解为最小化损失函数就可以最大化似然函数,最大可能找到正确的模型。