《动手学深度学习》笔记(三)

EliorFoy Lv3

(一)线性回归

1.模型

线性回归假设输出与各个输入之间是线性关系,下面是一个简单的线性回归模型:

y^=x1w1+x2w2+b\hat{y} = x_1w_1+x_2w_2+b

2.损失函数

索引为i的样本误差:

(i)(w1,w2,b)=12(y^(i)y(i))2\ell^{(i)}(w_1,w_2,b)=\frac{1}{2}\Big(\hat{y}^{(i)}-y^{(i)}\Big)^2

常数12\frac{1}{2}使对平方项求导之后常数项系数为1.
样本误差的平均衡量模型预测的质量(衡量误差的函数成为损失函数):

(w1,w2,b)=1ni=1n(i)(w1,w2,b)=1ni=1n12(x1(i)w1+x2(i)w2+by(i))2\ell(w_1,w_2,b)=\frac1n\sum_{i=1}^n\ell^{(i)}(w_1,w_2,b)=\frac1n\sum_{i=1}^n\frac12\Big(x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)}\Big)^2

3.优化算法

小批量随机梯度下降进行优化:

w1w1ηBiB(i)(w1,w2,b)w1=w1ηBiBx1(i)(x1(i)w1+x2(i)w2+by(i))w2w2ηBiB(i)(w1,w2,b)w2=w2ηBiBx2(i)(x1(i)w1+x2(i)w2+by(i))bbηBiB(i)(w1,w2,b)b=bηBiB(x1(i)w1+x2(i)w2+by(i))\begin{gathered} w_{1}\leftarrow w_{1}-\frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\frac{\partial\ell^{(i)}(w_{1},w_{2},b)}{\partial w_{1}}=w_{1}-\frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}}x_{1}^{(i)}\left(x_{1}^{(i)}w_{1}+x_{2}^{(i)}w_{2}+b-y^{(i)}\right) \\ w_{2}\leftarrow w_{2}-\frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\frac{\partial\ell^{(i)}(w_{1},w_{2},b)}{\partial w_{2}}=w_{2}-\frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}}x_{2}^{(i)}\left(x_{1}^{(i)}w_{1}+x_{2}^{(i)}w_{2}+b-y^{(i)}\right)\\ b\leftarrow b-\frac\eta{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\frac{\partial\ell^{(i)}(w_{1},w_{2},b)}{\partial b}=b-\frac\eta{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\left(x_{1}^{(i)}w_{1}+x_{2}^{(i)}w_{2}+b-y^{(i)}\right) \end{gathered}

B\mathcal{B}表示每个小批量中的样本数,η\eta表示学习率,批量大小和学习率的值通常是手动预先指定(称为超参数).可以由表达式看出,就是不断迭代减去损失函数的对于三个参数的偏导数的平均.
至于为什么使用梯度下降算法而不是直接令导数为0求解,可参看这篇回答 :

不是所有的函数都可以根据导数求出取得0值的点的, 现实的情况可能是:
1.可以求出导数在每个点的值, 但是直接解方程解不出来, 比如一些简单的神经网络
2.导数没有解析解, 像一个黑匣子一样, 给定输入值, 可以返回输出值, 但是具体里面是什么情况, 搞不清楚, 工程上似乎有这种情况
以上两种就不能直接令导数为0求解.
牛顿迭代和梯度下降法都可以计算极值, 区别在于, 梯度下降法的算法复杂度低一些, 但是迭代次数多一些; 牛顿迭代法计算的更快(初值必须设置的很合理), 但是牛顿迭代法因为有"除法"参与(对矩阵来说就是求逆矩阵), 所以每一步迭代计算量很大. 一般会根据具体的情况取舍.

4.矢量表示

广义上,当数据样本为n,表达式变为:

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

损失函数变为:

(θ)=12n(y^y)(y^y)\ell(\boldsymbol{\theta})=\frac{1}{2n}(\boldsymbol{\hat{y}}-\boldsymbol{y})^\top(\boldsymbol{\hat{y}}-\boldsymbol{y})

迭代步骤变成:

θθηBiBθ(i)(θ)\theta\leftarrow\theta-\frac{\eta}{|\mathcal{B}|}\sum_{i\in\mathcal{B}}\nabla_{\boldsymbol{\theta}}\ell^{(i)}(\boldsymbol{\theta})

θ(i)(θ)=[(i)(w1,w2,b)w1(i)(w1,w2,b)w2(i)(w1,w2,b)b]=[x1(i)(x1(i)w1+x2(i)w2+by(i))x2(i)(x1(i)w1+x2(i)w2+by(i))x1(i)w1+x2(i)w2+by(i)]=[x1(i)x2(i)1](y^(i)y(i))\nabla_{\boldsymbol{\theta}}\ell^{(i)}(\boldsymbol{\theta})=\begin{bmatrix}\frac{\partial\ell^{(i)}(w_1,w_2,b)}{\partial w_1}\\\frac{\partial\ell^{(i)}(w_1,w_2,b)}{\partial w_2}\\\frac{\partial\ell^{(i)}(w_1,w_2,b)}{\partial b}\end{bmatrix}=\begin{bmatrix}x_1^{(i)}(x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)})\\x_2^{(i)}(x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)})\\x_1^{(i)}w_1+x_2^{(i)}w_2+b-y^{(i)}\end{bmatrix}=\begin{bmatrix}x_1^{(i)}\\x_2^{(i)}\\1\end{bmatrix}(\hat{y}^{(i)}-y^{(i)})

5.为什么选择均方损失

均方损失函数可以用于线性回归的一个原因是:假设了观测中包含噪声,其中噪声服从正态分布:
y=wx+b+ϵy=\mathbf{w}^\top\mathbf{x}+b+\epsilon其中,ϵN(0,σ2)\epsilon\sim\mathcal{N}(0,\sigma^2)
首先复习一下概率论中的极大似然估计:
似然就是由已经发生的结果来推测产生这个结果的可能环境.
举个栗子,假设进行了n次独立随机测验,其中"状态1"发生了n1n_1次,"状态2"发生了n2n_2次(从经验和直觉出发,状态1发生的概率是n1n1+n2\frac{n_1}{n_1+n_2})定义似然函数L(θ)=θn1(1θ)n2L(\theta)=\theta^{n_1}(1-\theta)^{n_2},使得似然函数最大,就可以求出θ^=n1n1+n2\hat{\theta}=\frac{n_1}{n_1+n_2}.
在机器学习中使用极大似然估计的算法有朴素贝叶斯、EM算法等.利用极大似然估计建立的损失函数模型,需要进一步借助梯度下降法来不断的更新迭代参数,来对参数进行求解。
而在本节中,y的似然:

P(yx)=12πσ2exp(12σ2(ywxb)2)P(y\mid\mathbf{x})=\frac{1}{\sqrt{2\pi\sigma^2}}\exp\left(-\frac{1}{2\sigma^2}(y-\mathbf{w}^\top\mathbf{x}-b)^2\right)

求似然函数的最大(由于历史原因这里取最小):

logP(yX)=log(i=1np(y(i)x(i)))=i=1n(12log(2πσ2)+12σ2(y(i)wx(i)b)2)-\log P(\mathbf{y}\mid\mathbf{X})=-\log (\prod_{i=1}^np(y^{(i)}|\mathbf{x}^{(i)}))=\sum_{i=1}^n \left(\frac{1}{2}\log(2\pi\sigma^2)+\frac{1}{2\sigma^2}\left(y^{(i)}-\mathbf{w}^\top\mathbf{x}^{(i)}-b\right)^2\right)

后一项说明在高斯噪声的假设下最小均方误差等价于对线性模型的极大似然估计.

(二)线性回归的从零开始实现

1.生成数据集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
%matplotlib inline
import random
import torch
from d2l import torch as d2l
def synthetic_data(w, b, num_examples): #@save
"""生成y=Xw+b+噪声"""
X = torch.normal(0, 1, (num_examples, len(w))) # 正态分布的随机矩阵,num_examples指定样本数量,len(w)指定列数
y = torch.matmul(X, w) + b # 矩阵和向量相乘
y += torch.normal(0, 0.01, y.shape) # 添加噪声
return X, y.reshape((-1, 1))

true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)

2.读取数据集

以下是一个获取小批量数据的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
# 这些样本是随机读取的,没有特定的顺序
random.shuffle(indices)
for i in range(0, num_examples, batch_size):
batch_indices = torch.tensor(
indices[i: min(i + batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices] # 这是一个生成器,实际上深度学习框架中实现的内置迭代器要比这高效得多

batch_size = 10
for X, y in data_iter(batch_size, features, labels):
print(X, '\n', y)
break
# 将数据以10个为一组随机分配,取其中的一组

3.模型与模型参数

从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0.初始化后后续更新这些参数来拟合数据.

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

定义模型:

1
2
3
def linreg(X, w, b):  #@save
"""线性回归模型"""
return torch.matmul(X, w) + b

定义损失函数:

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

定义优化算法(小批量随机梯度下降):

1
2
3
4
5
6
def sgd(params, lr, batch_size):  #@save
"""小批量随机梯度下降"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_() # 在每次参数更新后,我们需要清除旧的梯度,以便于下一次迭代时计算新的梯度

4.训练

在每次迭代中,读取一小批量训练样本,并通过模型来获得一组预测。 计算完损失后,开始反向传播,存储每个参数的梯度。 最后,调用优化算法sgd来更新模型参数。在机器学习中,需要多次遍历整个训练数据集(即多个epoch),在每个迭代周期(epoch)中,使用data_iter函数遍历整个数据集, 并将训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除),num_epochs和学习率lr都是超参数,分别设为3和0.03(设置超参数需要反复试验调整)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 = loss(net(X, w, b), y) # X和y的小批量损失
# 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
# 并以此计算关于[w,b]的梯度
l.sum().backward() # 原地操作计算梯度存储在.grad属性中
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}')

(三)线性回归的简洁实现

利用框架提供的一些API,能够对线性回归进行简单实现.
首先还是生成数据集.
然后读取数据集使用Pytorch提供的API进行封装:

1
2
3
4
5
6
7
def load_array(data_arrays, batch_size, is_train=True):  #@save
"""构造一个PyTorch数据迭代器"""
dataset = data.TensorDataset(*data_arrays) # TensorDataset是PyTorch中用于存储数据和标签的类
return data.DataLoader(dataset, batch_size, shuffle=is_train)

batch_size = 10
data_iter = load_array((features, labels), batch_size)

这里的is_train表示是否希望数据迭代器对象在每个迭代周期内打乱数据,与上节的data_iter不同,这里我们使用iter构造Python迭代器,并使用next从迭代器中获取第一项.
然后定义模型和模型参数,使用Pytorch提供的Sequential类使用层来构造模型(其实可以不使用,但后续许多模型是多层的也会用到),使用Linear类来输入全连接层:

1
2
3
# nn是神经网络的缩写
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))

损失函数:

1
loss = nn.MSELoss()

优化算法:

1
trainer = torch.optim.SGD(net.parameters(), lr=0.03)

最后训练:

1
2
3
4
5
6
7
8
9
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
l = loss(net(X) ,y)
trainer.zero_grad() # 反向传播之前需要将梯度归零因为默认情况下梯度是累加的
l.backward()
trainer.step() # 更新网络参数
l = loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {l:f}')

(四)softmax回归

回归可以用于预测多少的问题,也可以用于分类问题.softmax 回归(softmax regression)其实是 logistic 回归的一般形式,logistic 回归用于二分类,而 softmax 回归用于多分类.

1.softmax简单介绍

对于输入数据{(x1,y1),(x2,y2),,(xm,ym)}\{(x_1,y_1),(x_2,y_2),\ldots,(x_m,y_m)\}有k个类别,即yi{1,2,,k}y_{i}\in\{1,2,\ldots,k\},那么对于softmax回归主要估算输入数据xix_i归属于每一类的概率,即:

hθ(xi)=[p(yi=1xi;θ)p(yi=2xi;θ)p(yi=kxi;θ)]=1j=1keθjTxi[eθ1Txieθ2TxieθhTxi]\begin{gathered}h_{\theta}\left(x_{i}\right)=\begin{bmatrix}p\left(y_{i}=1|x_{i};\theta\right)\\p\left(y_{i}=2|x_{i};\theta\right)\\\vdots\\p\left(y_{i}=k|x_{i};\theta\right)\end{bmatrix}=\frac{1}{\sum_{j=1}^{k}e^{\theta_{j}^{T}x_{i}}}\begin{bmatrix}e^{\theta_{1}^{T}x_{i}}\\e^{\theta_{2}^{T}x_{i}}\\\vdots\\e^{\theta_{h}^{T}x_{i}}\end{bmatrix}\end{gathered}

其中θ1,θ2,,θkθ\theta_1,\theta_2,\ldots,\theta_k \in \theta是模型的参数,乘以1j=1keθjTxi\frac{1}{\sum_{j=1}^{k}e^{\theta_{j}^{T}x_{i}}}是为了让概率位于[0,1][0,1]并且概率之和为1.
softmax回归的代价函数(代价函数通常是损失函数在所有训练样本上的平均值或总和):

L(θ)=1m[i=1mj=1k1{yi=j}logeθjTxil=1keθlTxi]L(\theta)=-\frac{1}{m}\left[\sum_{i=1}^{m}\sum_{j=1}^{k}1\left\{y_{i}=j\right\}\log\frac{e^{\theta_{j}^{T}x_{i}}}{\sum_{l=1}^{k}e^{\theta_{l}^{T}x_{i}}}\right]

其中1{}1\{\cdot\}是示性函数,即1{值为真的表达式}=11\{值为真的表达式\}=1,1{值为假的表达式}=01\{值为假的表达式\}=0.
至于梯度下降求解最小化代价函数可以查看这篇文章:softmax回归原理与实现 .

2.softmax回归的损失函数(交叉熵损失)

上述代价公式为什么是这样的形式?这里运用了信息论中的一个叫做交叉熵的知识.
首先我们通过softmax得到的概率向量可能是一个这样的p=[0.279,0.119,0.359,0.014,0.008,0.002,0.011,0.145,0.036,0.025]p=[0.279,0.119,0.359,0.014,0.008,0.002,0.011,0.145,0.036,0.025],但是我们实际上想要得到的y=[0,0,1,0,0,0,0,0,0,0]y=[0,0,1,0,0,0,0,0,0,0],所以我们需要去找一个函数来衡量求得的概率与真实标签的差异.
在信息论中有一个相对熵(KL散度)的概念:同一个随机变量X 有两个单独的概率分布P(x), Q(x),可以使用KL散度来衡量这两个概率分布之间的差异.

DKL(pq)=i=1np(xi)log(p(xi)q(xi))D_{KL}\left(p||q\right)=\sum_{i=1}^np\left(x_i\right)\log\left(\frac{p\left(x_i\right)}{q\left(x_i\right)}\right)

这个公式展开之后:

DKL(pq)=i=1np(xi)log(p(xi)q(xi))=i=1np(xi)log(p(xi))i=1np(xi)log(q(xi))=H(p(x))+[i=1np(xi)log(q(xi))]\begin{gathered} D_{KL}\left(p||q\right)=\sum_{i=1}^np\left(x_i\right)\log\left(\frac{p\left(x_i\right)}{q\left(x_i\right)}\right) \\ =\sum_{i=1}^np\left(x_i\right)log\left(p\left(x_i\right)\right)-\sum_{i=1}^np\left(x_i\right)log\left(q\left(x_i\right)\right) \\ =-H\left(p\left(x\right)\right)+\left[-\sum_{i=1}^np\left(x_i\right)log\left(q\left(x_i\right)\right)\right] \end{gathered}

前半部分也有定义,叫做信息熵,对于信息熵的解释是: 信息量的大小与信息发生的概率成反比。概率越大,信息量越小。概率越小,信息量越大.用I(x)=log(P(x))I(x)=-log(P(x))表示信息量,而信息熵就是信息量的期望值H(x)=P(xi)log(P(xi))H(x)=- \sum P(x_i)log(P(x_i)).后半部分叫做交叉熵,由于前半部分是一个常数,所以要想实现判定实际的输出分布与期望的输出分布的接近程度表示就可以使用交叉熵,越接近也就是交叉熵越小.以上参考自【损失函数系列】交叉熵做损失函数理论知识_交叉熵函数如何改为损失函数 ,写的很易懂.

3.softmax损失函数导数

softmax的矢量表达式:

O=XW+bY^=softmax(O)\begin{aligned} & \mathbf{O}=\mathbf{X} \mathbf{W}+\mathbf{b} \\ & \hat{\mathbf{Y}}=\operatorname{softmax}(\mathbf{O}) \end{aligned}

对于损失函数对于ojo_j的导数:

l(y,y^)=j=1qyjlogexp(oj)k=1qexp(ok)=j=1qyjlogk=1qexp(ok)j=1qyjoj=logk=1qexp(ok)j=1qyjoj.ojl(y,y^)=exp(oj)k=1qexp(ok)yj=softmax(o)jyj.\begin{aligned} l(\mathbf{y}, \hat{\mathbf{y}}) & =-\sum_{j=1}^q y_j \log \frac{\exp \left(o_j\right)}{\sum_{k=1}^q \exp \left(o_k\right)} \\ & =\sum_{j=1}^q y_j \log \sum_{k=1}^q \exp \left(o_k\right)-\sum_{j=1}^q y_j o_j \\ & =\log \sum_{k=1}^q \exp \left(o_k\right)-\sum_{j=1}^q y_j o_j . \\ \therefore \quad \partial_{o_j} l(\mathbf{y}, \hat{\mathbf{y}}) & =\frac{\exp \left(o_j\right)}{\sum_{k=1}^q \exp \left(o_k\right)}-y_j=\operatorname{softmax}(\mathbf{o})_j-y_j . \end{aligned}

从上式可以看出,softmax回归的损失函数的导数是我们softmax模型分配的概率与实际发生的情况(由独热标签向量表示)之间的差异.

(五)图像分类数据集

1.获取数据

本节使用的是Fashion-MNIST数据集 (Xiao et al., 2017),MNIST(Modified National Institute of Standards and Technology database)数据集 (LeCun et al., 1998) 是图像分类中广泛使用的数据集之一,Fashion-MNIST数据集更复杂.

1
2
3
4
5
6
7
8
9
10
11
# 使用框架内置函数下载数据集并读取到内存中
from torchvision import tansforms
trans = transforms.ToTensor()
# ToTensor实例将图像数据从PIL类变成32位浮点并除以255使得所有像素的数值均在0~1之间
import torchvision
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
# root表示存储路径
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)
mnist_train[0][0].shape # torch.Size([1, 28, 28]) 因为是灰度数据所以通道数为1

训练数据集有6000张,测试数据集有1000张。测试数据集不用于训练用于评估模型性能,另外这些图像也不以一般格式存储所以你是不能直接打开的,使用的可能是IDX文件格式.

2.显示数据(图像)

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
39
40
41
42
43
# 可视化样本
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5): #@save
'''
绘制图像列表

Parameters:
imgs:图像列表
num_rows:要显示图像的行数
num_cols:要显示的图像的列数
title:为每个图像设置标题
scale:缩放因子
'''
figsize = (num_cols * scale, num_rows * scale)
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
# matplotlib.pyplot库创建一个图形和一组子图轴
axes = axes.flatten() # 展平,类似于矩阵变成一个列表数组,axes是axis(坐标轴)的复数
for i, (ax, img) in enumerate(zip(axes, imgs)):
if torch.is_tensor(img):
# 图片张量
ax.imshow(img.numpy())
else:
# PIL图片
ax.imshow(img)
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

# 定义一个函数显示每个图像的title
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]

# 显示图像示例
from torch.utils import data
X, y = next(iter(data.DataLoader(mnist_train, batch_size=18)))
# X是一个批次的图像数据,它通常是一个四维张量,形状为 [batch_size, channels, height, width],y是标签序号
show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y));
# 将X维度压缩为三维以便适应show_images函数的要求

3.读取小批量数据

这里直接使用了内置的数据迭代器不是自己写读取函数.

1
2
3
4
5
6
7
8
9
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())
# shuffle就是表示是否打乱样本读取小批量(shuffle有洗牌的意思)

4.获取数据并读取

其实就是一个整合.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#这个函数还能通过resize参数调整图像大小
def load_data_fashion_mnist(batch_size, resize=None): #@save
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
trans = [transforms.ToTensor()]
if resize:
trans.insert(0, transforms.Resize(resize))
# transforms.Resize(resize) 将被添加到trans列表的最前面
trans = transforms.Compose(trans) # 所有变换组合为复合变换
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)
return (data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=get_dataloader_workers()))

(六)softmax回归从零开始实现

1.初始化和定义softmax

获取数据:

1
2
3
from IPython import display
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

因为图像是28×2828 \times 28的,可以看做是一个长度为784的向量,与线性回归一样使用正态分布初始化权重W\mathrm{W},偏置初始化为0.

1
2
3
4
5
num_inputs = 784
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)

回顾一下sum函数,keepdim参数为True的时候能够保持原始张量轴数:

1
2
3
4
5
6
7
X = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
X.sum(0, keepdim=True), X.sum(1, keepdim=True)
# 0是同一列求和
# 会是以下结果
# (tensor([[5., 7., 9.]]),
# tensor([[ 6.],
# [15.]]))

定义softmax函数如下:

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
3
4
5
6
X = torch.normal(0, 1, (2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(1)
# (tensor([[0.1686, 0.4055, 0.0849, 0.1064, 0.2347],
# [0.0217, 0.2652, 0.6354, 0.0457, 0.0321]]),
# tensor([1.0000, 1.0000]))

注意这里代码没有考虑到数值上溢或下溢,这里算不够完善.

2.定义模型

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

3.定义损失函数和精度

1
2
3
4
5
6
7
8
9
10
def cross_entropy(y_hat, y):
return - torch.log(y_hat[range(len(y_hat)), y])
# 这里的len(y_hat)返回的是行数

# 示例
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] pytorch提供的取矩阵中元素的方法
cross_entropy(y_hat, y)
# tensor([2.3026, 0.6931])

分类精度:正确预测数量与总预测数量之比.也定义一个函数:

1
2
3
4
5
6
7
8
9
def accuracy(y_hat, y):  #@save
"""计算预测正确的数组量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1)
# 沿着第一维(行)中找到最大的索引值
cmp = y_hat.type(y.dtype) == y
# y_hat的数据类型转换为y的数据类型得到bool数组
return float(cmp.type(y.dtype).sum())
# 返回预测正确样本数量,bool类型转换了一下可计算类型

评估模型的精度有框架函数定义:

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
# 一个实用类
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]

def evaluate_accuracy(net, data_iter): #@save
"""计算在指定数据集上模型的精度"""
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()) # y.numel()返回y中元素的总数
return metric[0] / metric[1]

4.训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def train_epoch_ch3(net, train_iter, loss, updater):  #@save
# updater是更新模型参数的常用函数,可以是对sgd函数(随机)
"""训练模型一个迭代周期(定义见第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()
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
39
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]
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)

多次迭代:

1
2
3
4
5
6
7
8
9
10
11
12
13
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):  #@save
"""训练模型(定义见第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
assert train_loss < 0.5, 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

如果使用自己的updater,以下是一个实例:

1
2
3
4
5
6
7
8
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)

得到结果大致如图:
参数图

5.预测

1
2
3
4
5
6
7
8
9
10
11
def predict_ch3(net, test_iter, n=6):  #@save
"""预测标签(定义见第3章)"""
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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))

def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
# 正态分布来初始化权重,0.01是标准差

net.apply(init_weights);

在之前提到过上溢和下溢的问题,在框架中已经解决好了.这里利用的是如果每个常数oko_k减去一个相同的常数那么得到的softmax返回值不会变,那么我们可以让ojmax(ok)o_j-max(o_k)就能尽量避免上溢.

y^=exp(ojmax(ok))exp(max(ok))kexp(okmax(ok))exp(max(ok))=exp(ojmax(ok))kexp(okmax(ok))\begin{aligned} \hat{y}& =\frac{\exp(o_j-\max(o_k))\exp(\max(o_k))}{\sum_k\exp(o_k-\max(o_k))\exp(\max(o_k))} \\ &=\frac{\exp(o_{j}-\max(o_{k}))}{\sum_{k}\exp(o_{k}-\max(o_{k}))} \end{aligned}

而避免下溢(主要是log(exp(ojmax(ok)))log\left(\exp(o_j-\max(o_k))\right)的溢出)就可以:

log(y^j)=log(exp(ojmax(ok))kexp(okmax(ok)))=log(exp(ojmax(ok)))log(kexp(okmax(ok)))=ojmax(ok)log(kexp(okmax(ok))).\begin{aligned} \log(\hat{y}_{j})& =\log\left(\frac{\exp(o_{j}-\max(o_{k}))}{\sum_{k}\exp(o_{k}-\max(o_{k}))}\right) \\ &=\log\left(\exp(o_j-\max(o_k))\right)-\log\left(\sum_k\exp(o_k-\max(o_k))\right) \\ &=o_j-\max(o_k)-\log\left(\sum_k\exp(o_k-\max(o_k))\right). \end{aligned}

1
2
3
loss = nn.CrossEntropyLoss(reduction='none')
# 交叉熵损失层,reduction='none'表示不进行任何聚合操作,返回每个样本的损失
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)
  • 标题: 《动手学深度学习》笔记(三)
  • 作者: EliorFoy
  • 创建于 : 2024-10-12 17:21:47
  • 更新于 : 2024-10-12 17:22:51
  • 链接: https://eliorfoy.github.io/2024/10/12/大三上/《动手学深度学习》(三)/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。