0%

深度学习第二篇——线性神经网络

本次讲述softamx回归

softmax回归

回归:

  • 单连续数值输出
  • 自然区间R
  • 跟真实值的区别作为损失

分类

  • 通常多个输出
  • 输出i是预测为第i类的置信度

无校验比例

  • 对每个类别进行一位有效编码

  • 最大值为预测

  • 需要更置信的识别正确类(大余量)

    分类数据的简单方法:独热编码。独热编码是一个向量,它的分量和类别一样多,如一个三维分量,其中(1,0,0)对应猫,(0,1,0)对应鸡,(0,0,1)对应狗。

    网络结构:

    我们有4个特征和3个可能的输出类别,我们将需要12个标量来表示权重(带下标的w),3个标量表示偏置,3个未规范化的预测:$o_1$、$o_2$和$o_3$。

    softmax回归是一个单层神经网络。

    softmax函数能将未规范化的预测变换未非负数并且总和为1,同时让模型保持可导的性质。

    尽管softmax是一个非线性函数,但softmax回归输出仍然由输入特征的仿射变换决定,因此,softmax回归是一个线性模型。

这里,对于所有的$j$总有$0{\leq}{\hat{y}_j}{\leq}1$。因此,${\hat{y}}$可以视为一个正确的概率分布。softmax运算不会改变未规范化的预测$o$之间的大小次序,只会确定分配给每个类别的概率。因此在预测过程中,我们仍然可以用下式来选择最有可能的类别:

导包:

1
2
3
4
5
6
7
8
9
10
%matplotlib inline
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2l

d2l.use_svg_display()
#The purpose of d2l.use_svg_display() is to set up the notebook environment to use Scalable Vector Graphics (SVG) for displaying images.
#By using SVG for display, you can achieve better image quality and scalability in your Jupyter notebook when working with visualizations provided by the D2L library.

读取数据集

1
2
3
4
5
#读取数据集
#通过ToTensor实例将图像数据从PIL类型变换成32位浮点数形式。
trans = transforms.ToTensor()
mnist_train = torchvision.datasets.FashionMNIST(root = "../data",train = True,transform=trans,download=True)
mnist_test = torchvision.datasets.FashionMNIST(root = "../data",train = False,transform=trans,download=True)
1
2
3
4
def get_fashion_mnist_labels(labels):#@save
#返回Fashion-mnist数据集的标签
text_labels = ['t-shirt','trouser','pullover','dress','coat','sandal','shirt','sneaker','bag','ankle boot']
return [text_labels[int(i)] for i in labels]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#可视化样本
def show_images(imgs,num_rows,num_cols,titles=None,scale = 1.5):#@save
figsize = (num_rows*scale,num_cols*scale)
_,axes = d2l.plt.subplots(num_rows,num_cols,figsize=figsize)
axes = axes.flatten()#将axes扁平化
for i,(ax,img) in enumerate(zip(axes,imgs)): #这个循环通过 enumerate 函数迭代处理每个子图 (ax) 和对应的图像 (img)。
if torch.is_tensor(img):#检查图像是否是 PyTorch 张量
#图像张量
ax.imshow(img.numpy())#将 PyTorch 张量转换为 NumPy 数组,并使用 ax.imshow() 在子图上显示图像
else:
#PIL图像
ax.imshow(img)#直接使用 ax.imshow() 在子图上显示 PIL 图像
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)#将 x 和 y 轴的刻度设置为不可见,以提供更干净的显示效果。
if titles:#检查是否提供了标题
ax.set_title(titles[i])#如果提供了标题,则将每个子图的标题设置为对应的标题。
return axes #返回包含所有子图 axes 的数组
1
2
x,y = next(iter(data.DataLoader(mnist_train,batch_size=18)))#从 MNIST 训练集中获得了一个包含18个图像(x)及其对应标签(y)的批次数据
show_images(x.reshape(18,28,28),2,9,titles=get_fashion_mnist_labels(y));
1
2
3
4
5
6
#读取小批量
batch_size = 256
def get_dataloader_workers():#@save
"使用4个进程读取数据"
return 4
train_iter = data.DataLoader(mnist_train,batch_size,shuffle=True,num_workers=get_dataloader_workers())
1
2
3
4
5
#训练数据所需要的时间
timer = d2l.Timer()
for x,y in train_iter:
continue
f'{timer.stop():.2f} sec'
1
2
3
4
5
6
7
8
9
10
#整合以上函数
def load_data_fashion_mnist(batch_size,resize=None):
trans = [transforms.ToTensor()]
if resize:
trans.insert(0,transforms.Resize(resize))
trans = transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST("../data",train=True,transform=trans,download=True)
mnist_test = torchvision.datasets.FashionMNIST("../data",train=False,transform=trans,download=True)
return (data.DataLoader(mnist_train,batch_size,shuffle=True,num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test,batch_size,shuffle=True,num_workers=get_dataloader_workers()))
1
2
3
4
5
#效果展示
train_iter,test_iter = load_data_fashion_mnist(32,resize=64)
for x,y in train_iter:
print(x.shape,x.dtype,y.shape,y.dtype)
break

softmax回归的从零开始实现

初始化模型参数

1
2
3
4
#softmax回归从零开始实现
import torch
from IPython import display
from d2l import torch as d2l
1
2
batch_size = 256 #设置数据迭代器的批量大小
train_iter,test_iter = d2l.load_data_fashion_mnist(batch_size)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#初始化参数模型
#这里的每个样本都将用固定长度的向量表示,原始数据集中的每个样本都是28*28像素的图像。
#本次将展平每张图像,把它们看作长度为784的向量。
#我们暂时只把每个像素位置看作一个特征
#我们的数据有10个类别,所以网络输出维度为10。
#因此,权重将构成一个784*10的矩阵,偏置将构成一个1*10的行向量。
#我们将使用正太分布初始化权重w,偏置初始化为0
num_inputs = 784
num_outputs = 10
W = torch.normal(0,0.01,size=(num_inputs,num_outputs),requires_grad=True)
#torch.normal(0, 0.01, size=(num_inputs, num_outputs)):
#这创建了一个大小为 (num_inputs, num_outputs) 的张量,其中每个元素都是从均值为 0、标准差为 0.01 的正态分布中随机抽取的
#requires_grad=True:这指定对涉及此张量的操作进行跟踪,以便进行自动微分,这在神经网络的反向传播过程中是必要的
#在神经网络的上下文中,W 通常用作连接输入层与输出层的权重矩阵。随机初始化有助于打破对称性,并允许网络在训练过程中学习有意义的表示。
b = torch.zeros(num_outputs,requires_grad=True) #创建一个大小为num_outputs的张量,所有元素都被初始化为0

定义softmax操作

1
2
X = torch.tensor([[1.0,2.0,3.0],[4.0,5.0,6.0]])#创建了一个包含两行三列的2D张量 X。
X.sum(0,keepdim=True),X.sum(1,keepdim=True)#沿着列(维度0)计算和,同时保持维度。沿着行(维度1)计算和,同时保持维度。

实现softmax由以下3个步骤组成:

  • 对每个项求幂(使用exp)
  • 对每一行求和(小批量中的每个样本是一行),得到每个样本的规范化常数
  • 将每一行除以其规范化常数,确保结果的和为1

回顾一下公式:

分母或规范化常数有时也称为配分函数(其对数称为对数-配分函数)。该名称来自统计物理学中一个模拟粒子群分布的方程。

1
2
3
4
def softmax(X):
X_exp = torch.exp(X)#对每一项求幂
partition = X_exp.sum(1,keepdim=True)#对每一行求和,得到每个样本的规范化常数
return X_exp / partition #这里应用了广播机制

上述代码,对于任何随机输入,我们将每个元素转变成一个非负数。

定义模型

定义输入如何通过网络映射到输出

1
2
def net(X):
return softmax(torch.matmul(X.reshape((-1,W.shape[0])),W ) + b)

定义损失函数

引入交叉熵损失函数,交叉熵采用实际标签的预测概率的负对数似然。这里我们不使用python的for循环迭代预测(这往往是低效的),而是通过一个运算符选择所有元素。下面,我们创建一个数据样本y_hat,其中包含2个样本在3个类别上的预测概率,以及它们对应的标签y。有了y,我们知道在第一个样本中,第一个类别是正确的预测;而在第二个样本中,第三个类别是正确的预测。然后使用y作为y_hat中的概率索引,我们选择第一个样本中第一个类别的概率和第二个样本中第三个类别的概率

1
2
3
y = torch.tensor([0,2])
y_hat = torch.tensor([[0.1,0.3,0.6],[0.3,0.2,0.5]])
y_hat[[0,1],y]#选择第一个样本中第一个类别的概率和第二个样本中第三个类别的概率

现在完成交叉熵损失函数

1
2
3
def cross_entropy(y_hat,y):
return - torch.log(y_hat[range(len(y_hat)),y])
cross_entropy(y_hat,y)

分类精度

给定预测概率分布y_hat,当我们必须输出硬预测时,我们通常选择预测概率最高的类别。当预测与分类标签y一致时是正确的。分类精度是正确预测数与预测总数之比。

为了计算精度,我们执行以下操作。首先,如果y_hat是矩阵,那么假定第二个维度存储每个类别的预测分数。我们使用argmax获得每行中最大元素的索引来获得预测类别。然后我们将预测类别与真实的y元素进行比较。由于等式运算符“==”对数据类型很敏感,因此我们将y_hat的数据类型转换为与y的数据类型一致。结果是一个包含0(错)和1(对)的张量。最后,我们求和会得到预测正确的数量。

1
2
3
4
5
6
7
def accuracy(y_hat,y):
"""计算预测正确的数量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1)#使用argmax获得每行中最大元素的索引来获得预测类别
cmp = y_hat.type(y.dtype) == y
return float(cmp.type(y.dtype).sum())
accuracy(y_hat,y) / len(y)

我们将继续使用之前的定义的变量y_hat和y分别作为预测的概率分布和标签。可以看到,第一个样本的预测类别是2(该行的最大元素为0.6,索引为2),这与实际标签0不一致。第二个样本的预测类别是2(该行的最大元素为0.5,索引为2),这与实际标签2一致。因此这两个样本的分类精度为0.5。

同样对于任意数据迭代器data_iter可访问的数据集,我们可以评估在任意模型net上的精度。

1
2
3
4
5
6
7
8
9
10
11
12
13
def evaluate_accuracy(net,data_iter): #@save
"""计算在指定数据集上模型的精度"""
##isinstance 是 Python 中的一个内置函数,用于检查一个对象是否是指定类或类型的实例。
if isinstance(net,torch.nn.Module): #用于检查变量 net 是否是 PyTorch 中的 torch.nn.Module 类的实例
net.eval() #这一行将模型切换到评估模式。在评估模式下,模型不会计算梯度,这对于推断是很有用的。
metric = Accumulator(2) #正确预测数、预测总数.创建了一个累加器,用于存储两个值,分别是正确预测数和总预测数。
# 使用 torch.no_grad() 禁用梯度计算
with torch.no_grad():
for X,y in data_iter:
# 计算当前批次的预测结果
prediction = net(X)
metric.add(accuracy(prediction,y),y.numel())#将当前批次的准确度和样本数量添加到累加器中
return metric[0] / metric[1] #返回模型在整个数据集上的准确度,即累加器中的正确预测数除以总预测数

这里定义一个使用程序类Accumulator,用于对多个变量进行累加。在上面的evaluate_accuracy函数中,我们在Accumulator实例中创建了两个变量,分别存储正确预测数和预测总数。当我们遍历数据集时,两者都将随着时间的推移而累加。

1
2
3
4
5
6
7
8
9
10
11
12
class Accumulator: #@save
"""在n个变量上累加"""
def __init__(self,n):
self.data = [0.0] * n

def add(self,*args):
self.data = [a + float(b) for a,b in zip(self.data,args)]
def reset(self):
self.data = [0.0] * len(self.data)

def __getitem__(self,idx):
return self.data[idx]

由于我们使用的随机权重初始化,所以模型精度应接近于随机猜测,如若有十个类别,情况下,精度趋向于0.1

1
evaluate_accuracy(net,test_iter)

训练

首先注意updater是更新模型参数的常用函数,它接收批量大小作为参数。它可以是d2l.sgd函数,也可以是框架的内置优化函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def train_epoch_ch3(net,train_iter,loss,updater): #@save
"""训练模型一轮"""
#将模型设置为训练模式
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()
updater(X.shape[0])
metric.add(float(l.sum()),accuracy(y_hat,y),y.numel())
#返回训练损失和训练精度
return metric[0] / metric[2],metric[1] / metric[2]

定义一个在动画中绘制图表的实用程序类Animator,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Animator: #@save
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 可以通过 len(y) 来获取长度,那么返回 True,否则返回 False
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)

接下来我们实现一个训练函数,它会在train_iter访问的训练数据集上训练一个模型net。该训练函数将会运行多轮(由num_epochs指定)。在每轮结束时,利用test_iter访问的测试数据集对模型进行评估。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def train_ch3(net,train_iter,test_iter,loss,num_epochs,updater):#@save
"""训练模型"""
# 创建动画器,用于实时动态绘制图表
animator = Animator(xlabel='epoch',xlim=[1,num_epochs],ylim=[0.3,0.9],
legend=['train loss','train acc','test acc'])
# 遍历每个 epoch
for epoch in range(num_epochs):
# 使用 train_epoch_ch3 函数进行一轮训练,并获取训练指标
train_metrics = train_epoch_ch3(net,train_iter,loss,updater)
# 使用 evaluate_accuracy 函数计算在测试集上的准确度
test_acc = evaluate_accuracy(net,test_iter)
# 将当前 epoch 的训练损失、训练准确度和测试准确度添加到动画器中
animator.add(epoch+1,train_metrics + (test_acc,))
# 获取最后一轮训练的损失和准确度
train_loss,train_acc = train_metrics
# 断言用于验证训练和测试的准确度在合理范围内
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
1
2
3
4
5
lr = 0.1
def updater(batch_size):
return d2l.sgd([W,b],lr,batch_size)
num_epochs = 10
train_ch3(net,train_iter,test_iter,cross_entropy,num_epochs,updater)

预测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def predict_ch3(net,test_iter,n=6):#@save
"""预测标签"""
# 从测试集中获取一个小批量数据
for X,y in test_iter:
break
# 获取真实标签
trues = d2l.get_fashion_mnist_labels(y)
# 使用模型进行预测,并将预测结果转换为标签
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
# 生成图像标题,包括真实标签和预测标签
titles = [true + '\n' + pred for true,pred in zip(trues,preds)]
# 可视化预测结果
d2l.show_images(
X[0:n].reshape((n,28,28)),1,n,titles = titles[0:n])
predict_ch3(net,test_iter)

softmax回归简洁实现

1
from torch import nn
1
2
batch_size = 256
train_iter,test_iter = d2l.load_data_fashion_mnist(batch_size)
1
2
3
4
5
net = nn.Sequential(nn.Flatten(),nn.Linear(784,10))
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight,std=0.01)
net.apply(init_weights)
1
loss = nn.CrossEntropyLoss(reduction='none')
1
trainer = torch.optim.SGD(net.parameters(),lr=0.1)
1
2
num_epochs = 10
d2l.train_ch3(net,train_iter,test_iter,loss,num_epochs,trainer)

课后问题

增加轮数,为什么测试精度会在一段时间后降低?我们怎么解决这个问题?

增加迭代周期的数量可能会导致过拟合,从而导致测试精度下降。具体来说,当我们增加迭代周期的数量时,模型可能会开始学习到一些只能满足训练样本的非共性特征(这些更多是一种偶然性特征,不适用于测试样本),从而导致过拟合。为了解决这个问题,可以使用早停技术或正则化技术。早停技术是指在模型出现过拟合时(测试集表现开始下降)停止训练。正则化技术是指通过向损失函数添加惩罚项来限制模型参数的大小,从而减少过拟合。

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道