1w什么是深度学习

深度学习(Deep Learning)是机器学习的分支,是一种以人工神经网络为架构对数据进行特征学习的算法。深度学习中的形容词“深度”是指在网络中使用多层。

神经元只做两件事:加权求和(算出概率),激活函数(映射)逻辑回归中的激活函数(即 Sigmoid 函数)与加权和,实际上是深度学习中神经网络的基本构建单元。它们之间的关系非常紧密,可以说:逻辑回归是深度学习中最简单的一种神经网络

概念 逻辑回归 深度学习(神经网络)
基本单元 一个 Sigmoid 神经元 多个神经元组成的层
加权和 wTx+bwTx+b Wx+bWx+b (矩阵形式)
激活函数 Sigmoid ReLU / Tanh / Sigmoid / Softmax
输出 二分类概率 可用于回归、分类、生成等任务
本质 线性模型 + 非线性激活 多层非线性变换的复合函数

逻辑回归中的“加权和 + Sigmoid 激活”正是深度学习中单个神经元的工作方式。深度学习可以看作是逻辑回归的多层次、高维、非线性扩展。理解逻辑回归,是理解神经网络的第一步。

深度学习核心思想是==通过模仿人脑的神经网络来处理和分析复杂的数据,从大量数据中自动提取复杂特征==,擅长处理高维数据,如图像、语音和文本。

深度学习特点

  • 多层非线性变换:深度学习模型由多个层次组成,每一层都应用非线性激活函数对输入数据进行变换。较低的层级通常捕捉到简单的特征(如边缘、颜色等),而更高的层级则可以识别更复杂的模式(如物体或面部识别)。
  • 自动特征提取:与传统机器学习算法不同,深度学习能够自动从原始数据中学习到有用的特征,而不需要人工特征工程。这使得深度学习在许多领域中表现出色。

  • 大数据和计算能力:深度学习模型通常需要大量的标注数据和强大的计算资源(如GPU)来进行训练。大数据和高性能计算使得深度学习在图像识别、自然语言处理等领域取得了显著突破。

  • 可解释性差:深度学习模型内部的运作机制相对不透明,被称为“黑箱”,这意味着理解模型为什么做出特定决策可能会比较困难。这对某些应用场景来说是一个挑战。

常见的深度学习模型        

  • 卷积神经网络 (Convolutional Neural Networks, CNN)
    • 主要用于图像处理任务,如图像分类、目标检测、图像分割等。
    • 特点是使用卷积层来自动提取图像中的局部特征,并通过池化层减少参数数量,提高计算效率。
  • 循环神经网络 (Recurrent Neural Networks, RNN)
    • 适用于处理序列数据,例如自然语言处理(NLP)、语音识别等。
    • R。NN具有记忆功能,可以处理输入数据的时间依赖性,但标准RNN难以捕捉长期依赖关系
  • 自编码器 (Autoencoders)
    • 一种无监督学习模型,通常用于降维、特征学习或者异常检测。
    • 自编码器由编码器和解码器两部分组成,前者将输入压缩成一个较低维度的表示,后者尝试从这个低维表示重建原始输入。
  • 生成对抗网络 (Generative Adversarial Networks, GAN)
    • 包含两个子网络:生成器和判别器。生成器负责创建看起来真实的假样本,而判别器则试图区分真假样本。
    • GAN广泛应用于图像生成、视频合成等领域。
  • Transformer
    • 主要用于自然语言处理(NLP)任务,尤其是机器翻译、文本生成等。
    • Transformer摒弃了传统的递归结构,采用自注意力机制(self-attention),使得它能够并行处理整个句子的信息,在机器翻译、文本摘要等任务中表现出色。

深度学习应用场景

==计算机视觉(Computer Vision)==

  • 图像分类:将图像分为不同的类别。常用于人脸识别、物体检测等。

    • 自动标注社交媒体照片、医疗影像中的病变检测。
  • 目标检测(Object Detection):在图像或视频中定位并分类多个对象。

    • 自动驾驶中的行人检测、监控视频中的入侵检测。
  • 面部识别:通过分析面部特征进行身份验证或分类。

    • 手机解锁、安防监控系统。
  • 图像生成:基于输入生成新的图像,如风格转换、图像超分辨率等。

    • 艺术风格迁移、老旧照片修复。

==自然语言处理(Natural Language Processing, NLP)==

  • 机器翻译:使用深度学习模型将一种语言的文本自动翻译成另一种语言。

    • Google翻译、实时语音翻译。
  • 情感分析:分析文本中的情感倾向,如正面、负面或中性。

    • 社交媒体监控、产品评论分析。
  • 文本生成:生成符合语法和语义的自然语言文本。

    • 自动写作助手、新闻生成。
  • 语音识别:将语音转化为文字。

    • 智能助手(如Siri、Alexa)、自动字幕生成。
  • 聊天机器人(Chatbot):通过深度学习理解用户输入并生成合理的回应。

    • 客服机器人、虚拟助手(如GPT类模型)。

==推荐系统(Recommendation Systems)==

  • 电影、音乐推荐:根据用户历史的评分和行为,推荐相关的电影、音乐或电视剧。
    • Netflix、Spotify的个性化推荐。
  • 电商推荐:根据用户的购买历史和浏览习惯推荐商品。
    • 亚马逊、淘宝的商品推荐系统。
  • 社交媒体推荐:分析用户的社交行为,推荐相关内容或朋友。
    • Facebook、Instagram的内容推荐。        

PyTorch框架简介

  • pytorch是深度学习的框架, python的第三方包, 数据是以张量类型存在
  • pytorch特点
    • 数据类型是张量类型
    • 自动微分模块, 自动求导/梯度
    • 可以在GPU/TPU/NPU上运行, 加速运行
    • 兼容各种平台 系统/硬件(显卡)
  • pytorch目前更新到2.5版本

张量创建

什么是张量

  • 张量是矩阵, 可以是多维
    • 0维->标量比如 5
    • 1维->比如[1 2 3 4 5]
    • 2维->比如[[1 2 3],[4 5 6]]
    • 3维...比如高维数组 → 比如彩色图像(高×宽×通道)
  • 张量是通过类创建出来的对象, 提供各种方法和属性
  •  张量 -> 存储同一类型元素的容器, 且元素值必须是 数值才可以.

  • 在深度学习中,所有数据(图像、文本、音频、表格)最终都会被编码成张量。

 基本创建方式

  • torch.tensor(data=): 指定数据

  • torch.Tensor(data=, size=): 指定数据或形状

  • torch.IntTensor(data=)/FloatTensor(): 指定数据

细节:

    Tensor方式较之于 tensor方式, 可以基于 形状来直接创建张量.

为什么 PyTorch 要使用张量

PyTorch 选择张量作为核心,主要有以下 四大优势

1. 统一的数据表示

  • 无论输入是数字、图像、文本还是时间序列,都可以用张量表示。
  • 模型参数(权重、偏置)本身也是张量。
  • 这使得整个计算流程(输入 → 计算 → 输出)都基于同一种数据结构,简洁高效。

2. 支持 GPU 加速

  • PyTorch 张量可以轻松在 CPU 和 GPU 之间切换:
  • x = torch.tensor([1, 2, 3])
    x_gpu = x.cuda()  # 移到 GPU

    GPU 擅长并行计算,而张量的批量操作(如矩阵乘法)正好可以并行化,极大提升训练速度

张量在 PyTorch 中的角色

功能 说明
数据载体 存储输入数据、模型参数、中间结果
计算单元 所有运算(加减乘除、卷积、矩阵乘)都在张量上进行
梯度容器 自动记录梯度,用于反向传播
硬件桥梁

无缝切换 CPU/GPU,利用硬件加速

PyTorch 的张量 = NumPy 数组 + GPU 支持 + 自动求导 + 深度学习优化

为什么会这样设计?

  • 数值稳定性:浮点数能更好地处理深度学习中的微小梯度

  • 硬件优化:GPU 对浮点运算有专门的优化

  • 数学基础:导数和微积分基于连续函数,需要浮点表示

  • 实际应用:模型参数通常需要小数点后多位精度

在深度学习中使用 PyTorch 时,需要计算梯度的张量必须是浮点数!

# 创建张量
x = torch.tensor([1, 2, 3])
y = torch.randn(3, 3)  # 随机张量
# 运算(类似 NumPy)
z = x + y[0] #(x:1+y[0]:3)
# 移到 GPU(如果有)
if torch.cuda.is_available():
    x = x.cuda()
# 自动求导(关键!)
a = torch.tensor(2.0, requires_grad=True)
b = a ** 2
b.backward()
print(a.grad)  # 输出 4.0
import torch.nn as nn  # 导入PyTorch的神经网络模块

class SimpleNet(nn.Module):  # 定义神经网络类,必须继承nn.Module
    def __init__(self):  # 初始化函数
        super().__init__()  # 调用父类初始化
        # 定义网络层
        self.fc1 = nn.Linear(784, 128)  # 全连接层1
        self.fc2 = nn.Linear(128, 10)   # 全连接层2
        self.relu = nn.ReLU()           # 激活函数

    def forward(self, x):  # 前向传播函数
        x = x.view(-1, 784)  # 将输入展平
        x = self.relu(self.fc1(x))  # 第一层+激活函数
        x = self.fc2(x)  # 第二层
        return x  # 返回输出

model = SimpleNet()  # 创建模型实例
print(model)  # 打印模型结构

前向传播流程

  1. 输入x(比如一批图片)

  2. 展平为784维向量

  3. 通过第一层:fc1(784→128)

  4. 通过ReLU激活函数

  5. 通过第二层:fc2(128→10)

  6. 返回10维输出(每个类别的得分

模型如何使用

# 创建模型
model = SimpleNet()

# 准备输入数据
# 假设batch_size=4,每个图片28×28
input_data = torch.randn(4, 1, 28, 28)  # 4张随机图片

# 前向传播
output = model(input_data)  # 自动调用forward方法
print(output.shape)  # 输出: torch.Size([4, 10])

# 解释输出:
# 4个样本,每个样本有10个数值
# 这10个数值对应0-9数字的"得分"
# 通常会用softmax转换为概率

演示PyTorch中如何创建 线性 和 随机张量.

涉及到的函数:

  •     torch.arange() 和 torch.linspace() 创建线性张量
  •     torch.random.initial_seed() 和 torch.random.manual_seed() 随机种子设置
  •     torch.rand/randn() 创建随机浮点类型张量
  •     torch.randint(low, high, size=()) 创建随机整数类型张量

 定义函数, 演示: 创建线性张量.随机张量

# 导包
import torch
def dm01():
    # 场景1: 创建指定范围的 线性张量.
    # 参1: 起始值, 参2: 结束值, 参3: 步长.
    t1 = torch.arange(0, 10, 2)     # 0, 2, 4, 6, 8
    print(f't1: {t1}, type: {type(t1)}')
    print('-' * 30)

    # 场景2: 创建指定范围的 线性张量 -> 等差数列.
    # 参1: 起始值, 参2: 结束值, 参3: 元素的个数
    t2 = torch.linspace(1, 10, 4)
    print(f't2: {t2}, type: {type(t2)}')
# 2. 定义函数, 演示: 创建随机张量.
# 2. 定义函数, 演示: 创建随机张量.
def dm02():
    # step1: 设置随机种子.
    # torch.initial_seed()    # 默认采用当前系统的时间戳作为随机种子.
    torch.manual_seed(3)      # 设置随机种子.

    # step2: 创建随机张量.
    # 场景1: 均匀分布的(0, 1) 随机张量
    t1 = torch.rand(size=(2, 3))
    print(f't1: {t1}, type: {type(t1)}')
    print('-' * 30)

    # 场景2: 符合正态分布的随机张量.
    t2 = torch.randn(size=(2, 3))
    print(f't2: {t2}, type: {type(t2)}')
    print('-' * 30)

    # 场景3: 创建随机整数张量.
    t3 = torch.randint(low=1, high=10, size=(3, 5))
    print(f't3: {t3}, type: {type(t3)}')

if __name__ == '__main__':
    # dm01()
    dm02()
3pyTorch创建指定类型的张量
import torch
# 场景1: 直接创建指定类型的张量.
t1 = torch.tensor([1, 2, 3, 4, 5], dtype=torch.float)  # 默认是: float32
# print(f't1: {t1}, (元素)类型: {t1.dtype}, (张量)类型: {type(t1)}')  # float32
# print('-' * 30)
# 场景2: 创建好张量后 -> 做类型转换.
# 思路1: type()函数, 推荐掌握.
t2 = t1.type(torch.int16)
print(f't2: {t2}, (元素)类型: {t2.dtype}, (张量)类型: {type(t2)}')  # int16

print(t2.half())        # float16
print(t2.float())       # float32, 默认
print(t2.double())      # float64
print(t2.short())       # int16
print(t2.int())         # int32
print(t2.long())        # int64, 默认
  • 常见的数据类型:

数据类型

说明

示例

torch.float32

32位浮点数

torch.float的别名

torch.float64

64位浮点数

torch.double的别名

torch.int32

32位整数

torch.int64

64位整数

torch.long的别名

torch.bool

布尔型

True/False

张量转换为NumPy数组

使用Tensor.numpy()函数可以将张量转换为ndarray数组,但是共享内存,可以使用copy()函数避免共享(Tensor.numpy().copy())

import torch
# 1. 将张量转换为 numpy 数组
data_tensor = torch.tensor([2, 3, 4])
# 使用张量对象中的 numpy 函数进行转换
# 使用张量对象中的 numpy 函数进行转换,通过copy方法拷贝对象
# data_numpy = data_tensor.numpy().copy()
data_numpy = data_tensor.numpy()
print(type(data_tensor))
print(type(data_numpy))
# 注意: data_tensor 和 data_numpy 共享内存
# 修改其中的一个,另外一个也会发生改变
# 使用张量对象中的 numpy 函数进行转换,通过copy方法拷贝对象则不会改变内容
# data_tensor[0] = 100
data_numpy[0] = 100
print(data_tensor)
print(data_numpy)

Numpy转换为张量

  • 使用 from_numpy 可以将 ndarray 数组转换为 Tensor,默认共享内存,使用 copy 函数避免共享。
  • 使用 torch.tensor 可以将 ndarray 数组转换为 Tensor,默认不共享内存。
  •  # 1. 创建numpy数组.
        n1 = np.array([11, 22, 33])
     # 2. 把上述的numpy数组, 转换成张量.
       t1 = torch.from_numpy(n1)               # 共享内存.
      t2 = torch.tensor(n1)                   # 不共享内存
        # 3. 演示上述方式 是否共享内存.
        n1[0] = 100
        print(f'n1: {n1}')  # 100, 22, 33
        print(f't1: {t1}')  # 100, 22, 33
        print(f't2: {t2}')  # 11, 22, 33

    从标量张量(只有1个值的张量) 中 提取其内容

  t1 = torch.tensor(100)
  # 2. 从张量中提取内容.
  a = t1.item()


演示张量的基本运算

张量的加减乘除

涉及到的API:

  • add(), sub(), mul(), div(), neg()       -> 加减乘除, 取反, substract, multiply, divide
  •  add_(), sub_(), mul_(), div_(), neg_()  -> 功能同上, 只不过可以修改源数据, 类似于 Pandas部分的 inplace = True

需要记忆的:

  •  可以用 +, -, *, / 符号来替代 上述的 加减乘除函数.
  •  如果是张量 和 数值运算, 则: 该数值会和张量中的每个值依次进行 对应的运算.
  • import torch
    # 1. 创建张量.
    t1 = torch.tensor([1, 2, 3])
    # 2. 演示 加法.
    # t2 = t1.add(10)      # 不会修改源数据.
    # t2 = t1 + 10         # 效果同上.
    # t1.add_(10)            # 会修改源数据.
    # t1 += 10               # 效果同上
    # 其它函数效果同上, 这里就快速演示了.
    # t2 = t1.sub(1)      # 减法
    # t2 = t1.mul(2)      # 乘法
    # t2 = t1.div(2)      # 除法, [0.5000, 1.0000, 1.5000]
    t2 = t1.neg()         # 取反
    # 3. 打印结果.
    print(f't1: {t1}')
    print(f't2: {t2}')

    张量点乘 和 矩阵乘法

  • 点乘:要求: 两个张量的维度(行列)保持一致, 对应元素直接做 乘法操作

  •   API:   t1 * t2   t1.mul(t2)          # multiply: 乘法

  • # t1 = torch.tensor([[1, 2, 3], [4, 5, 6]])
    # t2 = torch.tensor([[1, 2, 3], [4, 5, 6]])
    # t3 = t1.mul(t2)
    # print(f't3: {t3}')
    # t3: tensor([[ 1,  4,  9],
    #         [16, 25, 36]])
  • 矩阵乘法:要求: 两个张量, 第一个张量 的列数, 等于 第二个张量 的行数(A列 = B行)

  •   API:    t1 @ t2         t1.matmul(t2)         t1.dot(t2)          扩展: 只针对于一维张量有效

  • # 1. 定义张量, 2行3列.
    t1 = torch.tensor([[1, 2, 3], [4, 5, 6]])
    # 2. 定义张量, 3行2列.
    t2 = torch.tensor([[1, 2], [3, 4], [5, 6]])
    t3 = t1 @ t2
    print(f't3: {t3}')
    
    #t3: tensor([[22, 28],
    #        [49, 64]])

  • 具体计算过程 

  • T1的第一行[1,2,3] 和T2的第一列[1,3,5] 点乘 = 1*1+2*3+3*5 = 22,和T2的第二列1*2+2*4+3*6 = 28 故[[22,28],]
  • T1的第二行[4,5,6] 和T2的第一列[2,4,6] 点乘 = 4*1+5*3+6*5 = 49,和T2的第二列4*2+5*4+6*6 = 28 故[[49,64]] 
  • t3: tensor([[22, 28],[49, 64]])
  • 计算过程图示:
    t3[0,0] = 1×1 + 2×3 + 3×5 = 22
    t3[0,1] = 1×2 + 2×4 + 3×6 = 28
    t3[1,0] = 4×1 + 5×3 + 6×5 = 49
    t3[1,1] = 4×2 + 5×4 + 6×6 = 64

    矩阵乘法是深度学习的核心运算,神经网络中的每一层都可以看作是一个矩阵乘法运算。理解矩阵乘法是理解深度学习如何工作的基础!

 dot()函数

  • 用于一维向量

  • 对应元素相乘后求和

  • 结果是标量(单个数字)

t4 = torch.tensor([1, 2, 3])
t5 = torch.tensor([4, 5, 6])
t6 = t4.dot(t5)
print(f't6: {t6}')


对应元素相乘:
1 × 4 = 4
2 × 5 = 10
3 × 6 = 18

求和:4 + 10 + 18 = 32
  • 点积用于一维向量,结果是标量

  • 矩阵乘法用于矩阵,结果是矩阵

张量的运算函数

涉及到的 API(函数) 如下:

  • sum(), max(), min(), mean()                     -> 都有 dim 参数, 0表示列, 1表示行
  • pow(), sqrt(), exp(), log(), log2(), log10()    -> 没有dim参数

掌握的函数:

    sum(), max(), min(), mean()

import torch
t1 = torch.tensor([
    [1, 2, 3],
    [4, 5, 6]
], dtype=torch.float)
print(f't1: {t1}')
# 2. 演示 有dim参数的 函数.
# sum() 求和
print(t1.sum(dim=0))        # 按 列 求和
print(t1.sum(dim=1))        # 按 行 求和
print(t1.sum())             # 整 体 求和
print('-' * 30)

# max()求最大值, min()同理, 这里就不演示了.
print(t1.max(dim=0))        # 按 列 求最大值
print(t1.max(dim=1))        # 按 行 求最大值
print(t1.max())             # 整 体 求最大值
print('-' * 30)
# mean(), 计算平均值
print(t1.mean(dim=0))        # 按 列 求平均值
print(t1.mean(dim=1))        # 按 行 求平均值
print(t1.mean())             # 整 体 求平均值
print('*' * 30)

# 3. 演示 没有dim参数的 函数.
# pow() n次幂
print(t1.pow(2))            # 每个数的平方
print(t1.pow(3))            # 每个数的立方
print(t1 ** 3)              # 效果同上.
# sqrt() 平方根
print(t1.sqrt())            # 每个数的平方根
print('-' * 30)

# exp() e的n次幂,  n就是矩阵中的每个元素, 这里是: e^1, e^2, e^3, e^4, e^5, e^6
print(t1.exp())
print('-' * 30)
# log(), log2(), log10()  对数
print(t1.log())
print(t1.log2())
print(t1.log10())

张量的索引操作

张量索引是操作和访问张量数据的重要方式。PyTorch 的索引语法与 NumPy 和 Python 列表类似,但功能更强大。

1. 基本索引

1.1 一维张量索引
import torch
# 创建一维张量
t = torch.tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
# 单个元素索引
print(f"t[0]: {t[0]}")      # 第一个元素: 0
print(f"t[-1]: {t[-1]}")    # 最后一个元素: 9
print(f"t[5]: {t[5]}")      # 第6个元素: 5
# 范围切片
print(f"t[2:5]: {t[2:5]}")    # 索引2到4: [2, 3, 4]
print(f"t[:3]: {t[:3]}")      # 前3个: [0, 1, 2]
print(f"t[5:]: {t[5:]}")      # 从索引5开始: [5, 6, 7, 8, 9]
print(f"t[::2]: {t[::2]}")    # 步长为2: [0, 2, 4, 6, 8]
print(f"t[::-1]: {t[::-1]}")  # 反转: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0
1.2 多维张量索引
# 创建3×4矩阵
t = torch.tensor([
    [0, 1, 2, 3],
    [4, 5, 6, 7],
    [8, 9, 10, 11]
])
print(f"原始矩阵:\n{t}")

# 访问单个元素
print(f"t[0, 0]: {t[0, 0]}")    # 第0行第0列: 0
print(f"t[2, 3]: {t[2, 3]}")    # 第2行第3列: 11
print(f"t[1, -1]: {t[1, -1]}")  # 第1行最后一列: 7
# 获取整行
print(f"t[0]: {t[0]}")      # 第0行: [0, 1, 2, 3]
print(f"t[-1]: {t[-1]}")    # 最后一行: [8, 9, 10, 11]
# 获取整列
print(f"t[:, 0]: {t[:, 0]}")  # 第0列: [0, 4, 8]
print(f"t[:, -1]: {t[:, -1]}")  # 最后一列: [3, 7, 11]
# 获取子矩阵
print(f"t[:2, :2]:\n{t[:2, :2]}")  # 前2行前2列

2. 高级索引

2.1 布尔索引
t = torch.tensor([1, 2, 3, 4, 5, 6])
# 创建布尔掩码
mask = t > 3
print(f"掩码: {mask}")  # [False, False, False, True, True, True]
# 二维布尔索引
t2d = torch.tensor([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])
mask2d = t2d > 5
print(f"大于5的元素: {t2d[mask2d]}")  # [6, 7, 8, 9]
2.2 整数数组索引
t = torch.tensor([10, 20, 30, 40, 50])

# 使用列表索引
indices = [0, 2, 4]
print(f"t[indices]: {t[indices]}")  # [10, 30, 50]
# 使用张量索引
indices_tensor = torch.tensor([1, 3, 0])
print(f"t[indices_tensor]: {t[indices_tensor]}")  # [20, 40, 10]
# 多维数组索引
t2d = torch.arange(12).reshape(3, 4)
print(f"原始矩阵:\n{t2d}")
2.3 组合索引
t = torch.arange(12).reshape(3, 4)
print(f"原始矩阵:\n{t}")

# 组合使用
# 获取第0、2行的第1、3列
result = t[[0, 2], :][:, [1, 3]]
print(f"组合索引结果:\n{result}")
# [[1, 3],
#  [9, 11]]

3 使用 torch.gather

t = torch.tensor([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])

# 在每行中按索引收集元素
indices = torch.tensor([
    [0, 2, 1],
    [1, 0, 2],
    [2, 1, 0]
])

# dim=1 表示在列方向收集
result = torch.gather(t, dim=1, index=indices)
print(f"收集结果:\n{result}")
# [[1, 3, 2],  # 第0行: 取[0,2,1]列
#  [5, 4, 6],  # 第1行: 取[1,0,2]列
#  [9, 8, 7]]  # 第2行: 取[2,1,0]列

4. 修改值

4.1 直接赋值
t = torch.zeros(3, 4)
print(f"初始:\n{t}")
# 修改单个元素
t[0, 0] = 1.0
print(f"修改[0,0]后:\n{t}")
# 修改整行
t[1] = torch.tensor([2.0, 2.0, 2.0, 2.0])
print(f"修改第1行后:\n{t}")
# 修改整列
t[:, 2] = torch.tensor([3.0, 3.0, 3.0])
print(f"修改第2列后:\n{t}")
# 修改子矩阵
t[:2, :2] = torch.tensor([[4.0, 5.0], [6.0, 7.0]])
print(f"修改子矩阵后:\n{t}")

张量形状操作

  • 张量形状操作是指对张量的维度进行变换的一系列操作。
  • 张量的形状则描述了每个维度上的元素数量。
  • reshape

    保证张量数据不变的前提下改变数据的维度

import torch
data = torch.tensor([[10, 20, 30], [40, 50, 60]])
# 1. 使用 shape 属性或者 size 方法都可以获得张量的形状
print(data.shape)
print(data.size())
# 2. 使用 reshape 方法可以改变张量的形状
print(data.reshape(3, 2))
# 3. 使用 view 方法也可以改变张量的形状
print(data.view(3, 2))

torch.Size([2, 3])
torch.Size([2, 3])
tensor([[10, 20],
        [30, 40],
        [50, 60]])
tensor([[10, 20],
        [30, 40],
        [50, 60]])

squeeze和unsqueeze

squeeze:删除指定位置形状为1的维度,不指定位置删除所有形状为1的维度,==降维==

unsqueeze:在指定位置添加形状为1的维度,==升维==

import torch
mydata1 = torch.tensor([1, 2, 3, 4, 5])
print('原始张量:')
print('值:', mydata1)
print('形状:', mydata1.shape)
print('维度数:', mydata1.dim())
原始张量:
值: tensor([1, 2, 3, 4, 5])
形状: torch.Size([5])
维度数: 1
# 查看每个操作后的形状
mydata2 = mydata1.unsqueeze(0)
print('unsqueeze(0)后:')
print('形状:', mydata2.shape)  # (1, 5)
print('值:\n', mydata2)
print()
#unsqueeze(0)后:
#形状: torch.Size([1, 5])
#值:
# tensor([[1, 2, 3, 4, 5]])
mydata3 = mydata1.unsqueeze(1)
print('unsqueeze(1)后:')
print('形状:', mydata3.shape)  # (5, 1)
print('值:\n', mydata3)
print()
#unsqueeze(1)后:
#形状: torch.Size([5, 1])
#值:
# tensor([[1],
#        [2],
#        [3],
#        [4],
#        [5]])

unsqueeze(0)→ 在开头加1,形状从 (n,)变成 (1, n)

unsqueeze(1)→ 在结尾加1,形状从 (n,)变成 (n, 1)

 演示: unsqueeze()函数, squeeze()函数.

    # 1. 定义2行3列的张量.
    t1 = torch.randint(1, 10, size=(2, 3))
    print(f't1: {t1}, shape: {t1.shape}')       # (2, 3)

    # 2. 在0维上, 添加一个维度.
    t2 = t1.unsqueeze(0)
    print(f't2: {t2}, shape: {t2.shape}')       # (1, 2, 3)

    # 3. 在1维上, 添加一个维度.
    t3 = t1.unsqueeze(1)
    print(f't3: {t3}, shape: {t3.shape}')       # (2, 1, 3)

    # 4. 在2维上, 添加一个维度.
    t4 = t1.unsqueeze(2)
    print(f't4: {t4}, shape: {t4.shape}')       # (2, 3, 1)

    # 5. 在3微上(不存在), 添加一个维度.
    # t5 = t1.unsqueeze(3)        # 报错, 越界.
    # print(f't5: {t5}, shape: {t5.shape}')       # (2, 3, ???, 1)

    # 6. 删除所有为1的维度.
    t6 = torch.randint(1, 10, size=(2, 1, 3, 1, 1))
    print(f't6: {t6}, shape: {t6.shape}')        # (2, 1, 3, 1, 1)

    t7 = t6.squeeze()
    print(f't7: {t7}, shape: {t7.shape}')        # (2, 3)

transpose和permute

transpose:实现交换张量形状的指定维度, 例如: 一个张量的形状为 (2, 3, 4) ,把 3 和 4 进行交换, 将张量的形状变为 (2, 4, 3)

permute:一次交换更多的维度

t1 = torch.randint(1, 10, size=(2, 3, 4)) # 2个3×4矩阵 可以理解为:2个"页"
,每页是3行4列的矩阵
print(f"原始张量:\n{t1}")
原始张量:
tensor([[[6, 1, 8, 6],
         [9, 7, 1, 9],
         [5, 7, 5, 8]],

        [[3, 7, 7, 3],
         [5, 7, 8, 2],
         [4, 7, 7, 3]]])
# 2. 改变维度从 (2, 3, 4) -> (4, 3, 2)
t2 = t1.transpose(0, 2)
print(f"transpose(0, 2)后的张量:\n{t2}") 
# 记忆 2页 每页的3行 4 列(2,3,4)
                          ↓  ↓  ↓
                          0  1  2
所有把0和2的位置改变。使用t2 = t1.transpose(0, 2) 一共0  1  2
                                           ↓  ↓     ↓  ↓  ↓
                                           2  4     2  3  4
                          
transpose(0, 2) 就是把2和4更换一下 变成 4页 3行 2列
# 一次交换更多的维度
mydata6 = t1.permute([1, 2, 0]) # 3 4 2 =>3页4行两列
print(f"permute([1, 2, 0])后的张量:\n{mydata6}")

view和contiguous

view函数也可以用于修改张量的形状,只能用于修改连续的张量。在PyTorch中,有些张量的底层数据在内存中的存储顺序与其在张量中的逻辑顺序不一致,view函数无法对这样的张量进行变形处理,例如: 一个张量经过了 transpose 或者 permute 函数的处理之后,就无法使用 view 函数进行形状操作。

  • contiguous:将不连续张量转为连续张量
  • is_contiguous:判断张量是否连续,返回True或False
# 1 一个张量经过了 transpose 或者 permute 函数的处理之后,就无法使用 view 函数进行形状操作
# 若要使用view函数, 需要使用contiguous() 变成连续以后再使用view函数
# 2 判断张量是否连续
data = torch.tensor([[10, 20, 30],[40, 50, 60]])
print('data--->', data, data.shape)
# 1 判断张量是否连续
print(data.is_contiguous()) # True
# 2 view
mydata2 = data.view(3, 2)
print('mydata2--->', mydata2, mydata2.shape)
# 3 判断张量是否连续
print('mydata2.is_contiguous()--->', mydata2.is_contiguous())
# 4 使用 transpose 函数修改形状
mydata3 = torch.transpose(data, 0, 1)  
print('mydata3--->', mydata3, mydata3.shape)
print('mydata3.is_contiguous()--->', mydata3.is_contiguous())
# 5 需要先使用 contiguous 函数转换为连续的张量,再使用 view 函数
print (mydata3.contiguous().is_contiguous())
mydata4 = mydata3.contiguous().view(2, 3)
print('mydata4--->', mydata4.shape, mydata4)

张量拼接操作

张量拼接操作用于组合来自不同来源或经过不同处理的数据。

cat/concat

沿着现有维度连接一系列张量。所有输入张量除了指定的拼接维度外,其他维度必须匹配。

data1 = torch.randint(0, 10, [1, 2, 3])
data2 = torch.randint(0, 10, [1, 2, 3])
print(data1)
print(data2)
# 1. 按0维度拼接
new_data = torch.cat([data1, data2], dim=0)
print(new_data)
print(new_data.shape)
# 2. 按1维度拼接
new_data = torch.cat([data1, data2], dim=1)
print(new_data)
print(new_data.shape)
# 3. 按2维度拼接
new_data = torch.cat([data1, data2], dim=2)
print(new_data)
print(new_data.shape)

不同维度的拼接总结

  • 对于3D张量 (页, 行, 列)

    维度

    拼接方向

    形状变化

    实际效果

    dim=0

    页/深度方向

    (2,3,4) + (2,3,4) → (4,3,4)

    增加页面数

    dim=1

    行/垂直方向

    (2,3,4) + (2,3,4) → (2,6,4)

    增加行数

    dim=2

    列/水平方向

    (2,3,4) + (2,3,4) → (2,3,8)

    增加列数

stack

在一个新的维度上连接一系列张量,这会增加一个新维度,并且所有输入张量的形状必须完全相同。

# cat: 不增加维度
cat_result = torch.cat([A, B], dim=0)
print(cat_result)
print(f"cat 结果形状: {cat_result.shape}")  # (6,) ← 不增加维度

# stack: 增加一个维度
stack_result = torch.stack([A, B], dim=0)
print(stack_result)
print(f"stack 结果形状: {stack_result.shape}")  # (2, 3) ← 增加一个维度
tensor([1, 2, 3, 4, 5, 6])
cat 结果形状: torch.Size([6])
tensor([[1, 2, 3],
        [4, 5, 6]])
stack 结果形状: torch.Size([2, 3])

自动微分模块

自动微分就是自动计算梯度值,也就是计算导数。

  • 什么是梯度
    • 对函数求导的值就是梯度
  • 什么是梯度下降法
    • 是一种求最优梯度值的方法,使得损失函数的值最小
  • 梯度经典语录
    • 对函数求导得到的值就是梯度 (在数值上的理解)
      • 在某一个点上,对函数求导得到的值就是该点的梯度
      • 没有点就无法求导,没有梯度
    • 梯度就是上山下山最快的方向 (在方向上理解)
    • 在平面内,梯度就是某一点上的斜率
      • y = 2x^2 某一点x=1的梯度,就是这一点上的斜率
    • 反向传播传播的是梯度
      • 反向传播利用链式法则不断的从后向前求导,求出来的值就是梯度,所以大家都经常说反向传播传播的是梯度
    • 链式法则中,梯度相乘,就是传说中的梯度传播
  • 训练神经网络时,最常用的算法就是反向传播。在该算法中,参数(模型权重)会根据损失函数关于对应参数的梯度进行调整。为了计算这些梯度,PyTorch内置了名为 torch.autograd 的微分模块。它支持任意计算图的自动梯度计算: 
  • W新 = W旧 - 学习率 * 梯度 
  • 梯度 = 损失函数的导数

mse 均方误差,loss 对其求导用手动算自动微分算求w结合反向传播

梯度基本计算

  • ==pytorch不支持向量张量对向量张量的求导,只支持标量张量对向量张量的求导==
    • x如果是张量,y必须是标量(一个值)才可以进行求导
  • 计算梯度: y.backward(), y是一个标量
  • 获取x点的梯度值: x.grad, 会累加上一次的梯度值
  • 标量张量梯度计算
  • # 定义一个标量张量(点)
    # requires_grad=:默认为False,不会自动计算梯度;为True的话是将自动计算的梯度值保存到grad中
    x = torch.tensor(10, requires_grad=True, dtype=torch.float32)
    print("x-->", x)
    
    # 定义一个曲线
    y = 2 * x ** 2
    print("y-->", y)
    # 查看梯度函数类型,即曲线函数类型
    print(y.grad_fn)
    
    # 计算x点的梯度
    # 此时y是一个标量,可以不用使用y.sum()转换成标量
    print("y.sum()-->", y.sum()) # 2*2x²=2*2x= 4x = 40
    # y'|(x=10) = (2*x**2)'|(x=10) = 4x|(x=10) = 40
    y.sum().backward()
    
    # 打印x的梯度值
    print("x的梯度值是:", x.grad) #40

    向量张量梯度计算

  • # 定义一个向量张量(点)
    x = torch.tensor([10, 20], requires_grad=True, dtype=torch.float32)
    print("x-->", x)
    
    # 定义一个曲线
    y = 2 * x ** 2
    print("y-->", y)
    
    # 计算梯度
    # x和y都是向量张量,不能进行求导,需要将y转换成标量张量-->y.sum()
    # y'|(x=10) = (2*x**2)'|(x=10) = 4x|(x=10) = 40
    # y'|(x=20) = (2*x**2)'|(x=20) = 4x|(x=20) = 80
    y.sum().backward()
    
    # 打印x的梯度
    print("x.grad-->", x.grad)

梯度下降法求最优解

# 求 y = x**2 + 20 的极小值点 并打印y是最小值时 w的值(梯度)
# 1 定义点 x=10 requires_grad=True  dtype=torch.float32
# 2 定义函数 y = x**2 + 20
# 3 利用梯度下降法 循环迭代1000 求最优解
# 3-1 正向计算(前向传播)
# 3-2 梯度清零 x.grad.zero_()
# 3-3 反向传播
# 3-4 梯度更新 x.data = x.data - 0.01 * x.grad
# 1 定义点x=10 requires_grad=True  dtype=torch.float32
# 1. 定义点 w=10 requires_grad=True  dtlosspe=torch.float32
w = torch.tensor(10, requires_grad=True, dtype=torch.float32)
# 2. 定义函数 loss = w**2 + 20
loss = w ** 2 + 20          # 求导: loss' = 2w
# 3. 利用梯度下降法 循环迭代100 求最优解
for i in range(1, 101):
    # 3.1 正向计算(前向传播)
    loss = w ** 2 + 20
    # 3.2 梯度清零 w.grad.zero_()   默认梯度会累加.
    # 至此(第一次的时候), 还没有计算梯度, 所以w.grad = None, 要做非空判断.
    if w.grad is not None:
        w.grad.zero_()

    # 3.3 反向传播
    loss.sum().backward()
    # 3.4 梯度更新 w.data = w.data - 0.01 * w.grad
    # print(f'梯度值为: {w.grad}')
    w.data = w.data - 0.01 * w.grad
    # 3.5 打印本次 梯度更新后的 权重参数结果.
    print(f'第 {i} 次, 权重初始值: {w}, (0.01 * w.grad): {0.01 * w.grad:.5f}, loss: {loss:.5f}')

# 4. 打印最终结果.
print(f'最终结果 权重: {w}, 梯度: {w.grad:.5f}, loss: {loss:.5f}')

梯度计算注意点

  • 不能将自动微分的张量转换成numpy数组,会发生报错,可以通过detach()方法实现
  • # 定义一个张量
    # 定义一个张量
    x1 = torch.tensor([10, 20], requires_grad=True, dtype=torch.float64)
    
    # 将x张量转换成numpy数组
    # 发生报错,RuntimeError: Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.
    # 不能将自动微分的张量转换成numpy数组
    print(x1.numpy())
    # 通过detach()方法产生一个新的张量,作为叶子结点
    # x2 = x1.detach()
    # # x1和x2张量共享数据,但是x2不会自动微分
    print(x1.requires_grad) # True
    print(x2.requires_grad) # False
    # # x1和x2张量的值一样,共用一份内存空间的数据
    print(x1.data) # tensor([10., 20.], dtype=torch.float64)
    print(x2.data) # tensor([10., 20.], dtype=torch.float64)
    print(id(x1.data)) # 2330103827168
    print(id(x2.data)) # 2330103827168
    # # 将x2张量转换成numpy数组
    print(x2.numpy()) # [10. 20.]
    

    自动微分模块应用

    • # 自动微分真实场景应用
      # 输入张量 2*5
      x = torch.ones(2, 5)
      # 目标值是 2*3
      y = torch.zeros(2, 3)
      # 设置要更新的权重和偏置的初始值
      w = torch.randn(5, 3, requires_grad=True) # a行 b列 X(2,5) * W(5,3) = Y(2,3) xw+b=y
      b = torch.randn(3, requires_grad=True)
      # x: (batch, m)
      # W: (m, n)
      # b: (n,)
      # z: (batch, n)
      # 设置网络的输出值
      z = torch.matmul(x, w) + b  # 矩阵乘法
      # # 设置损失函数,并进行损失的计算
      loss = torch.nn.MSELoss()
      loss = loss(z, y)
      # # 自动微分
      loss.backward()
      # # 打印 w,b 变量的梯度
      # backward 函数计算的梯度值会存储在张量的 grad 变量中
      print("W的梯度:", w.grad)
      print("b的梯度", b.grad)

      PyTorch构建线性回归模型

  • 我们使用 PyTorch 的各个组件来构建线性回归模型。在pytorch中进行模型构建的整个流程一般分为四个步骤:
  • 准备训练集数据
  • 构建要使用的模型
  • 设置损失函数和优化器
  • 模型训练

要使用的API:

  • 使用 PyTorch 的 nn.MSELoss() 代替平方损失函数
  • 使用 PyTorch 的 data.DataLoader 代替数据加载器
  • 使用 PyTorch 的 optim.SGD 代替优化器
  • 使用 PyTorch 的 nn.Linear 代替假设函数
# 导入相关模块
import torch
from torch.utils.data import TensorDataset  # 构造数据集对象
from torch.utils.data import DataLoader  # 数据加载器
from torch import nn  # nn模块中有平方损失函数和假设函数
from torch import optim  # optim模块中有优化器函数
from sklearn.datasets import make_regression  # 创建线性回归模型数据集
import matplotlib.pyplot as plt  # 可视化
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号
# 1. 定义函数, 创建线性回归样本数据.
def create_dataset():
    # 1. 创建数据集对象.
    x, y, coef = make_regression(
        n_samples=100,  # 100条样本(100个样本点)
        n_features=1,  # 1个特征(1个特征点)
        noise=10,  # 噪声, 噪声越大, 样本点越散, 噪声越小, 样本点越集中
        coef=True,  # 是否返回系数, 默认为False, 返回值为None
        bias=14.5,  # 偏置
        random_state=3  # 随机种子, 随机种子相同, 输出数据相同
    )
    # 2. 把上述的数据, 封装成 张量对象.
    x = torch.tensor(x, dtype=torch.float32)
    y = torch.tensor(y, dtype=torch.float32)
    # 3. 返回结果.
    return x, y, coef
# 2. 定义函数, 表示模型训练.
def train(x, y, coef):
    # 1. 创建数据集对象. 把 tensor -> 数据集对象 -> 数据加载器.
    dataset = TensorDataset(x, y)
    # 2. 创建数据加载器对象.
    # 参1: 数据集对象, 参2: 批次大小, 参3: 是否打乱数据(训练集打乱, 测试集不打乱)
    dataloader = DataLoader(dataset, batch_size=16, shuffle=True)
    # 3. 创建初始的 线性回归模型.
    # 参1: 输入特征维度, 参2: 输出特征维度.
    model = nn.Linear(1, 1)
    # 4. 创建损失函数对象.
    criterion = nn.MSELoss()
    # 5. 创建优化器对象.
    # 参1: 模型参数, 参2: 学习率.
    optimizer = optim.SGD(model.parameters(), lr=0.01)
    # 6. 具体的训练过程.=
    # 6.1 定义变量, 分别表示: 训练轮数, 每轮的(平均)损失值, 训练总损失值, 训练的样本数.
    epochs, loss_list, total_loss, total_sample = 100, [], 0.0, 0
    # 6.2 开始训练, 按轮训练.
    for epoch in range(epochs):  # epoch的值: 0, 1, 2...99
        # 6.3 每轮是分 批次 训练的, 所以从 数据加载器中 获取 批次数据.
        for train_x, train_y in dataloader:  # 7批(16, 16, 16, 16, 16, 16, 4)
            # 6.4 模型预测.
            y_pred = model(train_x)
            # 6.5 计算(每批的平均)损失值.
            loss = criterion(y_pred, train_y.reshape(-1, 1))  # -1 自动计算.
            # 6.6 计算总损失 和 样本(批次)数
            total_loss += loss.item()
            total_sample += 1
            # 6.7 梯度清零 + 反向传播 + 梯度更新.
            optimizer.zero_grad()  # 梯度清零
            loss.backward()  # 反向传播, 计算梯度
            optimizer.step()  # 梯度更新
        # 6.8 把本轮的(平均)损失值, 添加到列表中.
        loss_list.append(total_loss / total_sample)
        print(f'轮数: {epoch + 1}, 平均损失值: {total_loss / total_sample}')

    # 7. 打印(最终的)训练结果.
    print(f'{epochs} 轮的平均损失分别为: {loss_list}')
    print(f'模型参数, 权重: {model.weight}, 偏置: {model.bias}')
    # 8. 绘制损失曲线.
    #                 100轮         每轮的平均损失值
    plt.plot(range(epochs), loss_list)
    plt.title('损失值曲线变化图')
    plt.grid()      # 绘制网格线
    plt.show()
    # 9. 绘制预测值和真实值的关系.
    # 9.1 绘制样本点分布情况.
    plt.scatter(x, y)
    # 9.2 绘制训练模型的预测值.
    # x: 100个样本点的特征.
    y_pred = torch.tensor(data = [v * model.weight + model.bias for v in x])
    # 9.3 计算真实值.
    y_true = torch.tensor(data = [v * coef + 14.5 for v in x])
    # 9.4 绘制预测值 和 真实值的 折线图.
    plt.plot(x, y_pred, color='red', label='预测值')
    plt.plot(x, y_true, color='green', label='真实值')
    # 9.5 图例, 网格.
    plt.legend()
    plt.grid()
    # 9.6 显示图像.
    plt.show()
    plt.show()

# 3. 测试.
if __name__ == '__main__':
    # 3.1 创建数据集.
    x, y, coef = create_dataset()
    # print(f'x: {x}, y: {y}, coef: {coef}')
    # 3.2 模型训练.
    train(x, y, coef)

神经网络基础

神经网络

深度学习神经网络就是大脑仿生,数据从输入到输出经过一层一层的神经元产生预测值的过程就是==前向传播(也叫正向传播)==

神经网络概念

什么是神经网络

人工神经网络(Artificial Neural Network, 简写为ANN)也简称为神经网络(NN),是一种模仿生物神经网络和结构的计算模型,它又多个相互链接的神经元(也成节点)构成,可以用于和学习复杂的数据,尤其适合解释非线性问题,人工神经网络是机器学习的一个重要模型,尤其深度学习中得到了广泛的应用

人脑可以看做是一个生物神经网络,由众多的神经元连接而成。各个神经元传递复杂的电信号,树突接收到输入信号,然后对信号进行处理,通过轴突输出信号。下图是生物神经元示意图:

当电信号通过树突进入到细胞核时,会逐渐聚集电荷。达到一定的电位后,细胞就会被激活,通过轴突发出电信号

如何构建神经网络

神经网络是由多个神经元组成构建神经网络就是在构建神经元。以下是神经网络中神经元的构建说明

这个流程就像,来源不同树突(树突都会有不同的权重)的信息, 进行的加权计算, 输入到细胞中做加和,再通过激活函数输出细胞值。

同一层的多个神经元可以看作是通过并行计算来处理相同的输入数据,学习输入数据的不同特征。每个神经元可能会关注输入数据中的不同部分,从而捕捉到数据的不同属性。

接下来,我们使用多个神经元来构建神经网络,相邻层之间的神经元相互连接,并给每一个连接分配一个强度,如下图所示:        

平时搭网络,隐藏层默认可选 ReLU;遇到不同类型的预测任务,在最后输出层根据是“二分类”、“多分类”还是“回归预测”来针对性地选择 Sigmoid、Softmax 或不加激活函数即可。

隐藏层里找 ReLU,又快又稳省时间;

二分类要概率,Sigmoid 靠得住;

多分类争输赢,Softmax 来定乾坤;

若是碰到 RNN,Tanh 或许能顶阵。

神经网络中信息只向一个方向移动,即从输

入节点向前移动,通过隐藏节点,再向输出节点移动。其中的基本部分是:

  1. 输入层(Input Layer): 即输入x的那一层(如图像、文本、声音等)。每个输入特征对应一个神经元。输入层将数据传递给下一层的神经元。
  2. 输出层(Output Layer): 即输出y的那一层。输出层的神经元根据网络的任务(回归、分类等)生成最终的预测结果。
  3. 隐藏层(Hidden Layers): 输入层和输出层之间都是隐藏层,神经网络的“深度”通常由隐藏层的数量决定。隐藏层的神经元通过加权和激活函数处理输入,并将结果传递到下一层。

特点是:

  • 同一层的神经元之间没有连接
  • 第N层的每个神经元和第N-1层的所有神经元相连(这就是Fully Connected的含义),这就是全连接神经网络(FCNN)
  • 全连接神经网络接收的样本数据是二维的,数据在每一层之间需要以二维的形式传递
  • 第N-1层神经元的输出就是第N层神经元的输入
  • 每个连接都有一个权重值(w系数和b系数)
  • 神经网络内部状态值和激活值
  • 每一个神经元工作时,前向传播会产生两个值,内部状态值(加权求和值)激活值反向传播时会产生激活值梯度内部状态值梯度
  • 内部状态值

    • 神经元或隐藏单元的内部存储值,它反映了当前神经元接收到的输入、历史信息以及网络内部的权重计算结果。
    •  每个输入 xi

    • 都有一个与之相乘的权重 wi​​

    • 表示每个输入信号的重要性。

    • z=w⋅x+b
      • w:权重矩阵
      • x:输入值
      • b:偏置
    • ​激活值

    • 通过激活函数(如 ReLU、Sigmoid、Tanh)对内部状态值进行非线性变换后得到的结果。激活值决定了当前神经元的输出。
    • a=f(z)
      • f:激活函数
      • z:内部状态值
  • 通过控制每个神经元的内部状态值、激活值的大小;每一层的内部状态值的方差、每一层的激活值的方差可让整个神经网络工作的更好。

激活函数

  • 激活函数用于对每层的输出数据进行变换, 进而为整个网络注入了非线性因素。此时, 神经网络就可以拟合各种曲线。如果不使用激活函数,整个网络虽然看起来复杂,其本质还相当于一种线性模型,如下公式所示:
网络非线性因素理解
  • 没有引入非线性因素的网络等价于使用一个线性模型来拟合

  • 通过给网络输出增加激活函数, 实现引入非线性因素, 使得网络模型可以逼近任意函数, 提升网络对复杂问题的拟合能力

  • 活函数用于对每层的输出数据进行变换, 进而为整个网络注入了非线性因素。此时, 神经网络就可以拟合各种曲线。如果不使用激活函数,整个网络虽然看起来复杂,其本质还相当于一种线性模型如下公式所示:

  • 另外通过图像可视化的形式理解:
  • 神经网络可视化
  • 发现增加激活函数之后, 对于线性不可分的场景,神经网络的拟合能力更强。
常见激活函数

激活函数主要用来向神经网络中加入非线性因素,以解决线性模型表达能力不足的问题,它对神经网络有着极其重要的作用。我们的网络参数在更新时,使用的反向传播算法(BP),这就要求我们的激活函数必须可微。

Sigmoid 激活函数

激活函数公式:

激活函数求导公式:

sigmoid 激活函数的函数图像如下:

  • 从sigmoid函数图像可以得到,sigmoid 函数可以将任意的输入映射到 (0, 1) 之间,当输入的值大致在**<-6或者>6**时,意味着输入任何值得到的激活值都是差不多的,这样会丢失部分的信息。比如:输入100和输入10000经过 sigmoid的激活值几乎都是等于1的,但是输入的数据之间相差100倍的信息就丢失了。

  • 对于sigmoid函数而言,输入值在**[-6, 6]之间输出值才会有明显差异,输入值在**[-3, 3]之间才会有比较好的效果

  • 通过上述导数图像,我们发现导数数值范围是 (0, 0.25),当输入的值**<-6或者>6时,sigmoid激活函数图像的导数接近为 0**,此时网络参数将更新极其缓慢,或者无法更新

  • 一般来说,sigmoid网络在5层之内就会产生梯度消失现象。而且,该激活函数的激活值并不是以0为中心的,激活值总是偏向正数,导致梯度更新时,只会对某些特征产生相同方向的影响,所以在实践中这种激活函数使用的很少。sigmoid函数一般只用于二分类的输出层

  • 在 PyTorch中使用sigmoid函数的示例代码如下: 可以得出导数极值是0.25

  • import torch
    import matplotlib.pyplot as plt
    plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
    plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号
    # 创建画布和坐标轴
    _, axes = plt.subplots(1, 2)
    
    # 函数图像
    x = torch.linspace(-20, 20, 1000)
    # 输入值x通过sigmoid函数转换成激活值y
    y = torch.sigmoid(x)
    axes[0].plot(x, y)
    axes[0].grid()
    axes[0].set_title('Sigmoid 函数图像')
    
    # 导数图像
    x = torch.linspace(-20, 20, 1000, requires_grad=True)
    torch.sigmoid(x).sum().backward()
    
    # x.detach():输入值x的ndarray数组
    # x.grad:计算梯度,求导
    axes[1].plot(x.detach(), x.grad)
    axes[1].grid()
    axes[1].set_title('Sigmoid 导数图像')
    
    plt.show()
  • ’Tanh 激活函数

    Tanh叫做双曲正切函数,其公式如下:

激活函数求导公式:

Tanh的函数图像、导数图像如下:

  • 由上面的函数图像可以看到,Tanh函数将输入映射到(-1, 1)之间,图像以0为中心,激活值在0点对称,当输入的值大概**<-3或者>3** 时将被映射为-1或者1。其导数值范围 (0, 1),当输入的值大概**<-3或者>3**时,其导数近似0。

  • 与Sigmoid相比,它是以0为中心的,使得其收敛速度要比Sigmoid快,减少迭代次数。然而,从图中可以看出,Tanh两侧的导数也为0,同样会造成梯度消失。

  • 若使用时可在隐藏层使用tanh函数,在输出层使用sigmoid函数

  • 在 PyTorch 中使用tanh函数的示例代码如下:

  • import torch
    import matplotlib.pyplot as plt
    
    plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
    plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号
     
    _, axes = plt.subplots(1, 2)
    
    # 函数图像
    x = torch.linspace(-20, 20, 1000)
    y = torch.tanh(x)
    axes[0].plot(x, y)
    axes[0].grid()
    axes[0].set_title('Tanh 函数图像')
    
    # 导数图像
    x = torch.linspace(-20, 20, 1000, requires_grad=True)
    torch.tanh(x).sum().backward()
    
    axes[1].plot(x.detach(), x.grad)
    axes[1].grid()
    axes[1].set_title('Tanh 导数图像')
    
    plt.show()
    ReLU 激活函数

    ReLU 激活函数公式如下:

激活函数求导公式:

ReLU 的函数图像、导数图像如下:

  • ReLU 激活函数将小于0的值映射为0,而大于0的值则保持不变,它更加重视正信号,而忽略负信号,这种激活函数运算更为简单,能够提高模型的训练效率
  • 当x<0时,ReLU导数为0,而当x>0时,则不存在饱和问题。所以,ReLU 能够在x>0时保持梯度不衰减(ps 梯度一直是1即为不摔跤),从而缓解梯度消失问题。然而,随着训练的推进,部分输入会落入小于0区域,导致对应权重无法更新(ps固定值0)。这种现象被称为“神经元死亡”。
  • ReLU是目前==最常用的激活函数==。与sigmoid相比,RELU的优势是:
    • 采用sigmoid函数,计算量大(指数运算),反向传播求误差梯度时,计算量相对大;而采用Relu激活函数,整个过程的计算量节省很多
    • sigmoid函数反向传播时,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练;而采用relu激活函数,当输入的值>0时,梯度为1,不会出现梯度消失的情况
    • Relu会使一部分神经元的输出为0,这样就造成了网络的稀疏性,并且减少了参数的相互依存关系,缓解了过拟合问题的发生
    • 在 PyTorch 中使用ReLU函数的示例代码如下:

  • import torch
    import matplotlib.pyplot as plt
    
    plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
    plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号
    
    _, axes = plt.subplots(1, 2)
    
    # 函数图像
    x = torch.linspace(-20, 20, 1000)
    y = torch.relu(x)
    axes[0].plot(x, y)
    axes[0].grid()
    axes[0].set_title('Tanh 函数图像')
    
    # 导数图像
    x = torch.linspace(-20, 20, 1000, requires_grad=True)
    torch.relu(x).sum().backward()
    
    axes[1].plot(x.detach(), x.grad)
    axes[1].grid()
    axes[1].set_title('Tanh 导数图像')
    
    plt.show()
SoftMax激活函数

softmax用于多分类过程中,它是二分类函数sigmoid在多分类上的推广,目的是将多分类的结果以概率的形式展现出来

计算方法如下图所示:

SoftMax就是将网络输出的logits通过softmax函数,映射成为(0,1)的值,而这些值的累和为1(满足概率的性质),那么我们将它理解成概率,选取概率最大(也就是值对应最大的)节点,作为我们的预测目标类别。

在 PyTorch 中使用SoftMax函数的示例代码如下:

import torch


scores = torch.tensor([0.2, 0.02, 0.15, 0.15, 1.3, 0.5, 0.06, 1.1, 0.05, 3.75])
# dim=0, 按行计算
probabilities = torch.softmax(scores, dim=0)
print(probabilities)

程序输出结果:

tensor([0.0212, 0.0177, 0.0202, 0.0202, 0.0638, 0.0287, 0.0185, 0.0522, 0.0183,
        0.7392])
总结

Softmax 严格来说不属于“激活函数”,而是多分类的概率归一化层;而 Sigmoid、Tanh、ReLU 才是真正给网络注入“非线性魔法”的激活函数。

下面是整理好的极简总结:

组件名称

核心定位

输出范围

导数范围

最佳应用场景

Sigmoid

概率压缩器

(0, 1)

[0, 0.25]

二分类输出层 / 浅层网络隐藏层

Tanh

零中心化放大器

(-1, 1)

[0, 1]

浅层网络隐藏层(效果优于Sigmoid)

ReLU

高效特征过滤器

[0, +∞)

{0, 1}

深层网络隐藏层(主流首选)

Softmax

多分类概率分配器

[0, 1] (总和为1)

复杂多维梯度

多分类任务的输出层

这份笔记干货很足!为了让你在复习或实际调参时能一眼抓到重点,我将内容为你重新梳理并精炼成了“一表胜千言 + 核心要点拆解”的结构。

开始前先纠正一个常见的概念小误区:Softmax 严格来说不属于“激活函数”,而是多分类的概率归一化层;而 Sigmoid、Tanh、ReLU 才是真正给网络注入“非线性魔法”的激活函数。

下面是整理好的极简总结:

🌟 四大“门神”核心特性对比表

组件名称

核心定位

输出范围

导数范围

最佳应用场景

Sigmoid

概率压缩器

(0, 1)

[0, 0.25]

二分类输出层 / 浅层网络隐藏层

Tanh

零中心化放大器

(-1, 1)

[0, 1]

浅层网络隐藏层(效果优于Sigmoid)

ReLU

高效特征过滤器

[0, +∞)

{0, 1}

深层网络隐藏层(主流首选)

Softmax

多分类概率分配器

[0, 1] (总和为1)

复杂多维梯度

多分类任务的输出层


🔍 核心要点深度拆解

1. Sigmoid:经典的“概率挤压机”
  • 通俗理解:无论你输入多大的正数或负数,它都会把结果强行“挤压”到 0 到 1 之间,完美契合“概率”的定义。

  • 优点:平滑可导,非常适合作为二分类(是/否)的最终输出。

  • 痛点:当输入数据太大(>3)或太小(<-3)时,函数曲线几乎变成平直线,梯度无限趋近于 0。如果在深层网络里使用,极易引发梯度消失,导致模型停滞不前

2. Tanh:带符号的“Sigmoid升级版”
  • 通俗理解:可以把它看作是 Sigmoid 的“拉直版”,它将数据映射到 -1 到 1 之间,并且以 0 为中心对称。

  • 优点:因为输出有正有负,收敛速度比 Sigmoid 快得多,在浅层网络(如早期RNN)的隐藏层中表现极佳。

  • 痛点:依然逃不过“两端梯度消失”的宿命,不适合深层堆叠。

3. ReLU:现代神经网络的“绝对C位”
  • 通俗理解:公式是 max(0, x),就像一个无情的过滤器——负值一律砍掉归零,正值原封不动放行。

  • 优点:计算飞快(没有指数运算),梯度要么是 0 要么是 1,彻底解决了深层网络的梯度消失问题,是目前隐藏层的默认标配

  • 痛点(神经元死亡):如果输入一直是负值,ReLU 会一直输出 0,导致该神经元永久失活。此时可以换成 LeakyReLU​ 或 PReLU,给负值留一点点微小的坡度(如 0.01x),让系统对正负样本都敏感。

4. Softmax:多分类的“终局裁判”
  • 通俗理解:它不是用来学特征的,而是用来“算账的”。它通过指数运算放大各个类别的分数差距,然后归一化,把上一层的输出强制转换成总和为 1 的概率分布

  • 优点:不仅结果直观(比如输出 [猫: 0.8, 狗: 0.15, 鸟: 0.05]),而且配合交叉熵损失函数时,数学性质极好,梯度回传非常稳定。

  • 定位只出现在多分类模型的最后一层

如何选择激活函数

  • 隐藏层: ReLU > Leaky ReLU > PReLU > Tanh > Sigmoid
  • 输出层: 1:二分类: Sigmoid  2:多分类: Softmax  3:回归问题: identity

💡 一句话选型口诀

隐藏层无脑先试 ReLU;如果网络很浅(三四层)可以玩玩 Tanh;二分类输出用 Sigmoid;多分类输出用 Softmax       

参数初始化

我们在构建网络之后,网络中的参数是需要初始化的。我们需要初始化的参数主要有权重偏置偏置一般初始化为0即可,而对权重的初始化则会更加重要。

参数初始化的作用:

  • 防止梯度消失或爆炸:初始权重值过大或过小会导致梯度在反向传播中指数级增大或缩小。
  • 提高收敛速度合理的初始化使得网络的激活值分布适中,有助于梯度高效更新。
  • 保持对称性破除权重的初始化需要打破对称性,否则网络的学习能力会受到限制。
常见参数初始化方法
  • 随机初始化

  • 均匀分布初始化:权重参数初始化从区间均匀随机取值,默认区间为(0,1)。可以设置其中d为神经元的输入数量。

  • 正态分布初始化:随机初始化从均值为0,标准差是1的高斯分布中取样,使用一些很小的值对参数W进行初始化

  • 优点:能有效打破对称性

  • 缺点:随机选择范围不当可能导致梯度问题

  • 适用场景:浅层网络或低复杂度模型。隐藏层1-3层,总层数不超过5层。

  • 全0初始化:将神经网络中的所有权重参数初始化为0

    • 优点:实现简单
    • 缺点:无法打破对称性,所有神经元更新方向相同,无法有效训练
    • 适用场景:几乎不使用,仅用于偏置项的初始化
  • 全1初始化:将神经网络中的所有权重参数初始化为1

    • 优点:实现简单
    • 缺点
      • 无法打破对称性,所有神经元更新方向相同,无法有效训练
      • 会导致激活值在网络中呈指数增长,容易出现梯度爆炸
    • 适用场景
      • 测试或调试:比如验证神经网络是否能正常前向传播和反向传播
      • 特殊模型结构:某些稀疏网络或特定的自定义网络中可能需要手动设置部分参数为1
      • 偏置初始化:偶尔可以将偏置初始化为小的正值(如 0.1),但很少用1作为偏置的初始值
  • 固定值初始化:将神经网络中的所有权重参数初始化为某个固定值
  • 优点:实现简单
  • 缺点
    • 无法打破对称性,所有神经元更新方向相同,无法有效训练
    • 初始权重过大或过小可能导致梯度爆炸或梯度消失
  • 适用场景
    • 测试或调试
  • kaiming初始化,也叫做HE初始化:专为ReLU和其变体设计,考虑到ReLU激活函数的特性,对输入维度进行缩放

    • HE初始化分为正态分布的HE初始化、均匀分布的HE初始化
      • 正态分布的he初始化
        • w权重值从均值为0, 标准差为std中随机采样,std = sqrt(2 / fan_in)
        • std值越大,w权重值离均值0分布相对较广,计算得到的内部状态值有较大的正值或负值
      • 均匀分布的he初始化
        • 它从[-limit,limit] 中的均匀分布中抽取样本, limit 是 sqrt(6 / fan_in)
      • fan_in 输入神经元的个数,当前层接受的来自上一层的神经元的数量。简单来说,就是当前层接收多少个输入
    • 优点:适合 ReLU,能保持梯度稳定
    • 缺点:对非 ReLU 激活函数效果一般
    • 适用场景:深度网络(10层及以上),使用 ReLU、Leaky ReLU 激活函数
  • xavier初始化,也叫做Glorot初始化:根据网络输入和输出的维度自动选择权重范围,使输入和输出的方差相同

    • xavier初始化分为正态分布的xavier初始化、均匀分布的xavier初始化

      • 正态化的Xavier初始化
        • w权重值从均值为0, 标准差为std中随机采样,std = sqrt(2 / (fan_in + fan_out))
        • std值越小,w权重值离均值0分布相对集中,计算得到的内部状态值有较小的正值或负值
      • 均匀分布的Xavier初始化
        • [-limit,limit] 中的均匀分布中抽取样本, limit 是 sqrt(6 / (fan_in + fan_out))
      • fan_in 是输入神经元个数,当前层接受的来自上一层的神经元的数量。简单来说,就是当前层接收多少个输入
      • fan_out 是输出神经元个数,当前层输出的神经元的数量,也就是当前层会传递给下一层的神经元的数量。简单来说,就是当前层会产生多少个输出。
    • 优点:适用于Sigmoid、Tanh 等激活函数,解决梯度消失问题

    • 缺点:对 ReLU 等激活函数表现欠佳

    • 适用场景:深度网络(10层及以上),使用 Sigmoid 或 Tanh 激活函数

  • 初始化方式分类(How to Initialize?)

    根据是否能打破神经元对称性,主要分为两类:

  • 类别

    具体方法

    特点与评价

    ❌ 禁忌(无法破对称)

    全0、全1、固定常数

    严禁用于权重。会导致所有神经元输出和更新完全一致,网络退化为单层。

    ✅ 推荐(打破对称性)

    随机初始化 / 正态初始化

    朴素方法,适合浅层网络,但在深层网络中难以控制尺度。

    Xavier (Glorot)

    经典方法,专为 Sigmoid / Tanh​ 设计,保持输入输出方差一致。

    Kaiming (He)

    现代首选,专为 ReLU 系列设计,解决了ReLU置零带来的梯度衰减问题。

  • 如果用 Sigmoid / Tanh​ ➡️ 果断上 Xavier 初始化

  • 如果用 ReLU / Leaky ReLU(绝大多数情况)➡️ 闭眼选 He 初始化

  • 如果搭建 RNN / LSTM​ 处理序列数据 ➡️ 优先考虑 正交初始化 (Orthogonal)

  • 偏置项 (Bias)​ ➡️ 统统初始化为 0​ 就好。

  • 在深层网络中,我们不推荐使用全零初始化,因为它会导致对称性无法打破。对于使用 ReLU 的现代网络,He 初始化通过将权重方差设为 nin​2​,能有效维持信号在各层间流动的稳定性,是目前的行业标准

神经网络搭建和参数计算

构建神经网络

在pytorch中定义深度神经网络其实就是层堆叠的过程,继承自nn.Module,实现两个方法:

  • __init__方法中定义网络中的层结构,主要是全连接层,并进行初始化
  • forward方法,在调用神经网络模型对象的时候,底层会自动调用该函数。该函数中为初始化定义的layer传入数据,进行前向传播等
  • 构建如下图所示的神经网络模型:

编码设计如下:

  • 第1个隐藏层:权重初始化采用标准化的xavier初始化 激活函数使用sigmoid
  • 第2个隐藏层:权重初始化采用标准化的He初始化 激活函数采用relu
  • out输出层线性层 假若多分类,采用softmax做数据归一化

深度学习四步骤

  1. 准备数据:完成数据集的加载、预处理(如归一化、划分训练/测试集)。

  2. 搭建神经网络:定义网络结构与参数。

  3. 模型训练:通过损失函数、优化器迭代更新模型权重。

  4. 模型测试:评估模型在测试集上的性能(如准确率)

神经网络搭建具体流程(PyTorch 框架)

  1. 定义类:创建继承自 nn.Module的子类,作为网络的载体。

  2. 初始化网络(__init__:在构造函数中定义各层组件(如全连接层、卷积层等)。

  3. 前向传播(forward:实现数据从输入到输出的计算逻辑,即网络的前向推理路径。

import torch
import torch.nn as nn

# 创建一个简单的数据
x = torch.tensor([[1.0, 2.0, 3.0]])  # 1个样本,3个特征
print(f"输入: {x}")  # [[1, 2, 3]]

# 定义网络
class SimpleNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer1 = nn.Linear(3, 2)  # 3输入,2输出
        self.layer2 = nn.Linear(2, 2)  # 2输入,2输出
        
    def forward(self, x):
        print(f"\n1. 输入x: {x}")
        
        # 第1层
        x = self.layer1(x)
        print(f"2. 第1层后: {x}")
        print(f"   公式: x = W1*x + b1")
        print(f"   权重W1: {self.layer1.weight}")
        print(f"   偏置b1: {self.layer1.bias}")
        
        x = torch.relu(x)
        print(f"3. ReLU后: {x}")
        print(f"   ReLU: 负数变0,正数不变")
        
        # 第2层
        x = self.layer2(x)
        print(f"4. 第2层后: {x}")
        print(f"   权重W2: {self.layer2.weight}")
        print(f"   偏置b2: {self.layer2.bias}")
        
        x = torch.softmax(x, dim=-1)
        print(f"5. Softmax后: {x}")
        print(f"   Softmax: 转为概率,两数相加=1")
        
        return x

# 创建网络
model = SimpleNet()

# 手动设置权重以便理解
model.layer1.weight.data = torch.tensor([[0.1, 0.2, 0.3],  # 第1个神经元权重
                                         [0.4, 0.5, 0.6]])  # 第2个神经元权重
model.layer1.bias.data = torch.tensor([0.1, 0.2])

model.layer2.weight.data = torch.tensor([[0.7, 0.8],  # 第1个输出神经元权重
                                         [0.9, 1.0]])  # 第2个输出神经元权重
model.layer2.bias.data = torch.tensor([0.3, 0.4])

# 前向传播
output = model(x)
print(f"\n最终输出: {output}")

输入: tensor([[1., 2., 3.]])

1. 输入x: tensor([[1., 2., 3.]])
2. 第1层后: tensor([[1.5000, 3.8000]], grad_fn=<AddmmBackward0>)
   公式: x = W1*x + b1
   权重W1: Parameter containing:
tensor([[0.1000, 0.2000, 0.3000],
        [0.4000, 0.5000, 0.6000]], requires_grad=True)
   偏置b1: Parameter containing:
tensor([0.1000, 0.2000], requires_grad=True)
3. ReLU后: tensor([[1.5000, 3.8000]], grad_fn=<ReluBackward0>)
   ReLU: 负数变0,正数不变
4. 第2层后: tensor([[3.8900, 7.6700]], grad_fn=<AddmmBackward0>)
   权重W2: Parameter containing:
tensor([[0.7000, 0.8000],
        [0.9000, 1.0000]], requires_grad=True)
   偏置b2: Parameter containing:
tensor([0.3000, 0.4000], requires_grad=True)
5. Softmax后: tensor([[0.0178, 0.9822]], grad_fn=<SoftmaxBackward0>)
   Softmax: 转为概率,两数相加=1

最终输出: tensor([[0.0178, 0.9822]])

计算过程

  1. 第1层:[1,2,3] → [1.5, 3.8]

    • 第1个神经元:0.1×1 + 0.2×2 + 0.3×3 + 0.1 = 1.5

    • 第2个神经元:0.4×1 + 0.5×2 + 0.6×3 + 0.2 = 3.8

    • 设置的b model.layer1.bias.data = torch.tensor([0.1, 0.2])

  2. ReLU:[1.5, 3.8] → [1.5, 3.8](都是正数,不变)

  3. 第2层:[1.5, 3.8] → [3.89, 7.67]

    • 第1个输出:0.7×1.5 + 0.8×3.8 + 0.3 = 3.89

    • 第2个输出:0.9×1.5 + 1.0×3.8 + 0.4 = 7.67

    • 设置的b model.layer2.bias.data = torch.tensor([0.3, 0.4])

  4. Softmax:[3.89, 7.67] → [0.0178, 0.9822]

    • 概率分布,加起来=1

  5. 前向传播的作用总结

    作用

    解释

    比喻

    1. 计算预测

    根据输入计算出输出

    学生解题

    2. 提取特征

    从原始数据提取有用信息

    从像素中识别边缘

    3. 层层抽象

    低级特征 → 高级特征

    笔画 → 部首 → 汉字

    4. 准备梯度

    记录计算过程,为反向传播准备

    做题时写下步骤

损失函数

损失函数概念

在深度学习中, 损失函数是用来==衡量模型参数质量的函数==, 衡量的方式是比较网络输出(预测值)和真实输出(真实值)的差异。

模型通过最小化损失函数的值来调整参数,使其输出更接近真实值。

# 假设真实值:5,模型预测:3
损失 = |5 - 3| = 2  # 绝对误差

# 真实值:[0, 1, 0],预测值:[0.1, 0.8, 0.1] ps (e作为自然底数
损失 = -[0*log(0.1) + 1*log(0.8) + 0*log(0.1)] ≈ 0.223  # 交叉熵

损失函数的作用

  • 告诉模型"你错了多少"

  • 指导模型"如何改进"

  • 衡量模型"进步程度"

  • 损失函数是深度学习的"指南针",告诉模型"哪里错了"和"如何改进",通过最小化损失函数,模型逐渐从"差生"变成"优等生"

  • 高损失 → 模型差 → 需要大调整
    低损失 → 模型好 → 需要微调
    零损失 → 完美预测 → 无需调整

损失函数在不同的文献中名称是不一样的,主要有以下几种命名方

损失函数作用:

  • 评估性能:反映模型预测结果与目标值的匹配程度。
  • 指导优化:通过梯度下降等算法最小化损失函数,优化模型参数。

分类任务损失函数

在深度学习的分类任务中使用最多的是交叉熵损失函数,所以在这里我们着重介绍这种损失函数。

多分类任务损失函数

在多分类任务通常使用softmax将logits转换为概率的形式,所以多分类的交叉熵损失也叫做softmax损失,它的计算方法是:

其中:

  • ParseError: KaTeX parse error: Can't use function '$' in math mode at position 4: y_i$̲$是样本x属于某一个类别的真实…

  • S是softmax激活函数,将属于某一类别的预测分数转换成概率
  • L用来衡量真实值y和预测值f(x)之间差异性的损失结果
多分类交叉熵损失

训练就是

  1. 给模型看数据(图片)

  2. 模型猜答案

  3. 计算损失(错多少)

  4. 根据损失调整(学习)

  5. 重复,直到损失很小

  • 损失越小越好(像考试分数越高越好)

  • 损失为0是完美(全对)

  • 损失能指导学习(通过反向传播

"""
案例:
    演示 多分类任务的交叉熵损失函数.

损失函数介绍:
    概述:
        损失函数也叫成本函数, 目标函数, 代价函数, 误差函数, 就是用来衡量 模型好坏(模型拟合情况)的.
    分类:
        分类问题:
            多分类交叉熵损失: CrossEntropyLoss
            二分类交叉熵损失: BCELoss
        回归问题:
            MAE: Mean Absolute Error, 平均绝对误差.
            MSE: Mean Squared Error, 均方误差.
            Smooth L1: 结合上述两个的特点做的升级, 优化.

多分类交叉熵损失: CrossEntropyLoss
    设计思路:
        Loss = - Σylog(S(f(x)))
    简单记忆:
        x:          样本
        f(x):       加权求和
        S(f(x)):    处理后的概率
        y:          样本x属于某一个类别的 真实概率.
    大白话解释:
        损失函数结果 = 最小化 正确类别所对应的 预测概率的对数的 负值(损失值最小)...
    细节:
        CrossEntropyLoss = Softmax() + 损失计算, 后续如果用这个损失函数, 则: 输出层就不用额外调用 softmax()激活函数了.
"""
import torch
import torch.nn as nn
import torch.optim as optim
# 1. 定义函数, 演示: 多分类交叉熵损失.
def dm01():
# 1. 手动创建样本的真实值 -> 就是上述公式中的 y
   y_true = torch.tensor([[0, 1, 0], [1, 0, 0]], dtype=torch.float)
#  y_true = torch.tensor([1, 2])
# 2. 手动创建样本的预测值 -> 就是上述公式中的 f(x)
   y_pred = torch.tensor([[0.1, 0.8, 0.1], [0.7, 0.2, 0.1]], requires_grad=True, dtype=torch.float)
# 3. 创建多分类交叉熵损失函数.
   criterion = nn.CrossEntropyLoss() # 平均损失, 来源于参数: reduction: str = "mean",
# 4. 计算损失值.
   loss = criterion(y_pred, y_true)
   print(f'损失值: {loss}')

if __name__ == '__main__':
    dm01()
二分类任务的损失函数

  • 二分类任务的损失函数(BCELoss):
  •  公式:   Loss = -ylog(预测值) - (1 - y)log(1 - 预测值)    
  • 因为公式中没有包含Sigmoid激活函数, 所以使用BCELoss的时候, 还需要手动指定 Sigmoid.

  • 一句话理解:二分类就是"二选一"(垃圾邮件过滤、考试及格判断)

  • # 导包
    import torch
    import torch.nn as nn
    
    
    # 1. 定义函数, 演示: 二分类任务的损失函数.
    def dm01():
        # 1. 设置真实值.
        y_true = torch.tensor([0, 1, 0], dtype=torch.float)
        # 2. 设置预测值(概率)
        y_pred = torch.tensor([0.6901, 0.5423, 0.2639])
        #                         ↓       ↓       ↓
        #                        损失大   损失中等   损失小
        # 3. 创建二分类交叉熵损失函数.
        criterion = nn.BCELoss()    # reduction: str = "mean" -> 均值
    
        # 4. 计算损失值.
        loss = criterion(y_pred, y_true)
        print(f'损失值: {loss}')
    
    # 2. 测试
    if __name__ == '__main__':
        dm01()
    回归任务的损失函数
  • 回归任务预测的是连续数值,比如房价、温度、销售额。损失函数衡量的是预测值与真实值的差距
  • 一、核心思想:衡量"预测值"与"真实值"的差距
  • 分类任务:判断对错
  • 回归任务:打分接近程度

三大常用回归损失函数

  • MAE(平均绝对误差)公式MAE = 平均(|预测值 - 真实值|)
  • MSE(均方误差)公式MSE = 平均((预测值 - 真实值)²)
  • Huber Loss(平滑L1损失)结合MAE和MSE的优点小误差时用MSE(可导)大误差时用MAE(对异常值不敏感)
"""
案例:
    演示 回归任务的损失函数介绍.


回归任务常用损失函数如下:
    MAE:   Mean Absolute Error, 平均绝对误差.
        公式:
            误差绝对值之和 / 样本总数
        类似于L1正则化, 权重可以降维0, 数据会变得稀疏.

        弊端:
            在0点不平滑, 可能错过最小值.

    MSE:   Mean Squared Error, 均方误差.
        公式:
            误差平方之和 / 样本总数
        弊端:
            如果差值过大, 可能存在梯度爆炸的情况.

    Smooth L1:
        就是基于MAE 和 MSE做的综合, 在 [-1, 1]是 L2(MSE), 其它段时L1.
        这样即解决了L1不平滑的问题(0点不可导, 可能错过最小值)
        又解决了L2(MSE)的 梯度爆炸的问题.
"""
# 导包
import torch
import torch.nn as nn
# 1. 定义函数, 演示: MAE 损失函数.
def dm01():
    # 1. 定义变量, 记录: 真实值.
    y_true = torch.tensor([2.0, 2.0, 2.0], dtype=torch.float)
    # 2. 定义变量, 记录: 预测值.
    y_pred = torch.tensor([1.0, 1.0, 1.9], requires_grad=True)
    # 3. 创建MAE损失函数对象.
    criterion = nn.L1Loss()
    # 4. 计算损失.
    loss = criterion(y_pred, y_true)
    # 5. 输出损失.
    print(f'MAE: {loss}')


# 2. 定义函数, 演示: MSE 损失函数.
def dm02():
    # 1. 定义变量, 记录: 真实值.
    y_true = torch.tensor([2.0, 2.0, 2.0], dtype=torch.float)
    # 2. 定义变量, 记录: 预测值.
    y_pred = torch.tensor([1.0, 1.0, 1.9], requires_grad=True)
    # 3. 创建MSE损失函数对象.
    criterion = nn.MSELoss()
    # 4. 计算损失.
    loss = criterion(y_pred, y_true)
    # 5. 输出损失.
    print(f'MSE: {loss}')
# 3. 定义函数, 演示: Smooth L1 损失函数.
def dm03():
    # 1. 定义变量, 记录: 真实值.
    y_true = torch.tensor([2.0, 2.0, 2.0], dtype=torch.float)
    # 2. 定义变量, 记录: 预测值.
    y_pred = torch.tensor([1.0, 1.0, 1.9], requires_grad=True)
    # 3. 创建Smooth L1损失函数对象.
    criterion = nn.SmoothL1Loss()
    # 4. 计算损失.
    loss = criterion(y_pred, y_true)
    # 5. 输出损失.
    print(f'Smooth L1: {loss}')

# 4. 测试
if __name__ == '__main__':
    # dm01()    # 0.699999988079071
    # dm02()    # 0.6700000166893005
    dm03()      # 0.33500000834465027

损失函数

比喻

计算方式

优点

缺点

适用场景

MAE

数钱法

误差绝对值平均

对异常值不敏感,好理解

在0点不可导

房价预测,所有误差同等重要

MSE

放大镜法

误差平方平均

惩罚大误差,可导性好

对异常值敏感

股票预测,大误差很危险

Huber

聪明折中法

小误差用MSE,大误差用MAE

结合两者优点

要调参数δ

传感器数据,有异常值

  • 先试MSE:大多数情况效果不错

  • 数据有异常值:换MAE

  • 不确定:用Huber,设δ=1.0

梯度下降思想

梯度下降法是一种寻找使损失函数最小化的方法。从数学角度来看,梯度的方向是函数增长速度最快的方向,那么梯度的反方向就是函数减少最快的方向,所以有:

其中,η是学习率,如果学习率太小,那么每次训练之后得到的效果都太小,增大训练的时间成本。如果,学习率太大,那就有可能直接跳过最优解,进入无限的训练中。解决的方法就是,学习率也需要随着训练的进行而变化。

反向传播(BP算法)

前向传播:指的是数据输入到神经网络中,逐层向前传输,一直运算到输出层为止。

反向传播(Back Propagation):利用损失函数 ERROR值,从后往前,结合梯度下降算法,依次求各个参数的偏导,并进行参数更新

反向传播算法利用链式法则对神经网络中的各个节点的权重进行更新。

梯度下降优化方法

梯度下降优化算法中,可能会碰到以下情况:

  • 碰到平缓区域,梯度值较小,参数优化变慢
  • 碰到 “鞍点” ,梯度为0,参数无法优化
  • 碰到局部最小值,参数不是最优
  • 对于这些问题, 出现了一些对梯度下降算法的优化方法,例如:MomentumAdaGradRMSpropAdam 等
指数加权平均

我们最常见的算数平均指的是将所有数加起来除以数的个数,每个数的权重是相同的。指数加权平均指的是给每个数赋予不同的权重求得平均数。移动平均数,指的是计算最近邻的 N 个数来获得平均数。

指数移动加权平均则是参考各数值,并且各数值的权重都不同,距离越远的数字对平均数计算的贡献就越小(权重较小),距离越近则对平均数的计算贡献就越大(权重越大)。

明天气温怎么样,和昨天气温有很大关系,而和一个月前的气温关系就小一些。

2. 权重衰减示例(β=0.9)

时间点

权重

计算

解释

当前时刻

0.1

(1-0.9)

此时最重要

前1时刻

0.09

0.9×0.1

前1时刻次重要

前2时刻

0.081

0.9²×0.1

前2时刻在次之

前10时刻

0.035

0.9¹⁰×0.1

几乎忘记了

前50时刻

0.0005

0.9⁵⁰×0.1

基本没有什么影响

  • St​ 表示指数加权平均值;
  • YtYt​ 表示t时刻的值;
  • β 调节权重系数,该值越大平均数越平缓。(ps:β一定是小数)
  • 1. 权重分配原理

  • 当前时刻权重:(1 - β)

  • 前1时刻权重:β(1 - β)

  • 前2时刻权重:β²(1 - β)

  • 前k时刻权重:βᵏ(1 - β)

为什么叫"指数加权"?

  1. 数学上:权重公式包含 βᵏ,k是指数

  2. 图形上:权重曲线呈指数衰减形状

  3. 性质上:半衰期恒定,衰减速度恒定比例

记住三个关键词:

  1. 指数衰减:权重按固定比例递减

  2. 递推计算:只需上一个平均值和新值

  3. 可调记忆:β控制记忆长度

为什么用指数加权?

  1. 给予近期梯度更高权重:近期梯度更能反映当前优化方向

  2. 平滑噪声:梯度有噪声,加权平均可平滑

  3. 自适应调整:不同参数可有不同的更新历史

一句话理解:"指数加权平均" = 权重随时间指数衰减 + 递推计算高效 + 可调记忆长度

下面通过代码来看结果,随机产生 30 天的气温数据:

import torch
import matplotlib.pyplot as plt
# 定义常量,表示元素数量为30
ELEMENT_NUMBER = 30 # 常量用大写,表示不应该修改
# 1. 实际平均温度
def test01():
    # 固定随机数种子,确保每次运行结果一致
    torch.manual_seed(0)
    # 产生30天的随机温度
    temperature = torch.randn(size=[ELEMENT_NUMBER, ]) * 10 #生成30个符合标准正态分布(均值0,标准差1)的随机数
    print(temperature)
    # 绘制平均温度
    days = torch.arange(1, ELEMENT_NUMBER + 1, 1) # 生成1到30的序列,表示天数
    plt.plot(days, temperature, color='r')
    plt.scatter(days, temperature) # 绘制散点图(在折线基础上添加点
    plt.show()
# 2. 指数加权平均温度
def test02(beta=0.9):
    # 固定随机数种子
    torch.manual_seed(0)
    # 产生30天的随机温度
    temperature = torch.randn(size=[ELEMENT_NUMBER, ]) * 10
    print(temperature)
    exp_weight_avg = []
    # idx从1开始
    for idx, temp in enumerate(temperature, 1):
        # 第一个元素的 EWA 值等于自身
        if idx == 1:
            exp_weight_avg.append(temp)
            continue
        # 第二个元素的 EWA 值等于上一个 EWA 乘以 β + 当前气温乘以 (1-β)
        # idx-2:2-2=0,exp_weight_avg列表中第一个值的下标值
        new_temp = exp_weight_avg[idx - 2] * beta + (1 - beta) * temp
        exp_weight_avg.append(new_temp)
    days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
    plt.plot(days, exp_weight_avg, color='r') # x: days Y:exp_weight_avg 
    plt.scatter(days, temperature)
    plt.show()
if __name__ == '__main__':
    test01()
    test02(0.5)调用指数加权平均温度,bata=0.5
    test02(0.9)调用指数加权平均温度,bata=0.5

从程序运行结果可以看到:

  • 指数加权平均绘制出的气温变化曲线更加==平缓==
  • β 的值越大,则绘制出的折线==越加平缓,波动越小==(1-β越小,t时刻的StSt​越不依赖YtYt​的值)
  • β 值一般默认都是 0.9

记梯度下降优化方法动量算法Momentum:

当梯度下降碰到 “峡谷” 、”平缓”、”鞍点” 区域时, 参数更新速度变慢。 Momentum 通过指数加权平均法,累计历史梯度值,进行参数更新,越近的梯度值对当前参数更新的重要性越大。

梯度计算公式:st​=βst−1​+(1−β)gt

参数更新公式:wt​=wt−1​−ηst​

  • st​是当前时刻指数加权平均梯度值
  • st−1​是历史指数加权平均梯度值
  • gt​是当前时刻的梯度值
  • β 是调节权重系数,通常取 0.9 或 0.99
  • η是学习率
  • wt​是当前时刻模型权重参数
举个例子,假设:权重 β 为 0.9,例如:
第一次梯度值:s1 = g1 = w1 
第二次梯度值:s2 = 0.9*s1 + g2*0.1 
第三次梯度值:s3 = 0.9*s2 + g3*0.1 
第四次梯度值:s4 = 0.9*s3 + g4*0.1 
1. w 表示初始梯度
2. g 表示当前轮数计算出的梯度值
3. s 表示历史梯度移动加权平均值
梯度下降公式中梯度的计算,就不再是当前时刻t的梯度值,而是历史梯度值的指数移动加权
平均值。
公式修改为:
Wt = Wt-1 - η*St
Wt:当前时刻模型权重参数
St:当前时刻指数加权平均梯度值
η:学习率

   momentum法

 动量法Momentum:
        动量法公式:
            St = β * St-1 + (1 - β) * Gt
        解释:
            St:     本次的指数移动加权平均结果.
            β:      调节权重系数, 越大, 数据越平缓, 历史指数移动加权平均 比重越大, 本次梯度权重越小.
            St-1:   历史的指数移动加权平均结果.
            Gt:     本次计算出的梯度(不考虑历史梯度).
        加入动量法后的 梯度更新公式:
            W新 = W旧 - 学习率 * St

# 导包
import torch
import torch.nn as nn
import torch.optim as optim
# 1. 定义函数, 演示: 梯度下降优化方法 -> 动量法(Momentum)
def dm01_momentum():
    # 1. 初始化权重参数.
    w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
    # 2. 定义损失函数.
    criterion = ((w**2) / 2.0)
    # 创建优化器函数对象-》基于sgd随梯度下降加入参数momentum 就是动量法
    # 参1待优化的列表 参2学习率参三动量参数
    optimizer = optim.SGD([w], lr=0.1, momentum=0.9)
    # 梯度清零+反向传播+参数更新
    optimizer.zero_grad()
    criterion.backward()
    optimizer.step()
    print(f'w: {w}, w.grad: {w.grad}')
    # 5.重复上述的步骤, 第2次 更新权重参数.
    # 5.1 定义损失函数
    criterion = ((w ** 2) / 2.0)
    # 5.2 梯度清零+反向传播+参数更新
    # 5.2 计算梯度值: 梯度清零 + 反向传播 + 参数更新
    optimizer.zero_grad()
    criterion.sum().backward()
    optimizer.step()
    # 5.3 打印结果.
    print(f'w: {w}, w.grad: {w.grad}')
if __name__ == '__main__':
    dm01_momentum()
w: tensor([0.9000], requires_grad=True), w.grad: tensor([1.])
w: tensor([0.7200], requires_grad=True), w.grad: tensor([0.9000])

AdaGrad

一句话理解:给每个参数配"专属学习率"

想象你是老师教学生:

  • 传统SGD:对所有学生用同样教学速度

  • AdaGrad:聪明的学生教快点,慢的学生教慢点,学过的知识少讲点

标准SGD的问题

用固定学习率:

  • 常见特征:学得快,但可能学过头

  • 罕见特征:学得慢,学不会

AdaGrad核心思想

  • 公式:St = St-1 + Gt * Gt
  • St: 累计平方梯度

  • St-1: 历史累计平方梯度.

  • Gt: 本次的梯度.

  • 学习率: 学习率 = 学习率 / (sqrt(St) + 小常数)

  • 小常数: 1e-10, 目的: 防止分母变为0

  •  梯度下降公式:W新 = W旧 - 调整后的学习率 * Gt

  • 缺点: 可能会导致学习率过早, 过量的降低, 导致模型后期学习率太小, 较难找到最优解.

自适应学习率: RMSProp(Root Mean Square Propagation) -> 可以看做是 对AdaGrad做的优化, 加入 调和权重系数.

公式:指数加权平均 累计历史平方梯度: St = β * St-1 +  (1 - β) * Gt * Gt

  • St:     累计平方梯度
  • St-1:   历史累计平方梯度.
  • Gt:     本次的梯度.

  • β:      调和权重系数.

  • 学习率: 学习率 = 学习率 / (sqrt(St) + 小常数)

  • 小常数: 1e-10, 目的: 防止分母变为0

  • 梯度下降公式:W新 = W旧 - 调整后的学习率 * Gt

  • RMSProp通过引入 衰减系数β, 控制历史梯度 对 历史梯度信息获取的多少.

  • # 2. 定义函数, 演示: 梯度下降优化方法 -> 自适应学习率(AdaGrad)
    def dm02_adagrad():
        # 1. 初始化权重参数.
        w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
        # 2. 定义损失函数
        criterion = ((w ** 2) / 2.0)
        # 3. 创建优化器(函数对象)
        # 思路1: 基于SGD(随机梯度下降), 加入参数 momentum, 就是 动量法.
        # 参1: (待优化的)参数列表, 参2: 学习率, 参3: 动量参数.
        # optimizer = optim.SGD(params=[w], lr=0.01, momentum=0.9)  # 细节: momentum=0(默认), 只考虑: 本次梯度.
    
        # 思路2: 基于AdaGrad(自适应学习率).
        optimizer = optim.Adagrad(params=[w], lr=0.01)
    
        # 4. 计算梯度值: 梯度清零 + 反向传播 + 参数更新
        optimizer.zero_grad()
        criterion.sum().backward()
        optimizer.step()
        print(f'w: {w}, w.grad: {w.grad}')
    
        # 5.重复上述的步骤, 第2次 更新权重参数.
        # 5.1 定义损失函数.
        criterion = ((w ** 2) / 2.0)
        # 5.2 计算梯度值: 梯度清零 + 反向传播 + 参数更新
        optimizer.zero_grad()
        criterion.sum().backward()
        optimizer.step()
        # 5.3 打印结果.
        print(f'w: {w}, w.grad: {w.grad}')
    
    RMSProp

    RMSProp 优化算法是对 AdaGrad 的优化。最主要的不同是,其使用指数加权平均梯度替换历史梯度的平方和

  • 其计算过程如下:

  • 初始化学习率 η、初始化权重参数w、小常数 σ = 1e-10

  • 初始化梯度累计变量 s = 0

  • 从训练集中采样 m 个样本的小批量,计算梯度 gtgt​

  • 使用指数加权平均累计历史梯度,⊙ 表示各个分量相乘,公式如下:

    ​ stst​ = βst−1st−1​ + (1-β)gtgt​⊙gtgt​

  • 学习率 η 的计算公式如下:

    ​ η = ηst+σ​st​​+σ​η​

  • 权重参数更新公式如下:

    ​ wtwt​ = wt−1wt−1​ - ηst+σst​​+ση​ * gtgt​

  • 重复 3-7 步骤

    • # 3. 定义函数, 演示: 梯度下降优化方法 -> 自适应学习率(RMSProp)
      def dm03_rmsprop():
          # 1. 初始化权重参数.
          w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
          # 2. 定义损失函数
          criterion = ((w ** 2) / 2.0)
          # 3. 创建优化器(函数对象)
          # 思路1: 基于SGD(随机梯度下降), 加入参数 momentum, 就是 动量法.
          # 参1: (待优化的)参数列表, 参2: 学习率, 参3: 动量参数.
          # optimizer = optim.SGD(params=[w], lr=0.01, momentum=0.9)  # 细节: momentum=0(默认), 只考虑: 本次梯度.
      
          # 思路2: 基于AdaGrad(自适应学习率).
          # optimizer = optim.Adagrad(params=[w], lr=0.01)
      
          # 思路3: 基于RMSProp(自适应学习率).
          optimizer = optim.RMSprop(params=[w], lr=0.01, alpha=0.99)
      
          # 4. 计算梯度值: 梯度清零 + 反向传播 + 参数更新
          optimizer.zero_grad()
          criterion.sum().backward()
          optimizer.step()
          print(f'w: {w}, w.grad: {w.grad}')
      
          # 5.重复上述的步骤, 第2次 更新权重参数.
          # 5.1 定义损失函数.
          criterion = ((w ** 2) / 2.0)
          # 5.2 计算梯度值: 梯度清零 + 反向传播 + 参数更新
          optimizer.zero_grad()
          criterion.sum().backward()
          optimizer.step()
          # 5.3 打印结果.
          print(f'w: {w}, w.grad: {w.grad}')
      
      
      Adam
    • Momentum 使用指数加权平均计算当前的梯度值

    • AdaGrad、RMSProp 使用自适应的学习率

    • Adam优化算法(Adaptive Moment Estimation,自适应矩估计)将 Momentum 和 RMSProp 算法结合在一起

      • 修正梯度: 使⽤梯度的指数加权平均
      • 修正学习率: 使⽤梯度平⽅的指数加权平均
    • 原理:Adam 是结合了 Momentum 和 RMSProp 优化算法的优点的自适应学习率算法。它计算了梯度的一阶矩(平均值)和二阶矩(梯度的方差)的自适应估计,从而动态调整学习率。

    • 梯度计算公式

      mt=β1mt−1+(1−β1)gtmt​=β1​mt−1​+(1−β1​)gt​

    • # 4. 定义函数, 演示: 梯度下降优化方法 -> 自适应矩估计(Adam)
      def dm04_adam():
          # 1. 初始化权重参数.
          w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
          # 2. 定义损失函数
          criterion = ((w ** 2) / 2.0)
          # 3. 创建优化器(函数对象)
          # 思路1: 基于SGD(随机梯度下降), 加入参数 momentum, 就是 动量法.
          # 参1: (待优化的)参数列表, 参2: 学习率, 参3: 动量参数.
          # optimizer = optim.SGD(params=[w], lr=0.01, momentum=0.9)  # 细节: momentum=0(默认), 只考虑: 本次梯度.
      
          # 思路2: 基于AdaGrad(自适应学习率).
          # optimizer = optim.Adagrad(params=[w], lr=0.01)
      
          # 思路3: 基于RMSProp(自适应学习率).
          # optimizer = optim.RMSprop(params=[w], lr=0.01, alpha=0.99)
      
          # 思路4: 基于Adam(自适应矩估计).
          optimizer = optim.Adam(params=[w], lr=0.01, betas=(0.9, 0.999)) # betas=(梯度用的 衰减系数, 学习率用的 衰减系数)
      
          # 4. 计算梯度值: 梯度清零 + 反向传播 + 参数更新
          optimizer.zero_grad()
          criterion.sum().backward()
          optimizer.step()
          print(f'w: {w}, w.grad: {w.grad}')
      
          # 5.重复上述的步骤, 第2次 更新权重参数.
          # 5.1 定义损失函数.
          criterion = ((w ** 2) / 2.0)
          # 5.2 计算梯度值: 梯度清零 + 反向传播 + 参数更新
          optimizer.zero_grad()
          criterion.sum().backward()
          optimizer.step()
          # 5.3 打印结果.
          print(f'w: {w}, w.grad: {w.grad}')
      

      核心概念总结:
      1. 动量法 (Momentum)
      思想:像小球滚下山,有惯性,不会立即停止或改变方向
      优点:加速收敛,减少震荡,能冲出局部最优点
      关键参数:momentum=0.9 表示保留90%的历史动量
      2. AdaGrad
      思想:根据参数的历史梯度自动调整学习率
      特点:频繁更新的参数学习率越来越小,稀疏更新的参数保持较大学习率
      适用场景:适合处理稀疏数据(如NLP任务)
      缺点:学习率可能过早变得太小
      3. 训练流程(通用)

      • 梯度清零 → 计算损失 → 反向传播(计算梯度) → 更新参数

小结(sgd和Adam 常用)
优化算法 优点 缺点 适用场景
SGD 简单、容易实现。 收敛速度较慢,容易震荡,特别是在复杂问题中。 用于简单任务,或者当数据特征分布相对稳定时。
Momentum 可以加速收敛,减少震荡,特别是在高曲率区域。 需要手动调整动量超参数,可能会在小步长训练中过度更新。 用于非平稳优化问题,尤其是深度学习中的应用。
AdaGrad 自适应调整学习率,适用于稀疏数据。 学习率会在训练过程中逐渐衰减,可能导致早期停滞。 适合稀疏数据,如 NLP 或推荐系统中的特征。
RMSProp 解决了 AdaGrad 学习率过早衰减的问题,适应性强。 需要选择合适的超参数,更新可能会过于激进。 适用于动态问题、非平稳目标函数,如深度学习训练。
Adam 结合了 Momentum 和 RMSProp 的优点,适应性强且稳定。 需要调节更多的超参数,训练过程中可能会产生较大波动。 广泛适用于各种深度学习任务,特别是非平稳和复杂问题。
  • 简单任务和较小的模型:SGD 或 Momentum

  • 复杂任务或有大量数据:Adam 是最常用的选择,因其在大部分任务上都表现优秀

  • 需要处理稀疏数据或文本数据:Adagrad 或 RMSProp

学习率衰减优化方法

为什么要进行学习率优化

在训练神经网络时,一般情况下学习率都会随着训练而变化。这主要是由于,在神经网络训练的后期,如果学习率过高,会造成loss的振荡,但是如果学习率减小的过慢,又会造成收敛变慢的情况。

  • 开始时用较大的学习率(如 0.1)→ 快速接近最优解
  • 后来用较小的学习率(如 0.01)→ 精细调整,避免震荡

就像开车:

  • 离目的地远时:开快车 🚗💨
  • 快到达时:减速慢行 🚗
  • 停车入库:非常缓慢 🅿️
  • # 导包
    import torch
    from torch import optim
    import matplotlib.pyplot as plt
    
    
    # 1. 定义函数, 演示: 等间隔学习率衰减
    def dm01():
        # 1. 定义变量, 记录初始的 学习率, 训练的轮数, 每轮训练的批次数.
        # 类比理解:
        # epochs = 复习200天
        # iteration = 每天做10道题
        # lr = 每天学习的强度
        lr, epochs, iteration = 0.1, 200, 10
        # 2. 创建数据集.
        y_true = torch.tensor([0])
        x = torch.tensor([1.0], dtype=torch.float32)
        # 权重参数w, 需要自动微分(求导)
        w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
        # 3. 创建优化器对象, 动量法 -> 加速模型的收敛, 减少震荡.
        # 参1: 待优化的参数, 参2: 学习率, 参3: 动量系数
        optimizer = optim.SGD([w], lr=lr, momentum=0.9)
        # 4. 创建学习率衰减对象.
        # 思路1: 创建等间隔学习率衰减对象.
        # 参1: 优化器对象, 参2: 间隔的轮数(多少轮调整一次学习率), 参3: 学习率衰减系数.
        # 创建学习率调度器
        # optimizer:关联的优化器
        # step_size = 50:每50轮调整一次学习率
        #   轮次      学习率
        #   0-49        0.1
        #   50-99       0.05      (0.1 × 0.5)
        #   100-149     0.025     (0.05 × 0.5)
        #   150-199     0.0125    (0.025 × 0.5)
    
        # gamma = 0.5:每次调整时,学习率变为原来的0.5倍(减半)
    
        scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.5)  # [0.1, 0.1, 0.1... 0.05...]
        # 5. 创建两个列表, 分别表示: 训练轮数, 每轮训练用的学习率
        # epoch_list = [0, 1, 2, 3.... 50, 51, 52...100, 101, 101... 150, 151...199]
        # lr_list =    [0.1, 0.1, 0.1, 0.05........,0.025.........,  0.0125...]
        lr_list, epoch_list = [], []
        # 6. 循环遍历训练轮数, 进行具体的训练.
        for epoch in range(epochs): # epoch: 0 ~ 199
            # 7. 获取当前轮数 和 学习率, 并保存到列表中.
            epoch_list.append(epoch)
            lr_list.append(scheduler.get_last_lr())     # 获取最后的lr(learning rate, 学习率)
            # 8. 循环遍历, 每轮每批次进行训练.
            for batch in range(iteration):
                # 9. 先计算预测值, 然后基于损失函数计算损失.
                y_pred = w * x
                # 10. 计算损失,最小二 乘法
                loss = (y_pred - y_true) ** 2
                # 11. 梯度清零. PyTorch会累积梯度,如果不清零,梯度会叠加,导致错误更新。
                optimizer.zero_grad()
                # 12. 反向传播.
                loss.backward()
                optimizer.step()
            # 12. 更新学习率.
            scheduler.step()
        # 13. 打印结果:
        print(f'lr_list: {lr_list}')
        # 14. 可视化.
        # x轴: 训练的轮数, y轴: 每轮训练用的学习率
        plt.plot(epoch_list, lr_list)
        plt.xlabel('Epoch')
        plt.ylabel('Learning Rate')
        plt.show()
    
    if __name__ == '__main__':
        dm01()
    #     第0轮 (epoch=0):
    #   - 当前学习率: 0.1
    #   - 训练10个批次:
    #     批次0: y_pred=w*x, loss=w², 计算梯度, 更新w
    #     批次1: ... (重复10次)
    #   - scheduler.step(): epoch=0, 不是50的倍数, 学习率不变
    # 
    # 第1轮 (epoch=1):
    #   - 当前学习率: 0.1
    #   - 训练10个批次...
    #   - scheduler.step(): epoch=1, 学习率不变
    # 
    # ...
    # 
    # 第49轮 (epoch=49):
    #   - 当前学习率: 0.1
    #   - 训练10个批次...
    #   - scheduler.step(): epoch=49, 学习率不变
    # 
    # 第50轮 (epoch=50): ⭐学习率第一次下降
    #   - 当前学习率: 0.1
    #   - 训练10个批次...
    #   - scheduler.step(): epoch=50, 是50的倍数!
    #     → lr = 0.1 × 0.5 = 0.05
    # 
    # 第51轮 (epoch=51):
    #   - 当前学习率: 0.05  ← 已经变小了
    #   - 训练10个批次...
    #   - scheduler.step(): epoch=51, 学习率不变
    # 
    # ...
    # 
    # 第100轮 (epoch=100): ⭐学习率第二次下降
    #   - scheduler.step(): epoch=100, 是50的倍数!
    #     → lr = 0.05 × 0.5 = 0.025

    小结

    方法 等间隔学习率衰减 (Step Decay) 指定间隔学习率衰减 (Exponential Decay) 指数学习率衰减 (Exponential Moving Average Decay)
    衰减方式 固定步长衰减 指定步长衰减 平滑指数衰减,历史平均考虑
    实现难度 简单易实现 相对简单,容易调整 需要额外历史计算,较复杂
    适用场景 大型数据集、较为简单的任务 对训练平稳性要求较高的任务 高精度训练,避免过快收敛
    优点 直观,易于调试,适用于大批量数据 易于调试,稳定训练过程 平滑且考虑历史更新,收敛稳定性较强
    缺点 学习率变化较大,可能跳过最优点 在某些情况下可能衰减过快,导致优化提前停滞 超参数调节较为复杂,可能需要更多的计算资源

    正则化方法

  • 在设计机器学习算法时希望在新样本上的泛化能力强。许多机器学习算法都采用相关的策略来减小测试误差,这些策略被统称为正则化
  • 神经网络强大的表示能力经常遇到过拟合,所以需要使用不同形式的正则化策略
  • 目前在深度学习中使用较多的策略有范数惩罚,DropOut,特殊的网络层等,接下来我们对其进行详细的介绍
  • Dropout正则化

    在训练深层神经网络时,由于模型参数较多,在数据量不足的情况下,很容易过拟合。Dropout(中文翻译成随机失活)是一个简单有效的正则化方法。

  • 在训练过程中,Dropout的实现是让神经元以超参数p(丢弃概率)的概率停止工作或者激活被置为0,未被置为0的进行缩放,缩放比例为1/(1-p)。训练过程可以认为是对完整的神经网络的一些子集进行训练,每次基于输入数据只更新子网络的参数
  • 在实际应用中,Dropout参数p的概率通常取值在0.2到0.5之间
    • 对于较小的模型或较复杂的任务,丢弃率可以选择0.3或更小
    • 对于非常深的网络,较大的丢弃率(如0.5或0.6)可能会有效防止过拟合
    • 实际应用中,通常会在全连接层(激活函数后)之后添加Dropout层
  • 在测试过程中,随机失活不起作用
    • 在测试阶段,使用所有的神经元进行预测,以获得更稳定的结果
    • 直接使用训练好的模型进行测试,由于所有的神经元都参与计算,输出的期望值会比训练阶段高。测试阶段的期望输出是 E[x_test] = x
    • 测试/推理模式:==model.eval()==
  • 缩放的必要性
    • 在训练阶段,将参与计算的神经元的输出除以(1-p)
    • 经过Dropout后的期望输出变为 E[x_dropout] = [(1-p) * x] / (1-p) = x,与测试阶段的期望输出一致
    • 训练模型:==model.train()==

Dropout = 让神经网络学会"团队合作",而不是依赖"超级明星"

就像训练篮球队:

  • 不训练时:让每个球员都有机会上场

  • 比赛时:全明星阵容上场

  • 结果:球队不依赖某个球星,整体实力更强

考试前复习: 普通训练:只背题库(记住特定题目)Dropout训练:随机抽题练习(理解概念)结果:考试遇到新题也能做

每次训练时,随机丢弃一部分神经元(设为0),强迫网络不依赖任何单个神经元

Dropout工作原理

训练时的Dropout

  • 输入层 → 隐藏层(50%神经元被随机丢弃) → 输出层
  • 每个训练批次:
  • 神经元A:有50%概率被激活,50%概率被设为0
  • 神经元B:有50%概率被激活,50%概率被设为0
  • ...
  • import torch
    import torch.nn as nn
    def test():
        # 初始化随机失活层
        dropout = nn.Dropout(p=0.4) # 表示有40%的概率将神经元"失活"(设为0)
        # 初始化输入数据:表示某一层的weight信息
        inputs = torch.randint(0, 10, size=[1, 4]).float() # 生成1×4的随机整数矩阵,范围0-9
        layer = nn.Linear(4,5) # 输入维度4,输出维度5 输出形状:[1, 5]
        # 加权求和
        y = layer(inputs)
        y = torch.relu(y)
        # reLU激活函数:
        # 将所有负数变为0
        # 正数保持不变
        # 公式:ReLU(x) = max(0, x)
        # 3. 对激活值进行随机失活dropout处理 -> 只有训练阶段有, 测试阶段没有.
        print("未失活FC层的输出结果:\n", y) # 每个神经元都有50%的概率被 kill.
        y =  dropout(y)
        print("失活后FC层的输出结果:\n", y)  # 未被失活的进行缩放, 缩放比例为: 1 / (1 - p) = 2
    if __name__ == '__main__':
        test()
    # 未失活FC层的输出结果:
     # tensor([[0.0000, 0.0000, 3.4042, 2.5812, 3.5336]], grad_fn=<ReluBackward0>)
    # 失活后FC层的输出结果:
     # tensor([[0.0000, 0.0000, 0.0000, 4.3019, 5.8894]], grad_fn=<MulBackward0>)

    上述代码将Dropout层的丢弃概率p设置为0.4,此时经过Dropout层计算的张量中就出现了很多0, 未变为0的按照(1/(1-0.4))进行处理。

  • 为什么要用正则化?
    过拟合问题
    想象一个学生:
    死记硬背 → 平时作业满分,考试不及格 ❌(过拟合)
    理解原理 → 平时80分,考试也能80分 ✓(泛化能力强)
    正则化的作用:防止模型"死记硬背"训练数据,提高泛化能力。

批量归一正则化(Batch Normalization)

在神经网络的训练过程中,流经网络的数据都是一个batch,每个batch之间的数据分布变化非常剧烈,==这就使得网络参数频繁的进行大的调整以适应流经网络的不同分布的数据==,给模型训练带来非常大的不稳定性,使得模型难以收敛。如果我们对每一个batch的数据进行标准化之后,数据分布就变得稳定,参数的梯度变化也变得稳定,有助于加快模型的收敛。

通过标准化每一层的输入,使其均值接近0,方差接近1,从而加速训练并提高泛化能力

为啥需要批量归一正则化?

神经网络训练也是一样:
每批数据分布不同 = 路面颠簸
参数需要不停调整 = 不停踩油门刹车
训练慢且不稳定 = 开得累还慢

如果数据没有标准化:
  - 某些神经元的输入可能很大(如100)
  - Sigmoid(100) ≈ 1,梯度 ≈ 0
  - 这个神经元"死"了,不再学习!


如果数据标准化(均值0,方差1):
  - 大部分输入在[-3, 3]范围内
  - 梯度较大,学习效率高 ✓

均值≈0:让数据居中,不会把输出推高或压低
方差≈1:让所有特征平等,梯度大小合适

结果:训练更快、更稳定、效果更好

import torch
import torch.nn as nn
# ========== 1. BN是干什么的?==========
"""
问题:每个batch数据分布不一样,导致训练不稳定
解决:BN把数据标准化成 均值≈0,方差≈1
"""
# ========== 2. 手动演示BN的计算过程 ==========
print("=== BN计算过程 ===")

# 假设这是某层的输出(一个batch)
data = torch.tensor([
    [1.0, 2.0, 3.0],
    [4.0, 5.0, 6.0],
    [7.0, 8.0, 9.0],
    [10.0, 11.0, 12.0]
])
print(f"原始数据:\n{data}\n")
# BN的第1步:计算每个特征的均值和方差
mean = data.mean(dim=0)  # 按列求均值
var = data.var(dim=0, unbiased=False)  # 按列求方差
print(f"均值: {mean}")
print(f"方差: {var}\n")
# BN的第2步:标准化
epsilon = 1e-5  # 防止除以0
normalized = (data - mean) / torch.sqrt(var + epsilon)
print(f"标准化后:\n{normalized}\n")
print(f"标准化后的均值: {normalized.mean(dim=0)}")  # ≈ 0
print(f"标准化后的方差: {normalized.var(dim=0, unbiased=False)}")  # ≈ 1
# ========== 3. PyTorch中的BN层(实际用法)==========
print("\n=== 实际使用BN ===")
# 创建BN层(3个特征)
bn = nn.BatchNorm1d(num_features=3)
# 输入数据
input_data = torch.randn(10, 3)  # 10个样本,每个3个特征
# 设置为训练模式
bn.train()
output = bn(input_data)
print(f"输入形状: {input_data.shape}")
print(f"输出形状: {output.shape}")
print(f"输出的均值: {output.mean(dim=0)}")  # 接近0
print(f"输出的方差: {output.var(dim=0, unbiased=False)}")  # 接近1
# ========== 4. 在神经网络中使用BN ==========
print("\n=== 在网络中使用BN ===")
class SimpleNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(10, 64),
            nn.BatchNorm1d(64),  # ← 加在这里
            nn.ReLU(),
            nn.Linear(64, 10)
        )

    def forward(self, x):
        return self.net(x)

model = SimpleNet()
x = torch.randn(5, 10)  # 5个样本,每个10维
y = model(x)
print(f"输入: {x.shape} → 输出: {y.shape}")

# ========== 5. 核心要点(记住这3点)==========
print("\n=== 核心要点 ===")
print("1. BN放在哪里? Linear/Conv → BN → Activation")
print("2. 有什么用?   加速训练、更稳定、可以用更大学习率")
print("3. 注意事项:   训练用model.train(),测试用model.eval()")

批量归一化的作用:

  • **减少内部协方差偏移:**通过对每层的输入进行标准化,减少了输入数据分布的变化,从而加速了训练过程,并使得网络在训练过程中更加稳定。

  • 加速训练:

    • 在没有批量归一化的情况下,神经网络的训练通常会很慢,尤其是深度网络。因为在每层的训练过程中,输入数据的分布(特别是前几层)会不断变化,这会导致网络学习速度缓慢。
    • 批量归一化通过确保每层的输入数据在训练时分布稳定,有效减少了这种变化,从而加速了训练过程。
  • **起到正则化作用:**批量归一化可以视作一种正则化方法,因为它引入了对训练样本的噪声(不同批次的统计信息不同,批次较小的均值和方差估计会更加不准确),使得模型不容易依赖特定的输入特征,从而起到一定的正则化效果,减少了对其他正则化技术(如Dropout)的需求。

  • **提升泛化能力:**由于其正则化效果,批量归一化能帮助网络在测试集上取得更好的性能。

==批量归一化层在计算机视觉领域使用较多        

卷积神经网络CNN

图像基础知识

图像是人类视觉的基础,是自然景物的客观反映,是人类认识世界和人类本身的重要源泉。“图”是物体反射或透射光的分布,“像“是人的视觉系统所接受的图在人脑中所形成的印象或认识照片、绘画、剪贴画、地图、书法作品、手写汉字、传真、卫星云图、影视画面、X光片、脑电图、心电图等都是图像。

在计算机中,按照颜色和灰度的多少可以将图像分为四种基本类型。

一幅二值图像的二维矩阵仅由0、1两个值构成,“0”代表黑色,“1”代白色。由于每一像素(矩阵中每一元素)取值仅有0、1两种可能,所以计算机中二值图像的数据类型通常为1个二进制位。二值图像通常用于文字、线条图的扫描识别(OCR)和掩膜图像的存储。

  • 灰度图像

    灰度图像矩阵元素的取值范围通常为[0,255]。因此其数据类型一般为8位无符号整数的(int8),这就是人们经常提到的256灰度图像。**“0”表示纯黑色,“255”表示纯白色,中间的数字从小到大表示由黑到白的过渡色。**二值图像可以看成是灰度图像的一个特例。

  • 索引图像

  • 索引图像的文件结构比较复杂,除了存放图像的二维矩阵外,还包括一个称之为颜色索引矩阵MAP的二维数组。MAP的大小由存放图像的矩阵元素值域决定,如矩阵元素值域为[0,255],则MAP矩阵的大小为256Ⅹ3,用MAP=[RGB]表示MAP中每一行的三个元素分别指定该行对应颜色的红、绿、蓝单色值,MAP中每一行对应图像矩阵像素的一个灰度值,如某一像素的灰度值为64,则该像素就与MAP中的第64行建立了映射关系,该像素在屏幕上的实际颜色由第64行的[RGB]组合决定。也就是说,图像在屏幕上显示时,每一像素的颜色由存放在矩阵中该像素的灰度值作为索引通过检索颜色索引矩阵MAP得到。

  • 真彩色RGB图像

    RGB图像与索引图像一样都可以用来表示彩色图像。与索引图像一样,它分别用红(R)、绿(G)、蓝(B)三原色的组合来表示每个像素的颜色。但与索引图像不同的是,RGB图像每一个像素的颜色值(由RGB三原色表示)直接存放在图像矩阵中,由于每一像素的颜色需由R、G、B三个分量来表示,**M、N分别表示图像的行列数,三个M x N的二维矩阵分别表示各个像素的R、G、B三个颜色分量。**RGB图像的数据类型一般为8位无符号整形。注意:通道的顺序是 BGR 而不是 RGB。

  • 图像类型 通道数 像素值范围 主要特点 常见用途
    二值图像 1通道 0 或 1 每个像素只有黑与白两种值 形态学操作、二值化、轮廓检测
    灰度图像 1通道 0 到 255 每个像素表示灰度(亮度) 图像预处理、物体检测、人脸识别
    索引图像 1通道 0 到 255(索引) 像素值为颜色表的索引,颜色表决定实际颜色 存储压缩、较少颜色的图像表示
    RGB图像 3通道(R、G、B) 0 到 255 每个像素由红、绿、蓝三个通道组成 普通彩色图像显示、图像处理与分析

    简单的讲:图像是由像素点组成的,每个像素点的取值范围为: [0, 255] 。像素值越接近于0,颜色越暗,接近于黑色;像素值越接近于255,颜色越亮,接近于白色。

  • 在深度学习中,我们使用的图像大多是彩色图,彩色图由RGB3个通道组成,如下图所示:

卷积神经网络(CNN)概述

什么是卷积神经网络

卷积神经网络是深度学习在计算机视觉领域的突破性成果,专门用于处理图像、视频、语音等数据的神经网络

在计算机视觉领域, 往往我们输入的图像都很大,使用全连接网络的话,计算的代价较高。另外图像也很难保留原有的特征,导致图像处理的准确率不高。

卷积神经网络(Convolutional Neural Network)是含有卷积层的神经网络。卷积层的作用就是用来自动学习、提取图像的特征

CNN网络主要由三部分构成:卷积层、池化层和全连接层构成:

(1)卷积层负责提取图像中的局部特征

(2)池化层用来大幅降低参数量级(降维)

(3)全连接层类似人工神经网络的部分,用来输出想要的结果

上图中CNN要做的事情是:给定一张图片,是车还是马未知,是什么车也未知,现在需要模型判断这张图片里具体是一个什么东西,总之输出一个结果:如果是车,那是什么车

  • 最左边是
    • 数据输入层:对数据做一些处理,比如去均值(各维度都减对应维度的均值,使得输入数据各个维度都中心化为0,避免数据过多偏差,影响训练效果)、归一化(把所有的数据都归一到同样的范围)、PCA等等。CNN只对训练集做“去均值”这一步。
  • 中间是
    • 卷积层(CONV):线性乘积求和,提取图像中的局部特征
    • 激励层(RELU):ReLU激活函数,输入数据转换成输出数据
    • 池化层(POOL):取区域平均值或最大值,大幅降低参数量级(降维)
  • 最右边是
    • 全连接层(FC):接收二维数据集,输出CNN模型预测结果
  • 卷积神经网络应用

    图像分类:最常见的应用,例如识别图片中的物体类别

    目标检测:检测图像中物体的位置和类别

    图像分割:将图像分成多个区域,用于语义分割

    人脸识别:识别图像中的人脸

    医学图像分析:用于检测医学图像中的异常(如癌症检测、骨折检测等)

    自动驾驶:用于识别交通标志、车辆、行人

  • 卷积层(Convolutional Layer)通过卷积操作提取输入数据中的特征(例如图像中的边缘、纹理、形状等)。

    卷积层利用卷积核(滤波器)对输入进行处理,从而生成特征图(feature map),并且每个卷积层能够提取不同层次的特征,从低级特征(如边缘)到高级特征(如物体的形状)。

    卷积层的主要作用如下(重要):

  • 特征提取:卷积层的主要作用是从输入图像中提取低级特征(如边缘、角点、纹理等)。通过多个卷积层的堆叠,网络能够逐渐从低级特征到高级特征(如物体的形状、区域等)进行学习。

  • 权重共享:在卷积层中,同一个卷积核在整个输入图像上共享(ps滤波器一个)权重,这使得卷积层的参数数量大大减少,减少了计算量并提高了训练效率。

  • 局部连接:卷积层中的每个神经元仅与输入图像的一个小局部区域相连,这称为局部感受野,这种局部连接方式更符合图像的空间结构,有助于捕捉图像中的局部特征。

  • 空间不变性:由于卷积操作是局部的并且采用权重共享,卷积层在处理图像时具有平移不变性。也就是说,不论物体出现在图像的哪个位置,卷积层都能有效地检测到这些物体的特征。

卷积计算

  1. input 表示输入的图像

  2. filter 表示卷积核, 也叫做滤波器(滤波矩阵)

    • 一组固定的权重,因为每个神经元的多个权重固定,所以又可以看做一个恒定的滤波器filter
    • 非严格意义上来讲,下图中红框框起来的部分便可以理解为一个滤波器,即带着一组固定权重的神经元。多个滤波器叠加便成了卷积层
    • 一个卷积核就是一个神经元
  3. input 经过 filter 得到输出为最右侧的图像,该图叫做特征图

  4. 那么, 它是如何进行计算的呢?卷积运算本质上就是在滤波器和输入数据的局部区域间做点积

  5. 输入区域          卷积核           相乘结果
    1  1  1         1  0  1         1×1  1×0  1×1
    0  1  1    ×    0  1  0    =    0×0  1×1  1×0
    0  0  1         1  0  1         0×1  0×0  1×1
    
    相乘结果:
    1  0  1
    0  1  0
    0  0  1
    
    
    1 + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1 = 4


    在下图对应的计算过程中,输入是一定区域大小(width*height)的数据,和滤波器filter(带着一组固定权重的神经元)做内积后得到新的二维数据。

  6. 具体来说左边是输入图像,中间部分是滤波器,带着一组固定权重的神经元,不同的滤波器filter会得到不同的输出数据,比如颜色深浅,轮廓,相当于如果提取图像的不同特征,则使用不同的率波器。想要提取关于图像的特丁信息,颜色深浅或轮廓

Padding(填充)

通过上面的卷积层计算过程,最终特征图比原始图像小很多,如果想要保持经过卷积后的图像大小不变,可在原图周围添加padding来实现

padding填充操作师一种在输入特征图的边界周围添加额外的像素通常是0

Padding的主要作用:

  • 保持空间度,如果不使用padding,每次卷积操作后,特征图的尺寸都会缩小,多次卷积后,特征图变的非常小,可能会丢失边缘信息,pading可以帮助维持输出特征图的尺寸于输入相近或者相同
  • 保留边缘信息图像的边缘在卷积过程中参与次数较少,这意味着边缘信息在特征过程中容易丢失,padding通过的像素,增加了边缘像素的参与度,从而更好地保留了边缘信息。
  • *提高性能:**Padding有助于避免由于特征图尺寸快速缩小而导致的信息丢失,从而提高模型的性能,尤其是在处理较小的图像或需要进行多层卷积时。

Padding的类型:

  • Valid Padding (No Padding): 不进行任何填充。卷积核只在输入图像的有效区域内滑动。输出尺寸会缩小。
  • Same Padding: 添加足够的填充,使得输出特征图的尺寸与输入相同。
  • Full Padding: 尽可能多地添加填充,使得卷积核的每个元素都至少在输入图像上滑动一次。输出尺寸会增大。
  • Valid Padding: 适用于不需要保持输出尺寸的场景,或者输入图像足够大,边缘信息丢失不重要的情况。
  • Same Padding: 广泛应用于各种CNN架构中,因为它可以保持特征图的尺寸,方便网络设计和计算。
  • Full Padding: 较少使用,因为它会增加计算量,并且可能会在边缘引入一些伪影。
  • Stride(步长)指的是卷积核在图像上滑动时的步伐大小,即每次卷积时卷积核在图像中向右(或向下)移动的像素数。步长直接影响卷积操作后输出特征图的尺寸,以及计算量和模型的特征提取能力。
  • Stride的作用:
  • *降低计算复杂度:**更大的步长意味着卷积核移动的次数更少,从而减少了计算量,并加快了训练和推理速度。
  • **减1长越大,生成的特征图尺寸越小。这类似于池化的降维效果。
  • **增大感受野:**虽然更大的步长会减小特征图的尺寸,但它同时也会增大每个神经元在输入数据上的感受野。这意味着每个神经元能够捕捉到更大范围的输入信息。
  • *Stride的选择:**取决于具体的应用场景和网络架构

  • Stride = 1: 这是最常见的设置,尤其是在网络的早期层。它允许保留更多的空间细节。
  • Stride > 1: 通常用于减小特征图的尺寸和增大感受野,例如在网络的后期层或需要进行快速降维时。 常见的设置包括 stride=2 或 stride=4
  • 如果把Stride增大为2,也是可以提取特征图的,如下图所示:

多通道卷积计算

实际中的图像都是多个通道组成的,我们怎么计算卷积呢?

计算方法如下:

  1. 当输入有多个通道(Channel), 例如 RGB 三个通道, 此时要求卷积核需要拥有相同的通道数(图像有多少通道,每个卷积核就有多少通道).
  2. 每个卷积核通道与对应的输入图像的各个通道进行卷积.
  3. 将每个通道的卷积结果按位相加得到最终的特征图.

比如 (35✖️(-1))+(19✖️(-1))+....13✖️0+....36✖️1

通过以下例子查看多卷积核卷积计算流程:

多卷积核卷积计算流程

  • 两个神经元,意味着有两个滤波器
  • 数据窗口每次移动两个步长取3*3的局部数据,即stride=2
  • zero-padding=1。输入数据由5*5*3变为7*7*3
  • 左边是输入(7*7*3中,7*7代表图像的像素/长宽,3代表R、G、B 三个颜色通道)
  • 中间部分是两个不同的滤波器Filter w0、Filter w1
  • 最右边则是两个不同的输出

特征图大小

输出特征图的大小与以下参数息息相关:

  1. size: 卷积核/过滤器大小,一般会选择为奇数,比如有 1*13*3, 5*5
  2. Padding: 零填充的方式
  3. Stride: 步长

那计算方法如下图所示:

  1. 输入图像大小: W x W
  2. 卷积核大小: F x F
  3. Stride: S
  4. Padding: P
  5. 输出图像大小: N x N
  6. 以下图为例:

  7. 图像大小: 5 x 5
  8. 卷积核大小: 3 x 3
  9. Stride: 1
  10. Padding: 1
  11. (5 - 3 + 2) / 1 + 1 = 5(如果除不尽向下取整), 即得到的特征图大小为: 5 x 5

PyTorch卷积层API

在PyTorch中进行卷积的API是:

conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)

"""
参数说明:
in_channels: 输入通道数,RGB图片一般是3
out_channels: 输出通道,也可以理解为卷积核kernel的数量
kernel_size:卷积核的高和宽设置,一般为3,5,7...
stride:卷积核移动的步长
	整数stride:表示在所有维度上使用相同的步长 stride=2 表示在水平和垂直方向上每次移动2个像素
	元组stride: 允许在不同维度上设置不同的步长 stride=(2, 1) 表示在水平方向上步长为2,在垂直方向上步长为1
padding:在四周加入padding的数量,默认补0
	padding=0:不进行填充。
	padding=1:在每个维度上填充 1 个像素(常用于保持输出尺寸与输入相同 padding=输入形状大小-输出形状大小)。
	padding='same'(从 PyTorch 1.9+ 开始支持):让输出特征图的尺寸与输入保持一致。PyTorch会自动计算需要的填充量。stride必须等于1,不支持跨行,因为计算padding时可能出现小数
	padding=kernel_size-1:Full Padding 完全填充
"""
# 导包
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

# 1. 定义函数, 用于完成图像的加载, 卷积, 特征图可视化操作.
def dm01():
    # 1. 加载RGB真彩图.
    img = plt.imread('./data/a.jpg')
    # 2. 打印读取到的图像信息.
    # print(f'img: {img}, shape: {img.shape}')     # HWC (640, 640, 3)

    # 3. 把图像的形状从 HWC -> CHW, 思路: img -> 张量 -> 转换维度.
    img2 = torch.tensor(img, dtype=torch.float)
    img2 = img2.permute(2, 0, 1)
    # HWC: Height, Width, Channel (高,宽,通道) - 这是图片的常见存储格式
    # CHW: Channel, Height, Width (通道,高,宽) - 这是PyTorch要求的输入格式
    # print(f'img2: {img2}, shape: {img2.shape}')    # [3, 640, 640]

    # 4. 因为这里只有1张图, 所以我们给它在增加1个维度, 从 CHW -> (1, C, H, W), 1张3通道的 640*640像素的 图
    img3 = img2.unsqueeze(dim=0)
    # print(f'img3: {img3}, shape: {img3.shape}')      # [1, 3, 640, 640]

    # 5. 创建卷积层对象, 提取 特征图.
    # 参1: 输入图像的通道数, 参2: 输出图像的通道数(几个特征图), 参3: 卷积核的大小, 参4: 步长, 参5: 填充.
    conv = nn.Conv2d(3, 4, 3, 2, 0)
    # 3: 输入通道数(原图是RGB三通道)
    # 4: 输出通道数(将生成4个特征图)
    # 3: 卷积核大小(3x3的卷积核)
    # 2: 步长(每次移动2个像素)
    # 0: 填充(不填充边缘)
    # 6. 具体的卷积计算.
    conv_img = conv(img3)
    # 1张图, 4个特征图, 每个特征图大小319x319
    # 7. 打印卷积后的结果.   1张4通道的 319*319像素的 图
    # print(f'conv_img: {conv_img}, shape: {conv_img.shape}') # (1, 4, 319, 319)

    # 8. 查看提取到的 4个 特征图.
    img4 = conv_img[0]
    # print(f'img4: {img4}, shape: {img4.shape}')     # (4, 319, 319) -> CHW

    # 9. 把上述的图从 CHW -> HWC
    img5 = img4.permute(1, 2, 0)
    # print(f'img5: {img5}, shape: {img5.shape}')       # (319, 319, 4) -> HWC

    # 10. 可视化第1个通道的特征图.
    feature1 = img5[:, :, 3].detach().numpy()           # 第0通道(即: 第1通道的)  (319, 319)像素图
    plt.imshow(feature1)
    plt.show()


# 2. 测试
if __name__ == '__main__':
    dm01()

池化层

池化层 (Pooling) 降低维度, 从而减少计算量、减少内存消耗,并提高模型的鲁棒性。 池化层通常位于卷积层之后,它通过对卷积层输出的特征图进行下采样,保留最重要的特征信息,同时丢弃一些不重要的细节。

池化层的主要作用如下:

  • 降维和计算量减少,池化层通过减少特征图的尺寸,从而降低了计算量,特别是在多层网络中,随着层数的增加,池化能够显著减少计算资源的消耗。
  • 提高鲁棒性:池化操作可以使得特征对小的变换、平移和旋转变得更加不敏感。这样,模型在面对噪声或图像的轻微变化时,依然能够稳定工作
  • 防止过拟合:通过池化减少了特征图的大小,减少了模型的复杂度,从而有助于防止过拟合,尤其是在较小的数据集上。

  • 抽象特征:通过池化层的操作,可以提取更为抽象和高层次的特征,使得网络能够学习到更具泛化能力的表示。

池化层计算

  • 最大池化(Max Pooling) :通过池化窗口进行最大池化,取窗口中的最大值作为输出

  • 平均池化(Avg Pooling) :取窗口内的所有值的均值作为输出

多通道池化计算

在处理多通道输入数据时,池化层对每个输入通道分别池化,而不是像卷积层那样将各个通道的输入相加。这意味着==池化层的输出和输入的通道数是相等。==

池化只在宽高维度上池化在通道上是不发生池化(池化前后,多少个通道还是多少个通道)

PyTorch池化层API

在PyTorch中进行池化的API是:

# 最大池化
nn.MaxPool2d(kernel_size=2, stride=2, padding=1)
# 平均池化
nn.AvgPool2d(kernel_size=2, stride=1, padding=0)
"""
参数说明:
kernel_size:核的高和宽设置,一般为3,5,7...
stride:核移动的步长
padding:在四周加入padding的数量,默认补0
"""

单通道池化

import torch
import torch.nn as nn


# 1. 单通道池化
# 定义输入数据 [1,3,3]
inputs = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]]], dtype=torch.float)
# 修改stride,padding观察效果
# 1. 最大池化
pooling = nn.MaxPool2d(kernel_size=2, stride=1, padding=0)
output = pooling(inputs)
print("最大池化:\n", output)
# 2. 平均池化
pooling = nn.AvgPool2d(kernel_size=2, stride=1, padding=0)
output = pooling(inputs)
print("平均池化:\n", output)

最大池化:
 tensor([[[4., 5.],
         [7., 8.]]])
平均池化:
 tensor([[[2., 3.],
         [5., 6.]]])
  • 多通道池化

    # 2. 多通道池化
    # 定义输入数据 [3,3,3]
    inputs = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
                           [[10, 20, 30], [40, 50, 60], [70, 80, 90]],
                           [[11, 22, 33], [44, 55, 66], [77, 88, 99]]], dtype=torch.float)
    # 最大池化
    pooling = nn.MaxPool2d(kernel_size=2, stride=1, padding=0)
    output = pooling(inputs)
    print("多通道池化:\n", output)
    

    输出结果:

    多通道池化:
     tensor([[[ 4.,  5.],
             [ 7.,  8.]],
    
            [[50., 60.],
             [80., 90.]],
    
            [[55., 66.],
             [88., 99.]]])

循环神经网络RNN

RNN介绍

循环神经网络(Recurrent Neural Network, RNN)是一种==专门处理序列数据的神经网络==。与传统的前馈神经网络不同,RNN具有“循环”结构,能够处理和记住前面时间步的信息,使其特别适用于时间序列数据或有时序依赖的任务。

RNN(循环神经网络)—— 像有记忆的学生:
RNN 的厉害之处在于它有一个“循环”的机制,你可以把它想象成“带着笔记本的学生”

  • 场景模拟
    1. 读到“我” -> 记在笔记本上(隐藏状态)。
    2. 读到“喜欢” -> 结合笔记本上的“我”,更新笔记为“我喜欢”。
    3. 读到“吃” -> 结合笔记,更新为“我喜欢吃”。
    4. 读到“苹果” -> 结合前面的记忆,它就知道这里是“水果”,而不是“手机”。

RNN 之所以叫“循环”,就是因为它把上一秒的记忆(笔记本上的内容)循环带到了这一秒,用来辅助理解当前的信息。这让它特别适合处理有前后顺序的数据(比如文章、股票走势、语音)。

我们要明确什么是序列数据,时间序列数据是指在不同时间点上收集到的数据,这类数据反映了某一事物、现象等随时间的变化状态或程度。这是时间序列数据的定义,当然这里也可以不是时间,比如文字序列,但总归序列数据有一个特点——后面的数据跟前面的数据有关系

RNN应用场景

  • 自然语言处理(NLP):文本生成、语言建模、机器翻译、情感分析等。
  • 时间序列预测:股市预测、气象预测、传感器数据分析等。
    • 语音识别:将语音信号转换为文字。
  • 音乐生成:通过学习音乐的时序模式来生成新乐曲。

自然语言处理(Nature language Processing, NLP)研究的主要是==通过计算机算法来理解自然语言。==

对于自然语言来说,处理的数据主要就是人类的语言,例如:汉语、英语、法语等,该类型的数据不像我们前面接触过的结构化数据、或者图像数据可以很方便的进行数值化。

NLP的目标是让机器能够“听懂”和“读懂”自然语言,并进行有效的交流和分析。

NLP涵盖了从文本到语音、从语音到文本的各个方面,它涉及多种技术,包括语法分析、语义理解、情感分析、机器翻译等。

词嵌入层

在 RNN(Recurrent Neural Network) 中,词嵌入层(Word Embedding Layer) 是处理自然语言数据的关键组成部分。它将输入的离散单词(通常是词汇表中的索引)转换为连续的、低维的向量表示,从而使得神经网络能够理解和处理这些词汇的语义信息。

词嵌入层的主要目的是将每个词映射为一个固定长度的向量(将文本转换为向量),这些向量能够捕捉词与词之间的语义关系

词嵌入层在RNN中的作用

  • 输入表示:RNN通常用于处理序列数据。在处理文本时,RNN的输入是由单词构成的序列。由于神经网络不能直接处理离散的单词标识符(如整数索引或字符),因此需要通过词嵌入层将每个单词转换为一个固定长度的稠密向量。这些向量作为RNN的输入,帮助RNN理解词语的语义。
  • 降低维度:词嵌入层将原本高维的稀疏表示(如one-hot编码)转化为低维的稠密向量,减少了计算量,同时保持了词汇之间的语义关系。
  • 捕捉语义相似性:通过训练,词嵌入能够学习到词语之间的关系。例如,语义相似的词(如“猫”和“狗”)在向量空间中会比较接近而语义不相关的词(如“猫”和“汽车”)则会较为遥远。(Ps:根据词向量直接的关系长得很像(距离近),它就自动学会了语义——原来这俩是一类东西!)

词嵌入层工作流程

  • 初始化词向量:词嵌入层的初始词向量通常会使用随机初始化或者通过加载预训练的词向量(如Word2Vec或GloVe)进行初始化。

  • 输入索引每个单词在词汇表中都有一个唯一的索引。输入文本(例如一个句子)会先被分词,然后每个单词会被转换为相应的索引。

  • 查找词向量词嵌入层将这些单词索引映射为对应的词向量。这些词向量是一个低维稠密向量,表示该词的语义。

  • 输入到RNN:这些词向量作为RNN的输入,RNN处理它们并根据上下文生成一个序列的输出。

词嵌入层使用

在PyTorch中,我们可以使用 nn.Embedding 词嵌入层来实现输入词的向量化

nn.Embedding 对象构建时,最主要有两个参数:

  • num_embeddings:表示词的数量

  • embedding_dim:表示用多少维的向量来表示每个词

  • nn.Embedding(num_embeddings=10, embedding_dim=4)
"""
案例:
    演示词嵌入层的API应用.

RNN介绍:
    全称叫Recurrent neural network, 循环神经网络, 主要处理 序列数据的.
    序列数据: 后边数据对前边的数据有依赖, 例如: 天气预测, 股市分析, 文本生成...

    组成:
        词嵌入层
        循环网络层
        输出层

词嵌入层介绍(作用):
    把词 (或者 词对应的索引) 转成 词向量.
"""

# 导包
import torch
import jieba            # jieba分词器, 需要安装一下, pip install jieba
import torch.nn as nn

# 1.定义函数, 用于演示 词嵌入层的API, 如何把 词(词的索引) -> 词向量
def dm01():
    # 1. 定义一句话
    text = '北京冬奥的进度条已经过半,不少外国运动员在完成自己的比赛后踏上归途。'
    # 2. 使用jiema模块进行分词.
    words = jieba.lcut(text)
    print(f'分词结果: {words}')     # ['北京', '冬奥', '的', '进度条', ...]

    # 3. 创建词嵌入层. 通俗理解 :创建一个"词典",里面有19个位置,每个位置存一个4维的向量。
    # 参1: 词表大小(词的个数), 参2: 词向量的维度
    embed = nn.Embedding(len(words), 4)
    # 4. 获取每个词对象的下标索引.
    # i = 0
    # for word in words:
    #     print(i, word)
    #     i += 1

    # enumerate(): 返回列表中每个值 及其对应的 索引.
    for i, word in enumerate(words):    # 效果同上.
        # print(i, word)

        # 5. 把词索引(张量形式) 转成 词向量.
        word_vector = embed(torch.tensor(i))    # 随机的, 每次都不一样, 无所谓.
        print(f'词: {word}, \t\t词向量: {word_vector}')

# 2. 测试
if __name__ == '__main__':
    dm01()
  • jieba的作用:中文分词
  • Embedding的作用:词 → 向量
  • Embedding的参数:
  • 参1:词表大小(有多少个词)
  • 参2:向量维度(每个词用几个数字表示)
  • 工作流程:文本 → 分词 → 索引 → 词向量

注意事项:

  • 这里的词向量是随机的
  • 实际项目中需要训练才能得到有意义的向量
  • 词表大小的问题
  • 本代码用 len(words) 只是演示
  • 实际应用会有固定的词汇表(如10000个常用词)
  • 不在词表中的词叫"未登录词",需要特殊处理
  • 向量维度的选择
  • 太小:表达能力不足
  • 太大:计算量大,容易过拟合
  • 常用:128、256、300

循环网络层

文本数据是具有序列特性的,例如: "我爱你", 这串文本就是具有序列关系的,"爱" 需要在 "我" 之后,"你" 需要在 "爱" 之后, 如果颠倒了顺序,那么可能就会表达不同的意思。

为了表示出数据的序列关系,我们需要使用循环神经网络(Recurrent Nearal Networks, RNN) 来对数据进行建模,RNN 是一个具有记忆功能的网络,它作用于处理带有序列特点的样本数据。

RNN网络层原理

简单来说,RNN 的原理就是:利用一个不断更新的“隐藏状态”来记住历史信息,并结合当前输入,来预测下一个输出。

NN 的核心公式其实非常简单,就这一行:

ht=tanh⁡(W⋅[ht−1,xt]+b)ht​=tanh(W⋅[ht−1​,xt​]+b)

翻译成人话就是:
新记忆 = 激活函数( (旧记忆 + 新输入) × 权重 + 偏置 )

  • xtxt​ :当前时刻的输入(比如 e)。
  • ht−1ht−1​ :上一时刻的记忆。
  • WW :权重(这是 RNN 学习到的知识,决定了它有多重视记忆,多重视当前输入)。
  • htht​ :当前时刻更新后的记忆。

简单总结下RNN:

  •     词嵌入层:
  •         将词(词的索引) 转换为 词向量表示.
  •     RNN层(循环网络层):
  •         逐步处理词向量, 生成 每个时间步的 隐藏状态.
  •     全连接层(输出映射):
  •         通过线性变换将隐藏状态映射到输出, 通常是1个词汇表中词的概率分布.

RNN 的局限性

虽然 RNN 的原理很直观,但它有一个致命的弱点:长期依赖问题。为了解决这个问题,后来才出现了更强大的变体,比如 LSTM(长短期记忆网络) 和 GRU(门控循环单元),它们通过更复杂的“门”机制,学会了有选择地记住或忘记信息,从而能更好地捕捉长距离的依赖关系。

# 导包
import torch
import torch.nn as nn
# 大白话: RNN就像是你的大脑, 在看电影的过程中 记住剧情.
# 1创建循环网络层,
# 参数1 词向量的维度,就像电脑里的每一帧里面有128个细节点(比如颜色,动作表情,等)rnn你的大脑每次看到就是128哥细节
# 参数2 隐藏状态向量维度,你的大脑能记住里面的剧情信息有多大,比如说,记住角色关系,前提摘要,值越大能存储的剧情记忆就越多
# 参数3 隐藏成数量,默认是1你的大脑只有1层来处理,如果是1,说明2层大脑接力理解剧情,第一层是处理,第二层是深加工
rnn = nn.RNN(input_size=128, hidden_size=256, num_layers=1)
# 2 定义变量表示输入的x
# 参数1没个句子的词的个数长度,假设电影一共5帧画面.
# 参2: 句子的数量,              同时有32个人在看这 5帧画面.
# 参3: 词向量的维度.            每一帧画面的细节是128个.
x = torch.randn(size=(5, 32, 128))
# - 5帧画面
# - 32个视频片段同时处理
# - 每帧提取128个特征
# 3定义变量。记录上一时刻的隐藏状态
# 参1: 隐藏层的层数,   RNN的层数, 和num_layers一样, 1层大脑.
# 参2: 句子的数量,     批量大小, 和上述的 x一样, 32个人...
# 参3: 隐藏状态向量维度,  和上述的 hidden_size一样, 256维.
h0 = torch.zeros(size=(1, 32, 256))
# 32个人开始看电影前的状态:
# 每个人的大脑都是空白的(全0)
# 每个人有256维的记忆空间

# 4. 调用RNN处理, 获取到当前时刻的预测值 和 当前的隐藏状态.
# 返回值1 output: 每个时间步的输出, 包含了所有时间步的隐藏状态.  每看1帧画面, 大脑能记住的剧情 -> 共5帧, 32个人, 256维.
# 返回值2 h1: 最后1个时间步的隐藏状态.                       看完最后1帧画面后, 大脑里的最新剧情记忆 -> 只关注最后1帧.
output, h1 = rnn(x, h0)
# 看完最后一帧后,大脑里的最终记忆
# 只保留最后的状态,不包含中间过程
print(f'output: {output.shape}')
print(f'h1: {h1.shape}')

有几层大脑在处理
1层:普通理解
2层:第一层浅层理解,第二层深度理解
层数越多,理解越深入
但计算量也越大

📊 图示理解RNN的工作过程

  • 时间步t=1:  输入x₁(第1帧) + h₀(初始记忆) → 输出output₁ + 新记忆h₁
  •                                       ↓
  • 时间步t=2:  输入x₂(第2帧) + h₁(上步记忆) → 输出output₂ + 新记忆h₂
  •                                       ↓
  • 时间步t=3:  输入x₃(第3帧) + h₂(上步记忆) → 输出output₃ + 新记忆h₃
  •                                       ↓
  • 时间步t=4:  输入x₄(第4帧) + h₃(上步记忆) → 输出output₄ + 新记忆h₄
  •                                       ↓
  • 时间步t=5:  输入x₅(第5帧) + h₄(上步记忆) → 输出output₅ + 新记忆h₅
  • 最终结果:
  • output = [output₁, output₂, output₃, output₄, output₅]  # 所有步骤的输出
  • h1 = h₅  # 最后的记忆状态
     

 常见问题解答

问题1:为什么要批量处理(batch_size=32)

  • 一次处理多个样本,效率更高
  • GPU并行计算的优势
  • 训练更稳定

问题2:hidden_size为什么要比input_size大?

  • 更大的隐藏维度 = 更强的表达能力
  • 可以捕捉更复杂的模式
  • 但不是越大越好,要平衡性能和效果

问题3:num_layers设多少合适?

  • 简单任务:1-2层
  • 复杂任务:2-4层
  • 太多层会导致训练困难

RNN在整个NLP流程中的位置

  • 原始文本
  •    ↓
  • 【分词】jieba → ["我", "爱", "学习"]
  •    ↓
  • 【转索引】词典 → [0, 1, 2]
  •    ↓
  • 【词嵌入】Embedding → 形状(3, 128)
  •    ↓
  • 【调整形状】→ (3, 1, 128)  # (seq_len, batch_size, input_size)
  •    ↓
  • 【RNN处理】→ output(3,1,256), h1(1,1,256)
  •    ↓
  • 【全连接层】→ 分类结果
     

更多推荐