前言:神经网络的搭建本身是一个较为复杂的过程,但是现在有非常多的、非常人性化的开源框架提供给我们使用,但是即便如此,网络的搭建也是有多种方法可以选择,本文以pytorch为例子加以说明。

神经网络的基本流程可以分为两大步骤,即

网络结构搭建+参数的梯度更新(后者又包括  “前向传播+计算参数的梯度+梯度更新”)

这其实也是后面pytorch搭建神经网络的一个基本思路

 原始搭建——使用numpy实现

# -*- coding: utf-8 -*-
import numpy as np
 
# N是训练的batch size; D_in 是input输入数据的维度;
# H是隐藏层的节点数; D_out 输出的维度,即输出节点数.
N, D_in, H, D_out = 64, 1000, 100, 10
 
# 创建输入、输出数据
x = np.random.randn(N, D_in)  #(64,1000)
y = np.random.randn(N, D_out) #(64,10)可以看成是一个10分类问题
 
# 权值初始化
w1 = np.random.randn(D_in, H)  #(1000,100),即输入层到隐藏层的权重
w2 = np.random.randn(H, D_out) #(100,10),即隐藏层到输出层的权重
 
learning_rate = 1e-6   #学习率
 
for t in range(500):
    # 第一步:数据的前向传播,计算预测值p_pred
    h = x.dot(w1)
    h_relu = np.maximum(h, 0)
    y_pred = h_relu.dot(w2)
 
    # 第二步:计算计算预测值p_pred与真实值的误差
    loss = np.square(y_pred - y).sum()
    print(t, loss)
 
    # 第三步:反向传播误差,更新两个权值矩阵
    grad_y_pred = 2.0 * (y_pred - y)     #注意:这里的导函数也是自己求的,因为这个地方是很简答的函数表达式
    grad_w2 = h_relu.T.dot(grad_y_pred)
    grad_h_relu = grad_y_pred.dot(w2.T)
    grad_h = grad_h_relu.copy()
    grad_h[h < 0] = 0
    grad_w1 = x.T.dot(grad_h)
 
    # 更新参数
    w1 -= learning_rate * grad_w1
    w2 -= learning_rate * grad_w2

缺点:

(1)没办法搭建复杂的网络结构(网络的结构搭建太底层);

(2)梯度需要自己求导函数,如果函数太复杂,网络太深就很难求了;

(3)没有办法使用GPU加速


使用torch的Tensor原始实现

import torch
 
dtype = torch.float
device = torch.device("cpu")
# device = torch.device("cuda:0") # 这里使用CPU,但实际上可以使用GPU
 
# N是训练的batch size; D_in 是input输入数据的维度;
# H是隐藏层的节点数; D_out 输出的维度,即输出节点数.
N, D_in, H, D_out = 64, 1000, 100, 10
 
# 创建输入、输出数据
x = torch.randn(N, D_in, device=device, dtype=dtype)  #(64,1000)
y = torch.randn(N, D_out, device=device, dtype=dtype) #(64,10)可以看成是一个10分类问题
 
# 权值初始化
w1 = torch.randn(D_in, H, device=device, dtype=dtype) #(1000,100),即输入层到隐藏层的权重
w2 = torch.randn(H, D_out, device=device, dtype=dtype)#(100,10),即隐藏层到输出层的权重
 
learning_rate = 1e-6
 
for t in range(500):
    
    # 第一步:数据的前向传播,计算预测值p_pred
    h = x.mm(w1)
    h_relu = h.clamp(min=0)
    y_pred = h_relu.mm(w2)
 
    # 第二步:计算计算预测值p_pred与真实值的误差
    loss = (y_pred - y).pow(2).sum().item()
    print(t, loss)
 
    # 第三步:反向传播误差,更新两个权值矩阵
    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

其中:

x = x.mm(self.w) #x与w相乘

注:x必须是tensor,才可以应用该方法。

torch.clamp(input, min, max, out=None) → Tensor

将输入input张量每个元素的夹紧到区间 [min,max][min,max],并返回结果到一个新张量。

操作定义如下:

| min, if x_i < min

    y_i = | x_i, if min <= x_i <= max

          | max, if x_i > max

如果输入是FloatTensor or DoubleTensor类型,则参数min max 必须为实数,否则须为整数。【译注:似乎并非如此,无关输入类型,min, max取整数、实数皆可。】

参数:

input (Tensor) – 输入张量
min (Number) – 限制范围下限
max (Number) – 限制范围上限
out (Tensor, optional) – 输出张量

  • .t()表示转置

缺点:

(1)没办法搭建复杂的网络结构(网络的结构搭建太底层);

(2)梯度需要自己求导函数,如果函数太复杂,网络太深就很难求了;

解决了GPU计算问题、少了一个缺点。


torch的自动求导autograd

import torch
 
dtype = torch.float
device = torch.device("cpu")
# device = torch.device("cuda:0") # 这里使用CPU,但实际上可以使用GPU
 
# N是训练的batch size; D_in 是input输入数据的维度;
# H是隐藏层的节点数; D_out 输出的维度,即输出节点数.
N, D_in, H, D_out = 64, 1000, 100, 10
 
# 创建输入、输出数据
x = torch.randn(N, D_in, device=device, dtype=dtype,requires_grad=True)  #(64,1000)
y = torch.randn(N, D_out, device=device, dtype=dtype,requires_grad=True) #(64,10)可以看成是一个10分类问题
 
# 权值初始化
w1 = torch.randn(D_in, H, device=device, dtype=dtype) #(1000,100),即输入层到隐藏层的权重
w2 = torch.randn(H, D_out, device=device, dtype=dtype)#(100,10),即隐藏层到输出层的权重
 
learning_rate = 1e-6
 
for t in range(500):
  
    # 第一步:数据的前向传播,计算预测值p_pred
    y_pred = x.mm(w1).clamp(min=0).mm(w2)
 
    # 第二步:计算计算预测值p_pred与真实值的误差
    loss = (y_pred - y).pow(2).sum()
    print(t, loss.item())
 
    # 第三步:反向传播误差,更新两个权值矩阵,这就是关键了,不再需要自己写出导函数,求导是自动完成的
    loss.backward()  #一步到位、自动求导
 
    # 参数梯度更新
    with torch.no_grad():
        w1 -= learning_rate * w1.grad  #grad属性获取梯度,其实这个地方就是相当于是梯度下降法,优化的过程可以自己定义,因为这里参数很少
        w2 -= learning_rate * w2.grad
 
        #求完一次之后将梯度归零
        w1.grad.zero_()
        w2.grad.zero_()

缺点:

(1)没办法搭建复杂的网络结构(网络的结构搭建太底层);

解决了GPU计算问题、而且梯度导数也不用自己求了,少了两个缺点。那现在就只剩一个问题没解决了,那就是怎么快速搭建更复杂、更深层的网络结构。

(2)上面更新参数w1和w2的过程其实就是一个优化过程,这里是用的就是简单的梯度下降法,这样做有一个很大的缺点,那就是这里只有两个参数需要优化,所以可以自己写,但是现在的网络有很多的参数需要优化,都自己写的话实在是太麻烦了。于是就有了后面的方法:


使用pytorch.nn模块

可以将它理解为一个较高层次的API封装

# -*- coding: utf-8 -*-
import torch
 
# N是训练的batch size; D_in 是input输入数据的维度;
# H是隐藏层的节点数; D_out 输出的维度,即输出节点数.
N, D_in, H, D_out = 64, 1000, 100, 10
 
# 创建输入、输出数据
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
 
#模型搭建:这是与前面关键的区别,不再需要自己手动进行矩阵相乘,而是这种一步到位的方法
model = torch.nn.Sequential(
    torch.nn.Linear(D_in, H),
    torch.nn.ReLU(),
    torch.nn.Linear(H, D_out),
)
 
#定义损失函数
loss_fn = torch.nn.MSELoss(reduction='sum')
 
learning_rate = 1e-4
 
for t in range(500):
    
    # 第一步:数据的前向传播,计算预测值p_pred
    y_pred = model(x)
 
    # 第二步:计算计算预测值p_pred与真实值的误差
    loss = loss_fn(y_pred, y)
    print(t, loss.item())
 
    # 在反向传播之前,将模型的梯度归零
    model.zero_grad()
 
    # 第三步:反向传播误差,更新两个权值矩阵,这就是关键了,不再需要自己写出导函数,求导是自动完成的
    loss.backward()
 
    #更新参数的梯度
    with torch.no_grad():
        for param in model.parameters():
            param -= learning_rate * param.grad #这其实就是梯度下降法,优化参数,通过循环自动实现,不要再一个一个写了,相较于上面的参数更新方法简单了很多。但是还不够

均方损失函数

pytorch模型训练教程 pytorch模型搭建_pytorch模型训练教程

有三个可选参数:reduce、size_average、reduction

很多的 loss 函数都有 size_average 和 reduce 两个布尔类型的参数。因为一般损失函数都是直接计算 batch 的数据,因此返回的 loss 结果都是维度为 (batch_size, ) 的向量。

(1)如果 reduce = False,那么 size_average 参数失效,直接返回向量形式的 loss
(2)如果 reduce = True,那么 loss 返回的是标量

a)如果 size_average = True,返回 loss.mean(),即loss的平均值b)如果 size_average = False,返回 loss.sum(),loss的和

注意:默认情况下, reduce = True,size_average = True

(3) reduction = ‘none’,直接返回向量形式的 loss
(4) reduction = ‘sum’,返回loss之和,是标量
(5) reduction = ''elementwise_mean",返回loss的平均值,是向量
(6) reduction = ''mean",返回loss的平均值,是标量

总结:

到这一步,基本解决了前面的三个致命问题,即解决了GPU计算问题、而且梯度导数也不用自己求了,并且可以搭建复杂的网络,不需要自己进行一个一个的矩阵相乘,少了三个缺点。

使用这里的层次,一些简单的网络类型就没什么问题了,当然我们还可以进一步优化,因为上面我们对loss进行自动求导之后,还需要通过一个循环来对模型的各个参数逐个进行参数的更新,所以下面提供了两个层次方面的优化措施:

使用torch.optim来进一步简化训练过程

进一步省略手动的参数更新,更加一步到位

# -*- coding: utf-8 -*-
import torch
 
# N是训练的batch size; D_in 是input输入数据的维度;
# H是隐藏层的节点数; D_out 输出的维度,即输出节点数.
N, D_in, H, D_out = 64, 1000, 100, 10
 
# 创建输入、输出数据
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
 
#模型搭建:这是与前面关键的区别,不再需要自己手动进行矩阵相乘,而是这种一步到位的方法
model = torch.nn.Sequential(
    torch.nn.Linear(D_in, H),
    torch.nn.ReLU(),
    torch.nn.Linear(H, D_out),
)
 
#定义损失函数
loss_fn = torch.nn.MSELoss(reduction='sum')
 
learning_rate = 1e-4
#构造一个optimizer对象
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
 
for t in range(500):
    
    # 第一步:数据的前向传播,计算预测值p_pred
    y_pred = model(x)
 
    # 第二步:计算计算预测值p_pred与真实值的误差
    loss = loss_fn(y_pred, y)
    print(t, loss.item())
 
    # 在反向传播之前,将模型的梯度归零,这
    optimizer.zero_grad()
 
    # 第三步:反向传播误差
    loss.backward()
 
    # 直接通过梯度一步到位,更新完整个网络的训练参数,一句话优化所有的参数,是不是很牛逼
    optimizer.step()

torch.optim是一个实现了多种优化算法的包,大多数通用的方法都已支持,提供了丰富的接口调用,未来更多精炼的优化算法也将整合进来。
为了使用torch.optim,需先构造一个优化器对象Optimizer,用来保存当前的状态,并能够根据计算得到的梯度来更新参数。
要构建一个优化器optimizer,你必须给它一个可进行迭代优化的包含了所有参数(所有的参数必须是变量s)的列表。 然后,您可以指定程序优化特定的选项,例如学习速率,权重衰减等。

是变量s)的列表。 然后,您可以指定程序优化特定的选项,例如学习速率,权重衰减等。

optimizer = optim.SGD(model.parameters(), lr = 0.01, momentum=0.9)
optimizer = optim.Adam([var1, var2], lr = 0.0001)
self.optimizer_D_B = torch.optim.Adam(self.netD_B.parameters(), lr=opt.lr, betas=(opt.beta1, 0.999))

Optimizer还支持指定每个参数选项。 只需传递一个可迭代的dict来替换先前可迭代的Variable。dict中的每一项都可以定义为一个单独的参数组,参数组用一个params键来包含属于它的参数列表。其他键应该与优化器接受的关键字参数相匹配,才能用作此组的优化选项。

optim.SGD([ {'params': model.base.parameters()}, {'params': model.classifier.parameters(), 'lr': 1e-3} ], lr=1e-2, momentum=0.9)

如上,model.base.parameters()将使用1e-2的学习率,model.classifier.parameters()将使用1e-3的学习率。0.9的momentum作用于所有的parameters。

优化步骤:
所有的优化器Optimizer都实现了step()方法来对所有的参数进行更新,它有两种调用方法:

optimizer.step()

这是大多数优化器都支持的简化版本,使用如下的backward()方法来计算梯度的时候会调用它。

for input, target in dataset:
    optimizer.zero_grad()
    output = model(input)
    loss = loss_fn(output, target)
    loss.backward()
    optimizer.step()optimizer.step(closure)

一些优化算法,如共轭梯度和LBFGS需要重新评估目标函数多次,所以你必须传递一个closure以重新计算模型。 closure必须清除梯度,计算并返回损失

for input, target in dataset:
    def closure():
        optimizer.zero_grad()
        output = model(input)
        loss = loss_fn(output, target)
        loss.backward()
        return loss
    optimizer.step(closure)

Adam算法:

adam算法来源:Adam: A Method for Stochastic Optimization

Adam(Adaptive Moment Estimation)本质上是带有动量项的RMSprop,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。它的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳。其公式如下:

pytorch模型训练教程 pytorch模型搭建_数据_02

其中,前两个公式分别是对梯度的一阶矩估计和二阶矩估计,可以看作是对期望E|gt|,E|gt^2|的估计;
公式3,4是对一阶二阶矩估计的校正,这样可以近似为对期望的无偏估计。可以看出,直接对梯度的矩估计对内存没有额外的要求,而且可以根据梯度进行动态调整。最后一项前面部分是对学习率n形成的一个动态约束,而且有明确的范围

class torch.optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0)

参数:

params(iterable):可用于迭代优化的参数或者定义参数组的dicts。 lr (float, optional) :学习率(默认: 1e-3) betas (Tuple[float, float], optional):用于计算梯度的平均和平方的系数(默认: (0.9, 0.999)) eps (float, optional):为了提高数值稳定性而添加到分母的一个项(默认: 1e-8) weight_decay (float, optional):权重衰减(如L2惩罚)(默认: 0)step(closure=None)函数:执行单一的优化步骤 closure (callable, optional):用于重新评估模型并返回损失的一个闭包

torch.optim.adam源码:

import math
from .optimizer import Optimizer

class Adam(Optimizer):
    def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8,weight_decay=0):
        defaults = dict(lr=lr, betas=betas, eps=eps,weight_decay=weight_decay)
        super(Adam, self).__init__(params, defaults)

    def step(self, closure=None):
        loss = None
        if closure is not None:
            loss = closure()

        for group in self.param_groups:
            for p in group['params']:
                if p.grad is None:
                    continue
                grad = p.grad.data
                state = self.state[p]

                # State initialization
                if len(state) == 0:
                    state['step'] = 0
                    # Exponential moving average of gradient values
                    state['exp_avg'] = grad.new().resize_as_(grad).zero_()
                    # Exponential moving average of squared gradient values
                    state['exp_avg_sq'] = grad.new().resize_as_(grad).zero_()

                exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq']
                beta1, beta2 = group['betas']

                state['step'] += 1

                if group['weight_decay'] != 0:
                    grad = grad.add(group['weight_decay'], p.data)

                # Decay the first and second moment running average coefficient
                exp_avg.mul_(beta1).add_(1 - beta1, grad)
                exp_avg_sq.mul_(beta2).addcmul_(1 - beta2, grad, grad)

                denom = exp_avg_sq.sqrt().add_(group['eps'])

                bias_correction1 = 1 - beta1 ** state['step']
                bias_correction2 = 1 - beta2 ** state['step']
                step_size = group['lr'] * math.sqrt(bias_correction2) / bias_correction1

                p.data.addcdiv_(-step_size, exp_avg, denom)

        return loss

Adam的特点有:
1、结合了Adagrad善于处理稀疏梯度和RMSprop善于处理非平稳目标的优点;
2、对内存需求较小;
3、为不同的参数计算不同的自适应学习率;
4、也适用于大多非凸优化-适用于大数据集和高维空间。

这是不是更加简单了?

现在来总结一下使用pytorch搭建神经网路的一般步骤:

  • 第一步:搭建网络的结构,得到一个model。网络的结构可以是上面这种最简单的序贯模型,当然还可以是多输入-单输出模型、单输入-多输出模型、多输入-多输出模型、跨层连接的模型等,我们好可以自己定义模型,后面再讲。
  • 第二步:定义损失函数。loss = torch.nn.MSELoss(reduction='sum')
  • 第三步:定义优化方式。构造一个optimizer对象  optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

上面是模型以及模型相关的配置三步走,下面是训练的五步走

  • 第一步:计算y_pred;
  • 第二步:根据损失函数计算loss
  • 第三步:梯度归零,optimizer.zero_grad()
  • 第四步:反向传播误差 loss.backward()
  • 更新参数,使用step()optimizer.step()

大佬观点:

深度学习框架最大的地方在于两个点,正是这两个点大大简化了我们自己的实现思路,当然这两个点不在于一层一层的搭建,个人认为最重要的在于以下两个:

(1)第一,自动求导。设想一下,如果一个如此复杂的“高维度”、“非线性”的函数,需要自己写出求导公式,在进行矩阵运算,这一项就很不现实了。

(2)第二,参数的优化。即所谓的optimizer的作用,它是每一个参数的更新规则,由于模型参数众多,不可能一个一个来更新,而且没一个更新的原理是重复的,深度学习框架正好提供了一步到位的方法,不管是tensorflow还是pytorch。