本文主要参考Pytorch的官方教程编写,主要介绍神经网络的训练过程以及pytorch训练神经网络过程中使用的模块以及它们的作用。

一、典型的神经网络训练过程

1、定义一个包含可训练参数的神经网络;
在定义神经网络时只需要定义__init__方法和forward函数,backward函数(用于计算梯度)会用autograd来自动定义。其中,可训练参数一般指在训练中可以改变的参数,下面展示pytorch查看可训练参数的代码:

for name, param in model.named_parameters():
    if param.requires_grad:
        print(name)

2、迭代整个输入
3、通过神经网络处理输入
在这两个过程中,生成一个输入张量(input),并将其输入到定义的神经网络,经神经网络处理后可得到输出(output)

4、计算损失(loss)
损失指的是输出(output)与目标(target)之间的差异,用来衡量这个差异的函数就是损失函数,一个损失函数需要一对输入:模型输出和目标,然后计算一个值来评估输出距离目标有多远。我们最常用的一种就是均方根损失函数(MSELoss)

5.反向传播梯度到神经网络的参数

(1)数学上的理解

反向传播计算梯度在数学上我们可以理解为使用链式法则做损失函数关于网络参数的微分,从而得到该参数处的梯度值。

(2)pytorch中的实际操作

如果我们跟随损失到反向传播路径,可以使用它的 .grad_fn 属性,当我们调用 loss.backward(),整个图都会微分,而且所有的在图中的requires_grad=True的张量将会让他们的 grad 张量累计梯度。

为了实现反向传播损失,我们所有需要做的事情仅仅是调用loss.backward()。但是要注意的是在计算梯度之前我们需要先清空现存的梯度。

6.更新网络的参数
根据反向传播得到的梯度,我们可以更新网络的参数(一般更新的是权重)。
最简单和典型的一个更新方法就是梯度下降:
weight(new) = weight(orign) - learning_rate*gradient
式中:weight(new)代表的是更新后的权重值,weight(orign)代表的是原始权重,learning_rate代表的是学习率,gradient代表的是此权重值处的梯度。

二、使用pytorch训练神经网络示例

在这里会介绍使用tensor随机数据训练一个两层的的网络,并通过一步步对其简化来展示pytorch中一些模块的作用。
1、首先手动实现神经网络
为防止长串代码影响大家的阅读心情,这里将代码分成几个部分:
(1)导入torch并设置数据类型、运行设备和网络规模

import torch
dtype = torch.float
device = torch.device("cuda:0")
#N是批量大小; D_in是输入维度;
#H是隐藏的维度; D_out是输出维度。
N, D_in, H, D_out = 64, 1000, 100, 10

(2)创建随机输入、输出数据并随机初始化权重,设置学习率

#创建随机输入和输出数据
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)
# 随机初始化权重
w1 = torch.randn(D_in, H, device=device, dtype=dtype)
w2 = torch.randn(H, D_out, device=device, dtype=dtype)
#设置学习率
learning_rate = 1e-6

(3)设置前向传播,计算预测的y值

#计算预测的y值
    h = x.mm(w1)               #将输入传递到隐藏层
    h_relu = h.clamp(min = 0)  #实现隐藏层的relu函数(负值置0)
    y_pred = h_relu.mm(w2)     #将输入从隐藏层传至输出层求取预测的y
    #也可写为:
    #y_pred = x.mm(w1).clamp(min=0).mm(w2)

(4)计算和打印损失

#计算和打印损失
    loss = (y_pred - y).pow(2).sum().item()
    #sum()函数用于求和
    #item用于提取高精度的loss值
    print(t, loss)

(5)反向传播梯度并更新权值

# Backprop计算w1和w2相对于损耗的梯度
    grad_y_pred = 2.0 * (y_pred - y)
    grad_w2 = h_relu.t().mm(grad_y_pred)
    grad_h_relu = grad_y_pred.mm(w2.t())
    grad_h = grad_h_relu.clone()
    grad_h[h < 0] = 0
    grad_w1 = x.t().mm(grad_h)
    # 使用梯度下降更新权重
    w1 -= learning_rate * grad_w1
    w2 -= learning_rate * grad_w2

至此,我们纯手动的实现了一个两层的神经网络。
2、使用自动求导
(5)部分梯度的反向传播看起来十分的繁琐,在pytorch中我们可以将其替换为自动求导。如果我们想计算某些的tensor的梯度,我们只需要在建立这个tensor时加入这么一句: requires_grad=True,也就是改用如下方式初始化权重:

w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True)

这样,在w1和w2上的任何pytorch的操作都将构造一个计算图,从而允许我们稍后在图中执行反向传播。但是在训练神经网络时,我们通常不希望通过权重更新步骤进行反向传播,我们可以使用 torch.no_grad() 来防止构造计算图。由此,(5)部分的代码可改写为:

loss.backward()
    with torch.no_grad():
        w1 -= learning_rate * w1.grad
        w2 -= learning_rate * w2.grad
        #反向传播后手动将梯度设置为零
        w1.grad.zero_()
        w2.grad.zero_()

最后展示一下最终的神经网络代码:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

import torch
dtype = torch.float
device = torch.device("cuda:0")

# N是批量大小; D_in是输入维度;
# H是隐藏的维度; D_out是输出维度。
N, D_in, H, D_out = 64, 1000, 100, 10

#创建随机Tensors以保持输入和输出
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)

# 权重创建随机Tensors。
# 设置requires_grad = True表示我们想要计算渐变
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True)

learning_rate = 1e-6
for t in range(500):
    
    y_pred = x.mm(w1).clamp(min=0).mm(w2)

    loss = (y_pred - y).pow(2).sum()
    print(t, loss.item())

    loss.backward()

    with torch.no_grad():
        w1 -= learning_rate * w1.grad
        w2 -= learning_rate * w2.grad

        # 反向传播后手动将梯度设置为零
        w1.grad.zero_()
        w2.grad.zero_()

三、补充

1、将计算安排成层
计算图和autograd是十分强大的工具,可以定义复杂的操作并自动求导;然而对于大规模的网络,autograd太过于底层。 在构建神经网络时,我们经常考虑将计算安排成层,其中一些具有可学习的参数,它们将在学习过程中进行优化。
pytorch的nn包(torch.nn)可实现此过程:

model = torch.nn.Sequential(
torch.nn.Linear(D_in, H),
torch.nn.ReLU(),
torch.nn.Linear(H, D_out),
)
# nn.Sequential是包含其他模块的模块,并按顺序应用这些模块来产生其输出。
# 每个线性模块使用线性函数从输入计算输出,并保存其内部的权重和偏差张量。
# 在构造模型之后,我们使用.to()方法将其移动到所需的设备。

2、常见损失函数
在范例中我们定义了均方根损失函数,实际搭建网络时我们可以调用torch.nn中定义的损失函数,如torch.nn.MSELoss、torch.nn.L1Loss、torch.nn.KLDivLoss等。

3、多样的优化方法
我们在范例中使用梯度下降手动更新模型的权重,实际搭建网络时我们可以调用torch.optim中实现的AdaGrad、RMSProp、Adam等优化器。