[深度学习]动手学深度学习笔记-2

Task-1 线性回归;Softmax与分类模型;多层感知机

1.1 线性回归原理

1.1.1 线性回归的基本要素

我们以一个简单的房屋价格预测作为例子来解释线性回归的基本要素。这个应用的目标是预测一栋房子的售出价格(元)。我们知道这个价格取决于很多因素,如房屋状况、地段、市场行情等。为了简单起见,这里我们假设价格只取决于房屋状况的两个因素,即面积(平方米)和房龄(年)。接下来我们希望探索价格与这两个因素的具体关系。

1.1.1.1 模型定义

设房屋的面积为 x 1 x_1 x1,房龄为 x 2 x_2 x2,售出价格为 y y y。我们需要建立基于输入 x 1 x_1 x1 x 2 x_2 x2 来计算输出 y y y 的表达式,也就是模型(model)。顾名思义,线性回归假设输出与各个输入之间是线性关系:
y ^ = x 1 w 1 + x 2 w 2 + b \hat{y} = x_1 w_1 + x_2 w_2 + b y^=x1w1+x2w2+b
其中 w 1 w_1 w1 w 2 w_2 w2 是权重(weight), b b b 是偏差(bias),且均为标量。它们是线性回归模型的参数(parameter)。模型输出 y ^ \hat{y} y^ 是线性回归对真实价格 y y y 的预测或估计。我们通常允许它们之间有一定误差。

1.1.1.2 模型训练

接下来我们需要通过数据来寻找特定的模型参数值,使模型在数据上的误差尽可能小。这个过程叫作模型训练(model training)。下面我们介绍模型训练所涉及的3个要素。

(1) 训练数据

我们通常收集一系列的真实数据,例如多栋房屋的真实售出价格和它们对应的面积和房龄。我们希望在这个数据上面寻找模型参数来使模型的预测价格与真实价格的误差最小。在机器学习术语里,该数据集被称为训练数据集(training data set)或训练集(training set),一栋房屋被称为一个样本(sample),其真实售出价格叫作标签(label),用来预测标签的两个因素叫作特征(feature)。特征用来表征样本的特点。

假设我们采集的样本数为 n n n,索引为 i i i 的样本的特征为 x 1 ( i ) x_1^{(i)} x1(i) x 2 ( i ) x_2^{(i)} x2(i),标签为 y ( i ) y^{(i)} y(i)。对于索引为 i i i 的房屋,线性回归模型的房屋价格预测表达式为
y ^ ( i ) = x 1 ( i ) w 1 + x 2 ( i ) w 2 + b \hat{y}^{(i)} = x_1^{(i)} w_1 + x_2^{(i)} w_2 + b y^(i)=x1(i)w1+x2(i)w2+b

(2) 损失函数

在模型训练中,我们需要衡量价格预测值与真实值之间的误差。通常我们会选取一个非负数作为误差,且数值越小表示误差越小。一个常用的选择是平方函数。它在评估索引为 i i i 的样本误差的表达式为

ℓ ( i ) ( w 1 , w 2 , b ) = 1 2 ( y ^ ( i ) − y ( i ) ) 2 \ell^{(i)}(w_1, w_2, b) = \frac{1}{2} \left(\hat{y}^{(i)} - y^{(i)}\right)^2 (i)(w1,w2,b)=21(y^(i)y(i))2

其中常数 1 2 \frac 1 2 21 使对平方项求导后的常数系数为1,这样在形式上稍微简单一些。显然,误差越小表示预测价格与真实价格越相近,且当二者相等时误差为0。给定训练数据集,这个误差只与模型参数相关,因此我们将它记为以模型参数为参数的函数。在机器学习里,将衡量误差的函数称为损失函数(loss function)。这里使用的平方误差函数也称为平方损失(square loss)。

通常,我们用训练数据集中所有样本误差的平均来衡量模型预测的质量,即

ℓ ( w 1 , w 2 , b ) = 1 n ∑ i = 1 n ℓ ( i ) ( w 1 , w 2 , b ) = 1 n ∑ i = 1 n 1 2 ( x 1 ( i ) w 1 + x 2 ( i ) w 2 + b − y ( i ) ) 2 \ell(w_1, w_2, b) =\frac{1}{n} \sum_{i=1}^n \ell^{(i)}(w_1, w_2, b) =\frac{1}{n} \sum_{i=1}^n \frac{1}{2}\left(x_1^{(i)} w_1 + x_2^{(i)} w_2 + b - y^{(i)}\right)^2 (w1,w2,b)=n1i=1n(i)(w1,w2,b)=n1i=1n21(x1(i)w1+x2(i)w2+by(i))2

在模型训练中,我们希望找出一组模型参数,记为 w 1 ∗ , w 2 ∗ , b ∗ w_1^*, w_2^*, b^* w1,w2,b,来使训练样本平均损失最小:

w 1 ∗ , w 2 ∗ , b ∗ = arg ⁡ min ⁡ w 1 , w 2 , b ℓ ( w 1 , w 2 , b ) w_1^*, w_2^*, b^* = \underset{w_1, w_2, b}{\arg\min} \ell(w_1, w_2, b) w1,w2,b=w1,w2,bargmin(w1,w2,b)

(3) 优化算法

当模型和损失函数形式较为简单时,上面的误差最小化问题的解可以直接用公式表达出来。这类解叫作解析解(analytical solution)。本节使用的线性回归和平方误差刚好属于这个范畴。然而,大多数深度学习模型并没有解析解,只能通过优化算法有限次迭代模型参数来尽可能降低损失函数的值。这类解叫作数值解(numerical solution)。

在求数值解的优化算法中,小批量随机梯度下降(mini-batch stochastic gradient descent)在深度学习中被广泛使用。它的算法很简单:先选取一组模型参数的初始值,如随机选取;接下来对参数进行多次迭代,使每次迭代都可能降低损失函数的值。在每次迭代中,先随机均匀采样一个由固定数目训练数据样本所组成的小批量(mini-batch) B \mathcal{B} B,然后求小批量中数据样本的平均损失有关模型参数的导数(梯度),最后用此结果与预先设定的一个正数的乘积作为模型参数在本次迭代的减小量。

在训练本节讨论的线性回归模型的过程中,模型的每个参数将作如下迭代:

w 1 ← w 1 − η ∣ B ∣ ∑ i ∈ B ∂ ℓ ( i ) ( w 1 , w 2 , b ) ∂ w 1 = w 1 − η ∣ B ∣ ∑ i ∈ B x 1 ( i ) ( x 1 ( i ) w 1 + x 2 ( i ) w 2 + b − y ( i ) ) , w 2 ← w 2 − η ∣ B ∣ ∑ i ∈ B ∂ ℓ ( i ) ( w 1 , w 2 , b ) ∂ w 2 = w 2 − η ∣ B ∣ ∑ i ∈ B x 2 ( i ) ( x 1 ( i ) w 1 + x 2 ( i ) w 2 + b − y ( i ) ) , b ← b − η ∣ B ∣ ∑ i ∈ B ∂ ℓ ( i ) ( w 1 , w 2 , b ) ∂ b = b − η ∣ B ∣ ∑ i ∈ B ( x 1 ( i ) w 1 + x 2 ( i ) w 2 + b − y ( i ) ) . \begin{aligned} 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{aligned} w1w2bw1BηiBw1(i)(w1,w2,b)=w1BηiBx1(i)(x1(i)w1+x2(i)w2+by(i)),w2BηiBw2(i)(w1,w2,b)=w2BηiBx2(i)(x1(i)w1+x2(i)w2+by(i)),bBηiBb(i)(w1,w2,b)=bBηiB(x1(i)w1+x2(i)w2+by(i)).

在上式中, ∣ B ∣ |\mathcal{B}| B 代表每个小批量中的样本个数(批量大小,batch size), η \eta η 称作学习率(learning rate)并取正数。需要强调的是,这里的批量大小和学习率的值是人为设定的,并不是通过模型训练学出的,因此叫作超参数(hyperparameter)。我们通常所说的“调参”指的正是调节超参数,例如通过反复试错来找到超参数合适的值。在少数情况下,超参数也可以通过模型训练学出。本书对此类情况不做讨论。

1.1.1.3 模型预测

模型训练完成后,我们将模型参数 w 1 , w 2 , b w_1, w_2, b w1,w2,b 在优化算法停止时的值分别记作 w ^ 1 , w ^ 2 , b ^ \hat{w}_1, \hat{w}_2, \hat{b} w^1,w^2,b^。注意,这里我们得到的并不一定是最小化损失函数的最优解 w 1 ∗ , w 2 ∗ , b ∗ w_1^*, w_2^*, b^* w1,w2,b,而是对最优解的一个近似。然后,我们就可以使用学出的线性回归模型 x 1 w ^ 1 + x 2 w ^ 2 + b ^ x_1 \hat{w}_1 + x_2 \hat{w}_2 + \hat{b} x1w^1+x2w^2+b^ 来估算训练数据集以外任意一栋面积(平方米)为 x 1 x_1 x1、房龄(年)为 x 2 x_2 x2的房屋的价格了。这里的估算也叫作模型预测、模型推断或模型测试。

1.1.2 矢量计算表达式

在模型训练或预测时,我们常常会同时处理多个数据样本并用到矢量计算。在介绍线性回归的矢量计算表达式之前,让我们先考虑对两个向量相加的两种方法。a

下面先定义两个1000维的向量。

import torch
from time import time

a = torch.ones(1000)
b = torch.ones(1000)

向量相加的一种方法是,将这两个向量按元素逐一做标量加法。

start = time()
c = torch.zeros(1000)
for i in range(1000):
    c[i] = a[i] + b[i]
print(time() - start)

输出:

0.02039504051208496

向量相加的另一种方法是,将这两个向量直接做矢量加法。

start = time()
d = a + b
print(time() - start)

输出:

0.0008330345153808594

结果很明显,后者比前者更省时。因此,我们应该尽可能采用矢量计算,以提升计算效率

让我们再次回到本节的房价预测问题。如果我们对训练数据集里的3个房屋样本(索引分别为1、2和3)逐一预测价格,将得到
y ^ ( 1 ) = x 1 ( 1 ) w 1 + x 2 ( 1 ) w 2 + b , y ^ ( 2 ) = x 1 ( 2 ) w 1 + x 2 ( 2 ) w 2 + b , y ^ ( 3 ) = x 1 ( 3 ) w 1 + x 2 ( 3 ) w 2 + b . \begin{aligned} \hat{y}^{(1)} &= x_1^{(1)} w_1 + x_2^{(1)} w_2 + b,\\ \hat{y}^{(2)} &= x_1^{(2)} w_1 + x_2^{(2)} w_2 + b,\\ \hat{y}^{(3)} &= x_1^{(3)} w_1 + x_2^{(3)} w_2 + b. \end{aligned} y^(1)y^(2)y^(3)=x1(1)w1+x2(1)w2+b,=x1(2)w1+x2(2)w2+b,=x1(3)w1+x2(3)w2+b.

现在,我们将上面3个等式转化成矢量计算。设

y ^ = [ y ^ ( 1 ) y ^ ( 2 ) y ^ ( 3 ) ] , X = [ x 1 ( 1 ) x 2 ( 1 ) x 1 ( 2 ) x 2 ( 2 ) x 1 ( 3 ) x 2 ( 3 ) ] , w = [ w 1 w 2 ] \boldsymbol{\hat{y}} = \begin{bmatrix} \hat{y}^{(1)} \\ \hat{y}^{(2)} \\ \hat{y}^{(3)} \end{bmatrix},\quad \boldsymbol{X} = \begin{bmatrix} x_1^{(1)} & x_2^{(1)} \\ x_1^{(2)} & x_2^{(2)} \\ x_1^{(3)} & x_2^{(3)} \end{bmatrix},\quad \boldsymbol{w} = \begin{bmatrix} w_1 \\ w_2 \end{bmatrix} y^=y^(1)y^(2)y^(3),X=x1(1)x1(2)x1(3)x2(1)x2(2)x2(3),w=[w1w2]

对3个房屋样本预测价格的矢量计算表达式为 y ^ = X w + b , \boldsymbol{\hat{y}} = \boldsymbol{X} \boldsymbol{w} + b, y^=Xw+b, 其中的加法运算使用了广播机制。例如:

a = torch.ones(3)
b = 10
print(a + b)

输出:

tensor([11., 11., 11.])

广义上讲,当数据样本数为 n n n,特征数为 d d d 时,线性回归的矢量计算表达式为
y ^ = X w + b \boldsymbol{\hat{y}} = \boldsymbol{X} \boldsymbol{w} + b y^=Xw+b
其中模型输出 y ^ ∈ R n × 1 \boldsymbol{\hat{y}} \in \mathbb{R}^{n \times 1} y^Rn×1 批量数据样本特征 X ∈ R n × d \boldsymbol{X} \in \mathbb{R}^{n \times d} XRn×d,权重 w ∈ R d × 1 \boldsymbol{w} \in \mathbb{R}^{d \times 1} wRd×1, 偏差 b ∈ R b \in \mathbb{R} bR。相应地,批量数据样本标签 y ∈ R n × 1 \boldsymbol{y} \in \mathbb{R}^{n \times 1} yRn×1。设模型参数 θ = [ w 1 , w 2 , b ] ⊤ \boldsymbol{\theta} = [w_1, w_2, b]^\top θ=[w1,w2,b],我们可以重写损失函数为
ℓ ( θ ) = 1 2 n ( y ^ − y ) ⊤ ( y ^ − y ) \ell(\boldsymbol{\theta})=\frac{1}{2n}(\boldsymbol{\hat{y}}-\boldsymbol{y})^\top(\boldsymbol{\hat{y}}-\boldsymbol{y}) (θ)=2n1(y^y)(y^y)

小批量随机梯度下降的迭代步骤将相应地改写为
θ ← θ − η ∣ B ∣ ∑ i ∈ B ∇ θ ℓ ( i ) ( θ ) , \boldsymbol{\theta} \leftarrow \boldsymbol{\theta} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \nabla_{\boldsymbol{\theta}} \ell^{(i)}(\boldsymbol{\theta}), θθBηiBθ(i)(θ),

其中梯度是损失有关3个为标量的模型参数的偏导数组成的向量:
∇ θ ℓ ( i ) ( θ ) = [ ∂ ℓ ( i ) ( w 1 , w 2 , b ) ∂ w 1 ∂ ℓ ( i ) ( w 1 , w 2 , b ) ∂ w 2 ∂ ℓ ( i ) ( w 1 , w 2 , b ) ∂ b ] = [ 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 ) ] = [ x 1 ( i ) x 2 ( 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)}) θ(i)(θ)=w1(i)(w1,w2,b)w2(i)(w1,w2,b)b(i)(w1,w2,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))

1.1.3小结

  • 和大多数深度学习模型一样,对于线性回归这样一种单层神经网络,它的基本要素包括模型、训练数据、损失函数和优化算法。
  • 既可以用神经网络图表示线性回归,又可以用矢量计算表示该模型。
  • 应该尽可能采用矢量计算,以提升计算效率。

1.2 线性回归的从零开始实现

首先,导入本节中实验所需的包或模块,其中的matplotlib包可用于作图,且设置成嵌入显示。

%matplotlib inline
import torch
from IPython import display
from matplotlib import pyplot as plt
import numpy as np
import random

1.2.1 生成数据集

我们构造一个简单的人工训练数据集,它可以使我们能够直观比较学到的参数和真实的模型参数的区别。设训练数据集样本数为1000,输入个数(特征数)为2。给定随机生成的批量样本特征 X ∈ R 1000 × 2 \boldsymbol{X} \in \mathbb{R}^{1000 \times 2} XR1000×2,我们使用线性回归模型真实权重 w = [ 2 , − 3.4 ] ⊤ \boldsymbol{w} = [2, -3.4]^\top w=[2,3.4] 和偏差 b = 4.2 b = 4.2 b=4.2,以及一个随机噪声项 ϵ \epsilon ϵ 来生成标签
y = X w + b + ϵ \boldsymbol{y} = \boldsymbol{X}\boldsymbol{w} + b + \epsilon y=Xw+b+ϵ

其中噪声项 ϵ \epsilon ϵ 服从均值为0、标准差为0.01的正态分布。噪声代表了数据集中无意义的干扰。下面,让我们生成数据集。

num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2
features = torch.randn(num_examples, num_inputs,
                       dtype=torch.float32)
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()),
                       dtype=torch.float32)

注意,features的每一行是一个长度为2的向量,而labels的每一行是一个长度为1的向量(标量)。

print(features[0], labels[0])

输出:

tensor([0.8557, 0.4793]) tensor(4.2887)

通过生成第二个特征features[:, 1]和标签 labels 的散点图,可以更直观地观察两者间的线性关系。

def use_svg_display():
    # 用矢量图显示
    display.set_matplotlib_formats('svg')

def set_figsize(figsize=(3.5, 2.5)):
    use_svg_display()
    # 设置图的尺寸
    plt.rcParams['figure.figsize'] = figsize

# # 在../d2lzh_pytorch里面添加上面两个函数后就可以这样导入
# import sys
# sys.path.append("..")
# from d2lzh_pytorch import * 

set_figsize()
plt.scatter(features[:, 1].numpy(), labels.numpy(), 1);

我们将上面的plt作图函数以及use_svg_display函数和set_figsize函数定义在d2lzh_pytorch包里。以后在作图时,我们将直接调用d2lzh_pytorch.plt。由于pltd2lzh_pytorch包中是一个全局变量,我们在作图前只需要调用d2lzh_pytorch.set_figsize()即可打印矢量图并设置图的尺寸。

原书中提到的d2lzh里面使用了mxnet,改成pytorch实现后本项目统一将原书的d2lzh改为d2lzh_pytorch

1.2.2 读取数据

在训练模型的时候,我们需要遍历数据集并不断读取小批量数据样本。这里我们定义一个函数:它每次返回batch_size(批量大小)个随机样本的特征和标签。

# 本函数已保存在d2lzh包中方便以后使用
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):
        j = torch.LongTensor(indices[i: min(i + batch_size, num_examples)]) # 最后一次可能不足一个batch
        yield  features.index_select(0, j), labels.index_select(0, j)

让我们读取第一个小批量数据样本并打印。每个批量的特征形状为(10, 2),分别对应批量大小和输入个数;标签形状为批量大小。

batch_size = 10

for X, y in data_iter(batch_size, features, labels):
    print(X, y)
    break

输出:

tensor([[-1.4239, -1.3788],
        [ 0.0275,  1.3550],
        [ 0.7616, -1.1384],
        [ 0.2967, -0.1162],
        [ 0.0822,  2.0826],
        [-0.6343, -0.7222],
        [ 0.4282,  0.0235],
        [ 1.4056,  0.3506],
        [-0.6496, -0.5202],
        [-0.3969, -0.9951]]) 
 tensor([ 6.0394, -0.3365,  9.5882,  5.1810, -2.7355,  5.3873,  4.9827,  5.7962,
         4.6727,  6.7921])

1.2.3 初始化模型参数

我们将权重初始化成均值为0、标准差为0.01的正态随机数,偏差则初始化成0。

w = torch.tensor(np.random.normal(0, 0.01, (num_inputs, 1)), dtype=torch.float32)
b = torch.zeros(1, dtype=torch.float32)

之后的模型训练中,需要对这些参数求梯度来迭代参数的值,因此我们要让它们的requires_grad=True

w.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True) 

1.2.4 定义模型

下面是线性回归的矢量计算表达式的实现。我们使用mm函数做矩阵乘法。

def linreg(X, w, b):  # 本函数已保存在d2lzh_pytorch包中方便以后使用
    return torch.mm(X, w) + b

1.2.5 定义损失函数

我们使用上一节描述的平方损失来定义线性回归的损失函数。在实现中,我们需要把真实值y变形成预测值y_hat的形状。以下函数返回的结果也将和y_hat的形状相同。

def squared_loss(y_hat, y):  # 本函数已保存在d2lzh_pytorch包中方便以后使用
    # 注意这里返回的是向量, 另外, pytorch里的MSELoss并没有除以 2
    return (y_hat - y.view(y_hat.size())) ** 2 / 2

1.2.6 定义优化算法

以下的sgd函数实现了上一节中介绍的小批量随机梯度下降算法。它通过不断迭代模型参数来优化损失函数。这里自动求梯度模块计算得来的梯度是一个批量样本的梯度和。我们将它除以批量大小来得到平均值。

def sgd(params, lr, batch_size):  # 本函数已保存在d2lzh_pytorch包中方便以后使用
    for param in params:
        param.data -= lr * param.grad / batch_size # 注意这里更改param时用的param.data

1.2.7 训练模型

在训练中,我们将多次迭代模型参数。在每次迭代中,我们根据当前读取的小批量数据样本(特征X和标签y),通过调用反向函数backward计算小批量随机梯度,并调用优化算法sgd迭代模型参数。由于我们之前设批量大小batch_size为10,每个小批量的损失l的形状为(10, 1)。回忆一下自动求梯度一节。由于变量l并不是一个标量,所以我们可以调用.sum()将其求和得到一个标量,再运行l.backward()得到该变量有关模型参数的梯度。注意在每次更新完参数后不要忘了将参数的梯度清零。

在一个迭代周期(epoch)中,我们将完整遍历一遍data_iter函数,并对训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除)。这里的迭代周期个数num_epochs和学习率lr都是超参数,分别设3和0.03。在实践中,大多超参数都需要通过反复试错来不断调节。虽然迭代周期数设得越大模型可能越有效,但是训练时间可能过长。而有关学习率对模型的影响,我们会在后面“优化算法”一章中详细介绍。

lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss

for epoch in range(num_epochs):  # 训练模型一共需要num_epochs个迭代周期
    # 在每一个迭代周期中,会使用训练数据集中所有样本一次(假设样本数能够被批量大小整除)。X
    # 和y分别是小批量样本的特征和标签
    for X, y in data_iter(batch_size, features, labels):
        l = loss(net(X, w, b), y).sum()  # l是有关小批量X和y的损失
        l.backward()  # 小批量的损失对模型参数求梯度
        sgd([w, b], lr, batch_size)  # 使用小批量随机梯度下降迭代模型参数
        
        # 不要忘了梯度清零
        w.grad.data.zero_()
        b.grad.data.zero_()
    train_l = loss(net(features, w, b), labels)
    print('epoch %d, loss %f' % (epoch + 1, train_l.mean().item()))

输出:

epoch 1, loss 0.028127
epoch 2, loss 0.000095
epoch 3, loss 0.000050

训练完成后,我们可以比较学到的参数和用来生成训练集的真实参数。它们应该很接近。

print(true_w, '\n', w)
print(true_b, '\n', b)

输出:

[2, -3.4] 
 tensor([[ 1.9998],
        [-3.3998]], requires_grad=True)
4.2 
 tensor([4.2001], requires_grad=True)

1.3 线性回归的Pytorch实现

1.3.1 生成数据集

我们生成与上一节中相同的数据集。其中features是训练数据特征,labels是标签。

num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2
features = torch.tensor(np.random.normal(0, 1, (num_examples, num_inputs)), dtype=torch.float)
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float)

1.3.2 读取数据

PyTorch提供了data包来读取数据。由于data常用作变量名,我们将导入的data模块用Data代替。在每一次迭代中,我们将随机读取包含10个数据样本的小批量。

import torch.utils.data as Data

batch_size = 10
# 将训练数据的特征和标签组合
dataset = Data.TensorDataset(features, labels)
# 随机读取小批量
data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)

这里data_iter的使用跟上一节中的一样。让我们读取并打印第一个小批量数据样本。

for X, y in data_iter:
    print(X, y)
    break

输出:

tensor([[-2.7723, -0.6627],
        [-1.1058,  0.7688],
        [ 0.4901, -1.2260],
        [-0.7227, -0.2664],
        [-0.3390,  0.1162],
        [ 1.6705, -2.7930],
        [ 0.2576, -0.2928],
        [ 2.0475, -2.7440],
        [ 1.0685,  1.1920],
        [ 1.0996,  0.5106]]) 
 tensor([ 0.9066, -0.6247,  9.3383,  3.6537,  3.1283, 17.0213,  5.6953, 17.6279,
         2.2809,  4.6661])

1.3.3 定义模型

在上一节从零开始的实现中,我们需要定义模型参数,并使用它们一步步描述模型是怎样计算的。当模型结构变得更复杂时,这些步骤将变得更繁琐。其实,PyTorch提供了大量预定义的层,这使我们只需关注使用哪些层来构造模型。下面将介绍如何使用PyTorch更简洁地定义线性回归。

首先,导入torch.nn模块。实际上,“nn”是neural networks(神经网络)的缩写。顾名思义,该模块定义了大量神经网络的层。之前我们已经用过了autograd,而nn就是利用autograd来定义模型。nn的核心数据结构是Module,它是一个抽象概念,既可以表示神经网络中的某个层(layer),也可以表示一个包含很多层的神经网络。在实际使用中,最常见的做法是继承nn.Module,撰写自己的网络/层。一个nn.Module实例应该包含一些层以及返回输出的前向传播(forward)方法。下面先来看看如何用nn.Module实现一个线性回归模型。

class LinearNet(nn.Module):
    def __init__(self, n_feature):
        super(LinearNet, self).__init__()
        self.linear = nn.Linear(n_feature, 1)
    # forward 定义前向传播
    def forward(self, x):
        y = self.linear(x)
        return y
    
net = LinearNet(num_inputs)
print(net) # 使用print可以打印出网络的结构

输出:

LinearNet(
  (linear): Linear(in_features=2, out_features=1, bias=True)
)

事实上我们还可以用nn.Sequential来更加方便地搭建网络,Sequential是一个有序的容器,网络层将按照在传入Sequential的顺序依次被添加到计算图中。

# 写法一
net = nn.Sequential(
    nn.Linear(num_inputs, 1)
    # 此处还可以传入其他层
    )

# 写法二
net = nn.Sequential()
net.add_module('linear', nn.Linear(num_inputs, 1))
# net.add_module ......

# 写法三
from collections import OrderedDict
net = nn.Sequential(OrderedDict([
          ('linear', nn.Linear(num_inputs, 1))
          # ......
        ]))

print(net)
print(net[0])

输出:

Sequential(
  (linear): Linear(in_features=2, out_features=1, bias=True)
)
Linear(in_features=2, out_features=1, bias=True)

可以通过net.parameters()来查看模型所有的可学习参数,此函数将返回一个生成器。

for param in net.parameters():
    print(param)

输出:

Parameter containing:
tensor([[-0.0277,  0.2771]], requires_grad=True)
Parameter containing:
tensor([0.3395], requires_grad=True)

回顾图3.1中线性回归在神经网络图中的表示。作为一个单层神经网络,线性回归输出层中的神经元和输入层中各个输入完全连接。因此,线性回归的输出层又叫全连接层。

注意:torch.nn仅支持输入一个batch的样本不支持单个样本输入,如果只有单个样本,可使用input.unsqueeze(0)来添加一维。

1.3.4 初始化模型参数

在使用net前,我们需要初始化模型参数,如线性回归模型中的权重和偏差。PyTorch在init模块中提供了多种参数初始化方法。这里的initinitializer的缩写形式。我们通过init.normal_将权重参数每个元素初始化为随机采样于均值为0、标准差为0.01的正态分布。偏差会初始化为零。

from torch.nn import init

init.normal_(net[0].weight, mean=0, std=0.01)
init.constant_(net[0].bias, val=0)  # 也可以直接修改bias的data: net[0].bias.data.fill_(0)

注:如果这里的net是用3.3.3节一开始的代码自定义的,那么上面代码会报错,net[0].weight应改为net.linear.weightbias亦然。因为net[0]这样根据下标访问子模块的写法只有当net是个ModuleList或者Sequential实例时才可以,详见4.1节。

1.3.5 定义损失函数

PyTorch在nn模块中提供了各种损失函数,这些损失函数可看作是一种特殊的层,PyTorch也将这些损失函数实现为nn.Module的子类。我们现在使用它提供的均方误差损失作为模型的损失函数。

loss = nn.MSELoss()

1.3.6 定义优化算法

同样,我们也无须自己实现小批量随机梯度下降算法。torch.optim模块提供了很多常用的优化算法比如SGD、Adam和RMSProp等。下面我们创建一个用于优化net所有参数的优化器实例,并指定学习率为0.03的小批量随机梯度下降(SGD)为优化算法。

import torch.optim as optim

optimizer = optim.SGD(net.parameters(), lr=0.03)
print(optimizer)

输出:

SGD (
Parameter Group 0
    dampening: 0
    lr: 0.03
    momentum: 0
    nesterov: False
    weight_decay: 0
)

我们还可以为不同子网络设置不同的学习率,这在finetune时经常用到。例:

optimizer =optim.SGD([
                # 如果对某个参数不指定学习率,就使用最外层的默认学习率
                {'params': net.subnet1.parameters()}, # lr=0.03
                {'params': net.subnet2.parameters(), 'lr': 0.01}
            ], lr=0.03)

有时候我们不想让学习率固定成一个常数,那如何调整学习率呢?主要有两种做法。一种是修改optimizer.param_groups中对应的学习率,另一种是更简单也是较为推荐的做法——新建优化器,由于optimizer十分轻量级,构建开销很小,故而可以构建新的optimizer。但是后者对于使用动量的优化器(如Adam),会丢失动量等状态信息,可能会造成损失函数的收敛出现震荡等情况。

# 调整学习率
for param_group in optimizer.param_groups:
    param_group['lr'] *= 0.1 # 学习率为之前的0.1倍

1.3.7 训练模型

在使用Gluon训练模型时,我们通过调用optim实例的step函数来迭代模型参数。按照小批量随机梯度下降的定义,我们在step函数中指明批量大小,从而对批量中样本梯度求平均。

num_epochs = 3
for epoch in range(1, num_epochs + 1):
    for X, y in data_iter:
        output = net(X)
        l = loss(output, y.view(-1, 1))
        optimizer.zero_grad() # 梯度清零,等价于net.zero_grad()
        l.backward()
        optimizer.step()
    print('epoch %d, loss: %f' % (epoch, l.item()))

输出:

epoch 1, loss: 0.000457
epoch 2, loss: 0.000081
epoch 3, loss: 0.000198

下面我们分别比较学到的模型参数和真实的模型参数。我们从net获得需要的层,并访问其权重(weight)和偏差(bias)。学到的参数和真实的参数很接近。

dense = net[0]
print(true_w, dense.weight)
print(true_b, dense.bias)

输出:

[2, -3.4] tensor([[ 1.9999, -3.4005]])
4.2 tensor([4.2011])

2.1 softmax回归

2.1.1 softmax回归模型

softmax回归跟线性回归一样将输入特征与权重做线性叠加。与线性回归的一个主要不同在于,softmax回归的输出值个数等于标签里的类别数。因为一共有4种特征和3种输出动物类别,所以权重包含12个标量(带下标的 w w w)、偏差包含3个标量(带下标的 b b b),且对每个输入计算 o 1 , o 2 , o 3 o_1, o_2, o_3 o1,o2,o3这3个输出:

o 1 = x 1 w 11 + x 2 w 21 + x 3 w 31 + x 4 w 41 + b 1 , o 2 = x 1 w 12 + x 2 w 22 + x 3 w 32 + x 4 w 42 + b 2 , o 3 = x 1 w 13 + x 2 w 23 + x 3 w 33 + x 4 w 43 + b 3 . \begin{aligned} o_1 &= x_1 w_{11} + x_2 w_{21} + x_3 w_{31} + x_4 w_{41} + b_1,\\ o_2 &= x_1 w_{12} + x_2 w_{22} + x_3 w_{32} + x_4 w_{42} + b_2,\\ o_3 &= x_1 w_{13} + x_2 w_{23} + x_3 w_{33} + x_4 w_{43} + b_3. \end{aligned} o1o2o3=x1w11+x2w21+x3w31+x4w41+b1,=x1w12+x2w22+x3w32+x4w42+b2,=x1w13+x2w23+x3w33+x4w43+b3.
既然分类问题需要得到离散的预测输出,一个简单的办法是将输出值 o i o_i oi当作预测类别是 i i i的置信度,并将值最大的输出所对应的类作为预测输出,即输出 arg ⁡ max ⁡ i o i \underset{i}{\arg\max} o_i iargmaxoi。例如,如果 o 1 , o 2 , o 3 o_1,o_2,o_3 o1,o2,o3分别为 0.1 , 10 , 0.1 0.1,10,0.1 0.1,10,0.1,由于 o 2 o_2 o2最大,那么预测类别为2,其代表猫。

然而,直接使用输出层的输出有两个问题。一方面,由于输出层的输出值的范围不确定,我们难以直观上判断这些值的意义。例如,刚才举的例子中的输出值10表示“很置信”图像类别为猫,因为该输出值是其他两类的输出值的100倍。但如果 o 1 = o 3 = 1 0 3 o_1=o_3=10^3 o1=o3=103,那么输出值10却又表示图像类别为猫的概率很低。另一方面,由于真实标签是离散值,这些离散值与不确定范围的输出值之间的误差难以衡量。

softmax运算符(softmax operator)解决了以上两个问题。它通过下式将输出值变换成值为正且和为1的概率分布:

y ^ 1 , y ^ 2 , y ^ 3 = softmax ( o 1 , o 2 , o 3 ) \hat{y}_1, \hat{y}_2, \hat{y}_3 = \text{softmax}(o_1, o_2, o_3) y^1,y^2,y^3=softmax(o1,o2,o3)

其中

y ^ 1 = exp ⁡ ( o 1 ) ∑ i = 1 3 exp ⁡ ( o i ) , y ^ 2 = exp ⁡ ( o 2 ) ∑ i = 1 3 exp ⁡ ( o i ) , y ^ 3 = exp ⁡ ( o 3 ) ∑ i = 1 3 exp ⁡ ( o i ) . \hat{y}_1 = \frac{ \exp(o_1)}{\sum_{i=1}^3 \exp(o_i)},\quad \hat{y}_2 = \frac{ \exp(o_2)}{\sum_{i=1}^3 \exp(o_i)},\quad \hat{y}_3 = \frac{ \exp(o_3)}{\sum_{i=1}^3 \exp(o_i)}. y^1=i=13exp(oi)exp(o1),y^2=i=13exp(oi)exp(o2),y^3=i=13exp(oi)exp(o3).

容易看出 y ^ 1 + y ^ 2 + y ^ 3 = 1 \hat{y}_1 + \hat{y}_2 + \hat{y}_3 = 1 y^1+y^2+y^3=1 0 ≤ y ^ 1 , y ^ 2 , y ^ 3 ≤ 1 0 \leq \hat{y}_1, \hat{y}_2, \hat{y}_3 \leq 1 0y^1,y^2,y^31,因此 y ^ 1 , y ^ 2 , y ^ 3 \hat{y}_1, \hat{y}_2, \hat{y}_3 y^1,y^2,y^3是一个合法的概率分布。这时候,如果 y ^ 2 = 0.8 \hat{y}_2=0.8 y^2=0.8,不管 y ^ 1 \hat{y}_1 y^1 y ^ 3 \hat{y}_3 y^3的值是多少,我们都知道图像类别为猫的概率是80%。此外,我们注意到

arg ⁡ max ⁡ i o i = arg ⁡ max ⁡ i y ^ i \underset{i}{\arg\max} o_i = \underset{i}{\arg\max} \hat{y}_i iargmaxoi=iargmaxy^i

因此softmax运算不改变预测类别输出。

2.1.2 单样本分类的矢量计算表达式

为了提高计算效率,我们可以将单样本分类通过矢量计算来表达。在上面的图像分类问题中,假设softmax回归的权重和偏差参数分别为

W = [ w 11 w 12 w 13 w 21 w 22 w 23 w 31 w 32 w 33 w 41 w 42 w 43 ] , b = [ b 1 b 2 b 3 ] , \boldsymbol{W} = \begin{bmatrix} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \\ w_{31} & w_{32} & w_{33} \\ w_{41} & w_{42} & w_{43} \end{bmatrix},\quad \boldsymbol{b} = \begin{bmatrix} b_1 & b_2 & b_3 \end{bmatrix}, W=w11w21w31w41w12w22w32w42w13w23w33w43,b=[b1b2b3],

设高和宽分别为2个像素的图像样本 i i i的特征为

x ( i ) = [ x 1 ( i ) x 2 ( i ) x 3 ( i ) x 4 ( i ) ] , \boldsymbol{x}^{(i)} = \begin{bmatrix}x_1^{(i)} & x_2^{(i)} & x_3^{(i)} & x_4^{(i)}\end{bmatrix}, x(i)=[x1(i)x2(i)x3(i)x4(i)],

输出层的输出为

o ( i ) = [ o 1 ( i ) o 2 ( i ) o 3 ( i ) ] , \boldsymbol{o}^{(i)} = \begin{bmatrix}o_1^{(i)} & o_2^{(i)} & o_3^{(i)}\end{bmatrix}, o(i)=[o1(i)o2(i)o3(i)],

预测为狗、猫或鸡的概率分布为

y ^ ( i ) = [ y ^ 1 ( i ) y ^ 2 ( i ) y ^ 3 ( i ) ] . \boldsymbol{\hat{y}}^{(i)} = \begin{bmatrix}\hat{y}_1^{(i)} & \hat{y}_2^{(i)} & \hat{y}_3^{(i)}\end{bmatrix}. y^(i)=[y^1(i)y^2(i)y^3(i)].

softmax回归对样本 i i i分类的矢量计算表达式为

o ( i ) = x ( i ) W + b , y ^ ( i ) = softmax ( o ( i ) ) . \begin{aligned} \boldsymbol{o}^{(i)} &= \boldsymbol{x}^{(i)} \boldsymbol{W} + \boldsymbol{b},\\ \boldsymbol{\hat{y}}^{(i)} &= \text{softmax}(\boldsymbol{o}^{(i)}). \end{aligned} o(i)y^(i)=x(i)W+b,=softmax(o(i)).

2.1.3 小批量样本分类的矢量计算表达式

为了进一步提升计算效率,我们通常对小批量数据做矢量计算。广义上讲,给定一个小批量样本,其批量大小为 n n n,输入个数(特征数)为 d d d,输出个数(类别数)为 q q q。设批量特征为 X ∈ R n × d \boldsymbol{X} \in \mathbb{R}^{n \times d} XRn×d。假设softmax回归的权重和偏差参数分别为 W ∈ R d × q \boldsymbol{W} \in \mathbb{R}^{d \times q} WRd×q b ∈ R 1 × q \boldsymbol{b} \in \mathbb{R}^{1 \times q} bR1×q。softmax回归的矢量计算表达式为

O = X W + b , Y ^ = softmax ( O ) , \begin{aligned} \boldsymbol{O} &= \boldsymbol{X} \boldsymbol{W} + \boldsymbol{b},\\ \boldsymbol{\hat{Y}} &= \text{softmax}(\boldsymbol{O}), \end{aligned} OY^=XW+b,=softmax(O),

其中的加法运算使用了广播机制, O , Y ^ ∈ R n × q \boldsymbol{O}, \boldsymbol{\hat{Y}} \in \mathbb{R}^{n \times q} O,Y^Rn×q且这两个矩阵的第 i i i行分别为样本 i i i的输出 o ( i ) \boldsymbol{o}^{(i)} o(i)和概率分布 y ^ ( i ) \boldsymbol{\hat{y}}^{(i)} y^(i)

2.1.4 交叉熵损失函数

前面提到,使用softmax运算后可以更方便地与离散标签计算误差。我们已经知道,softmax运算将输出变换成一个合法的类别预测分布。实际上,真实标签也可以用类别分布表达:对于样本 i i i,我们构造向量 y ( i ) ∈ R q \boldsymbol{y}^{(i)}\in \mathbb{R}^{q} y(i)Rq ,使其第 y ( i ) y^{(i)} y(i)(样本 i i i类别的离散数值)个元素为1,其余为0。这样我们的训练目标可以设为使预测概率分布 y ^ ( i ) \boldsymbol{\hat y}^{(i)} y^(i)尽可能接近真实的标签概率分布 y ( i ) \boldsymbol{y}^{(i)} y(i)

我们可以像线性回归那样使用平方损失函数 ∥ y ^ ( i ) − y ( i ) ∥ 2 / 2 \|\boldsymbol{\hat y}^{(i)}-\boldsymbol{y}^{(i)}\|^2/2 y^(i)y(i)2/2。然而,想要预测分类结果正确,我们其实并不需要预测概率完全等于标签概率。例如,在图像分类的例子里,如果 y ( i ) = 3 y^{(i)}=3 y(i)=3,那么我们只需要 y ^ 3 ( i ) \hat{y}^{(i)}_3 y^3(i)比其他两个预测值 y ^ 1 ( i ) \hat{y}^{(i)}_1 y^1(i) y ^ 2 ( i ) \hat{y}^{(i)}_2 y^2(i)大就行了。即使 y ^ 3 ( i ) \hat{y}^{(i)}_3 y^3(i)值为0.6,不管其他两个预测值为多少,类别预测均正确。而平方损失则过于严格,例如 y ^ 1 ( i ) = y ^ 2 ( i ) = 0.2 \hat y^{(i)}_1=\hat y^{(i)}_2=0.2 y^1(i)=y^2(i)=0.2 y ^ 1 ( i ) = 0 , y ^ 2 ( i ) = 0.4 \hat y^{(i)}_1=0, \hat y^{(i)}_2=0.4 y^1(i)=0,y^2(i)=0.4的损失要小很多,虽然两者都有同样正确的分类预测结果。

改善上述问题的一个方法是使用更适合衡量两个概率分布差异的测量函数。其中,交叉熵(cross entropy)是一个常用的衡量方法:

H ( y ( i ) , y ^ ( i ) ) = − ∑ j = 1 q y j ( i ) log ⁡ y ^ j ( i ) , H\left(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}\right ) = -\sum_{j=1}^q y_j^{(i)} \log \hat y_j^{(i)}, H(y(i),y^(i))=j=1qyj(i)logy^j(i),

其中带下标的 y j ( i ) y_j^{(i)} yj(i)是向量 y ( i ) \boldsymbol y^{(i)} y(i)中非0即1的元素,需要注意将它与样本 i i i类别的离散数值,即不带下标的 y ( i ) y^{(i)} y(i)区分。在上式中,我们知道向量 y ( i ) \boldsymbol y^{(i)} y(i)中只有第 y ( i ) y^{(i)} y(i)个元素 y y ( i ) ( i ) y^{(i)}_{y^{(i)}} yy(i)(i)为1,其余全为0,于是 H ( y ( i ) , y ^ ( i ) ) = − log ⁡ y ^ y ( i ) ( i ) H(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}) = -\log \hat y_{y^{(i)}}^{(i)} H(y(i),y^(i))=logy^y(i)(i)。也就是说,交叉熵只关心对正确类别的预测概率,因为只要其值足够大,就可以确保分类结果正确。当然,遇到一个样本有多个标签时,例如图像里含有不止一个物体时,我们并不能做这一步简化。但即便对于这种情况,交叉熵同样只关心对图像中出现的物体类别的预测概率。

假设训练数据集的样本数为 n n n,交叉熵损失函数定义为
ℓ ( Θ ) = 1 n ∑ i = 1 n H ( y ( i ) , y ^ ( i ) ) , \ell(\boldsymbol{\Theta}) = \frac{1}{n} \sum_{i=1}^n H\left(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}\right ), (Θ)=n1i=1nH(y(i),y^(i)),

其中 Θ \boldsymbol{\Theta} Θ代表模型参数。同样地,如果每个样本只有一个标签,那么交叉熵损失可以简写成 ℓ ( Θ ) = − ( 1 / n ) ∑ i = 1 n log ⁡ y ^ y ( i ) ( i ) \ell(\boldsymbol{\Theta}) = -(1/n) \sum_{i=1}^n \log \hat y_{y^{(i)}}^{(i)} (Θ)=(1/n)i=1nlogy^y(i)(i)。从另一个角度来看,我们知道最小化 ℓ ( Θ ) \ell(\boldsymbol{\Theta}) (Θ)等价于最大化 exp ⁡ ( − n ℓ ( Θ ) ) = ∏ i = 1 n y ^ y ( i ) ( i ) \exp(-n\ell(\boldsymbol{\Theta}))=\prod_{i=1}^n \hat y_{y^{(i)}}^{(i)} exp(n(Θ))=i=1ny^y(i)(i),即最小化交叉熵损失函数等价于最大化训练数据集所有标签类别的联合预测概率

2.1.5 模型预测及评价

在训练好softmax回归模型后,给定任一样本特征,就可以预测每个输出类别的概率。通常,我们把预测概率最大的类别作为输出类别。如果它与真实类别(标签)一致,说明这次预测是正确的。在3.6节的实验中,我们将使用准确率(accuracy)来评价模型的表现。它等于正确预测数量与总预测数量之比。

  • softmax回归适用于分类问题。它使用softmax运算输出类别的概率分布。
  • softmax回归是一个单层神经网络,输出个数等于分类问题中的类别个数。
  • 交叉熵适合衡量两个概率分布的差异。

2.2 softmax回归的从零开始实现

首先导入本节实现所需的包或模块。

import torch
import torchvision
import numpy as np
import sys
sys.path.append("..") # 为了导入上层目录的d2lzh_pytorch
import d2lzh_pytorch as d2l

2.2.1 获取和读取数据

我们将使用Fashion-MNIST数据集,并设置批量大小为256。

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

2.2.2 初始化模型参数

跟线性回归中的例子一样,我们将使用向量表示每个样本。已知每个样本输入是高和宽均为28像素的图像。模型的输入向量的长度是 28 × 28 = 784 28 \times 28 = 784 28×28=784:该向量的每个元素对应图像中每个像素。由于图像有10个类别,单层神经网络输出层的输出个数为10,因此softmax回归的权重和偏差参数分别为 784 × 10 784 \times 10 784×10 1 × 10 1 \times 10 1×10的矩阵。

num_inputs = 784
num_outputs = 10

W = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_outputs)), dtype=torch.float)
b = torch.zeros(num_outputs, dtype=torch.float)

同之前一样,我们需要模型参数梯度。

W.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True) 

2.2.3 实现softmax运算

在介绍如何定义softmax回归之前,我们先描述一下对如何对多维Tensor按维度操作。在下面的例子中,给定一个Tensor矩阵X。我们可以只对其中同一列(dim=0)或同一行(dim=1)的元素求和,并在结果中保留行和列这两个维度(keepdim=True)。

X = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(X.sum(dim=0, keepdim=True))
print(X.sum(dim=1, keepdim=True))

输出:

tensor([[5, 7, 9]])
tensor([[ 6],
        [15]])

下面我们就可以定义前面小节里介绍的softmax运算了。在下面的函数中,矩阵X的行数是样本数,列数是输出个数。为了表达样本预测各个输出的概率,softmax运算会先通过exp函数对每个元素做指数运算,再对exp矩阵同行元素求和,最后令矩阵每行各元素与该行元素之和相除。这样一来,最终得到的矩阵每行元素和为1且非负。因此,该矩阵每行都是合法的概率分布。softmax运算的输出矩阵中的任意一行元素代表了一个样本在各个输出类别上的预测概率。

def softmax(X):
    X_exp = X.exp()
    partition = X_exp.sum(dim=1, keepdim=True)
    return X_exp / partition  # 这里应用了广播机制

可以看到,对于随机输入,我们将每个元素变成了非负数,且每一行和为1。

X = torch.rand((2, 5))
X_prob = softmax(X)
print(X_prob, X_prob.sum(dim=1))

输出:

tensor([[0.2206, 0.1520, 0.1446, 0.2690, 0.2138],
        [0.1540, 0.2290, 0.1387, 0.2019, 0.2765]]) tensor([1., 1.])

2.2.4 定义模型

有了softmax运算,我们可以定义上节描述的softmax回归模型了。这里通过view函数将每张原始图像改成长度为num_inputs的向量。

def net(X):
    return softmax(torch.mm(X.view((-1, num_inputs)), W) + b)

2.2.5 定义损失函数

上一节中,我们介绍了softmax回归使用的交叉熵损失函数。为了得到标签的预测概率,我们可以使用gather函数。在下面的例子中,变量y_hat是2个样本在3个类别的预测概率,变量y是这2个样本的标签类别。通过使用gather函数,我们得到了2个样本的标签的预测概率。与3.4节(softmax回归)数学表述中标签类别离散值从1开始逐一递增不同,在代码中,标签类别的离散值是从0开始逐一递增的。

y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y = torch.LongTensor([0, 2])
y_hat.gather(1, y.view(-1, 1))

输出:

tensor([[0.1000],
        [0.5000]])

下面实现了3.4节(softmax回归)中介绍的交叉熵损失函数。

def cross_entropy(y_hat, y):
    return - torch.log(y_hat.gather(1, y.view(-1, 1)))

2.2.6 计算分类准确率

给定一个类别的预测概率分布y_hat,我们把预测概率最大的类别作为输出类别。如果它与真实类别y一致,说明这次预测是正确的。分类准确率即正确预测数量与总预测数量之比。

为了演示准确率的计算,下面定义准确率accuracy函数。其中y_hat.argmax(dim=1)返回矩阵y_hat每行中最大元素的索引,且返回结果与变量y形状相同。相等条件判断式(y_hat.argmax(dim=1) == y)是一个类型为ByteTensorTensor,我们用float()将其转换为值为0(相等为假)或1(相等为真)的浮点型Tensor

def accuracy(y_hat, y):
    return (y_hat.argmax(dim=1) == y).float().mean().item()

让我们继续使用在演示gather函数时定义的变量y_haty,并将它们分别作为预测概率分布和标签。可以看到,第一个样本预测类别为2(该行最大元素0.6在本行的索引为2),与真实标签0不一致;第二个样本预测类别为2(该行最大元素0.5在本行的索引为2),与真实标签2一致。因此,这两个样本上的分类准确率为0.5。

print(accuracy(y_hat, y))

输出:

0.5

类似地,我们可以评价模型net在数据集data_iter上的准确率。

# 本函数已保存在d2lzh_pytorch包中方便以后使用。该函数将被逐步改进:它的完整实现将在“图像增广”一节中描述
def evaluate_accuracy(data_iter, net):
    acc_sum, n = 0.0, 0
    for X, y in data_iter:
        acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
        n += y.shape[0]
    return acc_sum / n

因为我们随机初始化了模型net,所以这个随机模型的准确率应该接近于类别个数10的倒数即0.1。

print(evaluate_accuracy(test_iter, net))

输出:

0.0681

2.2.7 训练模型

训练softmax回归的实现跟3.2(线性回归的从零开始实现)一节介绍的线性回归中的实现非常相似。我们同样使用小批量随机梯度下降来优化模型的损失函数。在训练模型时,迭代周期数num_epochs和学习率lr都是可以调的超参数。改变它们的值可能会得到分类更准确的模型。

num_epochs, lr = 5, 0.1

# 本函数已保存在d2lzh包中方便以后使用
def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
              params=None, lr=None, optimizer=None):
    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
        for X, y in train_iter:
            y_hat = net(X)
            l = loss(y_hat, y).sum()
            
            # 梯度清零
            if optimizer is not None:
                optimizer.zero_grad()
            elif params is not None and params[0].grad is not None:
                for param in params:
                    param.grad.data.zero_()
            
            l.backward()
            if optimizer is None:
                d2l.sgd(params, lr, batch_size)
            else:
                optimizer.step()  # “softmax回归的简洁实现”一节将用到
            
            
            train_l_sum += l.item()
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
            n += y.shape[0]
        test_acc = evaluate_accuracy(test_iter, net)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
              % (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))

train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)

输出:

epoch 1, loss 0.7878, train acc 0.749, test acc 0.794
epoch 2, loss 0.5702, train acc 0.814, test acc 0.813
epoch 3, loss 0.5252, train acc 0.827, test acc 0.819
epoch 4, loss 0.5010, train acc 0.833, test acc 0.824
epoch 5, loss 0.4858, train acc 0.836, test acc 0.815

2.2.8 预测

训练完成后,现在就可以演示如何对图像进行分类了。给定一系列图像(第三行图像输出),我们比较一下它们的真实标签(第一行文本输出)和模型预测结果(第二行文本输出)。

X, y = iter(test_iter).next()

true_labels = d2l.get_fashion_mnist_labels(y.numpy())
pred_labels = d2l.get_fashion_mnist_labels(net(X).argmax(dim=1).numpy())
titles = [true + '\n' + pred for true, pred in zip(true_labels, pred_labels)]

d2l.show_fashion_mnist(X[0:9], titles[0:9])

2.3 softmax回归的简洁实现

首先导入所需的包或模块。

import torch
from torch import nn
from torch.nn import init
import numpy as np
import sys
sys.path.append("..") 
import d2lzh_pytorch as d2l

2.3.1 获取和读取数据

我们仍然使用Fashion-MNIST数据集和上一节中设置的批量大小。

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

2.3.2 定义和初始化模型

在3.4节(softmax回归)中提到,softmax回归的输出层是一个全连接层,所以我们用一个线性模块就可以了。因为前面我们数据返回的每个batch样本x的形状为(batch_size, 1, 28, 28), 所以我们要先用view()x的形状转换成(batch_size, 784)才送入全连接层。

num_inputs = 784
num_outputs = 10

class LinearNet(nn.Module):
    def __init__(self, num_inputs, num_outputs):
        super(LinearNet, self).__init__()
        self.linear = nn.Linear(num_inputs, num_outputs)
    def forward(self, x): # x shape: (batch, 1, 28, 28)
        y = self.linear(x.view(x.shape[0], -1))
        return y
    
net = LinearNet(num_inputs, num_outputs)

我们将对x的形状转换的这个功能自定义一个FlattenLayer并记录在d2lzh_pytorch中方便后面使用。

# 本函数已保存在d2lzh_pytorch包中方便以后使用
class FlattenLayer(nn.Module):
    def __init__(self):
        super(FlattenLayer, self).__init__()
    def forward(self, x): # x shape: (batch, *, *, ...)
        return x.view(x.shape[0], -1)

这样我们就可以更方便地定义我们的模型:

from collections import OrderedDict

net = nn.Sequential(
    # FlattenLayer(),
    # nn.Linear(num_inputs, num_outputs)
    OrderedDict([
        ('flatten', FlattenLayer()),
        ('linear', nn.Linear(num_inputs, num_outputs))
    ])
)

然后,我们使用均值为0、标准差为0.01的正态分布随机初始化模型的权重参数。

init.normal_(net.linear.weight, mean=0, std=0.01)
init.constant_(net.linear.bias, val=0) 

2.3.3 softmax和交叉熵损失函数

如果做了上一节的练习,那么你可能意识到了分开定义softmax运算和交叉熵损失函数可能会造成数值不稳定。因此,PyTorch提供了一个包括softmax运算和交叉熵损失计算的函数。它的数值稳定性更好。

loss = nn.CrossEntropyLoss()

2.3.4 定义优化算法

我们使用学习率为0.1的小批量随机梯度下降作为优化算法。

optimizer = torch.optim.SGD(net.parameters(), lr=0.1)

2.3.5 训练模型

接下来,我们使用上一节中定义的训练函数来训练模型。

num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)

输出:

epoch 1, loss 0.0031, train acc 0.745, test acc 0.790
epoch 2, loss 0.0022, train acc 0.812, test acc 0.807
epoch 3, loss 0.0021, train acc 0.825, test acc 0.806
epoch 4, loss 0.0020, train acc 0.832, test acc 0.810
epoch 5, loss 0.0019, train acc 0.838, test acc 0.823

3.1 感知机原理

3.1.1 感知机模型

感知机感知机是二类分类的线性分类模型

  • 损失函数 L ( w , b ) L(w,b) L(w,b)的经验风险最小化

  • 第一次涉及Gram Matrix G = [ x i ⋅ x j ] N × N G=[x_i\cdot x_j]_{N\times N} G=[xixj]N×N

  • 感知机的激活函数是符号函数。

  • 感知机是神经网络和支持向量机的基础。

  • 当我们讨论决策边界的时候, 实际上是在考虑算法的几何解释

  • 关于感知机为什么不能处理异或问题, 可以借助下图理解。


上面紫色和橙色为两类点, 线性的分割超平面应该要垂直于那些红粉和紫色的线.

  • 分离超平面将特征空间划分为两个部分,一部分是正类, 一部分是负类。法向量指向的一侧为正类,另一侧为负类。
  • 感知机损失函数 L = max ⁡ ( 0 , − y i ( w ⋅ x i + b ) ) L=\max(0, -y_i(w\cdot x_i+b)) L=max(0,yi(wxi+b))

3.1.2 三要素

1. 模型

输入空间: X ⊆ R n \mathcal X\sube \bf R^n XRn

输出空间: Y = + 1 , − 1 \mathcal Y={+1,-1} Y=+1,1

决策函数: f ( x ) = s i g n ( w ⋅ x + b ) f(x)=sign (w\cdot x+b) f(x)=sign(wx+b)

2. 策略

确定学习策略就是定义**(经验)**损失函数并将损失函数最小化。

注意这里提到了经验,所以学习是base在训练数据集上的操作

损失函数选择

损失函数的一个自然选择是误分类点的总数,但是,这样的损失函数不是参数 w , b w,b w,b的连续可导函数,不易优化

损失函数的另一个选择是误分类点到超平面 S S S的总距离,这是感知机所采用的

感知机学习的经验风险函数(损失函数)
L ( w , b ) = − ∑ x i ∈ M y i ( w ⋅ x i + b ) L(w,b)=-\sum_{x_i\in M}y_i(w\cdot x_i+b) L(w,b)=xiMyi(wxi+b)
其中 M M M是误分类点的集合

给定训练数据集 T T T,损失函数 L ( w , b ) L(w,b) L(w,b) w w w b b b的连续可导函数

3. 算法

原始形式

输入:KaTeX parse error: Undefined control sequence: \cal at position 52: …_N)\}\\ x_i\in \̲c̲a̲l̲ ̲X=\bf R^n\mit ,…

输出: w , b ; f ( x ) = s i g n ( w ⋅ x + b ) w,b;f(x)=sign(w\cdot x+b) w,b;f(x)=sign(wx+b)

  1. 选取初值 w 0 , b 0 w_0,b_0 w0,b0

  2. 训练集中选取数据 ( x i , y i ) (x_i,y_i) (xi,yi)

  3. 如果 y i ( w ⋅ x i + b ) ⩽ 0 y_i(w\cdot x_i+b)\leqslant 0 yi(wxi+b)0
    KaTeX parse error: Undefined control sequence: \nonumber at position 28: … w+\eta y_ix_i \̲n̲o̲n̲u̲m̲b̲e̲r̲\\ b\leftarrow …

  4. 转至(2),直至训练集中没有误分类点

注意这个原始形式中的迭代公式,可以对 x x x补1,将 w w w b b b合并在一起,合在一起的这个叫做扩充权重向量,书上有提到。

对偶形式

对偶形式的基本思想是将 w w w b b b表示为实例 x i x_i xi和标记 y i y_i yi的线性组合的形式,通过求解其系数而求得 w w w b b b

输入:KaTeX parse error: Undefined control sequence: \cal at position 52: …_N)\}\\ x_i\in \̲c̲a̲l̲{X}=\bf{R}^n , …

输出:
KaTeX parse error: Undefined control sequence: \nonumber at position 70: …cdot x+b\right)\̲n̲o̲n̲u̲m̲b̲e̲r̲\\ \alpha=(\alp…

  1. α ← 0 , b ← 0 \alpha \leftarrow 0,b\leftarrow 0 α0,b0

  2. 训练集中选取数据 ( x i , y i ) (x_i,y_i) (xi,yi)

  3. 如果 y i ( ∑ j = 1 N α j y j x j ⋅ x + b ) ⩽ 0 y_i\left(\sum_{j=1}^N\alpha_jy_jx_j\cdot x+b\right) \leqslant 0 yi(j=1Nαjyjxjx+b)0
    KaTeX parse error: Undefined control sequence: \nonumber at position 35: … \alpha_i+\eta \̲n̲o̲n̲u̲m̲b̲e̲r̲\\ b\leftarrow …

  4. 转至(2),直至训练集中没有误分类点

3.2 多层感知机的从零开始实现

首先导入实现所需的包或模块。

import torch
import numpy as np
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l

3.2.1 获取和读取数据

这里继续使用Fashion-MNIST数据集。我们将使用多层感知机对图像进行分类。

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

3.2.2 定义模型参数

num_inputs, num_outputs, num_hiddens = 784, 10, 256

W1 = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_hiddens)), dtype=torch.float)
b1 = torch.zeros(num_hiddens, dtype=torch.float)
W2 = torch.tensor(np.random.normal(0, 0.01, (num_hiddens, num_outputs)), dtype=torch.float)
b2 = torch.zeros(num_outputs, dtype=torch.float)

params = [W1, b1, W2, b2]
for param in params:
    param.requires_grad_(requires_grad=True)

3.2.3 定义激活函数

这里我们使用基础的max函数来实现ReLU,而非直接调用relu函数。

def relu(X):
    return torch.max(input=X, other=torch.tensor(0.0))

3.2.4 定义模型

同softmax回归一样,我们通过view函数将每张原始图像改成长度为num_inputs的向量。然后我们实现上一节中多层感知机的计算表达式。

def net(X):
    X = X.view((-1, num_inputs))
    H = relu(torch.matmul(X, W1) + b1)
    return torch.matmul(H, W2) + b2

3.2.5 定义损失函数

为了得到更好的数值稳定性,我们直接使用PyTorch提供的包括softmax运算和交叉熵损失计算的函数。

loss = torch.nn.CrossEntropyLoss()

3.2.6 训练模型

num_epochs, lr = 5, 100.0
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, params, lr)

输出:

epoch 1, loss 0.0030, train acc 0.714, test acc 0.753
epoch 2, loss 0.0019, train acc 0.821, test acc 0.777
epoch 3, loss 0.0017, train acc 0.842, test acc 0.834
epoch 4, loss 0.0015, train acc 0.857, test acc 0.839
epoch 5, loss 0.0014, train acc 0.865, test acc 0.845

3.3 多层感知机的ptorch实现

下面我们使用PyTorch来实现上一节中的多层感知机。首先导入所需的包或模块。

import torch
from torch import nn
from torch.nn import init
import numpy as np
import sys
sys.path.append("..") 
import d2lzh_pytorch as d2l

3.3.1 定义模型

num_inputs, num_outputs, num_hiddens = 784, 10, 256
    
net = nn.Sequential(
        d2l.FlattenLayer(),
        nn.Linear(num_inputs, num_hiddens),
        nn.ReLU(),
        nn.Linear(num_hiddens, num_outputs), 
        )

for params in net.parameters():
    init.normal_(params, mean=0, std=0.01)

3.3.2 读取数据并训练模型

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
loss = torch.nn.CrossEntropyLoss()

optimizer = torch.optim.SGD(net.parameters(), lr=0.5)

num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)

输出:

epoch 1, loss 0.0030, train acc 0.712, test acc 0.744
epoch 2, loss 0.0019, train acc 0.823, test acc 0.821
epoch 3, loss 0.0017, train acc 0.844, test acc 0.842
epoch 4, loss 0.0015, train acc 0.856, test acc 0.842
epoch 5, loss 0.0014, train acc 0.864, test acc 0.818

原书地址

中文版:动手学深度学习 | Github仓库
English Version: Dive into Deep Learning | Github Repo

你可能感兴趣的:(深度学习,深度学习)