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 滤波器同时处理空间和通道信息。
- 深度可分离卷积:
- 深度卷积:每个通道单独用 3×33 \times 33×3 卷积处理(参数量:3×3×Cin3 \times 3 \times C_{\text{in}}3×3×Cin)。
- 点卷积:用 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}")