3. 量化感知训练入门
任务目标
在CIFAR-10数据集上,使用PyTorch搭建一个简易CNN,通过torch.ao.quantization的FakeQuantize模块模拟量化误差,完成量化感知训练(QAT)。最终对比普通训练与QAT训练后的模型在验证集上的Top-1准确率,观察伪量化操作对精度的影响。
环境与依赖
- Python 3.8+,PyTorch 1.13+(需包含
torch.ao.quantization) - torchvision(用于CIFAR-10数据集加载)
- 无需GPU,CPU即可完成训练(为缩短时间,仅训练5个epoch)
关键实现说明
核心思路是在普通CNN的卷积层后插入FakeQuantize模块,使前向传播时权重和激活被量化到8bit再反量化回浮点,从而模拟推理时的量化误差。训练时保持浮点梯度更新,验证时关闭伪量化以对比真实精度。代码中通过QATNet与PlainNet两个结构完全相同的网络实现对照实验。
完整代码
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.ao.quantization import FakeQuantize, default_fake_quant
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
# ---------- 基础CNN模块 ----------
class ConvBlock(nn.Module):
def __init__(self, in_ch, out_ch, use_fq=False):
super().__init__()
self.conv = nn.Conv2d(in_ch, out_ch, 3, padding=1)
self.bn = nn.BatchNorm2d(out_ch)
self.use_fq = use_fq
if use_fq:
self.fq_weight = FakeQuantize(default_fake_quant, quant_min=-128, quant_max=127)
self.fq_act = FakeQuantize(default_fake_quant, quant_min=0, quant_max=127)
def forward(self, x):
if self.use_fq:
w = self.fq_weight(self.conv.weight)
x = F.conv2d(x, w, self.conv.bias, padding=1)
else:
x = self.conv(x)
x = self.bn(x)
x = F.relu(x)
if self.use_fq:
x = self.fq_act(x)
return x
# ---------- 两种网络:普通 vs QAT ----------
class PlainNet(nn.Module):
def __init__(self):
super().__init__()
self.block1 = ConvBlock(3, 32, use_fq=False)
self.block2 = ConvBlock(32, 64, use_fq=False)
self.pool = nn.MaxPool2d(2)
self.fc = nn.Linear(64 * 8 * 8, 10)
def forward(self, x):
x = self.pool(self.block1(x))
x = self.pool(self.block2(x))
x = x.view(x.size(0), -1)
return self.fc(x)
class QATNet(nn.Module):
def __init__(self):
super().__init__()
self.block1 = ConvBlock(3, 32, use_fq=True)
self.block2 = ConvBlock(32, 64, use_fq=True)
self.pool = nn.MaxPool2d(2)
self.fc = nn.Linear(64 * 8 * 8, 10)
def forward(self, x):
x = self.pool(self.block1(x))
x = self.pool(self.block2(x))
x = x.view(x.size(0), -1)
return self.fc(x)
# ---------- 训练与验证 ----------
def train_one_epoch(model, loader, optimizer, device):
model.train()
total_loss, correct, total = 0, 0, 0
for data, target in loader:
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
out = model(data)
loss = F.cross_entropy(out, target)
loss.backward()
optimizer.step()
total_loss += loss.item()
pred = out.argmax(dim=1)
correct += pred.eq(target).sum().item()
total += target.size(0)
return total_loss / len(loader), 100.0 * correct / total
@torch.no_grad()
def evaluate(model, loader, device):
model.eval()
correct, total = 0, 0
for data, target in loader:
data, target = data.to(device), target.to(device)
out = model(data)
pred = out.argmax(dim=1)
correct += pred.eq(target).sum().item()
total += target.size(0)
return 100.0 * correct / total
# ---------- 主流程 ----------
def main():
device = torch.device("cpu")
batch_size = 128
epochs = 5
# 数据加载(为演示速度,仅用5000个训练样本)
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616))
])
full_train = datasets.CIFAR10(root="./data", train=True, download=True, transform=transform)
train_subset = torch.utils.data.Subset(full_train, range(5000))
train_loader = DataLoader(train_subset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(datasets.CIFAR10(root="./data", train=False, download=True, transform=transform),
batch_size=batch_size, shuffle=False)
# 训练普通网络
plain_net = PlainNet().to(device)
opt = optim.SGD(plain_net.parameters(), lr=0.01, momentum=0.9)
for ep in range(epochs):
train_loss, train_acc = train_one_epoch(plain_net, train_loader, opt, device)
val_acc = evaluate(plain_net, val_loader, device)
print(f"Plain Epoch {ep+1}: loss={train_loss:.4f} train_acc={train_acc:.2f}% val_acc={val_acc:.2f}%")
# 训练QAT网络
qat_net = QATNet().to(device)
opt = optim.SGD(qat_net.parameters(), lr=0.01, momentum=0.9)
for ep in range(epochs):
train_loss, train_acc = train_one_epoch(qat_net, train_loader, opt, device)
val_acc = evaluate(qat_net, val_loader, device)
print(f"QAT Epoch {ep+1}: loss={train_loss:.4f} train_acc={train_acc:.2f}% val_acc={val_acc:.2f}%")
# 最终对比
plain_acc = evaluate(plain_net, val_loader, device)
qat_acc = evaluate(qat_net, val_loader, device)
print(f"\n最终验证集准确率对比:")
print(f"普通网络: {plain_acc:.2f}%")
print(f"QAT网络: {qat_acc:.2f}%")
print(f"精度差异: {qat_acc - plain_acc:+.2f}%")
if __name__ == "__main__":
main()
运行方式
python qat_cifar10.py
首次运行会自动下载CIFAR-10数据集(约170MB)到./data目录。训练在CPU上约需5~10分钟(取决于机器性能)。
验证方法与判定标准
- 观察输出:程序会打印每个epoch的训练损失、训练准确率以及验证准确率。
- 判定通过条件:程序正常结束且输出最终对比行。QAT网络与普通网络的验证准确率差异通常在±3%以内(由于训练样本较少,波动可能稍大)。若QAT精度不低于普通网络5个百分点以上,说明伪量化模块工作正常,训练过程稳定收敛。
- 附加检查:若将
QATNet中use_fq改为False,其精度应与PlainNet基本一致,可验证网络结构本身无差异。
常见问题
- 下载失败:可手动下载CIFAR-10到
./data/cifar-10-batches-py,或更换网络环境重试。 - 精度差异过大:检查是否误将
FakeQuantize的quant_min/max设置错误,或学习率过高导致不收敛。可尝试降低学习率至0.005。 - 伪量化不生效:确认PyTorch版本≥1.13,且
FakeQuantize在torch.ao.quantization路径下可正常导入。