# 卷积神经网络

学习内容基于:Pytorch 框架与经典卷积神经网络与实战

# CNN 卷积神经网络算法原理

# 全连接神经网络

1

输入层是我们输入的数据,这里看到的第一列节点并不是输入层,中间为隐藏层。

输入层就像 X(自变量),模型或者说这些网络就是 F(函数),我们得到的输出就是 Y(因变量)。

# 为什么要使用激活函数

在神经网络中使用激活函数的根本原因是引入非线性,从而使模型能够拟合和表达复杂的函数关系。如果没有激活函数,神经网络无论堆叠多少层,本质上都是一个线性模型,能力极其有限。多层线性变换的叠加依然是线性变换,最终的模型只能拟合直线 / 平面,完全无法处理复杂的数据模式

# Sigmoid 激活函数

1

优点:简单、非常适用分类任务。

缺点:反向传播训练时有梯度消失的问题;输出值区间为 (0,1),关于 0 不对称;梯度更新在不同方向走得太远,使得优化难度增大,训练耗时。

# Tanh 激活函数

1

优点:解决了 Sigmoid 函数输出值非 0 对称的问题,训练比 Sigmoid 函数快,更容易收敛

缺点:反向传播训练时有梯度消失的问题,Tanh 函数和 Sigmoid 函数非常相似。

# ReLU 激活函数

1

优点:解决了梯度消失的问题;计算更为简单,没有 Sigmoid 函数和 Tanh 函数的指数运算

缺点:训练时可能出现神经元死亡

# Leaky ReLU 激活函数

1

优点:解决了 ReLU 的神经元死亡问题

缺点:无法为正负输入值提供一致的关系预测 (不同区间函数不同)

# 前向传播

前向传播是神经网络中数据从输入层依次流向输出层的过程,它的核心目标是根据当前的模型参数(权重和偏置)计算出预测结果。

前向传播就是 “把输入数据依次喂给每一层,经过线性计算 + 激活函数,逐层输出,最后得到预测值” 的过程。

# 损失函数

# 均方误差

1 1

前面有可能有出现 1/2,那只是为了方便求导,都是均方误差。

# 梯度下降法

1 1

# 全连接神经网络在图片中存在的问题

# 1. 参数量巨大

  • 全连接层的每一个神经元都与上一层的所有神经元相连。
  • 对于图片来说,输入通常是高维的,例如一张 224×224 的 RGB 图片就是 224×224×3 = 150,528 个输入特征。
  • 假设第一层有 1000 个神经元,那么权重数量就是 150,528 × 1000 ≈ 1.5 亿个参数!
  • 问题:参数太多 → 容易过拟合 → 训练时间长 → 需要大量显存。

# 2. 忽略空间结构

  • 图片是二维或三维(RGB)的网格数据,有局部空间相关性(邻近像素往往相关)。
  • 全连接层把图片 “拉平” 成一维向量,然后再进行矩阵乘法。
  • 问题:丢失了图片的空间信息(如边缘、纹理、形状),无法有效捕捉局部特征。

# 3. 缺乏平移不变性

  • 图像中物体的位置可能变化。
  • 全连接网络对输入的每个位置都固定,物体稍微移动,输出可能完全不同。
  • 问题:无法自动识别图像中的平移或局部位移,泛化能力差。

# 4. 计算效率低

  • 全连接层计算复杂度高(矩阵乘法量大)。
  • 对高分辨率图像,训练和推理速度都很慢。
  • 对比卷积神经网络(CNN),后者通过卷积核共享权重大幅减少计算量。

# 5. 不适合捕捉层次特征

  • 图片的特征是有层次结构的:边缘 → 纹理 → 形状 → 对象。
  • 全连接层一次性处理所有像素,无法自然学习层次特征。
  • CNN 则通过卷积和池化层逐步抽象特征,更符合视觉认知规律。

# 卷积、步幅、填充、池化

这部分请看:

# 经过卷积后特征图大小

1

FH:卷积核(filter)的高度(Filter Height)

FW:卷积核的宽度(Filter Width)

C_in:输入通道数(Input Channels)

C_out:输出通道数(Output Channels)

S:步幅(Stride)

P:填充(Padding)

如果算出来是小数,一般是向下取整。

# LeNet 与 AlexNet 原理

# LeNet-5 诞生背景

简单来说,LeNet-5 的诞生背景是为了解决手写数字识别这一实际应用问题,它是世界上首个成功商用的卷积神经网络,奠定了现代深度学习的基础。

# LeNet-5 网络结构

1
  1. 输入 1 * 28 * 28
  2. 5 * 5 卷积(6),填充 2
  3. 2 * 2 平均池化层,步幅 2
  4. 5 * 5 卷积(16),填充 0
  5. 2 * 2 平均池化层,步幅 2
  6. 全连接(120)
  7. 全连接(84)
  8. 全连接(10)

再强调一下:

1

这个计算公式非常重要。


# AlexNet 诞生背景

在算力达到临界点、大数据已经就位的环境下,一个古老但曾被忽视的算法(深度学习 / 卷积神经网络)迎来了证明自己的最佳时机。AlexNet 不仅仅是一个优秀的模型,它更是一个时代的开创者,是人工智能发展史上的一个关键转折点。

# AlexNet 网络结构

1

5 层卷积,3 层全连接,共 8 层,激活函数使用 ReLU。

  1. 输入 3 * 227 * 227(?这个可能有问题)
  2. 11 * 11 卷积(96),步幅 4,ReLU
  3. 3 * 3 最大池化,步幅 2
  4. 5 * 5 卷积(256),填充 2,ReLU
  5. 3 * 3 最大池化,步幅 2
  6. 3 * 3 卷积(384),填充 1,ReLU
  7. 3 * 3 卷积(384),填充 1,ReLU
  8. 3 * 3 卷积(256),填充 1,ReLU
  9. 3 * 3 最大池化,步幅 2
  10. 全连接(4096)
  11. 全连接(4096)
  12. 全连接(10)

这里在全连接层有很多的参数,参数太多容易过拟合,我们引入了 Dropout 操作。

# 图像增强 - 水平翻转

1

# 图像增强 - 随机裁剪

1

# 图像增强 - PCA

1
方面 描述
核心思想 利用 PCA 找到图像颜色的主要变化方向,并沿这些方向添加随机扰动来模拟光照变化。
目的 增强模型对颜色和光照变化的鲁棒性,是一种高效的数据增强手段。
优点 变化方式基于图像自身的统计特性,生成的图像颜色变化自然、合理。
缺点 计算成本相对较高(需要对每张图或每批图做 PCA)。
遗产 是 AlexNet 的一个创新点,启发了对颜色增强的重视,但已被更简单高效的方法所取代。

# LRN 正则化

这是一个针对通道间的计算

1

尽管 LRN 是 AlexNet 的一个关键创新,但在后续更深、更先进的网络(如 VGG、ResNet)中,它几乎被完全弃用了。主要原因如下:

  1. 效果有限且不稳定:后续研究发现,LRN 带来的性能提升非常微弱,甚至有时不稳定。其正则化效果远不如 Dropout Batch Normalization(BN) 那样显著和可靠。
  2. 被更好的技术取代
    • Dropout:通过随机断开神经元连接来防止过拟合,更为直接有效。
    • Batch Normalization(批归一化):这是革命性的技术。BN 对整个 Batch 的每个通道进行归一化(均值为 0,方差为 1),极大地改善了梯度流动,加速了训练,同时本身也具有轻微的正则化效果。BN 的效果远超 LRN,并且已经成为现代深度网络的标准组件。
  3. 增加计算开销和超参数:LRN 引入了额外的计算量,并且 k, α, β, n 这些超参数需要调优,增加了模型设计的复杂性。

# 重叠池化

与 LRN 类似,重叠池化在现代深度学习架构中也已经不常用了

  • 计算成本更高:由于存在重叠,为了得到相同尺寸的输出特征图,重叠池化需要进行更多次池化操作。例如,将 5x5 降采样到 2x2 ,非重叠池化 ( 2x2 , stride=2) 需要 4 次操作,而重叠池化 ( 3x3 , stride=2) 也需要 4 次操作,但每次操作的窗口更大,计算量稍高。
  • 被更有效的技术取代:如今,防止过拟合和提升性能的重任更多地由 Batch Normalization更深的网络结构(如 ResNet 的残差连接)、更先进的优化器Dropout 等方法来承担。
  • 设计趋势变化:现代网络有时甚至会完全摒弃池化层,转而使用步长大于 1 的卷积(Strided Convolution) 来同时实现特征提取和降采样,这被认为能提供更大的模型容量和灵活性。

# LeNet 实战

# 模型

class LeNet(nn.Module):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.block = nn.Sequential(
            nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, stride=1, padding=2),
            nn.Sigmoid(),
            nn.AvgPool2d(kernel_size=2, stride=2),
            nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5, stride=1, padding=0),
            nn.Sigmoid(),
            nn.AvgPool2d(kernel_size=2, stride=2),
            nn.Flatten(),
            nn.Linear(in_features=400, out_features=120),
            nn.Sigmoid(),
            nn.Linear(in_features=120, out_features=84),
            nn.Sigmoid(),
            nn.Linear(in_features=84, out_features=10)
        )
    def forward(self, x):
        return self.block(x)

# 设备、实例化与 summary

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net = LeNet().to(device)
summary(net, (1, 28, 28))
1

这里的 -1 代表的是 批量大小,更具体地说:-1 是一个占位符,表示这个维度的大小是由其他维度推断出来的,而不是一个固定值。

批量大小:在深度学习训练中,数据通常是按批次(batch)输入的。比如,你可能会一次输入 32 张图片、64 张图片等。这个数量就是批量大小。

为什么是 -1?:PyTorch 模型在设计时,其核心计算逻辑不依赖于具体的批量大小。为了增加灵活性,在定义模型的前向传播时,我们通常将输入张量的第一个维度设为批量大小。当打印模型摘要时,库(如 torchsummary)无法预先知道你会用多大的批量大小来训练,所以它使用 -1 来代表 “任何尺寸”。

动态推断:在实际运行中,这个 -1 会被你输入数据的真实批量大小所替代。

例如,如果你用一批 32 张图片输入到模型,那么 [-1, 1, 32, 32] 就会变成 [32, 1, 32, 32]。

如果你用 128 张图片,它就会变成 [128, 1, 32, 32]。

# 加载 FashionMNIST

from torchvision.datasets import FashionMNIST
from torchvision.transforms import transforms
import numpy as np
transform = transforms.Compose([
    transforms.Resize((28, 28)),
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])
train_data = FashionMNIST(root="./data/FashionMNIST", train=True, download=True, transform=transform)
test_data = FashionMNIST(root="./data/FashionMNIST", train=False, download=True, transform=transform)
from torch.utils.data import DataLoader
train_dataLoader = DataLoader(train_data, batch_size=64, shuffle=True)
test_dataLoader = DataLoader(test_data, batch_size=64, shuffle=False)

# 展示

import matplotlib.pyplot as plt
for setp, (features, label) in enumerate(train_dataLoader):
    if setp == 0:
        x = features.squeeze().numpy()
        y = label.numpy()
        break
plt.figure(figsize=(12, 5))
for ii in np.arange(len(y)):
    plt.subplot(4, 16, ii+1)
    plt.imshow(x[ii,:,:], cmap=plt.cm.gray)
    plt.title(y[ii])
    plt.axis("off")
plt.show()
1

# 训练、验证(此处为最佳实践)

from torch.optim.lr_scheduler import CosineAnnealingLR
from tqdm import tqdm
import copy
import time
import pandas as pd
def train_model_process(model, train_dataloader, val_dataloader, num_epochs):
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    scheduler = CosineAnnealingLR(
        optimizer,
        T_max=num_epochs,
        eta_min=1e-6
    )
    criterion = nn.CrossEntropyLoss().to(device)
    model = model.to(device)
    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0
    train_loss_all = []
    val_loss_all = []
    train_acc_all = []
    val_acc_all = []
    learning_rates = []
    since = time.time()
    for epoch in range(num_epochs):
        print("Epoch {}/{}".format(epoch, num_epochs - 1))
        print("-" * 10)
        current_lr = optimizer.param_groups[0]['lr']
        learning_rates.append(current_lr)
        print(f"当前学习率: {current_lr:.6f}")
        train_loss = 0.0
        train_corrects = 0
        val_loss = 0.0
        val_corrects = 0
        train_num = 0
        val_num = 0
        for step, (b_x, b_y) in tqdm(enumerate(train_dataloader), desc=f"总步骤:{len(train_dataloader)}",
                                     leave=False, unit="step", total=len(train_dataloader),
                                     bar_format="{desc}: |{bar:30}| {percentage:3.0f}% 唱跳Rap🏀,Music~",
                                     ascii="🏀🥰🥰😘"):
            b_x = b_x.to(device)
            b_y = b_y.to(device)
            model.train()
            output = model(b_x)
            pre_lab = torch.argmax(output, dim=1)
            loss = criterion(output, b_y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            train_loss += loss.item() * b_x.size(0)
            # 如果预测正确,则准确度 train_corrects 加 1
            train_corrects += torch.sum(pre_lab == b_y.data)
            train_num += b_x.size(0)
        for step, (b_x, b_y) in enumerate(val_dataloader):
            b_x = b_x.to(device)
            b_y = b_y.to(device)
            model.eval()
            output = model(b_x)
            pre_lab = torch.argmax(output, dim=1)
            loss = criterion(output, b_y)
            val_loss += loss.item() * b_x.size(0)
            val_corrects += torch.sum(pre_lab == b_y.data)
            val_num += b_x.size(0)
        train_loss_all.append(train_loss / train_num)
        train_acc_all.append(train_corrects.double().item() / train_num)
        val_loss_all.append(val_loss / val_num)
        val_acc_all.append(val_corrects.double().item() / val_num)
        print("{} train loss:{:.4f} train acc: {:.4f}".format(epoch, train_loss_all[-1], train_acc_all[-1]))
        print("{} val loss:{:.4f} val acc: {:.4f}".format(epoch, val_loss_all[-1], val_acc_all[-1]))
        if val_acc_all[-1] > best_acc:
            best_acc = val_acc_all[-1]
            best_model_wts = copy.deepcopy(model.state_dict())
        time_use = time.time() - since
        print("训练和验证耗费的时间{:.0f}m{:.0f}s".format(time_use // 60, time_use % 60))
        scheduler.step()
    model.load_state_dict(best_model_wts)
    torch.save(best_model_wts, "models/best_model.pth")
    train_process = pd.DataFrame(data={"epoch": range(num_epochs),
                                       "train_loss_all": train_loss_all,
                                       "val_loss_all": val_loss_all,
                                       "train_acc_all": train_acc_all,
                                       "val_acc_all": val_acc_all,
                                       "learn_rates": learning_rates})
    return train_process
def matplot_acc_loss(train_process):
    # 显示每一次迭代后的训练集和验证集的损失函数和准确率
    plt.figure(figsize=(12, 4))
    plt.subplot(1, 3, 1)
    plt.plot(train_process['epoch'], train_process.train_loss_all, "ro-", label="Train loss")
    plt.plot(train_process['epoch'], train_process.val_loss_all, "bs-", label="Val loss")
    plt.legend()
    plt.xlabel("epoch")
    plt.ylabel("Loss")
    plt.subplot(1, 3, 2)
    plt.plot(train_process['epoch'], train_process.train_acc_all, "ro-", label="Train acc")
    plt.plot(train_process['epoch'], train_process.val_acc_all, "bs-", label="Val acc")
    plt.xlabel("epoch")
    plt.ylabel("acc")
    plt.legend()
    plt.subplot(1, 3, 3)
    plt.plot(train_process['epoch'], train_process.learn_rates, "go-", label="Learn rates")
    plt.xlabel("epoch")
    plt.ylabel("Learn rates")
    plt.legend()
    plt.tight_layout()
    plt.show()

# 这里是优化后的版本

from torch.optim import lr_scheduler
import copy
import time
import pandas as pd
def train_model_process(model, train_dataloader, val_dataloader, num_epochs):
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    if not os.path.exists("models"):
        os.mkdir("models")
    optimizer = torch.optim.AdamW(model.parameters(), lr=3e-3, weight_decay=0.05)
    scheduler = lr_scheduler.SequentialLR(
        optimizer,
        schedulers=[
            lr_scheduler.LinearLR(optimizer, start_factor=0.1, total_iters=15),
            lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_epochs-15, eta_min=1e-6)
        ],
        milestones=[15]
    )
    criterion = nn.CrossEntropyLoss(label_smoothing=0.1).to(device)
    model = model.to(device)
    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0
    train_loss_all = []
    val_loss_all = []
    train_acc_all = []
    val_acc_all = []
    learning_rates = []
    since = time.time()
    for epoch in range(num_epochs):
        print("Epoch {}/{}".format(epoch, num_epochs - 1))
        print("-" * 10)
        current_lr = optimizer.param_groups[0]['lr']
        learning_rates.append(current_lr)
        print(f"当前学习率: {current_lr:.6f}")
        train_loss = 0.0
        train_corrects = 0
        val_loss = 0.0
        val_corrects = 0
        train_num = 0
        val_num = 0
        for step, (b_x, b_y) in tqdm(enumerate(train_dataloader), desc=f"总步骤:{len(train_dataloader)}",
                                     leave=False, unit="step", total=len(train_dataloader),
                                     bar_format="{desc}: |{bar:30}| {percentage:3.0f}% 我在努力训练,唱跳Rap🏀,Music~",
                                     ascii="🏀🥰🥰😘"):
            b_x = b_x.to(device)
            b_y = b_y.to(device)
            model.train()
            output = model(b_x)
            pre_lab = torch.argmax(output, dim=1)
            loss = criterion(output, b_y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            train_loss += loss.item() * b_x.size(0)
            # 如果预测正确,则准确度 train_corrects 加 1
            train_corrects += torch.sum(pre_lab == b_y.data)
            train_num += b_x.size(0)
        for step, (b_x, b_y) in tqdm(enumerate(val_dataloader), desc=f"总步骤:{len(val_dataloader)}",
                                     leave=False, unit="step", total=len(val_dataloader),
                                     bar_format="{desc}: |{bar:30}| {percentage:3.0f}% 该我上场表演了,唱跳Rap🏀,Music~",
                                     ascii="🏀🥰🥰😘"):
            b_x = b_x.to(device)
            b_y = b_y.to(device)
            model.eval()
            output = model(b_x)
            pre_lab = torch.argmax(output, dim=1)
            loss = criterion(output, b_y)
            val_loss += loss.item() * b_x.size(0)
            val_corrects += torch.sum(pre_lab == b_y.data)
            val_num += b_x.size(0)
        train_loss_all.append(train_loss / train_num)
        train_acc_all.append(train_corrects.double().item() / train_num)
        val_loss_all.append(val_loss / val_num)
        val_acc_all.append(val_corrects.double().item() / val_num)
        print("{} train loss:{:.4f} train acc: {:.4f}".format(epoch, train_loss_all[-1], train_acc_all[-1]))
        print("{} val loss:{:.4f} val acc: {:.4f}".format(epoch, val_loss_all[-1], val_acc_all[-1]))
        if val_acc_all[-1] > best_acc:
            best_acc = val_acc_all[-1]
            best_model_wts = copy.deepcopy(model.state_dict())
            checkpoint = {
                'epoch': epoch,
                'model_state_dict': model.state_dict(),
                'optimizer_state_dict': optimizer.state_dict(),
                'scheduler_state_dict': scheduler.state_dict(),
                'best_acc': best_acc,
            }
            torch.save(checkpoint, "models/best_checkpoint.pth")
        time_use = time.time() - since
        print("训练和验证耗费的时间{:.0f}m{:.0f}s".format(time_use // 60, time_use % 60))
        scheduler.step()
    model.load_state_dict(best_model_wts)
    torch.save(best_model_wts, "models/theBest.pth")
    train_process = pd.DataFrame(data={"epoch": range(num_epochs),
                                       "train_loss_all": train_loss_all,
                                       "val_loss_all": val_loss_all,
                                       "train_acc_all": train_acc_all,
                                       "val_acc_all": val_acc_all,
                                       "learn_rates": learning_rates})
    return train_process

这里是学习率使用 0.01,可以得到更好的效果,学习率为 0.001 在图像上更美观。

# 测试(此处为最佳实践)

def test_model_process(model, test_dataloader):
    device = "cuda" if torch.cuda.is_available() else 'cpu'
    model = model.to(device)
    test_corrects = 0.0
    test_num = 0
    with torch.no_grad():
        for test_data_x, test_data_y in test_dataloader:
            test_data_x = test_data_x.to(device)
            test_data_y = test_data_y.to(device)
            model.eval()
            output= model(test_data_x)
            pre_lab = torch.argmax(output, dim=1)
            test_corrects += torch.sum(pre_lab == test_data_y.data)
            test_num += test_data_x.size(0)
    test_acc = test_corrects.double().item() / test_num
    print("测试的准确率为:", test_acc)

# AlexNet 实战

class AlexNet(nn.Module):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.block = nn.Sequential(
            nn.Conv2d(in_channels=1, out_channels=96, kernel_size=11, stride=4),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(in_channels=96, out_channels=256, kernel_size=5, stride=1, padding=2),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(in_channels=256, out_channels=384, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=384, out_channels=384, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=384, out_channels=256, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Flatten(),
            nn.Linear(256 * 5 * 5, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(4096, 10)
        )
    def forward(self, x):
        x = self.block(x)
        return x

此处并不是标准是 AlexNet 模型,主要是为了适配 FashionMNIST。

train_process = train_model_process(AlexNet(), train_dataloader, val_dataloader, num_epochs=5)
matplot_acc_loss(train_process)

由于主包的 GPU 实在是太慢了,就不放结果图了。

# VGG 网络原理

VGGNet 有 6 种不同的结构,主要以 VGG-16 为核心拆解。

1

vgg-block 内的卷积层都是同结构的,池化层都得上一层的卷积层特征缩减一半,深度较深,参数量够大,较小的 filter size/kernel size

VGG 大量使用了 3 x 3 的卷积核,参数很小而且效果还不错。

还有 VGG 使用了块状结构,相当于一个小单元,非常方便。

# 模型

class VGG16(nn.Module):
    def __init__(self):
        super(VGG16, self).__init__()
        self.block1 = nn.Sequential(
            nn.Conv2d(1, 64, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(64, 64, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.block2 = nn.Sequential(
            nn.Conv2d(64, 128, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(128, 128, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.block3 = nn.Sequential(
            nn.Conv2d(128, 256, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.block4 = nn.Sequential(
            nn.Conv2d(256, 512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.block5 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.block6 = nn.Sequential(
            nn.Flatten(),
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(),
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Linear(4096, 10)
        )
    def forward(self, x):
        x = self.block1(x)
        x = self.block2(x)
        x = self.block3(x)
        x = self.block4(x)
        x = self.block5(x)
        x = self.block6(x)
        return x
1

# 最佳实践 —— 权重初始化

在我们训练的时候,我们的模型可能不收敛,训练出来的结果图很难看,实际上大概率可能是出现了梯度消失问题,核心原因是我们的权重初始化过于随机了。

为什么权重初始化如此重要?

在深度神经网络中,权重初始化直接影响:

  • 激活值的分布(前向传播)
  • 梯度的大小和稳定性(反向传播)
  • 模型是否收敛、收敛速度、最终性能

如果权重初始化不当,比如:

  • 权重太小 → 激活值趋近于 0 → 梯度消失
  • 权重太大 → 激活值饱和 → 梯度爆炸

所以我们必须要引入权重初始化

# 何凯明 - 凯明初始化法

凯明初始化法(Kaiming Initialization),又称 He 初始化,由 何恺明(Kaiming He) 在 2015 年提出,专为 ReLU 及其变种(如 LeakyReLU) 设计的权重初始化方法。

for param in self.modules():
    if isinstance(param, nn.Conv2d):
        nn.init.kaiming_normal_(param.weight, nonlinearity='relu')
        if param.bias is not None:
            nn.init.constant_(param.bias, 0)
for param in self.modules():
    # 卷积层初始化
    if isinstance(param, nn.Conv2d):
        nn.init.kaiming_normal_(param.weight, nonlinearity='relu')
        if param.bias is not None:
            nn.init.constant_(param.bias, 0)
    # 全连接层初始化
    elif isinstance(param, nn.Linear):
        nn.init.normal_(param.weight, 0, 0.01)
        if param.bias is not None:
            nn.init.constant_(param.bias, 0)

以上可以放在:

class VGG16_init(nn.Module):
    def __init__(self):
        super(VGG16, self).__init__()

初始化函数之下。

# 最佳实践 —— 调整批次

在深度学习中,调整批次大小(Batch Size) 是最有效、最低成本的性能调优手段之一。“最佳实践” 不是越大越好,也不是越小越精,而是根据硬件、任务、训练阶段动态权衡

# GoogLeNet 网络原理

1

这里面最唬人的地方就是这个 Inception 块,实际上没有那么吓人。

以前流行的网络使用小到 1×1,大到 7×7 的卷积核。 本文的一个观点:有时使用不同大小的卷积核组合是有利的。

1

通道合并 :将四个路线输出的通道合并。

# 1 x 1 卷积的优点

** 在不改变空间结构的前提下,高效地融合通道信息、调整通道维度、引入非线性,从而提升模型表达能力并降低计算成本。** 实现跨通道的交互和信息整合,卷积核通道数的降维和升维,减少网络参数。

# 全局平均池化 GAP

优点:“无参降维 + 抗过拟合”—— 把特征图全局平均成单个数值,直接当分类 logits,省掉全连接层,大幅减少参数量且强制保留通道级语义,降低过拟合风险。

缺点:“丢细节 + 强假设”—— 空间信息被压成一点,对细粒度特征或目标定位任务无能为力,并隐含 “通道即类别” 的假设,若类别间特征重叠则易混淆。

注意区别:全局平均池化 GAP 与直接 Flatten 平展的区别

# 如何训练自己的数据集

在深度学习中,设计一个良好的模型需要基础知识与运气,在此基础之上,数据的预处理往往是拉开差距的关键点。

# 数据集的划分

如何将这样的数据目录:

data_cat_dog
├── cat
└── dog

变成:

data
├── train
│   ├── cat
│   └── dog
└── test
    ├── cat
    └── dog

有这样的脚本:

import os
from shutil import copy
import random
def mkfile(file):
    if not os.path.exists(file):
        os.makedirs(file)
# 获取 data 文件夹下所有文件夹名(即需要分类的类名)
file_path = 'data_cat_dog'
flower_class = [cla for cla in os.listdir(file_path)]
# 创建 训练集 train 文件夹,并由类名在其目录下创建 5 个子目录
mkfile('data/train')
for cla in flower_class:
    mkfile('data/train/' + cla)
# 创建 验证集 val 文件夹,并由类名在其目录下创建子目录
mkfile('data/test')
for cla in flower_class:
    mkfile('data/test/' + cla)
# 划分比例,训练集:测试集 = 9 : 1
split_rate = 0.1
# 遍历所有类别的全部图像并按比例分成训练集和验证集
for cla in flower_class:
    cla_path = file_path + '/' + cla + '/'  # 某一类别的子目录
    images = os.listdir(cla_path)  # iamges 列表存储了该目录下所有图像的名称
    num = len(images)
    eval_index = random.sample(images, k=int(num * split_rate))  # 从 images 列表中随机抽取 k 个图像名称
    for index, image in enumerate(images):
        # eval_index 中保存验证集 val 的图像名称
        if image in eval_index:
            image_path = cla_path + image
            new_path = 'data/test/' + cla
            copy(image_path, new_path)  # 将选中的图像复制到新路径
        # 其余的图像保存在训练集 train 中
        else:
            image_path = cla_path + image
            new_path = 'data/train/' + cla
            copy(image_path, new_path)
        print("\r[{}] processing [{}/{}]".format(cla, index + 1, num), end="")  # processing bar
    print()
print("processing done!")

# 数据的预处理

# 重调整

transforms.Resize(160)

transforms.Resize((160,160))

第一个是的比例裁剪,第二个是指定像素裁剪,他们都是重调整。

# 随机裁剪

transforms.RandomResizedCrop(128, scale=(0.8, 1.0))

先随机在原图里裁出一块面积占 80 %–100 % 的区域,再直接 resize 成 128×128,既做了随机裁剪又做了尺度增强。

# 原始数据增强

transforms.AutoAugment(transforms.AutoAugmentPolicy.IMAGENET),

自动从 ImageNet 预训练好的 25 种增强策略里,随机挑一条子策略(含 5 种强度可变的图像变换)作用到输入图上,属于 “自动数据增强” 里的经典算法,无需手工设计组合。

  • 策略搜索阶段用强化学习在 ImageNet 上离线搜出 25 条子策略(每条含 5 个变换)。
  • 每次训练迭代时:
    1. 随机选一条子策略;
    2. 按该子策略里指定的概率、幅度依次对图像做 5 次变换;
    3. 变换列表包括 ShearX/Y , TranslateX/Y , Rotate , Color , Posterize , Solarize , Contrast , Sharpness , Brightness , AutoContrast , Equalize 等。

# 标准化

transforms.ToTensor()

本身就具有归一化的功能,他将数值转化为 0-1 的区间,但这只是 “线性缩放”,不是真正意义上的 “标准化 (normalization)”。

transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])

这才是真正的标准化:把数值变成均值为 0、方差为 1 的分布,加速模型收敛。

怎么算呢?还是来一个预处理。

from PIL import Image
import os
import numpy as np
# 文件夹路径,包含所有图片文件
folder_path = 'data_cat_dog'
# 初始化累积变量
total_pixels = 0
sum_normalized_pixel_values = np.zeros(3)  # 如果是 RGB 图像,需要三个通道的均值和方差
# 遍历文件夹中的图片文件
for root, dirs, files in os.walk(folder_path):
    for filename in files:
        if filename.endswith(('.jpg', '.jpeg', '.png', '.bmp')):
            image_path = os.path.join(root, filename)
            image = Image.open(image_path)
            image_array = np.array(image)
            # 归一化像素值到 0-1 之间
            normalized_image_array = image_array / 255.0
            # 累积归一化后的像素值和像素数量
            total_pixels += normalized_image_array.size
            sum_normalized_pixel_values += np.sum(normalized_image_array, axis=(0, 1))
# 计算均值和方差
mean = sum_normalized_pixel_values / total_pixels
sum_squared_diff = np.zeros(3)
for root, dirs, files in os.walk(folder_path):
    for filename in files:
        if filename.endswith(('.jpg', '.jpeg', '.png', '.bmp')):
            image_path = os.path.join(root, filename)
            image = Image.open(image_path)
            image_array = np.array(image)
            # 归一化像素值到 0-1 之间
            normalized_image_array = image_array / 255.0
            try:
                diff = (normalized_image_array - mean) ** 2
                sum_squared_diff += np.sum(diff, axis=(0, 1))
            except:
                print(f"捕获到自定义异常")
variance = sum_squared_diff / total_pixels
print("Mean:", mean)
print("Variance:", variance)

# 随机擦除

transforms.RandomErasing(p=0.3, scale=(0.02, 0.2))

以 30 % 的概率在图像上随机挖掉一块矩形区域(面积占图 2 %–20 %),用随机值(灰色、白色或黑色)填平,迫使模型学会 “靠局部也能猜对”,属于简单的正则化 / 抗遮挡增强。

# ResNet 原理与实战

1

# 网络持续加深带来那些问题

一、优化难题:网络越深越 “学不动”

二、表达难题:网络越深越 “记不住”

三、工程难题:网络越深越 “养不起”

“深” 本身不是错,错的是深 + Plain 堆叠;,

Plain Network = 只有 “卷积–BN–ReLU” 一路串行下去,不带任何跳跃连接的直筒式架构。

“链式求导” 本身就是根源 —— 但要把话拆成两句说:

  1. 链式求导必然导致深度网络里的梯度是 “连乘” 形式;
  2. 连乘的因子一旦持续小于 1(或大于 1),层数一多就指数级衰减 / 爆炸,这就是梯度消失 / 爆炸的数学本质

所以 Plain Net 的退化问题虽然表现形式是 “越深层训练误差越大”,但底层机制仍然绕不开链式求导带来的数值不稳定。
ResNet 的 skip connection 正是人为在链式乘积里插进一项 1,把 “连乘” 改写成 “连乘 + 1”,从而打断指数衰减 —— 用加法给链式法则打了一个补丁

# 残差块

ResNet(Residual Network)的核心创新就是残差连接(Residual Connection),它解决了深层网络的梯度消失问题,使得训练非常深的网络成为可能。

残差块的设计是深度学习领域的重大突破,它不仅在图像识别任务中表现出色,还被广泛应用于各种深度学习架构中。a = h (x) + x

1

上图有一个错误,填充应该是 0,步幅是 1。

# Batch Normalization 归一化

Batch Normalization(批归一化,简称 BN) 的目的是 让神经网络训练更快、更稳定、更容易收敛。

1

BatchNorm 不是 “锦上添花”,而是 “深度网络能训得动” 的刚需 —— 它把每层的输入分布强行拉回 N (0,1),切断梯度消失 / 爆炸与内部协变量偏移的恶性循环,让链式求导的连乘因子始终落在 1 附近,于是非常深的 Plain/ResNet 才吃得下大学习率、快速收敛且不用特别精调初始化。

BN 的解决方案非常直观有力:既然层输入的分布老在变,那我们就强制把它拉回一个稳定、标准的分布。

BN 的位置通常放在:

全连接层/卷积层 → BatchNorm → 激活函数(ReLU等)

# ResNet 的基本实现

# 残差块

class ResidualBlock(nn.Module):
    def __init__(self, in_channels, out_channels, use_conv1x1=False, stride=1) -> None:
        super(ResidualBlock).__init__()
        self.RelU = nn.ReLU(inplace=True)
        self.conv1 = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=stride,
                               padding=1)
        self.conv2 = nn.Conv2d(in_channels=out_channels, out_channels=out_channels, kernel_size=3, stride=stride,
                               padding=1)
        self.BN1 = nn.BatchNorm2d(out_channels)
        self.BN2 = nn.BatchNorm2d(out_channels)
        if use_conv1x1:
            self.conv3 = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=stride,
                                   stride=stride, padding=0)
        else:
            self.conv3 = None
    def forward(self, x):
        y = self.RelU(self.BN1(self.conv1(x)))
        y = self.BN2(self.conv2(y))
        if self.conv3 is not None:
            x = self.conv3(x)
        y = self.RelU(y + x)
        return y

# ResNet18

class ResNet18(nn.Module):
    def __init__(self, ResidualBlock) -> None:
        super(ResNet18, self).__init__()
        self.b1 = nn.Sequential(
            nn.Conv2d(in_channels=1, out_channels=64, kernel_size=7, stride=2, padding=3),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        )
        self.b2 = nn.Sequential(
            ResidualBlock(64, 64, use_conv1x1=False, stride=1),
            ResidualBlock(64, 64, use_conv1x1=False, stride=1)
        )
        self.b3 = nn.Sequential(
            ResidualBlock(64, 128, use_conv1x1=True, stride=2),
            ResidualBlock(128, 128, use_conv1x1=False, stride=1)
        )
        self.b4 = nn.Sequential(
            ResidualBlock(128, 256, use_conv1x1=True, stride=2),
            ResidualBlock(256, 256, use_conv1x1=False, stride=1)
        )
        self.b5 = nn.Sequential(
            ResidualBlock(256, 512, use_conv1x1=True, stride=2),
            ResidualBlock(512, 512, use_conv1x1=False, stride=1)
        )
        self.b6 = nn.Sequential(
            nn.AdaptiveAvgPool2d((1, 1)),
            nn.Flatten(),
            nn.Linear(512, 10)
        )
    def forward(self, x):
        x = self.b1(x)
        x = self.b2(x)
        x = self.b3(x)
        x = self.b4(x)
        x = self.b5(x)
        x = self.b6(x)
        return x
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

KarryLiu 微信支付

微信支付

KarryLiu 支付宝

支付宝