PyTorch深度学习笔记(6):神经网络的训练--梯度下降法

在前面的文章中,我们介绍了为线性回归、二分类、三分类神经网络的优化选取适当的损失函数。我们知道,当损失函数的值越小,代表神经网络预测值与真实值之间的差异越小,模型效果越好。对于是凸函数的损失函数,我们可以对权重向量\boldsymbol{w}求导,再令其导数等于0,来求出使损失函数最小化的\boldsymbol{w}值。遗憾的是,在绝大多数问题中,损失函数都不是一个简单的凸函数,无法通过数学推导来求得\boldsymbol{w}的最优解。对于这样的问题,我们通常采用数值最优化的方法(如:梯度下降法、牛顿法等),通过迭代的方式,逐步逼近局部最优解。本文我们将介绍如何在神经网络中使用梯度下降法。

梯度下降法

在优化神经网络之前,我们先简单介绍一下什么是梯度下降法。梯度下降法可以说是机器学习中最常用的数值最优化方法了,它从一个随机的起点出发,沿着梯度(损失函数在当前点的微分)的反方向移动一小段距离,到达一个新的点,再次计算梯度并移动,通过不断迭代,最终到达函数的最低点(这个最低点不一定是函数全局的最低点)。
梯度下降的迭代公式为:

\boldsymbol{w}^{(j+1)} = \boldsymbol{w}^{(j)} - \eta \left. \frac{\partial L(\boldsymbol{w})}{\partial \boldsymbol{w}} \right|_{\boldsymbol{w} = \boldsymbol{w}^{(j)}}

其中:

  • 上标 j 代表第 j 次迭代,\boldsymbol{w}^{(j)} 代表在第 j 次迭代时的 \boldsymbol{w} 的取值;
  • L(\boldsymbol{w}) 为待优化函数/损失函数;
  • \left. \frac{\partial L(\boldsymbol{w})}{\partial \boldsymbol{w}} \right|_{\boldsymbol{w} = \boldsymbol{w}^{(j)}}L(\boldsymbol{w})\boldsymbol{w} = \boldsymbol{w}^{(j)} 处的梯度;
  • \eta 为自行设定的步长,也叫学习率

|\boldsymbol{w}^{(j+1)} - \boldsymbol{w}^{(j)}| \lt \epsilon 时停止迭代(\epsilon 为自行设定的大于0的阈值,如:\epsilon=1e-6),取 \boldsymbol{w} = \boldsymbol{w}^{(j+1)} 为最终解。

PyTorch中的AutoGrad模块

在Pytorch中,张量本身可以支持微分运算。

import torch 
import numpy as np

# 用 requires_grad=True 构建可微分张量
x = torch.tensor(1., requires_grad=True)
x

tensor(1., requires_grad=True)

# 构建函数关系
y = x**2
y

tensor(1., grad_fn=<PowBackward0>)

此时张量y具有属性grad_fn=<PowBackward0>,我们可以通过y.grad_fn查看该属性。grad_fn属性存储了可微分张量y在计算过程中的函数关系(即 y=x**2)。由于y是由可微分张量x计算而来,所以y本身也是一个可微分张量,可通过y.requires_grad查看。

y.requires_grad

True

我们可以尝试由y构建的张量z是否也具有同样的性质。

z = y + 30
print(z)
print(z.grad_fn)
print(z.requires_grad)

tensor(31., grad_fn=<AddBackward0>)

<AddBackward0 object at 0x000001ADD091B6A0>

True

可以看出,如果初始张量是可微的,在计算过程中,由其构建的新张量都是可微的,并且新张量会保存与前一步的函数关系。

张量的计算图

根据张量间的函数关系,可以绘制张量计算图。在上述例子中,张量的计算图如下:

  • 张量计算图是用于记录可微分张量之间计算关系的图;
  • 由节点和有向边构成,其中节点表示张量,边表示函数计算关系,方向表示实际运算方向;
  • 本质上是有向无环图。

张量的节点可以分为三类:

  • 叶节点:初始输入的可微分张量;上例中的x;
  • 输出节点:最后计算得出的张量;上例中的z;
  • 中间节点:除了叶节点和输出节点之外的节点;上例中的y。

在一张计算图中,可以有多个叶节点和中间节点,但通常只有一个输出节点,若有多个输出结果,我们也会保存在一个张量中。

Pytorch中的计算图是动态计算图,会根据可微分张量的计算过程自动生成,并且伴随新张量或运算的加入不断更新,使运算更灵活高效。

反向传播

我们可以使用torch.autograd.grad()来计算可微张量的导数。

import torch 
import numpy as np

x = torch.tensor(1., requires_grad=True)
y = x**2
z = y + 30

# y 对 x 求导
print(torch.autograd.grad(y, x, retain_graph=True))

# z 对 y 求导
print(torch.autograd.grad(z, y, retain_graph=True))

# z 对 x 求导
print(torch.autograd.grad(z, x))

(tensor(2.),)

(tensor(1.),)

(tensor(2.),)

我么也可以使用反向传播来进行计算导数/梯度。反向传播,可以简单理解为,在此前记录的函数关系基础上,反向传播函数关系,进而求得叶节点的导数值。我们还是用上面x,y,z的计算关系来作为例子。

首先,对于某一个可微分张量的导数值(梯度值),存储在grad属性中:

import torch 
import numpy as np

x = torch.tensor(1., requires_grad=True)
y = x**2
z = y + 30

x.grad

x.grad属性是空值,不会返回任何结果,我们虽然已经构建了x、y、z三者之间的函数关系,x也有具体取值,但要计算x点导数,还需要进行具体的求导运算,也就是执行所谓的反向传播。

我们执行反向传播:

z.backward()

反向传播结束后,即可查看叶节点的导数值。

x.grad

tensor(2.)

注意:

  • 在默认情况下,在一张计算图上执行backward()或者autograd.grad(),只能计算一次,再次调用将报错,除非将函数参数retain_graph设置为True。
    在中间节点上也可以进行反向传播:
import torch 
import numpy as np

x = torch.tensor(1., requires_grad=True)
y = x**2
z = y + 30

y.backward()
x.grad

tensor(2.)

默认情况下,在反向传播过程中,中间节点并不会保存梯度;若想保存中间节点的梯度,我们可以使用retain_grad()方法:

import torch 
import numpy as np

x = torch.tensor(1., requires_grad=True)
y = x**2
y.retain_grad()
z = y**2

z.backward()
print(y.grad)
print(x.grad)

tensor(2.)

tensor(4.)

在一些特殊情况下,我们不希望可微张量从创建到运算结果输出都被记录,此时就可以使用一些方法来阻止部分运算被记录。

  • with torch.no_grad()方法:with相当于是一个上下文管理器,with torch.no_grad()内部代码都“屏蔽”了计算图的追踪记录;
import torch 
import numpy as np

x = torch.tensor(1., requires_grad=True)
y = x**2
with torch.no_grad():
    z = y**2

# z 只是一个普通张量
z

tensor(1.)

.detach()方法:创建一个不可导的相同张量;

import torch 
import numpy as np

x = torch.tensor(1., requires_grad=True)
y = x**2
y1 = y.detach()
z = y1**2

print(y)
print(y1)
print(z)

tensor(1., grad_fn=<PowBackward0>)

tensor(1.)

tensor(1.)

如果需要识别在一个计算图中某张量是否是叶节点,可以使用is_leaf属性查看对应张量是否是叶节点。

import torch 
import numpy as np

x = torch.tensor(1., requires_grad=True)
y = x**2
z = y**2

print(x.is_leaf)
print(y.is_leaf)
print(z.is_leaf)

True

False

False

注意:is_leaf方法也有容易混淆的地方,对于任何一个新创建的张量,无论是否可导、是否加入计算图,都是可以是叶节点,这些节点距离真正的叶节点,只差一个requires_grad属性调整。

神经网络中的反向传播

在之前的文章中我们介绍过,神经网络的正向传播是指神经网络从输入层出发,依据设定好的函数运算关系,从左往右逐层计算,得到输出层结果的过程。而神经网络的反向传播,恰好相反,是从输出结果出发,依据函数关系及链式法则,从右向左求解梯度的过程。在PyTorch中,这一过程由.backward()自动完成。

回顾我们在之前文章中构建神经网络正向传播的例子。

我们有500条数据,20个特征,标签3分类。我们要实现一个三层神经网络,架构是:第一层有13个神经元,第二层有8个神经元,第三层是输出层。第一层的激活函数是ReLU,第二层的激活函数是sigmoid。它的正向传播的代码如下:

import torch 
import torch.nn as nn

torch.manual_seed(523)

X = 100*torch.randn((500,20), dtype=torch.float32)
y = torch.randint(0, 3, (500,), dtype=torch.float32)


class Model(nn.Module):

    def __init__(self, in_feature=10, out_feature=2):
        super(Model, self).__init__()
        self.linear1 = nn.Linear(in_feature, 13)
        self.linear2 = nn.Linear(13, 8)
        self.output = nn.Linear(8, out_feature)
    
    def forward(self, X):
        sigma1 = torch.relu(self.linear1(X))
        sigma2 = torch.sigmoid(self.linear2(sigma1))
        z_hat = self.output(sigma2)
        
        return z_hat

# 在forward函数中,我们将z_hat作为输出结果,
# 是因为我们将使用nn中的CrossEntroyLoss作为损失函数,
# 而z_hat是CrossEntroyLoss的输入参数
由于该神经网络为三分类网络,我们用CrossEntropyLoss作为其损失函数:
input_ = X.shape[1]
output_ = len(y.unique())

net = Model(input_, output_)

z_hat = net.forward(X)
criterion  = nn.CrossEntropyLoss()
loss = criterion(z_hat, y.long())

该神经网络的计算图如下:

对损失函数进行反向传播:

loss.backward()

查看返回的梯度的shape(与权重矩阵的shape是一致的):

# 神经网络第一层权重的梯度的shape
net.linear1.weight.grad.shape

torch.Size([13, 20])

我们注意到,在上述过程中,我们并没有对权重矩阵设置 requires_grad=Ture,那是因为我们的权重矩阵是由继承的nn.Module类自动生成的,在生成过程中,已经默认将requires_grad设置为True了。

在神经网络中使用随机梯度下降法

当我们从反向传播中获得了梯度后,就可以对权重的值进行更新。回顾本文开头介绍的梯度下降法的公式:

\boldsymbol{w}^{(j+1)} = \boldsymbol{w}^{(j)} - \eta \left. \frac{\partial L(\boldsymbol{w})}{\partial \boldsymbol{w}} \right|_{\boldsymbol{w} = \boldsymbol{w}^{(j)}}

我们继续沿用上一小节中的例子来进行代码实现。

# 设定超参数
lr = 0.01
  • lr 代表 learning rate 学习率(即步长,梯度下降公式中的\eta)通常为一个很小的数,如:0.001,0.01,0.05,0.1。
# 对net中第一层权重矩阵进行第一次迭代
net.linear1.weight.data -= lr * net.linear1.weight.grad

print(net.linear1.weight.data[0:5])

代码打印结果如下:

tensor([[ 0.0870,  0.1616, -0.2232,  0.0182, -0.0080, -0.2116, -0.0446,  0.1539,
          0.1337,  0.1365,  0.0592, -0.1800, -0.1057, -0.2055,  0.0790,  0.1459,
         -0.1940,  0.1789, -0.1130,  0.0987],
        [ 0.0939,  0.1414, -0.1520, -0.1762, -0.0556,  0.0523,  0.0731,  0.0059,
          0.1907,  0.0755,  0.1626,  0.1898,  0.1082,  0.1420, -0.1051,  0.0681,
         -0.1620,  0.0750,  0.0382, -0.0955],
        [-0.0734, -0.1051, -0.2168,  0.1326, -0.1887, -0.1151,  0.2064,  0.0848,
          0.0797, -0.1815, -0.1496, -0.2139, -0.0666,  0.1644, -0.2142, -0.1242,
         -0.0604, -0.0372, -0.1209,  0.1619],
        [ 0.1495,  0.0230, -0.1951, -0.1492, -0.0597, -0.1927,  0.0299,  0.1652,
         -0.1647,  0.2015, -0.1960,  0.1051,  0.1920, -0.1612,  0.1436, -0.1860,
          0.0525, -0.1239,  0.1356,  0.0672],
        [ 0.1308, -0.0689,  0.1470, -0.1147, -0.0099,  0.1324, -0.1758,  0.0781,
         -0.1289,  0.1064,  0.0458,  0.1904,  0.1662, -0.0075,  0.1079, -0.0063,
          0.0656, -0.0992,  0.1638, -0.1795]])

完成迭代之后,我们将重复下面的步骤,直至梯度下降算法收敛(找到权重的最优解):

正向传播(获得loss)---> 反向传播(获得梯度)---> 更新权重 ---> 正向传播(获得loss)---> ...

# 再次进行正向传播,计算loss
z_hat = net.forward(X)
loss = criterion(z_hat, y.long())

# 再次进行反向传播
loss.backward()

# 对net中第一层权重矩阵进行第二次迭代
net.linear1.weight.data -= lr * net.linear1.weight.grad

print(net.linear1.weight.data[0:5])

代码打印结果如下:

tensor([[ 0.0867,  0.1617, -0.2232,  0.0184, -0.0083, -0.2116, -0.0448,  0.1544,
          0.1336,  0.1365,  0.0588, -0.1803, -0.1056, -0.2057,  0.0792,  0.1459,
         -0.1940,  0.1787, -0.1131,  0.0985],
        [ 0.0938,  0.1411, -0.1523, -0.1763, -0.0555,  0.0524,  0.0731,  0.0061,
          0.1906,  0.0756,  0.1626,  0.1898,  0.1081,  0.1420, -0.1049,  0.0683,
         -0.1619,  0.0752,  0.0381, -0.0953],
        [-0.0732, -0.1050, -0.2168,  0.1325, -0.1887, -0.1153,  0.2064,  0.0847,
          0.0796, -0.1816, -0.1495, -0.2137, -0.0666,  0.1643, -0.2142, -0.1241,
         -0.0606, -0.0374, -0.1211,  0.1621],
        [ 0.1494,  0.0229, -0.1951, -0.1491, -0.0596, -0.1923,  0.0301,  0.1652,
         -0.1646,  0.2017, -0.1957,  0.1049,  0.1918, -0.1614,  0.1437, -0.1860,
          0.0526, -0.1240,  0.1358,  0.0673],
        [ 0.1309, -0.0688,  0.1467, -0.1152, -0.0099,  0.1327, -0.1755,  0.0781,
         -0.1292,  0.1060,  0.0463,  0.1905,  0.1660, -0.0076,  0.1079, -0.0061,
          0.0655, -0.0989,  0.1638, -0.1795]])

对比两次迭代后的结果我们发现,每一次迭代更新后,权重矩阵将发生微小的变化,使损失函数沿着减小的方向迈出了一小步。

使用动量法(Momentum)改进前进方向

每次迭代时,都会产生一个梯度,权重矩阵沿着梯度的反方向前进,为了提高迭代的效率,我们希望能够实现:

  • 当本次迭代和上次迭代的方向相近时,加大前进的步伐;
  • 当本次迭代和上次迭代的方向有很大差异时,综合考虑两次的方向。

动量法可以帮助我们实现上述目标。假设第j次迭代的前进方向为\boldsymbol{v}^{(j)},则:
\begin{aligned} \boldsymbol{v}^{(j+1)} &= \gamma \boldsymbol{v}^{(j)} - \eta \left. \frac{\partial L(\boldsymbol{w})}{\partial \boldsymbol{w}} \right|_{\boldsymbol{w} = \boldsymbol{w}^{(j)}} \\ \boldsymbol{w}^{(j+1)} &= \boldsymbol{w}^{(j)} + \boldsymbol{v}^{(j+1)} \end{aligned}

从上述公式不难看出,第 (j+1) 次迭代前进的方向,实际上是第 j 次迭代前进方向:\boldsymbol{v}^{(j)} 与本轮求得的梯度反方向:(- \frac{\partial L(\boldsymbol{w})}{\partial \boldsymbol{w}})的加权平均和,权重分别为 \gamma\eta 。前进方向 \boldsymbol{v} 被称为动量,参数 \gamma 被称为动量参数
下面我们将介绍使用torch.optim库来实现上述更新权重矩阵的过程。

import torch 
import torch.nn as nn
import torch.optim as optim # 导入torch.optim库

torch.manual_seed(523)

# 原始数据准备
X = 100*torch.randn((500,20), dtype=torch.float32)
y = torch.randint(0, 3, (500,), dtype=torch.float32)

# 模型所用超参数准备
lr = 0.01 # 学习率
gamma = 0.9 # 动量参数


# 神经网络架构
class Model(nn.Module):

    def __init__(self, in_feature=10, out_feature=2):
        super(Model, self).__init__()
        self.linear1 = nn.Linear(in_feature, 13)
        self.linear2 = nn.Linear(13, 8)
        self.output = nn.Linear(8, out_feature)
    
    def forward(self, X):
        sigma1 = torch.relu(self.linear1(X))
        sigma2 = torch.sigmoid(self.linear2(sigma1))
        z_hat = self.output(sigma2)
        
        return z_hat
    
    
# 实例化神经网络
torch.manual_seed(523)
input_ = X.shape[1]
output_ = len(y.unique())
net = Model(input_, output_)

# 实例化损失函数、优化算法
criterion  = nn.CrossEntropyLoss()
opt = optim.SGD(net.parameters() # 待优化的参数为神经网络net中的所有参数
                , lr=lr # 学习率
                , momentum=gamma # 动量参数
                )

进行一次迭代循环:

opt.zero_grad() # 清除梯度
z_hat = net.forward(X) # 正向传播
loss = criterion(z_hat, y.long()) # 损失函数值
loss.backward() # 反向传播
opt.step() # 更新权重

print(loss)
print(net.linear1.weight.data[0])

tensor(1.2273, grad_fn=<NllLossBackward>)

tensor([ 0.0688, 0.0787, 0.0049, -0.0480, -0.1132, 0.1122, 0.2166, -0.0108,
0.1118, -0.1780, -0.1765, 0.1041, -0.1062, -0.1410, 0.0300, 0.0522,
-0.1015, -0.0356, 0.1306, 0.1921])

重复一迭代循环:

opt.zero_grad() # 清除梯度
z_hat = net.forward(X) # 正向传播
loss = criterion(z_hat, y.long()) # 损失函数值
loss.backward() # 反向传播
opt.step() # 更新权重

print(loss)
print(net.linear1.weight.data[0])

tensor(1.2231, grad_fn=<NllLossBackward>)

tensor([ 0.0689, 0.0791, 0.0049, -0.0478, -0.1131, 0.1122, 0.2162, -0.0110,
0.1117, -0.1776, -0.1762, 0.1042, -0.1063, -0.1412, 0.0301, 0.0521,
-0.1015, -0.0357, 0.1304, 0.1921])

随机梯度下降法/小批量随机梯度下降法

我们可以注意到,在梯度下降法中,每次进行迭代时,我们都在计算中使用了全部的样本,这样做可能会造成两个问题:1)运算效率偏低,尤其在样本量巨大的情况下;2)优化的结果过于拟合当前的样本。针对这样的情况,我们可以使用随机梯度下降法或者小批量随机梯度下降法

  • 随机梯度下降法:每次进行迭代时,随机抽取一个样本用于计算损失函数的值;
  • 小批量随机梯度下降法:每次进行迭代时,随机抽取一小部分样本用于计算损失函数的值。

以上两种方法都是梯度下降法的变种,因此,从广义上讲,梯度下降法包含了其原始形态以及由它衍生的变种。这两种方法的核心思想都是用局部(小部分样本)模拟整体(全部样本),这样做的好处是能够使算法更容易跳脱出局部最小值(从而更有可能找到全局最小值);缺点就是不如原始的梯度下降法稳定。

在用梯度下降法优化神经网络时,一般使用的小批量梯度下降法。
小批量梯度下降法中有两个比较重要的概念:

  • batch size:单个批量 batch 含有的样本数被称为 batch size;

  • epoch:是衡量训练数据被使用次数的单位;一个epoch表示优化算法将全部训练数据都使用了一次。
    在数据预处理模块torch.utils中,TensorDatasetDataLoader是对数据进行预处理的重要的类。

  • TensorDataset:用来打包数据的类(如:打包特征和标签);它可以将第一个维度数量一样的tensor打包在一起。

import torch
from torch.utils.data import TensorDataset

a = torch.randn((500, 3, 4, 5))
b = torch.randn((500, 1))

查看打包的第一个数据:

TensorDataset(a,b)[0]

代码打印结果如下:

(tensor([[[-0.0233,  1.9141, -0.5995,  1.9380,  0.1375],
          [-0.1492, -0.8560, -1.5663,  1.0305,  0.3425],
          [ 0.1487, -0.3927, -0.1159, -0.3164,  0.0164],
          [-0.6294, -0.4785, -1.4078,  1.6561, -0.1408]],
          
         [[-1.3591, -0.1150, -0.8489, -0.4108, -0.7836],
          [-2.9301, -0.4060, -0.5938, -0.2844,  1.3479],
          [ 2.6921, -0.3981,  0.2316,  1.0572,  0.4549],
          [-1.9132,  0.6705,  0.4639,  0.4736,  0.6568]], 
          
         [[-0.4895, -0.7288,  0.0756,  1.8666,  1.8532],
          [ 0.5564,  1.1624,  0.2139, -0.2616,  0.0708],
          [ 0.6012, -1.8113,  1.0708,  0.3703,  0.8664],
          [ 0.3937,  0.9258,  0.6254,  1.1905,  0.3784]]]),
 tensor([-0.5008]))

查看打包的前两个数据:

TensorDataset(a,b)[0:2]

代码打印结果如下:

(tensor([[[[-0.0233,  1.9141, -0.5995,  1.9380,  0.1375],
           [-0.1492, -0.8560, -1.5663,  1.0305,  0.3425],
           [ 0.1487, -0.3927, -0.1159, -0.3164,  0.0164],
           [-0.6294, -0.4785, -1.4078,  1.6561, -0.1408]],
 
          [[-1.3591, -0.1150, -0.8489, -0.4108, -0.7836],
           [-2.9301, -0.4060, -0.5938, -0.2844,  1.3479],
           [ 2.6921, -0.3981,  0.2316,  1.0572,  0.4549],
           [-1.9132,  0.6705,  0.4639,  0.4736,  0.6568]],
 
          [[-0.4895, -0.7288,  0.0756,  1.8666,  1.8532],
           [ 0.5564,  1.1624,  0.2139, -0.2616,  0.0708],
           [ 0.6012, -1.8113,  1.0708,  0.3703,  0.8664],
           [ 0.3937,  0.9258,  0.6254,  1.1905,  0.3784]]],
 
 
         [[[-0.5772, -1.1265,  0.1254,  0.3652,  1.7364],
           [ 0.3899,  0.2242, -0.3251,  0.6372, -0.4175],
           [ 1.0690, -0.2528,  1.2352,  0.9929, -0.6703],
           [-1.3898,  1.2658, -0.3822, -1.2853,  1.2490]],
 
          [[-0.3043, -1.9394,  0.2151,  0.9892, -1.0453],
           [-0.6024,  1.1334, -1.4179, -1.7300,  0.0180],
           [-0.8412, -1.6207,  2.5445,  1.0061, -1.2067],
           [ 0.1509, -0.0163,  0.0898, -1.0971,  0.9862]],
...
           [-1.1061, -0.0206,  0.8473,  0.5660, -1.4148],
           [-1.0360,  1.2338, -1.8632, -2.7693, -2.2408],
           [-0.1658, -0.3251, -0.5832,  1.8602,  0.9598]]]]),
 tensor([[-0.5008],
         [-0.0585]]))
  • DataLoader:用来切割小批量数据的类;可以接受任意数组,张量作为输入,并把它们一次性转换为神经网络可以接入的tensor;
import torch
from torch.utils.data import DataLoader
import numpy as np

DataLoader(np.random.randn(5,2))

for i in DataLoader(np.random.randn(5,2)):
    print(i)
# 数据类型是自动读取的,无法在DataLoader里面修改

tensor([[-1.2635, -0.6255]], dtype=torch.float64)

tensor([[-1.5648, 0.4153]], dtype=torch.float64)

tensor([[0.8743, 0.5387]], dtype=torch.float64)

tensor([[ 0.9333, -1.4642]], dtype=torch.float64)

tensor([[1.7805, 0.6314]], dtype=torch.float64)

#如果在输入时直接设置好类型,则可以使用设置的类型
for i in DataLoader(torch.randn((5,2), dtype=torch.float32)):
    print(i.dtype)

torch.float32

torch.float32

torch.float32

torch.float32

torch.float32

DataLoader中比较重要的参数有:
- batch_size:单个批量的样本量有多少;
- shuffle:分批之前是否将样本打乱;
- drop_last:是否舍弃最后一个batch;
data = DataLoader(torch.randn(500,2)
                  , batch_size=120
                  , shuffle=True
                  , drop_last=False
                  )

for i in data:
    print(i.shape)

torch.Size([120, 2])

torch.Size([120, 2])

torch.Size([120, 2])

torch.Size([120, 2])

torch.Size([20, 2])

对于小批量梯度下降而言,我们一般这样使用 TensorDataset 和 DataLoader。

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader

torch.manual_seed(526)
X = torch.randn((50000,20), dtype=torch.float32)
y = torch.randint(0, 3, (50000,), dtype=torch.float32)


# 神经网络架构
class Model(nn.Module):

    def __init__(self, in_feature=10, out_feature=2):
        super(Model, self).__init__()
        self.linear1 = nn.Linear(in_feature, 13)
        self.linear2 = nn.Linear(13, 8)
        self.output = nn.Linear(8, out_feature)
    
    def forward(self, X):
        sigma1 = torch.relu(self.linear1(X))
        sigma2 = torch.sigmoid(self.linear2(sigma1))
        z_hat = self.output(sigma2)
        
        return z_hat
    
    
# 实例化神经网络
torch.manual_seed(526)
input_ = X.shape[1]
output_ = len(y.unique())
net = Model(input_, output_)

# 设定优化算法超参数
lr = 0.01 
gamma = 0.9 

# 实例化损失函数、优化算法
criterion  = nn.CrossEntropyLoss()
opt = optim.SGD(net.parameters() # 待优化的参数为神经网络net中的所有参数
                , lr=lr # 学习率
                , momentum=gamma # 动量参数
                )
# 对数据进行预处理
epochs = 2
batch_size = 4000

data = TensorDataset(X, y)
batchdata = DataLoader(data, batch_size=batch_size, shuffle=True)

#可以使用.datasets查看数据集相关的属性
print(len(batchdata.dataset)) #总共有多少数据

50000

print(batchdata.dataset[0:5]) # 查看其中前五个样本(是前五个样本,而不是前五个batch)

代码打印结果如下:

(tensor([[ 8.8439e-01, -1.3363e-01,  6.6058e-01,  9.2611e-01,  7.8543e-01,
         -5.5079e-01, -2.7012e-01, -5.5204e-01,  1.2068e+00,  1.4919e+00,
          9.6343e-01,  8.7922e-01,  1.5607e+00,  4.8819e-01, -1.1414e-01,
          6.1098e-02,  3.0282e-01,  1.0431e+00, -9.7493e-01, -1.0644e+00],
        [-5.7288e-01,  2.6316e+00,  3.0401e-01,  7.4866e-01,  3.1152e-01,
         -1.8385e+00, -2.2854e-01,  5.2794e-01,  1.7308e+00, -3.5668e-01,
         -8.9991e-01, -1.8500e-01, -2.6706e-01,  1.8283e+00, -2.8022e-01,
         -1.8416e-01,  6.4034e-01, -1.5039e+00,  3.2260e-01,  5.0082e-01],
        [-1.0566e+00, -2.6430e-01,  3.2868e+00,  9.8606e-01, -8.6114e-01,
          2.5253e+00,  1.6629e+00,  2.3904e-01, -7.2164e-01, -5.8973e-01,
         -7.1223e-01, -6.8196e-01, -1.3684e-01,  1.3941e+00, -2.0936e+00,
          8.1910e-01, -9.5350e-01,  1.3128e-03, -9.2216e-01, -1.1640e-01],
        [-4.5827e-01,  1.1861e+00,  9.8771e-01,  6.4958e-01, -1.0657e+00,
          5.7455e-01, -2.0034e-01,  1.2669e+00,  1.0582e+00,  2.5852e+00,
          1.4684e+00,  1.6047e+00,  1.7991e+00, -7.6422e-01,  8.1970e-01,
          1.3128e+00, -2.3513e-01,  8.0393e-01, -4.1873e-01,  4.4923e-01],
        [ 1.2122e+00, -1.0183e+00,  1.4373e+00,  9.6765e-01,  5.5025e-01,
         -8.8258e-01, -3.0236e-01,  1.2925e+00,  1.1455e+00,  1.0128e+00,
         -7.7963e-01, -6.5754e-01, -1.4936e-01,  4.8650e-01, -8.1125e-02,
          1.3128e+00,  3.0601e-01,  1.9770e+00, -1.0962e+00, -1.0372e+00]]), tensor([2., 1., 0., 2., 0.]))

也可以分别查看样本的特征张量和标签:

print(batchdata.dataset[0][0]) #查看第一个样本的特征张量

tensor([ 0.8844, -0.1336, 0.6606, 0.9261, 0.7854, -0.5508, -0.2701, -0.5520,
1.2068, 1.4919, 0.9634, 0.8792, 1.5607, 0.4882, -0.1141, 0.0611,
0.3028, 1.0431, -0.9749, -1.0644])

print(batchdata.dataset[0][1]) #查看第一个样本的标签

tensor(2.)

#属性batch_size,查看现在的batch_size是多少
batchdata.batch_size

4000

我们在迭代时,常常这样使用:

# 迭代循环
for i_epoch in range(epochs):
    for i_batch, (xb, yb) in enumerate(batchdata):
        opt.zero_grad()
        z_hat = net.forward(xb)
        loss = criterion(z_hat, yb.long())
        loss.backward()
        opt.step()
        print("epoch: {0}; batch: {1}; loss: {2}".format(i_epoch, i_batch, loss))

epoch: 0; batch: 0; loss: 1.1020163297653198

epoch: 0; batch: 1; loss: 1.1016513109207153

epoch: 0; batch: 2; loss: 1.1053823232650757

epoch: 0; batch: 3; loss: 1.1038187742233276

epoch: 0; batch: 4; loss: 1.101757526397705

epoch: 0; batch: 5; loss: 1.100555419921875

epoch: 0; batch: 6; loss: 1.1004079580307007

epoch: 0; batch: 7; loss: 1.1001160144805908

epoch: 0; batch: 8; loss: 1.1007158756256104

epoch: 0; batch: 9; loss: 1.1017191410064697

epoch: 0; batch: 10; loss: 1.09959077835083

epoch: 0; batch: 11; loss: 1.0989680290222168

epoch: 0; batch: 12; loss: 1.1008515357971191

epoch: 1; batch: 0; loss: 1.0995385646820068

epoch: 1; batch: 1; loss: 1.0994977951049805

epoch: 1; batch: 2; loss: 1.0994902849197388

epoch: 1; batch: 3; loss: 1.0982191562652588

epoch: 1; batch: 4; loss: 1.0990889072418213

epoch: 1; batch: 5; loss: 1.099034070968628

epoch: 1; batch: 6; loss: 1.0993760824203491

epoch: 1; batch: 7; loss: 1.0985561609268188

epoch: 1; batch: 8; loss: 1.0986825227737427

epoch: 1; batch: 9; loss: 1.0986409187316895

epoch: 1; batch: 10; loss: 1.098567008972168

epoch: 1; batch: 11; loss: 1.0989420413970947

epoch: 1; batch: 12; loss: 1.0988399982452393

我们看到,在多次迭代中,损失函数虽有波动,但总体呈现下降的趋势,神经网络在迭代中逐步被优化。

在下一篇文章中,我们将把小批量梯度下降法应用到 FashionMNIST 数据集的训练中,敬请期待!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容