第三章 线性神经网络
在介绍深度神经网络之前,我们需要了解神经网络训练的基础知识。本章我们将介绍神经网络的整个训练过程,包括:
- 定义简单的神经网络架构
- 数据处理
- 指定损失函数
- 如何训练模型
为了更容易学习,我们将从经典算法 —— 线性神经网络 开始,介绍神经网络的基础知识。经典统计学习 技术中的线性回归和 softmax 回归可以视为线性神经网络,这些知识将为本书其他部分中更复杂的技术奠定基础。
3.1 线性回归
回归 regression 是能为一个或多个自变量与因变量之间关系建模的一类方法。在自然科学和社会科学领域,回归经常用来表示输入和输出之间的关系。
在机器学习领域中的大多数任务通常都与 预测 prediction 有关。当我们想预测一个数值时,就会涉及到回归问题。常见的例子包括:
- 预测价格:房屋、股票等
- 预测住院时间:针对住院病人等
- 预测需求:零售销量等
但不是所有的预测都是回归问题。在后面的章节中,我们将介绍 分类问题。分类问题的目标是预测数据属于 一组类别中的哪一个。
3.1.1 线性回归的基本元素
线性回归 linear regression 可以追溯到 19 世纪初,它在回归的各种标准工具中最简单而且最流行。线性回归基于几个简单的假设:
- 首先,假设自变量
和因变量 之间的关系是线性的,即 可以表示为 中元素的加权和,这里通常允许包含观测值的一些噪声 - 其次,我们假设任何噪声都比较正常,如噪声遵循正态分布
为了解释线性回归,我们举一个实际的例子:我们希望根据房屋的面积(平方英尺)和房龄(年)来估算房屋价格(美元)。为了开发一个能预测房价的模型,我们需要收集一个真实的数据集。这个数据集包括了房屋的销售价格、面积和房龄。在机器学习的术语中:
- 训练数据集 training data set:收集到的数据集
- 训练集 training set
- 样本 sample:每行数据,比如一次房屋交易相对应的数据
- 数据点 data point
- 数据样本 data instance
- 标签 label:试图预测的目标,比如房屋的价格
- 目标 target
- 特征 feature:预测所依据的自变量:比如面积和房龄
- 协变量 covariate
通常,我们使用一个固定的字母
例如其中的
3.1.1.1 线性模型
线性假设是指 目标/标签可以表示为特征的加权和,也就是房屋价格可以表示为面积和房龄的线性加权组合。如下面的式子:
其中:
被称为 权重 weight,它决定了每个特征对预测值的影响 被称为 偏置 bias,也可以称为偏移量 offset 或者截距 intercept。偏置的几何解释是指当所有特征都取值为 0 时,预测值应该为多少 - 即使现实中不会有任何房子的面积是 0 或房龄正好是 0 年,我们仍然需要偏置项。如果没有偏置项,我们模型的表达能力将受到限制
从数学意义上来讲,线性假设方程是输入特征的一个 仿射变换 affine transformation,仿射变换的特点是通过加权和对特征进行 线性变换 linear transformation,同时通过偏置项进行 平移 translation。
对于给定的数据集,每一项数据都有三个数,分别是房价、面积和房龄,我们预先假设它们之间呈线性关系。因此我们的目标是寻找模型的权重
注意,在机器学习领域,我们通常使用的是高维数据集(每一项样本都有多个特征),建模时采用线性代数表示法会比较方便。当我们的输入包含
因此所有的特征向量都在
上式是对于单个数据样本的特征
对于 特征集合
这个过程中的求和将使用 广播机制。
注意,我们已知的是数据集,也就是训练数据的特征
接下来我们探究一下:假如特征和标签之间的关系真是线性的,那我们是否能够找到最合适的权重和偏置呢?答案是否定的,因为我们很难找到一个有
对于以上这两种情况,我们都通过引入一个噪声项来解决,考虑误差带来的影响。在开始寻找最好的 模型参数 model parameters
- 一种模型质量的度量方式
- 一种能够 更新模型 以提高模型预测质量的方法
3.1.1.2 损失函数
在我们开始考虑如何用模型 拟合 fit 数据之前,我们需要确定一个拟合程度的度量。我们用 损失函数 loss function 来量化目标的 实际值 与 预测值 之间的差距。通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为 0。因此回归问题中最常用的损失函数是 平方误差函数:当样本
常数 1/2 不会带来本质的差别,但这样在形式上稍微简单一些(对损失函数求导后系数为 1)。由于训练数据集是已知的,所以经验误差只是 关于模型参数的函数。为了进一步说明,来看下面的例子: 我们为一维情况下的回归问题绘制图像,如图所示:
由于平方误差函数中的二次方项,估计值/预测值
该式子中,
3.1.1.3 解析解
线性回归刚好是一个很简单的优化问题。与我们将在本书中所讲到的其他大部分模型不同,线性回归的解可以用一个公式简单地表达出来,这类解叫作 解析解 analytical solution。
首先,
3.1.1.4 随机梯度下降
即使在我们无法得到解析解的情况下,我们仍然可以有效地训练模型。在许多任务上,那些难以优化的模型效果要更好。因此,弄清楚如何训练这些难以优化的模型(找不到解析解的模型)是非常重要的,需要找到一个通用的解决方法。
本书中我们用到一种名为 梯度下降 gradient descent 的方法,这种方法几乎可以优化所有深度学习模型。它通过不断地在损失函数递减的方向上更新参数来降低误差/损失函数的值。
梯度下降最简单的用法是计算损失函数
3.1.2 矢量化加速
在训练我们的模型时,我们经常希望能够 同时处理整个小批量的样本。为了实现这一点,需要我们对计算进行矢量化,从而利用线性代数库,而不是在 Python 中编写开销高昂的 for
循环:
%matplotlib inline
import math
import time
import numpy as np
import torch
from d2l import torch as d2l
为了说明矢量化为什么如此重要,我们考虑对向量相加的两种方法。我们实例化两个全为 1
的 10000 维向量。在一种方法中,我们将使用 Python 的 for
循环遍历向量;在另一种方法中,我们将依赖对运算符 +
的调用。
n = 10000
a = torch.ones([n]) # for
b = torch.ones([n]) # +
由于在本书中我们将频繁地进行运行时间的基准测试,所以我们定义一个计时器:
class Timer: #@save
"""记录多次运行时间"""
def __init__(self):
self.times = []
self.start()
def start(self):
"""启动计时器"""
self.tik = time.time()
def stop(self):
"""停止计时器并将时间记录在列表中"""
self.times.append(time.time() - self.tik)
return self.times[-1]
def avg(self):
"""返回平均时间"""
return sum(self.times) / len(self.times)
def sum(self):
"""返回时间总和"""
return sum(self.times)
def cumsum(self):
"""返回累计时间"""
return np.array(self.times).cumsum().tolist()
现在我们可以对工作负载进行基准测试。
首先,我们使用 for
3.1.3 正态分布与平方损失
不知道你们会不会有个疑问,为什么损失函数就是平方损失函数呢?接下来,我们通过对噪声分布的假设来解读平方损失目标函数。
正态分布
3.2 线性回归的从零开始实现
在了解线性回归的关键思想之后,我们可以开始通过代码来动手实现线性回归了。在这一节中,我们将从零开始实现整个方法,包括:
- 数据流水线
- 模型
- 损失函数
- 小批量随机梯度下降优化器
虽然现代的深度学习框架几乎可以自动化地进行所有这些工作,但从零开始实现可以确保我们真正知道自己在做什么。同时,了解更细致的工作原理将方便我们 自定义模型、自定义层或自定义损失函数。在这一节中,我们将只使用 张量和自动求导。在之后的章节中,我们会充分利用深度学习框架的优势,介绍更简洁的实现方式。
%matplotlib inline
import random
import torch
from d2l import torch as d2l
3.2.1 生成数据集
为了简单起见,我们将根据带有噪声的线性模型构造一个人造数据集。我们的任务是使用这个有限样本的数据集来恢复这个模型的参数。我们将使用低维数据,这样可以很容易地将其可视化。在下面的代码中,我们生成一个包含 1000 个样本的数据集, 每个样本包含从标准正态分布中采样的 2 个特征。我们的合成数据集是一个矩阵
我们认为:
def synthetic_data(w, b, num_examples): #@save
"""生成 y = Xw + b + 噪声"""
X = torch.normal(0, 1, (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)
3.2.2 读取数据集
回想一下,训练模型时要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新我们的模型。由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数,该函数能 打乱数据集中的样本 并以小批量方式获取数据。
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]