六万字硬核详解:卷积神经网络CNN(原理详解 + 项目实战 + 经验分享)

本文围绕卷积神经网络CNN展开,涵盖项目实战、前向与反向传播、模型训练优化等内容。介绍了PyTorch搭建与训练CNN模型的方法,阐述卷积层、激活函数等网络层原理,还提及常见问题及解决办法,以及进阶的项目开发经验、权重参数计算等知识。
该文章已生成可运行项目,

文章目录

一、项目实战

卷积神经网络的发展历史(超详细介绍)

卷积神经网络(Convolutional Neural Network,CNN):是一种前馈神经网络,最早可追溯到1986年的BP算法。

  • 三大网络结构:卷积层(Conv2d)、池化层(MaxPool2d)、全连接层(Linear)
  • 两大特点:稀疏连接、权值共享(使用更少的参数,获得更高的性能)

1、PyTorch:搭建 CNN 模型

在这里插入图片描述

# 胖墩会武术 - 20241107
import torch
import torch.nn as nn
import torch.nn.functional as F


class SimpleCNN(nn.Module):
    def __init__(self):
        """__init__:定义网络结构"""
        super(SimpleCNN, self).__init__()  # 作用: 在 SimpleCNN 类中,使用 self 来调用父类的 nn.Module 的初始化方法。
        # SimpleCNN   :当前子类的名字。
        # self        :当前实例,传入 super() 以告诉它要操作的是当前对象。
        # __init__    :表示调用的是 nn.Module 中的初始化方法。
        
        # 定义模型的各个层
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=5, stride=1, padding=0)   # 卷积层1:输入通道 3,输出通道16,卷积核大小5x5
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)  # 最大池化层1
        self.conv2 = nn.Conv2d(in_channels=16, out_channels=36, kernel_size=3, stride=1, padding=0)  # 卷积层2:输入通道16,输出通道32,卷积核大小3x3
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)  # 最大池化层2

        self.fc3 = nn.Linear(6 * 6 * 36, 128)  # 全连接层3:将多维特征图展平为一维向量(6 * 6 * 36 = 1296)
        self.fc4 = nn.Linear(128, 10)  # 全连接层4(输出层)

    def forward(self, x):
        """forward:前向传播过程"""
        x = self.pool1(F.relu(self.conv1(x)))        # 卷积层1 -> 激活函数(relu) -> 池化层1
        x = self.pool2(F.relu(self.conv2(x)))        # 卷积层2 -> 激活函数(relu) -> 池化层2
        x = x.view(-1, 6 * 6 * 36)                  # 展平操作(参数-1表示自动调整size)
        x = F.relu(self.fc3(x))                # 全连接层3 -> 激活函数(relu)
        x = self.fc4(x)                        # 输出层(最后一层通常不使用激活函数)
        return x


if __name__ == "__main__":
    model = SimpleCNN()  # 模型实例化
    #############################################################################
    input_data = torch.randn(1, 3, 32, 32)  # batch_size=1, 通道数=3, 图像尺寸=32x32
    output = model(input_data)  # 前向传播
    output_forward = model.forward(input_data)  # (显示)前向传播
    #############################################################################
    print("模型结构:\n", model)
    print("模型输出(十分类):\n", output)
    print(output_forward)

"""
模型结构:
SimpleCNN(
  (conv1): Conv2d(3, 16, kernel_size=(5, 5), stride=(1, 1))
  (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv2): Conv2d(16, 36, kernel_size=(3, 3), stride=(1, 1))
  (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc3): Linear(in_features=1296, out_features=128, bias=True)
  (fc4): Linear(in_features=128, out_features=10, bias=True)
)
#####################################################################################
模型输出(十分类):
tensor([[ 0.1202,  0.0287, -0.0160,  0.0384,  0.0442,  0.0127, -0.0626, -0.1158,
         -0.0499,  0.1266]], grad_fn=<AddmmBackward0>)
tensor([[ 0.1202,  0.0287, -0.0160,  0.0384,  0.0442,  0.0127, -0.0626, -0.1158,
         -0.0499,  0.1266]], grad_fn=<AddmmBackward0>)
"""

1.1、面向对象编程(object-oriented programming,OOP )

(1)类(Class)和对象(Object)

在面向对象编程(OOP)中,类(Class) 和 对象(Object) 是两个非常重要的概念。

类本身并不占用内存,只有通过类实例化(创建对象)之后,才会在内存中分配空间。

  • 类(Class):是一个模板或蓝图,定义了一个对象的属性(成员变量)和方法(成员函数),从而规范了对象的行为和状态。类并不是实际的数据,而是定义数据结构的框架。
    • 类通常由两部分组成:
      • 属性(Attributes):类中的数据成员(成员变量或字段),用于保存对象的状态。
      • 方法(Methods):类中的函数(成员函数或行为),用于定义对象的行为和操作。
        • 构造函数__ init __在 Python 中,每个类都有一个特殊的构造函数,它会在创建对象时自动调用,用来初始化对象的属性。
  • 对象(Object)是类的实例化。通过类实例化(创建对象)之后,实际的内存空间会根据类的定义分配,且对象可以有不同的属性值。
    • 一个类可以创建多个对象,虽然它们共享同一个类,但它们的属性值可以不同。
class Car:
    """定义一个类(Car)"""
    def __init__(self, brand, model, year):
        """(对象方法)__init__ 是类的初始化方法(构造函数),用于创建类的对象时初始化属性。"""
        self.brand = brand  # (属性)品牌
        self.model = model  # (属性)型号
        self.year = year    # (属性)年份
        self.mileage = 0    # (属性)初始里程为0

    def drive(self, distance):
        """(对象方法)描述汽车的行为,接受一个行驶的距离,增加里程数"""
        self.mileage += distance
        print(f"{self.brand} {self.model} 行驶了 {distance} 公里,累计里程 {self.mileage} 公里。")

    def display_info(self):
        """(对象方法)打印汽车的详细信息"""
        print(f"品牌: {self.brand}")
        print(f"型号: {self.model}")
        print(f"年份: {self.year}")
        print(f"累计里程: {self.mileage} 公里")


if __name__ == '__main__':
    # (1)类的实例化 ———— 通过类 Car 创建一个名为 my_car 的对象,并传入品牌、型号和年份作为参数。
    my_car = Car("Toyota", "Camry", 2020)  # 实例化1
    my_car2 = Car("Toyota2", "Camry2", 20202)  # 实例化2
    # (2)对象的方法
    my_car.display_info()  # 打印汽车信息(默认)
    my_car.drive(50)       # 行驶50公里
    my_car.drive(100)      # 再行驶100公里
    my_car.display_info()  # 打印汽车信息(更新)

"""
品牌: Toyota
型号: Camry
年份: 2020
累计里程: 0 公里
Toyota Camry 行驶了 50 公里,累计里程 50 公里。
Toyota Camry 行驶了 100 公里,累计里程 150 公里。
品牌: Toyota
型号: Camry
年份: 2020
累计里程: 150 公里
"""
类方法的第一个参数(self):表示类的实例

在 Python 中,类方法的第一个参数(self)表示类的实例。这个 self 关键字是 Python 中实例方法的约定用法。

作用如下:

  • 指向实例: 当我们调用一个类的方法时,Python 会自动将类的实例作为第一个参数传递给该方法。这个实例可以通过 self 访问。
  • 访问实例属性和方法: 通过 self,可以访问类的实例属性(例如 self.attribute)和其他实例方法。

举例:
(1)在 Car 类中,self 用于表示每个实例本身。当我们创建一个 Car 类的实例(my_car)时,self 指向 my_car;
(2)当你调用 my_car.drive() 方法时,self 会自动指向 my_car,所以 drive 方法可以访问到 my_car 的 brand、model、year 和 mileage 属性。

② 是否需要在类定义时添加括号 ()

在定义类时,是否需要添加括号取决于是否指定了父类:

  • 不加括号class ClassName:在 Python3 中,所有类默认继承自 object(称为“新式类”),因此不加括号等价于class MyClass(object):。如果没有显式父类,可以省略括号。
  • 加括号class ClassName(BaseClass):如果一个类需要继承其他类(比如父类、接口、事件基类等),则必须在括号中写出父类名称。
(2)类的继承(Inheritance)

继承:是面向对象编程的一个重要特性。它允许我们通过创建一个新类来继承已有类的属性和方法,称为子类(派生类)继承父类(基类)的属性和方法。

  • 继承可以重用父类的代码,也可以在子类中扩展或修改父类的行为。
class Car:
    """定义一个类(Car)"""
    def __init__(self, brand, model, year):
        """(对象方法)__init__ 是类的初始化方法(构造函数),用于创建类的对象时初始化属性。"""
        self.brand = brand  # (属性)品牌
        self.model = model  # (属性)型号
        self.year = year    # (属性)年份
        self.mileage = 0    # (属性)初始里程为0

    def drive(self, distance):
        """(对象方法)描述汽车的行为,接受一个行驶的距离,增加里程数"""
        self.mileage += distance
        print(f"{self.brand} {self.model} 行驶了 {distance} 公里,累计里程 {self.mileage} 公里。")

    def display_info(self):
        """(对象方法)打印汽车的详细信息"""
        print(f"品牌: {self.brand}")
        print(f"型号: {self.model}")
        print(f"年份: {self.year}")
        print(f"累计里程: {self.mileage} 公里")


class ElectricCar(Car):
    def __init__(self, brand, model, year, battery_capacity):
        super().__init__(brand, model, year)
        self.battery_capacity = battery_capacity

    def charge(self):
        print(f"{self.brand} is charging.")


if __name__ == '__main__':
    ev_car = ElectricCar("Tesla", "Y", 2022, 100)
    ev_car.display_info()
    ev_car.charge()

"""
品牌: Tesla
型号: Y
年份: 2022
累计里程: 0 公里
Tesla is charging.
"""
(3)封装(Encapsulation)

封装是将对象的属性和方法隐藏在类内部,并通过公共接口(方法)与外界进行交互。封装有助于数据的安全性,防止外部直接修改对象的内部状态。

(1)__brand 是私有属性,不能直接从外部访问。
(2)通过 get_brand() 和 set_brand() 方法来获取和设置属性。

class Car:
    """定义一个类(Car)"""
    def __init__(self, brand, model, year):
        """(对象方法)__init__ 是类的初始化方法(构造函数),用于创建类的对象时初始化属性。"""
        self.__brand = brand  # (属性)品牌
        self.__model = model  # (属性)型号
        self.__year = year    # (属性)年份
        self.__mileage = 0    # (属性)初始里程为0

    def get_brand(self):
        return self.__brand

    def set_brand(self, brand):
        self.__brand = brand

    def drive(self, distance):
        """(对象方法)描述汽车的行为,接受一个行驶的距离,增加里程数"""
        self.__mileage += distance
        print(f"{self.__brand} {self.__model} 行驶了 {distance} 公里,累计里程 {self.__mileage} 公里。")

    def display_info(self):
        """(对象方法)打印汽车的详细信息"""
        print(f"品牌: {self.__brand}")
        print(f"型号: {self.__model}")
        print(f"年份: {self.__year}")
        print(f"累计里程: {self.__mileage} 公里")


if __name__ == '__main__':
    car = Car("Tesla", "Y", 2022)
    print(f"品牌: {car.get_brand()}")  # 获取品牌
    car.set_brand("BMW")  # 设置品牌
    print(f"品牌: {car.get_brand()}")  # 获取更新后的品牌
"""
品牌: Tesla
品牌: BMW
"""
(4)多态(Polymorphism)

多态:是指不同的对象可以对相同的方法做出不同的响应。多态可以通过方法重载或方法覆盖来实现。在子类中重写父类的方法就是方法覆盖(Method Overriding),使得同一方法在不同类的对象中具有不同的行为。

class Animal:
    def speak(self):
        print("Animal is making a sound.")

class Dog(Animal):
    def speak(self):
        print("Dog is barking.")

class Cat(Animal):
    def speak(self):
        print("Cat is meowing.")

if __name__ == '__main__':
    animals = [Dog(), Cat()]
    for animal in animals:
        animal.speak()
"""
Dog is barking.
Cat is meowing.
"""

1.2、模型的类函数:__ init __ () 、 forward() 、 __ call __ () 、 super()

通过分布教程,展示了 call() 函数是如何自动调用 init() 函数,然后调用 forword() 函数。

  • __init__() 构造函数每当创建类的实例时,都会自动调用它。
    • 用于定义和初始化模型的层结构,如(卷积层、池化层、全连接层等),以及设置层的超参数(比如卷积核的大小、卷积层的输入输出通道数等)。
  • forward() 前向传播函数每次模型接收到输入数据时,PyTorch 会自动调用 forward() 函数。
    • 用于定义 " 数据流动 " 过程:每次输入一个或 batch 数据,都会依次经过各个层(卷积、池化、ReLU 激活等),最后输出结果。
  • __call__() 特殊函数可以像调用函数一样调用对象,使得模型的调用变得非常直观。
    • 在 PyTorch 中:(1)__ call __ 函数已经由 nn.Module 类隐式定义和实现了,因此我们不需要显式地在子类中重新定义它。(2)根据当前模型是处于训练模式(model.train())还是评估模式(model.eval()),自动切换模型的行为。如 Dropout 和 Batch Normalization 在训练和评估时会有所不同。
    • 当你调用 model(input_data) 时,实际上是调用了模型的 __ call __ 方法,其会自动调用 forward() 方法,完成前向传播的计算。而不需要(显示地)手动调用 model.forward(input_data)。这也是为什么 PyTorch 模型的使用是 model(input_data) 而不是 model.forward(input_data),因为 call 自动完成了这一过程。
  • super() 常用函数用于调用父类(基类)的初始化方法
    • 在 PyTorch 中,nn.Module 是所有神经网络模型的基类。nn.Module 实现了大量管理模型、参数和行为的基础设施(例如自动求导、参数初始化等)。当我们创建一个新的模型类时,继承 nn.Module 可以使我们自动获得这些功能。
    • 但是,仅仅继承 nn.Module 并不足够,我们还需要在子类中显式地调用父类的 __ init __ 方法。如:nn.Module 提供的功能之一是自动注册模型的所有参数。当我们在模型中定义层(如 nn.Conv2d、nn.Linear 等)时,PyTorch 会将这些层的参数加入模型的参数列表中。如果不调用父类的 init,这些参数不会被注册,模型也不会知道这些参数的存在,从而导致无法进行参数更新和梯度计算。
    • 若不显式调用,则模型初始化不完整,导致训练和评估时出现各种问题。

1.3、类函数: __ init __ 、 __ len __ 、 __ getitem __

import torch
from torch.utils.data import Dataset, DataLoader

class CustomDataset(Dataset):
    def __init__(self, data, labels):
        self.data = data
        self.labels = labels

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        return self.data[idx], self.labels[idx]

if __name__ == '__main__':
    data = torch.randn(100, 3)  # 100个样本,每个样本有3个特征
    labels = torch.randint(0, 2, (100,))  # 100个样本,每个样本的标签为0或1

    dataset = CustomDataset(data, labels)
    dataloader = DataLoader(dataset, batch_size=10, shuffle=True)

    for batch_data, batch_labels in dataloader:
        print("批数据:", batch_data)
        print("批标签:", batch_labels)
        break
"""
批数据: tensor([[ 0.4106, -0.8040,  0.1003],
    [ 0.5072,  0.3028, -0.5967],
    [ 0.8407, -0.6981,  2.1016],
    [ 0.3916,  0.9360,  0.6978],
    [-0.4918,  1.6023, -0.0661],
    [-1.1485, -1.0621,  1.0461],
    [ 0.3963, -0.4333, -0.7826],
    [ 0.0556,  1.5501, -0.5498],
    [ 3.7245, -0.0163, -0.9922],
    [-0.8662, -0.9823, -0.8796]])
批标签: tensor([1, 0, 1, 1, 1, 0, 1, 1, 1, 1])
"""

2、PyTorch:完成 CNN 模型的训练 + 验证 + 测试(保存与加载模型)

【Pytorch项目实战】手写数字识别(MNIST)、普适物体识别(CIFAR-10)

二、前向传播(Forward propagation)

函数定义:def forward(self, x):
函数调用:outputs = net(inputs)

作用:实现信息的前向传导。
过程:将数据从输入层,依次经过多个隐藏层(如果有),最终到达输出层。其中,数据每经过一个网络层,其节点输出的值所代表的信息层次就越高阶和概括。

例如:人脸识别。

  • (1)输入图像:每个像素值,只表示灰度值;
  • (2)经过第一个网络层:(可能)第二层中每个节点输出的值,表示边缘特征(如:脸部轮廓);
  • (3)经过第二个网络层:(可能)第三层中每个节点输出的值,表示局部特征(如:鼻子、眼睛);
  • (4)最后输出层:表示网络对输入图像的判断结果,即是否是一张人脸。

备注:节点中输出的值是通过与其相连的前一层中所有的节点输出值的加权求和处理后的结果。

1、卷积层:torch.nn.Conv2d()

torch.nn.Conv2d(in_channels=3, out_channels=16, kernel_size=5, stride=1, padding=0)

卷积神经网络的核心网络层是卷积层,而卷积层的核心是卷积。

PyTorch:torch.nn.Conv2d()函数的图解
PyTorch:conv1D,conv2D和conv3D

(1)卷积运算 —— 提取特征

卷积运算就是互相关( cross-correlation )运算。
在这里插入图片描述
卷积运算卷积核中的 2 分别乘以输入中的每个值,然后输出卷积结果。
其中,输入、输出和卷积核都是张量,卷积核(kernel)又叫权重过滤器,简称过滤器(Filter)。

卷积运算公式
(1)假设输入图像大小是input x input,卷积核大小是kernel,补0的圈数为padding,步长为stride,卷积后输出特征图大小为output x output
在这里插入图片描述
(2)图像高度和宽度的计算公式:
在这里插入图片描述
备注:若输入图像为 m x n,想要转换为 m x m 或 n x n 的图像,则可以采用裁剪、拼接、填充的方法。

(2)稀疏连接 —— 降低了模型的复杂度,防止过拟合

【输入条件】:(1)输入单通道图像大小 3 x 3;(2)卷积核大小 2 x 2;
【分析说明】

  • 卷积运算的详细过程:(1)取(3 x 3)输入图像中的(2 x 2)矩阵,然后与(2 x 2)卷积核进行卷积运算,得到一个计算结果0 * 0 + 1 * 1 + 3 * 2 + 4 * 3 = 19);(2)通过滑窗,循环计算得到输出特征图(2 x 2)。
  • 特征图中的每个元素(即:神经元)只与上一层的所有神经元中的4个进行连接。

【结论】:稀疏连接相比于全连接,大大缩减了权重参数数量,也极大避免了过拟合。
在这里插入图片描述

(3)权值共享 —— 减少权值数量,模型易优化

在这里插入图片描述
输入图像

  • (1)输入彩色图像Input = 8 * 8 * 3;其中,3表示RGB三个通道
  • (2)卷积层的卷积核个数为Kernels = 5每一个 Kernel 都有 3 个channel(3 * 3 * 3),即共有 5 个特征图(3 * 3 * 3 * 5)

卷积运算(多通道)

  • 1、Kernels卷积核计算
    • 每个 Kernel 的 3 个channle(R/G/B)分别与 Input 对应的 3 个channle(R/G/B)进行卷积运算,最后每个 Kernel 得到 3 个(6 * 6) feature_map_0
  • 2、Feature Maps特征图计算
    • 将 3 个(6 * 6)的 feature_map0 逐元素相加(即通道融合)得到 1 个 feature_map_1
    • 然后将 feature_map1 的每个元素都加上其对应的偏置 b1,得到该 Kernel 的 FeatureMap1
      • 同一个 Kernel1 对应的 3 个 channle 共享一个偏置 b1(权值共享1)
      • 同一个 FeatureMap1 的所有元素共享一个卷积核 Kernel1(权值共享2)
    • 同理其余Kernel,最后得到(6 * 6 * 5) 个 Feature Maps(即最终输出的 channel 数等于 Kernel 的个数)。
  • 3、Feature Vector向量计算
    • 采用全局最大池化,分别计算每一个Feature Map,最终得到一个(1 * 1 * 5)Feature Vector。

结论
(0)权值共享:大大减少了模型的权重参数。
(1)权值共享:当前隐藏层中的所有神经元都在检测图像不同位置的同一个特征,即检测特征相同。因此也将输入层到隐藏层的这种映射称为特征映射。
(2)多个卷积核分别用于识别图像不同位置的特征。通过全连接层将所有特征整合,输出一个值(猫 / 狗)。

在这里插入图片描述

(4)权值初始化 —— 决定算法是否收敛

(1)深度学习为何要初始化? —— 影响算法结果

  • 深度学习算法:由于迭代训练 + 网络层数多 + 权重参数多
  • 机器学习算法:影响小。

(2)初始化对训练的影响?—— 决定算法是否收敛

  • (1)若参数太大,将导致在前向传播或反向传播中产生爆炸的值;
  • (2)若参数太小,将导致信息丢失;
  • (3)适当的参数初始化,能加快收敛速度。

偏置初始化

  • 采用启发式的方法挑选常数,进而设置每个单元的偏置。

权重初始化

  • 实验表明:正态分布初始化(高斯分布)、正交分布初始化、均匀分布初始化的效果更好。
  • 权重初始化的方法:零值初始化、随机初始化、均匀分布初始化、正态分布初始化和正交分布初始化等。

(5)感受野 —— 在原始图像上映射的区域大小

感受野(Receptive Field,RF)每个网络层的输出特征图上的像素点,在原始图像上映射的区域大小。
在这里插入图片描述
注意1:感受野的计算不考虑" 边界填充 " 。
注意2:最后一层(卷积层或池化层)输出特征图的感受野大小等于卷积核的大小。
注意3:第(i)层卷积层的感受野大小和卷积核大小,与步长、第(i+1)层的感受野大小都有关。

详细计算过程:根据卷积得到的最后一层感受野,反算前一层感受野,然后循环且逐层的传递到第一层。
公式如下:
在这里插入图片描述

  • 【举例1】:两层3 * 3的卷积核,卷积操作之后的感受野是5 * 5。其中,卷积核的步长为1、padding为0。
    计算如下:(1-1)* 1+3 = 3 => (3-1)*1+3 = 5
    在这里插入图片描述
  • 【举例2】:三层3 * 3卷积核,卷积操作之后的感受野是7 * 7。其中,卷积核的步长为1,padding为0。
    计算如下:(1-1)* 1+3 = 3 => (3-1)*1+3 = 5 => (5-1)*1+3 = 7
    在这里插入图片描述
  • 【举例3】:神经网络
    在这里插入图片描述

❤️ 感受野与卷积核的关系,是否卷积核越大越好?

  • 如果堆叠3个3 * 3的卷积核,并且保持步长为1,其感受野等于7 * 7;那么这和直接使用7 * 7卷积核的结果是一样的,为什么要如此麻烦呢?

在这里插入图片描述

(6)边界填充Padding —— 图像尺寸对齐,保持边界信息

torch.nn.Conv2d(in_channels=3, out_channels=16, kernel_size=5, stride=1, padding=1)

padding=1:表示在输入图像的边界填充 1 圈,全 0 元素。
作用:

  • (1)图像尺寸对齐:对图像进行扩展,扩展区域补零。
  • (2)保持边界信息:若没有padding,卷积核将对输入图像的边界像素只卷积一次,而中间像素卷积多次,导致边界信息丢失。

在这里插入图片描述

假设输入图像大小是 input x input,卷积核大小是 kernel,步长为stride,补0的圈数为padding。
在这里插入图片描述
Python深度学习pytorch神经网络填充和步幅的理解

2、激活函数:torch.nn.Sigmoid() + ReLU() + softmax() —— 增加非线性能力

(1)用于神经网络的隐含层与输出层之间(如:卷积层+激活函数+池化层),为神经网络提供非线性建模能力
(2)若没有激励函数,则模型只能处理线性可分问题。与最原始的感知机相当,拟合能力有限。

左图A - 线性可分:一条直线将图像A中的蓝点和绿点完全分开(最理想、最简单的二分类问题)
右图B - 线性不可分:一条封闭曲线将图像B中的蓝点和绿点完全分开
在这里插入图片描述

深度学习的四种激活函数:PyTorch中激活函数的方法示例在这里插入图片描述
在这里插入图片描述
如何选择激活函数

  • (1)任选其一若网络层数不多
  • (2)ReLU若网络层数较多
    • 不宜选择sigmoid、tanh,因为它们的导数都小于1,sigmoid的导数更是在[0, 1/4]之间。
    • 根据微积分链式法则,随着网络层数增加,导数或偏导将指数级变小。
    • 所以,网络层数较多的激活函数其导数不宜小于1也不能大于1,大于1将导致梯度爆炸,导数为1最好,而relu正好满足这个要求。
  • (3)Softmax用于多分类神经网络输出层一分钟理解softmax函数(超简单)

3、池化层:torch.nn.MaxPool2d() / AvgPool2d() —— 降采样,增大感受野,防止过拟合。

为什么要进行池化操作?

  • 通过卷积层获得的图像特征,可以直接训练分类,但很容易导致过拟合。

池化(Pooling)又叫下采样、降采样。
作用: 降采样(减少参数),增大感受野,提高运算速度及减小噪声影响,防止过拟合。
分类:

  • (1)局部池化
    • 最大池化(Max Pooling):取Pooling窗口内的最大值作为采样值。
    • 均值池化(Average Pooling):取Pooling窗口内的所有值相加取均值作为采样值。
      • torch.nn.MaxPool2d(kernel_size=2, stride=1, padding=0)
      • torch.nn.AvgPool2d(kernel_size=2, stride=1, padding=0)
  • (2)全局池化
    • 全局最大池化(Global Max Pooling):取以整个特征图为单位的最大值作为采样值。
    • 全局均值池化(Global Average Pooling):取以整个特征图为单位的所有值相加取均值作为采样值。
      • torch.nn.AdaptiveMaxPool2d(output_size=(2, 2))
      • torch.nn.AdaptiveAvgPool2d(output_size=(2, 2))

备注:全局池化相比局部池化能减少更多参数,且泛化能力比较好,但唯一的不足之处是收敛速度比较慢。
在这里插入图片描述
Python:池化的用法及代码(示例)

4、全连接层:torch.nn.Linear() —— 分类器

全连接层(Fully Connected Layers,FC)torch.nn.Linear(n_hidden, classification_num)

  • 卷积层、池化层、激活函数 —— 将原始数据映射到隐层特征空间,提取特征

  • 全连接层 —— 将学到的 “分布式特征” 映射到样本标记空间,进行分类

全连接层FC的缺点

  • (1)由于忽略了空间结构,故不适用于目标检测。如:分割任务中的FCN采用卷积层替换全连接层。
  • (2)全连接层的参数过多,导致模型复杂度提升,容易过拟合。如:ResNet、GoogleNet采用全局平均池化替换全连接层。

全连接层详解
一文带你了解CNN(卷积神经网络)

  • 详解1:神经网络在经过卷积+激活后,得到 3 x 3 x 5 的输出结果,它是怎么转换为1 x 4096?在这里插入图片描述
  • 详解2:可以理解为在中间做了一个卷积操作,详细步骤如下:
    • (1)将【3 x 3 x 5特征图】分别与对应的【3 x 3 x 5卷积核】进行【卷积convolution】【不带偏置】,然后通过全连接FC得到一个预测结果【即一个神经元的输出 1 x 1 】
    • (2)当我们有4096个神经元,将得到【4092个神经元的输出 1 x 4096】。

全连接层中的每个神经元都可以看成是一个加权平均且不带偏置的多项式。可以简单写成:y = W * x。

在这里插入图片描述

全连接层FC的作用:这一步卷积还有一个非常重要的作用,就是将分布式特征representation映射到样本标记空间。简单来说,就是将输入图像的所有特征整合到一起,输出一个值(猫 / 狗)
效果:大大减少目标位置的不同,而对分类结果带来的影响。
在这里插入图片描述
下张图是上面任意一张图的图解:
在这里插入图片描述
由图可得:

  • (1)猫虽然在不同的位置输出的特征值相同,但可能分类结果不同。
  • (2)全连接层FC:忽略了空间结构特性。即目标不管在什么位置,只要图像中有这个猫,那么就能判断它是猫。

三、反向传播(Backward propagation)

反向传播:loss.backward()

作用:迭代训练,降低损失(loss)
介绍:与最优化方法(如:梯度下降法)结合使用的,用来训练神经网络的常见方法。

计算过程:通过损失函数,计算模型中所有权重参数的梯度,并反馈给优化算法进行梯度(权值)更新。迭代训练 N 次,获得最小损失。
单次反向传播的过程:首先是离输出层最近的网络层E,然后是网络层D,并按照EDCBA的依次顺序进行反向传播,直到所有层都完成一次。

1、损失函数:torch.nn.CrossEntropyLoss() / MSELoss() —— 衡量模型预测值与真实值之间的差异

损失函数:用来衡量模型预测值与真实值之间的差异。

  • 损失函数越小说明模型和参数越符合训练样本。
  • 任何能够衡量模型预测值与真实值之间差异的函数都可以叫做损失函数。
  • 为了避免过拟合,一般在损失函数的后面添加正则化项(F = 损失函数 + 正则化项)

常用的损失函数:

  • (1)交叉熵(Cross Entropy),用于分类:反应两个概率分布的距离(不是欧式距离)。
    交叉熵又称为对数似然损失、对数损失;二分类时还可称为逻辑回归损失。
    torch.nn.CrossEntropyLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean')
    在这里插入图片描述
    其中:c表示损失值;
        n表示样本数量,也就是batchsize;
        x表示预测向量维度,主要是因为需要在输出的特征向量维度上一个个计算并求和;
        y表示真实值,对应x维度上的标签(1 或 0);
        a表示输出的预测概率值(0~1之间,总和为1)。
  • (2)均方误差(Mean Squared error,MSE),用于回归:反应预测值与实际值之间的欧氏距离。
    torch.nn.MSELoss(size_average=None, reduce=None, reduction='mean')
    在这里插入图片描述

2、正则化:torch.nn.Dropout() —— 提升模型的泛化能力,防止过拟合。

正则化(Regularization):对于减少测试误差的方法的统称。
作用: 降低模型的复杂度,减少模型对训练样本的依赖度,提升模型的泛化能力,防止过拟合。
在这里插入图片描述

思路在损失函数的后面添加一个惩罚项。
作用:正则化系数 λ :控制模型的可学习参数,防止过拟合。
计算过程(求解线性模型):找到一个 θ 值,然后对 J(θ) 进行求导,使得代价函数 J(x) 取得最小值(导数为0)。
在这里插入图片描述

常用的惩罚项:L1正则化、L2正则化、Dropout正则化

  • 在机器学习的线性回归模型中,使用L1正则化得到的模型叫Lasso模型,使用L2正则化得到的模型叫岭回归(Ridge regression)

(1)L1正则化(特征选择) —— 冗余参数置零

  • L1正则化(L1范数),通常表示为:||W||1指权值向量 W 中各个元素的绝对值之和。
  • 特点:又叫特征选择,产生稀疏权值矩阵。即提取权重值最大的前N个值,并将冗余的权重值置0,直接删除。

(2)L2正则化(权重衰减) —— 过拟合参数趋近于0

  • L2正则化(L2范数),通常表示为:||W||2指权值向量 W 中各个元素的平方和,然后求平方根。
  • 特点:又叫权重衰减。即抑制模型中产生过拟合的参数,使其趋近于0(而不等于0),影响变小。
  • 特点:倾向于让所有参数的权值尽可能小。

若不关心显式特征(哪些特征重要或不重要),L2正则化的性能往往优于L1正则化。

(3)Dropout正则化 —— 随机删除一定比例的神经元

torch.nn.Dropout(p=0.5) # 表示随机删除 50% 的神经元

  • Dropout正则化在训练过程中,随机删除一定比例的神经元(比例参数可设置)。
  • 一般只在训练阶段使用,测试阶段不使用。
  • 一般控制在20% ~ 50%。太低没有效果,太高则会导致模型欠学习。

应用场景
(1)在大型网络模型上效果显著
(2)在输入层和隐藏层都使用Dropout
(3)当学习率和冲量值较大时:如学习率扩大10 ~ 100倍,冲量值调高到0.9 ~ 0.99。
(4)用于限制网络模型的权重时:学习率越大,权重值越大。
在这里插入图片描述

3、优化器:torch.optim.SGD() / Adam()

optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

优化器(optimizer):通过优化策略(梯度下降)来更新可学习参数(权值W和偏置bias),使得损失函数Loss值逐步降低,输出的模型更接近真实标签。
影响因素:(1)梯度方向(2)学习率
 
常用优化器

  • 00、经典的梯度下降法。
  • 11、梯度下降优化算法:SGD、SGDM、NAG
       缺点:缓解了参数空间的方向问题,但需要新增参数,且对学习率的控制也不太理想。
  • 22、自适应优化算法:AdaGrad(累积梯度平方)、RMSProp(累积梯度平方的滑动平均)、Adam(带动量项的RMSProp)
       而自适应优化算法,学习率不再是一个固定不贬值,它会根据实际情况自动调整以适应环境。

(1)梯度下降 —— 使得loss值逐步降低

梯度清零:optimizer.zero_grad()
梯度更新:optimizer.step()

梯度下降的公式如下图:
在这里插入图片描述
其中:
   θ(t+1):表示参数θ在t+1次迭代时需要更新的参数;
   θ(t):表示参数θ在第t次迭代时更新的参数值;
   J(θt):表示目标函数(损失函数)在θ(t)该点上的梯度;
   lr:表示学习率。其控制参数更新速度以及模型学习速度。

梯度下降:沿着目标函数梯度的反方向搜索极小值。在这里插入图片描述
梯度下降算法原理讲解

  • 经典的梯度下降法先假设一个学习率,参数沿梯度的反方向移动。
  • 计算:每次迭代过程中,采用所有的训练数据的平均损失来近似目标函数。
  • 特点:(1)靠近极值点时收敛速度减慢;(2)可能会" 之 "字形的下降。(3)对学习率非常敏感,难以驾驭;(4)对参数空间的方向没有解决方法。
    在这里插入图片描述
  • 左图一 学习率过小,将导致收敛速度慢,耗时长;
  • 中图二 当学习率取值恰当时,可以收敛到全面最优点(凸函数)或局部最优点(非凸函数)。
  • 右图三 学习率过大,将导致越过极值点(如:在平坦的区域,因梯度接近于0,可能导致训练提前终止)
    备注: 可能出现在迭代过程中保持不变,容易造成算法被卡在鞍点的位置。

(2)SGD、SGDM、NAG

  • SGD(随机梯度下降法)
    计算:每次只使用一个数据样本,去计算损失函数,求梯度,更新参数。
    特点:(1)计算速度快,但是梯度下降慢(2)可能会在最低处两边震荡,停留在局部最优。
  • SGDM(SGM with Momentum:动量梯度下降)
    计算:对已有的梯度进行指数加权平均,然后加上(1 - beta)。即在现有梯度信息的基础上,加入一个惯性。而梯度方向,由之前与现在的梯度方向共同决定。
    优缺点:(1)与SGD相比,训练过程的震荡幅度会变小,速度变快。(2)SGDM速度没Adam快,但泛化能力好。
  • NAG(Nesterov Accelerated Gradient)
    计算:先使用累积的动量计算一次,得到下一次的梯度方向,再把下一个点的梯度方向,与历史累积动量相结合,计算现在这个时刻的累计动量。
    在这里插入图片描述

(3)AdaGrad、RMSProp、Adam

不同优化算法(理论推导)

  • AdaGrad(Adaptive Gradient,自适应步长)累积梯度平方
    • 计算:在现有的(梯度 * 步长)上,添加了一个系数:1 /(历史梯度的平方和,再开根号)。
    • 优缺点:(1)适合处理稀疏数据,可以对稀疏特征进行大幅更新。(2)学习率单调递减,最终会趋于0,学习提前终止。
  • RMSProp(root mean square prop,梯度平方根)累积梯度平方的滑动平均
    • 计算:该方法和Adagrad的区别就是分母不一样,使得系数不会因为前几步的梯度太大而导致分母太大,从而导致系数变得太小而走不动。
  • Adam(Adaptive Moment Estimation,自适应估计)利用梯度的一阶、二阶矩估计
    • 优缺点:Adam是一种在深度学习模型中用来替代随机梯度下降的优化算法。它是SGDM和RMSProp算法的结合,训练速度快,泛化能力不太行。

MNIST数据集:多种优化器的收敛效果对比
在这里插入图片描述

4、学习率:Learning Rate —— 控制可学习参数的更新速度

optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

学习率(learning rate,lr)用于控制权重参数的学习速度一般情况下,学习率随着epoch的增大而逐渐减小,可以达到更好的训练效果。

学习率学习速度适用时期适用范围缺点
模型刚训练时[0.01,0.001]易震荡;模型不收敛;
模型训练后期[0.001,0.00001]局部收敛,找不到最优解;

为什么加入学习率?

  • (1)神经网络通过梯度下降+反向传播进行参数更新
    • 主要流程:将可学习参数(权值W和偏置bias)反向传播给网络层,使得损失函数Loss值逐步降低,输出的模型更接近真实标签。
  • (2)神经网络在参数学习过程中,需要多轮训练,而每一论训练得到的可学习参数,需要通过学习率来控制学习速度,使得下一轮训练时的损失更小,最终达到最优模型

(1)自定义更新学习率(根据epoch)

for epoch in range(num_epoch):
    if epoch % 5 == 0:      
        optimizer.param_groups[0]['lr'] *= 0.1       

(2)动态调整学习率:torch.optim.lr_scheduler

  • optimizer.step()用于在每 1 个 batch_size 中,更新一次参数;
  • scheduler_StepLR.step()用于在每 1 / N个 epoch 中,更新一次参数
""" torch.optim.lr_scheduler 动态调整学习率 """
torch.optim.lr_scheduler.LambdaLR()				:通过自定义 Lambda 函数来设置每个参数组的学习率。
torch.optim.lr_scheduler.MultiplicativeLR()		:通过指定乘法因子来调整每个参数组的学习率。
torch.optim.lr_scheduler.StepLR()				:在每个给定的步骤上降低学习率,步骤由用户定义。
torch.optim.lr_scheduler.MultiStepLR()			:在每个给定的步骤上降低学习率,步骤由用户定义。
torch.optim.lr_scheduler.ExponentialLR()		:每个周期按指数衰减学习率。
torch.optim.lr_scheduler.CosineAnnealingLR()	:每个周期按余弦函数衰减。
# scheduler.step()  			# 上述方法不需要输入参数

torch.optim.lr_scheduler.ReduceLROnPlateau()	:根据验证集的性能指标来调整学习率。
# scheduler.step(metrics)  		# metrics参数:表示验证集的性能指标
# 若验证集的损失值在连续 patience 个 epoch 中都没有减少,则学习率会乘以 factor 参数(通常是一个小于 1 的值)以降低学习率。

"""###########################################################################################
# 函数功能:在每个指定的步骤(epoch)之后,将学习率降低一个给定的因子(factor)。
# 函数说明:torch.optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1, last_epoch=-1)
# 参数说明:
# 		(1)optimizer :			需要调整学习率的优化器。
# 		(2)step_size(int):		每训练step_size个epoch,更新一次参数;
# 		(3)gamma(float):		学习率调整的乘法因子;
# 		(4)last_epoch (int):	最后一个epoch的index。
#               当训练N个epoch后中断,然后继续训练时,该值等于加载模型的epoch。
#               默认为-1:表示从头开始训练,即从epoch=1开始。
###########################################################################################"""

"""###########################################################################################
# 函数功能:根据验证集的性能指标(如损失值或准确率)来自动调整学习率(下降)。
# 函数说明:scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=5,
#                           verbose=True, threshold=0.0001, threshold_mode='rel', cooldown=0, min_lr=0, eps=1e-08)
# 参数说明:
#       optimizer:      需要调整学习率的优化器。
#       mode:           监控指标的模式,可以是 "min" 或 "max",分别表示监控指标越小越好,还是越大越好。
#       factor:         学习率调整的缩放因子,即在性能不再改善时将学习率乘以该因子。
#       patience:       指标不再改善时等待的周期数,等待 patience 个周期后学习率才会被调整。
#       threshold:      控制学习率是否被调整的阈值,例如如果 mode='min',当指标监控的值下降小于 threshold 时,学习率会被调整。
#       threshold_mode: 阈值模式,可以是 'rel' 或 'abs',分别表示阈值是相对值还是绝对值。
#       cooldown:       调整学习率后等待的周期数,防止在连续多个周期内反复调整学习率。
#       min_lr:         学习率的下限,学习率不会被调整到低于这个值。
#       eps:            防止除零错误的小值。
#
# 备注:若验证集的损失值在连续 patience 个 epoch 中都没有减少,则学习率会乘以 factor 参数(通常是一个小于 1 的值)以降低学习率。
###########################################################################################"""
import torch.nn as nn
import torch.optim.lr_scheduler


class Model(nn.Module):
    # 构建网络模型
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3)

    def forward(self, x):
        pass


if __name__ == '__main__':
    # (1)超参数初始化
    lr = 0.1
    epochs = 5
    batch_size = 64

    # (2)模型实例化
    model = Model()
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)  # 优化器

    # scheduler_StepLR = torch.optim.lr_scheduler.StepLR(optimizer_1, step_size=3, gamma=0.1)
    # scheduler_LambdaLR = torch.optim.lr_scheduler.LambdaLR(optimizer_1, lr_lambda=lambda epoch: 1/(epoch+1))
    # scheduler_MultiStepLR = torch.optim.lr_scheduler.MultiStepLR(optimizer_1, milestones=[3, 7], gamma=0.1)
    scheduler_ExponentialLR = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.1)
    # scheduler_ReduceLROnPlateau = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer_1, mode='min', factor=0.1, patience=2)
    print("初始化的学习率:", optimizer.defaults['lr'])

    # (3)迭代训练
    for epoch in range(epochs):
        for batch in range(batch_size):
            model.train()  # train模型
            optimizer.zero_grad()  # 提取清零
            optimizer.step()  # 参数更新

        # scheduler_StepLR.step()  # 学习率更新
        # scheduler_LambdaLR.step()
        # scheduler_MultiStepLR.step()
        scheduler_ExponentialLR.step()
        # scheduler_ReduceLROnPlateau.step()
        print("第%d个epoch的学习率:%f" % (epoch, optimizer.param_groups[0]['lr']))

"""
初始化的学习率: 0.1
第0个epoch的学习率:0.010000
第1个epoch的学习率:0.001000
第2个epoch的学习率:0.000100
第3个epoch的学习率:0.000010
第4个epoch的学习率:0.000001
"""

四、模型训练(优化)

1、超参数(类型 + 经验值)

超参数:影响网络模型的性能。通过参数调整(调参),以寻找模型的全局最优解。

  • 调参是一门技术活。
  • 超参数是模型训练前设置的模型参数,而不是训练后得到的参数。
超参数名称经验范围介绍
Epoch周期50、100一个Epoch:将整个训练集完成一次训练的过程
Batch Size批大小8、16、32、64将整个训练集分成 N 个 Batch Size 大小的子数据集,然后迭代训练。
Learing Rate学习率0.01,0.001在训练过程中,学习率(先)取较大值以加快训练;其后需逐渐衰减,使模型收敛。
weight decay权重衰减[0, 0.0001]用于抑制损失函数的优化参数,避免过拟合;通常使用建议值,不必过多尝试。
Dropout正则化[0,0.5]随机删除一定比例的神经元,避免过拟合。
momentum优化器的动量[0, 1]控制了梯度更新的速度和方向,即当前方向考虑之前更新方向的程度比。常用值=0.9
kernel_size卷积核大小1x1、3x3、5x5(1)在相同感受野下,卷积核与参数越小,则计算量越小;(2)一般取卷积核=3;(3)【=1】不改变感受野;(4)通常取奇数,【=偶数】无法保证输入与输出的特征图尺寸不变;
kernel_channels卷积层的通道数1、3、5(1)数量越多,提取特征越多;(2)网络性能先上升后下降
超参数类型介绍
优化器SGD、Adam(1)逐步降低Loss,更接近真实标签;(2)例如:Adam优化器中的参数β1,β2,ϵ(通常取默认值);
池化层最大池化层、均值池化层(1)降采样,避免过拟合(2)卷积核大小:取一半
激活函数Sigmoid、ReLu(1)增加非线性能力;(2)依据网络层的深度,选择类型
网络层数\(1)网络层数越多,更" 接近现实 ",但也更容易过拟合。(2)网络层数越多,训练难度越大,模型越难以收敛。

(1)Epoch

一个Epoch(周期)将整个训练集在模型上完成一次训练的过程,包括前向传播和反向传播。

多个 Epoch 训练(目的):实现模型的迭代学习,充分学习数据集中的特征并提高性能,直到收敛到最佳性能或达到停止条件。

  • 在训练过程中,可以根据模型的性能和训练效果来决定训练的 Epoch 数量。
  • 通常情况下,Epoch 数量越多,模型的性能可能会越好,但也可能会导致过拟合的问题。因此,在选择 Epoch 数量时需要进行适当的调整和权衡。

(2)Batch Size

Batch Size(批大小)在训练过程中,每次输入模型的样本数量。

  • batch_size 经验值 = 8、16、32、64、128 等等 —— (GPU对 2 的幂次的 batch 可以发挥更佳的性能)
  • 影响因素:整个训练集大小、硬件资源、训练速度、收敛速度和模型稳定性等
  • 如何选择最优的 Batch Size ? (具体问题具体分析)测试不同的 batch_size,并根据测试结果不同调整,从而找到适合当前任务的最佳设置。

Batch Size:决定了梯度下降的方向。

(1)Batch Size 较小:噪声对模型的影响越大,降低了模型的泛化性能。
(2)Batch Size 较大:噪声对模型的影响越小,更容易学习到真实数据。

  • 训练速度提升:使用GPU多线程并行处理,加速参数更新过程,直到达到时间最优。
    • 前提:显卡显存支持,否则明显减慢。 —— 若超过显存(专用 GPU 内存),则超出部分转移到(共享 GPU 内存),此时速度会明显减慢。
    • 若(创建和销毁线程的时间开销)小于(将Batch_size数据从CPU上传到GPU),则加速,反之减速。
  • 收敛速度变慢:导致梯度估计的不准确性和方差较大,使得模型需要更多的 Epoch 来收敛到较优的解。
  • 泛化性能增强:提供更多的样本信息来估计梯度,使得模型更容易学习数据集的特征,并提高泛化性能。而较小的 batch-size 下更易受到噪声影响。
  • 最终收敛精度提升:可以减少梯度的方差,使得参数更新的方向更加稳定,从而更容易收敛到较优的解。
  • (3)较小的 Batch Size(适用于中 / 小样本) —— 如:几百到几千个样本
    • batch_size 经验范围:小几十以内
    • 优点:降低内存消耗、模型收敛速度更快
    • 缺点:梯度的方差较大,使得参数更新的方向不稳定(梯度震荡大),从而增加训练的不确定性。
  • (4)较大的 Batch Size(适用于大样本) —— 如:几万到几百万个样本
    • batch_size 经验范围:大几十到几百,但一般不会超过几千。
    • 减少梯度的方差,使得参数更新的方向更加稳定(梯度震荡小),但也可能陷入局部最优。
    • 充分利用硬件资源:使用多线程并行计算,提高训练效率。
    • 缺点:内存占用增加,特别是在处理大型数据集或单个数据较大的情况下。
  • (5)Batch Size = all:全部数据集的梯度方向,能够更好的代表样本总体,确定其极值所在。只适用于小样本,且大样本有内存限制。

(3)Epoch、batch_size、patch_size的关系

(1)在模型训练过程中,通常将整个训练集完成多次迭代训练;
(2)每一次训练通常将整个数据集划分为若干个批次(每个批次包含多个样本),然后每次输入一个批次数据到模型中。
(3)每个批次数据在输入模型训练之前,通常采用图像预处理(灰度化 + 归一化 + 去噪 + 裁剪等等)。

  • Epoch(周期):表示将整个训练集在模型中的完成一次训练的过程;
    • 在每个Epoch中,通常都会采用 " 数据集随机打乱 " 的策略,提高模型的泛化能力。
  • batch_size(批大小):表示每次输入到模型的样本数量;
    • 如:训练集样本数 = 500,Batch Size = 32,共得到 batches = (500/64) = 7.8125。
    • 在Pytorch数据加载器中,可以选择是否丢弃最后一个不足Batch Size的样本,若不丢弃则最后一个训练集样本数量 = 0.8125 * 64 = 52。
    • 用于提高训练效率。
  • patch_size(图像块):表示一幅图像中的一个子区域;
    • 如:图像尺寸 = 256 x 256,patch = 64 x 64,重叠率overlap = 0,共得到 patches = (256/64)x(256/64)= 16。
    • (数据增强)用于局部特征的学习,从而更好地理解整体图像。

(4)影响因素:时耗

  • 模型的网络层数:网络层越多,耗时约长。如:Unet与轻量级Unet
  • DataLoader数据加载器的耗时问题
    • (1)在每个epoch开始时,数据加载器会重新创建并启动。因此,前几个batch_size的耗时总是很高。
    • (2)参数 - batch_size:对于小样本数据,批大小越大,时耗越高。对于大样本则相反。
    • (3)参数 - num_workers:多进程加载数据
      • 缺点:增加内存占用量(较大,根据加载数据的数量而定)
    • (4)参数 - pin_memory:加快数据从主机内存到GPU内存的传输速度。
      • 缺点:增加内存占用量(较少)
  • image.to(device)将图像(或batch_image)从CPU上传GPU的过程,会产生一定的耗时。与数据传输带宽、图像大小和 GPU 设备性能等有关。
    • (1)若上传图所需内存超过了专用 GPU 内存(显存),则超出部分将转移到共享 GPU 内存中,以防止程序奔溃,但由于共享GPU内存是主存划分出来与GPU共享的内存,因此速度会明显减慢。
    • (2)显存性能测试(batch=8):3060TI - 耗时=0.9秒,3090TI=0.3秒
    • (3)显存性能测试(batch=8、16、32、64):
      • (1)batch=8 / 16:所需内存在专用 GPU 内存范围内;
      • (2)batch=32及以上:所需内存超过专用 GPU 内存,则超出部分转移到共享 GPU 内存中。

2、数据分配:训练集 + 验证集 + 测试集

  • 数据随机分配:
    (1)训练集、验证集、测试集(比例为8 : 1 : 1),用于防止模型过拟合。
    (2)若没有验证集,则训练集与验证集的划分比例为2:8 or 7 : 3
  • 图像前处理(预处理):
    • (1)若训练集的全部数据处理,则训练集 + 验证集 + 测试集必须保持一致。
      • 如:图像归一化,用于降低不同图像的灰度强度对模型的影响力。
    • (2)若训练集的部分数据处理,则训练集 + 验证集一致,而测试集不应用。
      • 如:随机高斯噪声、明亮度调整。用于提高模型的泛化性。
  • 不同数据集的作用:
    • 训练集(Training set):用于迭代以得到最优化模型,使模型更接近真实数据。
    • 验证集(Validation set)用于测试训练集效果并通过多次调参(超参数),使模型泛化性能更好。
    • 测试集(Test set):用来测试最终模型的效果。此时,既不参与权值的学习(权值更新),也不参与超参数的选择(调参)。

备注:如果把测试集当做验证集,通过调参去拟合测试集,等于在作弊。

3、数据增强(Data Augmentation) —— 提升模型鲁棒性与泛化性

数据增强(Data Augmentation) 是计算机视觉任务中常用的一种提升模型泛化能力的策略,主要通过对原始图像施加一系列变换,生成具有多样性的训练样本,以抵抗过拟合,提高模型对不同场景的鲁棒性。

(1)两种类型:离线增强(扩充样本量) + 在线增强(随机变换) —— 在线增强是深度学习训练中的主流做法

备注:无论哪种方式,建议对增强策略进行合理设计与可控扰动,避免过度增强导致训练目标偏移。

  • 离线增强(Offline Augmentation)数据增强前,直接将所有变换结果保存为新样本,从而扩大训练集的实际样本数量。
    • 特点:
      • 每个变换结果都作为独立训练样本参与训练;
      • 数据集体积显著增大,占用更多存储资源;
      • 适用于数据量极小、样本稀缺场景(如医学图像)或训练过程不支持在线增强的框架。
    • 示例:
      • 一张图像原始版本 + 翻转版本 + 色彩扰动版本 ⇒ 增加3倍样本;
      • 所有图像经过增强后保存在磁盘,成为扩充后的训练集。
  • 在线增强(Online Augmentation)在每个训练epoch期间,通过随机变换动态生成图像变体,不增加数据集总样本数,但通过多轮epoch累积多样性。
    • 特点:
      • 样本数量不变,但每轮训练时图像内容略有不同;
      • 提高训练过程的样本多样性,但对存储开销小;
      • 是现代深度学习训练中的主流做法,已被广泛支持于各类数据加载器中(如PyTorch的transforms.Compose);
    • 示例:
      • 第一轮epoch中图像随机旋转+加噪声,第二轮中随机裁剪+调色;
      • 最终模型 " 看过 " 的图像虽多样,但原始训练集大小不变。

(2)常见方法:几何变换 + 像素变换

torchvision.transforms:图像变换

  • 单样本数据增强
    • (1)常用的几何变换方法:翻转,旋转,裁剪,缩放,平移,抖动
    • (2)常用的像素变换方法:椒盐噪声,高斯噪声,高斯模糊,亮度 / 饱和度,直方图均衡化,白平衡等。
  • 多样本数据增强
    • (1)mixup(分类任务):将随机的两张样本按比例混合(分类的结果按比例分配)
    • (2)cutmix(分类任务)将一部分区域cut减掉,但不填充0像素,而是随机填充训练集中的其他数据的区域像素值(分类结果按一定的比例分配)
    • (3)cutout(分类、检测、识别):随机的将样本中的部分区域cut减掉,并填充0像素值(分类的结果不变)

(3)主要作用 —— 降低模型对特定属性的依赖,具有不变性的特征表示。

通过数据增强,可以有效打破模型对图像中空间位置、尺度大小、颜色分布、照明条件等静态属性的过度依赖,促使其学习到更具 不变性(invariance) 的特征表示。

典型增强操作包括:

  • 几何变换(如:旋转、缩放、裁剪、翻转)—— 干扰物体位置与结构排列;
  • 颜色扰动(如:亮度、对比度、饱和度、色调扰动)—— 打破对光照与色彩的敏感;
  • 仿射与透视变换 —— 逼近真实摄像头视角变化,提高空间适应性;
  • 模糊与噪声注入 —— 模拟低质量图像或环境干扰,增强鲁棒性。

以CNN为例,经过图像增强训练的模型将更专注于抽象语义特征的提取,而非局部图像细节,从而具备如下不变性:

  • 位置不变性:物体在图像中任意位置均可被识别;
  • 方向不变性:旋转后的图像不影响分类结果;
  • 尺度不变性:不同大小的目标均可被有效检测。

典型应用:在图像分类、目标检测、语义分割等任务中,数据增强显著降低模型过拟合风险,增强对多样化输入的识别能力,是深度学习视觉模型训练中不可或缺的模块。

4、数据归一化

(1)概念学习:归一化 + 标准化(正态分布)

❤️ 归一化(Normaliation):归一化通常指将数据按比例缩放,使数据的范围落在特定区间内。最常见的做法是将数据缩放至[0,1],也可以将其缩放到[-1,1]。其目的是消除不同特征间的尺度差异,从而避免某些特征主导模型的训练。

  • 公式:X' = (X - MIN) / (MAX - MIN)
  • 特点:转换后数据范围固定,但对于异常值(outliers)非常敏感。

❤️ 标准化(Standardization):也称为Z-score标准化。将数据转换为均值为0,标准差为1的标准正态分布。它通过以下步骤实现:减去均值:使数据的均值接近0;除以标准差:使数据的标准差接近1。

  • 公式:X' = (X - μ) / σ。其中:𝜇表示特征的均值;σ表示特征的标准差;
  • 特点:标准化后数据具有零均值和单位方差,适用于要求输入符合正态分布的模型。

❤️ 正态分布(Normal distribution),又名高斯分布(Gaussian distribution)。是一种重要的统计分布,其概率密度函数为:
在这里插入图片描述

  • μ:均数,决定了正态分布的位置。
    • 与μ越近,被取到的概率就越大,反之越小。
  • σ:标准差,表示正态分布的离散程度。
    • σ越大,数据分布越分散,曲线越扁平;
    • σ越小,数据分布越集中,曲线越陡峭。

正态分布是一种概率分布,其曲线特征:
(1)以均数为中心,左右对称,且曲线的两端无限趋近于横轴,但永不相交。
(2)曲线与横轴之间的面积恒等于1

在这里插入图片描述

  • 正态分布:满足均值=μ,方差=σ^2 —— 记为N(μ,σ^2)
  • 标准正态分布:满足均值=μ0,方差=σ^2=1 —— 记为N(0,1)
import numpy as np

def min_max(x):
    """Min-Max归一化通过将数据线性转换到指定的范围,通常是[0, 1]。其公式为:x = (x - min) / (max - min)"""
    return (x - x.min()) / (x.max() - x.min())

def z_score(x):
    """Z-score标准化(也叫标准化)将数据转换为均值为0、标准差为1的分布。其公式为:x = x - mean / std"""
    return (x - x.mean()) / x.std()

# 逆Z-score标准化
def inverse_z_score(x):
    """逆Z-score标准化是将已经标准化的数据转换回原始尺度。其公式为:x = x * std + mean"""
    return x * x.std() + x.mean()

# Log归一化
def log_normalize(x):
    """Log归一化是对数据进行对数变换,通常用来减小数据的幅度,尤其是对具有偏态分布的数据。其公式为:x = log(x + 1)"""
    return np.log1p(x)  # 相当于log(x + 1)

# 逆Log归一化
def inverse_log_normalize(x):
    """逆Log归一化是将已经Log归一化的数据转换回原始尺度。其公式为:x = exp(x) - 1"""
    return np.expm1(x)  # 相当于exp(x) - 1


"""
Min-Max归一化:适合于需要将数据映射到指定范围时。
Z-score标准化:适用于不同尺度的特征,特别是需要基于标准差进行比较的场景。
逆Z-score标准化:将标准化的数据恢复成原始尺度。
Log归一化:用对数变换减小数据的偏态分布,尤其对大范围的数值有效。
逆Log归一化:将经过对数变换的数据还原为原始数据。
"""
if __name__ == '__main__':
    x = np.array([7, 8, 9, 10])
    print('Min-Max归一化:', min_max(x))             # Min-Max归一化:   [ 0.        0.33        0.66        1. ]

    print('Z-score标准化:', z_score(x))             # Z-score标准化:   [ -1.34     -0.44       0.44        1.34 ]
    print('逆Z-score标准化:', inverse_z_score(x))   # 逆Z-score标准化:  [ 16.32     17.44       18.5623     19.68 ]

    print('Log归一化:', log_normalize(x))           # Log归一化:       [ 2.07      2.19        2.30        2.39 ]
    print('逆Log归一化:', inverse_log_normalize(x))  # 逆Log归一化:     [ 1095.63   2979.95     8102.08     22025.46 ]

(2)归一化方式:动态归一化(np.min / np.max) + 固定归一化(如:0 / 255)

最小 - 最大归一化方法主要有两大类:

  • 使用动态归一化(即 np.min(image) 和 np.max(image))
    • 适用场景:
      • 图像增强与可视化;
      • 单张图像处理(如图像预览、直方图拉伸);
      • 非监督学习任务中作为特征标准化手段。
    • 优点:具有良好的灵活性和自适应性,可消除图像亮度范围差异,使图像在局部对比度差异显著时得到有效增强。
    • 缺点:对极端值敏感,可能放大噪声或削弱真实特征。
  • 使用固定归一化(即 min_val=0 和 max_val=255 / 65535)
    • 适用场景:
      • 与模型相关的图像处理任务;
      • 配对图像输入,如低分辨率/高分辨率对(超分)、原图/标签图对(分割);
      • 模型训练、验证与推理阶段,尤其是在批量图像输入时。
    • 优点:提供高度一致的归一化尺度,确保训练与推理阶段图像输入分布一致;避免图像亮度差异对学习过程造成干扰,提升模型稳定性和泛化能力。
    • 缺点:对图像动态范围变化不敏感;若图像亮度分布远小于全范围(如最高灰度只有两三万),会出现对比度压缩、细节损失现象。
# 使用动态归一化(np.min和np.max)
min_value = np.min(image)
max_value = np.max(image)
normalized_image= (image - min_value) / (max_value - min_value)

# 使用固定归一化(min=0和max=65535)
min_value = 0
max_value = 65535
normalized_image= (image - min_value) / (max_value - min_value)

(3)为什么图像在输入模型之前必须进行固定归一化?

在深度学习的图像处理任务中,归一化操作是不可忽视的关键步骤。无论是图像分类、分割、超分辨率还是去噪,几乎所有与模型相关的图像处理任务都采用固定归一化。这一方法已成为当前深度学习图像任务中的通行标准,适用于大多数监督学习场景。

原因分析:

  • 标准化输入图像的亮度、对比度和动态范围往往有所不同。未经归一化的数据会让模型难以有效理解和区分这些差异。归一化能够将不同图像的特征映射到统一的数值范围,减少输入特征的干扰,进而帮助模型更好地识别有用信息,提高对未见图像的适应性。
  • 加速收敛若输入数据的范围不一致,一些特征的数值可能过大或过小,这会导致梯度更新不均匀,甚至引发梯度爆炸或梯度消失问题。归一化能将所有特征的数值约束在合理的范围内,从而稳定梯度更新,避免极端值影响训练过程,最终加速收敛,提高训练效率。
  • 改善梯度传播在深度神经网络中,输入数据范围的不一致会导致某些层的梯度更新过大,其他层则变化微小。归一化有助于平衡每一层的梯度变化,使得网络的训练更加均衡,提升整体训练质量。
  • 避免模型对特定特征的偏好未归一化的数据可能导致模型偏向某些特征(如亮度较高的区域或特定频段的图像特征),从而影响模型的学习过程。通过归一化,避免了这种偏向,确保了模型在所有特征上都能够平衡学习。
  • 提高数值稳定性数据的数值范围过大或过小可能导致溢出或下溢问题,进而引起数值计算错误。通过归一化,保证输入数据的数值稳定性,从而提高计算精度和稳定性。

总结:归一化是深度学习中不可或缺的预处理步骤,尤其是在图像处理任务中。它不仅能加速训练过程,改善训练稳定性,还能提高模型的鲁棒性,避免对特定特征的偏好。归一化确保了数值计算的稳定性,并有助于提高模型的准确性和训练效率,无论是在训练阶段还是推理阶段,都能够显著提升模型性能。

五、常见问题

1、什么是网络层? —— 只有具有可学习参数的才叫做网络层

在神经网络中,层(Layer)大致可以分为两类:网络层和操作层(非官方名称)。主要区别在于是否包含可学习的参数(权重和偏置)。

  • 网络层(具有可学习参数):通过反向传播不断优化参数,使得网络模型更好地拟合训练数据。
  • 操作层(没有可学习参数):通常对输入数据进行某种固定的数学操作,提升网络的表现或执行其他任务。
层类型功能描述可学习参数
网络层(具有可学习参数)卷积层(Convolutional)通过卷积核提取输入特征,广泛用于图像数据处理。卷积核权重、偏置
全连接层(Fully Connected, FC)将上一层的所有神经元与当前层的神经元相连接,生成最终输出。权重矩阵、偏置
批量归一化层(Batch Normalization)对每一层输入进行标准化,通常加速训练并提高性能。缩放(gamma)与偏移(beta)
反卷积层(Deconvolutional)用于图像的上采样或重建,反向执行卷积操作。反卷积核权重、偏置
长短期记忆层(LSTM)处理序列数据,捕捉长期依赖关系,常用于自然语言处理。输入门、遗忘门、输出门的权重和偏置
门控循环单元(GRU)类似LSTM层,计算效率更高,用于处理序列数据。更新门、重置门的权重和偏置
自注意力层(Self-Attention)计算输入数据中不同位置之间的依赖关系,广泛用于Transformer架构。查询、键、值的权重
操作层(没有可学习参数)激活函数层将线性变换的结果通过非线性函数转换,使神经网络能够学习复杂的模式。
池化层进行下采样,减少输入数据的空间维度,常用于图像处理中。
Dropout层随机“丢弃”神经元,防止过拟合。
Flatten层将多维输入展平成一维向量,通常用于连接卷积层和全连接层。
上采样层将输入数据的空间尺寸增加,常用于生成模型。
归一化层标准化输入数据的分布,提升训练稳定性。部分有(如Batch Normalization)

2、什么是泛化性?如何提升泛化性?

泛化性(Generalization) :模型在未见过的数据上的表现能力。换句话说,泛化性描述了模型如何从训练数据中学到的规律能够应用到新的、未见过的数据集上,而不仅仅是记住训练数据。

  • 高泛化性:模型不仅能够在训练数据上表现良好,还能在未知的数据上保持良好的性能。
  • 低泛化性(过拟合):模型在训练数据上表现得很好,但在新数据上表现差,这表明模型在训练过程中记住了训练数据的噪声或细节,失去了对新样本的预测能力。

以下是几种常见的方法,用于提升模型的泛化性:

  • 1. 数据增加与数据增强
    • 增加训练数据(小样本 / 大样本):帮助模型学习到更具代表性的规律,而不是记住训练数据中的细节或噪声。
    • 数据增强(Data Augmentation):对现有数据进行变换(例如,旋转、缩放、裁剪等),以增加数据多样性,防止模型仅依赖于固定的训练数据模式。
  • 2. 数据清洗与特征提取
    • 数据清洗:去除噪声、处理缺失值等,确保训练数据的质量。
    • 特征选择:选择与任务相关的特征,去除不相关或冗余的特征,减少模型复杂性并提高泛化能力。
  • 3. 批量归一化(Batch Normalization):常用于规范化每一层的输入,帮助加速训练过程,稳定梯度更新,并避免过拟合。
  • 4. 交叉验证(Cross-Validation):将训练数据划分为多个子集,训练模型并在不同子集上进行验证,可以有效评估模型的泛化能力,避免过拟合。
  • 5. 随机打乱(Shuffling):防止模型记忆数据的顺序,提高模型的泛化能力并防止过拟合,特别是在批量训练中。
    • 1. 模型选择与复杂度控制
      • 选择合适的模型:复杂的模型(例如深层神经网络)可能容易过拟合,特别是在训练数据量不足时。选择简单的模型(例如线性回归、决策树等)并对其进行调参,可能会提高泛化能力。
      • 限制模型复杂度:通过限制模型的自由度或复杂度(例如,树的最大深度、神经网络的层数
    • 2. 正则化(Regularization)
      • L1/L2 正则化:通过在损失函数中加入惩罚项,限制模型参数的大小,防止过拟合。L2正则化(即权重衰减)在训练过程中会惩罚权重的平方,而L1正则化则会惩罚权重的绝对值,促使模型产生稀疏的特征表示。
      • Dropout:在训练过程中随机丢弃一部分神经元,以减少神经网络的依赖性,避免过拟合。
      • 早停(Early Stopping):在训练过程中监控验证集的表现,当验证集误差不再下降时停止训练,从而避免模型在训练数据上的过拟合。
        等),避免过拟合。
    • 3. 对抗训练(Adversarial Training):通过生成对抗样本(即输入数据的轻微扰动)来训练模型,使其能够对扰动数据有更好的适应性,增强模型的鲁棒性和泛化能力。

3、什么是欠拟合 + 过拟合?

在机器学习和深度学习的模型训练中,过拟合(Overfitting) 和 欠拟合(Underfitting) 是两种常见的现象。它们都与模型的泛化能力(即在未知数据上的表现)密切相关,但它们发生的原因和表现是截然不同的。

情况表现原因解决方法
欠拟合(Underfitting)训练误差和验证误差都高模型过于简单,特征不足,训练不充分增加模型复杂度,优化特征工程,增加训练数据
过拟合(Overfitting)训练误差很低,验证误差很高模型过于复杂,数据量不足,训练过长,正则化不足增加训练数据,应用正则化,使用早停,简化模型

欠拟合通常是由于模型过于简单,无法捕捉到数据的真实模式;
过拟合通常是由于模型过于复杂,捕捉了训练数据中的噪声;
要解决这些问题,需要在模型复杂度和数据量之间找到合适的平衡,合理地调整训练过程。

4、什么是梯度消失 + 梯度爆炸? —— 解决方法:激活函数 + 批量归一化 + 残差网络 + 权重初始化 + 学习率

梯度消失(Vanishing Gradient)和梯度爆炸(Exploding Gradient) 是神经网络训练过程中两种典型的梯度问题,它们的根源主要与激活函数的选择、权重初始化、以及网络结构的深度等因素有关。

  • 梯度(Gradient)指的是损失函数对模型参数的偏导数,用于表示损失函数相对于模型参数的变化率。在神经网络中,梯度是在反向传播过程中通过链式法则计算的,它反映了模型参数对损失函数的影响。
    • 激活函数的求导:对于神经网络中的每一层,激活函数的导数决定了梯度在该层的传递情况。梯度的大小与该层的激活函数导数相乘,决定了梯度的传播速度。
    • 梯度与权重(W)的乘积:在反向传播时,梯度会与权重 W 相乘,这有助于更新权重。
  • 梯度消失(gradient < 1)随着网络层数的增加,梯度以指数形式衰减,最终变得接近零,从而无法学习到有效特征,导致神经网络的早期层几乎没有更新。
    • 原因1:激活函数如 Sigmoid 或 Tanh 在输入值非常大或非常小时,其导数的值接近零,导致梯度在反向传播时逐层衰减,无法有效传递到深层。
    • 原因2:网络过深时,梯度会通过链式法则被逐步乘积并逐渐减小,直到消失。
  • 梯度爆炸(gradient > 1)随着网络层数的增加,梯度的值逐渐增大,导致权重更新过大,数值溢出;损失函数的值剧烈波动,无法收敛,甚至导致程序崩溃。
    • 原因1:过大的权重初始化导致梯度在反向传播过程中不断放大。
    • 原因2:激活函数如 ReLU,若输入值过大,可能导致梯度的增大,特别是在深层网络中,这种效应会被逐层放大。

如何解决梯度消失和梯度爆炸:

  • (1)合理选择激活函数
    • Sigmoid:其输出范围是 (0, 1),并且导数的取值范围是 (0, 0.25),当输入值较大或较小时,导数非常小,容易导致梯度消失。
    • Tanh:其输出范围是 (-1, 1),导数在输入值较大或较小时接近零,也容易导致梯度消失。
    • ReLU(Rectified Linear Unit):ReLU的输出范围是 (0, ∞),并且它的导数在正值区间内为常数1,不容易引发梯度消失问题。ReLU通常是避免梯度消失问题的首选激活函数。
    • Leaky ReLU、Parametric ReLU、ELU(Exponential Linear Unit):这些都是ReLU的变种,能够在负数区间保持一定的梯度,进一步缓解“死亡神经元”问题(即ReLU中神经元的激活值为0的问题)。
  • (2)批量归一化(Batch Normalization,BN):通过对每一层的输入进行标准化处理,将其均值和方差调整到一个固定的范围内,从而有效地减少了梯度消失和梯度爆炸的风险。批量归一化有助于稳定训练过程,并提高收敛速度。
    • 作用:消除权重缩放的影响,避免梯度消失或爆炸。加速训练过程,减少学习率调节的难度。
    • 使用方法:在每个卷积层或全连接层的输入之前应用批量归一化,将每一层的输出规范化到均值为0、方差为1的范围内。
  • (3)残差网络(ResNet):通过引入跳跃连接(skip connection)来解决梯度消失的问题。跳跃连接可以让梯度直接通过短路路径传递,从而减少了梯度在多层网络中逐层消失的现象。
    • 作用:跳跃连接让梯度可以直接从网络的更深层传递回更浅的层,避免了深层网络中梯度消失的问题。
    • ResNet设计:通过增加“残差块”(residual block),每个残差块都包含一个输入直接跳跃到输出的路径。这样,梯度可以沿着这个路径直接传递,避免了在反向传播过程中梯度的消失。
  • (4)权重初始化
    • Xavier初始化:适用于Sigmoid和Tanh激活函数,通过根据输入输出的维度合理初始化权重,避免过大或过小的初始权重导致梯度问题。
    • He初始化:适用于ReLU激活函数,它通过考虑ReLU的特性来设置权重初始化,能有效避免梯度爆炸或消失。
  • (5)学习率调整
    • 学习率过大可能导致梯度爆炸,学习率过小则可能导致梯度消失或训练缓慢。适当的学习率调整可以缓解这两种问题。
    • 学习率衰减:通过在训练过程中逐渐降低学习率,避免在训练后期梯度过大导致梯度爆炸。

5、PyTorch模型状态控制:model.train() + model.eval()

【PyTorch】model.train()和model.eval()用法和区别,以及model.eval()和torch.no_grad()的区别

PyTorch控制模型状态的两个方法:

  • model.train():将模型设置为训练模式,用于在训练集上参数计算。
    • (1)启用 Dropout 和 Batch Normalization
      • Dropout:增加模型的泛化能力,防止过拟合。
      • Batch Normalization:帮助稳定训练过程,改善梯度传播,减少内偏差。
    • (2)启用梯度计算 —— 在训练过程中,梯度会通过反向传播计算,并通过优化器更新模型的参数。
  • model.eval():将模型设置为评估模式,用于在验证集上评估性能。
    • (1)关闭 Dropout 和 Batch Normalization
      • Dropout:评估过程中,关闭Dropout以避免随机失活,从而确保模型输出稳定、可重复。
      • Batch Normalization:使用训练过程中学习到的统计数据进行推理,确保推理结果一致。
    • (2)关闭梯度计算 —— 在推理或验证阶段,关闭梯度计算,以避免不必要的计算资源消耗,并防止对模型参数进行更新。

备注:在训练过程中,model.train()和model.eval()的切换非常重要。它们帮助模型根据不同阶段的任务(训练或评估)进行适当的行为调整,以确保在每个阶段都能获得准确的结果。

6、PyTorch模型操作:模型实例化 + 保存与加载模型(状态字典)

  • 1. 模型实例化的两种方式:
    • 自定义模型:通过继承nn.Module类,并定义__ init__()和forward()方法来创建网络结构类。
    • PyTorch内置模型:直接使用torchvision.models等库提供的预训练模型。
    • 注意:实例化模型的过程是创建网络结构类后,调用该类来生成模型对象。
  • 2. 保存模型的状态字典(state_dict):状态字典保存了模型的所有可学习参数(权重和偏置),以及优化器的状态(如动量和学习率等),但不包含模型的结构定义。通过保存state_dict,可以更轻松地在之后的阶段重新加载模型。
    • 注意: 只保存state_dict而不是整个模型对象,能够避免在不同的PyTorch版本之间不兼容的问题。
  • 3. 加载模型的状态字典:在恢复训练或进行推理时,可以通过加载保存的状态字典来重建模型的权重和偏置。
    • 注意:加载模型时,需要首先定义相同的网络结构,然后加载权重。
import torch
import torchvision.models as models

#################################################################
# (1)模型实例化
model = models.resnet18(weights=True)
print("模型结构:\n", model)
# weights表示是否(在线)下载并加载预训练模型的权重。
# 		weights=True:  下载并加载基于ImageNet的预训练权重。
# 		weights=False: 不加载预训练权重,随机初始化模型权重。

#################################################################
# (2)保存模型权重
torch.save(obj=model.state_dict(), f=r'model.pth')

#################################################################
# (3)加载模型权重
state_dict = torch.load(f=r'model.pth', map_location=torch.device('cpu'), weights_only=True)  # 加载模型的状态字典
model.load_state_dict(state_dict=state_dict, strict=False)  # 将状态字典应用到模型中(strict=False表示忽略最后一层的权重)
print("状态字典:\n", state_dict)
################################
# 若不需要加载模型权重,可直接使用torch.load()函数加载模型状态字典(效果与上述方法一致)
# model.load_state_dict(torch.load('model.pth', weights_only=True))  # 加载模型的状态字典
# model.to(device=device)  # 将模型加载到GPU上


"""##############################################################################################
# 函数功能:用于加载模型或张量。
# 函数说明:model = torch.load(f, map_location=None, weights_only=False, pickle_module=pickle)
# 参数说明:
#         - f:                    模型文件的路径。支持的扩展名:.pt或.pth。
#         - map_location(可选) : 将数据加载到指定的设备上。
#                         (0)map_location=None                  :若不指定,则加载到默认设备上。
#                         (1)map_location='cpu'                 :将数据加载到CPU上
#                         (2)map_location=torch.device('cuda')  :将数据加载到默认的CUDA设备上。
#		  - weights_only(可选) : 若为Flase,表示只加载预训练模型权重。
#								   若为True,表示加载预训练模型文件,包括状态字典(weights)、整个模型、训练状态等
#         - pickle_module(可选): 使用Python的内置 pickle 模块来反序列化对象。用于兼容不同的Python版本或自定义的序列化格式。
# 返回参数:
#         返回一个包含模型或张量的Python对象。———— 若加载的文件包含一个字典,则该字典将包含从保存的对象名称到对象本身的映射。
##################################################################
# 预训练模型文件通常包含以下信息:
#       (1)模型的权重参数:     如:各个层的权重矩阵和偏置向量。
#       (2)模型的网络结构:     如:层的类型、层的连接方式等。
#       (3)模型的优化器状态:   如: 学习率、动量、权重衰减等优化器相关的信息。
##############################################################################################"""

"""##############################################################################################
# 函数功能:用于加载预训练模型的状态字典并应用到模型中。
#		   状态字典是一个Python字典对象,其中包含了模型的所有权重参数(如:weights和biases)。
# 函数说明:model.load_state_dict(state_dict, strict=True)
# 参数说明:
#         - model:                 加载权重参数的PyTorch预训练模型对象。
#         - state_dict:            预训练模型权重参数的状态字典。
#         - strict(可选):          是否严格匹配state_dict中的键与当前模型的键。
#                         (1)默认True,表示要求严格匹配。
#                         (2)若为False,则允许state_dict中的键只是当前模型键的子集,或者键的顺序不同。
#                                   但是,若state_dict中的键不包含在当前模型中,或者形状不匹配,则加载失败。
##############################################################################################"""

"""##############################################################################################
# 函数功能:用于保存模型
# 函数说明:torch.save(obj, f, pickle_module=pickle)
# 参数说明:
#          - obj:                   要保存的对象。
#                         (1)整个模型:model
#                         (2)模型的状态字典:model.state_dict()
#          - f:                     文件路径(字符串类型)或类文件对象(比如一个已打开的文件)。
#          - pickle_module(可选):   使用Python的内置 pickle 模块来序列化对象。用于兼容不同的Python版本或自定义的序列化格式。
##############################################################################################"""

7、常用术语

在人工智能与软件开发领域中,常见术语具有明确含义,理解这些术语有助于建立清晰的工程认知与协作基础。以下列出若干核心术语并予以简要解释:

(1)Pipeline(管线)

Pipeline(流水线)指从原始数据采集、预处理、模型构建、训练、评估、部署到推理服务的端到端处理流程,通常由一系列串联的任务或模块组成。

  • 示例:图像分类任务的Pipeline包括:图像采集→图像增强→模型训练→模型导出→部署上线。
  • 在MLOps中,Pipeline可由工具如Kubeflow、Airflow或Roboflow自动化调度执行。

(2)Baseline(基线)

Baseline(基线模型/基准方法)指在某一任务上可用于对比和衡量的性能参考标准==,通常为第一个实现版本、行业公开模型或历史最优模型。

  • 示例:在目标检测任务中使用YOLOv5s作为baseline模型,用于与改进版本进行对比。
  • Baseline不等同于最佳结果,而是最低可接受或可迭代优化的起点。

(3)Benchmark(基准测试)

一种标准化评估方法,用于在公开数据集或统一任务上测试模型、系统或算法的性能表现,从而实现跨模型的横向对比。

  • 示例:在图像识别中,使用ImageNet作为benchmark数据集,在同一评估指标(如Top-1 Accuracy)下对不同模型进行性能比较。
  • 常见基准数据集(Benchmarks)包括:COCO(目标检测)、MMLU(通用语言理解)、GLUE(自然语言推理)等。
  • 例如,OpenAI GPT模型就会在MMLU、HumanEval、BIG-Bench等基准测试上公布成绩,作为其多领域能力表现的“官方证据”。

(4)Backlog(待办事项)

指项目开发过程中尚未完成、待处理的任务清单,涵盖功能需求、Bug修复、技术债务等,通常用于Scrum或敏捷开发中进行优先级管理

  • 示例:在Scrum迭代中,团队每日从Product Backlog中选择任务推进至Sprint中。
  • Backlog不仅用于开发流程,也常用于问题跟踪(Issue Tracking)与产品管理。

(5)MVP(Minimum Viable Product,最小可行产品)

MVP指的是在产品开发早期,通过最少的功能与最小的成本,快速构建一个能够体现核心价值的雏形,用以验证产品设想是否可行、是否具备市场潜力。
核心特征:

  1. 保留核心功能:只聚焦最关键的价值点,而不是追求完美。
  2. 快速迭代:让目标用户尽早使用,并基于反馈进行优化。
  3. 低成本试错:避免在未验证市场需求前,就投入大量时间和资源。

一句话概括:MVP就是用最小的功能,验证最大的价值

六、进阶知识

1、项目开发(工程经验)

(1)新手指南

# (1)环境配置与部署(基本)
# (2)模型优化(包括图像前处理、超参优化、图像后处理、算法加速、模型加速等)
#       注意:数据和模型是强相关的,根据开发需求选择适配模型(需调研),用于在开发前期先确定所定模型必须具备开发需求的效果。
#       注意:模型中任何算法,必须熟悉其内部实现逻辑,包括输入与输出,重点关注算法对数据的像素级变化。
# (3)框架优化: 该领域属于科研范围,开发不应该涉及。

标准的开源项目应具有以下几个特点:清晰的架构设计、明确的执行流程、经过优化后的算法(且具有逻辑)。

在开发或复现一个开源项目时,需注意以下几个核心问题:

  • 理论上,模型框架才是深度学习的核心。
  • 模型训练 + 模型预测:必须确保一致的图像前处理。 前处理的步骤直接影响模型的泛化能力,因此需要保证前处理的一致性,以避免不必要的性能下降。
  • 理解算法内部实现
    • 任何算法或函数,都必须熟悉其内部实现逻辑,特别是其输入和输出,重点关注数据的像素级变化(如:int、float 类型转换)。
      • 例如:图像归一化(是否有截断?数据类型?)
      • 冗余的图像处理与超参设置:避免冗余的图像处理步骤和超参数设置。前处理流程应保持简洁与高效,不做不必要的图像处理。
    • 理解算法内部实现:无论是调用开源库还是自定义算法(即使是开源项目)
      • 简单算法:可以直接调用 —— 如:cv2.size(),cv2.equalizeHist()等
      • 复杂算法:自定义实现 —— 如:rollingball等
  • (1)图像前处理
    • 主要目的是增强数据的泛化性,减少过拟合,同时避免过于复杂的处理流程。
    • (例如 UNet 模型)常见的图像前处理方法包括:
      • 全局归一化[0,1]:统一图像数据的范围。
      • 图像切块 (Patch):对于大图像,切割成小块以提高训练效率并避免内存溢出。切块的重叠率 (Overlap) 设置需适当,过大或过小都会影响效果。
      • 图像缩放 (Resize):调整图像尺寸,使其与网络的输入要求匹配。
      • 数据增强 (Augmentation):增加训练数据的多样性,提升模型的鲁棒性,避免模型对训练集过拟合。
    • 上述步骤帮助解决以下问题(数据的泛化性):
      • 图像尺度不一致(如:200x300、300x300等)
      • 数据类型不一致(如:uint8、uint16等)
  • (2)模型训练
    • 在训练集上,若模型的训练结果与给定标签匹配度超过95%,则证明当前模型的学习能力没有问题。
    • 验证方法:
      • 逐步改进观察:在每个 epoch 打印或保存预测结果,观察模型训练过程中是否逐步改进(个别异常不影响整体效果)。
      • 最终匹配度检查:验证模型的训练结果是否符合预期。
  • (3)模型预测
    • 在测试集上进行泛化性测试,评估模型的实际应用能力,确保训练中获得的模型在未知数据上的表现同样优异。

(2)模型调试 - 整个流程

  • (1)确认数据加载逻辑与可视化检查样本
    • 打印input.shape、label.shape确保尺寸一致;
    • 使用matplotlib等工具可视化部分样本;
    • 检查数据是否已归一化/标准化(通常是0,1或−1,1),是否存在异常像素值(如65535或NaN);
    • 检查标签是否错位、错配、缺失;
    • 对于配对图像任务(如超分、分割),确保图像对齐、格式统一。
  • (2)设置基础网络结构,快速训练几个epoch看是否收敛
    • 构建简化网络结构(如去掉高阶模块)快速验证任务可行性;
    • 观察loss是否持续下降,若震荡或无变化,立即排查梯度传播/学习率问题;
    • 保证loss函数的输出范围合理(如避免极小loss长期不变);
  • (3)在验证集上观察定量与定性结果,评估是否需要结构改动
    • 定量: 指标是否改善,是否达标;
    • 定性: 图像结果是否存在伪影、结构模糊、边界错乱;
    • 若验证性能低但训练良好,排查过拟合或数据分布差异。
  • (4)逐步尝试损失函数、优化器与学习率的组合
    • 多目标loss组合时建议逐项调权重,避免某一项主导训练;
    • 学习率建议从较大值逐步减小,结合LR scheduler进行动态调整;
    • 若使用Adam仍收敛缓慢,可尝试AdamW、Lookahead、SGD+momentum。
  • (5)适配合适的增强策略与正则项,关注是否过拟合
    • 监督任务中,数据增强应保持label一致性(如配对裁剪、仿射同步);
    • 使用Dropout、EarlyStopping等手段观察其对泛化能力的提升;
    • 特别注意高分辨率图像中增强导致的信息损失问题。
  • (6)记录训练日志,必要时引入可视化工具(如TensorBoard、WandB)
    • 建议统一使用如TensorBoard、Weights&Biases进行loss、指标、样例图像对比;
    • 记录重要超参数、数据版本、时间戳,利于重现实验;
    • 日志结构应具备对比、溯源、分析能力(如绘制loss/metric曲线、保存中间图像);

(3)模型调试 - 经验建议

  • 使用固定随机种子提高实验可重复性
  • (控制变量法)一次只改动一个因素,否则难以定位问题根源
  • 不要迷信更复杂的网络结构,优先保证数据和损失函数合理性(可以通过浅层模型快速定位问题是否出在 " 表达能力 " 或 " 数据质量 " )

训练 / 验证:

  • 调试阶段优先使用小规模子集,先验证模型能否基本收敛,再扩大数据规模
  • 在正式训练前,先做过拟合实验(如用几十张图像跑到100%准确),可验证模型与损失函数是否构建正确
  • 合理使用预训练模型可加速收敛并提升表现,但要确保适配任务(如通道数、输入尺寸)
  • 验证集应具备可靠性、代表性与多样性,既要避免数据泄露或标签错误影响评估结果,也要防止样本过于简单或集中于单一类型,从而确保模型表现判断的客观性与泛化能力。

打印 / 保存:

  • 尽早添加日志记录和可视化机制,如loss、精度、学习率等,便于追踪训练曲线
  • 定期输出模型预测结果进行人工质检,不要仅依赖指标
  • 保存最优模型而非最后模型,设置验证集最优点保存机制(early stop + best checkpoint)
  • 合理设置loss print频率和checkpoint频率,避免训练过程因异常值覆盖掉良好模型
  • 多卡训练时注意BatchNorm同步问题,尤其是在小batch size下容易不稳定

2、权重参数

(1)计算模型的权重参数(总数量)

【PyTorch项目实战】之ResNet系列:resnet18、resnet34、resnet50、resnet101、resnet152

import torch


class SimpleModel(torch.nn.Module):
    def __init__(self):
        super(SimpleModel, self).__init__()
        self.conv1 = torch.nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1)
        self.fc1 = torch.nn.Linear(64 * 32 * 32, 10)  # 假设输入图片大小为 32x32

    def forward(self, x):
        x = self.conv1(x)
        x = x.view(x.size(0), -1)  # 展平
        x = self.fc1(x)
        return x


if __name__ == "__main__":
    model = SimpleModel()  # 创建模型
    print(f"模型总参数量: {sum(layer_param.numel() for layer_param in model.parameters())}")
    # 模型总参数量: 657162

(2)模型预测时,GPU显存占用计算(输入数据 + 权重参数 + 中间结果) —— 模型参数(同时保留在CPU+GPU)

  • GPU显存占用:主要来源于输入数据、模型权重、中间结果和临时缓存等。
  • GPU-CPU数据传输:将数据从CPU传输到GPU进行计算,从GPU传回CPU以获取计算结果。
  • 优化方法:可以通过调整批处理大小、精度压缩、减少数据传输次数和使用异步传输等方式来优化显存和数据传输效率。

在深度学习模型的预测过程中,GPU显存和CPU内存的占用通常是通过以下几个因素计算和管理的:

  • GPU显存占用计算 —— 在进行模型训练或预测时,GPU显存主要用于存储以下几类数据:
    • 输入数据
      • 在训练或预测时,输入数据(如图像或其他张量)需要传输到GPU显存中进行处理。
      • 输入数据 - 占用的显存量 = batch_size × channels × height × width × 每个像素的字节数(如:每个像素是32位,4字节)
    • 模型参数(权重和偏置)
      • 在训练或预测时,模型参数需要加载到GPU显存中,用于前向传播和反向传播。在深度学习模型中,通常这些参数是浮点数(如32-bit浮动点数)
      • 模型参数 - 占用的显存量 = 模型总参数量 × 每个参数的字节数(如:每个参数是32位,4字节)
    • 中间结果(激活值)
      • 在前向传播过程中,每一层的中间结果(如卷积层的输出Feature Map、激活函数的结果、池化层的输出、全连接层的输出等)都需要保存直到梯度计算完成,因此需要暂时存储在GPU显存中。随着网络深度的增加和输入图像尺寸的增大,显存占用会显著增加。
      • 在反向传播过程中,深度学习框架会通过计算图和动态显存管理来优化显存的使用。例如,当一个中间结果不再用于后续的梯度计算时,它会被及时从显存中释放,或者被标记为可复用。
    • 梯度信息(训练时)
      • 在训练过程中,计算梯度时需要额外的显存来存储每一层的梯度信息。
      • 但是在预测阶段,通常不需要计算梯度,因此显存占用不会包括梯度。
    • 临时缓存和工作空间
      • 在计算过程中,GPU需要使用一定的临时缓存来存储中间计算结果,例如矩阵乘法的临时存储空间等。显存占用会增加一些额外的缓存需求。
  • GPU与CPU之间的数据传输 —— GPU和CPU之间的数据传输速度相对较慢(尤其是在不使用高速互连技术的情况下)。这个过程通常是瓶颈所在,尤其是当数据量很大时(例如处理高分辨率图像或大批量数据)。
    • 输入数据(从CPU传输到GPU):在预测时,输入数据通常会先存储在主机的内存(CPU内存)中,然后传输到GPU内存(显存)进行处理。
    • 模型参数(同时保留在CPU+GPU):即使模型在GPU上运行,模型的权重也会保留在CPU内存中(尤其是在PyTorch或TensorFlow等框架中,GPU和CPU是分开的)。这意味着在模型加载时,模型参数通常会被同时加载到CPU和GPU的内存中。
    • 输出数据(从GPU传回到CPU):在计算完结果后,GPU内存中的输出需要传回到CPU内存,以便后续处理(例如可视化、保存结果等)。
  • 如何减少显存占用
    • 优化显存占用:
      • 减少批量大小(batch size):较小的批量大小可以减少显存占用,但会增加推理时间,因为每次推理的样本数量减少了。
      • 模型剪枝:去除冗余的网络层或降低模型的复杂度,可以减少参数量和显存占用。
      • 精度压缩:将模型的权重和输入数据的精度降低(如使用16位浮点数替代32位浮点数)可以显著减少显存占用。
      • 梯度累积:在训练过程中使用梯度累积,可以让你在使用较小批量的情况下仍然保持大的有效批量大小,从而减少显存占用。
    • 优化GPU-CPU数据传输:
      • 减少数据传输:尽量减少GPU和CPU之间频繁的数据传输。如果可以,将所有需要处理的操作尽量集中在GPU端执行,减少数据来回传输的时间。
      • 异步数据传输:使用CUDA的异步数据传输(如cudaMemcpyAsync)来在执行计算的同时进行数据传输,从而提高效率。
      • 数据预加载:在可能的情况下,可以在GPU上预加载多个输入数据,避免每次预测时都进行数据传输。

(3)获取GPU显存

获取系统显存:总内存 + 已占用内存 + 未占用内存
获取进程显存:总显存 + (最大)已分配显存 / 已缓存显存

3、不同任务下的损失 loss 与指标【分类、分割、检测、超分】

在深度学习的图像任务中,不同任务自具有不同的训练和验证损失函数需求,直接影响模型的性能表现。

以下是对分类、分割和检测任务的损失函数选择和推荐的详细说明。

  • (1)分类任务
    • 训练损失函数:
      • 交叉熵损失(Cross-Entropy Loss):最常用的分类损失函数,用于多类分类问题。它能有效地衡量预测类别与真实类别的匹配程度。
      • Focal Loss:适用于类别不平衡的情况,能更关注难以分类的样本。
    • 验证指标:
      • 分类准确率:简单直观,适用于大多数平衡分类任务。
      • F1-score:适合类别不平衡任务,综合了查准率和查全率。
  • (2)分割任务
    • 训练损失函数:
      • 交叉熵损失(Cross-Entropy Loss):像素级分类损失,用于每个像素的类别判断。
      • Dice 损失(Dice Loss):重叠度量损失,对小目标和不平衡数据更加敏感。
      • 组合损失(例如交叉熵 + Dice 损失)推荐):结合了像素分类准确性和区域重叠,有助于提升整体分割质量。
    • 验证指标:
      • Dice 系数或IoU:直观衡量分割重叠度,反映模型在实际分割任务中的效果。
  • (3)检测任务
    • 训练损失函数:
      • 分类损失(如交叉熵或Focal Loss):用于目标的类别预测。
      • 回归损失(如Smooth L1 Loss、GIoU Loss、CIoU Loss):用于优化边界框回归,使预测框更接近真实框。
      • 组合损失(分类损失 + 回归损失):目标检测模型一般采用组合损失来同时优化类别预测和边界框定位。
    • 验证指标:
      • mAP(平均精度均值):在不同 IOU 阈值下的检测精度,综合考虑了分类和定位性能。
  • (4)超分任务
    • 训练损失函数:MSE loss、L1 loss、VGG 感知损失、GAN 对抗损失
    • 验证指标:PSNR + SSIM

MSE loss 和 L1 loss 的计算方式(手动计算 + torch.nn.MSELoss/L1Loss)

import torch
import torch.nn as nn

x = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
target = torch.tensor([[1.5, 2.5], [2.5, 4.5]])

# # 创建 3D 张量 (C, H, W),这里 C=3, H=2, W=2
# x = 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]]])  # 3通道输入
# 
# target = torch.tensor([[[1.5, 2.5], [2.5, 4.5]],
#                        [[4.5, 6.5], [7.5, 8.5]],
#                        [[8.5, 10.5], [11.5, 12.5]]])  # 3通道目标

# MSELoss方式一:手动计算
manual_loss = torch.mean((x - target) ** 2)  # MSE损失
print(manual_loss, f"value of loss: {manual_loss.item()}")

# MSELoss方式二:使用 nn.MSELoss
mse_loss = nn.MSELoss()
loss = mse_loss(x, target)
print(loss, f"value of loss: {loss.item()}")
"""
tensor(0.2500) value of loss: 0.25
tensor(0.2500) value of loss: 0.25
"""
##################################################################
# 推荐:使用 nn.MSELoss()/nn.L1Loss() 更规范,且 reduction 可调节,以适应不同需求。
# 函数说明:mse_loss = nn.MSELoss(reduction='mean')
# 函数说明:mse_loss = nn.L1Loss(reduction='mean')
# 参数说明:
# 		reduction="mean":(默认)计算所有元素的均值。
# 		reduction="sum":计算所有元素的总和。
# 		reduction="none":返回逐元素损失,不做归一化处理。
##################################################################
# L1Loss方式一:手动计算
manual_loss = torch.mean(torch.abs(x - target))  # L1损失
print(manual_loss, f"value of loss: {manual_loss.item()}")

# L1Loss方式二:使用 nn.L1Loss
l1_loss = nn.L1Loss()
loss = l1_loss(x, target)
print(loss, f"value of loss: {loss.item()}")
"""
tensor(0.5000) value of loss: 0.5
tensor(0.5000) value of loss: 0.5
"""

4、整图预测 + 切块预测(归一化的影响)

在深度学习图像处理任务中,整图预测和切块预测是两种常见的处理方式,尤其在处理高分辨率图像时非常重要。归一化对这两种预测方式的使用和效果有着直接的影响。

  • 整图预测直接对整个图像进行模型预测(不涉及切割和重叠overlap)
    • 特点:(1)模型可以利用整个图像的上下文信息(全局信息);(2)避免了切块和拼接操作的额外开销。
    • 缺点:显存需求大或硬件不支持
    • 应用:图像分类、语义分割等任务
  • 切块预测将图像裁剪成较小的块,每次只对其中的一个块进行模型预测,最后将各个块的结果拼接回整张图像。
    • 特点:(1)专注于图像的局部特征,尤其在细节学习上有优势,但可能忽略一些全局信息;(2)避免了显存不足的问题
    • 缺点:切块之间存在重叠区域(overlap),对于这些重叠区域的拼接需要谨慎处理。若拼接不当,可能会导致图像质量不一致,出现明显的接缝或对比度问题。
    • 应用:超分辨率、目标检测等任务

无论使用整图预测还是切块预测,全局归一化都是确保图像一致性的重要步骤。它能够确保所有区域具有相同的对比度和亮度,从而减少图像处理过程中的误差。

  • 在切块预测时,应避免对每个小块单独进行归一化操作。最佳实践是在进行切块操作之前,对整图进行全局归一化。这能有效避免切块拼接时出现接缝问题和图像对比度不一致的情况,确保处理结果的质量。
  • 需要注意的是,许多深度学习模型默认会执行归一化,因此在切块预测时,应避免对每个小块进行额外的局部归一化操作,以免破坏整体的一致性。

5、训练集与验证集的样本关系:(1)同一样本不同切块(2)同一类别不同样本(3)不同类别样本

备注:是训练过程中的验证集不是测试集!!!
举例:在基于切块策略(patch_size)进行RCAN模型训练与验证时,针对 训练集与验证集 的样本关系,通常有以下三种主要方式:

样本关系数据来源特点优缺点适用场景
同一样本,不同切块同一张原始大图,仅切块区域不同(1)分布非常接近(2)验证指标收敛快,结果稳定容易产生过拟合,模型真实泛化能力不足;验证结果可能高估模型性能快速收敛、验证网络有效性(如超分论文研究阶段、算法baseline构建阶段)
同一类别,不同样本同一类别的不同大图样本例如(例如:拍摄了多个心肌图像,训练和验证分别使用不同心肌样本)(1)相似但非完全一致(2)验证能够较好反映模型在同类数据上的泛化性能验证难度略高,收敛速度稍慢,但性能评估更真实可靠实际部署、追求强泛化性能(如真实卫星图、遥感恢复、医学图像处理)
不同类别样本不同类别的数据例如(例如:训练用心肌样本,验证用微管样本)(1)存在明显分布差异(2)能严格检验模型跨领域泛化能力验证难度显著增加;训练模型容易出现性能下降,评估结果波动大用于测试模型在极端分布变化场景下的适应性评估(如跨组织、跨模态应用)

在实际应用中(同时部署训练模块和测试模块),用户往往只提供训练样本而缺少专门的验证样本,因此需要结合实际落地场景的考虑。

  • 验证集的分布应尽量贴近实际应用;
  • 综合考虑训练效率、模型稳定性与泛化能力,合理制定数据划分策略。

6、为什么开源项目中存在大量空的__init__.py文件?

在开源项目中,大量存在的空__init__.py文件,其作用是标识目录为包(Package)

  • 作用:在Python早期(特别是Python2时代),只有包含 __ init __ .py文件的目录,该目录才会被解释器识别为包,从而允许导入模块。即使文件为空,也必须存在。
  • 兼容性:虽然在Python 3.3及以上版本中引入了隐式命名空间包(即目录没有 __ init __ .py也能导入),但为了保持对旧版本Python的兼容,许多开源项目仍然保留空的 __ init __ .py。
  • 便于扩展:空的 __ init __ .py文件不仅能标识包,还为以后扩展提供了便利。项目在初期可能不需要任何初始化逻辑,但随着项目的增大,可能会在 __ init __ .py中加入包级别的导入或配置。
假设一个开源项目的结构如下:
		my_project/
		├── my_package/
		│   ├── __init__.py
		│   ├── module1.py
		│   ├── module2.py

在项目初期,__init__.py文件可能是空的,仅用于标识my_package目录为一个包,便于模块导入。例如:
		# my_package/__init__.py
		# 空文件

这时,项目用户可以这样导入模块:
		from my_package import module1

但随着项目的增大,可能需要在__init__.py中做一些包级别的配置或导入操作。例如,随着新模块的加入,开发者可能希望统一导入包中的核心模块或提供一个统一的API接口:
		# my_package/__init__.py
		from .module1 import ClassA
		from .module2 import function_b
		
		__all__ = ['ClassA', 'function_b']

这种情况下,__init__.py文件不仅仅是空的,它为用户提供了一个更简洁的接口:
		from my_package import ClassA, function_b

注意区别:__ init __ () 和 __ init __.py 看似相似,但它们的作用和用途完全不同。

特性__init__()__init__.py
类型方法(构造函数)文件(包标识)
所属位置类内部目录中
作用初始化类实例标识目录为包,允许模块导入
触发时机创建类实例时自动调用无需调用,存在即表示该目录是包
关联性无直接关联用于模块结构管理

七、常见网络模块

1、Bottleneck(瓶颈模块) —— 最早出现在ResNet中(Residual Network)

【PyTorch项目实战】ResNet系列:resnet18、resnet34、resnet50、resnet101、resnet152

# 其设计在网络中形成一个“窄口”,因此得名Bottleneck。
 1			# 1×1降维
111			# 3×3卷积
 1			# 1×1升维

Bottleneck模块是深度卷积神经网络中的一种高效结构设计,最早应用于ResNet系列(如ResNet-50、ResNet-101、ResNet-152)。其核心思想是在网络中引入“瓶颈层”,通过通道压缩与恢复实现计算效率与特征表达能力的平衡。

核心思想:
(1)高效计算:相比直接堆叠多个3×3卷积,Bottleneck可节省约三分之二的计算量。
(2)信息保真:先降维减少冗余,再卷积提取特征,最后升维恢复表达能力。
(3)设计目标在保证模型表达能力的同时,减少计算量和参数量,是深度残差网络成功的关键设计之一。
(4)应用领域:Bottleneck现已成为多种主干网络与检测、分割模型(如ResNet系列R-CNN / YOLO目标检测和分割Swin-UNet医学图像分割)的基础组件,是现代深层网络实现高效特征提取的重要结构单元。

假设输入特征图大小为 H × W × C i n H \times W \times C_{in} H×W×Cin,Bottleneck 降维中间通道为 C m i d C_{mid} Cmid,则:

  • 直接使用三个 3×3 卷积 FLOPs = 3 ⋅ ( 3 ⋅ 3 ⋅ C i n ⋅ C o u t ⋅ H ⋅ W ) \text{FLOPs} = 3 \cdot (3 \cdot 3 \cdot C_{in} \cdot C_{out} \cdot H \cdot W) FLOPs=3(33CinCoutHW)
  • Bottleneck FLOPs = ( 1 ⋅ 1 ⋅ C i n ⋅ C m i d ⋅ H ⋅ W ) + ( 3 ⋅ 3 ⋅ C m i d ⋅ C m i d ⋅ H ⋅ W ) + ( 1 ⋅ 1 ⋅ C m i d ⋅ C o u t ⋅ H ⋅ W ) \text{FLOPs} = (1 \cdot 1 \cdot C_{in} \cdot C_{mid} \cdot H \cdot W) + (3 \cdot 3 \cdot C_{mid} \cdot C_{mid} \cdot H \cdot W) + (1 \cdot 1 \cdot C_{mid} \cdot C_{out} \cdot H \cdot W) FLOPs=(11CinCmidHW)+(33CmidCmidHW)+(11CmidCoutHW)

具体而言,Bottleneck采用 1×1降维 → 3×3卷积 → 1×1升维 的三步结构:

  • 1×1卷积(降维):输入特征图的通道数较大,减少通道数以降低计算量,同时保留关键信息
    • 降维通道:通常为原通道数的 1/4,例如 ResNet-50 中 Bottleneck 模块, C i n = 256 → C m i d = 64 → C o u t = 256 C_in=256 → C_mid=64 → C_out=256 Cin=256Cmid=64Cout=256
  • 3×3卷积(特征提取):在低维空间中进行空间特征学习;
  • 1×1卷积(升维):恢复通道数以保持特征容量并便于残差连接。
  • 残差连接保证梯度流畅,便于训练深层网络。

在这里插入图片描述

公式化表示: H ( x ) = F ( x ) + x H(x) = F(x) + x H(x)=F(x)+x
其中:

  • x x x 是残差连接。当 F ( x ) = 0 F(x)=0 F(x)=0 时, H ( x ) = x H(x)=x H(x)=x,此时输出变为输入本身。
  • F ( x ) F(x) F(x) 是 Bottleneck 模块的卷积操作: F ( x ) = W 3 ⋅ σ ( W 2 ⋅ σ ( W 1 ⋅ x ) ) F(x) = W_3 \cdot \sigma(W_2 \cdot \sigma(W_1 \cdot x)) F(x)=W3σ(W2σ(W1x))
    • W 1 W_1 W1:1×1卷积降维
    • W 2 W_2 W2:3×3卷积
    • W 3 W_3 W3:1×1卷积升维
    • σ \sigma σ:激活函数(如 ReLU)

2、Attention(注意力机制 + 自注意力机制 + 多头自注意力机制)

阶段 (Type)时间代表模型核心思想特点应用场景
加性注意力 (Additive Attention)2014Seq2Seq Attention对序列中不同位置分配权重,提升信息聚焦能力解决RNN/LSTM长序列依赖问题,可动态计算上下文权重机器翻译、文本生成
点积注意力 (Dot-Product Attention)2015Luong Attention计算效率更高的序列注意力适合RNN/LSTM Seq2Seq,计算速度快机器翻译、摘要生成
自注意力 (Self-Attention)2017Transformer EncoderQ、K、V来自同一序列,实现序列内部全局依赖建模并行计算,捕获长距离依赖,序列内部信息交互NLP编码器/解码器、序列建模
多头自注意力 (Multi-Head Attention)2017Transformer/BERT/GPT多个注意力头并行,捕获不同子空间特征关系表达能力强,分头降低单头维度,训练稳定NLP模型、Vision Transformer
交叉注意力 (Cross-Attention) 2017–Transformer DecoderQ来自解码器,K/V来自编码器,实现序列间依赖建模支持编码器-解码器信息交互,增强上下文理解机器翻译、文本生成、跨模态任务
稀疏/局部注意力 (Sparse / Local Attention)2018–Longformer / BigBird限制注意力计算范围,只关注局部或子序列降低计算复杂度,适合超长序列长文本建模、视频序列处理
视觉Transformer (Vision Transformer, ViT)2020ViT将图像切分为Patch,应用多头自注意力进行全局建模全局特征捕获能力强,可替代卷积图像分类、图像理解任务
局部/层次化注意力 (Swin Transformer Attention)2021Swin Transformer窗口内局部注意力 + 层次化结构兼顾局部和全局信息,降低计算量图像分类、目标检测、图像分割
通道/空间注意力 (Channel / Spatial Attention)2017–SENet / CBAM对卷积特征图的通道或空间维度分配权重提升卷积网络特征表示能力CNN图像分类、检测、分割
多模态注意力 (Multi-Modal Attention)2019–CLIP / Flamingo不同模态间特征交互,通过注意力对齐捕获跨模态关联图像-文本匹配、视觉问答、多模态生成
本文章已经生成可运行项目
评论 37
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胖墩会武术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值