课程2. 用PyTorch训练神经网络与梯度下降
- Pytorch
- torch.Tensor
- Pytorch 上的神经网络
- 用于余弦函数逼近的全连接神经网络
- 训练神经网络
- 梯度下降
- 最小化一个变量的函数
- 最小化多个变量的函数
- 使用梯度下降训练神经网络
- 在 Pytorch 中训练神经网络
- 从 nn.Module 类继承
- 将计算传输到显卡
- Pytorch 中的优化器
- 有哪些类型的优化器?
- 选择优化器
Pytorch
PyTorch 是当今最流行的深度学习框架之一。 “流行”的意思是,如今大多数与神经网络相关的代码都是用 PyTorch 编写的。这个框架如此受欢迎的原因在于 PyTorch 使用起来非常方便。我们稍后会看到,使用 PyTorch 与使用 Numpy 非常相似。 PyTorch 还具有非常方便且内容丰富的文档和教程。
深度学习框架快速浏览:
- Caffee、Theano、Lasagne - 几乎不再使用;
- Tensorflow 是 Google 推出的一个框架,在 PyTorch 出现之前一直流行到 2017-2018 年。当时,与神经网络相关的代码大部分都是用它编写的。但是它非常复杂,因此 PyTorch 很快就占据了领先地位。
Tensorflow 还具有 Keras 包装器,这使得使用神经网络变得更加容易。但是,Keras 的灵活性不如 PyTorch,因此无法完全取代它。
此外,2019 年,Tensorflow 2.0 发布,大大简化了神经网络的工作。但那时市场已经被 PyTorch 占领,因此 Tensorflow 2.0 从未流行起来。
- PyTorch;
- JAX 是 Google 为取代 Tensorflow 而设计的新框架。
torch.Tensor
框架的基本结构是一个称为“torch.Tensor”的结构。 Tensor 是 numpy.array
的类似物,许多使用 torch.Tensor
的方法完全重复了 numpy array
的方法。
张量是 PyTorch 中所有神经网络处理的数据类型。网络层的权重矩阵是张量。我们提供给网络输入的数据也必须转换为“torch.Tensor”类型。嗯,网络输出当然也是“torch.Tensor”类型。
让我们导入该库并看看创建张量的典型方法。
import torch
import warnings
warnings.filterwarnings('ignore')
- 可以从工作表、数组和其他 Python 容器创建张量。
x_list = [1., 2., 3.]
x_tensor = torch.tensor(x_list)
x_tensor
输出:tensor([1., 2., 3.])
- 可以使用初始化函数创建张量,如在“numpy”中。
zeros_tensor = torch.zeros(2, 3)
zeros_tensor
ones_tensor = torch.ones(2, 3)
ones_tensor
eye_tensor = torch.eye(6)
eye_tensor
- 一般来说,Numpy数组有的几乎所有方法,torch.Tensor也同样有:
x_tensor = torch.tensor([[1, 2],[3, 4]
])y_tensor = torch.tensor([[-10, 3],[5, -4]
])
x_tensor + y_tensor
x_tensor @ y_tensor
# 函数 np.concatenate([x_tensor, y_tensor], axis=1)
torch.cat([x_tensor, y_tensor], dim=0)
输出:
tensor([[-9, 5],
[ 8, 0]])
tensor([[ 0, -5],
[-10, -7]])
tensor([[ 1, 2],
[ 3, 4],
[-10, 3],
[ 5, -4]])
- 张量可以转换回 Numpy 格式
x_tensor = torch.tensor([[1, 2],[3, 4]
])x_numpy = x_tensor.numpy()
x_numpy
输出:
array([[1, 2],
[3, 4]])
Pytorch 上的神经网络
让我们学习如何在 Pytorch 上创建一个完全连接的神经网络。
Pytorch 中的神经网络就像构造函数一样创建:有一组标准块,我们可以从中组装最终的模型。这些块中最简单的是线性层和激活函数。在接下来的课程中,我们将熟悉可以嵌入到网络架构中的新模块。
让我们回忆一下完全连接的神经网络是如何构成的。
y ^ = σ ( W 3 T σ ( W 2 T σ ( W 1 T X + b 1 ) + b 2 ) + b 3 ) \widehat{y} = \sigma(W_3^T \sigma(W_2^T \sigma(W_1^TX + b_1 ) + b_{2}) + b_{3}) y =σ(W3Tσ(W2Tσ(W1TX+b1)+b2)+b3)
每个全连接层都是线性变换 c ^ = W X + b \widehat{c} = WX + b c =WX+b 和某个激活函数 σ ( c ^ ) \sigma(\widehat{c}) σ(c ) 的组合:
- 首先,将输入向量 X X X 与矩阵 W W W 相乘,并将偏移向量 b b b 添加到结果中:
c ^ = W X + b \widehat{c} = WX + b c =WX+b
- 接下来,将获得的结果通过激活函数运行。将得到的结果作为输入向量馈送到网络的下一层。
σ ( c ^ ) \sigma(\widehat{c}) σ(c )
神经网络被定义为一系列相互衔接的类似转换的列表。
让我们学习如何定义线性变换和激活函数。然后我们将它们结合起来,创建一个完全连接的神经网络。
使用“torch.nn.Linear”类定义表示线性变换的层。让我们创建一个具有 5 个输入神经元和 3 个输出神经元的层。
import torch.nn as nnn = 5
m = 3
linear_layer = nn.Linear(n, m)
linear_layer
输出:
Linear(in_features=5, out_features=3, bias=True)
接下来是激活函数。许多众所周知的激活函数在“nn”模块中实现,并在“nn. functional”模块中重复。我们选择其中之一,例如双曲正切(tanh)。
import torch.nn.functional as F# 你可以使用此选项
# activation = F.tanh
# 或者更正确的选项:
activation = nn.Tanh()
x_tensor = torch.tensor([[1, 2],[3, 4]
])
activation(x_tensor)
输出:
tensor([[0.7616, 0.9640],
[0.9951, 0.9993]])
注意:声明激活函数的第二种选择(使用 nn 模块)是更好的选择,因为直接从“torch.nn.functional”中使用函数有时会导致复杂模型无法正常工作。使用来自“torch.nn”的类更安全、更正确,但是,在专用于“Pytorch”的各种资源上,经常可以找到来自“torch.nn. functional”对象的使用,因此我们认为有必要讲述这种使用激活函数的方法。
现在,将使用一系列命令来指定一些带有数据的张量通过这种完全连接的层:
random_input = torch.rand(5)z = linear_layer(random_input)
output = activation(z)
output
输出:tensor([ 0.7512, -0.1367, 0.6380], grad_fn=< TanhBackward0>)
让我们关注“grad_fn=< TanhBackward0>”。所有作为神经网络中训练参数的张量上都会出现类似的注释。这意味着对于给定的张量,在网络训练过程中,将计算梯度,借助该梯度来更新张量值。我们将在本课后面详细讨论网络训练过程。
现在让我们用线性和 Tanh 块组装一个神经网络。链接多个模块的最简单方法是使用“nn.Sequential”模块。这个类将允许我们将几个模块组合成一个容器,以便当这个容器应用于某个张量时,计算将按照我们传递元素的顺序进行。也就是说,通过这种方式我们可以得到一个具有连续的Linear和Tanh层的神经网络。
FF_layer = nn.Sequential(linear_layer,activation
)
FF_layer(random_input)
输出:
tensor([ 0.7512, -0.1367, 0.6380], grad_fn=< TanhBackward0>)
我们得到了与上面单元格完全相同的结果。现在让我们以同样的方式建立一个完全连接的神经网络来解决一些简单的问题。例如,让我们尝试对余弦函数进行建模。
用于余弦函数逼近的全连接神经网络
首先,我们定义一个一维数据集。
X = torch.normal(mean=torch.zeros((1000, 1)), std= 2)
Y = torch.cos(X)
import matplotlib.pyplot as plt
import seaborn as snssns.set_theme()plt.figure(figsize=(20,7))
plt.scatter(x=X, y=Y);
让我们定义一个具有三层的神经网络。假设隐藏层中有五个神经元。作为激活函数,我们选择双曲正切,如上例所示。
import torch.nn as nn
NN = nn.Sequential(nn.Linear(1, 5, bias=True),nn.Tanh(),nn.Linear(5, 5, bias=True),nn.Tanh(),nn.Linear(5, 1, bias=True),nn.Tanh())
让我们看看未经训练的神经网络可以做什么:
X_test = torch.linspace(-6, 6, 1000)
Y_test = torch.cos(X_test)nn_prediction = NN(X_test.view(-1, 1))
nn_prediction = nn_prediction.detach().numpy()plt.figure(figsize=(20,7))
plt.scatter(x=X_test, y=Y_test, label='True Cosine');
plt.scatter(x=X_test, y=nn_prediction, label='NN predictions');
plt.legend()
由于我们的神经网络尚未经过训练,因此获得的结果是预期的。让我们训练她。但首先,让我们来讨论一下如何才能做到这一点。
注 1:在上一个单元格的第 4 行中我们使用了 .view()
函数。 .view()
是 .reshape()
函数的替代,它允许你更改张量的维度而不更改其中的数据。这里我们用它将长度为 n n n 的输入向量转换为大小为 ( n , 1 ) (n, 1) (n,1) 的矩阵。这种形式对于张量正确通过我们的变换是必要的。关键在于神经网络(以及任何其他机器学习模型)以大小为(n,k)的矩阵形式接收数据作为输入,其中n是数据元素的数量,k是每个数据元素的特征数量。在我们的例子中,每个元素都有 1 个特征(沿 OX 轴的坐标)。我们创建了一个由元素-特征组成的矩阵,其中有 n n n 个元素,每个元素都有 1 个特征。
注 2:在第 5 行中,我们执行了张量方法的组合 .detach().numpy()
。需要 .detach()
函数从神经网络的计算图中提取张量。粗略地说,神经网络的输出仍然是网络计算图的一部分,为了将这个张量转换成numpy,我们首先需要将其从网络图中分离出来。
训练神经网络
神经网络使用梯度优化算法进行训练。此类算法的思想是基于通过网络权重依次计算损失函数的梯度(偏导数),并更新网络权重。
现在我们来一步一步分析梯度优化算法的思想。
首先让我们记住,训练神经网络时的目标是选择这样的网络超参数,使得所选损失函数的平均值在训练数据集上平均最小。
例如,我们取上一课的数据集:
让我们选择一个我们想要在数据上最小化的损失函数 L L L。
那么网络训练的任务就是找到这样的网络参数 W 1 , b 1 , W 2 , b 2 , … W_1, b_1, W_2, b_2, \dots W1,b1,W2,b2,…,使得损失函数在训练数据集元素上的平均值最小: l o s s = ∑ i = 1 n L ( y i , y ^ i ) n → m i n loss = \frac{\sum_{i=1}^n L(y_i, \widehat{y}_i)}{n} \to min loss=n∑i=1nL(yi,y i)→min
,其中 n n n是数据中的元素数量, y i y_i yi是第 i i i个数据元素的目标变量的正确值, y ^ i \widehat{y}_i y i是第 i i i个数据元素的模型响应。
让我们再次看一下神经网络,并了解它的公式可以被视为其参数( W i W_i Wi 和 b i b_i bi)的函数
y ^ = σ ( W 3 T σ ( W 2 T σ ( W 1 T X + b 1 ) + b 2 ) + b 3 ) \widehat{y} = \sigma(W_3^T \sigma(W_2^T \sigma(W_1^TX + b_1 ) + b_{2}) + b_{3}) y =σ(W3Tσ(W2Tσ(W1TX+b1)+b2)+b3)
将此公式中的 y ^ \widehat{y} y 代入公式 l o s s loss loss 可得出:
l o s s = ∑ i = 1 n L ( y i , σ ( W 3 σ ( W 3 T σ ( W 1 T X i + b 1 ) + b 2 ) + b 3 ) ) n → m i n loss = \frac{\sum_{i=1}^n L(y_i, \sigma(W_3 \sigma(W_3^T \sigma(W_1^TX_i + b_1 ) + b_{2}) + b_{3}))}{n} \to min loss=n∑i=1nL(yi,σ(W3σ(W3Tσ(W1TXi+b1)+b2)+b3))→min
事实证明, l o s s loss loss是网络参数 W i W_i Wi和 b i b_i bi的函数。在训练网络时,任务是最小化许多变量的函数,即寻找 W 1 , b 1 , W 2 , b 2 , W 3 , b 3 W_1, b_1, W_2, b_2, W_3, b_3 W1,b1,W2,b2,W3,b3 的这样的值使得它们的 l o s s loss loss 值最小的问题。
我们如何解决最小化函数的问题?在 ML 课程中我们讨论了梯度下降的思想。我们先简单回顾一下。
首先,我们将研究解决函数最小化问题的方法和一元函数的梯度下降算法。然后我们将其推广到多个变量的函数。
梯度下降
最小化一个变量的函数
在这里我们将讨论如何解决最小化一个变量函数的问题
第一种方法是分析性的。对于许多函数来说,可以通过使导数等于零来找到最小点。
y = x 3 − 3 x − 4 y = x^3 - 3x - 4 y=x3−3x−4
d y d x = 3 x 2 − 3 \frac{dy}{dx} = 3x^2-3 dxdy=3x2−3
x m i n = { − 1 , 1 } x_{min} = \{-1, 1\} xmin={−1,1}
但是,这种方法并不适用于所有函数,即使只适用于一个变量。关于多变量函数我们能说些什么呢?
y = x 10 − 3 x 7 − 4 x 3 + 4 x y = x^{10} - 3x^7 - 4x^3 + 4x y=x10−3x7−4x3+4x
d y d x = 10 x 9 − 21 x 6 − 12 x 2 + 4 \frac{dy}{dx} = 10x^9-21x^6 -12x^2 + 4 dxdy=10x9−21x6−12x2+4
x m i n = ? x_{min} = \ ? xmin= ?
这里偏导数的两个性质对我们有帮助。对于一个变量的函数,这些属性可以表述如下:
- 某一点的导数的符号表示函数在该点是增还是减;
- 导数值的模表示函数在该点的增长/减少的速率。 f’(x) 的模值越高,函数 f 在点 x 处的减少/增加率就越高。
让我们说明一下这些属性。让我们考虑一个单变量函数 f ( x ) = x 4 + 5 x 3 − 10 x f(x) = x^4 + 5x^3 - 10x f(x)=x4+5x3−10x
其导数:
d f d x = 4 x 3 + 15 x 2 − 10 \frac{df}{dx} = 4x^3 + 15x^2 - 10 dxdf=4x3+15x2−10
让我们计算一下点 -5、-3.55、-2 处的导数的值:
d f d x ( − 5 ) = − 135 \frac{df}{dx}(-5) = -135 dxdf(−5)=−135
d f d x ( − 3.55 ) = 0 \frac{df}{dx}(-3.55) = ~0 dxdf(−3.55)= 0
d f d x ( − 2 ) = 18 \frac{df}{dx}(-2) = 18 dxdf(−2)=18
事实证明,计算出点 x x x 处的函数导数后,我们就知道需要从点 x x x 向哪个方向移动才能使函数值减小。
我们可以利用这个性质来寻找函数的最小值。想法是这样的:让我们选择一个随机点,例如 x = 5 x=5 x=5。我们来计算一下此时函数导数的值。根据导数的符号,我们将了解需要移动到哪里才能到达最小点。我们将搬到那里。
然而,存在一个问题:我们知道最小点位于哪个位置,但不知道它有多远。也就是说,我们不知道需要从当前点移动多少才能到达最小点。
您可以尝试这样做:固定步长,比如 δ x = 1 \delta x = 1 δx=1。选择一个随机点,例如 x = 5 x=5 x=5。计算此点处函数导数的值。根据导数的符号,您可以了解需要移动到哪里才能到达最小点。朝此方向移动步长值 δ x \delta x δx。在新的点处,再次计算函数导数的值。再次,了解你现在需要移动到哪里才能达到最低点。移动 δ x \delta x δx 到那里等。
但这个想法是相当无效的。至少有两个原因:
- 有时起点距离最小点非常远,为了到达最小点,你必须采取非常多的步骤;
- 有时起点(或我们在算法过程中到达的点)非常接近最小点,以至于我们以 δ x = 1 \delta x = 1 δx=1 的步长“跳过”最小点。
这里提到的导数的第二个性质将会对我们有所帮助:导数值的模数说明了该点处函数的增长/减少率。这里的想法是这样的:我们每次移动的不是 δ x = 1 \delta x = 1 δx=1,而是 δ x = a l p h a ∗ d f d x \delta x = alpha*\frac{df}{dx} δx=alpha∗dxdf。那么当我们远离最小点时,我们将移动更大的步长,如果我们接近最小点,我们将移动较小的步长。
然后,单变量函数的梯度下降算法将如下所示:
- 选择一个随机的起点 x x x。我们选择梯度下降步长 α \alpha α 的值
- 执行以下操作,直到满足停止标准:
- 计算当前点 x x x 的 f ( x ) f(x) f(x) 值; ——我们计算导数 d f d x \frac{d f}{d x} dxdf的值;
- 我们转向一个新的观点:
x = x − α d f d x x = x - \alpha \frac{df}{dx} x=x−αdxdf
停止标准可能会有所不同。例如,如果当前点的导数值(以及相应的梯度下降步骤)已经变得非常小。
最小化多个变量的函数
对于具有两个或多个变量的函数,关于每个变量的偏导数具有相同的性质:
- 在某一点处关于变量 x 1 x_1 x1的偏导数的符号表示该点处函数相对于变量 x 1 x_1 x1是增加还是减少;
- 偏导数值关于变量 x 1 x_1 x1的模表示函数在该点关于变量 x 1 x_1 x1的增长/减少率。值 f ’ x 1 ( x ) f’_{x_1}(x) f’x1(x) 的模越高,函数 f 相对于点 x 处的变量 x 1 x_1 x1 的减少/增加率就越高。
例如,考虑两个变量的函数 f ( x 1 , x 2 ) = 3 x 1 2 + 2 x 1 x 2 f(x_1, x_2) = 3x_1^2 + 2x_1x_2 f(x1,x2)=3x12+2x1x2
它关于 x 1 x_1 x1 的偏导数是:
d f d x 1 = 6 x 1 + 2 x 2 \frac{df}{dx_1} = 6x_1 + 2x_2 dx1df=6x1+2x2
其在点 x = (-20, -20) 处的值:
d f d x 1 ( − 20 , − 20 ) = 6 ⋅ ( − 20 ) + 2 ⋅ ( − 20 ) = − 160 \frac{df}{dx_1}(-20, -20) = 6 \cdot (-20) + 2\cdot (-20) = -160 dx1df(−20,−20)=6⋅(−20)+2⋅(−20)=−160
事实证明,通过计算点 x x x 处函数关于变量 x 1 x_1 x1 的偏导数,我们知道需要从点 x x x 相对于变量 x 1 x_1 x1 向哪个方向移动才能减少函数的值。
然后,多变量函数的梯度下降算法将如下所示:
- 选择一个随机起点 x = ( x 1 , x 2 , . . . , x n ) x = (x_1, x_2, ..., x_n) x=(x1,x2,...,xn)。我们选择梯度下降步长 α \alpha α 的值
- 执行以下操作,直到满足停止标准:
- 计算当前点 f ( x ) f(x) f(x) 处的函数值
- 计算导数 d f d x i ( x ) \frac{d f}{d x_i}(x) dxidf(x)的值;
- 我们转向一个新的观点:
x i = x i − α d f d x i ( x ) x_i = x_i - \alpha \frac{df}{dx_i}(x) xi=xi−αdxidf(x)
该算法被称为梯度下降,因为函数 ∇ f = ( d f d x 1 , d f d x 2 , . . . , d f d x n ) \nabla f = (\frac{df}{dx_1}, \frac{df}{dx_2}, ..., \frac{df}{dx_n}) ∇f=(dx1df,dx2df,...,dxndf) 的偏导数向量被称为函数 f 的梯度。第二步,梯度被更新:
x = x − α ∇ f x = x - \alpha \nabla f x=x−α∇f
使用梯度下降训练神经网络
我们再看一下神经网络公式。我们已经了解到,神经网络的损失公式可以被视为来自网络权重的许多变量的函数。
y ^ = σ ( W 3 T σ ( W 2 T σ ( W 1 T X + b 1 ) + b 2 ) + b 3 ) \widehat{y} = \sigma(W_3^T \sigma(W_2^T \sigma(W_1^TX + b_1 ) + b_{2}) + b_{3}) y =σ(W3Tσ(W2Tσ(W1TX+b1)+b2)+b3)
l o s s = ∑ i = 1 n L ( y i , σ ( W 3 σ ( W 3 T σ ( W 1 T X i + b 1 ) + b 2 ) + b 3 ) ) n → m i n loss = \frac{\sum_{i=1}^n L(y_i, \sigma(W_3 \sigma(W_3^T \sigma(W_1^TX_i + b_1 ) + b_{2}) + b_{3}))}{n} \to min loss=n∑i=1nL(yi,σ(W3σ(W3Tσ(W1TXi+b1)+b2)+b3))→min
然后,神经网络的梯度优化算法如下所示:
- 用随机值初始化所有网络权重 W i W_i Wi和 b i b_i bi。我们选择梯度下降步长 α \alpha α 的值
- 执行以下操作,直到满足停止标准:
- 利用参数 W i W_i Wi和 b i b_i bi的当前值计算 l o s s loss loss的值;
- 我们计算偏导数 ∂ l o s s ∂ W i \frac{\partial loss}{\partial W_i} ∂Wi∂loss、 ∂ l o s s ∂ b i \frac{\partial loss}{\partial b_i} ∂bi∂loss的值;
- 更新网络参数值:
W i = W i − α ∂ l o s s ∂ W i , b i = b i − α ∂ l o s s ∂ b i W_i = W_i - \alpha \frac{\partial loss}{\partial W_i}, \ \ b_i = b_i - \alpha \frac{\partial loss}{\partial b_i} Wi=Wi−α∂Wi∂loss, bi=bi−α∂bi∂loss
该算法是训练我们在本课程中学习的所有神经网络的基础。它有许多变体,可以在许多情况下改善算法的行为,但其思想保持不变。
我们不会讨论导数 ∂ L ∂ W i \frac{\partial L}{\partial W_i} ∂Wi∂L和 ∂ L ∂ b i \frac{\partial L}{\partial b_i} ∂bi∂L的具体计算方式。让我们停留在可以计算它们这一事实上。您可以在该模块的附加材料中了解有关如何计算它们的更多信息。值得一说的是,计算 ∂ L ∂ W i \frac{\partial L}{\partial W_i} ∂Wi∂L和 ∂ L ∂ b i \frac{\partial L}{\partial b_i} ∂bi∂L偏导数的算法被称为反向传播算法,或反向传播。
我们将继续训练我们的神经网络来近似 PyTorch 上的余弦函数。
在 Pytorch 中训练神经网络
PyTorch 内部实现了计算损失函数关于网络权重的偏导数的算法。我们不需要编写它的代码。
让我们编写一个“训练”函数来训练神经网络。
import tqdm
from tqdm.auto import tqdmdef train(model, X, y, criterion, optimizer, num_epoch):'''args:model - 神经网络模型X 和 y - 训练样本criterion - 从 `torch.nn` 模块中获取的损失函数optimizer - 从 `torch.optim` 模块中获取的优化器num_epoch - 训练周期数。 这是对样本中每个对象执行的梯度步骤数。'''# 按训练周期数循环for t in tqdm(range(num_epoch)):# 让我们计算一下模型的预测y_pred = model(X)# 让我们计算一下所得预测的损失函数值loss = criterion(y_pred, y)# 让我们重置之前计算的梯度值optimizer.zero_grad()# 让我们计算新的梯度loss.backward()# optimizer.step()return model
让我们再次声明我们的网络:
NN = nn.Sequential(nn.Linear(1, 5, bias=True),nn.Tanh(),nn.Linear(5, 5, bias=True),nn.Tanh(),nn.Linear(5, 1, bias=True))
让我们声明一个损失函数和一个优化器,并对网络进行 30 个 epoch 的训练:
# 损失函数
criterion = torch.nn.MSELoss()
# 优化器
optimizer = torch.optim.Adam(NN.parameters(), lr=1e-2)NN = train(NN, X, Y, criterion,optimizer, 30)
现在让我们看看结果会怎样。
nn_prediction = NN(X_test.view(-1, 1))
nn_prediction = nn_prediction.detach().numpy()plt.figure(figsize=(20,7))
plt.scatter(x=X_test, y=Y_test, label='True Cosine');
plt.scatter(x=X_test, y=nn_prediction, label='NN predictions');
plt.legend()
它明显好多了,但显然需要更多的训练迭代。
NN = train(NN, X, Y, criterion, optimizer, 15)
nn_prediction = NN(X_test.view(-1, 1))
nn_prediction = nn_prediction.detach().numpy()plt.figure(figsize=(20,7))
plt.scatter(x=X_test, y=Y_test, label='True Cosine');
plt.scatter(x=X_test, y=nn_prediction, label='NN predictions');
plt.legend()
提高收敛性的一个可能选择是减少梯度步骤。让我们将其降低到 0.001 并运行更多训练迭代。
optimizer = torch.optim.Adam(NN.parameters(), lr=1e-5)NN = train(NN, X, Y, criterion,optimizer, 50)
nn_prediction = NN(X_test.view(-1, 1))
nn_prediction = nn_prediction.detach().numpy()plt.figure(figsize=(20,7))
plt.scatter(x=X_test, y=Y_test, label='True Cosine');
plt.scatter(x=X_test, y=nn_prediction, label='NN predictions');
plt.legend()
结果明显变得更好。
我们看到图形边缘的近似值存在缺陷,这主要是因为在我们的原始样本中,坐标取自正态分布,因此很少有物体位于 -3 和 +3 的边界之外,这意味着我们的神经网络训练的先例很少。
从 nn.Module 类继承
让我们再看一下如何定义我们的神经网络:
NN = nn.Sequential(nn.Linear(1, 5, bias=True),nn.Tanh(),nn.Linear(5, 5, bias=True),nn.Tanh(),nn.Linear(5, 1, bias=True),nn.Tanh())
有时像“顺序”这样的简单结构不足以创建相当复杂的模型。有时这是由于需要在计算中创建几个独立的分支,有时是因为需要记录计算的历史记录。可能还有其他原因。无论如何,如果能够干扰中间计算的结果就更好了。为此,在 Pytorch 中使用神经网络有一种更灵活的方法——那就是编写从 nn.Module
类继承的自己的类。
通过这种块的前向通道的描述以“前向”函数的规范形式出现。
class Net(nn.Module):def __init__(self, dim=1):super(Net, self).__init__()self.fc1 = nn.Linear(dim, 5)self.tanh1 = nn.Tanh()self.fc2 = nn.Linear(5, 5)self.tanh2 = nn.Tanh()self.fc3 = nn.Linear(5, 1)self.tanh3 = nn.Tanh()def forward(self, x):x = self.fc1(x)x = self.tanh1(x)x = self.fc2(x)x = self.tanh2(x)x = self.fc3(x)x = self.tanh3(x)return x
NN = Net(1)
您可以用完全相同的方式训练这样的模型——使用我们已经拥有的“训练”函数。
criterion = torch.nn.MSELoss()
optimizer = torch.optim.Adam(NN.parameters(), lr=1e-2)NN = train(NN, X.view(-1, 1), Y, criterion,optimizer, 100)
nn_prediction = NN(X_test.view(-1, 1))
nn_prediction = nn_prediction.detach().numpy()plt.figure(figsize=(20,7))
plt.scatter(x=X_test, y=Y_test, label='True Cosine');
plt.scatter(x=X_test, y=nn_prediction, label='NN predictions');
plt.legend()
从“nn.Module”类继承使我们能够实现模型与“Pytorch”接口的兼容性,以及使用一些附加功能。例如,我们可以以矩阵形式访问模型参数:
for param in NN.parameters():print('parameter shape: ', param.shape)
输出:
parameter shape: torch.Size([5, 1])
parameter shape: torch.Size([5])
parameter shape: torch.Size([5, 5])
parameter shape: torch.Size([5])
parameter shape: torch.Size([1, 5])
parameter shape: torch.Size([1])
将计算传输到显卡
使用图形核心是使用深度神经网络的强制性属性。图形核心可实现快速、高效的并行计算。 Pytorch
实现了将计算传输到显卡的接口。为了执行此传输,必须将模型参数和输入数据都传输到显卡。
import torch
# 此命令检查 GPU 是否可用
torch.cuda.is_available()
device = 'cuda' if torch.cuda.is_available() else 'cpu'
现在我们可以在 GPU 上执行计算了。使用 .to(device)
或 .cuda()
函数将张量和模块传输到 GPU。
注意:我们必须重复定义主要函数,因为连接硬件加速器后运行时会重新启动
import torch
import torch.nn as nnclass Net(nn.Module):def __init__(self, dim):super(Net, self).__init__()self.fc1 = nn.Linear(dim, 5)self.tanh1 = nn.Tanh()self.fc2 = nn.Linear(5, 5)self.tanh2 = nn.Tanh()self.fc3 = nn.Linear(5, 1)self.tanh3 = nn.Tanh()def forward(self, x):x = self.fc1(x)x = self.tanh1(x)x = self.fc2(x)x = self.tanh2(x)x = self.fc3(x)x = self.tanh3(x)return x
NN = Net(1)
NN = NN.to(device) # 传输至设备
NN
输出:
Net(
(fc1): Linear(in_features=1, out_features=5, bias=True)
(tanh1): Tanh()
(fc2): Linear(in_features=5, out_features=5, bias=True)
(tanh2): Tanh()
(fc3): Linear(in_features=5, out_features=1, bias=True)
(tanh3): Tanh()
)
import tqdm
from tqdm.auto import tqdmdef train(model, X, y, criterion, optimizer, num_epoch):'''args:model - 神经网络模型X 和 y - 训练样本criterion - 从 `torch.nn` 模块中获取的损失函数optimizer - 从 `torch.optim` 模块中获取的优化器num_epoch - 训练周期数。 这是对样本中每个对象执行的梯度步骤数。'''# 按训练周期数循环for t in tqdm(range(num_epoch)):# 让我们计算一下模型的预测y_pred = model(X)# 让我们计算一下所得预测的损失函数值loss = criterion(y_pred, y)# 让我们重置之前计算的梯度值optimizer.zero_grad()# 让我们计算新的梯度loss.backward()# 让我们执行梯度下降步骤optimizer.step()return model
我们再次输入余弦的数据:
X = torch.normal(mean=torch.zeros((1000, 1)), std= 2)
Y = torch.cos(X)
让我们在 GPU 上训练神经网络
criterion = torch.nn.MSELoss()
optimizer = torch.optim.Adam(NN.parameters(), lr=1e-2)NN = train(NN, X, Y, criterion,optimizer, 50)
现在要将张量转换为 tgzn 格式,我们必须在 .detach().numpy()
命令集中添加一个命令 - .cpu()
import seaborn as sns
import matplotlib.pyplot as plt
sns.set_theme()X_test = torch.linspace(-6, 6, 1000).to(device)
Y_test = torch.cos(X_test)nn_prediction = NN(X_test.view(-1, 1))
nn_prediction = nn_prediction.cpu().detach().numpy()plt.figure(figsize=(20,7))
plt.scatter(x=X_test.cpu(), y=Y_test.cpu(), label='True Cosine');
plt.scatter(x=X_test.cpu(), y=nn_prediction, label='NN predictions');
plt.legend()
Pytorch 中的优化器
有哪些类型的优化器?
事实证明,时至今日,许多先进的梯度优化算法都是基于梯度下降的基本思想发明的。其中许多都是在‘pytorch’中实现的。以下是此类算法的简短列表,它们在 torch.optim
模块中作为类实现:
ADAM
(带动量的自适应梯度)是默认使用的事实上的优化方法。实践证明,在其他条件相同的情况下,它在大多数任务中都是最好的优化器。pytorch
中的类:torch.optim.Adam
Adagrad
(自适应子梯度)——一种独立选择梯度下降“步长”的方法。pytorch
中的类:torch.optim.Adagrad
RMSProp
是另一种根据训练历史调整梯度前的乘数的方法。pytorch
中的类:torch.optim.RMSprop
Adadelta
是一种类似于 adagrad 的算法。pytorch
中的类:torch.optim.Adadelta
Pytorch 还有许多其他优化器。 在每个类别的文档中,您可以找到相关文章的链接。
选择优化器
让我们再次重复一遍,“Adam”在绝大多数实际应用中都表现出了最好的效果。但有时尝试几种不同的优化器是有意义的。以下是使用其他优化器的一些建议:
- SGD 适用于简单问题,并且适用于大型数据集
- RMSProp 适用于解决非平稳目标函数问题,并且可以自适应地改变每个参数的学习率,这使其适用于循环神经网络
- Adagrad 对于稀疏数据非常有效,因为它会根据参数更新率调整学习率
最后,优化器的选择应基于问题的具体情况和模型架构。建议尝试多种优化器及其设置以获得最佳结果。