news 2026/10/17 8:50:07

【Bug已解决】How to use groups parameter in PyTorch conv2d function 解决方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Bug已解决】How to use groups parameter in PyTorch conv2d function 解决方案

【Bug已解决】How to use groups parameter in PyTorch conv2d function 解决方案

问题标签:PyTorch、Conv2d、groups参数、分组卷积、深度可分离卷积
适用版本:PyTorch 1.0 ~ 2.x
难度等级:⭐⭐⭐(中等)


一、问题描述

在 PyTorch 的nn.Conv2d中,groups参数是一个经常被忽视但非常重要的参数。很多开发者不清楚它的含义和用法,导致:

  1. 无法理解 MobileNet、ResNeXt 等模型中的分组卷积
  2. 设置错误的groups值导致运行时错误
  3. 不知道如何利用groups减少参数量和计算量

核心问题:

nn.Conv2d的groups参数是什么含义?如何正确使用它?


二、错误复现

场景:设置不兼容的 groups 值

import torch import torch.nn as nn # 场景 1: in_channels 不能被 groups 整除 try: conv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, groups=2) # 3 不能被 2 整除 except ValueError as e: print(f"错误 1: {e}") # 场景 2: out_channels 不能被 groups 整除 try: conv = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, groups=3) # 32 不能被 3 整除 except ValueError as e: print(f"错误 2: {e}") # 场景 3: groups > in_channels try: conv = nn.Conv2d(in_channels=4, out_channels=8, kernel_size=3, groups=8) # groups 不能大于 in_channels except ValueError as e: print(f"错误 3: {e}")

典型错误输出

错误 1: in_channels must be divisible by groups (3 vs 2) 错误 2: out_channels must be divisible by groups (32 vs 3) 错误 3: in_channels must be divisible by groups (4 vs 8)

三、根因分析

3.1 groups 参数的含义

groups参数控制分组卷积(Grouped Convolution)的行为:

  • groups=1(默认):标准卷积,所有输入通道与所有输出通道全连接
  • groups=G:将输入通道分成 G 组,每组独立卷积,输出通道也分成 G 组
  • groups=in_channels:深度卷积(Depthwise Convolution),每个输入通道单独卷积

3.2 分组卷积的数学原理

标准卷积(groups=1):

输入: (N, C_in, H, W) 权重: (C_out, C_in, kH, kW) 输出: (N, C_out, H_out, W_out) 每个输出通道 = Σ(所有输入通道 × 对应权重) 参数量: C_out × C_in × kH × kW

分组卷积(groups=G):

输入: (N, C_in, H, W) → 分成 G 组,每组 C_in/G 个通道 权重: (C_out, C_in/G, kH, kW) → 分成 G 组,每组 C_out/G 个输出通道 输出: (N, C_out, H_out, W_out) → G 组输出拼接 每组输出 = Σ(该组输入通道 × 对应权重) 参数量: C_out × (C_in/G) × kH × kW = 标准卷积参数量 / G

3.3 参数量与计算量对比

卷积类型groups参数量计算量说明
标准卷积1C_out × C_in × k²C_out × C_in × k² × H × W全连接
分组卷积GC_out × (C_in/G) × k²C_out × (C_in/G) × k² × H × W参数减少 G 倍
深度卷积C_inC_in × k²C_in × k² × H × W每通道独立
逐点卷积1C_out × C_in × 1²C_out × C_in × H × W1×1 卷积

3.4 约束条件

1. in_channels 必须能被 groups 整除 2. out_channels 必须能被 groups 整除 3. groups ≤ min(in_channels, out_channels)

四、解决方案

方案一:标准卷积(groups=1)

import torch import torch.nn as nn # 标准卷积:所有输入通道与所有输出通道全连接 conv_standard = nn.Conv2d( in_channels=3, out_channels=64, kernel_size=3, padding=1, groups=1 # 默认值,可省略 ) x = torch.randn(1, 3, 224, 224) out = conv_standard(x) print(f"标准卷积: {x.shape} → {out.shape}") print(f"参数量: {sum(p.numel() for p in conv_standard.parameters())}") # 参数量 = 64 × 3 × 3 × 3 + 64(bias) = 1792

方案二:分组卷积(groups=G)

# 分组卷积:将通道分组,减少参数量和计算量 conv_grouped = nn.Conv2d( in_channels=64, out_channels=64, kernel_size=3, padding=1, groups=4 # 分成 4 组,每组 16 个输入通道 → 16 个输出通道 ) x = torch.randn(1, 64, 224, 224) out = conv_grouped(x) print(f"分组卷积 (groups=4): {x.shape} → {out.shape}") print(f"参数量: {sum(p.numel() for p in conv_grouped.parameters())}") # 参数量 = 64 × (64/4) × 3 × 3 + 64(bias) = 9280 # 对比标准卷积: 64 × 64 × 3 × 3 + 64 = 36928,减少了 4 倍

方案三:深度可分离卷积(Depthwise Separable Convolution)

MobileNet 的核心组件,将标准卷积拆分为深度卷积 + 逐点卷积。

import torch import torch.nn as nn class DepthwiseSeparableConv(nn.Module): """深度可分离卷积 = Depthwise + Pointwise""" def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1): super().__init__() # 深度卷积:每个输入通道单独卷积,groups=in_channels self.depthwise = nn.Conv2d( in_channels=in_channels, out_channels=in_channels, # 输出通道 = 输入通道 kernel_size=kernel_size, stride=stride, padding=padding, groups=in_channels, # 每个通道独立卷积 bias=False ) # 逐点卷积:1×1 卷积,通道间信息融合 self.pointwise = nn.Conv2d( in_channels=in_channels, out_channels=out_channels, kernel_size=1, # 1×1 卷积 stride=1, padding=0, groups=1, bias=False ) self.bn1 = nn.BatchNorm2d(in_channels) self.bn2 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU6(inplace=True) def forward(self, x): x = self.depthwise(x) x = self.bn1(x) x = self.relu(x) x = self.pointwise(x) x = self.bn2(x) x = self.relu(x) return x # 对比标准卷积和深度可分离卷积 in_ch, out_ch = 64, 128 x = torch.randn(1, 64, 224, 224) # 标准卷积 standard_conv = nn.Conv2d(in_ch, out_ch, kernel_size=3, padding=1, bias=False) standard_params = sum(p.numel() for p in standard_conv.parameters()) # 深度可分离卷积 ds_conv = DepthwiseSeparableConv(in_ch, out_ch, kernel_size=3, padding=1) ds_params = sum(p.numel() for p in ds_conv.parameters()) print(f"标准卷积参数量: {standard_params:,}") print(f"深度可分离卷积参数量: {ds_params:,}") print(f"参数减少: {100*(1 - ds_params/standard_params):.1f}%") # 前向传播 out_standard = standard_conv(x) out_ds = ds_conv(x) print(f"标准卷积输出: {out_standard.shape}") print(f"深度可分离卷积输出: {out_ds.shape}")

方案四:ResNeXt 分组卷积

ResNeXt 使用分组卷积来增加模型宽度而不显著增加参数量。

import torch import torch.nn as nn class ResNeXtBlock(nn.Module): """ResNeXt 基本块:使用分组卷积""" def __init__(self, in_channels, out_channels, cardinality=32, bottleneck_width=4, stride=1): super().__init__() # cardinality = groups(分组数) # 每组宽度 = bottleneck_width D = cardinality * bottleneck_width # 中间层通道数 self.conv1 = nn.Conv2d(in_channels, D, kernel_size=1, bias=False) self.bn1 = nn.BatchNorm2d(D) # 分组卷积 self.conv2 = nn.Conv2d( D, D, kernel_size=3, stride=stride, padding=1, groups=cardinality, # 关键:分组卷积 bias=False ) self.bn2 = nn.BatchNorm2d(D) self.conv3 = nn.Conv2d(D, out_channels, kernel_size=1, bias=False) self.bn3 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU(inplace=True) # 残差连接 self.shortcut = nn.Sequential() if stride != 1 or in_channels != out_channels: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(out_channels) ) def forward(self, x): residual = self.shortcut(x) out = self.relu(self.bn1(self.conv1(x))) out = self.relu(self.bn2(self.conv2(out))) out = self.bn3(self.conv3(out)) out += residual out = self.relu(out) return out # 测试 block = ResNeXtBlock(in_channels=256, out_channels=256, cardinality=32, bottleneck_width=4) x = torch.randn(1, 256, 56, 56) out = block(x) print(f"ResNeXt Block: {x.shape} → {out.shape}") print(f"参数量: {sum(p.numel() for p in block.parameters()):,}")

五、完整修复代码

import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset # ==================== 分组卷积工具类 ==================== class GroupedConvUtils: """分组卷积工具类""" @staticmethod def validate_groups(in_channels, out_channels, groups): """验证 groups 参数是否合法""" if groups < 1: raise ValueError(f"groups 必须 >= 1,得到 {groups}") if in_channels % groups != 0: raise ValueError( f"in_channels ({in_channels}) 必须能被 groups ({groups}) 整除" ) if out_channels % groups != 0: raise ValueError( f"out_channels ({out_channels}) 必须能被 groups ({groups}) 整除" ) if groups > in_channels: raise ValueError( f"groups ({groups}) 不能大于 in_channels ({in_channels})" ) return True @staticmethod def count_parameters(conv): """计算卷积层参数量""" return sum(p.numel() for p in conv.parameters()) @staticmethod def compare_convolutions(in_channels, out_channels, kernel_size=3, groups_list=None): """对比不同 groups 的参数量""" if groups_list is None: # 自动生成合法的 groups 列表 groups_list = [] for g in range(1, min(in_channels, out_channels) + 1): if in_channels % g == 0 and out_channels % g == 0: groups_list.append(g) print(f"{'groups':>8} {'参数量':>12} {'相比标准':>12} {'每组输入':>10} {'每组输出':>10}") print("-" * 60) base_params = None for g in groups_list: conv = nn.Conv2d(in_channels, out_channels, kernel_size, groups=g, bias=False) params = GroupedConvUtils.count_parameters(conv) if base_params is None: base_params = params ratio = params / base_params per_in = in_channels // g per_out = out_channels // g print(f"{g:>8} {params:>12,} {ratio:>11.2f}x {per_in:>10} {per_out:>10}") # ==================== MobileNet 简化版 ==================== class MobileNetV1Block(nn.Module): """MobileNet V1 基本块:深度可分离卷积""" def __init__(self, in_channels, out_channels, stride=1): super().__init__() self.depthwise = nn.Conv2d( in_channels, in_channels, kernel_size=3, stride=stride, padding=1, groups=in_channels, bias=False ) self.pointwise = nn.Conv2d( in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False ) self.bn1 = nn.BatchNorm2d(in_channels) self.bn2 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU6(inplace=True) def forward(self, x): x = self.relu(self.bn1(self.depthwise(x))) x = self.relu(self.bn2(self.pointwise(x))) return x class SimpleMobileNet(nn.Module): """简化版 MobileNet""" def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( # 初始标准卷积 nn.Conv2d(3, 32, kernel_size=3, stride=2, padding=1, bias=False), nn.BatchNorm2d(32), nn.ReLU6(inplace=True), # 深度可分离卷积块 MobileNetV1Block(32, 64, stride=1), MobileNetV1Block(64, 128, stride=2), MobileNetV1Block(128, 128, stride=1), MobileNetV1Block(128, 256, stride=2), MobileNetV1Block(256, 256, stride=1), MobileNetV1Block(256, 512, stride=2), # 全局平均池化 nn.AdaptiveAvgPool2d((1, 1)), ) self.classifier = nn.Linear(512, num_classes) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) x = self.classifier(x) return x # ==================== 完整训练示例 ==================== def train_with_grouped_convolution(): """使用分组卷积的完整训练示例""" device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') print(f"使用设备: {device}") # 创建模拟数据 X = torch.randn(200, 3, 32, 32) y = torch.randint(0, 10, (200,)) dataset = TensorDataset(X, y) dataloader = DataLoader(dataset, batch_size=16, shuffle=True) # 创建模型 model = SimpleMobileNet(num_classes=10).to(device) # 统计参数 total_params = sum(p.numel() for p in model.parameters()) print(f"MobileNet 总参数量: {total_params:,}") # 对比标准 CNN standard_cnn = nn.Sequential( nn.Conv2d(3, 32, 3, stride=2, padding=1), nn.BatchNorm2d(32), nn.ReLU6(inplace=True), nn.Conv2d(32, 64, 3, stride=1, padding=1), nn.BatchNorm2d(64), nn.ReLU6(inplace=True), nn.Conv2d(64, 128, 3, stride=2, padding=1), nn.BatchNorm2d(128), nn.ReLU6(inplace=True), nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(128, 10), ) standard_params = sum(p.numel() for p in standard_cnn.parameters()) print(f"标准 CNN 参数量: {standard_params:,}") print(f"参数减少: {100*(1 - total_params/standard_params):.1f}%") # 训练 optimizer = optim.Adam(model.parameters(), lr=0.001) criterion = nn.CrossEntropyLoss() for epoch in range(5): model.train() total_loss = 0 correct = 0 total = 0 for inputs, labels in dataloader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() print(f"Epoch [{epoch+1}/5] Loss: {total_loss/len(dataloader):.4f}, " f"Acc: {100.*correct/total:.2f}%") # ==================== 运行 ==================== if __name__ == '__main__': print("=" * 60) print("分组卷积参数对比") print("=" * 60) GroupedConvUtils.compare_convolutions(64, 64, kernel_size=3) print() GroupedConvUtils.compare_convolutions(128, 256, kernel_size=3) print() print("=" * 60) print("训练示例") print("=" * 60) train_with_grouped_convolution()

六、常见陷阱与注意事项

陷阱 1:groups 与通道数不匹配

import torch.nn as nn # ❌ in_channels=10 不能被 groups=3 整除 # conv = nn.Conv2d(10, 30, 3, groups=3) # ValueError! # ✓ 确保 in_channels 和 out_channels 都能被 groups 整除 conv = nn.Conv2d(12, 24, 3, groups=3) # 12/3=4, 24/3=8 ✓

陷阱 2:深度卷积后忘记逐点卷积

# ❌ 仅用深度卷积,通道间无信息交流 depthwise_only = nn.Conv2d(64, 64, 3, groups=64, padding=1) # 输出每个通道只依赖输入对应通道,无法学习通道间关系 # ✓ 深度卷积 + 逐点卷积 depthwise = nn.Conv2d(64, 64, 3, groups=64, padding=1) # 空间特征 pointwise = nn.Conv2d(64, 128, 1) # 通道融合

陷阱 3:混淆 groups 和 dilation

# groups: 分组卷积(减少参数量) conv_grouped = nn.Conv2d(64, 64, 3, groups=8, padding=1) # dilation: 空洞卷积(扩大感受野,不增加参数量) conv_dilated = nn.Conv2d(64, 64, 3, dilation=2, padding=2) # 两者可以同时使用 conv_both = nn.Conv2d(64, 64, 3, groups=8, dilation=2, padding=2)

陷阱 4:分组卷积的权重形状理解错误

import torch.nn as nn # 标准卷积权重形状: (out_channels, in_channels, kH, kW) conv_std = nn.Conv2d(64, 128, 3, groups=1) print(f"标准卷积权重: {conv_std.weight.shape}") # (128, 64, 3, 3) # 分组卷积权重形状: (out_channels, in_channels/groups, kH, kW) conv_grp = nn.Conv2d(64, 128, 3, groups=4) print(f"分组卷积权重: {conv_grp.weight.shape}") # (128, 16, 3, 3) ← in_channels/groups = 64/4 = 16 # 深度卷积权重形状: (in_channels, 1, kH, kW) conv_dw = nn.Conv2d(64, 64, 3, groups=64) print(f"深度卷积权重: {conv_dw.weight.shape}") # (64, 1, 3, 3)

陷阱 5:groups 过大导致性能下降

# groups 过大(接近 in_channels)时,通道间信息交流过少 # 可能导致模型性能下降 # ❌ 对于小通道数使用过大 groups # conv = nn.Conv2d(16, 16, 3, groups=16) # 每组只有 1 个通道,信息交流为零 # ✓ 合理的 groups 设置 # conv = nn.Conv2d(64, 64, 3, groups=4) # 每组 16 个通道

七、总结

groups 值卷积类型参数量典型应用
1标准卷积C_out × C_in × k²ResNet, VGG
G (1 < G < C_in)分组卷积C_out × (C_in/G) × k²ResNeXt
C_in深度卷积C_in × k²MobileNet
C_in + 1×1 conv深度可分离C_in × k² + C_out × C_inMobileNet, EfficientNet

groups 参数速查

groups=1 → 标准卷积(全连接) groups=G → 分组卷积(参数减少 G 倍) groups=in_channels → 深度卷积(每通道独立) 约束: in_channels % groups == 0 out_channels % groups == 0 groups ≤ in_channels

选择建议

场景推荐 groups原因
追求精度1全连接信息交流最充分
追求效率(移动端)in_channels + 1×1深度可分离,参数最少
平衡精度和效率4~32ResNeXt 策略
大通道数通道数的 1/4 ~ 1/8保持每组有足够通道

核心原则

groups参数是控制卷积层参数量和通道间信息交流程度的旋钮。groups=1信息交流最充分但参数最多;groups=in_channels参数最少但无通道间交流。深度可分离卷积(深度卷积 + 逐点卷积)是移动端部署的最佳实践。

通过合理使用groups参数,可以在模型精度和效率之间找到最佳平衡点,这对于在资源受限设备上部署深度学习模型至关重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 8:49:55

FPGA视音频光端机工程:一芯光纤同时跑视频音频的完整实现

简介&#xff1a;基于现场可编程门阵列&#xff08;FPGA&#xff09;的视音频光端机完整工程代码&#xff0c;适合FPGA开发工程师、视频监控系统集成人员与通信设备研发者参考&#xff0c;用于解决长距离多路视音频信号实时传输和系统集成问题。代码经实际部署验证并成功商业化…

作者头像 李华
网站建设 2026/10/17 8:49:18

OSPF区域外路由计算:FA值与Type-4/5 LSA深度解析

1. 从一个让人抓头的现象说起如果你平时维护过企业内网或者数据中心的网络&#xff0c;大概率遇到过这种场景&#xff1a;明明路由表里看到了一条外部路由&#xff0c;下一跳也正确&#xff0c;但数据包就是不通。抓包一看&#xff0c;流量被丢到了错误的出口&#xff0c;或者干…

作者头像 李华
网站建设 2026/10/17 8:49:12

深度学习混合精度训练:FP16/FP32原理、显存节省与避坑指南

如果你自己从零训过超过十亿参数的大模型&#xff0c;大概率经历过这些场景&#xff1a;代码写好了&#xff0c;数据也检查过了&#xff0c;结果训练卡刚跑起来&#xff0c;显存不足的报错迎面砸过来&#xff1b;把模型换成低精度之后显存安静了&#xff0c;loss 却开始抽风&am…

作者头像 李华
网站建设 2026/10/17 8:48:34

diagram-design:可执行系统建模的工程实践

1. 项目概述&#xff1a;这不是画图&#xff0c;是构建可执行的系统逻辑骨架“diagram-design”这个词组乍看像美术课作业&#xff0c;但在我过去十年带过的几十个跨领域项目里&#xff0c;它从来不是PPT里的装饰性插图&#xff0c;而是工程师、产品负责人、甚至硬件调试员每天…

作者头像 李华
网站建设 2026/10/17 8:48:19

Qt实时波形绘制:QCustomPlot高性能实践与数据管道设计

简介&#xff1a;本资源是一套基于Qt5与QCustomPlot开发的轻量级实时波形绘制控件&#xff0c;专为Qt初学者及嵌入式上位机开发人员设计&#xff0c;有效降低实时曲线显示界面的实现门槛。资源包共15个文件&#xff0c;含4个核心cpp源码、3个头文件&#xff08;含qcustomplot.h…

作者头像 李华
网站建设 2026/10/17 8:47:46

2021数学建模C题MIP实战:生鲜供应链调度建模与PuLP求解

简介&#xff1a;本资源为2021年全国大学生数学建模竞赛C题完整解题资料包&#xff0c;面向参赛学生、指导教师及数学建模初学者&#xff0c;聚焦实际问题建模、算法实现与结果验证全流程训练。压缩包共27个文件&#xff0c;含13个MATLAB脚本&#xff08;.m&#xff09;——覆盖…

作者头像 李华