pytorch入门使用及前置知识(2)NLP

1 深度学习的介绍

  深度学习(deep learning) 是机器学习的分支,是一种以人工神经网络为架构,对数据进行特征学习的算法。

  机器学习是人工智能的一种实现方式,深度学习是机器学习中的一种方法。

  • 机器学习和深度学习的区别

  (1)特征抽取的方式不同。机器学习需要有人工的特征提取的过程,深度学习没有复杂的人工特征提取的过程,特征提取的过程可以通过深度神经网络自动完成。

  (2)数据量不同。深度学习需要大量的训练数据集,会有更高的效果。深度学习训练深度神经网络需要大量的算力,因为其中有更多的参数

  • 深度学习的应用场景
      (1)图像识别:(物体识别)(场景识别)(人脸跟踪技术)(人脸身份认证)
      (2)自然语言处理:(机器翻译)(文本识别)(聊天对话)
      (3)语音技术:(语音识别)
  • 常见深度学习框架

  目前企业中常见的深度学习框架有很多,TensorFlow, Caffe2, Keras, Theano, PyTorch, Chainer, DyNet, MXNet, and CNTK等等。

  其中tensorflow和Kears是google出品的。使用者很多,工业界一般使用它们,但是其语法晦涩而且和python的语法不尽相同,对于入门玩家而言上手难度较高。然而后续的tensorflow2.0版本和Kears变得很很简单,语法结构变得和pytorch越来越接近。

  学术界一般使用facebook出的PyTorch,掌握了pytorch过后,后续接触tensorflow2.0版本和Kears也可以快速上手,所以不用拘泥于要学哪一种深度学习框架,PyTorch的使用和python的语法相同,整个操作类似Numpy的操作,并且 PyTorch使用的是动态计算,会让代码的调试变的更加简单。

  • 深度学习的步骤

2 神经网络的介绍

2.1 人工神经网络

  • 人工神经网络(Artificial Neural Network,ANN),简称神经网络(Neural Network,NN)或类神经网络,是一种模仿生物神经网络(动物的中枢神经系统,特别是大脑)的结构和功能的数学模型,用于对函数进行估计或近似。
    和其他机器学习方法一样,神经网络已经被用于解决各种各样的问题,例如机器视觉和语音识别。这些问题都是很难被传统基于规则的编程所解决的。
  • 神经元
      (1)在生物神经网络中,每个神经元与其他神经元相连,当它“兴奋”时,就会向相连的神经元发送化学物质,从而改变这些神经元内的电位;如果某神经元的电位超过了一个“阈值”,那么它就会被激活,即“兴奋”起来,向其他神经元发送化学物质。
    1943 年,McCulloch 和 Pitts 将上述情形抽象为上图所示的简单模型,这就是一直沿用至今的 M-P 神经元模型。把许多这样的神经元按一定的层次结构连接起来,就得到了神经网络。
      (2) 实例:一个简单的神经元如下图所示

  使用数学公式表达就是:     t = f ( W T A + b ) t = f(W^TA+b) t=f(WTA+b)

  可见,一个神经元的功能是求得输入向量与权向量的内积后,经一个非线性传递函数得到一个标量结果。

  • 单层神经网络

  单层神经网络是最基本的神经元网络形式,由有限个神经元构成,所有神经元的输入向量都是同一个向量。由于每一个神经元都会产生一个标量结果,所以单层神经元的输出是一个向量,向量的维数等于神经元的数目。

  • 感知机
      感知机由两层神经网络组成,输入层接收外界输入信号后传递给输出层(输出+1正例,-1反例),输出层是 M-P 神经元。

  感知机的作用把一个n维向量空间用一个超平面分割成两部分,给定一个输入向量,超平面可以判断出这个向量位于超平面的哪一边,得到输入时正类或者是反类,对应到2维空间就是一条直线把一个平面分为两个部分。

  • 多层神经网络
      多层神经网络就是由单层神经网络进行叠加之后得到的,所以就形成了层的概念,常见的多层神经网络有如下结构:
        输入层(Input layer):众多神经元(Neuron)接受大量输入消息。输入的消息称为输入向量。
        输出层(Output layer):消息在神经元链接中传输、分析、权衡,形成输出结果。输出的消息称为输出向量。
        隐藏层(Hidden layer):简称“隐层”,是输入层和输出层之间众多神经元和链接组成的各个层面。隐层可以有一层或多层。隐层的节点(神经元)数目不定,但数目越多神经网络的非线性越显著,从而神经网络的强健性(robustness)更显著。

2.2 全链接层

  • 全连接层:当前一层和前一层每个神经元相互链接,我们称当前这一层为全连接层。
  • 思考:假设第N-1层有m个神经元,第N层有n个神经元,当第N层是全连接层的时候,则N-1和N层之间有1,这些参数可以如何表示?

  从上图可以看出,所谓的经过全连接层操作后等同于就是进行了一个矩阵乘法,把n-1层的特征数量变换成为另一种数量。

2.3 激活函数

  • 激活函数的引入
      在前面的神经元的介绍过程中我们提到了激活函数,那么他到底是干什么的呢?
      假设我们有这样一组数据,三角形和四边形,需要把他们分为两类。

  通过不带激活函数的感知机模型我们可以划出一条线, 把平面分割开。

  假设我们确定了参数w和b之后,那么带入需要预测的数据,如果y>0,我们认为这个点在直线的右边,也就是正类(三角形),否则是在左边(四边形)。

  但是可以看出,三角形和四边形是没有办法通过直线分开的,那么这个时候该怎么办?

  在前面感知机的基础上,如果在加一层,变成多层,经过计算还是一个直线分割。

  所以现在引入激活函数,在加上非线性的激活函数之后,输出的结果就不在是一条直线了。

  右边是sigmoid函数,对感知机的结果,通过sigmoid函数进行处理。

  如果给定合适的参数w和b,就可以得到合适的曲线,能够完成对最开始问题的非线性分割。所以激活函数很重要的一个作用就是增加模型的非线性分割能力

  • 常见的激活函数

  看图可知:
    (1)sigmoid 只会输出正数,以及靠近0的输出变化率最大。
    (2)tanhsigmoid不同的是,tanh输出可以是负数。
    (3)Relu是输入只能大于0,如果你输入含有负数,Relu就不适合,如果你的输入是图片格式,Relu就挺常用的,因为图片的像素值作为输入时取值为[0,255]。

  • 激活函数的作用
    (1)增加模型的非线性分割能力
    (2)提高模型的鲁棒性
    (3)缓解梯度消失的问题
    (4)加快模型的收敛

2.4 神经网络例子

  一个男孩想要找一个女朋友,于是实现了一个女友判定机,随着年龄的增长,他的判定机也一直在变化。
14岁的时候:

  无数次碰壁之后,男孩意识到追到女孩的可能性和颜值一样重要,于是修改了判定机:

  在15岁的时候终于找到呢女朋友,但是一顿时间后他发现有各种难以忍受的习惯,最终决定分手。一段空窗期中,他发现找女朋友很复杂,需要更多的条件才能够帮助他找到女朋友,于是在25岁的时候,他再次修改了判定机:

  上述的超级女友判定机其实就是神经网络,它能够接受基础的输入,通过隐藏层的线性的和非线性的变化最终的到输出。

  • 通过上面例子,希望大家能够理解深度学习的思想

  输出的最原始、最基本的数据,通过模型来进行特征工程,进行更加高级特征的学习,然后通过传入的数据来确定合适的参数,让模型去更好的拟合数据。这个过程可以理解为盲人摸象,多个人一起摸,把摸到的结果乘上合适的权重,进行合适的变化,让他和目标值趋近一致。整个过程只需要输入基础的数据,程序自动寻找合适的参数。

3 Pytorch的介绍和使用

  首先我们先学习深度学习框架工具Pytorch的安装以及常用的使用方法

  安装官网https://pytorch.org/get-started/locally/

3.1 Pytorch的介绍

3.2 Pytorch的使用

3.2.1 创建张量

  • 张量的定义:张量是一个统称。如下都是张量

  (1)0阶张量:标量、常数,0-D Tensor
  (2)1阶张量:向量,1-D Tensor
  (3)2阶张量:矩阵,2-D Tensor
  (4)3阶张量
  (5)…
  (6)N阶张量

  • 使用实例

  使用python中的列表或者序列创建tensor

import torch
import numpy as np

# 1 使用python中的列表或者序列创建tensor
tensor = torch.tensor([[1.,-1.],[1.,-1.]])
print(tensor)
# tensor([[ 1., -1.],
#         [ 1., -1.]])

# 2 使用numpy中的数组创建tensor
tensor = torch.tensor(np.array([[1,2,3],[4,5,6]]))
print(tensor)
# tensor([[1, 2, 3],
#         [4, 5, 6]], dtype=torch.int32)

# 3 使用torch的API创建tensor
# 3.1 torch.empty(3,4)创建3行4列的空的tensor,会用无用数据进行填充
tensor = torch.empty(3,4)
print(tensor)
# tensor([[0.0000e+00, 0.0000e+00, 2.1019e-44, 0.0000e+00],
#         [0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00],
#         [0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00]])
# 3.2 torch.ones([3,4]) 创建3行4列的全为1的tensor
tensor = torch.ones(3,4)
print(tensor)
# tensor([[1., 1., 1., 1.],
#         [1., 1., 1., 1.],
#         [1., 1., 1., 1.]])
# 3.3 torch.zeros([3,4])创建3行4列的全为0的tensor
tensor = torch.zeros(3,4)
print(tensor)
# tensor([[0., 0., 0., 0.],
#         [0., 0., 0., 0.],
#         [0., 0., 0., 0.]])
tensor = torch.rand(3,4)
print(tensor)
# 3.4 torch.rand([3,4]) 创建3行4列的随机值的tensor,随机值的区间是[0, 1)
# tensor([[0.8135, 0.8997, 0.0663, 0.6364],
#         [0.3529, 0.8294, 0.8426, 0.1294],
#         [0.9024, 0.4060, 0.3591, 0.5211]])
# 3.5 torch.randint(low=0,high=10,size=[3,4]) 创建3行4列的随机整数的tensor,随机值的区间是[low, high)
tensor = torch.randint(3,10,size=(3,4))
print(tensor)
# tensor([[9, 6, 4, 7],
#         [3, 4, 3, 6],
#         [5, 4, 7, 6]])
# 3.6 torch.randn([3,4]) 创建3行4列的随机数的tensor,随机值的分布式均值为0,方差为1
tensor = torch.randn(3,4)
print(tensor)
# tensor([[-0.2704, -0.8910, -0.7306,  0.0561],
#         [ 1.6085, -0.7441,  0.2791, -0.9556],
#         [ 0.1022, -0.2119, -0.5276, -0.2539]])

3.2.2 tensor的常用方法

  • 使用实例
import torch
import numpy as np

# 1 获取tensor中的数据(当tensor中只有一个元素可用,获取该元素的值):tensor.item()
tensor = torch.tensor(np.arange(1))
print(tensor) # tensor([0], dtype=torch.int32)
item = tensor.item()
print(item) # 0

# 2 转化为numpy数组
tensor = torch.tensor(np.arange(4))
print(tensor) # tensor([0, 1, 2, 3], dtype=torch.int32)
numpy = tensor.numpy()
print(numpy) # [0 1 2 3]

# 3 获取形状:tensor.size()(size中可以传入参数,获取某一维度的数值,索引从0开始,-1表示倒数第一个)
tensor = torch.tensor(np.array([[1,2,3],[4,5,6],[7,8,9]]))
print(tensor)
# tensor([[1, 2, 3],
#         [4, 5, 6],
#         [7, 8, 9]], dtype=torch.int32)
size = tensor.size()
print(size) # torch.Size([3, 3])

# 4 形状改变:tensor.view((3,4))。
# 类似numpy中的reshape,是一种浅拷贝,仅仅是形状发生改变
# (view()中若参数为-1,表示根据确定的一维,自动设置另一维度数值)
tensor = torch.tensor(np.array([[1,2,3],[4,5,6],[7,8,9]]))
print(tensor)
# tensor([[1, 2, 3],
#         [4, 5, 6],
#         [7, 8, 9]], dtype=torch.int32)
tensor = tensor.view(9,1)
print(tensor)
# tensor([[1],
#         [2],
#         [3],
#         [4],
#         [5],
#         [6],
#         [7],
#         [8],
#         [9]], dtype=torch.int32)

# 5 获取阶数:tensor.dim()
tensor = torch.tensor(np.array([[1,2,3],[4,5,6],[7,8,9]]))
dim = tensor.dim()
print(dim) # 2

# 6 获取最大值:tensor.max()
tensor = torch.tensor(np.array([[1,2,3],[4,555,6],[7,8,9]]))
max = tensor.max()
print(max) # tensor(555, dtype=torch.int32)

# 7 tensor.argmax() 取最大值对应的下标
tensor = torch.tensor(np.array([[1,2,3],[4,555,6],[7,8,9]]))
max_index = tensor.argmax()
print(max_index) # tensor(4)

# 8 tensor.max(dim=0) 返回两个数组,一个是按列取最大值以及最大值的索引
tensor = torch.tensor(np.array([[1,2,3],[4,555,6],[7,8,9]]))
max = tensor.max(dim=0)
print(max)
# torch.return_types.max(
# values=tensor([  7, 555,   9], dtype=torch.int32),
# indices=tensor([2, 1, 2]))

# 9 tensor.max(dim=0) [0] 按列取最大值
tensor = torch.tensor(np.array([[1,2,3],[4,555,6],[7,8,9]]))
max = tensor.max(dim=0)[0]
print(max) # tensor([  7, 555,   9], dtype=torch.int32)

# 10 tensor.t() 转置
tensor = torch.tensor(np.array([[1,2,3],[4,555,6],[7,8,9]]))
tensor2 = tensor.T
print(tensor2)
tensor3 = tensor.t()
print(tensor3)
# tensor([[  1,   4,   7],
#         [  2, 555,   8],
#         [  3,   6,   9]], dtype=torch.int32)

# 11 tensor[1,3] 获取tensor中第一行第三列的值
tensor = torch.tensor(np.array([[1,2,3],[4,555,6],[7,8,9]]))
print(tensor)
# tensor([[1, 2, 3],
#         [4, 5, 6],
#         [7, 8, 9]], dtype=torch.int32)
print(tensor[1,2]) # tensor(6, dtype=torch.int32)

# 12 transpose(a,b) 交换某两维度的值
tensor = torch.tensor(np.array([[[1,2,3],[4,555,6],[7,8,9]],[[11,12,13],[15,16,17],[18,19,20]]]))
print(tensor)
# tensor([[[  1,   2,   3],
#          [  4, 555,   6],
#          [  7,   8,   9]],
#
#         [[ 11,  12,  13],
#          [ 15,  16,  17],
#          [ 18,  19,  20]]], dtype=torch.int32)
tensor = tensor.transpose(1,0)
print(tensor)
# tensor([[[  1,   2,   3],
#          [ 11,  12,  13]],
#
#         [[  4, 555,   6],
#          [ 15,  16,  17]],
#
#         [[  7,   8,   9],
#          [ 18,  19,  20]]], dtype=torch.int32)

# 13 permute()用法类似transpose(),但是permute()针对多维度,而transpose()最多操作2维度
tensor = torch.tensor(np.array([[[1,2,3],[4,555,6],[7,8,9]],[[11,12,13],[15,16,17],[18,19,20]]]))
print(tensor)
# tensor([[[  1,   2,   3],
#          [  4, 555,   6],
#          [  7,   8,   9]],
#
#         [[ 11,  12,  13],
#          [ 15,  16,  17],
#          [ 18,  19,  20]]], dtype=torch.int32)
tensor = tensor.permute(2,0,1)
print(tensor)
# tensor([[[  1,   4,   7],
#           [ 11,  15,  18]],
#
#          [[  2, 555,   8],
#           [ 12,  16,  19]],
#
#          [[  3,   6,   9],
#           [ 13,  17,  20]]], dtype=torch.int32)

# 14 tensor的切片
tensor = torch.tensor(np.array([[[1,2,3],[4,555,6],[7,8,9]],[[11,12,13],[15,16,17],[18,19,20]]]))
print(tensor)
# tensor([[[  1,   2,   3],
#          [  4, 555,   6],
#          [  7,   8,   9]],
#
#         [[ 11,  12,  13],
#          [ 15,  16,  17],
#          [ 18,  19,  20]]], dtype=torch.int32)
tensor = tensor[:,1]
print(tensor)
# tensor([[  4, 555,   6],
#         [ 15,  16,  17]], dtype=torch.int32)

3.2.3 tensor的数据类型

  tensor中的数据类型非常多,常见类型如下:

  上图中的Tensor types表示这种type的tensor是其实例

  • 使用实例
import torch
import numpy as np

# 1 获取tensor的数据类型:tensor.dtype
tensor = torch.randint(0,100,size=(3,4))
print(tensor)
# tensor([[83, 20, 27, 34],
#         [65, 55, 81, 67],
#         [21, 64, 14, 11]])
print(tensor.dtype) # torch.int64

# 2 创建数据的时候指定类型(torch.Tensor()创建时候,无法使用dtype指定形状;torch.tensor()相反)
tensor = torch.ones((3,4),dtype=torch.float32)
print(tensor)
# tensor([[1., 1., 1., 1.],
#         [1., 1., 1., 1.],
#         [1., 1., 1., 1.]])
print(tensor.dtype) # torch.float32

# 3 类型修改
tensor = torch.ones(3,4,dtype=torch.int32)
print(tensor.dtype) # torch.int32
tensor = tensor.type(torch.float32)
print(tensor.dtype) # torch.float32

3.2.4 tensor其他操作

代码实例

  • tensor相加
import torch

# 1 tensor和tensor相加
x = torch.ones(3,4,dtype=torch.float)
print(x)
# tensor([[1., 1., 1., 1.],
#         [1., 1., 1., 1.],
#         [1., 1., 1., 1.]])
y = torch.randint(0,5,size=(3,4))
print(y)
# tensor([[4, 4, 3, 1],
#         [2, 3, 0, 3],
#         [3, 4, 3, 0]])
z = x+y
print(z)
# tensor([[5., 5., 4., 2.],
#         [3., 4., 1., 4.],
#         [4., 5., 4., 1.]])
z = x.add(y)
print(z)
# tensor([[5., 5., 4., 2.],
#         [3., 4., 1., 4.],
#         [4., 5., 4., 1.]])
z = torch.add(x,y)
print(z)
# tensor([[5., 5., 4., 2.],
#         [3., 4., 1., 4.],
#         [4., 5., 4., 1.]])
x.add_(y) # 带下划线的方法(比如:add_)会对tensor进行就地修改
print(x)
# tensor([[5., 5., 4., 2.],
#         [3., 4., 1., 4.],
#         [4., 5., 4., 1.]])
  • CUDA中的tensor

  CUDA(Compute Unified Device Architecture),是NVIDIA推出的运算平台。 CUDA™是一种由NVIDIA推出的通用并行计算架构,该架构使GPU能够解决复杂的计算问题。

  torch.cuda这个模块增加了对CUDA tensor的支持,能够在cpu和gpu上使用相同的方法操作tensor

  通过.to方法能够把一个tensor转移到另外一个设备(比如从CPU转到GPU)

import torch

x = torch.ones(3,4)
# device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 'cuda:0' 0 表示多块GPU存在时,指定第0块GPU进行运算
if torch.cuda.is_available():
    device = torch.device("cuda")  # cuda device对象
    y = torch.ones_like(x, device=device)  # 创建一个在cuda上的tensor
    x = x.to(device)  # 使用方法把x转为cuda 的tensor
    z = x + y
    print(z) #  tensor([1.9806], device='cuda:0')
    # .to方法也能够同时设置类型
    print(z.to("cpu", torch.double))   # >> tensor([1.9806], dtype=torch.float64)

  因为深度学习需要强大的算力,而CPU虽然在调度任务上速度很快,但是在计算能力上相比GPU还是差了很多,所以我们在要进行计算的时候可以将tensor放入到GPU上计算,然后返回结果再返回到CPU上显示。

4 梯度下降和反向传播

4.1 什么是梯度下降

  • 梯度:一维就叫导数,多维以后就叫梯度,可以形象的认为是相切于函数图形线上的一个点的单位向量,而梯度下降就是顺着这个函数的梯度反方向移动,找到这个函数的最低点为止。

  • 回顾机器学习的线性回归和逻辑回归模型,首先收集数据,然后构建函数 f f f,得到 f ( x , w ) = Y p r e d i c t f(x,w)=Y_{predict} f(x,w)=Ypredict 。我们要做的就是收集足够的x数据,然后把x输入要函数中,用梯度下降来调整w。判断好坏的方法有回归损失和分类损失两种:

     l o s s = ( Y p r e d i c t − Y t r u e ) 2 loss=(Y_{predict}-Y_{true})^2 loss=(YpredictYtrue)2  (回归损失)

     l o s s = Y t r u e ⋅ log ⁡ Y p r e d i c t loss=Y_{true}{\cdot}\log{Y_{predict}} loss=YtruelogYpredict  (分类损失)

  而我们的目标就是通过调整(学习)参数w,尽可能的降低 l o s s loss loss,那么如何调整w呢,就是要梯度下降来做

   w w w的更新方法

    1、计算w的梯度(导数): Δ w = f ( w + 0.00001 ) − f ( w − 0.00001 ) 2 ∗ 0.00001 \Delta w=\frac{f(w+0.00001)-f(w-0.00001)}{2*0.00001} Δw=20.00001f(w+0.00001)f(w0.00001)

    2、更新w:    KaTeX parse error: Undefined control sequence: \Deltaw at position 16: w = w - \alpha \̲D̲e̲l̲t̲a̲w̲

      其中 α Δ w < 0 \alpha \Delta w<0 αΔw<0,意味着w将增大
      其中 α Δ w > 0 \alpha \Delta w>0 αΔw>0,意味着w将减小

练习

4.2 反向传播

  • 反向传播的作用是为了在计算出结果之后,根据结果如何调整输入参数权重w的一种方法
      在上面的练习中, J ( a , b , c ) = 3 ( a + b c ) J(a,b,c) = 3 (a + bc) J(a,b,c)=3(a+bc),令 u = a + v u = a + v u=a+v v = b c v=bc v=bc,可以绘制成计算图的形式,可以清晰的看到向前计算的过程。那么,反向传播的过程就是一个从右往左的过程,自变量a,b,c各自的偏导就是连线上梯度的乘积

4.3 神经网络中的反向传播

  • w 1 w_1 w1, w 2 w_2 w2,… w n w_n wn表示第n层的权重, w n [ i , j ] w_n[i,j] wn[i,j]表示第i个神经元,链接到n+1层第j个神经元的权重。

  那么此时 w 1 [ 1 , 2 ] w_1[1,2] w1[1,2]的偏导该如何求解呢?通过观察,发现从 o u t out out w 1 [ 1 , 2 ] w_1[1,2] w1[1,2]来连接线有两条?结果如下:

     d o u t d W 1 [ 1 , 2 ] = x 1 ∗ f ′ ( a 2 ) ∗ ( W 2 [ 2 , 1 ] ∗ f ′ ( b 1 ) ∗ W 3 [ 1 , 1 ] ∗ Δ o u t + W 2 [ 2 , 2 ] ∗ f ′ ( b 2 ) ∗ W 3 [ 2 , 1 ] ∗ Δ o u t ) \frac{dout}{dW_1[1,2]}=x_1*f^{'}(a2)*(W_2[2,1]*f^{'}(b1)*W_3[1,1]*{\Delta}out+W_2[2,2]*f^{'}(b2)*W_3[2,1]*{\Delta}out) dW1[1,2]dout=x1f(a2)(W2[2,1]f(b1)W3[1,1]Δout+W2[2,2]f(b2)W3[2,1]Δout)

  但是这样做,当模型很大的时候,计算量非常大

  所以反向传播的思想就是对其中的某一个参数单独求梯度,之后更新,如下图所示:

  更新参数之后,继续反向传播

  继续反向传播

  通过反向传播,并且空间换时间的方式,这样就完成了每一次的参数更新,这就是深度学习基本每个算法都要经历的本质原理。

4.4 梯度计算和反向传播的相关代码

  了解了反向传播的原理,那么在代码中,我们是如何获取梯度的呢?首先要用.requires_grad=True设置tensor,此时这个tensor的每一个运算都会被记录下来,然后使用一个参数.backward()方法来进行反向传播,对参数(requires_grad=True)的去计算他的梯度,并且把它累加保存到.gard中,此时还并未更新其梯度(因此每次反向传播之前,需要将梯度设置为0)。

代码实例:对下面函数进行反向传播求梯度

import torch
# 反向传播求梯度案例:
x = torch.ones(2, 2, requires_grad=True)  # 初始化参数x并设置requires_grad=True用来追踪其计算历史
print(x)
# tensor([[1., 1.],
#        [1., 1.]], requires_grad=True)

y = x + 2
print(y)
# tensor([[3., 3.],
#        [3., 3.]], grad_fn=<AddBackward0>)

z = y * y * 3  # 平方x3
print(z)
# tensor([[27., 27.],
#        [27., 27.]], grad_fn=<MulBackward0>)

out = z.mean()  # 求均值
print(out)
# tensor(27., grad_fn=<MeanBackward0>)

# 反向传播,计算out的每一个参数
out.backward()
print(x.grad) 
# tensor([[4.5000, 4.5000],
#         [4.5000, 4.5000]])

# 操作requires_grad属性的各种方法
a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
print(a.requires_grad)  # False
a.requires_grad_(True)  # 就地修改
print(a.requires_grad)  # True
b = (a * a).sum()
print(b.grad_fn)  # <SumBackward0 object at 0x4e2b14345d21>
with torch.no_grad():
    c = (a * a).sum()  # tensor(151.6830),此时c没有gard_fn

print(c.requires_grad)  # False

  • 注意:
      tensor.data:
        tensor的require_grad=False时,tensor.datatensor等价
        require_grad=True时,tensor.data仅仅是获取tensor中的数据内容,不带grad等属性
      tensor.numpy():
        require_grad=True时不能够直接转换,需要使用tensor.detach().numpy(),实现对tensor中数据的深拷贝,将tensor转换为ndarry类型

4.5 反向传播完成线性回归

  下面,我们使用一个自定义的数据,来使用torch实现一个简单的线性回归

  假设我们的基础模型就是y = wx+b,其中w和b均为参数,我们使用y = 3x+0.8来构造数据x、y,所以最后通过模型应该能够得出w和b应该分别接近3和0.8

  步骤:(1)准备数据(2)计算预测值(3)计算损失,把梯度置为0,反向传播(4)更新参数

import torch
import numpy as np
from matplotlib import pyplot as plt

# 1. 准备数据 y = 3x+0.8,准备参数
x = torch.rand([50])
random = np.random.rand(50)
y = 3 * x + 0.8+random

w = torch.rand(1, requires_grad=True)
b = torch.rand(1, requires_grad=True)

def loss_fn(y, y_predict):
    loss = (y_predict - y).pow(2).mean()
    for i in [w, b]:
        # 每次反向传播前把梯度置为0
        if i.grad is not None:
            i.grad.data.zero_()
    # [i.grad.data.zero_() for i in [w,b] if i.grad is not None]
    loss.backward()
    return loss.data

def optimize(learning_rate):
    # print(w.grad.data,w.data,b.data)
    w.data -= learning_rate * w.grad.data
    b.data -= learning_rate * b.grad.data

for i in range(3000):
    # 2. 计算预测值
    y_predict = x * w + b

    # 3.计算损失,把参数的梯度置为0,进行反向传播
    loss = loss_fn(y, y_predict)

    if i % 500 == 0:
        print(i, loss)
    # 4. 更新参数w和b
    optimize(0.01)

# 绘制图形,观察训练结束的预测值和真实值
predict = x * w + b  # 使用训练后的w和b计算预测值

plt.scatter(x.data.numpy(), y.data.numpy(), c="r")
plt.plot(x.data.numpy(), predict.data.numpy())
plt.show()

print("w", w) # w tensor([2.9068], requires_grad=True)
print("b", b) # b tensor([1.4096], requires_grad=True)

图形效果如下:

5 Pytorch模型及其API

  在前一部分,我们自己实现了通过torch的相关方法完成反向传播和参数更新,在pytorch中预设了一些更加灵活简单的对象,让我们来构造模型、定义损失,优化损失等。那么接下来,我们一起来了解一下其中常用的API。

5.1 nn.Module

  nn.Moduletorch.nn提供的一个类,是pytorch中我们自定义网络的一个基类,在这个类中定义了很多有用的方法,让我们在继承这个类定义网络的时候非常简单。

  当我们自定义网络的时候,有两个方法需要特别注意:
  (1)__init__需要调用super方法,继承父类的属性和方法
  (2)farward方法必须实现,用来定义我们的网络的向前计算的过程

  用前面的y = wx+b的模型举例如下:

from torch import nn
class Lr(nn.Module):
    def __init__(self):
        super(Lr, self).__init__()  #继承父类init的参数
        self.linear = nn.Linear(1, 1)  # Linear(输入特征形状,输出特征形状即列数)

	# forward 完成一次前向计算过程
    def forward(self, x):
        out = self.linear(x)
        return out

注意
  nn.Linear为torch预定义好的线性模型,也被称为全链接层,传入的参数为输入的数量输出的数量(in_features, out_features),注意是不算(batch_size)的列数的的。
  nn.Module定义了__call__方法,实现的就是调用forward方法,即Lr的实例,能够直接被传入参数调用,实际上调用的是forward方法并传入参数

# 实例化模型
model = Lr()
# 传入数据,计算结果
predict = model(x)

5.2 优化器类

  优化器(optimizer),可以理解为torch为我们封装的用来进行更新参数的方法,比如常见的随机梯度下降(stochastic gradient descent SGD)

  优化器类都是由torch.optim提供的,例如:
  (1)torch.optim.SGD(参数,学习率)
  (2)torch.optim.Adam(参数,学习率)

注意
  (1)参数可以使用model.parameters()来获取,获取模型中所有requires_grad=True的参数
  (2)优化类的使用方法
    1 实例化
    2 所有参数的梯度,将其值置为0
    3 反向传播计算梯度
    4 更新参数值

optimizer = optim.SGD(model.parameters(), lr=1e-3) #1. 实例化
optimizer.zero_grad() #2. 梯度置为0
loss.backward() #3. 计算梯度
optimizer.step()  #4. 更新参数的值

5.3 损失函数

  前面的例子是一个回归问题,torch中也预测了很多损失函数

    (1)均方误差:nn.MSELoss(),常用于回归问题

    (2)交叉熵损失:nn.CrossEntropyLoss(),常用于分类问题

model = Lr() #1. 实例化模型
criterion = nn.MSELoss() #2. 实例化损失函数
optimizer = optim.SGD(model.parameters(), lr=1e-3) #3. 实例化优化器类
for i in range(100):
    y_predict = model(x_true) #4. 向前计算预测值
    loss = criterion(y_true,y_predict) #5. 调用损失函数传入真实值和预测值,得到损失结果
    optimizer.zero_grad() #5. 当前循环参数梯度置为0
    loss.backward() #6. 计算梯度
    optimizer.step()  #7. 更新参数的值

5.4 用Pytorch的模型完成线性回归

import torch
from torch import nn
from torch import optim

from matplotlib import pyplot as plt

x = torch.rand([50, 1])
ran = torch.rand([50,1])
print(ran)
y = x * 3 + 0.8+ran


class Lr(nn.Module):
    def __init__(self):
        super(Lr, self).__init__()
        self.linear = nn.Linear(1, 1)

    def forward(self,x):
        out = self.linear(x)
        return out


model = Lr()
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=1e-3)

for i in range(30000):
    out = model(x)
    loss = criterion(y, out)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    if (i + 1) % 20 == 0:
        print('Epoch[{}/{}],loss:{:.6f}'.format(i, 30000, loss.data))

#4. 模型评估
# 或者model.train(False),model.train属性默认为True表示该模型用于训练,False等同于model.eval()
model.eval() #设置模型为评估模式,即预测模式
predict = model(x)
predict = predict.data.numpy()
plt.scatter(x.data.numpy(),y.data.numpy(),c="r")
plt.plot(x.data.numpy(),predict)
plt.show()


注意
  model.eval()表示设置模型为评估模式,即预测模式

  model.train(mode=True) 表示设置模型为训练模式

  在当前的线性回归中,上述并无区别

  但是在其他的一些模型中,训练的参数预测的参数会不相同,到时候就需要具体告诉程序我们是在进行训练还是预测,比如模型中存在DropoutBatchNorm的时候

5.5 使用GPU完成训练

import torch
from torch import nn
from torch import optim
import numpy as np
from matplotlib import pyplot as plt
import time

# 1. 定义数据
x = torch.rand([50,1])
y = x*3 + 0.8

#2 .定义模型
class Lr(nn.Module):
    def __init__(self):
        super(Lr,self).__init__()
        self.linear = nn.Linear(1,1)

    def forward(self, x):
        out = self.linear(x)
        return out

# 2. 实例化模型,loss,和优化器

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
x,y = x.to(device),y.to(device)

model = Lr().to(device)
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=1e-3)

#3. 训练模型
for i in range(300):
    out = model(x)
    loss = criterion(y,out)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    if (i+1) % 20 == 0:
        print('Epoch[{}/{}], loss: {:.6f}'.format(i,30000,loss.data))

#4. 模型评估
model.eval() #
predict = model(x)
predict = predict.cpu().detach().numpy() #转化为numpy数组
plt.scatter(x.cpu().data.numpy(),y.cpu().data.numpy(),c="r")
plt.plot(x.cpu().data.numpy(),predict,)
plt.show()

6 常见的优化算法

6.1 梯度下降算法(batch gradient descent BGD)

  每次迭代都需要把所有样本都送入,这样的好处是每次迭代都顾及了全部的样本,做的是全局最优化,但是有可能达到局部最优。

6.2 随机梯度下降法 (Stochastic gradient descent SGD)

  针对梯度下降算法训练速度过慢的缺点,提出了随机梯度下降算法,随机梯度下降算法算法是从样本中随机抽出一组,训练后按梯度更新一次,然后再抽取一组,再更新一次,在样本量及其大的情况下,可能不用训练完所有的样本就可以获得一个损失值在可接受范围之内的模型了。
  torch中的api为:torch.optim.SGD()

6.3 小批量梯度下降 (Mini-batch gradient descent MBGD)

  SGD相对来说要快很多,但是也有存在问题,由于单个样本的训练可能会带来很多噪声,使得SGD并不是每次迭代都向着整体最优化方向,因此在刚开始训练时可能收敛得很快,但是训练一段时间后就会变得很慢。在此基础上又提出了小批量梯度下降法,它是每次从样本中随机抽取一小批进行训练,而不是一组,这样即保证了效果又保证的速度。

6.4 动量法

  SGD算法虽然这种算法能够带来很好的训练速度,但是在到达最优点的时候并不能够总是真正到达最优点,而是在最优点附近徘徊

  SGD另一个缺点是需要我们挑选一个合适的学习率,当我们采用小的学习率的时候,会导致网络在训练的时候收敛太慢;当我们采用大的学习率的时候,会导致在训练过程中优化的幅度跳过函数的范围,也就是可能跳过最优点。我们所希望的仅仅是网络在优化的时候网络的损失函数有一个很好的收敛速度同时又不至于摆动幅度太大。

  所以使用动量法Momentum优化器刚好可以解决我们所面临的问题,它主要是基于梯度的移动指数加权平均,对网络的梯度进行平滑处理的,让梯度的摆动幅度变得更小。

  •    g r a d e n t = 0.8 Δ w + 0.2 h i s t o r y − g r a d e n t , Δ w 表 示 当 前 一 次 的 梯 度 gradent = 0.8 {\Delta}w + 0.2 history_-gradent \quad ,{\Delta}w表示当前一次的梯度 gradent=0.8Δw+0.2historygradent,Δw
       w = w − a ∗ g r a d e n t , a 表 示 学 习 率 w = w - a*gradent \quad ,a表示学习率 w=wagradent,a

6.5 AdaGrad

  AdaGrad算法就是将每一个参数的每一次迭代的梯度取平方累加后在开方,用全局学习率除以这个数,作为学习率的动态更新,从而达到自适应学习率的效果。

  •    g r a d e n t = h i s t o r y − g r a d e n t + ( Δ w ) 2 gradent = history_-gradent +({\Delta}w)^2 gradent=historygradent+(Δw)2
       w = w − a g r a d e n t + δ Δ w Δ w , δ 为 小 常 数 , 为 了 数 值 稳 定 大 约 设 置 为 1 0 − 7 w = w - \frac{a}{\sqrt{gradent}+\delta}{\Delta}w \quad {\Delta}w,{\delta}为小常数,为了数值稳定大约设置为10^{-7} w=wgradent +δaΔwΔw,δ107

6.6 RMSProp

  Momentum优化算法中,虽然初步解决了优化中摆动幅度大的问题,为了进一步优化损失函数在更新中存在摆动幅度过大的问题,并且进一步加快函数的收敛速度,RMSProp算法对参数的梯度使用了平方加权平均数。

  •    g r a d e n t = 0.8 ∗ h i s t o r y − g r a d e n t + 0.2 ∗ ( Δ w ) 2 gradent = 0.8 * history_-gradent +0.2 * ({\Delta}w)^2 gradent=0.8historygradent+0.2(Δw)2
       w = w − a g r a d e n t + δ Δ w w = w - \frac{a}{\sqrt{gradent}+\delta}{\Delta}w w=wgradent +δaΔw

6.7 Adam(常用)

  Adam(Adaptive Moment Estimation)算法是将Momentum算法和RMSProp算法结合起来使用的一种算法,能够达到防止梯度的摆幅多大(自适应学习率),同时还能够加开收敛速度

  •   1.需要初始化梯度的累积量和平方累积量
       v w = 0 , s w = 0 v_w = 0, s_w = 0 vw=0,sw=0
      2.第t轮训练中,我们首先可以计算得到Momentum和RMSProp的参数更新:
       v w = 0.8 v + 0.2 Δ w , M o m e n t u m 计 算 的 梯 度 v_w = 0.8 v + 0.2 {\Delta}w \quad ,Momentum计算的梯度 vw=0.8v+0.2Δw,Momentum
       s w = 0.8 ∗ s + 0.2 ∗ ( Δ w ) 2 , R M S P r o p 计 算 的 梯 度 s_w = 0.8 * s +0.2 * ({\Delta}w)^2 \quad ,RMSProp计算的梯度 sw=0.8s+0.2(Δw)2,RMSProp
      3.最后得到本次参数
       w = w − a s w + δ v w w = w - \frac{a}{\sqrt{s_w}+\delta}v_w w=wsw +δavw

  torch中的api为:torch.optim.Adam()

7 Pytorch的数据加载

7.1 数据集类

  但是在深度学习中,数据量通常是都非常多,非常大的,如此大量的数据,不可能一次性的在模型中进行向前的计算和反向传播,经常我们会对整个数据进行随机的打乱顺序,把数据处理成一个个的batch,同时还会对数据进行预处理。所以,以下pytorch中的数据加载的方法。

  在torch中提供了数据集的基类torch.utils.data.Dataset,继承这个基类,我们能够非常快速的实现对数据的加载。

  torch.utils.data.Dataset的源码如下:

class Dataset(object):
    """An abstract class representing a Dataset.
    All other datasets should subclass it. All subclasses should override
    ``__len__``, that provides the size of the dataset, and ``__getitem__``,
    supporting integer indexing in range from 0 to len(self) exclusive.
    """

    def __getitem__(self, index):
        raise NotImplementedError

    def __len__(self):
        raise NotImplementedError

    def __add__(self, other):
        return ConcatDataset([self, other])

  可知:我们需要在自定义的数据集类中继承Dataset类,同时还需要实现两个方法:
  (1)__len__方法,能够实现通过全局的len()方法获取其中的元素个数
  (2)__getitem__方法,能够通过传入索引的方式获取数据,例如通过dataset[i]获取其中的第i条数据

案例代码:
  下面通过一个例子来看看如何使用Dataset来加载数据

  数据来源http://archive.ics.uci.edu/ml/datasets/SMS+Spam+Collection

  数据介绍:SMS Spam Collection是用于骚扰短信识别的经典数据集,完全来自真实短信内容,包括4831条正常短信和747条骚扰短信。正常短信和骚扰短信保存在一个文本文件中。 每行完整记录一条短信内容,每行开头通过ham和spam标识正常短信和骚扰短信

  数据实例

  实现如下:

from torch.utils.data import Dataset,DataLoader
import pandas as pd

data_path = r"data\SMSSpamCollection"

class CifarDataset(Dataset):
    def __init__(self):
        lines = open(data_path,"r",encoding="UTF-8")
        #对数据进行处理,前4个为label,后面的为短信内容
        lines = [[i[:4].strip(),i[4:].strip()] for i in lines]
        #转化为dataFrame
        self.df = pd.DataFrame(lines,columns=["label","context"])

    def __getitem__(self, index):
        single_item = self.df.iloc[index,:]
        return single_item.values[0],single_item.values[1]

    def __len__(self):
        return self.df.shape[0]

dataset = CifarDataset()
for i in range(len(dataset )):
    print(i,dataset [i])
# ....
# 5571 ('ham', 'Pity, * was in mood for that. So...any other suggestions?')
# 5572 ('ham', "The guy did some bitching but I acted like i'd be interested in buying something else next week and he gave it to us for free")
# 5573 ('ham', 'Rofl. Its true to its name')

7.2 迭代数据集

  使用上述的方法能够进行数据的读取,但是其中还有很多想要处理数据集的功能没有实现
  (1)批处理数据(Batching the data)
  (2)打乱数据(Shuffling the data)
  (3)使用多线程 multiprocessing 并行加载数据。

  在pytorch中torch.utils.data.DataLoader提供了上述的所用方法。其中参数含义如下:
  (1)dataset:提前定义的dataset的实例
  (2)batch_size:传入数据的batch的大小,常用128,256等等
  (3)shuffle:bool类型,表示是否在每次获取数据的时候提前打乱数据
  (4)num_workers:加载数据的线程数

DataLoader的使用方法示例:

from torch.utils.data import Dataset,DataLoader
import pandas as pd

data_path = r'data\SMSSpamCollection'

class CifarDataset(Dataset):
    def __init__(self):
        lines = open(data_path,"r",encoding="UTF-8")
        lines = [[i[:4].strip(),i[4:].strip()] for i in lines]
        self.df = pd.DataFrame(lines,columns=["label","context"])

    def __getitem__(self, item):
        single_item = self.df.iloc[item,:]
        return single_item.values[0],single_item.values[1]

    def __len__(self):
        return self.df.shape[0]

dataset = CifarDataset()
data_loader = DataLoader(dataset=dataset,batch_size=10,shuffle=True,num_workers=2)

if __name__=='__main__':
    for index, (label, context) in enumerate(data_loader):
      print(index,label,context)
      print("*"*100)
  

  注意
  (1)len(dataset) = 数据集的样本数
  (2)len(dataloader) = math.ceil(样本数/batch_size) 即向上取整

7.3 Pytorch自带的数据集

  pytorch中自带的数据集由两个上层api提供,分别是torchvisiontorchtext。其中:

  (1)torchvision提供了对图片数据处理相关的api和数据。数据位置:torchvision.datasets,例如:torchvision.datasets.MNIST(手写数字图片数据)
  (2)torchtext提供了对文本数据处理相关的API和数据。数据位置:torchtext.datasets,例如:torchtext.datasets.IMDB(电影评论文本数据)

  下面我们以Mnist手写数字为例,来看看pytorch如何加载其中自带的数据集。使用方法和之前一样:准备好Dataset实例,然后
把dataset交给dataloder 打乱顺序,最后组成batch。返回值为(图片,目标值)组,这个结果也可以通过观察源码得到

  MNIST是由Yann LeCun等人提供的免费的图像识别的数据集,其中包括60000个训练样本和10000个测试样本,其中图拍了的尺寸已经进行的标准化的处理,都是黑白的图像,大小为28X28。数据集的原始地址为:http://yann.lecun.com/exdb/mnist/

  MNIST API中的参数介绍
  torchvision.datasets.MNIST(root='/files/', train=True, download=True, transform=)
    root参数表示数据存放的位置
    train:bool类型,表示是使用训练集的数据还是测试集的数据
    download:bool类型,表示是否需要下载数据到root目录
    transform:实现的对图片的处理函数

代码实例

import torchvision

dataset = torchvision.datasets.MNIST(root="./data",train=True,download=True,transform=None)

print(dataset)
# Dataset MNIST
#     Number of datapoints: 60000
#     Root location: ./data
#     Split: Train
print(dataset[0])
print(dataset[0][0])  # image 
print(dataset[0][1])  # label

8 案例:数字识别

8.1 思路

(1)准备数据,这些需要准备DataLoader
(2)构建模型,这里可以使用torch构造一个深层的神经网络
(3)模型的训练
(4)模型的保存,保存模型,后续持续使用
(5)模型的评估,使用测试集,观察模型的好坏

8.2 准备数据

  注意:调用MNIST返回的结果中图形数据是一个Image对象,所以需要对其进行处理为了进行数据的处理,需要使用torchvision.transfroms.ToTensortorchvision.transforms.Normalize(mean, std)、和torchvision.transforms.Compose(transforms)方法。

8.2.1 torchvision.transfroms.ToTensor

  .ToTensor是把一个取值范围是[0,255]PIL.Image或者shape为(H,W,C)的numpy.ndarray,转换成形状为[C,H,W]

  其中(H,W,C)意思为(高,宽,通道数),黑白图片的通道数只有1,其中每个像素点的取值为[0,255]。彩色图片的通道数为(R,G,B),每个通道的每个像素点的取值为[0,255],三个通道的颜色相互叠加,形成了各种颜色。

示例如下

from torchvision import transforms
import numpy as np

data = np.random.randint(0,255,size=12)
img = data.reshape([2,2,3])
print(img.shape) # (2, 2, 3)
img_tensor = transforms.ToTensor()(img)
print(img_tensor)
# tensor([[[198, 138],
#          [116, 238]],
# 
#         [[201,  97],
#          [134, 194]],
# 
#         [[215, 108],
#          [  6, 226]]], dtype=torch.int32)
print(img_tensor.shape) # torch.Size([3, 2, 2])

8.2.2 torchvision.transforms.Normalize

  .Normalize(mean, std)是指给定均值:mean,shape和图片的通道数相同(指的是每个通道的均值),方差:std,和图片的通道数相同(指的是每个通道的方差),将会把Tensor规范化处理。
即:Normalized_image=(image-mean)/std。

from torchvision import transforms
import numpy as np
import torchvision
data = np.random.randint(0, 255, size=12)
img = data.reshape([2,2,3])
img = transforms.ToTensor()(img) # 转换成tensor
img.float()
print(img)
# tensor([[[119., 206.],
#          [ 81.,  45.]],
# 
#         [[180., 121.],
#          [ 38., 164.]],
# 
#         [[ 26., 224.],
#          [164., 128.]]])
print("*"*100)
norm_img = transforms.Normalize((10,10,10), (1,1,1))(img) #进行规范化处理,指定均值为10,标准差为1
print(norm_img)
# tensor([[[109., 196.],
#          [ 71.,  35.]],
# 
#         [[170., 111.],
#          [ 28., 154.]],
# 
#         [[ 16., 214.],
#          [154., 118.]]])

注意:在sklearn中,默认上式中的std和mean为数据每列的std和mean,sklearn会在标准化之前算出每一列的std和mean。但是在api:Normalize中并没有帮我们计算,所以我们需要手动计算。

  (1)当mean为全部数据的均值,std为全部数据的std的时候,才是进行了标准化。

  (2)如果mean(x)不是全部数据的mean的时候,std(y)也不是的时候,Normalize后的数据分布满足下面的关系

     n e w − m e a n = m e a n − x y new_-mean=\frac{mean-x}{y} newmean=ymeanx,mean为原数据的均值,x为传入的均值
     n e w − s t d = s t d y new_-std=\frac{std}{y} newstd=ystd,y为传入的标准差y

8.2.3 torchvision.transforms.Compose(transforms)

  将多个transform组合起来使用。

transforms.Compose([
     torchvision.transforms.ToTensor(), #先转化为Tensor
     torchvision.transforms.Normalize(mean,std) #在进行正则化
 ])

8.2.4 构建数据集

import torchvision
import torch
from torch.utils.data import Dataset,DataLoader

dataset = torchvision.datasets.MNIST('\data',train=True,download=True,
                                     transform=torchvision.transforms.Compose([
                                         torchvision.transforms.ToTensor(),
                                         torchvision.transforms.Normalize(
                                             (0.1307,),(0.3081,))
                                     ]))
train_dataloader = DataLoader(dataset,batch_size=64,shuffle=True)

8.3 构建模型

  该模型的构建准备使用一个三层的神经网络,其中包括两个全连接层和一个输出层,第一个全连接层会经过激活函数的处理,将处理后的结果交给下一个全连接层,进行变换后输出结果。

import torch
from torch import nn
import torch.nn.functional as F

class MnistNet(nn.Module):
    def __init__(self):
        super(MnistNet,self).__init__()
        self.fc1 = nn.Linear(28*28*1,28)  #定义Linear的输入和输出的形状
        self.fc2 = nn.Linear(28,10)  #定义Linear的输入和输出的形状

    def forward(self,x):
        x = x.view(-1,28*28*1)  #对数据形状变形,-1表示该位置根据后面的形状自动调整
        x = self.fc1(x) #[batch_size,28]
        x = F.relu(x)  #[batch_size,28]
        x = self.fc2(x) #[batch_size,10]
 

8.4 模型训练

  • 训练的流程:
    (1)实例化模型,设置模型为训练模式
    (2)实例化优化器类,实例化损失函数
    (3)获取,遍历dataloader
    (4)梯度置为0
    (5)进行向前计算
    (6)计算损失
    (7)反向传播
    (8)更新参数
mnist_net = MnistNet()
optimizer = optim.Adam(mnist_net.parameters(),lr= 0.001)
def train(epoch):
    mode = True
    mnist_net.train(mode=mode) #模型设置为训练模型
    
    train_dataloader = get_dataloader(train=mode) #获取训练数据集
    for idx,(data,target) in enumerate(train_dataloader):
        optimizer.zero_grad() #梯度置为0
        output = mnist_net(data) #进行向前计算
        loss = F.nll_loss(output,target) #带权损失
        loss.backward()  #进行反向传播,计算梯度
        optimizer.step() #参数更新
        if idx % 10 == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, idx * len(data), len(train_dataloader.dataset),
                       100. * idx / len(train_dataloader), loss.item()))

8.5 模型保存和加载

torch.save(mnist_net.state_dict(),"model/mnist_net.pt") #保存模型参数
torch.save(optimizer.state_dict(), 'results/mnist_optimizer.pt') #保存优化器参数

8.6 模型的评估

  • 评估的过程和训练的过程相似,但是:
    (1)不需要计算梯度
    (2)需要收集损失和准确率,用来计算平均损失和平均准确率
    (3)损失的计算和训练时候损失的计算方法相同
  • 准确率的计算:
    (1)模型的输出为[batch_size,10]的形状
    (2)其中最大值的位置就是其预测的目标值(预测值进行过sotfmax后为概率,sotfmax中分母都是相同的,分子越大,概率越大)
    (3)最大值的位置获取的方法可以使用torch.max,返回最大值和最大值的位置
    (4)返回最大值的位置后,和真实值([batch_size])进行对比,相同表示预测成功
def test():
    test_loss = 0
    correct = 0
    mnist_net.eval()  #设置模型为评估模式
    test_dataloader = get_dataloader(train=False) #获取评估数据集
    with torch.no_grad(): #不计算其梯度
        for data, target in test_dataloader:
            output = mnist_net(data)
            test_loss += F.nll_loss(output, target, reduction='sum').item()
            pred = output.data.max(1, keepdim=True)[1] #获取最大值的位置,[batch_size,1]
            correct += pred.eq(target.data.view_as(pred)).sum()  #预测准备样本数累加
    test_loss /= len(test_dataloader.dataset) #计算平均损失
    print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(
        test_loss, correct, len(test_dataloader.dataset),
        100. * correct / len(test_dataloader.dataset)))

8.7 完整的代码

import torch
from torch import nn
from torch import optim
import torch.nn.functional as F
import torchvision

train_batch_size = 64
test_batch_size = 1000
img_size = 28

def get_dataloader(train=True):
    assert isinstance(train,bool),"train 必须是bool类型"

    #准备数据集,其中0.1307,0.3081为MNIST数据的均值和标准差,这样操作能够对其进行标准化
    #因为MNIST只有一个通道(黑白图片),所以元组中只有一个值
    dataset = torchvision.datasets.MNIST('/data', train=train, download=True,
                                         transform=torchvision.transforms.Compose([
                                         torchvision.transforms.ToTensor(),
                                         torchvision.transforms.Normalize((0.1307,), (0.3081,)),]))
    #准备数据迭代器
    batch_size = train_batch_size if train else test_batch_size
    dataloader = torch.utils.data.DataLoader(dataset,batch_size=batch_size,shuffle=True)
    return dataloader

class MnistNet(nn.Module):
    def __init__(self):
        super(MnistNet,self).__init__()
        self.fc1 = nn.Linear(28*28*1,28)
        self.fc2 = nn.Linear(28,10)

    def forward(self,x):
        x = x.view(-1,28*28*1)
        x = self.fc1(x) #[batch_size,28]
        x = F.relu(x)  #[batch_size,28]
        x = self.fc2(x) #[batch_size,10]
        # return x
        return F.log_softmax(x,dim=-1)

mnist_net = MnistNet()
optimizer = optim.Adam(mnist_net.parameters(),lr= 0.001)
# criterion = nn.NLLLoss()
# criterion = nn.CrossEntropyLoss()
train_loss_list = []
train_count_list = []

def train(epoch):
    mode = True
    mnist_net.train(mode=mode)
    train_dataloader = get_dataloader(train=mode)
    print(len(train_dataloader.dataset))
    print(len(train_dataloader))
    for idx,(data,target) in enumerate(train_dataloader):
        optimizer.zero_grad()
        output = mnist_net(data)
        loss = F.nll_loss(output,target) #对数似然损失
        loss.backward()
        optimizer.step()
        if idx % 10 == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, idx * len(data), len(train_dataloader.dataset),
                       100. * idx / len(train_dataloader), loss.item()))

            train_loss_list.append(loss.item())
            train_count_list.append(idx*train_batch_size+(epoch-1)*len(train_dataloader))
            torch.save(mnist_net.state_dict(),"model/mnist_net.pkl")
            torch.save(optimizer.state_dict(), 'results/mnist_optimizer.pkl')


def test():
    test_loss = 0
    correct = 0
    mnist_net.eval()
    test_dataloader = get_dataloader(train=False)
    with torch.no_grad():
        for data, target in test_dataloader:
            output = mnist_net(data)
            test_loss += F.nll_loss(output, target, reduction='sum').item()
            pred = output.data.max(1, keepdim=True)[1] #获取最大值的位置,[batch_size,1]
            correct += pred.eq(target.data.view_as(pred)).sum()
    test_loss /= len(test_dataloader.dataset)
    print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(
        test_loss, correct, len(test_dataloader.dataset),
        100. * correct / len(test_dataloader.dataset)))


if __name__ == '__main__':

    test()  
    for i in range(5): #模型训练5轮
        train(i)
        test()

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jayden-leo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值