动手学深度学习(Pytorch)第2章深度学习基础-上

原书籍出处

https://raw.githubusercontent.com/OUCMachineLearning/OUCML/master/BOOK/Dive-into-DL-PyTorch.pdf

本书GitHub代码链接为:Dive-Into-Deep-Learning-PyTorch-PDF。

第2章 深度学习基础

作为机器学习的一类,深度学习通常基于神经网络模型逐级表示越来越抽象的概念或模式

在本章的前几节,我们先介绍单层神经网络——线性回归和softmax回归

2.1 线性回归

线性回归输出是一个连续值,因此适用于回归问题。回归问题在实际中很常见,如预测房屋价格、 气温、销售额等连续值的问题。与回归问题不同,分类问题中模型的最终输出是一个离散值。我们所说的图像分类、垃圾邮件识别、疾病检测等输出为离散值的问题都属于分类问题的范畴。softmax回归则适用于分类问题

2.1.1 线性回归的基本要素

1.模型

我们需要建立基于输入和来计算输出的表达式,也就是模型(model)。顾名思义,线性回归假设输出与各个输入之间是线性关系:

2-1

其中 w1和w2是权重(weight),b是偏差(bias),且均为标量。它们是线性回归模型的参数 (parameter)。模型输出y^是线性回归对真实价格的预测或估计。我们通常允许它们之间有一定误差。

2.模型训练

需要通过数据来寻找特定的模型参数值,使模型在数据上的误差尽可能小。这个过程叫作模型训练(model training)。

3.训练数据

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

4.损失函数

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

显然,误差越小表示预测价格与真实价格越相近,且当二者相等时误差为0。给定训练数据集,这个误差只与模型参数相关,因此我们将它记为以模型参数为参数的函数。在机器学习里,将衡量误差的函数称为损失函数(loss function)。平方误差函数也称为平方损失(square loss)

在模型训练中,我们希望找出一组模型参数 ,来使训练样本平均损失最小。

5.优化算法

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

在求数值解的优化算法中,**小批量随机梯度下降(mini-batch stochastic gradient descent)**在深度学习中被广泛使用。它的算法很简单:1.先选取一组模型参数的初始值,如随机选取;2.接下来对参数进行多次迭代,使每次迭代都可能降低损失函数的值。

在每次迭代中,先随机均匀采样一个由固定数目训练数据样本所组成的小批量(mini-batch) B B B,然后求小批量中数据样本的平均损失有关模型参数的导 数(梯度),最后用此结果与预先设定的一个正数的乘积作为模型参数在本次迭代的减小量。

动手学深度学习(Pytorch)第2章深度学习基础-上_第1张图片

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

梯度累积

所谓梯度累积,其实很简单,我们梯度下降所用的梯度,实际上是多个样本算出来的梯度的平均值,以 batch_size=128 为例,你可以一次性算出 128 个样本的梯度然后平均,我也可以每次算 16 个样本的平均梯度,然后缓存累加起来,算够了 8 次之后,然后把总梯度除以 8,然后才执行参数更新。当然,必须累积到了 8 次之后,用 8 次的平均梯度才去更新参数,不能每算 16 个就去更新一次,不然就是 batch_size=16 了。

6.模型预测

我们使用优化算法停止时的模型参数进行预测,这就叫做模型预测、模型推断或模型测试。

2.1.2 线性回归的表示方法

1.神经网络图

在深度学习中,我们可以使用神经网络图直观地表现模型结构。为了更清晰地展示线性回归作为神 经网络的结构,下图使用神经网络图表示本节中介绍的线性回归模型。神经网络图隐去了模型参数权重和偏差。

动手学深度学习(Pytorch)第2章深度学习基础-上_第2张图片

在上图所示的神经网络中,输入分别为 x 1 x_1 x1 x 2 x_2 x2,因此输入层的输入个数为2。输入个数也叫特征数或特征向量维度。图中网络的输出为 o o o,输出层的输出个数为1。需要注意的是,我们直接将图中神经 网络的输出 o o o作为线性回归的输出,即 y = 0 y=0 y=0。由于输入层并不涉及计算,按照惯例,图中所示的神经网络的层数为1。所以,线性回归是一个单层神经网络。输出层中负责计算的单元又叫神经元。在线性回归中, o o o的计算依赖于 x 1 x_1 x1 x 2 x_2 x2 。也就是说,输出层中的神经元和输入层中各个输入完全连接。因此,这里的输出层又叫全连接层(fully-connected layer)或稠密层(dense layer)

2.矢量计算表达式

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

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

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

In [2]:
	from time import time
	start = time()
	c = torch.zeros(1000)
	for i in range(1000):
		c[i] = a[i] + b[i]
	time() - start
Out [2]:
	0.030505657196044922

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

In [3]:
	start = time()
	d = a + b
	time() - start
Out [3]:
	0.00029587745666503906

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

小结

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

2.2 线性回归从零开始实现

只利用Tensorautograd来实现一个线性回归的训练。

首先,导入本节中实验所需的包或模块,其中的matplotlib包可用于作图。

import matplotlib.pyplot as plt
import random

2.2.1 生成数据集

我们使用线性回归模型真实权重 w = [ 2 , − 3.4 ] T w=[2, -3.4]^{T} w=[2,3.4]T和偏差 b = 4.2 b=4.2 b=4.2,以及一个随机噪声项 ε ε ε来生成标签.
y = X w + b + ε y=Xw+b+ε y=Xw+b+ε
其中噪声项 ε ε ε服从均值为0、标准差为0.01的正态分布。噪声代表了数据集中无意义的干扰。下面,让我 们生成数据集。

num_inputs = 2  #输入数目
num_examples = 1000  #样本数目
true_w = [2, -3.4]  #权重w的真实值
true_b = 4.2  #偏差b的真实值
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的向量(标 量)。

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

from IPython import display
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

set_figsize()
# 加分号只显示图
plt.scatter(features[:, 1].numpy(), labels.numpy(), 1);

动手学深度学习(Pytorch)第2章深度学习基础-上_第3张图片

我们将上面的 plt 作图函数以及 use_svg_display 函数和 set_figsize 函数定义在 d2lzh 包里。 以后在作图时,我们在作图前只需要调用 d2lzh.set_figsize() 即可打印矢量图并设置图的尺寸。

2.2.2 读取数据集

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

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,所以使用min
        j = torch.LongTensor(indices[i: min(i+batch_size,
        num_examples)])
        # index_select函数根据索引返回对应元素
        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([[-0.5465, -0.2522],
        [ 0.6113, -0.6595],
        [-0.2123,  1.8336],
        [ 0.5018,  0.5985],
        [ 1.6002, -0.6208],
        [-0.8184, -0.7053],
        [-0.4041, -0.5415],
        [-2.1808,  0.4398],
        [-0.1148, -1.2187],
        [-0.0279,  0.8764]]) tensor([ 3.9856,  7.6651, -2.4446,  3.1809,  9.4999,  4.9571,  5.2375, -1.6708,
         8.1219,  1.1881])

2.2.3 初始化模型参数

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

w = torch.tensor(np.random.normal(0, 0.01, (num_inputs, 1)),
dtype=torch.float32)  # 均值为0、标准差为0.01的正态随机数
b = torch.zeros(1, dtype=torch.float32)  # 偏差为0

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

w.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)
tensor([0.], requires_grad=True)

2.2.4 定义模型

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

def linreg(X, w, b):
    '''
    计算线性回归值
    
    X: 数据
    w: 权重
    b: 偏差
    '''
    return torch.mm(X, w) + b

2.2.5 定义损失函数

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

def squared_loss(y_hat, y):
    '''
    计算平方损失
    '''
    return (y_hat - y.view(y_hat.size())) ** 2 / 2

2.2.6 定义优化函数

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

def sgd(params, lr, batch_size):
    '''
    小批量随机梯度下降
    
    params: 权重
    lr: 学习率
    batch_size: 批大小
    '''
    for param in params:
        param.data -= lr * param.grad / batch_size

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

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

lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss
# 训练模型一共需要num_epochs个迭代周期
for epoch in range(num_epochs):
    # 在每一个迭代周期中,会使用训练集中所有样本一次(假设样本数能够被批量大小整除)。
    # X和y分别是小批量样本的特征和标签
    for X, y in data_iter(batch_size, features, labels):
        # l是有关小批量X和y的损失
        l = loss(net(X, w, b), y).sum()
        # 小批量损失对模型参数求梯度
        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.050391
epoch 2, loss 0.000224
epoch 3, loss 0.000055

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

print((true_w, w))
print((true_b, b))
([2, -3.4], tensor([[ 1.9988],
        [-3.3994]], requires_grad=True))
(4.2, tensor([4.1992], requires_grad=True))

小结

  • 可以看出,仅使用 Tensor 和 autograd 模块就可以很容易地实现一个模型。

2.3 线性回归的简洁实现

你可能感兴趣的:(机器学习,pytorch,深度学习,神经网络)