☰
PyTorch模型参数量与FLOPs统计详解:从原理到工具实践
2026/10/19 10:32:49 网站建设 项目流程

做深度学习久了,你会清楚地感觉到一个尴尬时刻:模型在训练集上准确率明明还行,一到要上线,要么显存爆了,要么单次推理耗时超标。这时候你最需要回答的问题,就是“这个模型到底有多少参数量、跑一次要多少计算量”。今天这篇就专门聊清楚一件事——用PyTorch怎么快速统计模型的参数量(Params)和计算量(FLOPs),以及如何通过现成工具把模型结构看得明明白白。

我会从最笨的print(model)一直讲到thop、ptflops、fvcore这些常用工具,过程中会穿插手动计算公式和踩坑记录。不管你是在做论文复现、模型压缩、边缘部署,还是只是好奇自己搭的CNN每天在忙活什么,这篇文章都应该能帮上忙。

1. 为什么要关心参数量和计算量

1.1 参数量和FLOPs到底是什么

参数量,英文叫Params,指的是模型里所有可学习的参数个数。这些参数包括卷积核的权重、全连接层的矩阵权重、批归一化层的缩放因子和偏置等。可以简单理解成模型“记忆容量”:参数量越大,模型能装下的“知识”就越多,但模型文件也越大,内存和显存占用也越高。

计算量,英文叫FLOPs,是Floating Point Operations的缩写,指模型前向推理一次要做的浮点运算次数。FLOPs更接近模型“计算速度”或“计算复杂度”的度量:如果参数量是模型的“体重”,那FLOPs就是模型全力跑一百米要“使出的劲儿”。注意,这里有个很容易混淆的概念叫MACs(Multiply-Accumulate Operations),指乘加运算次数。一次乘加包含乘法和加法两步,所以严格来说1个MACs约等于2个FLOPs。很多工具报告的数据其实单位是MACs,但都习惯性写成FLOPs,这就埋下了不少争论的伏笔,后面我会专门讲。

1.2 为什么部署场景下这两个数字决定生死

几乎所有需要把模型真正用起来的场景,都绕不开这两个指标。

第一个场景是边缘设备部署。手机、摄像头、嵌入式板卡的内存和算力都很有限,一个模型如果参数量有几千万,光权重文件就上百兆,别说运行,加载都费劲。这时参数量就决定了模型能不能塞进目标设备。

第二个场景是实时性判断。无人车、直播美颜、实时翻译这类低延迟任务,通常要求在几十毫秒内完成一次推理。模型FLOPs过大,即使参数量看着不大,也可能因为计算密度太高而跑不满帧率。

第三个场景是论文和项目汇报。发论文要做模型复杂度对比,不同模型如果不统一计算口径,对比结果会非常奇怪。我在实际处理过的一个“模型性能对比表”里见过别人用不同库算出的参数差异,根源就是有人统计了Buffers(比如BN的running_mean),有人只统计了parameters。这就逼着我们必须搞懂这些工具背后的统计规则。

2. 查看模型结构:从print到summary

2.1 最直接的 print(model) 能看出什么

刚接触PyTorch时,最顺手的工具就是直接print(model)。比如定义了一个简单的卷积模型,打印出来会看到这样的结构:

import torch.nn as nn class SimpleNet(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(3, 16, 3, padding=1) self.bn1 = nn.BatchNorm2d(16) self.conv2 = nn.Conv2d(16, 32, 3, padding=1) self.bn2 = nn.BatchNorm2d(32) self.fc = nn.Linear(32 * 8 * 8, 10) def forward(self, x): x = nn.functional.relu(self.bn1(self.conv1(x))) x = nn.functional.max_pool2d(x, 2) x = nn.functional.relu(self.bn2(self.conv2(x))) x = nn.functional.max_pool2d(x, 2) x = x.reshape(x.size(0), -1) return self.fc(x) model = SimpleNet() print(model)

输出会按模块嵌套关系打印出每一层,但不会告诉你每一层输出特征图的尺寸变化,也不会告诉你每层参数数量,更不会统计总体参数量。这个方式适合快速确认模块嵌套关系,比如自己有没有把卷积和BN顺序搞反,但要写报告就显然不够用了。

2.2 torchinfo.summary:结构、输出形状、参数量一站搞定

真正用得多的看结构工具是torchinfo。它提供了类似Keras里model.summary()的体验,安装很简单:

pip install torchinfo

然后运行:

from torchinfo import summary summary(model, input_size=(1, 3, 32, 32), col_names=["input_size", "output_size", "num_params", "mult_adds"], depth=4)

这样能看到每一层接收的输入尺寸、输出的尺寸、该层的参数量,以及累计的乘加运算量(mult_adds)。我最常用它来排查维数写没写对,因为在Summary输出里,如果某个layers的输出shape和预想的不一样,一眼就能发现是池化kernel算错还是reshape有问题。

这个工具对多数CNN、MLP都有效。但如果你用的是带自定义操作的模型(比如自己写的复杂注意力模块),偶尔会遇到“Unable to infer”这种报错,这时需要在summary里指定device或给模型加一个threshold参数,实话说排查起来有点麻烦,所以模型结构特别复杂时我通常只用它打印“是否有预期输出形状”,而不是完全依赖。

2.3 用named_modules自定义遍历结构

还有一个灵活度更高的方式:自己递归遍历模型的模块树。PyTorch里每个nn.Module都支持named_modules()和named_parameters(),写一个小工具就能输出结构树:

def print_struct(model, show_params=False): total = 0 for name, module in model.named_modules(): param_count = sum(p.numel() for p in module.parameters()) total += param_count if module is not model else 0 if param_count > 0 or isinstance(module, nn.Conv2d): print(f"{name}: {module.__class__.__name__}, params={param_count}") print(f"Total params: {total}")

这种方式的好处是能完全控制输出内容。我在对比不同模型变体时,会额外标记出哪些模块被冻结(parameters().requires_grad=False),方便快速定位可训练参数量。要使用print_struct时注意避免重复计数:named_modules()会遍历所有子模块,所以通常按叶子节点统计,或者只统计model.parameters(),不然容易把父模块的参数量重复累加。

3. 统计参数量:三种最常用方法

3.1 利用parameters()手动求和

最不加修饰、也最可信的参数量统计方法,就是遍历模型参数:

total_params = sum(p.numel() for p in model.parameters()) print(f"Total params: {total_params}") trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad) print(f"Trainable params: {trainable_params}")

这个统计的是所有Parameter类型的张量。对于卷积层,numel()是输出通道数 × 输入通道数 × 卷积核高 × 卷积核宽,再加上偏置参数的个数。比如nn.Conv2d(3, 16, 3, padding=1),如果带偏置,参数量是3 * 16 * 3 * 3 + 16 = 448。遍历出来的结果既直观又不容易出错,唯一的缺点是如果模型较大,手动统计一遍会慢一点,但对常规模型完全没问题。

这里必须说一个容易踩的坑:model.parameters()不包含BN层的running_mean和running_var,因为它们不是Parameter,而是Buffer。也就是说,纯参数统计并不能反映模型在显存中实际占用的全部存储空间。如果你的模型里有BatchNorm,用“参数量”当“模型大小”是不够的,还得额外把buffer加起来。我曾经在恢复某个BN特别多的小型网络时,就因为这个忽略了buffer,导致到板子上加载时内存比预期多出一块。

如果需要统计Buffer占用,可以用model.buffers():

total_buffer = sum(b.numel() for b in model.buffers())

然后模型实际权重+状态一共可能是params + buffers。写报告时建议说清楚“参数量”是多少、“Buffers”有多少,避免其他人复现时对不上。

3.2 torchinfo summary里的参数量,怎么读才准确

前面提到torchinfo.summary会输出Total params、Trainable params 和 Non-trainable params。这些数值其实内部也是通过遍历model.parameters()得到的,所以和前一种手动统计的结果理论上完全一致。但有一个细节要注意:如果你给summary传入的input_size和实际训练尺寸不一样,Summary表里某些层的参数计算可能受“特征图尺寸”影响,尤其是包含AdaptivePool或自定义层的模型。大多数情况下参数个数不会变,但Mult-Adds会变,因此需要确保input_size符合真实数据形状。

用summary还有一个好处,它可以直观展示每一层的参数分布。比如你怀疑某个全连接层参数太多,看表格立刻就能定位。通常fc层因为要把前面的特征图展平成向量,会出现参数量爆炸;这也是论文里把全连接替换成全局平均池化的常见原因。

3.3 thop.profile:一边计算量一边算参数量

thop是老牌工具,安装:

pip install thop

用法比较固定:

import torch from thop import profile input = torch.randn(1, 3, 32, 32) model.eval() macs, params = profile(model, inputs=(input,)) print(f"MACs: {macs / 1e6:.2f} M, Params: {params / 1e6:.2f} M")

这里名字里第一项返回的是macs,不要习惯性叫它FLOPs。如果你想得到传统意义上的FLOPs,大约要乘2。很多网上的博客不求甚解地把这个数字直接标成FLOPs,结果和其他工具不一致,这是最常见的“口径混乱”来源之一。还要注意profile默认会统计模型的全部参数,包括那些requires_grad=False的层;如果需要只统计可训练参数,可以加trainable_only=True参数。

thop对常见卷积、池化、全连接都有预设的钩子,效率比较高。但遇到复杂的自定义算子,比如某些注意力实现里的矩阵乘法,它可能统计得不准确,甚至直接提示没有实现对应的hook。这时候我需要回到“手动算”的老思路,或者用下面要讲的ptflops。

3.4 用ptflops统计,附带逐层报告

如果觉得thop的输出太单薄,可以试ptflops:

pip install ptflops

用法:

from ptflops import get_model_complexity_info macs, params = get_model_complexity_info(model, (3, 32, 32), as_strings=True, print_per_layer_stat=True) print("MACs:", macs) print("Params:", params)

ptflops的好处是能输出逐层的MACs和params,并且支持print_per_layer_stat=True时打印一份类似summary的统计表。它的统计逻辑是“计算图中出现的乘加运算”,对标准卷积层统计得比较准确。如果你正在写模型对比报告,建议固定用ptflops作为标准工具,至少在同一个表格里保持一致。

4. 计算FLOPs:原理与工具

4.1 FLOPs的计算口径为什么让人头大

先别急着跑工具,得先弄明白FLOPs到底怎么算。拿一个卷积层举例。假设输入特征图尺寸是H_in × W_in,卷积核是K × K,输入通道数是C_in,输出通道数是C_out,输出特征图尺寸是H_out × W_out。

在不考虑偏置的情况下,每输出一个特征图上的点,需要做C_in × K × K次乘法,以及对应的C_in × K × K次加法(将各通道乘积累加),所以单点运算量约是2 × C_in × K × K。整个输出特征图上有H_out × W_out × C_out个点,于是卷积层的FLOPs可以粗略写成:

FLOPs ≈ 2 × C_in × K × K × C_out × H_out × W_out

如果加了偏置,每个输出点额外多一次加法,数值上只增加一点点,很多工具会忽略这一步。而大多数库返回的“MACs”只统计了乘法和加法中更核心的乘加对,因此数值上约等于上面公式的一半,也就是:

MACs ≈ C_in × K × K × C_out × H_out × W_out

这就是为什么代码里的macs和理论FLOPs常常差一倍。BN层在推理阶段通常会被融合进去,所以在很多统计工具里不单独计算BN的FLOPs;但在训练阶段,BN每层也需要浮点运算。激活函数(如ReLU)通常不计入FLOPs,因为它的运算量相比卷积和矩阵乘来说很小。明白了这些口径差异,后面看工具输出就不会一脸迷茫了。

4.2 thop.profile快速上手与踩坑

thop.profile的使用我在前面已经写了核心代码。再补充几个实际操作中的注意点。

第一,传入的inputs必须是元组或列表的形式,哪怕只有一个输入也要写(input,),否则容易报错。第二,如果模型里包含Dropout,请务必提前切到model.eval(),虽然profile在统计FLOPs时不一定关心随机掩码,但遵循训练和推理一致的做法更稳妥。第三,对于带if分支的模型,比如不同输入shape走不同分支,profile只会统计你传入的这条输入路径的计算量,这不算bug,只是“单样本路径”快照。

我自己踩过的一个坑是profile在处理自定义torch.nn.functional.conv2d时,因为早期版本不支持某些参数组合,给出的FLOPs会明显少。后来我改用ptflops的get_model_complexity_info,并用official参数指定官方实现,问题就少了。

4.3 ptflops的常用参数详解

get_model_complexity_info的几个关键参数值得记一下:

  • input_res:一般传(C, H, W)。注意这里不是batch维度,默认batch是1。
  • as_strings:为True时直接返回“xxx M”这种字符串,方便人类阅读;为False时返回浮点数。
  • print_per_layer_stat:为True时输出逐层报告,调试时建议开。
  • input_constructor:如果模型需要多个输入,或者需要传入掩码、文本等,就得自己写一个input_constructor函数。

例如有两个输入的模型:

def input_constructor(input_res): image = torch.randn(1, *input_res) mask = torch.ones((1, 1, input_res[1], input_res[2])) return {"x": image, "mask": mask}

这一点让ptflops比thop更灵活。

4.4 用fvcore做更细粒度的统计

Meta开源的fvcore也提供了FLOPs统计工具,而且它是facebookresearch里很多研究的基石。安装:

pip install fvcore

统计代码:

from fvcore.nn import FlopCountAnalysis, parameter_count_table model.eval() input = torch.randn(1, 3, 32, 32) flop_analyzer = FlopCountAnalysis(model, (input,)) print(f"FLOPs (fvcore): {flop_analyzer.total()}") print(parameter_count_table(model))

fvcore的统计结果是“浮点运算次数”,不过它同样会把卷积层的乘加当作两次浮点运算吗?这一点需要看底层实现。我的经验是,不同库对“是否为乘加分别计数”的定义并不相同,所以在写报告时最好标注“本表格使用xx库统计,单位是MACs/FLOPs”,而不是只贴数字。

fvcore还支持给特定算子注册自定义统计函数,比如你有一个手写的稀疏卷积算子,可以手动添加hook覆盖默认统计。这已经是比较进阶的用法了,但当你面对的是VAE、扩散模型这类结构时,会非常有效。

4.5 计算FLOPs前记得固定输入形状和模式

无论是哪个库,FLOPs都和输入尺寸强相关。同一模型处理224×224和处理32×32,理论FLOPs会差几十倍甚至上百倍,因为卷积输出特征图大小直接受输入影响。所以比较两个模型时,务必使用相同的输入分辨率,并且明确写出来。

还要记得模型切换成评估模式model.eval()。虽然Dropout和BN在train模式下也有计算量,但你网络发布后通常用的是eval模式,统计这个更符合真实部署。另外with torch.no_grad()不会影响FLOPs统计,因为工具统计的是计算图结构,并不真正做梯度计算。

5. 完整实例:一个自定义CNN模型的统计实战

5.1 搭建一个样例网络

为了把上面的方法串起来,我搭一个小型CNN,输入输出都简单:

import torch import torch.nn as nn class SimpleNet(nn.Module): def __init__(self): super().__init__() self.conv1 = nn.Conv2d(3, 16, 3, padding=1) # 输入3通道,输出16通道 self.bn1 = nn.BatchNorm2d(16) self.pool1 = nn.MaxPool2d(2) self.conv2 = nn.Conv2d(16, 32, 3, padding=1) # 输入16,输出32 self.bn2 = nn.BatchNorm2d(32) self.pool2 = nn.MaxPool2d(2) self.fc = nn.Linear(32 * 8 * 8, 10) def forward(self, x): x = torch.relu(self.bn1(self.conv1(x))) x = self.pool1(x) x = torch.relu(self.bn2(self.conv2(x))) x = self.pool2(x) x = x.reshape(x.size(0), -1) return self.fc(x)

这个模型的输入是1×3×32×32的图片,经过两次池化后特征图变成32×8×8,然后展平接全连接层。

5.2 用torchinfo和print_struct看结构

先运行:

model = SimpleNet() summary(model, input_size=(1, 3, 32, 32), col_names=["output_size", "num_params"])

输出表格里会显示conv1.output_size=[1,16,32,32],params是448;conv2.output_size=[1,32,16,16],params是4672;两个BN层各自参数32;最后的fc层参数量是32*8*8 * 10 + 10 = 20480 + 10 = 20490。汇总一下,总参数就是这些数字之和,约26000左右。跑一下手动统计:

total_params = sum(p.numel() for p in model.parameters()) print(total_params)

结果没问题。如果想知道“实际模型状态”的总量,还得加上BN里running_mean和running_var的buffer,每个BN层两个buffer,每个buffer长度16或32,共额外2×(10+5)=30个数字?不对,还是以实际工具输出为准。这类细节虽然小,但在统计不到位时会带偏报告。

5.3 用thop和ptflops对比结果

实际跑一遍:

input_tensor = torch.randn(1, 3, 32, 32) macs, params = profile(model, inputs=(input_tensor,)) print(f"thop -> MACs: {macs / 1e6:.2f} M, Params: {params / 1e6:.2f} M")

对SimpleNet来说,主要计算集中在两个卷积层和全连接层。第一层卷积输入输出都是32×32,MACs大约是3×3×3×16×32×32 = 442368。第二层输入16×16,输出16×16,MACs是16×3×3×32×16×16 = 1179648。全连接层MACs是(32*8*8) × 10 = 20480。三个加一起大约164.6万MACs。再乘2就是约329万FLOPs。ptflops给出的MACs数字也应该落在同一量级,因为单位换算后都是“1.6M MACs”左右。

这里能清楚看到“MACs”和“FLOPs”是两个数值,如果写作时把thop的macs直接写成flops,你报告里可能少算一倍。我通常会在代码里一次性打印两个指标,并且标明单位。

5.4 手动验算一遍更安心

验算卷积层参数和MACs,我亲自算一遍给新入门的朋友看。以conv1为例,输入通道C_in=3,输出通道C_out=16,卷积核K=3,输入大小32×32,输出大小32×32(因为padding=1)。

参数量:

3 * 16 * 3 * 3 + 16 = 448

MACs:

3 * 16 * 3 * 3 * 32 * 32 = 442368

FLOPs(含乘和加):

2 * 3 * 16 * 3 * 3 * 32 * 32 = 884736

手动公式算出的数字和工具基本一致。之所以建议动手验算,是因为这能帮你建立对工具输出的直觉感,将来遇到输出特别异常时,能快速判断是模型结构问题还是工具统计问题。

6. 常见问题与排查技巧

6.1 为什么不同人统计的参数量不一样

这个问题我在帮别人对代码时遇到过很多回。根源大致有三个:一是没有区分parameters和buffers;二是使用了torch.save保存整个模型,加载后统计属性不同;三是用了DataParallel包装模型,导致model.module前多了一层,部分统计脚本没写好就会漏一层。正确做法是指定统计某个固定模型的叶子参数,最好写成一个小脚本,任何人拿同一份权重文件跑都得到相同数字。

常见的一个误区是直接sum(x.numel() for x in model.parameters()),这在大多数情况下没错。但遇到共享权重(某些层参数指向相同对象)时,可能重复计数。所以如果你自己实现了嵌入层共享,要对这种结构格外留意。

6.2 FLOPs和MACs单位混用的解决办法

单位不统一是最容易吵架的地方。“M”代表百万(1e6),“G”代表十亿(1e9)。有些库输出是Gitflops,有些是GMacs。比如一个模型的MACs如果是1.2G Macs,FLOPs大概就是2.4G Flops。如果换算错,你写“模型需要2.4G Flops”其实可能是1.2G Flops,差了整整一倍。

我的习惯是在项目根目录放一个model_cost.py,用统一函数统计,输出格式固定为“MACs(百万)”和“FLOPs(百万)”,并带上工具名。这样所有实验记录都能放在同一张表里对比,不会因为今天用thop、明天用ptflops而变得混乱。

6.3 Transformer以及动态分支模型怎么算

Transformer不是传统CNN,它更多是矩阵乘。self-attention的计算量主要由三部分组成:生成Q、K、V的三个线性层,计算注意力权重矩阵的矩阵乘法,以及加权求和。以batch_size=1为例,序列长度是L,隐藏维度是D,那么QKV三个线性层每个大约是L×D×D次MACs,三个就是3LDD。注意力得分矩阵计算是L×L×D次MACs,加权求和也是L×L×D次MACs。所以整体复杂度有一个关于L的二次项,这也是长序列慢的原因。

thop和ptflops通常能识别标准的nn.Linear和torch.matmul,但如果你在代码里自定义了复杂分支,比如用一个if结构控制不同输入长度走不同算子,工具只能统计当前输入路径的FLOPs。我在一个做自回归模型的项目里,就发现因为忽略了循环部分,统计出的FLOPs远低于真实推理。这种情况建议直接把公式写进代码,手动统计关键算子的计算量,再汇总到日志。

6.4 把统计结果自动记录到日志或模型卡

不要每次都从终端复制数字。更稳妥的方式是封装一个函数,把统计结果自动写成JSON,方便和模型文件放一起。参考写法:

def save_model_cost(model, input_res=(3, 32, 32), file_name="model_cost.json"): from ptflops import get_model_complexity_info from torchsummary import summary import json macs_str, params_str = get_model_complexity_info(model, input_res, as_strings=True, print_per_layer_stat=False) result = { "tool": "ptflops", "input_res": list(input_res), "macs": macs_str, "params": params_str } with open(file_name, "w") as f: json.dump(result, f, indent=2)

以后每次训练完保存完权重,就顺便调用一次,这样在复盘模型演进时能一眼看到参数和计算量有没有突然暴增,比翻聊天记录可靠多了。

7. 一点个人实践经验

最后聊点只有动手跑过才明白的经验。我在实际项目里发现,工具统计出来的数字再准确,也不如实机测试来得关键。尤其是算子融合、内存带宽限制、量化推理等优化之后,FLOPs数字好看但实际速度依然不理想,这是很常见的现象。所以我的工作流是:先拿ptflops固定一个口径统计“静态复杂度”,再跑到目标设备上实测一次延迟和吞吐,两者一起看,才不会被单一数字误导。

如果你刚刚上手,建议你现在就挑一个自己最近写的模型,分别用torchinfo、thop、ptflops跑一遍,再把我的示例里第二个卷积层手动算一次MACs,对一下结果。脚踩这一圈之后,你以后看任何模型复杂度报告都会心里有底。

我个人还有一个特别喜欢的小技巧:给model.eval()之后项目里所有模型统一调用一遍统计脚本,把耗时和FLOPs一起打印在实验表格里,既方便内部讨论,也方便向别人解释优化效果。这个方法不需要什么高深技术,但能省掉非常多“你这个数字怎么来的”的追问。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询