“自然搞懂”深度学习(基于Pytorch架构)——010203

Forester's Notebook

🤸‍♂️生活座右铭:勤而拂拭,莫染尘埃。 📚学习座右铭:一切烦恼来源于定义不清。 🙌留言:有任何问题欢迎交流学习,直接私信即可,一定会回!👌


学习中很重要的一点------隐含定义(或者说默认规则),一个人理解知识的链路基于严密逻辑,这个链路是否被打通并且正确决定了他是否真正理解了所学知识,但教授知识的人真正做到这点是很难的,由于他自身对知识已了然于胸往往无法感同身受,从而容易忽略链路中潜在隐含的一些定义规则,导致学习者学完模棱两可,在偶然间学生了解到链路中的堵点或纠正了错点,我们便常说这个学生"开窍了"。

因此,我会在讲解中穿插加入Think-Help,同时希望读者指出错误并提出宝贵意见,万分感谢!


"自然搞懂"深度学习(基于Pytorch架构)

++声明:本文以研0初学者视角撰写,力求通俗易懂,补齐坑点:将"定义不清"处用Think-Help讲解透彻,不易理解处用实例替换理论,同时句句都是经过认真思考后写下的,不说废话,保证笔记质量。++

++该笔记适合哪类人群?如何发挥作用?++

  1. 学习过高数、线代及概率论基础知识(有印象即可)。
  2. 学习过机器学习基本模型及理论。
  3. 应用过深度学习算法但并不了解其理论。
  4. 学习过深度学习基本理论但模棱两可,或些许遗忘。

该笔记的优势已声明,请根据自身情况加以利用:可以跟着笔记走,亦可以结合参考视频,相信该笔记一定能在某些关键节点让您"恍然大悟",避免不必要的坑点,浪费时间。

++目前参考视频:++

章节 参考视频
第Ⅰ章 初入茅庐 飞天闪客:一小时从函数到Transformer!一路大白话彻底理解AI原理
第Ⅱ章 小试牛刀 刘二大人
第Ⅲ章 渐入佳境 刘二大人

最后,该笔记凝聚作者大量时间精力,费了很多心血制作而成,如果给您带来一点帮助,欢迎关注/star哦,给作者持续创作的动力!
同时,如笔记中存在错误,欢迎指出;如您想要学习交流,欢迎私信!

精美原版PDF于Github自取!(烦请star哦)
https://github.com/Xueyouing/Study-Naturally


文章目录

  • [Forester's Notebook](#Forester's Notebook)
    • "自然搞懂"深度学习(基于Pytorch架构)
      • [第Ⅰ章 初入茅庐](#第Ⅰ章 初入茅庐)
        • [一、 数学的奥妙](#一、 数学的奥妙)
        • 二、梯度下降
        • [2.1 自然引出](#2.1 自然引出)
          • [2.2 梯度下降三种方法](#2.2 梯度下降三种方法)
            • [2.2.1 All(Gradient Descent)](#2.2.1 All(Gradient Descent))
            • [2.2.2 One-By-One(Stochastic Gradient Descent)](#2.2.2 One-By-One(Stochastic Gradient Descent))
            • [2.2.3 Mini-Batch(小批量梯度下降法)](#2.2.3 Mini-Batch(小批量梯度下降法))
        • 三、前向传播与反向传播
          • [3.1 定义](#3.1 定义)
          • [3.2 实例](#3.2 实例)
            • [3.2.1 单样本点实例](#3.2.1 单样本点实例)
            • [3.2.2 多样本点实例](#3.2.2 多样本点实例)
            • [3.3 Think-Help](#3.3 Think-Help)
      • [第Ⅱ章 小试牛刀](#第Ⅱ章 小试牛刀)
        • 一、线性回归
          • [1.1 Prepare Dataset](#1.1 Prepare Dataset)
          • [1.2 Design model using class](#1.2 Design model using class)
          • [1.3 Construct loss and optimizer](#1.3 Construct loss and optimizer)
          • [1.4 Train cycle](#1.4 Train cycle)
        • 二、逻辑斯蒂回归
          • [2.1 问题讲解](#2.1 问题讲解)
          • [2.2 代码实践](#2.2 代码实践)
        • 三、DataLoader完整流程
          • [3.1 python组件间关系](#3.1 python组件间关系)
          • [3.2 Prepare Dataset](#3.2 Prepare Dataset)
          • [3.3 Design model using class](#3.3 Design model using class)
          • [3.4 Construct loss and optimizer](#3.4 Construct loss and optimizer)
          • [3.5 Train cycle](#3.5 Train cycle)
        • 四、多分类问题
          • [4.1 问题讲解](#4.1 问题讲解)
          • [4.2 代码实践](#4.2 代码实践)
      • [第Ⅲ章 渐入佳境](#第Ⅲ章 渐入佳境)
        • 一、初识CNN
          • [1.1 输入层(Input Layer)](#1.1 输入层(Input Layer))
          • [1.2 卷积层(Convolution Layer)](#1.2 卷积层(Convolution Layer))
          • [1.3 池化层(Pooling Layer)](#1.3 池化层(Pooling Layer))
          • [1.4 激活层(Activation Layer)](#1.4 激活层(Activation Layer))
          • [1.5 全连接层(Fully Connected Layer,FC Layer)](#1.5 全连接层(Fully Connected Layer,FC Layer))
        • 二、深入CNN
          • [2.1 Inception Block](#2.1 Inception Block)
          • [2.2 简化运算](#2.2 简化运算)
          • [2.3 梯度消失](#2.3 梯度消失)
        • 三、实战CNN
        • 四、初识RNN
          • [4.1 前向传播](#4.1 前向传播)
          • [4.2 反向传播](#4.2 反向传播)
          • [4.3 更新方式](#4.3 更新方式)
            • [4.3.1 逐个样本训练(单样本模式)](#4.3.1 逐个样本训练(单样本模式))
            • [4.3.2 mini-batch 训练(常用方式)](#4.3.2 mini-batch 训练(常用方式))
        • 五、深入RNN
          • [5.1 多分类问题](#5.1 多分类问题)
          • [5.2 双向传播](#5.2 双向传播)
          • [5.3 GRU](#5.3 GRU)
            • [5.3.1 GRU的提出](#5.3.1 GRU的提出)
            • [5.3.2 GRU思想](#5.3.2 GRU思想)
        • 六、实战RNN

第Ⅰ章 初入茅庐

一、 数学的奥妙

【世上所有事物运行规律皆可用函数表达】

深度学习本质思想 :使用 ++"线性函数与非线性激活函数相互嵌套形成的一个函数"++ ,例如式1:
g ( w 3 g ( w 1 x 1 + w 2 x 2 + b 1 ) + b 2 ) ) g(w_3g(w_1x_1+w_2x_2+b_1)+b_2)) g(w3g(w1x1+w2x2+b1)+b2))

来拟合任何函数,解决任何复杂问题。

而这样的函数代数形式过于复杂,将其化为 ++"神经网络"++ 的形式形象表达:

对应式1 :输入层两个节点x1与x2,隐藏层一个节点:
g ( w 1 x 1 + w 2 x 2 + b 1 ) g(w_1x_1+w_2x_2+b_1) g(w1x1+w2x2+b1)

其中需要求解的参数有w1、w2、b1;输出层一个节点:
g ( w 3 g ( w 1 x 1 + w 2 x 2 + b 1 ) + b 2 ) ) g(w_3g(w_1x_1+w_2x_2+b_1)+b_2)) g(w3g(w1x1+w2x2+b1)+b2))

其中参数w3、b2。

如何求解w和b呢?根据已知输入输出值去"猜测"。

先随机给定一组参数值,计算损失函数L(以均方误差MSE为例,后面一说损失函数即指该式)
L ( w , b ) = 1 N ∑ i = 1 N ( y i − y ^ i ) 2 L(w,b) = \frac{1}{N} \sum_{i=1}^{N} (y_i - \hat{y}_i)^2 L(w,b)=N1i=1∑N(yi−y^i)2

的值,"调整"参数使L最小。

如何调整?尝试直接求偏导以找到损失函数最低点。

使L对每个参数的偏导值为0,求解此时的参数。例如,对多自变量函数,每个自变量偏导为0,此时为函数极小值。联想xyz坐标系的一个三维曲面,如下图,x轴横截面与y轴横截面最低点组成的坐标(x,y,z)为曲面最低点。

然而将函数式带入损失函数L后,L变得极为复杂,直接令偏导为0求解变得不现实。

可以举一个简单例子:拟合式为y=wx+b,L仍为MSE,给定三个点A(1,1) B(2,2) C(3,3)带入得:
L = 1 3 [ ( w + b − 1 ) 2 + ( 2 w + b − 2 ) 2 + ( 3 w + b − 3 ) 2 ] L=\frac{1}{3} \left[ (w + b - 1)^2 + (2w + b - 2)^2 + (3w + b - 3)^2 \right] L=31[(w+b−1)2+(2w+b−2)2+(3w+b−3)2]

分别使L对w,b偏导为0联立得w=1,b=0,故y=x,完美拟合三点。

Think-Help

这里的拟合式省去了激活函数,实际不可省略,因为这是神经网络拟合复杂问题的基础。

这是一个最简单的例子,试想成千上万的参数和数据集,分别将样本点代入L,然后分别将上万个偏导式联立,求解对应上万个参数的解,基本不可能,而且很多时候无法直接求出偏导为零的解析解。

那怎么办?

二、梯度下降
2.1 自然引出

没办法直接求出来那就一点点试,仍以式2为例,梯度下降公式3如下:
w = w − η ⋅ ∂ L ( w , b ) ∂ w b = b − η ⋅ ∂ L ( w , b ) ∂ b w = w - \eta \cdot \frac{\partial L(w,b)}{\partial w}\\ b = b - \eta \cdot \frac{\partial L(w,b)}{\partial b} w=w−η⋅∂w∂L(w,b)b=b−η⋅∂b∂L(w,b)

偏导大于0,即参数与L的变化方向一致(如式3中w若"偏导L/偏导w"大于0,表示固定b,w增大L就增大),反之变化相反。

这样,式3就实现了永远朝着L减小的方向前进。(方向一致,减小参数;方向相反,增大参数)

每次都代入全部样本点进行参数更新吗?

在每一次epoch中,我们都有一组样本点(训练集)作为信息可以使用。比如在公式3中,其损失函数就是用全部样本点作为信息进行更新:在一次epoch中,把所有样本点的

损失平方求和再平均(即MSE)作为损失函数代入损失函数L对参数的求导式中,进行一次参数更新。

但是,在神经网络中比局部最优解更棘手的问题是------鞍点问题------即在迭代过程中往往会出现梯度接近0的情况,这就会导致参数更新几乎停滞(Why?Think a Think)。

于是,**随机梯度下降(SGD)**成为一种解决方法。

SGD在每一次epoch中依次对每个样本点进行梯度计算并更新参数,这样的话,一个epoch中,参数会像一位中风的老爷爷般哆嗦着向前移动,而不至于陷入鞍点无法移动。

Think-Help

使用随机梯度下降每一个epoch中,++每个样本点++都会被用于单独更新一次参数,而不是随机选取部分样本用于更新参数,随机性主要体现在随机打乱(shuffle)训练数据的顺序。

多说一句 :++为什么要继续移动?++因为鞍点确实是一个或几个维度上的最优点,也就是部分参数达到了最优,但我们最终的目标是使目标函数达到最小,继续移动仍极大可能因整体参数变化而使目标函数最小。

前面说到:SGD 会使参数像一位中风的老爷爷般哆嗦着向前移动 ,也就是参数更新极小极慢;而GD 又像个疯狂老奶奶动不动就劈个叉 ,再也站不起来;那能不能正常点,小步快走呢?当然可以,在一个epoch中,将全部样本点分组打包,依次利用每一组样本进行梯度计算及参数更新即可,这就叫Mini-Batch

2.2 梯度下降三种方法

接下来,我们具体来看++一个epoch中++【我特意强调了这点:希望读者以epoch作为单位】,参数更新的三种方式:

2.2.1 All(Gradient Descent)

疯狂的老奶奶------在一个epoch中一次选取全部样本点为"信息"进行一次参数更新。

公式表现:所有样本点损失进行MSE作为损失函数进行BP。
θ : = θ − η ⋅ 1 N ∑ i = 1 N ∇ θ L ( f ( x i ; θ ) , y i ) \theta := \theta - \eta \cdot \frac{1}{N} \sum_{i=1}^{N} \nabla_\theta L(f(x_i;\theta), y_i) θ:=θ−η⋅N1i=1∑N∇θL(f(xi;θ),yi)

其中,θ:模型参数;η:学习率(learning rate);N:样本总数;L(⋅):损失函数;∇θL:对参数的梯度。

代码表现:

python 复制代码
# Gradient Descent
for epoch in range(num_epochs):
    y_pred = model(X)                      # 所有样本一次前向传播
    loss = loss_fn(y_pred, y_true)         # 计算整体损失
    grad = compute_gradients(loss, model)  # 计算所有样本平均梯度
    model.parameters -= lr * grad          # 一次更新参数
2.2.2 One-By-One(Stochastic Gradient Descent)

蹒跚的老爷爷------在一个epoch中依次选取每个样本点为"信息"进行N(指样本点数量)次参数更新。

公式表现:每个样本点单独计算损失作为损失函数进行BP。
θ : = θ − η ⋅ ∇ θ L ( f ( x i ; θ ) , y i ) , i = 1 , 2 , ... , N \theta := \theta - \eta \cdot \nabla_\theta L(f(x_i;\theta), y_i), \quad i = 1,2,\ldots,N θ:=θ−η⋅∇θL(f(xi;θ),yi),i=1,2,...,N

代码表现:

python 复制代码
# Stochastic Gradient Descent
for epoch in range(num_epochs):
    for i in range(N):                     # N为样本数
        x_i, y_i = X[i], y_true[i]
        y_pred = model(x_i)                # 单样本前向传播
        loss = loss_fn(y_pred, y_i)
        grad = compute_gradients(loss, model)
        model.parameters -= lr * grad      # 每个样本都更新一次
2.2.3 Mini-Batch(小批量梯度下降法)

稳健的你------在一个epoch中分批次选取样本点组合为"信息"进行X(指批次数量)次参数更新。

公式表现:
θ : = θ − η ⋅ 1 m ∑ j = 1 m ∇ θ L ( f ( x j ; θ ) , y j ) , ( x j , y j ) ∈ Batch k \theta := \theta - \eta \cdot \frac{1}{m} \sum_{j=1}^{m} \nabla_\theta L(f(x_j;\theta), y_j), \quad (x_j, y_j) \in \text{Batch}_k θ:=θ−η⋅m1j=1∑m∇θL(f(xj;θ),yj),(xj,yj)∈Batchk

代码表现:

python 复制代码
# Mini-Batch Gradient Descent
for epoch in range(num_epochs):
    for batch_X, batch_y in DataLoader(X, y_true, batch_size):
        y_pred = model(batch_X)                  # 小批量前向传播
        loss = loss_fn(y_pred, batch_y)
        grad = compute_gradients(loss, model)
        model.parameters -= lr * grad            # 每个批次更新一次

自然地,Mini-Batch是目前训练中最常用的梯度下降方法,所以在其它场景一说梯度下降,即指Mini-Batch方法。

接下来,我们只需要计算损失函数对各参数的梯度即可完成更新。

如何计算?

三、前向传播与反向传播

前向传播(Forward Propagation)与反向传播(Back Propagation,简称"BP")

许多学习资料中常把这节仅称作"反向传播",我认为还是不应该落下前向传播,这才是一个整体参数更新过程。

3.1 定义

前向传播:输入样本进入神经网络,计算得到所有中间及最终结果。

Think-Help

已知样本点或样本矩阵(矩阵由多个样本点组成)及标注y值,参数集θ(如果初步迭代,初始设定;否则,就是上一次epoch计算得到的)及激活函数,那么所有中间结果、最终预测结果及Loss值都可得到。换句话说,该神经网络的所有值你都已知道。

反向传播:在神经网络中,反向计算每两层间导数,通过链式法则相乘,得到损失函数对各参数的梯度。

Think_Help

链式法则的应用:要求"偏导L/偏导w"和"偏导L/偏导W",W是包含w的多项表达式,自然地想到先求后者,然后
∂ L ∂ w = ∂ L ∂ W ⋅ ∂ W ∂ w \frac{\partial L}{\partial w} = \frac{\partial L}{\partial W} \cdot \frac{\partial W}{\partial w} ∂w∂L=∂W∂L⋅∂w∂W

可不可以直接把L写成w的表达式然后再直接求"偏导L/偏导w",可以,但在神经网络复杂的体系中不现实。

3.2 实例
3.2.1 单样本点实例

建议先看一遍以下链接的例子(该例完整但过于简单):一文弄懂神经网络中的反向传播法------BackPropagation - Charlotte77 - 博客园

这是输入的样本点,相当于三种参数更新方法中的One-by-One,只通过这个例子学习的话会产生一个疑问:多个样本点(样本矩阵)同时输入是如何计算的呢?会不会需要一些特殊处理?

3.2.2 多样本点实例

我以Mini-Batch Size=2的情况手写了另一个例子:

采用学习率为0.01更新后:
W 2 new = [ 1 , 2 ] − 0.01 ⋅ [ 0 , − 30 ] = [ 1.00 , 2.30 ] W_2^{\text{new}} = [1, 2] - 0.01 \cdot [0, -30] = [1.00, 2.30] W2new=[1,2]−0.01⋅[0,−30]=[1.00,2.30]

b 2 new = 0 − 0.01 ⋅ ( − 8 ) = 0.08 b_2^{\text{new}} = 0 - 0.01 \cdot (-8) = 0.08 b2new=0−0.01⋅(−8)=0.08

W 1 new = [ 1 − 1 2 0 ] − 0.01 [ 0 0 − 34 − 50 ] = [ 1.00 − 1.00 2.34 0.50 ] W_1^{\text{new}} = \begin{bmatrix} 1 & -1 \\ 2 & 0 \end{bmatrix} - 0.01 \begin{bmatrix} 0 & 0 \\ -34 & -50 \end{bmatrix} =\begin{bmatrix} 1.00 & -1.00 \\ 2.34 & 0.50 \end{bmatrix} W1new=[12−10]−0.01[0−340−50]=[1.002.34−1.000.50]

b 1 new = [ 0.5 , − 0.5 ] − 0.01 [ 0 , − 16 ] = [ 0.50 , − 0.34 ] b_1^{\text{new}} = [0.5, -0.5] - 0.01[0, -16] = [0.50, -0.34] b1new=[0.5,−0.5]−0.01[0,−16]=[0.50,−0.34]

3.3 Think-Help

既然用了多个样本,为什么梯度计算过程中矩阵变大,但参数 w、b 的形状却没变?

参数w、b的形状只取决于网络结构,不取决于样本数量

Think-Help

也就是说,无论你用 1 个样本还是 1 万个样本,只要输入维度、输出维度、每层的神经元数量没变,则参数矩阵的维度始终固定。

这里关键的魔法在于:++矩阵乘法自动帮你把所有样本的梯度累加/求和,结果仍是固定形状的矩阵。++

个人认为这是一个理解的堵点,我又整理了一个更为复杂(6样本 × 3特征)的例子以供理解,已上传至:Xueyouing/Study-Naturally
至于为何在神经网络中比局部最优解更棘手的问题是鞍点问题?

在一个具有高维空间的损失函数中,如果一个维度上(对应一个参数w_i)的梯度为0,这个时候这个参数的值为0,那么在的某个邻域内,要么是凸函数,要么是凹函数,在一个维度上,凸函数对应着极小值,凹函数对应着极大值,如果训练到了所有维度的参数的梯度均为0时,如果真的达到了极小值,那么就要求所有维度上在此点的一个邻域内都是凸函数,很显然,这样的概率是很小的,我们更有可能遇到的是鞍点(或者是平坦区域)。

该题解采自鞍点问题 - Hexo


到这里,恭喜你!已经为神经网络运算提供了基本思路(函数形式)和可行方法(梯度下降+前反向传播)。


第Ⅱ章 小试牛刀

在初入茅庐后,我们首先将神经网络应用到较为简单的回归、分类问题中去,综合代码实践,知行合一。

一、线性回归

第Ⅰ章中,我们就是以线性回归举例的,所以我们围绕代码进行回顾及展开。

++值得注意:本小节的四个环节(四部曲)即为深度学习标准处理流程,为保障阅读体验,各小节完整代码放于文末,参考刘二大人视频讲解++

1.1 Prepare Dataset
python 复制代码
# 库------类------对象------实例
import torch
# 1. Prepare Dataset
x_data = torch.Tensor([[1.0],[2.0],[3.0]])  #创建二维张量(2D Tensor)
y_data = torch.Tensor([[2.0],[4.0],[6.0]])  #3个样本,1个特征

首先,主角登场:torch包------PyTorch核心包,其主要模块如下,都将是我们之后的常驻嘉宾:

模块 作用 举例
torch 基础张量操作 torch.tensor(), torch.mean()
torch.nn 构建神经网络层与模型 nn.Linear, nn.ReLU, nn.Module
torch.optim 各种优化算法 optim.SGD, optim.Adam
torch.utils.data 数据加载工具 DataLoader, Dataset
torch.autograd 自动求导机制 loss.backward()
torch.cuda GPU 控制与计算 torch.cuda.is_available()
torchvision (扩展包) 图像数据集、模型、预处理 transforms, datasets.CIFAR10

将其尽可能联系起来:

PyTorch 从 torch 模块出发,用 tensor 表示数据,autograd 实现自动求导,nn.Module 构建模型,optim 更新参数,最后通过 cuda 实现 GPU 加速。

接着,选取y=2x的3个点作为数据集,并将其转为张量------深度学习的基础单位。

Think-Help

张量(Tensor)就是多维数组,是标量、向量、矩阵的推广,有很好的数学表达性、可并行性和可求导性。

1.2 Design model using class
python 复制代码
# 2. Design model using class
class LinearModel(torch.nn.Module):  #该类继承自Module类
    def __init__(self):  #每次创建对象时调用
        super(LinearModel,self).__init__()   #调用父类的初始化
        self.linear = torch.nn.Linear(1,1)  #装一个线性层:y=wx+b

    def forward(self,x):  #当后续执行 model(x) 时,自动调用这个 forward 函数
        y_pred = self.linear(x)  #将输入x丢进线性层
        return y_pred
model = LinearModel()

使用线性模型类搭建神经网络

首先初始化:继承父类的方法(如注册网络层、管理参数、模块嵌套支持及模型保存),然后构建你的网络层(层类、层数、每层神经元数量);然后定义前向传播规则(得到初始计算值后如何做,通常加激活函数,返回最终预测值);最后实例化线性模型为model。

1.3 Construct loss and optimizer
python 复制代码
# 3. Construct loss and optimizer
criterion = torch.nn.MSELoss(reduction='sum')  #创建对象,调用MSELoss类,设置误差求和而非平均
optimizer = torch.optim.SGD(model.parameters(),lr=0.01)  #model.parameters()寻找所有待更新参数

选择损失函数及优化器

根据问题类型选择损失函数,这里为MSE均方误差,优化器通常选择SGD。

Think-Help

损失函数的计算方式没有严格要求,但应注意:++是否取平均等操作会影响学习率的选择++ ,因为求导后1/N仍然存在,如下图:
θ : = θ − η ⋅ 1 N ∑ i = 1 N ∇ θ L ( f ( x i ; θ ) , y i ) \theta := \theta - \eta \cdot \frac{1}{N} \sum_{i=1}^{N} \nabla_\theta L(f(x_i;\theta), y_i) θ:=θ−η⋅N1i=1∑N∇θL(f(xi;θ),yi)

1.4 Train cycle
python 复制代码
# 4. Train cycle
for epoch in range(1000):
    #前向传播
    y_pred = model(x_data)
    loss = criterion(y_pred,y_data)  #实例化对象
    print(epoch,loss.item())
    #反向传播
    optimizer.zero_grad()  #清空上一次计算的梯度(否则梯度会累加)
    loss.backward()  #自动计算每个参数的梯度
    optimizer.step()  #更新参数

进入训练周期

从这里可以明显看出该线性模型使用All(传统随机梯度下降GD,即使用一组样本进行一次参数更新),首先前向传播:计算最终预测值、损失函数并输出;然后反向传播:梯度清空、计算当前梯度、参数更新。

python 复制代码
print("w = ",model.linear.weight.item())  #.item() 把张量里的数值提取为普通的Python数字
print("b = ",model.linear.bias.item())

x_test = torch.Tensor([[4.0]])
y_test = model(x_test)
print("y_pred = ",y_test.data)  #.data:返回张量的实际数据部分
#tensor.data 仍然是张量(Tensor),但它不再追踪梯度,也不在计算图中

最后进行测试及所需其它操作。

二、逻辑斯蒂回归
2.1 问题讲解

有趣的是,Logistics回归虽然叫回归,但实际是一种二分类方法,简单来说,它相比线性回归只增添了一个Sigmoid函数,将线性回归的输出值代入Sigmoid中实现分类。
σ ( x ) = 1 1 + e − x \sigma(x) = \frac{1}{1 + e^{-x}} σ(x)=1+e−x1

如图所示:

故无论线性回归输出值是什么一定能将其转为(0,1)范围之间,将连续值转化为概率值实现分类。

同时,需要一起变化的还有损失函数,最常用的二分类(y=0/1)损失函数即为BCELoss(Binary Cross Entropy Loss,二元交叉熵损失函数 ),对于单个样本:
L ( y , y ^ ) = − [ y log ⁡ ( y ^ ) + ( 1 − y ) log ⁡ ( 1 − y ^ ) ] L(y, \hat{y}) = - \left[ y \log(\hat{y}) + (1 - y) \log(1 - \hat{y}) \right] L(y,y^)=−[ylog(y^)+(1−y)log(1−y^)]

对于N个样本取平均损失:
BCE Loss = − 1 N ∑ i = 1 N [ y i log ⁡ ( y ^ i ) + ( 1 − y i ) log ⁡ ( 1 − y ^ i ) ] \text{BCE Loss} = -\frac{1}{N} \sum_{i=1}^{N} \left[ y_i \log(\hat{y}_i) + (1 - y_i) \log(1 - \hat{y}_i) \right] BCE Loss=−N1i=1∑N[yilog(y^i)+(1−yi)log(1−y^i)]

Think-Help

用于记录当前模型输出的概率与真实标签的差距,为什么能做到呢?

首先要明确通常预测y值指的是预测为正类的概率值,即:
y ^ = P ( y = 1 ∣ x ) \hat{y} = P(y = 1 | x) y^=P(y=1∣x)

则:
L = − l o g ( 1 − y ^ ) , y = 0 L = − l o g ( y ^ ) , y = 1 L=-log(1-\hat{y}),y=0\\ L=-log(\hat{y}),y=1 L=−log(1−y^),y=0L=−log(y^),y=1

也就实现了预测正确的概率值越大,损失越小。

2.2 代码实践

在代码中,仅设计模型处多加一个Sigmoid函数,损失函数换为BCELoss即可。

python 复制代码
# 1、Design model using Class
class LogisticRegressionModel(torch.nn.Module):
    def __init__(self):
        super(LogisticRegressionModel, self).__init__()
        self.linear = torch.nn.Linear(1,1)

    def forward(self, x):
        y_pred = torch.sigmoid(self.linear(x))  # 此处加了sigmoid函数,对初始输出值进行sigmoid处理
        return y_pred
model = LogisticRegressionModel()

# 2、Construct loss and optimizer
criterion = torch.nn.BCELoss(size_average=False)   # Key:是否取均值影响学习率设置:Loss是否乘以1/N------Loss对参数的梯度是否乘以1/N
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
三、DataLoader完整流程

在前两节我们的参数更新方法都是All(传统GD),但在第Ⅰ章我们提到了最常用的是Mini-Batch方法,如何实现呢?就要使用DataLoader。

3.1 python组件间关系

++补充一下python基本知识点------组件间的关系++

python 复制代码
程序库 Library(比如 PyTorch)
 ├── 包 Package(torch、torch.nn、torch.utils)
 │     ├── 模块 Module(torch.nn.functional)
 │     │     ├── 类 Class(Linear, MSELoss 等)
 │     │     └── 函数 Function(sigmoid(), relu() 等)
 │     └── 子包 Subpackage(torch.utils.data)
 └── ...

PyTorch 中,DataLoader 属于 数据加载模块(torch.utils.data 下的一个类(Class)

3.2 Prepare Dataset

定义类进行完善的数据处理是个好习惯。

python 复制代码
# 1.Prepare Dataset
class DiabetesDataset(Dataset):
    def __init__(self, filepath):
        xy = np.loadtxt(filepath, delimiter=',', dtype=np.float32, skiprows=1)  # 第一行为标题列,无法转浮点型
        self.len = xy.shape[0]  # 查看第一列,即有多少个样本
        self.x_data = torch.from_numpy(xy[:, :-1])
        self.y_data = torch.from_numpy(xy[:, [-1]])
    '''
    Think-Help
    xy------numpy数组   形状:(样本数,特征数+1)
    xy[:, :-1] 取所有样本的前N-1列,即所有特征
    xy[:, [-1]] 取所有样本的第N列,即标注值
    torch.from_numpy 即将numpy数组转为pytorch张量,共享内存(不新建,一改具改)。
    注:xy[:, -1]和xy[:, [-1]]不同,前者是一维数组,后者仍是二维矩阵。
    '''
    def __getitem__(self, index):
        return self.x_data[index], self.y_data[index]

    def __len__(self):
        return self.len
    '''
    Think-Help
    Magic Method------魔法方法,即带有双下划线的方法,
    定义:不会被直接调用,隐式自动触发
    例:执行model = LinearModel()后,自动调用__new__ and __init__
    Python 的设计哲学:Everything is an object
    '''
dataset = DiabetesDataset('./dataset/pima-indians-diabetes.csv')  #文件链接:https://github.com/Xueyouing/Study-Naturally
train_loader = DataLoader(dataset=dataset, batch_size=32, shuffle=True, num_workers=4)

注意最后一行,从传入参数也可以看出DataLoader决定mini-batch中样本数量,是否打乱及并行方式。

3.3 Design model using class
python 复制代码
# 2.Design model using class
class Model(torch.nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.linear1 = torch.nn.Linear(8, 6)
        self.linear2 = torch.nn.Linear(6, 4)
        self.linear3 = torch.nn.Linear(4, 1)
        self.sigmoid = torch.nn.Sigmoid()

    def forward(self, x):
        x = self.sigmoid(self.linear1(x))
        x = self.sigmoid(self.linear2(x))
        x = self.sigmoid(self.linear3(x))
        return x

model = Model() #实例化

神经网络对应形式(从第一个隐含层开始输入值都要经过激活函数Sigmoid计算):

3.4 Construct loss and optimizer
python 复制代码
# 3.Construct loss and optimizer
criterion = torch.nn.BCELoss(reduction='mean')
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

Think-Help
torch.optim.SGD 的名字虽然叫 "Stochastic Gradient Descent",但它并不强制你一个样本一个样本地更新。

Saying again:是否使用 mini-batch(小批量样本) , 其实是由 你怎么喂数据(DataLoader) 决定的,而不是优化器。

3.5 Train cycle
py 复制代码
# 4.Train cycle
if __name__ == '__main__':
    for epoch in range(100):
        for i,data in enumerate(train_loader, 0):  
            inputs, labels = data
            y_pred = model(inputs)
            loss = criterion(y_pred, labels)
            print(epoch, i, loss.item())

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

'''
Think-Help
if __name__ == '__main__':当该脚本被直接执行时运行。这里是为了防止trainloader报错。
enumerate返回索引i及数据值data,其中data又分为(inputs,labels)
0指从第0批次开始。
'''

很关键的不同点,第二层循环即指代每个epoch分批量使用数据集进行参数更新。

四、多分类问题
4.1 问题讲解

对于多分类问题,自然仍先要找到适合它的损失函数。

首先要明确分类问题的两个条件:最终各类别概率值大于0;各类别概率值之和等于1。

我们可以先回顾一下二分类问题的求解过程:输出层得到一个输出值,代入Sigmoid函数得到P(y=1|x)的概率值,P(y=0|x)=1-P(y=1|x),满足要求,损失函数可以计算。

而多分类如何满足呢?输出层不止一个结点,会得到多个输出值。

显然,仍将输出值都代入Sigmoid函数无法满足要求,而另一个函数却可以------Softmax函数:
P ( y = i ) = e z i ∑ j = 0 K − 1 e z j , i ∈ { 0 , . . . , K − 1 } P(y = i) = \frac{e^{z_i}}{\sum_{j=0}^{K-1} e^{z_j}}, i \in \{0, ..., K - 1\} P(y=i)=∑j=0K−1ezjezi,i∈{0,...,K−1}

其中z便是输出值,将全部K个值都带入,会得到一个"值都大于0且K个值和为1的分布"。

得到概率值后,如何计算损失呢?

Loss = − log ⁡ ( y ^ c ) \text{Loss} = -\log(\hat{y}_c) Loss=−log(y^c)

其中yc表示预测正确类别的概率,概率值越高,损失越小。

Think-Help

二分类和多分类的损失函数实际是一样的,都是取预测正确类别的概率进行-log(),以实现**"预测正确的概率值越高,损失越小"**。

思路讲解完毕,来看具体实现。

图释:输出值,经过Softmax函数得到概率值,对应乘以独热编码后,只剩下了-log(yc)一项。

整体流程叫做交叉熵损失函数(CrossEntropyLoss),而-log(yc)叫做负对数似然损失(NLLLoss,Negative Log Likelihood Loss)。

二者的关系为:CrossEntropyLoss = Softmax + NLLLoss

Think-Help

在实际编程时,你会看到log_softmax函数,如下:

py 复制代码
log_probs = F.log_softmax(logits, dim=1)
loss = F.nll_loss(log_probs, target)

不要惊讶,这是因为考虑到数值稳定性(不会出现 exp(大数)log(接近 0) 的溢出问题),要将softmax和log共同进行:
log ⁡ ( e z i ∑ j e z j ) = z i − log ⁡ ( ∑ j e z j ) \log\left(\frac{e^{z_i}}{\sum_j e^{z_j}}\right) = z_i - \log\left(\sum_j e^{z_j}\right) log(∑jezjezi)=zi−log(j∑ezj)

nll_loss实际只起到了取值并取负的操作。

4.2 代码实践

本节实践是经典手写数字图像MNIST数据集,要求识别0-9的多分类问题。

流程仍是四部曲,重点在于使用到了图像识别的专用工具和操作,已在代码中详细注释。

python 复制代码
import torch
from torchvision import transforms  # 图像预处理
from torchvision import datasets  # 图像数据集加载
from torch.utils.data import DataLoader
import torch.nn.functional as F
import torch.optim as optim

# 1.Prepare dataset
batch_size = 64
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])
'''
ToTensor将图片转为张量,Normalize将其像素值转为[0, 1]区间(标准化)
(0.2307,) 是整个数据集中所有像素的 平均值(mean);
(0.3081,) 是所有像素的 标准差(std);
Because:神经网络更喜欢正态分布数据【加快收敛速度、防止某些特征主导模型、保持激活函数在有效区间工作等】
'''
train_dataset = datasets.MNIST(root='../L_Pytorch/dataset', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, shuffle=True, batch_size=batch_size)
test_dataset = datasets.MNIST(root='../L_Pytorch/dataset', train=False, download=True, transform=transform)
test_loader = DataLoader(test_dataset, shuffle=False, batch_size=batch_size)

# 2.Design model using class
class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.l1 = torch.nn.Linear(784, 512)
        self.l2 = torch.nn.Linear(512, 256)
        self.l3 = torch.nn.Linear(256, 128)
        self.l4 = torch.nn.Linear(128, 64)
        self.l5 = torch.nn.Linear(64, 10)

    def forward(self, x):
        x = x.view(-1, 784)  # 获得batch_size,摊平图片维度
        x = F.relu(self.l1(x))
        x = F.relu(self.l2(x))
        x = F.relu(self.l3(x))
        x = F.relu(self.l4(x))
        return self.l5(x)

model = Net()

# 3.Construct loss and optimizer
criterion = torch.nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)  # 动量使收敛更快、更稳定:每次参数更新时,有 50% 的"惯性"来自上一次的梯度方向。

# 4.Train cycle
def train(epoch):
    running_loss = 0.0
    for batch_idx, data in enumerate(train_loader, 0):
        inputs, targets = data
        outputs = model(inputs)
        loss = criterion(outputs, targets)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        running_loss += loss.item()
        if batch_idx % 300 == 299:  # 下标从0开始
            print(f"[{epoch+1}, {batch_idx+1:5d}] loss: {running_loss/300:.3f}")
            running_loss = 0

def test():
    correct = 0
    total = 0
    with torch.no_grad():  # 测试无需更新参数,故不用计算梯度
        for data in test_loader:
            images, labels = data
            outputs = model(images)
            _, prediction = torch.max(outputs.data, dim=1)
            '''
            Think-Help
            torch.max(tensor, dim=1):在指定维度 dim=1 上求最大值。
            返回两个结果: 第一维是最大值;第二维是最大值所在的位置(索引 index)。
            我们取概率值最大的类别索引,只关心类别索引而不关心概率值。
            '''
            total += labels.size(0)  # labels是长度为size的一维张量,每次加上当前批次(batch)的样本数量
            correct += (prediction == labels).sum().item()
    print(f'Accuracy: {correct/total}')

if __name__ == '__main__':
    for epoch in range(10):
        train(epoch)
        test()

恭喜你!又成功小试牛刀!你已经对四部曲有了深刻印象,这是很好的基础!同时可以自己设计基础神经网络并应用到简单的回归分类任务中去!

接下来让我们学习当下最流行的神经网络架构!


第Ⅲ章 渐入佳境

一、初识CNN
1.1 输入层(Input Layer)

卷积神经网络(Convolutional Neural Networks,CNN) 是一种专门用来处理图片(或具有空间结构数据) 的神经网络。

一般来说,当今图片分为像素图和矢量图,前者由一个个像素方格组成,一个像素格代表[0, 255]的亮度值,于是,一张黑白图片便是一个数字矩阵。

为什么说是黑白图片呢?

因为,RGB 彩色图片有三个通道,分别存红、绿、蓝三种颜色分量。现在,你可以有一双"黄金瞳",将一张普通彩色图片视为三个矩阵堆叠在一起。

1.2 卷积层(Convolution Layer)

首先,介绍一个关键概念------Kernal(卷积核) :也就是小矩阵,用作窗口在大矩阵上滑行,进行计算操作。

接下来,我将操作流程定义为一个**++卷积层操作公式++ **:
卷积层操作 = 多组运算 = B ∗ 多通道运算 = B ∗ ( C ∗ 卷积运算 ) 卷积层操作 = 多组运算 = B*多通道运算 = B*(C*卷积运算) 卷积层操作=多组运算=B∗多通道运算=B∗(C∗卷积运算)

其中,B为输出通道数量,C为输入通道数量。

紧接着,我依次解释上述三种运算:

卷积运算:以Kernal为窗口遍历当前输入矩阵------将Kernal内元素与当前输入矩阵窗口内元素对应相乘,再相加得到一个数字,即为输出矩阵的一个对应位置元素,遍历完成即填满输出矩阵。

多通道运算:以C个Kernal对C个通道(即C个输入矩阵)分别进行卷积运算,得到C个输出矩阵,将C个矩阵对应元素相加得到一个新矩阵,即为最终矩阵。

多组运算:以B组Kernal(每组C个Kernal)进行相应多通道运算得到B个最终矩阵,即为B个特征图(Feature maps),也称为有B个通道。

Think-Help

操作中关键的参数主要有4个:++输出通道数B,输入通道数C,矩阵高H,矩阵宽W++。

从公式或图片亦或定义中不难看出:B决定多通道运算次数,最终想要多少个通道(多少个矩阵),就做多少次多通道运算;C决定卷积运算次数,给定输入有多少个通道,就必须做多少次卷积运算。

而矩阵的高和宽(H和W)分为卷积前后,主要取决于Kernal的大小。

设输入矩阵为A * A,Kernal为a * a,输出矩阵为X * X。(因矩阵通常为方阵,故如此假设)关系如下:
X = A − ( a / / 2 ) ∗ 2 X = A-(a//2)*2 X=A−(a//2)∗2

其中,//为整除符号。

不难理解:输出矩阵X上下左右都要减去Kernal边长的一半。

1.3 池化层(Pooling Layer)

池化层的主要目的是对特征图进行下采样(subsampling),也就是"缩小尺寸、保留重要信息"。

池化操作是对一个局部区域取代表值,有两种操作方式:

类型 计算方法 含义
最大池化 Max Pooling 取区域内最大值 提取最强特征(常用)
平均池化 Average Pooling 取区域内平均值 平滑特征(早期网络使用)

其中,最大池化操作如下图:

Think-Help

值得注意:池化仅改变矩阵尺寸(即H和W),不改变通道数。

同时补充两个十分重要的参数:padding(填充)和stride(步幅),虽然在池化层补充,但这两种操作在卷积层和池化层都可以做。

  1. padding

    在卷积层我们提到:输出矩阵会根据Kernal大小减少不同程度的边长,也就是损失了边缘信息。如果我们不想损失这些信息,如何做呢?

    自然地,在边缘补充一圈0,即输入矩阵上下左右都多了一行或一列。

    如下图,本来5 * 5的输入矩阵边长要减去2【2*(3//2)】变为3 * 3,但是将其扩充一圈变为7 * 7,这样,输出矩阵仍为5 * 5。

  2. stride

    表示卷积核或池化窗口 每次移动的步长,stride=2的情况如下图:

    注:池化的主要目的是"压缩尺寸",通常窗口之间不重叠,所以通常让步长等于窗口大小最自然。

1.4 激活层(Activation Layer)

处理图像数据,加入非线性特征也是至关重要的。

ReLU (Rectified Linear Unit)是目前 CNN 中最常用的激活函数:
f ( x ) = m a x ⁡ ( 0 , x ) f(x)=max⁡(0,x) f(x)=max⁡(0,x)

原因:计算简单、收敛更快、不容易梯度消失。

1.5 全连接层(Fully Connected Layer,FC Layer)

"卷积层是特征提取器,全连接层是分类器。"

保留特征图(Feature maps)形式,是没有办法判断最终类别滴,仍然需要"展平"(Flatten),将其变为一维向量,便于建立类别映射(如下),计算输出概率值。
y j = f ( ∑ i w i j x i + b j ) y_j = f\left( \sum_i w_{ij} x_i + b_j \right) yj=f(i∑wijxi+bj)

二、深入CNN
2.1 Inception Block

在学习完卷积神经网络常见的层次结构后,我们同样需要将其"组装"起来,以往我们学习到的神经网络设计模式都是线性,也就是将层次顺序链接起来,但实际应用中,往往需要功能更为复杂的设计模式,如下图(出自论文 《Going Deeper with Convolutions》,即 GoogLeNet ,这是 Google 在 2014 年提出的经典结构)。

其中我们勾画出一个多种层次路线混合的模块(称为Inception 模块),不同的是,这些层次路线是并行的而非串行。

Inception 模块是一种特殊的卷积块(block),它的核心思想是:

"给定多种选择让网络自己决定使用哪种卷积核尺寸来提取特征,而不是我们人为设定一个固定的卷积核。"
Think-Help

++为什么这么设计?++

传统 CNN(比如 LeNet、AlexNet)在每层卷积中,只使用固定大小的卷积核(例如全是 3×3)。

但问题是:

  • 小卷积核(1×1、3×3)→ 适合捕捉局部特征
  • 大卷积核(5×5)→ 能看见更全局的模式
  • 池化 → 帮助降维并提取主要特征

Google 团队就想:"既然不同大小的卷积核提取的信息不同,那我为什么不在一层里全部用上,然后再让网络自己学到该重视哪种特征?"

于是 Inception 结构就诞生了。

将层次并行,不同层次路线得出的结果最后如何整合呢?

按照通道(Channels)维度拼接结果,注意另外两个维度(H和W)自然必须一致。相当于把不同尺度的信息融合在一起,得到一个更强的综合特征表示。

2.2 简化运算

观察上面图片,你会发现有许多1 * 1的卷积层(Kernal尺寸为1 * 1),代入卷积层运算思考一下,这有必要吗?

似乎在提取特征方面没什么用,输出矩阵的尺寸没变,输出通道取决于你想要多少通道。那为什么还用呢?

简化运算! ++时间复杂度过高一直是CNN的痛点++,如下图使用1 * 1的Kernal会极大减少运算量(参照1.2自己算一遍哦)。

本质上就是中间进行一次通道缩小

2.3 梯度消失

设计好模式,在训练和测试时我们可能会遇到这样的情况:

可能有两个问题:过拟合【test error > train error & 56-layer > 20-layer】 和 梯度消失【56-layer > 20-layer,56-layer出现梯度消失导致模型无法得到有效训练】。

过拟合暂不多说,需要减少模型layer、增大惩罚项等操作;CNN的重点在于梯度消失,由于模型层数过多很可能导致梯度消失。

Think-Help

++梯度消失和鞍点问题的区别++

我们在第Ⅰ章提到过,神经网络的棘手问题是鞍点问题,为应对这个问题提出了SGD(随机梯度下降)和Mini-Batch(小批量梯度下降)。

而我们现在谈的是梯度消失,二者区别在于:

  • 鞍点问题指的是在反向传播过程中可能会偶然遇到**一个梯度**接近于0;
  • 而梯度消失问题指的是在反向传播过程中**多个梯度**(小于1)相乘,导致越往前传播梯度越小,最终几乎为 0,使早期层无法有效更新。

二者区别要尤其搞清,为应对梯度消失问题我们提出了几种方法(第1种前面1.4提到过,本节我们主要看第二种):

  1. 使用ReLU等非饱和激活函数(避免Sigmoid、tanh饱和区间造成梯度趋近0);
  2. 引入残差结构(Residual Connections),如ResNet,使梯度能直接跨层传播。
  3. 优化权重初始化方法(如Xavier或He初始化);
  4. 采用批量归一化(Batch Normalization) 来稳定梯度分布。

++引入残差结构ResNet++,如下图所示:

先看左侧传统网络(Plain net):输入x经过两层带权重的线性变换和非线性激活后,得到输出 H(x)。

再看右侧残差网络(Residual net,简称ResNet):输入x一方面进入普通的卷积层(两层 Weight Layer 加 ReLU),生成F(x);另一方面,被直接跳跃连接到输出端。

ResNet提出了一种新思想:让网络学习"残差"而不是直接学习映射 。残差定义为:
F ( x ) = H ( x ) − x F(x)=H(x)−x F(x)=H(x)−x
++这里的"残差"不同于损失函数中的残差++,因为它是映射函数输出减去输入而非真实y值,事实上我们也没法减去真实y值,ResNet在中间层而非输出层。

映射函数便为:
H ( x ) = F ( x ) + x H(x)=F(x)+x H(x)=F(x)+x

这样做为什么可以解决梯度消失问题?

很容易理解------ResNet结构中求梯度时绝对值永远大于等于1:
∂ H ( x ) ∂ x = ∂ F ( x ) ∂ x + 1 \frac{\partial H(x)}{\partial x}=\frac{\partial F(x)}{\partial x} +1 ∂x∂H(x)=∂x∂F(x)+1

这样,反向传播时再也不怕梯度消失啦!

三、实战CNN

待补充

四、初识RNN

循环神经网络(Recurrent Neural Network,RNN) 是一种专门处理序列数据的神经网络。

其最大的特点/优势就是**++"记忆"++**------能够结合之前的信息进行思考。

如下图所示,一组序列按照时间步依次输入RNN Cell,而每个RNN Cell都需要结合序列输入x和先前信息h进行下一步输出。

Think-Help

++RNN的特殊样本++

**在RNN中,一个样本(sequence)是一个时间序列。**序列按照时间步有多个元素,可能是一句话中的一个单词,可能是股价序列中的某一天:
x 1 , x 2 , ... , x T x_1,x_2,...,x_T x1,x2,...,xT

每个时间步输入一个 x_t,RNN 输出一个 h_t。

如:如果你在做语言模型,序列是词序列 [x1="我",x2="喜欢",x3="学习"]。这里 x1,x2,x3 是不同时刻的输入(每个可能是词向量)。

4.1 前向传播

具体的前向传播计算图及公式如下:

Think-Help

++RNN的特殊权重++

RNN 模型本身就只有一组参数(权重矩阵):
W x h : 输入 → 隐藏 W h h : 隐藏 → 隐藏 W h y : 隐藏 → 输出 W_{xh}:输入 → 隐藏\\ W_{hh}:隐藏 → 隐藏\\ W_{hy}:隐藏 → 输出 Wxh:输入→隐藏Whh:隐藏→隐藏Why:隐藏→输出

具体来说,同一样本中,不同时间步共享同一组参数(每个RNN Cell内部的参数都相同);不同样本间也共享参数。

简单来说,整个模型就一组参数。

关于每个Cell中的参数类型,由任务类型决定。

  • 多对多:对于每一步都要输出的任务,Cell中三个参数都有,如时序预测(股价每天都预测下一天)。

  • 多对一:对于只需要最后一步输出的任务,前面的Cell中只有W_xh和W_hh,如情感分类(整句话 → 一个情绪标签)。

4.2 反向传播

RNN中的反向传播被称作BPTT(Backpropagation Through Time)。

***初步理解:***相对于BP,BPTT中损失对参数的梯度求解时多考虑了"时间"因素。
∂ L ∂ W h h = ∑ t = 1 T ∂ L ∂ h t ⋅ ∂ h t ∂ W h h \frac{\partial L}{\partial W_{hh}} = \sum_{t=1}^{T} \frac{\partial L}{\partial h_t} \cdot \frac{\partial h_t}{\partial W_{hh}} ∂Whh∂L=t=1∑T∂ht∂L⋅∂Whh∂ht

这部分还是看实例更好理解(完整实例计算过程Xueyouing/Study-Naturally):

如果该序列时间步长是3呢?

那就需要再加上一个损失函数对W_hh的导数:
∂ L ∂ W h h    = ∂ L ∂ h 3 ⋅ ∂ h 3 ∂ h 2 ⋅ ∂ h 2 ∂ h 1 ⋅ ∂ h 1 ∂ W h h \frac{\partial L}{\partial W_{hh}} \; = \frac{\partial L}{\partial h_3} \cdot \frac{\partial h_3}{\partial h_2} \cdot \frac{\partial h_2}{\partial h_1} \cdot \frac{\partial h_1}{\partial W_{hh}} ∂Whh∂L=∂h3∂L⋅∂h2∂h3⋅∂h1∂h2⋅∂Whh∂h1

***直观上理解:***从1到N所有时间步长的RNN Cell都用到了W_hh,然后输出了相应的h_t,一步步传递,才得出最终输出值及损失值,最终要更新这个参数,不得求出每个Cell中的梯度再求和。

4.3 更新方式
4.3.1 逐个样本训练(单样本模式)

最原始的训练方式是:每次取一个完整序列------RNN 在时间上展开------做完前向、反向传播------更新参数------再取下一个样本。

流程伪代码:

复制代码
for sample in dataset:
    h = 0
    for x_t in sample:
        h = RNNCell(x_t, h)
    loss = compute_loss(h, target)
    loss.backward()
    optimizer.step()

该方法效率太低,无法利用 GPU 并行。

4.3.2 mini-batch 训练(常用方式)

现代训练都是 mini-batch 模式------我们把多个样本(序列)组成一个 batch,一起前向传播、一起反向传播、再一起更新参数**【每个batch更新一次参数】**。

假设 batch_size=2,每个序列长度=3:

时间步 样本1输入 样本2输入
t=1 x₁₁ x₂₁
t=2 x₁₂ x₂₂
t=3 x₁₃ x₂₃

训练时:

  • 前向:同时处理两个序列,每个时间步共享参数;
  • 反向:梯度沿时间轴回传,并在 batch 维度求平均
  • 更新:优化器对共享权重更新一次。

伪代码:

复制代码
for batch in data_loader:
    optimizer.zero_grad()
    outputs, hidden = model(batch_inputs)
    loss = criterion(outputs, targets)
    loss.backward()    # 所有样本 + 所有时间步的梯度累积
    optimizer.step()   # 更新一次参数
五、深入RNN
5.1 多分类问题

将RNN用于多分类问题:

和之前将全连接层构建的线性回归模型转为多分类模型思路一致:在其输出后加入交叉熵损失函数(CrossEntropyLoss),转为输出各类别概率值。

5.2 双向传播

对于一个序列(以一句话为例),传统做法是根据语句正向传播信息得到结果,语义的逆向是否也存在有价值的信息呢?

RNN还可以进行双向传播(序列正反方向各走一遍):

该隐藏层输出为:
h i d d e n = [ h N f , h N b ] hidden=[h_N^f,h_N^b] hidden=[hNf,hNb]

5.3 GRU
5.3.1 GRU的提出

对于RNN,BPTT时很容易导致梯度消失或爆炸,使模型难以记住长序列信息

Think-Help

++RNN的特殊问题++

我们先回顾一下梯度消失和爆炸两种情况

在所有神经网络中,反向传播的梯度更新都要用链式法则:
∂ L ∂ W = ∂ L ∂ h n ⋅ ∂ h n ∂ h n − 1 ⋅ ∂ h n − 1 ∂ h n − 2 ⋅ ... ⋅ ∂ h 1 ∂ W \frac{\partial L}{\partial W} = \frac{\partial L}{\partial h_n} \cdot \frac{\partial h_n}{\partial h_{n-1}} \cdot \frac{\partial h_{n-1}}{\partial h_{n-2}} \cdot \ldots \cdot \frac{\partial h_1}{\partial W} ∂W∂L=∂hn∂L⋅∂hn−1∂hn⋅∂hn−2∂hn−1⋅...⋅∂W∂h1

如果每一步的梯度范数 > 1 → 会越来越大(梯度爆炸);

如果每一步的梯度范数 < 1 → 会越来越小(梯度消失)。

注:先前我们并未提过梯度爆炸,因梯度消失更常见且更难解决。

而RNN相对MLP(全连接层)、CNN(卷积)神经网络更易梯度消失或爆炸,为什么?

  1. RNN 的"时间展开"导致乘法次数极多

    ++RNN的BPTT中,序列长度基本等价于层数。同一个权重 W_hh 会被在所有时间步重复使用。++

    例如一个长度为 100 的序列,RNN 展开后就是 100 层的"共享权重网络":
    h t = f ( W h h h t − 1 + W x h x t ) h_t = f(W_{hh}h_{t-1} + W_{xh}x_t) ht=f(Whhht−1+Wxhxt)

    反向传播时梯度链路是:
    ∂ L ∂ W h h ∝ ∏ t = 1 T ∂ h t ∂ h t − 1 \frac{\partial L}{\partial W_{hh}} \propto \prod_{t=1}^{T} \frac{\partial h_t}{\partial h_{t-1}} ∂Whh∂L∝t=1∏T∂ht−1∂ht

    这里的乘法次数 T(时间步长度)通常比 CNN 或 MLP 的层数大得多,比如 50、100、甚至几百。

  2. RNN 的循环结构容易形成"指数放大/衰减"

++记得4.1我们曾说过:整个RNN只有/共享一组参数。这意味着每次传播都会乘上同一个权重矩阵 W_hh。++

假设 W_hh 的最大特征值为 λ,那么梯度大致会按 ∣λ∣^T 变化。

  • 如果 ∣λ∣<1,梯度趋于 0 → 梯度消失
  • 如果 ∣λ∣>1,梯度呈指数增长 → 梯度爆炸

因为这种循环乘法在时间维度上持续发生,所以即使权重稍有不合适,也可能导致数值不稳定;而其它网络∣λ∣可能大于1可能小于1,会有所抵消。

5.3.2 GRU思想

于是人们提出了改进版 ------ GRU(门控循环单元)【Gated Recurrent Unit = 有门的循环单元】

首先给定完整公式:
z t = σ ( W z x t + U z h t − 1 ) 更新门 r t = σ ( W r x t + U r h t − 1 ) 重置门 h ~ t = tanh ⁡ ( W h x t + U h ( r t ⊙ h t − 1 ) ) 候选新状态 h t = ( 1 − z t ) ⊙ h t − 1 + z t ⊙ h ~ t 最终输出 \begin{aligned} z_t &= \sigma(W_z x_t + U_z h_{t-1}) && \text{更新门} \\ r_t &= \sigma(W_r x_t + U_r h_{t-1}) && \text{重置门} \\ \tilde{h}t &= \tanh(W_h x_t + U_h (r_t \odot h{t-1})) && \text{候选新状态} \\ h_t &= (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t && \text{最终输出} \end{aligned} ztrth~tht=σ(Wzxt+Uzht−1)=σ(Wrxt+Urht−1)=tanh(Whxt+Uh(rt⊙ht−1))=(1−zt)⊙ht−1+zt⊙h~t更新门重置门候选新状态最终输出

接下来,我们据此讲解其思路:

由前两个公式不难看出,更新门和重置门的公式格式一致:新输入x和上一步隐藏层输出(后面将其叫做"先前信息")分别乘以相应权重。

所以要从二者用途(也就是后两个公式)来区分不同:

  • 候选新状态:重置门越大,先前信息占比越大。
  • 最终输出:更新门越大,新状态占比越大。

于是,这两个"门"对应的作用可以这样说:

  1. 重置门(reset gate) --- 决定"要不要遗忘过去"
  2. 更新门(update gate) --- 决定"要不要更新记忆"
六、实战RNN

首先需要了解RNN常见参数:

参数名称 含义 作用
input_size 每个时间步输入向量的维度 例如:如果每个时间步输入一个词向量,则为词向量长度
hidden_size 隐藏层神经元数量(隐藏状态维度) 控制网络的"记忆容量"与建模能力
num_layers RNN 堆叠的层数 决定网络深度(多层RNN输出上层输入)
output_size 输出层神经元数量 取决于任务类型:分类、预测、回归等
seq_len 序列长度(时间步数量) 决定每次输入序列的时间跨度
batch_size 每次训练输入的样本数 控制并行训练规模
dropout 随机丢弃比例 防止过拟合(在层与层之间使用)
bidirectional 是否为双向RNN True表示同时考虑正向和反向序列信息

++Embedding(将元素转为向量)++

面对不等长序列,需要填充0,如下图所示:

而这些 0 是不必要进行计算的,于是引入一种更高效的方式------PackedSequence



恭喜你!到这里你洞悉了最经典的CNN、RNN神经网络,这是处理图片、文本等非结构化数据的基石。下面,我们将以实践为导向,追溯到近年来具有划时代意义的AI大模型。


相关推荐
高洁019 小时前
【无标具身智能-多任务与元学习】
神经网络·算法·aigc·transformer·知识图谱
技术支持者python,php9 小时前
训练模型,物体识别(opencv)
人工智能·opencv·计算机视觉
爱笑的眼睛119 小时前
深入理解MongoDB PyMongo API:从基础到高级实战
java·人工智能·python·ai
辣椒酱.10 小时前
jupyter相关
python·jupyter
郝学胜-神的一滴10 小时前
Python中常见的内置类型
开发语言·python·程序人生·个人开发
软件开发技术深度爱好者10 小时前
基于多个大模型自己建造一个AI智能助手
人工智能
识醉沉香10 小时前
广度优先遍历
算法·宽度优先
中國龍在廣州10 小时前
现在人工智能的研究路径可能走反了
人工智能·算法·搜索引擎·chatgpt·机器人
快手技术10 小时前
NeurIPS 2025 | 可灵团队提出 Flow-GRPO, 首次将在线强化学习引入流匹配生成模型
算法
攻城狮7号10 小时前
小米具身大模型 MiMo-Embodied 发布并全面开源:统一机器人与自动驾驶
人工智能·机器人·自动驾驶·开源大模型·mimo-embodied·小米具身大模型