复合缩放EfficientNet原理详解(代码实现)

1. 为什么复合缩放更高效?

(1)单维度缩放的瓶颈

  • 增加深度(层数)
    更深的网络可以学习更复杂特征,但容易导致梯度消失/爆炸问题,且计算量随深度线性增长。
    问题:深层网络训练困难,性能提升呈现明显的收益递减。

  • 增加宽度(通道数)
    更宽的网络能捕捉更丰富的特征,但参数量和计算量随通道数平方增长。
    问题:过于浅层的宽网络可能浪费计算资源,无法有效捕捉高阶特征。

  • 提高分辨率
    高分辨率输入保留更多细节,但计算量随分辨率平方增长。
    问题:分辨率过高时,特征信息冗余且计算成本激增。

(2)复合缩放

传统模型的浪费 :
若仅增加网络深度(层数),可能导致梯度消失且计算量激增;若仅加宽通道数,参数量会平方级增长;若仅提高分辨率,冗余计算增多。这些单维度调整会导致 “投入多,回报少” ,即资源浪费。

EfficientNet 的改进 :
复合缩放通过同时但适度地增加这三个维度,能够更好地平衡模型的容量和计算效率,避免了某一维度过度增长带来的负面影响。

EfficientNet 提出用复合系数 ϕ\phiϕ同步缩放深度、宽度、分辨率:

深度=αϕ,宽度=βϕ,分辨率=γϕ\text{深度} = \alpha^\phi, \quad \text{宽度} = \beta^\phi, \quad \text{分辨率} = \gamma^\phi深度=αϕ,宽度=βϕ,分辨率=γϕ

其中 α,β,γ\alpha, \beta, \gammaα,β,γ 是通过网格搜索确定的最佳比例(例如 α=1.2,β=1.1,γ=1.15\alpha=1.2, \beta=1.1, \gamma=1.15α=1.2,β=1.1,γ=1.15),ϕ\phiϕ 控制总放大倍数。

  • 关键优势:三者按固定比例同步增长,确保每增加1倍计算量时,深度、宽度、分辨率均衡提升,避免资源浪费。

2. EfficientNet 的高效技术细节

(1)MBConv 模块:轻量化设计

  • 深度可分离卷积(Depthwise Separable Conv)

    • 传统卷积:一个 3×3×Cin×Cout3 \times 3 \times C_{\text{in}} \times C_{\text{out}}3×3×Cin×Cout 滤波器同时处理空间和通道信息。
    • 深度可分离卷积
      1. 深度卷积:每个通道单独用 3×33 \times 33×3 卷积处理(参数量:3×3×Cin3 \times 3 \times C_{\text{in}}3×3×Cin)。
      2. 点卷积:用 1×11 \times 11×1 卷积调整通道数(参数量:1×1×Cin×Cout1 \times 1 \times C_{\text{in}} \times C_{\text{out}}1×1×Cin×Cout)。
    • 效率提升:参数量和计算量减少约8~9倍,显著提高模型效率。
  • 残差连接
    在输入和输出通道数一致时添加残差连接,有效缓解梯度消失问题。

  • 膨胀机制(Expansion)
    通过 1×11 \times 11×1 卷积先扩展通道数(例如从32到144),再用深度卷积处理,最后压缩回原始通道数。
    效果:有效扩大感受野,显著增强特征表达能力。

(2)复合缩放的资源分配

  • 传统方法:单独放大某一维度(如加倍深度)会导致计算量激增,但性能提升有限。
  • EfficientNet方法:通过数学公式更合理地分配计算资源,例如:
    • ϕ=1\phi=1ϕ=1 时,深度 ×1.2,宽度 ×1.1,分辨率 ×1.15,总计算量 ≈ 原来的2倍。
    • 实验表明,这种均衡分配比单维度放大准确率提升约4%,且使用更少的参数量。

3. EfficientNet 的"高效"体现在哪里?

(1)准确率与计算量的优化平衡

  • 具体对比
    • EfficientNet-B4 在 ImageNet 上准确率达到83.0%,参数量仅19M
    • 相比之下,ResNet-50 准确率为76.5%,参数量25M
      核心优势:EfficientNet 用更少的参数和计算资源达到更高的准确率。

(2)灵活适应不同规模需求

  • 完整模型家族:从 B0(5M参数)到 B7(66M参数),全面覆盖从轻量级到高精度的各种应用需求。
  • 可调节性:通过调整 ϕ\phiϕ 值,用户可以灵活控制模型复杂度,在性能和效率间找到最佳平衡点。

4. 总结与直观解释

  • 核心技术优势
    复合缩放通过精确的数学公式均衡分配计算资源,避免单维度缩放的边际效应,同时 MBConv 模块设计大幅减少冗余计算。
  • 高效本质
    在相同计算量下,复合缩放能更充分地利用深度、宽度、分辨率三个维度的协同效应,最大化提升模型性能。

直观类比
传统方法就像给汽车单独升级引擎、轮胎或油箱,可能跑得更快但油耗剧增;
EfficientNet则像系统性地优化引擎功率、轮胎抓地力和油箱容量的平衡,让车在省油的同时跑得更远!🚗

关于分辨率

从技术上来说,"分辨率"通常指图片中像素的数量,与图像的尺寸直接相关。也就是说,大图片(像素多)在分辨率上是高的,而小图片(像素少)在分辨率上是低的。

但“清晰度”不仅仅取决于像素数量,还受到诸如对焦、噪点、压缩质量等因素的影响。所以一个分辨率高的大图片如果拍摄时模糊或存在噪点,可能看起来并不清晰;而一个小图片如果拍摄得非常精细和清晰,即使像素较少,视觉效果也可能更好。

总结来说,分辨率高与图片清晰度是两个不同的概念:

  • 分辨率高: 指的是图片包含更多像素,尺寸大。
  • 清晰度高: 指的是图片细节锐利、对比良好,质量好。

因此,“大图片”确实等于具有更高的分辨率,但并不必然意味着它的视觉清晰度也更好。

模型的表现很大程度上取决于输入图像的信息质量,而不仅仅是像素数量。具体来说:

  • 高分辨率的“大图片”
    如果图片虽然尺寸大,但不清晰(例如模糊、有噪点或者对焦不准),那么其中的细节信息可能会受到干扰,导致模型难以准确捕捉到有用的特征。

  • 清晰的“小图片”
    清晰的图片虽然尺寸较小、像素较少,但信息更集中、细节更明确,这样模型更容易提取到准确、可靠的特征。

总的来说,模型通常更倾向于从图像质量较高的输入中学习到更有用的信息。换句话说,即使大图片具有更多像素,如果图像质量较差,所提供的信息可能并不比一张清晰的小图片更有价值。理想状态下,模型最好能获得既清晰又高分辨率的图像,这样可以充分利用细节信息,同时保证图像的清晰度和质量。

所以在两者中,清晰的“小图片”通常会比不清晰的“大图片”带来更好的学习效果

代码案例(CIFAR-10数据集)

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from tqdm import tqdm


###############################################
# 1. 基本组件实现
###############################################

class DepthwiseSeparableConv(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0):
        """
        参数说明:
        - in_channels: 输入通道数
        - out_channels: 输出通道数
        - kernel_size: 卷积核尺寸
        - stride: 步长
        - padding: 填充尺寸
        """
        super().__init__()
        self.depthwise = nn.Conv2d(in_channels, in_channels, kernel_size,
                                   stride=stride, padding=padding, groups=in_channels, bias=False)
        self.bn1 = nn.BatchNorm2d(in_channels)
        self.act1 = nn.SiLU()
        self.pointwise = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)

    def forward(self, x):
        x = self.depthwise(x)
        x = self.bn1(x)
        x = self.act1(x)
        return self.pointwise(x)


class SEBlock(nn.Module):
    def __init__(self, in_channels, squeeze_ratio=4):
        """
        参数说明:
        - in_channels: 输入通道数
        - squeeze_ratio: 压缩比例,控制瓶颈通道数
        """
        super().__init__()
        self.se = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Conv2d(in_channels, in_channels // squeeze_ratio, kernel_size=1, bias=True),
            nn.SiLU(),
            nn.Conv2d(in_channels // squeeze_ratio, in_channels, kernel_size=1, bias=True),
            nn.Sigmoid()
        )

    def forward(self, x):
        return x * self.se(x)


class MBConv(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, stride, expand_ratio=4):
        """
        参数说明:
        - in_channels: 输入通道数
        - out_channels: 输出通道数
        - kernel_size: 深度卷积核尺寸
        - stride: 步长,决定是否下采样
        - expand_ratio: 通道扩展比率
        """
        super().__init__()
        expanded_channels = in_channels * expand_ratio
        self.use_residual = (in_channels == out_channels) and (stride == 1)

        # 优化后的MBConv实现
        self.expand_conv = nn.Sequential(
            nn.Conv2d(in_channels, expanded_channels, kernel_size=1, bias=False),
            nn.BatchNorm2d(expanded_channels),
            nn.SiLU()
        )

        self.depthwise = nn.Sequential(
            nn.Conv2d(expanded_channels, expanded_channels, kernel_size=kernel_size,
                      stride=stride, padding=kernel_size // 2, groups=expanded_channels, bias=False),
            nn.BatchNorm2d(expanded_channels),
            nn.SiLU()
        )

        self.se = SEBlock(expanded_channels)

        self.project_conv = nn.Sequential(
            nn.Conv2d(expanded_channels, out_channels, kernel_size=1, bias=False),
            nn.BatchNorm2d(out_channels)
        )

    def forward(self, x):
        residual = x

        x = self.expand_conv(x)
        x = self.depthwise(x)
        x = self.se(x)
        x = self.project_conv(x)

        if self.use_residual:
            return x + residual
        else:
            return x


###############################################
# 2. 构建适配CIFAR-10的EfficientNet模型
###############################################

class EfficientNet(nn.Module):
    def __init__(self, num_classes=10):
        """
        构造函数说明:
        - num_classes: 分类类别数,默认为CIFAR-10的10类
        """
        super().__init__()

        # 简化配置,减少模型大小
        configs = [
            (32, 16, 3, 1, 1, 1),  # (输入通道, 输出通道, 核大小, 步长, 扩展比例, 重复次数)
            (16, 24, 3, 1, 4, 2),  # 降低步长为1,减少特征图尺寸减少
            (24, 40, 5, 2, 4, 2),  # 降低扩展比例为4
            (40, 80, 3, 2, 4, 2),  # 降低重复次数为2
            (80, 112, 5, 1, 4, 1)  # 新配置:降低最终通道数和重复次数
        ]

        self.stem = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(32),
            nn.SiLU()
        )

        layers = []
        for in_channels, out_channels, kernel_size, stride, expand_ratio, repeats in configs:
            layers.append(MBConv(in_channels, out_channels, kernel_size, stride, expand_ratio))
            for _ in range(1, repeats):
                layers.append(MBConv(out_channels, out_channels, kernel_size, stride=1, expand_ratio=expand_ratio))

        self.blocks = nn.Sequential(*layers)

        # 减少最终特征通道数
        self.head = nn.Sequential(
            nn.Conv2d(112, 512, kernel_size=1, bias=False),  # 从1280减少到512
            nn.BatchNorm2d(512),
            nn.SiLU(),
            nn.AdaptiveAvgPool2d(1),
            nn.Flatten(),
            nn.Dropout(0.2),  # 添加Dropout防止过拟合
            nn.Linear(512, num_classes)
        )

    def forward(self, x):
        x = self.stem(x)
        x = self.blocks(x)
        return self.head(x)


###############################################
# 3. 主训练逻辑(主模块保护)
###############################################

if __name__ == '__main__':
    ###############################################
    # 3.1 数据准备与训练配置
    ###############################################

    # 减小图像尺寸,降低内存需求
    transform_train = transforms.Compose([
        transforms.Resize(56),  # 从64减小到56
        transforms.RandomCrop(56, padding=4),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
    ])

    transform_test = transforms.Compose([
        transforms.Resize(56),  # 保持一致
        transforms.ToTensor(),
        transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
    ])

    train_set = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_train)
    test_set = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform_test)

    # 减少批量大小,降低内存需求
    train_loader = torch.utils.data.DataLoader(train_set, batch_size=64, shuffle=True, num_workers=2)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=64, shuffle=False, num_workers=2)

    ###############################################
    # 3.2 模型、优化器与学习率调度器设置
    ###############################################
    use_gpu = True
    device = torch.device("cuda" if use_gpu and torch.cuda.is_available() else "cpu")
    print(f"Using device: {device}")

    # GPU内存监控
    if torch.cuda.is_available():
        print(f"GPU: {torch.cuda.get_device_name(0)}")
        print(f"初始内存分配: {torch.cuda.memory_allocated(0) / 1024 ** 2:.2f} MB")
        print(f"初始内存缓存: {torch.cuda.memory_reserved(0) / 1024 ** 2:.2f} MB")

    model = EfficientNet(num_classes=10).to(device)
    criterion = nn.CrossEntropyLoss()

    # 使用较低的学习率和权重衰减
    optimizer = optim.Adam(model.parameters(), lr=5e-4, weight_decay=1e-4)
    scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)


    ###############################################
    # 3.3 训练与测试函数定义
    ###############################################
    def train(epoch):
        model.train()
        total_loss = 0.0
        correct = 0
        total = 0
        progress_bar = tqdm(train_loader, desc=f'Epoch {epoch + 1}')

        for batch_idx, (inputs, targets) in enumerate(progress_bar):
            inputs, targets = inputs.to(device), targets.to(device)

            # 清零梯度
            optimizer.zero_grad()

            # 前向传播
            outputs = model(inputs)
            loss = criterion(outputs, targets)

            # 反向传播和优化
            loss.backward()
            optimizer.step()

            # 统计数据
            total_loss += loss.item()
            _, predicted = outputs.max(1)
            total += targets.size(0)
            correct += predicted.eq(targets).sum().item()

            # 更新进度条
            progress_bar.set_postfix(
                loss=total_loss / (batch_idx + 1),
                acc=100. * correct / total
            )

            # 每100个批次打印GPU内存使用情况
            if batch_idx % 100 == 0 and torch.cuda.is_available():
                print(f"\nBatch {batch_idx}, GPU内存: {torch.cuda.memory_allocated(0) / 1024 ** 2:.2f} MB")


    def test():
        model.eval()
        test_loss = 0.0
        correct = 0
        total = 0

        with torch.no_grad():
            for inputs, targets in tqdm(test_loader, desc='Testing'):
                inputs, targets = inputs.to(device), targets.to(device)
                outputs = model(inputs)
                loss = criterion(outputs, targets)

                test_loss += loss.item()
                _, predicted = outputs.max(1)
                total += targets.size(0)
                correct += predicted.eq(targets).sum().item()

        avg_loss = test_loss / len(test_loader)
        acc = 100. * correct / total
        print(f'测试结果 | 损失: {avg_loss:.4f} | 准确率: {acc:.2f}%')
        return acc


    ###############################################
    # 3.4 主训练循环
    ###############################################
    best_acc = 0
    num_epochs = 10  # 增加训练轮数

    try:
        for epoch in range(num_epochs):
            train(epoch)
            scheduler.step()

            # 测试当前模型性能
            acc = test()

            # 保存最佳模型
            if acc > best_acc:
                best_acc = acc
                print(f'这轮epoch准确率: {best_acc:.2f}%')
                # torch.save(model.state_dict(), 'efficientnet_cifar10_best.pth')

        # 保存最终模型
        torch.save(model.state_dict(), 'efficientnet_cifar10_final.pth')
        print(f'训练完成,保存了最终模型,最佳准确率: {best_acc:.2f}%')

    except Exception as e:
        print(f"训练过程中出现错误: {e}")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

frostmelody

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

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

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

打赏作者

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

抵扣说明:

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

余额充值