第3章 线性神经网络
3.1. 线性回归
3.1.1. 线性回归的基本元素
3.1.1.1. 线性模型
3.1.1.2. 损失函数
3.1.1.3. 解析解
线性回归刚好是一个很简单的优化问题。 与我们将在本书中所讲到的其他大部分模型不同,线性回归的解可以用一个公式简单地表达出来, 这类解叫作解析解(analytical solution)。
3.1.1.4. 随机梯度下降
即使在我们无法得到解析解的情况下,我们仍然可以有效地训练模型。 在许多任务上,那些难以优化的模型效果要更好。 因此,弄清楚如何训练这些难以优化的模型是非常重要的。
本书中我们用到一种名为梯度下降(gradient descent)的方法, 这种方法几乎可以优化所有深度学习模型。 它通过不断地在损失函数递减的方向上更新参数来降低误差。
梯度下降最简单的用法是计算损失函数(数据集中所有样本的损失均值) 关于模型参数的导数(在这里也可以称为梯度)。 但实际中的执行可能会非常慢:因为在每一次更新参数之前,我们必须遍历整个数据集。 因此,我们通常会在每次需要计算更新的时候随机抽取一小批样本, 这种变体叫做小批量随机梯度下降(minibatch stochastic gradient descent)。
批量大小和学习率的值通常是手动预先指定,而不是通过模型训练得到的。 这些可以调整但不在训练过程中更新的参数称为超参数(hyperparameter)。
调参(hyperparameter tuning)是选择超参数的过程。 超参数通常是我们根据训练迭代结果来调整的, 而训练迭代结果是在独立的验证数据集(validation dataset)上评估得到的。
3.1.2. 矢量化加速
在训练我们的模型时,我们经常希望能够同时处理整个小批量的样本。 为了实现这一点,需要我们对计算进行矢量化, 从而利用线性代数库,而不是在Python中编写开销高昂的for循环。
%matplotlib inline import math import time import numpy as np import torch from d2l import torch as d2l
n = 10000 a = torch.ones(n) 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()
为了说明矢量化为什么如此重要,我们考虑对向量相加的两种方法。 我们实例化两个全为1的10000维向量。
在一种方法中,我们将使用Python的for循环遍历向量; 在另一种方法中,我们将依赖对+的调用。
c = torch.zeros(n) timer = Timer() for i in range(n): c[i] = a[i] + b[i] f'{timer.stop():.5f} sec'
'0.08062 sec'
或者,我们使用重载的+运算符来计算按元素的和。
timer.start() d = a + b f'{timer.stop():.5f} sec'
'0.00000 sec'
结果很明显,第二种方法比第一种方法快得多。 矢量化代码通常会带来数量级的加速。
3.1.3. 正态分布与平方损失
定义一个Python函数来计算正态分布。
def normal(x, mu, sigma): p = 1 / math.sqrt(2 * math.pi * sigma**2) return p * np.exp(-0.5 / sigma**2 * (x - mu)**2)
我们现在可视化正态分布。
# 再次使用numpy进行可视化 x = np.arange(-7, 7, 0.01) # 均值和标准差对 params = [(0, 1), (0, 2), (3, 1)] d2l.plot(x, [normal(x, mu, sigma) for mu, sigma in params], xlabel='x', ylabel='p(x)', figsize=(4.5, 2.5), legend=[f'mean {mu}, std {sigma}' for mu, sigma in params])
3.2 线性回归的从零开始实现
%matplotlib inline import random import torch from d2l import torch as d2l
3.2.1. 生成数据集
def synthetic_data(w, b, num_examples): #@save """生成y=Xw+b+噪声""" X = torch.normal(0, 1, (num_examples, len(w))) #生成均值为0,方差为1的随机数,size:n个样本*w的长度 y = torch.matmul(X, w) + b y += torch.normal(0, 0.01, y.shape) #加噪音 return X, y.reshape((-1, 1)) #y做成列向量返回 # 定义真实的w和b true_w = torch.tensor([2, -3.4]) true_b = 4.2 # 生成特征和标注 features, labels = synthetic_data(true_w, true_b, 1000) print(features[:5]) print(labels[:5])
tensor([[-0.0121, -0.3654], [-0.1914, -0.8442], [ 2.0900, 1.2727], [ 1.7094, -0.0526], [-0.6532, 0.5247]]) tensor([[5.4116], [6.6812], [4.0552], [7.7907], [1.1062]])
注意,features中的每一行都包含一个二维数据样本, labels中的每一行都包含一维标签值(一个标量)。
print('features:', features[0],'\nlabel:', labels[0]) #输出第0个样本的信息
features: tensor([-0.0121, -0.3654]) label: tensor([5.4116])
通过生成第二个特征features[:, 1]和labels的散点图, 可以直观观察到两者之间的线性关系。
d2l.set_figsize() d2l.plt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1);
3.2.2 读取数据集
回想一下,训练模型时要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新我们的模型。 由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数, 该函数能打乱数据集中的样本并以小批量方式获取数据。
在下面的代码中,我们定义一个data_iter函数, 该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size的小批量。 每个小批量包含一组特征和标签。
# 读取小批量 def data_iter(batch_size, features, labels): num_examples = len(labels) indices = list(range(num_examples)) # 生成每个样本的index # 这些样本是随机读取的,没有特定的顺序 random.shuffle(indices) for i in range(0, num_examples, batch_size): # batch_size步长 batch_indices = torch.tensor(indices[i : min(i+batch_size, num_examples)]) yield features[batch_indices], labels[batch_indices]
每次随机选取10个样本参与计算
batch_size = 10 for X, y in data_iter(batch_size, features, labels): print(X, '\n', y) break
tensor([[ 0.4074, -0.8667], [ 0.2685, -1.4512], [ 0.4134, 0.5147], [ 2.1151, 0.2318], [-0.0404, -0.1825], [-0.0340, 0.8932], [ 0.2615, 0.3789], [-0.0181, 1.2089], [-0.2787, -0.2607], [-0.2790, 0.2957]]) tensor([[7.9558], [9.6703], [3.3044], [7.6358], [4.7363], [1.0928], [3.4373], [0.0567], [4.5363], [2.6374]])
3.2.3. 初始化模型参数
#w,b需要计算梯度 w = torch.normal(0, 0.01, size=(2,1), requires_grad=True) #w是长为2的向量 print(w) b = torch.zeros(1, requires_grad=True)
tensor([[-0.0079], [-0.0156]], requires_grad=True)
3.2.4. 定义模型
def linreg(X, w, b): #@save """线性回归模型""" return torch.matmul(X, w) + b
X维度:(样本个数,2)
w维度:(2,1)
b为标量,这里用到广播机制
返回结果维度:(样本个数,1)
3.2.5. 定义损失函数
def squared_loss(y_hat, y): #y_hat为预测值,y为真实值 """均方损失""" return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
3.2.6. 定义优化算法
梯度下降
因为我们计算的损失是一个批量样本的总和,所以我们用批量大小(batch_size) 来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。
def sgd(params, lr, batch_size): #所有参数w,b 学习率 batch_size """小批量随机梯度下降""" with torch.no_grad(): #更新的时候不需要计算梯度 for param in params: # 每一个参数,可能是w,可能是b param -= lr * param.grad / batch_size # 梯度下降,所以减去 param.grad.zero_() # 梯度设成0,方便下一次梯度计算 不累加梯度
3.2.7. 训练
这里的迭代周期个数num_epochs和学习率lr都是超参数,分别设为3和0.03。 设置超参数很棘手,需要通过反复试验进行调整
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() sgd([w, b], lr, batch_size) # 使用参数的梯度更新参数 #print(net(features, w, b).shape, labels.shape) 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.000050 epoch 2, loss 0.000050 epoch 3, loss 0.000050
print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}') print(f'b的估计误差: {true_b - b}')
w的估计误差: tensor([ 0.0008, -0.0009], grad_fn=<SubBackward0>) b的估计误差: tensor([0.0004], grad_fn=<RsubBackward1>)
3.3. 线性回归的简洁实现
3.3.1. 生成数据集
import numpy as np import torch from torch.utils import data from d2l import torch as d2l true_w = torch.tensor([2, -3.4]) true_b = 4.2 features, labels = d2l.synthetic_data(true_w, true_b, 1000) features[:5], labels[:5]
(tensor([[ 1.6126, -0.3805], [-1.4653, 1.5517], [-0.3078, -0.1138], [-2.2826, 1.4290], [ 0.1115, -1.7052]]), tensor([[ 8.7295], [-4.0137], [ 3.9659], [-5.2199], [10.2216]]))
3.3.2. 读取数据集
我们可以调用框架中现有的API来读取数据。 我们将features和labels作为API的参数传递,并通过数据迭代器指定batch_size。 此外,布尔值is_train表示是否希望数据迭代器对象在每个迭代周期内打乱数据
def load_array(data_arrays, batch_size, is_train=True): #@save """构造一个PyTorch数据迭代器""" dataset = data.TensorDataset(*data_arrays) return data.DataLoader(dataset, batch_size, shuffle=is_train) # 每次随机挑选 batch_size = 10 # 每次从样本中随机采样十个 data_iter = load_array((features, labels), batch_size)
next(iter(data_iter))
[tensor([[ 0.0539, 2.2867], [-0.4324, -1.5015], [-1.2343, 0.1497], [-0.5834, -0.2978], [ 0.2663, -0.6757], [-0.6002, -0.3529], [-1.7964, -0.1168], [ 0.3740, -0.1144], [-0.1221, 1.1180], [-0.2655, 1.1593]]), tensor([[-3.4808], [ 8.4551], [ 1.2200], [ 4.0385], [ 7.0235], [ 4.1996], [ 1.0143], [ 5.3379], [ 0.1421], [-0.2705]])]
3.3.3. 定义模型
Sequential类将多个层串联在一起。
当给定输入数据时,Sequential实例将数据传入到第一层, 然后将第一层的输出作为第二层的输入,以此类推。
Sequential类将多个层串联在一起。 当给定输入数据时,Sequential实例将数据传入到第一层, 然后将第一层的输出作为第二层的输入,以此类推。
# nn是神经网络的缩写 from torch import nn net = nn.Sequential(nn.Linear(2, 1)) #指定输入输出维度
3.3.4. 初始化模型参数
在使用net之前,我们需要初始化模型参数。
如在线性回归模型中的权重和偏置。
在这里,我们指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机采样, 偏置参数将初始化为零。
我们通过net[0]选择网络中的第一个图层, 然后使用weight.data和bias.data方法访问参数。
我们还可以使用替换方法normal_和fill_来重写参数值。
net[0].weight.data.normal_(0, 0.01) net[0].bias.data.fill_(0)
tensor([0.])
3.3.5. 定义损失函数
计算均方误差使用的是MSELoss类,也称为平方 L2 范数。 默认情况下,它返回所有样本损失的平均值。
loss = nn.MSELoss()
3.3.6. 定义优化算法
梯度下降
trainer = torch.optim.SGD(net.parameters(), lr=0.03)