【动手学深度学习】#2线性神经网络

发布于:2025-03-17 ⋅ 阅读:(14) ⋅ 点赞:(0)

主要参考学习资料:

《动手学深度学习》阿斯顿·张 等 著

【动手学深度学习 PyTorch版】哔哩哔哩@跟李牧学AI

2.1 线性回归

2.1.1 线性回归的基本元素

线性模型

对于存储特征的向量 x \boldsymbol x x、存储权重的向量 w \boldsymbol w w和偏置 b b b,预测结果 y ^ \hat y y^表示为:

y ^ = w ⊤ x + b \hat y=\boldsymbol w^\top\boldsymbol x+b y^=wx+b

该式是输入特征的一个仿射变换,其特点是通过加权和对特征进行线性变换,并通过偏置项进行平移。

n n n个样本的向量 x \boldsymbol x x合并为一个特征集合矩阵 X \boldsymbol X X,其中每一行是一个样本,每一列是一种特征。进而预测值 y ^ \hat{\boldsymbol y} y^可以通过矩阵-向量乘法表示为:

y ^ = X w + b \hat{\boldsymbol y}=\boldsymbol {Xw}+b y^=Xw+b

损失函数

回归问题中最常用的损失函数是平方误差函数。对于样本 i i i的预测值 y ^ ( i ) \hat y^{(i)} y^(i)及其相应的真实标签 y ( i ) y^{(i)} y(i),平方误差为:

l ( i ) ( w , b ) = 1 2 ( y ^ ( i ) − y ( i ) ) \displaystyle l^{(i)}(\boldsymbol w,b)=\frac12(\hat y^{(i)}-y^{(i)}) l(i)(w,b)=21(y^(i)y(i))

常数 1 2 \displaystyle\frac12 21使该式求导后常系数为 1 1 1,形式更简单。

为了度量模型在整个数据集上的预测质量,我们需计算在训练集 n n n个样本上的损失均值:

L ( w , b ) = 1 n ∑ i = 1 n l ( i ) ( w , b ) = 1 2 n ∣ ∣ y − X w − b ∣ ∣ 2 L(\boldsymbol w,b)=\displaystyle\frac1n\sum^n_{i=1}l^{(i)}(\boldsymbol w,b)=\frac1{2n}||\boldsymbol y-\boldsymbol {Xw}-b||^2 L(w,b)=n1i=1nl(i)(w,b)=2n1∣∣yXwb2

在训练模型时,我们希望寻找一组参数 ( w ∗ , b ∗ ) (\boldsymbol w^*,b^*) (w,b)使之最小化:

w ∗ , b ∗ = a r g m i n w , b L ( w , b ) \boldsymbol w^*,b^*=\underset{\boldsymbol w,b}{\mathrm{argmin}}L(\boldsymbol w,b) w,b=w,bargminL(w,b)

解析解

可以用公式表示的解叫做解析解

将偏差加入权重:

X ← [ X , 1 ] \boldsymbol X\leftarrow[\boldsymbol X,1] X[X,1]

w ← [ w b ] \boldsymbol w\leftarrow\begin{bmatrix}\boldsymbol w\\b\end{bmatrix} w[wb]

∣ ∣ y − X w ∣ ∣ 2 ||\boldsymbol y-\boldsymbol{Xw}||^2 ∣∣yXw2关于 w \boldsymbol w w的导数设为 0 0 0,得到解析解:

∂ ∂ w ∣ ∣ y − X w ∣ ∣ 2 = 0 \displaystyle\frac\partial{\partial\boldsymbol w}||\boldsymbol y-\boldsymbol{Xw}||^2=0 w∣∣yXw2=0

w ∗ = ( X ⊤ X ) − 1 X ⊤ y \boldsymbol w^*=(\boldsymbol X^\top\boldsymbol X)^{-1}\boldsymbol X^\top\boldsymbol y w=(XX)1Xy

随机梯度下降

无法得到解析解时,可以使用梯度下降,其最简单的方法是计算损失函数关于模型参数的梯度,将参数沿梯度下降方向以学习率 η \eta η更新。每次更新参数遍历整个数据集很慢,为此通常会在计算更新时随机抽取批量大小 ∣ B ∣ |B| B的一小批样本 B B B,称为小批量随机梯度下降

( w , b ) ← ( w , b ) − η ∣ B ∣ ∑ i ∈ B ∂ ( w , b ) l ( i ) ( w , b ) (\boldsymbol w,b)\leftarrow(\boldsymbol w,b)-\displaystyle\frac\eta{|B|}\sum_{i\in B}\partial_{(\boldsymbol w,b)}l^{(i)}(\boldsymbol w,b) (w,b)(w,b)BηiB(w,b)l(i)(w,b)

批量大小和学习率通常预先手动指定,而不是通过模型训练得到的。这些可以调整但不在训练过程中更新的参数称为超参数调参是选择超参数的过程。

线性回归恰好在整个域中只有一个最小值,但对复杂的模型来说损失平面上通常包含多个最小值。对调参而言,和使模型在训练集上的损失达到最小值相比更难的是在从未见过的数据上实现较小的损失,这一挑战称为泛化

2.1.3 最大似然估计

在线性回归中使用均方误差损失函数的一个理由来自最大似然估计,似然即可能性。

最大似然估计认为,如果从一个分布参数未知的概率分布中随机抽取到一组样本,那么分布参数应该使该组样本在该概率分布下被抽取到的可能性达到最大。

假设在线性回归模型中的观测包含服从正态分布的噪声:

y = w ⊤ x + b + ϵ y=\boldsymbol w^\top\boldsymbol x+b+\epsilon y=wx+b+ϵ

ϵ ∼ N ( 0 , σ 2 ) \epsilon\sim N(0,\sigma^2) ϵN(0,σ2)

那么通过给定的 x \boldsymbol x x观测到特定 y y y的似然为:

P ( y ∣ x ) = 1 2 π σ 2 exp ⁡ ( − 1 2 σ 2 ( y − w ⊤ x − b ) 2 ) P(y|\boldsymbol x)=\displaystyle\frac1{\sqrt{2\pi\sigma^2}}\exp\left(-\frac1{2\sigma^2}(y-\boldsymbol w^\top\boldsymbol x-b)^2\right) P(yx)=2πσ2 1exp(2σ21(ywxb)2)

则整个数据集的似然为:

P ( y ∣ X ) = ∏ i = 1 n P ( y ( i ) ∣ x ( i ) ) P(\boldsymbol y|\boldsymbol X)=\displaystyle\prod^n_{i=1}P(y^{(i)}|\boldsymbol x^{(i)}) P(yX)=i=1nP(y(i)x(i))

参数 w \boldsymbol w w b b b的最优值是使上式最大的值,根据极大似然估计选择的估计量称为极大似然估计量。将上式取负对数(对数简化乘积,负号为了贴合优化过程为最小化的惯例):

− log ⁡ P ( y ∣ X ) = ∑ i = 1 n 1 2 log ⁡ ( 2 π σ 2 ) + 1 2 σ 2 ( y ( i ) − w ⊤ x ( i ) − b ) 2 -\log P(\boldsymbol y|\boldsymbol X)=\displaystyle\sum^n_{i=1}\frac12\log(2\pi\sigma^2)+\frac1{2\sigma^2}(y^{(i)}-\boldsymbol w^\top\boldsymbol x^{(i)}-b)^2 logP(yX)=i=1n21log(2πσ2)+2σ21(y(i)wx(i)b)2

σ \sigma σ看做常数并忽略,保留的部分即均方误差。

2.2 线性回归从零开始实现

导入库:

import random  
import torch  
import matplotlib.pyplot as plt

2.2.1 生成数据集

使用线性模型参数 w = [ 2 , − 3.4 ] ⊤ \boldsymbol w=[2,-3.4]^\top w=[2,3.4] b = 4.2 b=4.2 b=4.2和噪声项 ϵ \epsilon ϵ生成每个样本包含两个特征的数据集及其标签:

def synthetic_data(w, b, num_examples):  
	"""生成 y = Xw + b + ε"""
	#生成元素服从标准正态分布、形状为(num_examples, len(w))的特征矩阵
    X = torch.normal(0, 1, (num_examples, len(w)))  
    #计算线性模型的预测值
    y = torch.matmul(X, w) + b  
    #向y添加均值为0、标准差为0.01的噪声以模拟随机误差
    y += torch.normal(0, 0.01, y.shape)  
    #将特征和转换为列向量的标签返回,-1代表自动计算长度
    return X, y.reshape(-1, 1)
  
#设置真实的权重和偏置
true_w = torch.tensor([2, -3.4])  
true_b = 4.2  
#生成1000个样本
features, labels = synthetic_data(true_w, true_b, 1000)  
  
#生成样本第二个特征和标签的散点图,部分python版本需detach才能转换为numpy
plt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1)  
plt.show()

2.2.2 读取数据集

def data_iter(batch_size, features, labels):  
	"""接收批量大小、特征矩阵和标签向量,生成大小为batch_size的小批量特征和标签"""
	#生成样本的下标列表
    num_examples = len(features)  
    indices = list(range(num_examples))  
    #shuffle函数将下标列表打乱
    random.shuffle(indices)  
    #从0开始抽取batch_size个乱序下标
    for i in range(0, num_examples, batch_size):  
    	#min函数防止索引超出列表
        batch_indices = torch.tensor(indices[i: min(i + batch_size, num_examples)])  
        #yield不断迭代直到返回完所有值
        yield features[batch_indices], labels[batch_indices]  
        
batch_size = 10

2.2.3 初始化模型参数

从标准正态分布中抽样随机数来初始化权重,并将偏置初始化为0,作为模型在训练之前的参数:

w = torch.normal(0, 1, size=(2,1), requires_grad=True)  
b = torch.zeros(1, requires_grad=True)

2.2.4 定义模型

def linreg(X, w, b):  
	"""线性回归模型"""
    return torch.matmul(X, w) + b

2.2.5 定义损失函数

def squared_loss(y_hat, y):  
	"""均方损失"""
    return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2

2.2.6 定义优化算法

def sgd(params, lr, batch_size):  
	"""小批量随机梯度下降"""
	#更新时停止梯度计算
    with torch.no_grad():  
        for param in params:  
        	#每个参数减去学习率乘以损失函数的均值
            param -= lr * param.grad / batch_size  
            #手动清空梯度
            param.grad.zero_()

2.2.7 训练

#设置学习率
lr = 0.03
#设置数据集遍历次数  
num_epochs = 3  
#设置模型和损失函数
net = linreg  
loss = squared_loss  
  
for epoch in range(num_epochs):  
    for X, y in data_iter(batch_size, features, labels):  
    	#小批量损失,l形状(batch_size,1)
        l = loss(net(X, w, b), y)  
        #向量先求和再梯度
        l.sum().backward()  
        #更新权重和偏置
        sgd([w, b], lr, batch_size)  
    #评价模型训练结果
    with torch.no_grad():  
    	#计算整个数据集的损失
        train_l = loss(net(features, w, b), labels)  
        #输出遍历次数和损失均值
        print(f'epoch: {epoch + 1}, loss: {float(train_l.mean()):f}')

运行结果:

epoch: 1, loss: 0.027973
epoch: 2, loss: 0.000102
epoch: 3, loss: 0.000055

2.3 线性回归的简洁实现

使用深度学习框架可以简洁地实现线性回归模型。

2.3.1 生成数据集

import torch  
from torch.utils import data  
#博主没能安装教材配套d2l库,于是将之前写好的synthetic_data函数放进自己的d2l.py并调用
from d2l import synthetic_data  
  
true_w = torch.Tensor([2, -3.4])  
true_b = 4.2  
features, labels = synthetic_data(true_w, true_b, 1000)

2.3.2 读取数据集

def load_array(data_arrays, batch_size, is_train=True):  
	"""构造PyTorch数据迭代器"""
	#TensorDataset方法将数据打包成torch的dataset类
    dataset = data.TensorDataset(*data_arrays)  
    #DataLoader方法对dataset类进行随机抽样,shuffle参数设置是否打乱
    return data.DataLoader(dataset, batch_size, shuffle=is_train)  
  
batch_size = 10  
data_iter = load_array((features, labels), batch_size)

2.3.3 定义模型

#nn包含了大量定义好的神经网络层
from torch import nn  

#Sequential实例能将神经网络层按顺序组织,本次只用到有2个输入维度和1个输出维度单层线性神经网络
net = nn.Sequential(nn.Linear(2, 1))

2.3.4 初始化模型参数

#索引0选中网络中的第一层,weight.data和bias.data方法访问参数,用normal_和fill_方法重写参数
net[0].weight.data.normal_(0, 0.01)  
net[0].bias.data.fill_(0)  

2.3.5 定义损失函数

#均方误差使用MSELoss类
loss = nn.MSELoss()  

2.3.6 定义优化算法

#小批量随机梯度下降使用optim模块的SGD实例,parameter方法获得参数并传入,lr参数设置学习率
trainer = torch.optim.SGD(net.parameters(), lr=0.03)

2.3.7 训练

num_epochs = 3  
for epoch in range(num_epochs):  
    for X, y in data_iter:  
    	#net自带模型参数,无需传入w和b
        l = loss(net(X), y)  
        trainer.zero_grad()  
        #torch损失函数自带求和,无需使用sum方法
        l.backward()  
        #优化器的step方法自动更新模型
        trainer.step()  
    l = loss(net(features), labels)  
    print(f'epoch {epoch + 1}, loss {l:f}')

运行结果:

epoch 1, loss 0.000237
epoch 2, loss 0.000112
epoch 3, loss 0.000111

2.4 softmax回归

2.4.1 分类问题

回归问题关心估计一个连续值,而分类问题预测一个离散类别。

为了将回归问题转化为分类问题,我们先引入一种表示分类数据的简单方法:独热编码

独热编码是一个分量和类别一样多的向量,类别对应的分量设置为 1 1 1,其他所有分量设置为 0 0 0。对于涉及三个类别的分类问题,其标签 y y y将是一个三维向量:

y ∈ { ( 1 , 0 , 0 ) , ( 0 , 1 , 0 ) , ( 0 , 0 , 1 ) } y\in\{(1,0,0),(0,1,0),(0,0,1)\} y{(1,0,0),(0,1,0),(0,0,1)}

2.4.2 网络架构

为了估计所有可能类别的条件概率,我们需要一个有多输出的模型,每个类别对应一个输出,每个输出都有自己的仿射函数。假设我们有 4 4 4个特征和 3 3 3个类别,则需要 12 12 12个权重标量, 3 3 3个偏置标量,进而为每个输入计算 3 3 3个未规范化的预测:

o 1 = x 1 w 11 + x 2 w 12 + x 3 w 13 + x 4 w 14 + b 1 o_1=x_1w_{11}+x_2w_{12}+x_3w_{13}+x_4w_{14}+b_1 o1=x1w11+x2w12+x3w13+x4w14+b1

o 2 = x 1 w 21 + x 2 w 22 + x 3 w 23 + x 4 w 24 + b 2 o_2=x_1w_{21}+x_2w_{22}+x_3w_{23}+x_4w_{24}+b_2 o2=x1w21+x2w22+x3w23+x4w24+b2

o 3 = x 1 w 31 + x 2 w 32 + x 3 w 33 + x 4 w 34 + b 3 o_3=x_1w_{31}+x_2w_{32}+x_3w_{33}+x_4w_{34}+b_3 o3=x1w31+x2w32+x3w33+x4w34+b3

可以用神经网络图来描述该计算过程:

每个输出都取决于所有输入的神经网络,其输出层被称为全连接层

用线性代数更简洁地表达模型:

o = W x + b \boldsymbol o=\boldsymbol{Wx}+\boldsymbol b o=Wx+b

2.4.3 全连接层的参数开销

对于任何具有 d d d个输入和 q q q个输出的全连接层,参数开销为 O ( d q ) O(dq) O(dq),在实践中数字过高。在实际应用中可以灵活指定超参数 n n n使参数开销减少到 O ( d q / n ) O(dq/n) O(dq/n),以便在参数节省和模型有效性之间进行权衡。

2.4.4 softmax运算

我们希望模型的输出 y ^ j \hat y_j y^j可以视为属于类 j j j的概率,并选择具有最大输出值的类别 a r g m a x j y j \mathrm{argmax}_jy_j argmaxjyj作为预测。但未规范化的预测无法直接视为输出,因为我们没有限制这些输出数值的总和为 1 1 1,而且输出可以是负值,违反了概率论基本公理。

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

y ^ = s o f t m a x ( o ) \hat{\boldsymbol y}=\mathrm{softmax}(\boldsymbol o) y^=softmax(o)

其中 y ^ j = exp ⁡ ( o j ) ∑ k exp ⁡ ( o k ) \hat y_j=\displaystyle\frac{\exp(o_j)}{\displaystyle\sum_k\exp(o_k)} y^j=kexp(ok)exp(oj)

softmax不改变预测结果的大小次序,因此仍可以用下式选择最有可能的类别:

a r g m a x j y ^ j = a r g m a x j o j \mathrm{argmax}_j\hat y_j=\mathrm{argmax}_jo_j argmaxjy^j=argmaxjoj

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

2.4.5 小批量样本的向量化

为了提高计算效率并充分利用GPU,我们通常针对小批量样本数据进行向量计算。

对于一个批量的样本 X \boldsymbol X X,softmax回归的向量计算表达式为:

O = X W + b \boldsymbol O=\boldsymbol{XW}+\boldsymbol b O=XW+b

Y ^ = s o f t m a x ( O ) \widehat{\boldsymbol Y}=\mathrm{softmax}(\boldsymbol O) Y =softmax(O)

X \boldsymbol X X的每一行代表一个数据样本,因此softmax运算也对 O \boldsymbol O O按行执行。

2.4.6 损失函数

softmax回归的损失函数为交叉熵损失

l ( y , y ^ ) = − ∑ j = 1 q y j log ⁡ y ^ j l(\boldsymbol y,\hat{\boldsymbol y})=-\displaystyle\sum^q_{j=1}y_j\log\hat y_j l(y,y^)=j=1qyjlogy^j

为了理解它,我们将softmax函数代入:

l ( y , y ^ ) = − ∑ j = 1 q y j exp ⁡ ( o j ) ∑ k = 1 q exp ⁡ ( o k ) l(\boldsymbol y,\hat{\boldsymbol y})=-\displaystyle\sum^q_{j=1}y_j\frac{\exp(o_j)}{\displaystyle\sum^q_{k=1}\exp(o_k)} l(y,y^)=j=1qyjk=1qexp(ok)exp(oj)

= ∑ j = 1 q y j log ⁡ ∑ k = 1 q exp ⁡ ( o k ) − ∑ j = 1 q y j o j =\displaystyle\sum^q_{j=1}y_j\log\sum^q_{k=1}\exp(o_k)-\sum^q_{j=1}y_jo_j =j=1qyjlogk=1qexp(ok)j=1qyjoj

= log ⁡ ∑ k = 1 q exp ⁡ ( o k ) − ∑ j = 1 q y j o j =\displaystyle\log\sum^q_{k=1}\exp(o_k)-\sum^q_{j=1}y_jo_j =logk=1qexp(ok)j=1qyjoj

再对其中一个预测 o j o_j oj求导:

∂ o j l ( y , y ^ ) = exp ⁡ ( o j ) ∑ k = 1 q exp ⁡ ( o k ) − y j = s o f t m a x ( o ) j − y j \displaystyle\partial_{o_j}l(\boldsymbol y,\hat{\boldsymbol y})=\frac{\exp(o_j)}{\displaystyle\sum^q_{k=1}\exp(o_k)}-y_j=\mathrm{softmax}(\boldsymbol o)_j-y_j ojl(y,y^)=k=1qexp(ok)exp(oj)yj=softmax(o)jyj

其导数即softmax模型分配的概率与实际发生的情况之差,符合梯度下降的目标。

2.4.8 模型预测和评估

接下来我们将使用精度评估模型的性能,其等于正确预测数与预测总数的比例。

2.5 图像分类数据集

MINIST数据集是图像分类中广泛使用的数据集之一,但作为基准数据集过于简单。我们将使用类似但更复杂的Fashion-MINIST数据集。

2.5.1 读取数据集

import torch  
import torchvision  
from matplotlib import pyplot as plt  
from torch.utils import data  
from torchvision import transforms  
  
#ToTensor实例将图像数据从PIL类型变换成32位浮点数形式,并除以255使得所有像素的均值为0~1
trans = transforms.ToTensor()  
#下载Fashion-MINIST数据集
minist_train = torchvision.datasets.FashionMNIST(root='./data', train=True, transform=trans, download=True)  
minist_test = torchvision.datasets.FashionMNIST(root='./data', train=False, transform=trans, download=True)  
  
def get_fashion_minist_labels(labels):  
	"""返回Fashion-MINIST数据集的文本标签"""
    text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',  
                   'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']  
    #将数字索引转换为文本名称返回
    return [text_labels[int(i)] for i in labels]  
  
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):  
	"""绘制图像列表"""
	#根据网格的行列数和缩放比计算图像显示大小
    figsize = (num_cols * scale, num_rows * scale)  
    #创建对应行列数和图像大小的网格
    _, axes = plt.subplots(num_rows, num_cols, figsize=figsize)  
    #将二维子图数组展平为一维数组方便遍历
    axes = axes.flatten()  
    #遍历图像并显示,zip将axes和imgs中的元素一一配对并由enumerate获取索引后传给循环变量
    for i, (ax, img) in enumerate(zip(axes, imgs)):  
    	#如果img是张量则转换为numpy数组显示
        if torch.is_tensor(img):  
            ax.imshow(img.numpy())  
        else:  
            ax.imshow(img)  
        #隐藏坐标轴
        ax.axes.get_xaxis().set_visible(False)  
        ax.axes.get_yaxis().set_visible(False)  
        #如果提供了标题则设置标题
        if titles:  
            ax.set_title(titles[i])  
    #返回子图数组
    return axes  
  
#读取前18个样本图像及标签
X, y = next(iter(data.DataLoader(minist_train, batch_size=18)))  
#将图像移除通道维度后显示为2行9列
show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_minist_labels(y))  
plt.show()

显示结果:

2.5.2 读取小批量

batch_size = 256  
  
def get_dataloader_workers(): 
	"""使用4个进程来读取数据""" 
    return 4  

#读取小批量
train_iter = data.DataLoader(minist_train, batch_size, shuffle=True, num_workers=get_dataloader_workers())

2.5.3 整合所有组件

def load_data_fashion_minist(batch_size, resize=None):  
	"""下载Fashion-MINIST数据集并加载到内存中"""
    trans = [transforms.ToTensor()]  
    #resize可选择调整图像大小的目标尺寸以匹配模型输入
    if resize:  
        trans.insert(0, transforms.Resize(resize))  
    trans = transforms.Compose(trans)  
    minist_train = torchvision.datasets.FashionMNIST(root='./data', train=True, transform=trans, download=True)  
    minist_test = torchvision.datasets.FashionMNIST(root='./data', train=False, transform=trans, download=True)  
    return (data.DataLoader(minist_train, batch_size=batch_size, shuffle=True, num_workers=get_dataloader_workers()),  
            data.DataLoader(minist_test, batch_size=batch_size, shuffle=True, num_workers=get_dataloader_workers()))  
  
train_iter, test_iter = load_data_fashion_minist(batch_size)

2.6 softmax回归的从零开始实现

如果未能下载d2l包,请将前文sgd()、get_fashion_minist_labels()、show_images()、get_dataloader_workers()、load_data_fashion_minist()整合进d2l.py。

在从零开始实现中,为了保证函数的泛用性,函数中会考虑诸如模型是否使用了pytorch框架、传入的参数形状是否符合等问题,而仅在该案例中并不会遇到这些多余情况。

import torch  
from matplotlib import pyplot as plt  
from torch.utils import data  
import d2l  
import torchvision  
from torchvision import transforms  

batch_size = 256  
#由于博主的系统设定不支持多进程读取数据,因此使用最初始的读取数据集方法
trans = transforms.ToTensor()  
minist_train = torchvision.datasets.FashionMNIST(root='./data', train=True, transform=trans, download=True)  
minist_test = torchvision.datasets.FashionMNIST(root='./data', train=False, transform=trans, download=True)  
train_iter = data.DataLoader(minist_train, batch_size=batch_size, shuffle=True)  
test_iter = data.DataLoader(minist_test, batch_size=batch_size, shuffle=False)

2.6.1 初始化模型参数

#输入28*28像素
num_inputs = 784  
#输出对10个类别的预测
num_outputs = 10  
#正态分布初始化权重W,偏重初始化为0
W = torch.normal(0, 1, (num_inputs, num_outputs), requires_grad=True)  
b = torch.zeros(num_outputs, requires_grad=True)

2.6.2 定义softmax操作

def softmax(X):  
	#对每一项求幂
    X_exp = torch.exp(X)  
    #对每一行求和
    partition = X_exp.sum(dim=1, keepdim=True)  
    #每一行的项除以所在行的和,使最终每行和为1
    return X_exp / partition

2.6.3 定义模型

def net(X):  
	#将每个原始图像的输入展平为向量后参与模型运算
	#最终reshape为(batch_size, num_inputs)
    return softmax(torch.matmul(X.reshape(-1, W.shape[0]), W) + b)

2.6.4 定义损失函数

def cross_entropy(y_hat, y):  
	#y_hat索引中,range()遍历每个样本的10个类别的预测值,y索引到预测值中正确类别的预测概率
    return - torch.log(y_hat[range(len(y_hat)), y])

2.6.5 分类精度

def accuracy(y_hat, y):  
	"""计算预测正确的样本数"""
	#确保y_hat形状符合要求
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: 
    	#每个样本选择概率最大的类别作为预测值 
        y_hat = y_hat.argmax(axis=1)  
    #记录每个样本预测的正误,type确保参与比较的两者类型一样
    cmp = y_hat.type(y.dtype) == y  
    #将布尔型转换为数值型累加得到预测正确的样本数
    return float(cmp.type(y.dtype).sum())

def evaluate_accuracy(net, data_iter):  
	"""计算在整个数据集上的精度"""
	#如果模型是使用torch搭建的则设为评估模式不计算梯度
    if isinstance(net, torch.nn.Module):  
        net.eval()  
    #对正确预测数和预测总数进行累加
    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]

evaluate_accuracy()函数用到的Accumulator类可以对多个变量进行累加:

class Accumulator:  
    def __init__(self, n):  
        self.data = [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.] * len(self.data)  
  
    def __getitem__(self, idx):  
        return self.data[idx]

2.6.6 训练

由于博主无法下载d2l包,书中绘制图表动画的功能无法实现故略去,训练过程仅使用文字显示。

def train_epoch_ch3(net, train_iter, loss, updater):  
	"""训练模型一轮"""
	#若使用torch搭建的模型则设为训练模式计算梯度
    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)  
        #使用pytorch内置的优化器和损失函数
        if isinstance(updater, torch.optim.Optimizer):  
            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]  
  
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):  
	"""训练模型"""
    for epoch in range(num_epochs):  
        train_metrics = train_epoch_ch3(net, train_iter, loss, updater)  
        test_acc = evaluate_accuracy(net, test_iter)  
        l = train_metrics[0]  
        print(f'epoch {epoch + 1}, loss {l:f}, test accuracy {test_acc * 100:.2f}%')  
    train_loss, train_acc = train_metrics  
  
#学习率
lr = 0.2  
  
#使用小批量随机梯度下降作为优化器
def updater(batch_size):  
    return d2l.sgd([W, b], lr, batch_size)  
  
#设置训练轮数并开始训练
num_epochs = 30  
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)

2.6.7 预测

def predict_ch3(net, test_iter, n=6):  
	"""预测标签"""
    for X, y in test_iter:  
        break  
    #提取真实值和预测值的文字标签
    trues = d2l.get_fashion_minist_labels(y)  
    preds = d2l.get_fashion_minist_labels(net(X).argmax(dim=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])  
    plt.show()  
  
predict_ch3(net, test_iter)

训练过程(最后5轮):

epoch 26, loss 0.620540, test accuracy 80.41%
epoch 27, loss 0.612298, test accuracy 80.41%
epoch 28, loss 0.606352, test accuracy 81.04%
epoch 29, loss 0.599426, test accuracy 78.86%
epoch 30, loss 0.595474, test accuracy 80.38%

预测结果:

2.7 softmax回归的简洁实现

2.7.1 代码

import torch  
from torch import nn  
import d2l  
import torchvision  
from torchvision import transforms  
import torch.utils.data as data  
  
batch_size = 256  
trans = transforms.ToTensor()  
minist_train = torchvision.datasets.FashionMNIST(root='./data', train=True, transform=trans, download=True)  
minist_test = torchvision.datasets.FashionMNIST(root='./data', train=False, transform=trans, download=True)  
train_iter = data.DataLoader(minist_train, batch_size=batch_size, shuffle=True)  
test_iter = data.DataLoader(minist_test, batch_size=batch_size, shuffle=False)  
  
#pytorch不会隐式地调整输入形状,需通过展平层将多维张量展平到二维,其中只保留第0维不变
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)  
  
#交叉熵损失函数
loss = nn.CrossEntropyLoss(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)  
  
d2l.predict_ch3(net, test_iter)

训练过程(最后5轮):

epoch 6, loss 0.473789, test accuracy 83.04%
epoch 7, loss 0.464444, test accuracy 83.16%
epoch 8, loss 0.456795, test accuracy 83.38%
epoch 9, loss 0.452170, test accuracy 83.31%
epoch 10, loss 0.447256, test accuracy 83.39%

预测结果:

2.7.2 重新审视softmax的实现

回顾softmax函数:

y ^ j = exp ⁡ ( o j ) ∑ k exp ⁡ ( o k ) \hat y_j=\displaystyle\frac{\exp(o_j)}{\displaystyle\sum_k\exp(o_k)} y^j=kexp(ok)exp(oj)

由于存在幂运算,当 o j o_j oj非常大时,幂运算的结果可能超出类型表示范围,即上溢,此时无法得到一个明确定义的交叉熵值。解决该问题的一个方法是在进行softmax运算之前先从所有 o k o_k ok中减去 max ⁡ ( o k ) \max(o_k) max(ok),其不会改变softmax的返回值。

但有些 o j − max ⁡ ( o k ) o_j-\max(o_k) ojmax(ok)可能具有较大的负值,由于精度受限,幂运算的结果将有接近零的值,即下溢,这些值最终取对数会得到nan。但softmax中的幂运算在交叉熵中会取对数,因此可以将softmax和交叉熵结合在一起:

log ⁡ ( y ^ i ) = log ⁡ ( exp ⁡ ( o j − max ⁡ ( o k ) ) ∑ k exp ⁡ ( o k − max ⁡ ( o k ) ) ) \log(\hat y_i)=\log\left(\displaystyle\frac{\exp(o_j-\max(o_k))}{\displaystyle\sum_k\exp(o_k-\max(o_k))}\right) log(y^i)=log kexp(okmax(ok))exp(ojmax(ok))

= o j − max ⁡ ( o k ) − log ⁡ ( ∑ k exp ⁡ ( o k − max ⁡ ( o k ) ) ) =o_j-\max(o_k)-\log\left(\displaystyle\sum_k\exp(o_k-\max(o_k))\right) =ojmax(ok)log(kexp(okmax(ok)))