pytorch 实战教程之基于Darknet-53(Darknet-53神经网络原理详解)对CIFAR-10数据集分类完整实现 (Yolo目标检测网络前置)

原文作者:aircraft

原文链接:https://www.cnblogs.com/DOMLX/p/18779326

 

 

 

 

 

              本来想写的是pytorch的入门教程的,不过想想入门教程需要解释的各种原理各种函数就觉得麻烦,还是先写一些实战篇的教程把。。。。。。偷懒中QAQ

 

 

            学习YOLOv5前的准备就是学习DarkNet53网络,FPN特征金字塔网络,PANet结构,(从SPP到SPPF)SPPF空间金字塔池化等。本篇讲DarkNet53网络。。。

 

Darknet-53 是一种高效的卷积神经网络架构,由 Joseph Redmon 在 ​​YOLOv3 目标检测系统​​中首次提出,作为特征提取的骨干网络(Backbone)。其名称中的“53”表示网络包含 53 个卷积层。以下是该网络的详细介绍:

 

 

图一开始看不懂没关系,先理解代码再回来看图,你发现你就能看懂了。 

 

 

一、核心设计理念​

  1. ​深层特征提取​
    通过堆叠卷积层和残差块(Residual Blocks),实现多尺度特征提取,适合处理复杂视觉任务。

  2. ​残差连接优化​
    借鉴 ResNet 思想,引入跳跃连接(Shortcut Connections),解决深层网络梯度消失问题。

  3. ​计算效率平衡​
    在模型深度与计算量之间取得平衡,适用于实时检测任务(如 YOLOv3)。

二、网络结构详解​

​1. 整体架构​

Darknet-53 由 ​​5 个阶段(Stage)​​ 构成,每阶段通过下采样(Strided Convolution)缩小特征图尺寸,同时增加通道数:

阶段操作序列输出尺寸 (CIFAR-10)通道数变化
Stage1 Conv3x3 → Conv3x3/2 → ResBlock×1 16x16 3 → 64
Stage2 Conv3x3/2 → ResBlock×2 8x8 64 → 128
Stage3 Conv3x3/2 → ResBlock×8 4x4 128 → 256
Stage4 Conv3x3/2 → ResBlock×8 2x2 256 → 512
Stage5 Conv3x3/2 → ResBlock×4 1x1 512 → 1024

​2. 残差块设计​-------残差结构Residual

每个残差块包含 ​​1x1 压缩 + 3x3 扩展​​ 的双卷积结构:

ResBlock = Sequential(
    Conv1x1(in_ch, mid_ch),   # 压缩通道至1/2
    Conv3x3(mid_ch, in_ch)    # 恢复原始通道数
)

 

  • ​跳跃连接​​:这里就是残差网络ResNet的核心思想了,输入参数直接与双卷积输出相加,公式:output = ConvBlock(x) + x

ResNet中的残差块通常有两种设计:一种是两个3x3卷积层,另一种是先用1x1减少通道数,再用3x3卷积,最后再用1x1恢复通道数,也就是所谓的Bottleneck结构。Darknet53可能采用了类似Bottleneck的结构,但具体为什么在残差块中使用1x1后接3x3呢?下面详细讲讲:

 

在Darknet53的残差块(Residual Block)中,先使用​​1×1卷积​​再使用​​3×3卷积​​的设计是一个关键优化策略,其核心目的是在​​减少计算量​​的同时​​保持网络表达能力​​,同时解决深层网络的梯度传递问题。以下是具体原因和设计逻辑:

 在详细一点可以看看这个博客深入浅出之Bottleneck层、Res unit和ResX模块(YOLO):https://blog.csdn.net/a8039974/article/details/142349866

 

​1. 降低计算复杂度​

 

​2. 增强特征表达能力​

    • ​非线性组合​​:
      1×1卷积后通常接激活函数(如LeakyReLU),将输入特征的通道信息进行非线性重组,提取更高维度的抽象特征。

      • ​示例​​:输入特征图的通道可能包含冗余信息,1×1卷积通过线性组合筛选重要通道(类似“注意力”机制),再通过3×3卷积捕捉空间局部特征。
    • ​多尺度特征融合​​:

      • 1×1卷积负责跨通道信息融合(Channel-wise Interaction)。
      • 3×3卷积负责局部空间特征提取(Spatial Interaction)。
        二者结合实现“通道-空间”双重优化。

​3. 解决梯度消失问题​

  • ​残差连接的作用​​:
    Darknet53的残差块通过跳跃连接(Shortcut)将输入直接叠加到输出,缓解梯度消失问题。
    • ​梯度传播路径​​:
      若直接使用3×3卷积,深层网络可能因多次卷积导致梯度衰减;而1×1卷积先压缩通道,减少后续3×3卷积的层数影响,使梯度更稳定地回传。

​4. 与ResNet的对比​

ResNet的Bottleneck结构(1×1→3×3→1×1)通过两次1×1卷积压缩和恢复通道数,但Darknet53的设计更简化:

  • ​Darknet53的残差块​​:1×1→3×3(无最终1×1恢复通道---因为也可以在3x3的部分直接设定与输出对应的卷积核完成升维)。
    • ​原因​​:Darknet53作为目标检测网络(如YOLOv3)的骨干,需平衡速度和精度,直接使用1×1+3×3减少计算量,同时残差连接本身已提供特征复用能力。

5. 实验验证的优势​

    • ​ImageNet分类任务​​:
      Darknet53在保持与ResNet-152相近精度的前提下,计算量减少约50%。
    • ​目标检测任务​​:
      在YOLOv3中,Darknet53的残差结构显著提升了对小目标和复杂场景的检测能力,得益于高效的特征融合和梯度传递。

 

3*3的卷积核作用都不陌生,1*1的大家就可能见得不频繁了:

 

 

 

这里想详细讲一下残差网络的优点:

​为什么深层网络训练困难?——梯度消失问题​

在深层神经网络中,信息需要通过多个层逐层传递。无论是前向传播(计算预测值)还是反向传播(计算梯度更新参数),信号都会经过多个非线性变换(如激活函数、矩阵乘法等)。梯度消失问题的本质是:

    1. ​反向传播的链式法则​
      梯度通过反向传播算法计算,使用链式法则(Chain Rule)逐层求导。假设有 L 层网络,梯度是各层导数的乘积:

 

 激活函数的导数问题​
例如 Sigmoid 函数的导数最大值为0.25,ReLU 在负数区域的导数为0。当网络较深时,多个小导数的乘积会趋近于0。

 实际效果​​:残差网络ResNet 是深度学习历史上第一个突破千层的网络,开启了超深度模型的新时代。

 

3. 分类头(本文代码实现)​

  • ​全局平均池化​​:将最终特征图压缩至 1x1(AdaptiveAvgPool2d(1)
  • ​全连接层​​:1024维特征 → 10类别概率(CIFAR-10)

三、技术优势​

​1. 残差连接的优势​

  • ​梯度直传​​:跳跃连接为反向传播提供“高速公路”,缓解梯度消失,对于深层神经网络的反向转播的梯度更新,如果。
  • ​特征复用​​:原始输入信息直接传递,增强特征表达能力。

​2. 计算效率​

  • ​参数量对比​​(与 ResNet-152 对比):
    模型参数量Top-1 准确率(ImageNet)
    Darknet-53 ~25M 77.2%
    ResNet-152 ~60M 78.3%
  • ​推理速度​​:Darknet-53 的浮点运算量(FLOPs)更低,适合实时任务。

​3. 多尺度特征提取​

  • ​层级特征图​​:不同阶段输出不同尺度的特征图(如 8x8、4x4),适用于目标检测中的多尺度预测。

四、应用场景​

  1. ​目标检测​

    • 作为 YOLOv3/v4 的骨干网络,提取多尺度特征。
    • 输出三个尺度的特征图(如 13x13、26x26、52x52)。
  2. 图像分类​

    • 用户代码适配 CIFAR-10 的 32x32 小尺寸图像,通过调整池化层实现分类。

五、本代码与原论文代码差异​

特性原版 Darknet-53用户代码实现
​输入尺寸​ 256x256(ImageNet) 32x32(CIFAR-10)
​残差块​ 每个块使用两个 3x3卷积 1x1 + 3x3 卷积组合
​分类头​ 全连接层(1000类) 全局池化 + 全连接层
​激活函数​ LeakyReLU(原版使用) LeakyReLU(与原文一致)

 

对于归一化函数BatchNorm2d可以看看这些博客:一文搞懂归一化(PyTorch)pytorch中对BatchNorm2d()函数的理解

卷积层和池化层的计算公式(图都是网上找的,怕你们看代码不知道经过卷积层和池化层后图像的长宽为什么这样变化的):

 

 

 

 

 

然后在讲一下本文代码中为什么使用LeakyReLU激活函数而不是ReLU激活函数,或者说LeakyReLU激活函数对比一下ReLU激活函数谁更优秀?

在卷积神经网络(CNN)中,​​LeakyReLU​​和​​ReLU​​各有优缺点,它们的适用性取决于具体任务和网络结构。以下是两者的对比分析及结论:

 

​1. ReLU的优缺点​

 

  • ​优点​​:
    • 计算高效​​:ReLU(f(x)=max(0,x))计算简单,加速模型训练。
    • ​稀疏激活​​:在正区间梯度为1,负区间为0,能有效稀疏化网络。
  • ​缺点​​:
    • 神经元死亡​​:负区间梯度为0,可能导致部分神经元永久失效(Dead ReLU问题),尤其在初始化不当或学习率过高时。

 

​2. LeakyReLU的改进​

 

  • ​设计​​:LeakyReLU(f(x)=max(αx,x),α通常取0.01)在负区间引入小的梯度α,缓解神经元死亡问题。
  • ​优点​​:
    • 避免梯度消失​​:负区间的非零梯度允许参数更新,提升训练稳定性。
    • ​潜在性能提升​​:在深层网络或复杂任务中,可能比ReLU表现更好(如某些图像分类任务)。
  • ​缺点​​:
    • ​需调参​​:需手动设置α(虽然通常固定为0.01),而PReLU(Parametric ReLU)可学习α,但增加计算量。
    • ​效果不确定性​​:并非所有任务都能显著优于ReLU,需实验验证。

 

3. 实验结论​

 

  • ​深层网络​​:LeakyReLU通常表现更好(如ResNet、DenseNet),因深层网络更易出现梯度消失和神经元死亡。
  • ​简单任务/浅层网络​​:ReLU可能足够且更高效,如MNIST分类。
  • ​数据量​​:大数据集下LeakyReLU的优势更明显,小数据集差异可能不显著。

 

4. 实际应用建议​

 

  1. ​默认尝试ReLU​​:作为基线,因其简单高效。
  2. ​复杂场景尝试LeakyReLU​​:若训练不稳定或性能不佳,可替换为LeakyReLU或PReLU。
  3. ​参数调整​​:对LeakyReLU,可尝试不同α值(如0.01、0.1)或使用PReLU自动学习。
  4. ​结合其他技术​​:如BatchNorm可缓解Dead ReLU问题,与ReLU搭配可能足够。

 

5. 总结​LeakyReLU激活函数与ReLU激活函数在什么场景下更优

 

  • ​LeakyReLU更优的场景​​:深层CNN、复杂任务、数据量大、训练中观察到神经元死亡。
  • ​ReLU更优的场景​​:浅层网络、简单任务、资源受限(需极简计算)。
  • ​最终策略​​:两者均需实验验证,结合任务需求选择。

 

 

 

 

实例代码(里面的数据集都是在线下载大概160M左右,注释都已经打的非常的详细了噢,认真看注释就行了):

 

"""
基于Darknet-53的CIFAR-10分类完整实现
包含数据加载、模型定义、训练、测试、可视化全流程
"""

# ----------------- 导入所需库 -----------------
import torch
import torch.nn as nn
from torch.nn import modules  # 模块化神经网络组件
from torch.utils.data import DataLoader  # 数据加载器
from torchvision.datasets import CIFAR10  # CIFAR-10数据集
from torchvision.transforms import transforms  # 数据预处理
import matplotlib.pyplot as plt  # 可视化绘图
import numpy as np  # 数值计算
from sklearn.metrics import confusion_matrix, classification_report  # 评估指标
import seaborn as sns  # 美观的可视化

# ----------------- 基础模块定义 -----------------
class ConvBnLeaky(modules.Module):
    """卷积+批量归一化+LeakyReLU组合层"""
    def __init__(self, in_it, out_it, kernels, padding=0, strides=1):
        """
        参数说明:
        - in_it:  输入通道数
        - out_it: 输出通道数
        - kernels: 卷积核尺寸(整数或元组)
        - padding: 填充像素数(默认为0)
        - strides: 卷积步长(默认为1)
        """
        super(ConvBnLeaky, self).__init__()
        self.convs = nn.Sequential(
            nn.Conv2d(in_it, out_it, kernels, 
                     padding=padding, stride=strides),  # 卷积操作
            nn.BatchNorm2d(out_it),  # 批量归一化加速训练收敛
            nn.LeakyReLU(0.1, inplace=True)  # 带泄露的ReLU(负轴斜率0.1)
        )
        
    def forward(self, x):
        """前向传播"""
        return self.convs(x)

class Resnet_Block(modules.Module):
    """残差模块(包含多个残差块)"""
    def __init__(self, ch, num_block=1):
        """
        参数说明:
        - ch:       通道数
        - num_block: 残差块的数量
        """
        super(Resnet_Block, self).__init__()
        self.module_list = modules.ModuleList()  # 存储子模块的列表
        
        # 构建指定数量的残差块
        for _ in range(num_block):
            resblock = nn.Sequential(
                # 第一个卷积压缩通道数
                ConvBnLeaky(ch, ch//2, 1),       # 1x1卷积(降维)
                # 第二个卷积恢复通道数
                ConvBnLeaky(ch//2, ch, 3, padding=1)  # 3x3卷积(特征提取)
            )
            self.module_list.append(resblock)
            
    def forward(self, x):
        """前向传播(含残差连接)"""
        for block in self.module_list:
            # 残差连接公式:F(x) + x
            x = block(x) + x  # 保留原始输入信息
        return x

# ----------------- 网络主体结构 -----------------
class Darknet_53(modules.Module):
    """Darknet-53主干网络"""
    def __init__(self, num_classes=10):
        super(Darknet_53, self).__init__()
        # 第一阶段(高分辨率特征提取)
        self.later_1 = nn.Sequential(
            ConvBnLeaky(3, 32, 3, padding=1),         # 3→32通道(保持32x32分辨率)
            ConvBnLeaky(32, 64, 3, padding=1, strides=2), # 下采样至16x16
            Resnet_Block(64, 1)                       # 1个残差块
        )
        # 第二阶段
        self.later_2 = nn.Sequential(
            ConvBnLeaky(64, 128, 3, padding=1, strides=2), # 下采样至8x8
            Resnet_Block(128, 2)                       # 2个残差块
        )
        # 第三阶段
        self.later_3 = nn.Sequential(
            ConvBnLeaky(128, 256, 3, padding=1, strides=2), # 下采样至4x4
            Resnet_Block(256, 8)                        # 8个残差块
        )
        # 第四阶段
        self.later_4 = nn.Sequential(
            ConvBnLeaky(256, 512, 3, padding=1, strides=2), # 下采样至2x2
            Resnet_Block(512, 8)                        # 8个残差块
        )
        # 第五阶段(深层特征提取)
        self.later_5 = nn.Sequential(
            ConvBnLeaky(512, 1024, 3, padding=1, strides=2), # 下采样至1x1
            Resnet_Block(1024, 4)                       # 4个残差块
        )
        # 分类头
        self.pool = nn.AdaptiveAvgPool2d((1,1))  # 全局平均池化(输出1x1x1024)
        self.fc = nn.Linear(1024, num_classes)   # 全连接层(1024→10类别)
        
    def forward(self, x):
        """前向传播流程"""
        x = self.later_1(x)
        x = self.later_2(x)
        x = self.later_3(x)
        x = self.later_4(x)
        x = self.later_5(x)
        x = self.pool(x)          # [batch,1024,1,1]
        x = torch.squeeze(x)      # 去除冗余维度 → [batch,1024]
        x = self.fc(x)            # [batch,10]
        return x

# ----------------- 主函数(训练+测试) -----------------
def main():
    # ----------------- 数据加载 -----------------
    # 训练集(自动下载到./data目录)
    train_data = CIFAR10(
        root='./data',
        train=True,
        transform=transforms.ToTensor()  # 转为Tensor并归一化到[0,1]
        download=True
          )

    train_loader = DataLoader(
        train_data,
        batch_size=64,
        shuffle=True    # 训练时打乱数据顺序
    )
    
    # 测试集
    test_data = CIFAR10(
        root='./data', 
        train=False,
        transform=transforms.ToTensor(),
        download=True  # 如果不存在则自动下载
    )
    test_loader = DataLoader(
        test_data, 
        batch_size=64, 
        shuffle=False  # 测试不需要打乱顺序
    )

    # ----------------- 模型配置 -----------------
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    net = Darknet_53().to(device)  # 实例化模型并移至GPU
    criterion = nn.CrossEntropyLoss().to(device)  # 交叉熵损失函数
    optimizer = torch.optim.Adam(net.parameters(), lr=0.001)  # Adam优化器
    
    # ----------------- 训练循环 -----------------
    print("开始训练...")
    train_losses = []  # 记录每个epoch的训练损失
    train_accs = []    # 记录每个epoch的训练准确率
    
    for epoch in range(10):
        net.train()  # 设置为训练模式
        total_correct = 0  # 累计正确预测数
        total_loss = 0     # 累计损失值
        
        # 迭代所有训练批次
        for images, labels in train_loader:
            # 数据移至GPU
            images = images.to(device)
            labels = labels.to(device)
            
            # 前向传播
            outputs = net(images)
            loss = criterion(outputs, labels)
            
            # 反向传播与优化
            optimizer.zero_grad()  # 清空梯度(重要!)
            loss.backward()        # 计算梯度
            optimizer.step()       # 更新参数
            
            # 统计训练指标
            _, preds = torch.max(outputs, 1)  # 获取预测类别(dim=1)
            total_correct += (preds == labels).sum().item()  # 累计正确数
            total_loss += loss.item()         # 累计损失
        
        # 计算epoch指标
        epoch_acc = total_correct / len(train_data)  # 准确率 = 正确数 / 总样本数
        epoch_loss = total_loss / len(train_loader)  # 平均损失 = 总损失 / 批次数
        
        # 记录指标
        train_losses.append(epoch_loss)
        train_accs.append(epoch_acc)
        
        # 打印训练进度
        print(f"\nEpoch {epoch+1}/10")
        print(f"Train Loss: {epoch_loss:.4f} | Acc: {epoch_acc*100:.2f}%")

    # ----------------- 训练过程可视化 -----------------
    plt.figure(figsize=(12, 5))
    # 损失曲线(左图)
    plt.subplot(1, 2, 1)
    plt.plot(train_losses, 'b-o', label='Training Loss')
    plt.title("Training Loss Curve")
    plt.xlabel("Epochs")
    plt.ylabel("Loss")
    plt.legend()
    
    # 准确率曲线(右图)
    plt.subplot(1, 2, 2)
    plt.plot(train_accs, 'r-o', label='Training Accuracy')
    plt.title("Training Accuracy Curve")
    plt.xlabel("Epochs")
    plt.ylabel("Accuracy")
    plt.legend()
    
    plt.tight_layout()  # 调整子图间距
    plt.show()

    # ----------------- 测试集验证 -----------------
    print("\n开始测试...")
    all_preds = []   # 存储所有预测结果
    all_labels = []  # 存储所有真实标签
    test_loss = 0    # 测试损失
    
    net.eval()  # 设置为评估模式(关闭Dropout等)
    with torch.no_grad():  # 禁用梯度计算(节省内存)
        for images, labels in test_loader:
            images = images.to(device)
            labels = labels.to(device)
            
            outputs = net(images)
            loss = criterion(outputs, labels)
            test_loss += loss.item()  # 累计测试损失
            
            _, preds = torch.max(outputs, 1)
            # 收集预测结果和标签(移至CPU)
            all_preds.extend(preds.cpu().numpy())
            all_labels.extend(labels.cpu().numpy())
    
    # 计算测试指标
    test_loss = test_loss / len(test_loader)  # 平均测试损失
    test_acc = np.mean(np.array(all_preds) == np.array(all_labels))  # 测试准确率
    
    # 打印测试结果
    print("\n测试结果:")
    print(f"Test Loss: {test_loss:.4f} | Acc: {test_acc*100:.2f}%")
    print("\n分类报告(按类别统计):")
    print(classification_report(all_labels, all_preds, target_names=test_data.classes))

    # ----------------- 混淆矩阵可视化 -----------------
    cm = confusion_matrix(all_labels, all_preds)
    plt.figure(figsize=(10, 8))
    sns.heatmap(
        cm, 
        annot=True,  # 显示数值
        fmt='d',     # 整数格式
        cmap='Blues',# 蓝色系颜色
        xticklabels=test_data.classes,  # x轴类别名称
        yticklabels=test_data.classes   # y轴类别名称
    )
    plt.title("Confusion Matrix")
    plt.xlabel("Predicted Labels")
    plt.ylabel("True Labels")
    plt.show()

    # ----------------- 预测示例可视化 -----------------
    classes = test_data.classes  # 获取类别名称列表
    # 从测试集中获取一批样本(10个)
    sample_images, sample_labels = next(iter(test_loader))
    sample_images = sample_images.to(device)
    
    # 预测
    net.eval()
    with torch.no_grad():
        outputs = net(sample_images)
        _, preds = torch.max(outputs, 1)
    
    # 调整图像格式(适用于matplotlib显示)
    sample_images = sample_images.cpu().numpy()
    sample_images = np.transpose(sample_images, (0, 2, 3, 1))  # [B,C,H,W] → [B,H,W,C]
    
    # 绘制预测结果对比图
    plt.figure(figsize=(15, 6))
    for i in range(10):  # 显示前10个样本
        plt.subplot(2, 5, i+1)
        plt.imshow(sample_images[i])
        plt.title(f"True: {classes[sample_labels[i]]}\nPred: {classes[preds[i]]}")
        plt.axis('off')  # 关闭坐标轴
    plt.tight_layout()
    plt.show()

if __name__ == '__main__':
    main()

 

看完代码中我注释标注的五个阶段再去看他的网络图,是不是就清晰了。

 

如果安装失败可以使用下面的方法试试安装scikit-learn
pip install --upgrade --force-reinstall --user scikit-learn -i https://pypi.tuna.tsinghua.edu.cn/simple

 

 运行过程会打印损失和ACC

 混淆矩阵显示结果:

 

 

预测示例可视化:

 

 

总结​

Darknet-53 是一种高效平衡深度与计算量的骨干网络,其残差设计和层级特征提取能力使其在目标检测和分类任务中表现卓越。代码通过调整下采样策略和分类头,成功将其适配到小尺寸图像分类任务,展现了该架构的灵活性。作为YOLO网络组成的一部分,可以在学习YOLO之前可以先学习这个,等你把YOLO的几个零件(比如PANet)学习完毕,自然就能很快的学完YOLO。

 

 

只要一个个的把YOLO我网络的前身给分别学习完毕,这样学习YOLO就很快了

posted @ 2025-04-10 21:44  aircraft  阅读(611)  评论(0)    收藏  举报