参考教程:DIVE INTO DEEP LEARNING
本文章是作为一新手,对李沐大神教材的复现。加入了一些自己的见解。
0. 导包
import randomimport torchfrom d2l import torch as d2l
导入d2l
包的教程——from CSDN
1. 生成数据集
为了简单起见,我们将根据带有噪声的线性模型构造一个人造数据集。 我们的任务是使用这个有限样本的数据集来恢复这个模型的参数。 我们将使用低维数据,这样可以很容易地将其可视化。 在下面的代码中,我们生成一个包含1000个样本的数据集, 每个样本包含从标准正态分布中采样的2个特征。 我们的合成数据集是一个矩阵 X ∈ R 1000 × 2 \mathbf{X} \in \mathbb{R}^{1000 \times 2} X ∈ R 1 0 0 0 × 2 。
我们使用线性模型参数 w = [ 2 , − 3.4 ] ⊤ , b = 4.2 \mathbf{w}=[2,-3.4]^{\top}, b=4.2 w = [ 2 , − 3 . 4 ] ⊤ , b = 4 . 2 和噪声项 ϵ \epsilon ϵ 生成数据集及其标签:
y = X w + b + ϵ \mathbf{y}=\mathbf{X} \mathbf{w}+b+\epsilon
y = X w + b + ϵ
你可以将ϵϵ视为模型预测和标签时的潜在观测误差。 在这里我们认为标准假设成立,即ϵϵ服从均值为0的正态分布。 为了简化问题,我们将标准差设为0.01。 下面的代码生成合成数据集。
1 2 3 4 5 6 7 8 9 10 11 12 def synthetic_data (w, b, num_examples ): """生成 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 ) print ('features:' , features[0 ], '\nlabel:' , labels[0 ])
通过生成第二个特征features[:, 1]
和labels
的散点图, 可以直观观察到两者之间的线性关系。
1 2 3 4 d2l.set_figsize() d2l.plt.scatter(features[:, (1 )].detach().numpy(), labels.detach().numpy(), 1 ) d2l.plt.show()
2. 读取数据集
回想一下,训练模型时要对数据集进行遍历,每次抽取一小批量样本 ,并使用它们来更新我们的模型。 由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数, 该函数能打乱数据集中的样本并以小批量方式获取数据 。
在下面的代码中,我们定义一个data_iter
函数, 该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size
的小批量。 每个小批量包含一组特征和标签。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 """ 该函数接收批量大小、特征矩阵和标签向量作为输入, 生成大小为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_indices = np.array( 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
补充:
shuffle()
函数
将序列的所有元素随机排序。
用法:
1 2 3 import random random.shuffle (lst )
Python3 range()
函数
两种用法:
1 2 range (stop)range (start, stop[, step])
yield
关键字用法
以一个斐波那契数列的打印算法来举例
1 2 3 4 5 6 7 8 9 10 11 12 13 def fab (max ): n, a, b = 0 , 0 , 1 while n < max : yield b a, b = b, a + b n = n + 1 for n in fab(5 ): print n
打印结果为:
简单地讲,yield 的作用就是把一个函数变成一个 generator,带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,调用 fab(5) 不会执行 fab 函数,而是返回一个 iterable 对象!在 for 循环执行时,每次循环都会执行 fab 函数内部的代码,执行到 yield b 时,fab 函数就返回一个迭代值,下次迭代时,代码从 yield b 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield。
也可以手动调用 fab(5) 的 next() 方法(因为 fab(5) 是一个 generator 对象,该对象具有 next() 方法),这样我们就可以更清楚地看到 fab 的执行流程:
3. 初始化参数模型
在我们开始用小批量随机梯度下降优化我们的模型参数之前, 我们需要先有一些参数。 在下面的代码中,我们通过从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0。
1 2 3 4 5 """ 初始化模型参数 """ w = torch.normal(0 , 0.01 , size=(2 , 1 ), requires_grad=True ) b = torch.zeros(1 , requires_grad=True )
在初始化参数之后,我们的任务是更新这些参数,直到这些参数足够拟合我们的数据。 每次更新都需要计算损失函数关于模型参数的梯度。
4. 定义模型
接下来,我们必须定义模型,将模型的输入和参数同模型的输出关联起来。 回想一下,要计算线性模型的输出, 我们只需计算输入特征X \mathbf{X} X 和模型权重w \mathbf{w} w 的矩阵-向量乘法后加上偏置b b b 。 注意,上面的X w \mathbf{X} \mathbf{w} X w 是一个向量,而b b b 是一个标量。
1 2 3 def linreg (X, w, b ): """线性回归模型""" return torch.matmul(X, w) + b
5. 定义损失函数loss function
因为需要计算损失函数的梯度,所以我们应该先定义损失函数。 这里我们使用平方损失函数。
l ( i ) ( w , b ) = 1 2 ( y ^ ( i ) − y ( i ) ) 2 l^{(i)}(\mathbf{w}, b)=\frac{1}{2}\left(\hat{y}^{(i)}-y^{(i)}\right)^{2}
l ( i ) ( w , b ) = 2 1 ( y ^ ( i ) − y ( i ) ) 2
n 个样本上的损失均值为:
L ( w , b ) = 1 n ∑ i = 1 n l ( i ) ( w , b ) = 1 n ∑ i = 1 n 1 2 ( w ⊤ x ( i ) + b − y ( i ) ) 2 L(\mathbf{w}, b)=\frac{1}{n} \sum_{i=1}^{n} l^{(i)}(\mathbf{w}, b)=\frac{1}{n} \sum_{i=1}^{n} \frac{1}{2}\left(\mathbf{w}^{\top} \mathbf{x}^{(i)}+b-y^{(i)}\right)^{2}
L ( w , b ) = n 1 i = 1 ∑ n l ( i ) ( w , b ) = n 1 i = 1 ∑ n 2 1 ( w ⊤ x ( i ) + b − y ( i ) ) 2
在实现中,我们需要将真实值y
的形状转换为和预测值y_hat
的形状相同。
1 2 3 4 5 """ 均方损失 """ def squared_loss (y_hat, y ): return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
6. 定义优化算法(小批量随机梯度下降)
在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。 接下来,朝着减少损失的方向更新我们的参数。 下面的函数实现小批量随机梯度下降更新。 该函数接受模型参数集合、学习速率和批量大小作为输入。每 一步更新的大小由学习速率lr
决定。 因为我们计算的损失是一个批量样本的总和,所以我们用批量大小(batch_size
) 来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。
1 2 3 4 5 6 7 8 """ 小批量随机梯度下降 """ def sgd (params, lr, batch_size ): with torch.no_grad(): for param in params: param -= lr * param.grad / batch_size param.grad.zero_()
补充
with
关键字用法
with
语句用于异常处理,封装了try…except…finally
编码范式,提高了易用性。
我们拿常用的异常处理语句来类比
以下是try...except...finally
语句:
1 2 3 4 5 file = open ('./test_runoob.txt' , 'w' )try : file.write('hello world' )finally : file.close()
以下是与之等价的with
语句:
1 2 with open ('./test_runoob.txt' , 'w' ) as file: file.write('hello world !' )
这两个语句是等价的。
参考资料——python的with关键字
with torch.no_grad()
的使用
被with torch.no_grad()
包住的代码,仅仅进行了计算,但不用跟踪反向梯度计算。
参考资料——with torch.no_grad()的使用
7. 训练
好了,前面的准备工作做完了,现在进入本篇文章的核心内容——训练
算法概括:
初始化参数
重复以下训练,直到完成
计算梯度:g ← ∂ ( w , b ) 1 ∣ B ∣ ∑ i ∈ B l ( x ( i ) , y ( i ) , w , b ) \mathbf{g} \leftarrow \partial_{(\mathbf{w}, b)} \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} l\left(\mathbf{x}^{(i)}, y^{(i)}, \mathbf{w}, b\right) g ← ∂ ( w , b ) ∣ B ∣ 1 ∑ i ∈ B l ( x ( i ) , y ( i ) , w , b )
更新参数:( w , b ) ← ( w , b ) − η g (\mathbf{w}, b) \leftarrow(\mathbf{w}, b)-\eta \mathbf{g} ( w , b ) ← ( w , b ) − η g
上面式子的详细写法:
w ← w − η ∣ B ∣ ∑ i ∈ B ∂ w l ( i ) ( w , b ) = w − η ∣ B ∣ ∑ i ∈ B x ( i ) ( w ⊤ x ( i ) + b − y ( i ) ) , \mathbf{w} \leftarrow \mathbf{w}-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{\mathbf{w}} l^{(i)}(\mathbf{w}, b)=\mathbf{w}-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)}\left(\mathbf{w}^{\top} \mathbf{x}^{(i)}+b-y^{(i)}\right),
w ← w − ∣ B ∣ η i ∈ B ∑ ∂ w l ( i ) ( w , b ) = w − ∣ B ∣ η i ∈ B ∑ x ( i ) ( w ⊤ x ( i ) + b − y ( i ) ) ,
b ← b − η ∣ B ∣ ∑ i ∈ B ∂ b l ( i ) ( w , b ) = b − η ∣ B ∣ ∑ i ∈ B ( w ⊤ x ( i ) + b − y ( i ) ) b \leftarrow b-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{b} l^{(i)}(\mathbf{w}, b)=b-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}}\left(\mathbf{w}^{\top} \mathbf{x}^{(i)}+b-y^{(i)}\right)
b ← b − ∣ B ∣ η i ∈ B ∑ ∂ b l ( i ) ( w , b ) = b − ∣ B ∣ η i ∈ B ∑ ( w ⊤ x ( i ) + b − y ( i ) )
在每个迭代周期 (epoch)中,我们使用data_iter
函数遍历整个数据集, 并将训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除)。 这里的迭代周期个数num_epochs
和学习率lr
都是超参数,分别设为3和0.03。 设置超参数很棘手,需要通过反复试验进行调整。
训练结果:
1 2 3 epoch 1 , loss 0 .055445 epoch 2 , loss 0 .000249 epoch 3 , loss 0 .000050
8.完整代码(可运行)
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 import randomimport torchfrom d2l import torch as d2limport numpy as np""" 根据带有噪声的线性模型构造一个人造数据集。 我们使用线性模型参数w=[2,−3.4]⊤、b=4.2和噪声项ϵ生成数据集及其标签: y=Xw+b+ϵ """ def synthetic_data (w, b, num_examples ): """生成 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 )) """ 该函数接收批量大小、特征矩阵和标签向量作为输入, 生成大小为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_indices = np.array( indices[i: min (i + batch_size, num_examples)]) yield features[batch_indices], labels[batch_indices]""" 线性回归模型 """ def linreg (X, w, b ): return torch.matmul(X, w) + b""" 均方损失 """ def squared_loss (y_hat, y ): return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2 """ 小批量随机梯度下降 """ def sgd (params, lr, batch_size ): with torch.no_grad(): for param in params: param -= lr * param.grad / batch_size param.grad.zero_()""" 生成数据集 """ true_w = torch.tensor([2 , -3.4 ]) true_b = 4.2 features, labels = synthetic_data(true_w, true_b, 1000 ) print ("生成数据集test:" )print ('features:' , features[0 ], '\nlabel:' , labels[0 ]) print () d2l.set_figsize() d2l.plt.scatter(features[:, (1 )].detach().numpy(), labels.detach().numpy(), 1 ) d2l.plt.show()""" 读取数据集 """ batch_size = 10 print ('读取数据集test:' )for X, y in data_iter(batch_size, features, labels): print ('X:\n' , X, '\n' , 'y:\n' , y) break print ()""" 初始化模型参数 """ w = torch.normal(0 , 0.01 , size=(2 , 1 ), requires_grad=True ) b = torch.zeros(1 , requires_grad=True )""" 训练 """ lr = 0.03 num_epochs = 3 net = linreg loss = squared_lossprint ('训练test:' )for epoch in range (num_epochs): for X, y in data_iter(batch_size, features, labels): l = loss(net(X, w, b), y) l.sum ().backward() 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} ' )
写在最后
当然,强大的PyTorch有很多现成的函数可以供我们使用,还想要继续了解请看这篇文章:线性回归的简洁实现