经典机器学习方法(3)—— 多层感知机

  • 参考:动手学深度学习
  • 注:本文是 jupyter notebook 文档转换而来,部分代码可能无法直接复制运行!

  • 前文介绍的线性回归和 softmax回归,在模型结构上都属于单层神经网络(只有一个输入层和一个输出层,输入层不计入层数),网络内部本质上只做了一次仿射变换

  • 为了提升模型的表示能力和性能,深度学习主要关注多层模型,并且在神经元中增加激活函数来引入非线性成分。本节以多层感知机(multilayer perceptron,MLP)为例,介绍多层神经网络的概念

    Note:多层感知机其实一个历史遗留词汇,它的前身是 单层感知机 或直接称 感知机模型,是 Frank Rosenblatt 在1957年所发明的一种二元线性分类器,感知机是第一个能根据每个类别的输入样本来学习权重的模型,引发了神经网络相关研究的第一次浪潮,由于当时没有激活函数的概念,这个纯线性模型无法学习异或函数,这次浪潮也由此衰退。目前我们正处于以“深度学习”为代表的第三波神经网络浪潮中。关于感知机的详细介绍可以参考 经典机器学习方法(4)—— 感知机

    Note:几十年的研究历程中,先后有 “控制论”、“联结主义”、“神经网络”直到现在的“深度学习”等多个术语指代神经网络相关的研究,现代术语“深度学习”超越了目前机器学习模型的神经科学观点。它诉诸于学习多层次组合这一更普遍的原理,这一原理也可以应用于那些并非受神经科学启发的机器学习框架

  • 理清概念

    1. 感知机 = 受生物神经元启发设计的一种二元线性分类器,是神经网络和支持向量机的前身
    2. 多层感知机MLP = 至少带一个隐藏层的,深度至少两层的神经网络
    3. BP神经网络 = 采用 BP 算法进行训练的神经网络

文章目录

  • 1. 多层感知机
    • 1.1 隐藏层
    • 1.2 激活函数
      • 1.2.1 激活函数的作用
      • 1.2.2 各种激活函数
    • 1.3 多层感知机
      • 1.3.1 理解多层感知机(全连接神经网络) —— 数据升维
      • 1.3.2 理解多层感知机(全连接神经网络) —— 提取特征
  • 2. 实现多层感知机
    • 2.1 数据准备
    • 2.2 模型设计
    • 2.3 模型训练
    • 2.4 完整代码
  • 3. 利用 Pytorch 简洁地实现线性回归
    • 3.1 模型设计
    • 3.2 模型训练
    • 3.3 完整代码

1. 多层感知机

1.1 隐藏层

  • 多层感知机在单层神经网络的输入层和输出层之间引入了一到多个隐藏层(hidden layer),其中的神经元称为隐藏单元(hidden unit),其输出称为隐藏层变量/隐藏变量。如下图例
    经典机器学习方法(3)—— 多层感知机_第1张图片

    这里输入输出和隐藏层的尺寸分别为 4、3、5,由于输入层不涉及计算,以上多层感知机的层数为2,注意多层感知机中的隐藏层和输出层都是全连接层

  • 具体来说,给定由 n n n d d d 维特征样本组成的 batch data X ∈ R n × d \pmb{X}\in\mathbb{R}^{n\times d} XXRn×d,假设多层感知机设输出个数为 q q q,只有一个含 h h h 个隐藏单元的隐藏层,记其输出的隐藏变量为 H ∈ R n × h \pmb{H}\in\mathbb{R}^{n\times h} HHRn×h,由于都是全连接层,设

    1. 隐藏层的权重和偏置参数为 W h ∈ R d × h , b h ∈ R 1 × h \pmb{W}_h\in \mathbb{R}^{d\times h}, \pmb{b}_h\in\mathbb{R}^{1\times h} WWhRd×h,bbhR1×h
    2. 输出层的权重和偏置参数为 W o ∈ R h × q , b o ∈ R 1 × q \pmb{W}_o\in \mathbb{R}^{h\times q}, \pmb{b}_o\in\mathbb{R}^{1\times q} WWoRh×q,bboR1×q

    则 batch data 的输出 O ∈ R n × q \pmb{O}\in\mathbb{R}^{n\times q} OORn×q 如下计算(其中加法用到广播机制)
    H n × h = X n × d W h d × h + b h 1 × h O n × q = H n × h W o h × q + b o 1 × q \pmb{H}_{n\times h} = \pmb{X}_{n\times d}\pmb{W_h}_{d\times h}+\pmb{b_h}_{1\times h}\\ \pmb{O}_{n\times q} = \pmb{H}_{n\times h}\pmb{W_o}_{h\times q}+\pmb{b_o}_{1\times q} HHn×h=XXn×dWhWhd×h+bhbh1×hOOn×q=HHn×hWoWoh×q+bobo1×q 注意到我们直接将隐藏层的输出作为输出层的输入。上述式子可以合并为
    O = ( X W h + b h ) W o + b o = X W h W o + b h W o + b o \pmb{O} = (\pmb{XW_h}+\pmb{b_h})\pmb{W_o}+\pmb{b_o} = \pmb{XW_hW_o}+\pmb{b_hW_o}+\pmb{b_o} OO=(XWhXWh+bhbh)WoWo+bobo=XWhWoXWhWo+bhWobhWo+bobo 从这个式子可以看出,如果仅仅引入隐藏层,不管多少层都依然等价于一个单层神经网络

1.2 激活函数

  • 1.1 节中问题的根源在于全连接层只是做仿射变换,多个仿射变换的叠加仍然是仿射变换。解决此问题的方法就是引入非线性变换,具体地说:对隐藏层的每个隐层变量按使用按元素运算的非线性函数进行变换,然后再作为下一层的输入。这里使用的非线性函数就称为 激活函数(activate function)
  • 激活函数可以在网络中加入非线性成分,具有一定的生物学基础
    1. 在生物神经元的输入和输出之间不是线性关系,而是在输入信号强度达到一定程度时发出一个脉冲信号,然后经历一段不应期才能再次发射。神经元的信号强度是一定的,但是发射频率会随输入强度非线性变化
    2. 对应到人工神经网络中,人工神经元的输出相当于生物神经元的放电频率,激活函数则相当于描述了输入强度和放电频率的关系

    Note:人工神经网络模型只是生物脑的极简模型,有一派研究人员专门从生物视角研究智能及其人工复现,对应的学科称为“认知神经科学”,那边有很多更贴近生物脑的神经网络模型

1.2.1 激活函数的作用

  • 更重要的是,引入激活函数后神经网络具有了“拟合任何函数”的能力,这非常重要,因为机器学习的本质,无论 CV 还是 NLP 甚至 RL,都是拟合函数(尽管这个函数很可能复杂到无法表示),在李宏毅老师的课程中对这一点有着非常清晰的讲解,请看以下图示
    1. 简单线性模型表示能力有限,无法拟合复杂的红色目标函数(这个问题称为 model bias
      经典机器学习方法(3)—— 多层感知机_第2张图片

    2. 任意分段函数都可以用一个常数偏置加上一组反Z字型函数(蓝色)拟合
      经典机器学习方法(3)—— 多层感知机_第3张图片
      注意任意曲线都可以用一大堆小直线拟合,因此使用这种方法就能拟合出任意函数。神经网络中常用的 sigmoid 和 tanh 激活函数就是这个蓝色反Z字函数的近似连续函数

    3. 这个反Z字本质也是一个分段函数,进一步拆开就得到了 ReLU 激活函数图形
      经典机器学习方法(3)—— 多层感知机_第4张图片

1.2.2 各种激活函数

  • 本节介绍常见的激活函数及其导数的图像
    1. ReLU (rectified linear unit 线性整流单元),它是一个很简单的线性变换,给定元素 x x x,该函数定义为
      ReLU ( x ) = max ⁡ ( x , 0 ) = { 0 , x ≤ 0 x , x > 0 \text{ReLU}(x) = \max(x,0) = \left\{ \begin{aligned} 0 && ,x\leq 0\\ x && ,x >0 \end{aligned} \right. ReLU(x)=max(x,0)={0x,x0,x>0 可见 ReLU 就是输入为负数时输出零,否则保持原样输出的一个分段函数
      经典机器学习方法(3)—— 多层感知机_第5张图片

    2. Sigmoid 函数可以将元素的值变换到 ( 0 , 1 ) (0,1) (0,1) 范围,该函数定义为
      sigmoid ( x ) = 1 1 + exp ⁡ ( − x ) \text{sigmoid}(x) = \frac{1}{1+\exp(-x)} sigmoid(x)=1+exp(x)1 sigmoid 函数在早期的神经网络中较为普遍,但它目前逐渐被更简单的ReLU函数取代。它的特点是输出值域在0到1之间,在 RNN 网络中利用此特性来控制信息在神经网络中的流动。其导数为
      sigmoid ′ ( x ) = sigmoid ( x ) ( 1 − sigmoid ( x ) ) \text{sigmoid}'(x) = \text{sigmoid}(x)(1-\text{sigmoid}(x)) sigmoid(x)=sigmoid(x)(1sigmoid(x)) 如下图所示,当输入为0时,sigmoid 函数的导数达到最大值0.25;当输入越偏离0时,sigmoid 函数的导数越接近0
      经典机器学习方法(3)—— 多层感知机_第6张图片

    3. tanh(双曲正切) 函数可以加将元素的值变换到 ( − 1 , 1 ) (-1,1) (1,1) 范围,该函数定义为
      tanh ( x ) = 1 − exp ⁡ ( − 2 x ) 1 + exp ⁡ ( − 2 x ) \text{tanh}(x) = \frac{1-\exp(-2x)}{1+\exp(-2x)} tanh(x)=1+exp(2x)1exp(2x) tanh 和 sigmoid 函数的形状很像,但 tanh 函数在坐标系的原点上对称。其导数为
      tanh ′ ( x ) = 1 − tanh 2 ( x ) \text{tanh}'(x) = 1-\text{tanh}^2(x) tanh(x)=1tanh2(x) 如下图所示,当输入为0时,tanh 函数的导数达到最大值1;当输入越偏离0时,sigmoid函数的导数越接近0
      经典机器学习方法(3)—— 多层感知机_第7张图片

  • 用以下代码片绘制图像
    import matplotlib.pyplot as plt
    import torch
    
    # 这两行代码解决 plt 中文显示的问题
    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.rcParams['axes.unicode_minus'] = False
    
    x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
    funcs = [x.relu(),x.sigmoid(),x.tanh()]
    names = ['ReLU', 'sigmoid', 'tanh']
    fig = plt.figure(figsize = (12,9))
    for i in range(3):
        p1 = fig.add_subplot(3,2,1+i*2,label='a{}'.format(1+i*2))
        p2 = fig.add_subplot(3,2,2+i*2,label='a{}'.format(2+i*2))
    
        name = names[i]
        y = funcs[i] 
        
        if x.grad != None:  # 清除上次计算的梯度
            x.grad.zero_()
        y.sum().backward()  # 这样会计算x中各元素梯度,存在x.grad中
        
        p1.plot(x.detach().numpy(), y.detach().numpy(), "r-",linewidth=2,c='r')
        p1.set_title(name)
        p1.grid(which='major',alpha=0.8)   
        p2.plot(x.detach().numpy(), x.grad, "r-",linewidth=2,c='r')
        p2.set_title(name+'梯度')
        p2.grid(which='major',alpha=0.8)   
    
    plt.tight_layout()  # 防止子图title和轴标签重叠
    

1.3 多层感知机

  • 多层感知机就是含有至少一个隐藏层的由全连接层组成的神经网络,且每个隐藏层的输出通过激活函数进行变换。其网络层数各隐藏层中隐藏单元个数都是超参数。以单隐藏层为例,输出可以如下计算:
    H = ϕ ( X W h + b h ) O = H W o + b o \begin{aligned} \pmb{H} &= \phi(\pmb{XW_h}+\pmb{b_h}) \\ \pmb{O} &= \pmb{HW_o}+\pmb{b_o} \end{aligned} HHOO=ϕ(XWhXWh+bhbh)=HWoHWo+bobo 其中 ϕ \phi ϕ 表示激活函数

    1. 对于分类问题,可以像 softmax 回归那样对输出 O \pmb{O} OO 进行 softmax 运算,然后优化交叉熵损失
    2. 对于回归问题,可以像线性回归那样将输出个数设为 1,然后优化平方损失函数

1.3.1 理解多层感知机(全连接神经网络) —— 数据升维

  • 注意到真正数据分类过程/回归过程发生在最后一个隐藏层和输出层之间。假设输入数据维度为 n n n,最后一个隐藏层的隐藏单元数为 m > n m>n m>n,可见这些隐藏层做的事其实是把数据从 n n n 维上升到 m m m
  • 数据升维是处理分类问题的常用技巧,比如下面例子经典机器学习方法(3)—— 多层感知机_第8张图片
    在二维空间中无法线性分开的两类样本,升维到三维空间中就变得线性可分了,神经网络特别善于构造复杂的高维空间

1.3.2 理解多层感知机(全连接神经网络) —— 提取特征

  • 有些时候,神经网络最后一个隐藏层的隐藏单元数目比输入维度更小(CV 相关的任务中常有这种情况)这时神经网络的作用就不是升维了,而是提取特征,比如在情感辨析任务中提取人物神态神态
    经典机器学习方法(3)—— 多层感知机_第9张图片

  • 每一个隐藏层都是对上一层的一次抽象/概括/整合,当隐藏层很多时,靠前的隐藏层会提取出更底层/基础的特征,靠后的隐藏层会得到更复杂一点的特征,并在这个过程中实现基础特征的复用,比如下面这时手写数字识别任务
    经典机器学习方法(3)—— 多层感知机_第10张图片
    靠前的层提取出各种笔画的特征,靠后的层将这些基础特征整合成圆圈、折线等复杂特征(整合过程会复用上一层的基础特征),最后输出层的每个神经元都代表一个具体数字的特征(所谓 “某个神经元代表一个特征”,是指输入含有整个特征时,这个神经元的输出会变大

    Note: 纯全连接网络不具有 “平移不变性”,注意构成数字 “8” 的上下两个圆形特征,对于全连接网络而言,即时这两个圆一模一样,再图像中的位置不同也会导致网络将其识别为不同的特征,而图片中物体位置移动是很常见的。卷积神经网络具有 “平移不变性”,因此在 CV 中特别常用

2. 实现多层感知机

  • 使用和 softmax 回归实验中相同的 Fashion-MNIST 数据集,利用多层感知机完成图像分类任务

2.1 数据准备

  • 定义好读取小批量数据的方法,构造数据读取迭代器
    import torch
    import torchvision
    import torchvision.transforms as transforms
    import numpy as np
    
    def load_data_fashion_mnist(batch_size, num_workers=0, root='./Datasets/FashionMNIST'):
        mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, download=True,transform=transforms.ToTensor())
        mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, download=True,transform=transforms.ToTensor())
    
        train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
        test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
    
        return train_iter, test_iter
    
    # 数据读取迭代器
    batch_size = 256
    train_iter, test_iter = load_data_fashion_mnist(batch_size, 4)
    

2.2 模型设计

  • 模型参数初始化:Fashion-MNIST 数据集中图像形状为 28×28,类别数为10。我们将图像拉平为 28 × 28 = 784 28\times 28=784 28×28=784 的向量作为输入,即

    1. 输入层单元数为 728
    2. 输出层单元数为 10,使用 softmax 运算将输出转为分类概率分布,构造交叉熵损失
    3. 隐藏层和输出层的所有权重参数都从正态分布 $ N(0,0.01^2)$ 采样初始化;所有偏置参数都初始化为 0
    4. 隐藏单元个数是超参数,这里设为 256
    num_inputs, num_outputs, num_hiddens = 784, 10, 256
    
    W1 = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_hiddens)), dtype=torch.float)
    b1 = torch.zeros(num_hiddens, dtype=torch.float)
    W2 = torch.tensor(np.random.normal(0, 0.01, (num_hiddens, num_outputs)), dtype=torch.float)
    b2 = torch.zeros(num_outputs, dtype=torch.float)
    
    params = [W1, b1, W2, b2]
    for param in params:
        param.requires_grad_(requires_grad=True)
    
  • 定义激活函数:这里使用 ReLU 激活函数,用 max 函数手动实现之

    def relu(X):
        return torch.max(input=X, other=torch.tensor(0.0))
    
  • 定义模型 O = ReLU ( X W h + b h ) W o + b o \pmb{O} = \text{ReLU}(\pmb{XW_h}+\pmb{b_h})\pmb{W_o}+\pmb{b_o} OO=ReLU(XWhXWh+bhbh)WoWo+bobo

    def net(X):
        X = X.view((-1, num_inputs))        # 原始图像拉平为一维向量输入
        H = relu(torch.matmul(X, W1) + b1)  # 隐藏层计算
        return torch.matmul(H, W2) + b2     # 输出层计算并返回
    
  • 定义损失函数:直接用 Pytorch 提供的 CrossEntropyLoss 方法实现交叉熵损失,它内部是用 logsoftmax + NLLLoss 实现的,可以避免数据溢出,保证数据稳定性

    loss = torch.nn.CrossEntropyLoss()
    

2.3 模型训练

  • 手动编写小批量随机梯度下降(sgd)方法来优化参数

    Note:PyTorch在计算 torch.nn.CrossEntropyLoss 时除过一次 batch_size,因此学习率设得比较大

    num_epochs = 5  # 训练轮数
    lr = 100.0          # 学习率
    batch_size = 256    # batch容量
    
    # 小批量随机梯度下降
    def sgd(params, lr, batch_size):
        for param in params:
            param.data -= lr * param.grad / batch_size # 注意这里更改 param 时用的param.data,这样不会影响梯度计算
    
    # 评估模型准确率
    def evaluate_accuracy(data_iter, net):
        acc_sum = 0.0  # 所有样本总准确率
        n =  0         # 总样本数量
        for X, y in data_iter:
            acc_sum += (net(X).argmax(dim=1) == y).float().sum().item() # 注意这里中间的 mean() 改成 sum() 了
            n += y.shape[0]
        return acc_sum / n
            
    def train(net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None):
        # 训练执行 num_epochs 轮
        for epoch in range(num_epochs):
            train_l_sum = 0.0    # 本 epoch 总损失
            train_acc_sum = 0.0  # 本 epoch 总准确率
            n = 0                # 本 epoch 总样本数
            
            # 逐小批次地遍历训练数据
            for X, y in train_iter:
                
                # 计算小批量损失
                y_hat = net(X)
                l = loss(y_hat, y).sum()  
    
                # 梯度清零
                if params is not None and params[0].grad is not None:
                    for param in params:
                        param.grad.data.zero_()
            
                # 小批量的损失对模型参数求梯度
                l.backward()
                
                # 做小批量随机梯度下降进行优化
                sgd(params, lr, batch_size)   # 手动实现优化算法
     
                # 记录训练数据
                train_l_sum += l.item()
                train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
                n += y.shape[0]
            
            # 训练完成一个 epoch 后,评估测试集上的准确率
            test_acc = evaluate_accuracy(test_iter, net)
            
            # 打印提示信息
            print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
                  % (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))
    
            
    # 进行训练
    train(net, train_iter, test_iter, loss, num_epochs, batch_size, params, lr)
    
    '''
    epoch 1, loss 0.0030, train acc 0.719, test acc 0.735
    epoch 2, loss 0.0019, train acc 0.826, test acc 0.807
    epoch 3, loss 0.0017, train acc 0.844, test acc 0.793
    epoch 4, loss 0.0015, train acc 0.855, test acc 0.842
    epoch 5, loss 0.0015, train acc 0.863, test acc 0.852
    '''
    

2.4 完整代码

  • 以下代码可以直接复制到 vscode 运行

    import torch
    import torchvision
    import torchvision.transforms as transforms
    import numpy as np
    
    # 数据集相关 --------------------------------------------------------------------------------------------------
    # 加载数据集
    def load_data_fashion_mnist(batch_size, num_workers=0, root='./Datasets/FashionMNIST'):
        mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, download=True,transform=transforms.ToTensor())
        mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, download=True,transform=transforms.ToTensor())
    
        train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
        test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
    
        return train_iter, test_iter
    
    # 模型相关 --------------------------------------------------------------------------------------------------------
    # 激活函数
    def relu(X):
        return torch.max(input=X, other=torch.tensor(0.0))
    
    # 定义模型
    def net(X):
        X = X.view((-1, num_inputs))        # 原始图像拉平为一维向量输入
        H = relu(torch.matmul(X, W1) + b1)  # 隐藏层计算
        return torch.matmul(H, W2) + b2     # 输出层计算并返回
    
    # 小批量随机梯度下降
    def sgd(params, lr, batch_size):
        for param in params:
            param.data -= lr * param.grad / batch_size # 注意这里更改 param 时用的param.data,这样不会影响梯度计算
    
    # 评估模型准确率
    def evaluate_accuracy(data_iter, net):
        acc_sum = 0.0  # 所有样本总准确率
        n =  0         # 总样本数量
        for X, y in data_iter:
            acc_sum += (net(X).argmax(dim=1) == y).float().sum().item() # 注意这里中间的 mean() 改成 sum() 了
            n += y.shape[0]
        return acc_sum / n
    
    # 进行训练
    def train(net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None):
        # 训练执行 num_epochs 轮
        for epoch in range(num_epochs):
            train_l_sum = 0.0    # 本 epoch 总损失
            train_acc_sum = 0.0  # 本 epoch 总准确率
            n = 0                # 本 epoch 总样本数
            
            # 逐小批次地遍历训练数据
            for X, y in train_iter:
                
                # 计算小批量损失
                y_hat = net(X)
                l = loss(y_hat, y).sum()  
    
                # 梯度清零
                if params is not None and params[0].grad is not None:
                    for param in params:
                        param.grad.data.zero_()
            
                # 小批量的损失对模型参数求梯度
                l.backward()
                
                # 做小批量随机梯度下降进行优化
                sgd(params, lr, batch_size)   # 手动实现优化算法
     
                # 记录训练数据
                train_l_sum += l.item()
                train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
                n += y.shape[0]
            
            # 训练完成一个 epoch 后,评估测试集上的准确率
            test_acc = evaluate_accuracy(test_iter, net)
            
            # 打印提示信息
            print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
                  % (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))
    
    
    if __name__ == '__main__':
        # 模型参数
        num_inputs, num_outputs, num_hiddens = 784, 10, 256
        W1 = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_hiddens)), dtype=torch.float)
        b1 = torch.zeros(num_hiddens, dtype=torch.float)
        W2 = torch.tensor(np.random.normal(0, 0.01, (num_hiddens, num_outputs)), dtype=torch.float)
        b2 = torch.zeros(num_outputs, dtype=torch.float)
        params = [W1, b1, W2, b2]
    
        for param in params:
            param.requires_grad_(requires_grad=True)    # 全部设为允许梯度追踪
    
        # 数据读取迭代器
        batch_size = 256
        train_iter, test_iter = load_data_fashion_mnist(batch_size, 4)
            
        # 交叉熵损失
        loss = torch.nn.CrossEntropyLoss()
    
        # 训练
        num_epochs = 5      # 训练轮数
        lr = 100.0          # 学习率
        batch_size = 256    # batch容量
    
        train(net, train_iter, test_iter, loss, num_epochs, batch_size, params, lr)
    

3. 利用 Pytorch 简洁地实现线性回归

  • pytorch 中提供了大量预定义的神经网络层,常用损失函数及优化器,可以大大简化 softmax 回归模型的实现
  • 数据准备、模型评价、使用模型进行预测等部分和第 2 节实现相同,本节不再重复

3.1 模型设计

  • 和之前 softmax 回归实验一样,按照深度学习习惯把数据拉平这件事定义成神经网络的一个层,然后用 Sequential 容器搭建网络模型

  • 相比之前的 softmax 回归网络,唯一的区别就是多加了一个全连接层作为隐藏层。它的隐藏单元个数为256,并使用ReLU函数作为激活函数

    class FlattenLayer(nn.Module):
        def __init__(self):
            super(FlattenLayer, self).__init__()
            
        def forward(self, x): # x shape: (batch, *, *, ...)
            return x.view(x.shape[0], -1)
    
    num_inputs, num_outputs, num_hiddens = 784, 10, 256
        
    net = nn.Sequential(
        FlattenLayer(),
        nn.Linear(num_inputs, num_hiddens),
        nn.ReLU(),
        nn.Linear(num_hiddens, num_outputs), 
    )
    
    for params in net.parameters():
        init.normal_(params, mean=0, std=0.01) # 用 nn.init 进行参数初始化
    

3.2 模型训练

  • 训练步骤和 softmax 回归几乎相同

    Note:这里使用了 PyTorch 的 SGD,里面没有除 batch_size,所以学习率不用设得太大了

    num_epochs = 5
    batch_size = 256
    loss = torch.nn.CrossEntropyLoss()
    
    optimizer = torch.optim.SGD(net.parameters(), lr=0.5)
    
    def train(net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None, optimizer=None):
        # 训练执行 num_epochs 轮
        for epoch in range(num_epochs):
            train_l_sum = 0.0    # 本 epoch 总损失
            train_acc_sum = 0.0  # 本 epoch 总准确率
            n = 0                # 本 epoch 总样本数
            
            # 逐小批次地遍历训练数据
            for X, y in train_iter:
                
                # 计算小批量损失
                y_hat = net(X)
                l = loss(y_hat, y).sum()  
    
                # 梯度清零
                optimizer.zero_grad()
    
                # 小批量的损失对模型参数求梯度
                l.backward()
                
                # 做小批量随机梯度下降进行优化
                optimizer.step()              
    
                # 记录训练数据
                train_l_sum += l.item()
                train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
                n += y.shape[0]
            
            # 训练完成一个 epoch 后,评估测试集上的准确率
            test_acc = evaluate_accuracy(test_iter, net)
            
            # 打印提示信息
            print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
                  % (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))
    
    train(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)
    
    '''
    epoch 1, loss 0.0033, train acc 0.690, test acc 0.754
    epoch 2, loss 0.0019, train acc 0.824, test acc 0.809
    epoch 3, loss 0.0016, train acc 0.845, test acc 0.803
    epoch 4, loss 0.0015, train acc 0.857, test acc 0.804
    epoch 5, loss 0.0014, train acc 0.865, test acc 0.823
    '''
    

3.3 完整代码

  • 以下代码可以直接复制到 vscode 运行

    import torch
    import torchvision
    import torchvision.transforms as transforms
    from torch import nn
    from torch.nn import init
    
    # 数据集相关 --------------------------------------------------------------------------------------------------
    # 加载数据集
    def load_data_fashion_mnist(batch_size, num_workers=0, root='./Datasets/FashionMNIST'):
        mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, download=True,transform=transforms.ToTensor())
        mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, download=True,transform=transforms.ToTensor())
    
        train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
        test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
    
        return train_iter, test_iter
    
    # 模型相关 --------------------------------------------------------------------------------------------------------
    class FlattenLayer(nn.Module):
        def __init__(self):
            super(FlattenLayer, self).__init__()
            
        def forward(self, x): # x shape: (batch, *, *, ...)
            return x.view(x.shape[0], -1)
    
    # 评估模型准确率
    def evaluate_accuracy(data_iter, net):
        acc_sum = 0.0  # 所有样本总准确率
        n =  0         # 总样本数量
        for X, y in data_iter:
            acc_sum += (net(X).argmax(dim=1) == y).float().sum().item() # 注意这里中间的 mean() 改成 sum() 了
            n += y.shape[0]
        return acc_sum / n
    
    # 进行训练
    def train(net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None, optimizer=None):
        # 训练执行 num_epochs 轮
        for epoch in range(num_epochs):
            train_l_sum = 0.0    # 本 epoch 总损失
            train_acc_sum = 0.0  # 本 epoch 总准确率
            n = 0                # 本 epoch 总样本数
            
            # 逐小批次地遍历训练数据
            for X, y in train_iter:
                
                # 计算小批量损失
                y_hat = net(X)
                l = loss(y_hat, y).sum()  
    
                # 梯度清零
                optimizer.zero_grad()
    
                # 小批量的损失对模型参数求梯度
                l.backward()
                
                # 做小批量随机梯度下降进行优化
                optimizer.step()              
    
                # 记录训练数据
                train_l_sum += l.item()
                train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
                n += y.shape[0]
            
            # 训练完成一个 epoch 后,评估测试集上的准确率
            test_acc = evaluate_accuracy(test_iter, net)
            
            # 打印提示信息
            print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
                  % (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))
    
    
    if __name__ == '__main__':
        # 模型参数
        num_inputs, num_outputs, num_hiddens = 784, 10, 256
        
        # 数据读取迭代器
        batch_size = 256
        train_iter, test_iter = load_data_fashion_mnist(batch_size, 4)
    
        # 定义模型网络结构
        net = nn.Sequential(
        FlattenLayer(),
        nn.Linear(num_inputs, num_hiddens),
        nn.ReLU(),
        nn.Linear(num_hiddens, num_outputs), 
        )
    
        # 初始化模型参数
        for params in net.parameters():
            init.normal_(params, mean=0, std=0.01)
    
        # 交叉熵损失 & 优化器
        loss = torch.nn.CrossEntropyLoss()
        optimizer = torch.optim.SGD(net.parameters(), lr=0.5)
    
        # 训练
        num_epochs = 5      # 训练轮数
        batch_size = 256    # batch容量
    
        train(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)
    

你可能感兴趣的:(#,实践,#,监督学习,#,PyTorch,深度学习,神经网络,多层感知机,动手学深度学习)