资讯动态

PyTorch模型统计:参数量、FLOPs与结构查看实战指南

发布时间:2026/10/20 13:25:24 来源:尧图企业网站定制
做深度学习模型开发难免会被问到你这个模型参数量Params是多少计算量FLOPs有多大尤其是做模型轻量化、端侧部署或者跟别人对比模型优劣时这两个数字几乎成了硬通货。用PyTorch搭好的模型想快速算出参数量和FLOPs顺便把框架结构完整看一遍其实有不止一种做法但很多人在用的时候总会在统计口径上吃暗亏。比如同样一个网络用不同工具统计FLOPs能差出一倍比如明明不算大的模型算出来参数却多出几百万再比如打印出来的结构只有名字和层类型根本看不清维度变化。这篇把我常用的方法、工具差异以及踩过的坑都整理出来帮你一次性把这些事捋清楚。1. 先搞清楚Params、FLOPs、框架结构分别能回答什么问题1.1 从一次模型评估的真实场景说起之前我做某个图像分类项目模型在开发机上跑得很欢精度也达标结果产品那边说要把模型塞进一块边缘计算板卡里。第一反应就是先问几个问题模型文件多大一次前向计算要跑多少次浮点运算显存 / 内存能不能放得下延迟能压到多少毫秒这几个问题对应的指标恰好就是参数量Params、计算量FLOPs以及模型结构本身的表达能力。如果不把这三个维度分开看很容易陷入一个误区只盯着精度调结构调了半天发现部署根本跑不动。所以第一步不是去敲代码而是搞清楚每个指标的含义和边界。1.2 参数量不代表计算量两者别混为一谈很多人觉得Params越小模型就跑得越快这是错觉。参数量衡量的是模型里有多少个需要学习的数值它决定了模型文件的体积、参数量本身占用的存储空间以及反向传播时梯度需要占用的显存。计算量则是一次前向传播需要完成的浮点运算次数。同样的参数量放在不同结构里的计算负担完全不同。举个最简单的例子一个全连接层和一组同样参数量但高度稀疏的卷积层前者在推理时的计算量往往更大因为它是稠密矩阵乘法每个参数都会被用到后者如果剪枝后很多参数是0可以跳过大量无效运算。打个生活化的比方参数量是仓库里货品的总数FLOPs是把仓库里所有货物按订单搬运一遍要走的步数。仓库大小决定存储成本走多少步决定作业时间但两者并不是线性对应关系——货物摆放得巧妙结构好步数可以远少于货品总数。1.3 除了数量你还得能看得见模型结构Params和FLOPs是聚合后的数字它们会告诉你模型整体有多“胖”、多“重”但不会告诉你瓶颈出在哪。比如某个模型有2000万参数FLOPs也合理可实际部署时就是慢那问题大概率出在某些特定层身上可能是一个输出张量特别大的中间层撑满了带宽可能是连续了很多个通道数暴涨的卷积也可能是某一处的张量维度没有对齐导致拷贝开销巨大。这种情况光靠打印print(model)很难发现因为它只显示每层的类型和shape既没有输出特征图的大小也没有参数量的逐层分布。真正想定位问题需要把模型结构、每层的输出shape、每层的Params和FLOPs做成一张表格从上往下看一遍谁是大头就一目了然。这也是为什么本文要同时讲“计算Params和FLOPs”和“查看框架结构”——它们本质上是同一件事的两面。2. 参数量统计从最笨的办法到顺手工具2.1 手动累加参数的三种写法先说最直观的做法。PyTorch里任何nn.Module都有parameters()方法返回的是Parameter对象集合。每个Parameter都有numel()方法返回张量里的元素个数。把所有参数的元素个数加起来就是总参数量import torch import torch.nn as nn model nn.Sequential( nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, 10), ) total_params sum(p.numel() for p in model.parameters()) print(fTotal Params: {total_params})这段代码最简单但实际使用中我会加一些细节。比如只想统计参与训练的参数量就要过滤掉requires_gradFalse的参数再比如想按层分组统计用named_parameters()更顺手# 只看可训练参数 trainable_params sum( p.numel() for p in model.parameters() if p.requires_grad ) # 按层统计 for name, param in model.named_parameters(): print(f{name}: {param.numel()} parameters)这里有个新手容易踩的坑model.parameters()只统计Parameter对象不统计普通buffer。像BatchNorm里的running_mean和running_var就不是Parameter它们不进parameters()。但它们是模型的一部分占了存储空间。如果你要估算模型文件大小或部署时的内存占用得把buffer也算进去。具体怎么算在后面第4章讲BN时细说。2.2 用 torchinfo 一行输出完整结构加Params手动写代码虽然透明但遇到几十层的大模型就不好用了输出也不好看。我日常用得最多的是torchinfo这个工具。它能在不跑完整前向的情况下基于你给的输入尺寸进行一层层推演输出一张完整表格包含每层的名称、输出shape、参数量。from torchinfo import summary model nn.Sequential( nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, 10), ) summary(model, input_size(1, 128))它的输出会类似这样 Layer (type:depth-idx) Output Shape Param # Sequential [1, 10] -- ├─Linear: 1-1 [1, 64] 8,256 ├─ReLU: 1-2 [1, 64] -- └─Linear: 1-3 [1, 10] 650 Total params: 8,906 Trainable params: 8,906 Non-trainable params: 0 注意output_shape里带有batch维度所以传入的input_size第一维是batch大小一般设成1统计FLOPs时也要保持batch为1这样结果才是单张样本的计算量。为什么不推荐老牌的torchsummary因为torchsummary已经很久没更新对新增的某些层和最新的PyTorch版本兼容性一般而且它用前向hook实现遇到多输入、非张量输入、自定义层时容易炸。torchinfo基于torch.jit对模块进行追踪兼容性更好输出也更清晰。2.3 注意共享权重和未初始化的Parameter还有一种情况很隐蔽共享权重。比如两个不同的子模块指向了同一个nn.Linear实例shared_linear nn.Linear(128, 64) model nn.Sequential(shared_linear, nn.ReLU(), shared_linear)这种结构下model.parameters()只会遍历到同一个Linear的参数一次因为参数对象是同一个引用numel()不会重复计算。这正好符合直觉——模型文件里只需要存一份权重所以参数统计不重复。但有一种假共享要注意你手动构造了两个完全相同的nn.Conv2d它们虽然初始权重一样但各自是独立的Parameter对象统计时会算两份。如果你想把两个层变成真正共享权重只能让它们持有同一个Layer实例或者手动把两个层的权重绑定为同一个对象。另外还有一类“未初始化但占用空间”的Parameter比如某些库为了保持接口统一会注册一些用不到的Parameter。统计时parameters()会把它们算进去所以如果看到Params明显偏大可以排查一下是不是模型定义里有一些废弃的Parameter没删干净。3. 计算量FLOPs统计口径决定结果差异3.1 FLOPs、FLOPs的拼写、MACs先统一概念“FLOPs”这个词经常被写错。全称是FLOating Point Operations浮点运算次数。注意别和FLOPS每秒浮点运算次数搞混——一个是次数一个是速率。在深度学习语境里我们最常讨论的是乘加操作。一个卷积操作本质是先做乘法再累加这种“乘加”的组合叫作一次MACMultiply-Accumulate。如果严格按浮点运算次数算一次MAC包含一次乘法浮点运算和一次加法浮点运算所以大多数论文和框架里FLOPs 2 × MACs但很多开源工具偷懒统计到的其实是MACs却直接标注成FLOPs。这就导致同一个模型用A工具算出来是110M用B工具算出来是220M两边说的都对只是口径不同。我自己的习惯是跟别人汇报时先把口径说清楚比如“模型MACs是110M相当于220M FLOPs”。如果只报FLOPs就统一用“乘加次数 × 2”的标准。这样至少不会产生歧义。3.2 不同开源工具的实现差异与典型结果对比目前社区里常见的计算FLOPs的工具主要有thop、ptflops、torchinfo新版本支持FLOPs计算以及英伟达在部分框架里提供的profiler。它们的底层思路类似注册hook在前向传播时按算子类型统计。但实现细节差异会带来明显不同的结果。我给个大概对比表工具是否默认乘2是否计入bias是否支持多输入对自定制算子友好度thop乘2计入一般需要传tuple中等需手动适配ptflops乘2计入较好中等可写自定义handlertorchinfo计算的是MACs需乘2得到FLOPs计入较好较好提示不同工具的版本也会影响统计结果最好固定用某一种并在论文或项目文档里标注清楚工具和版本。工具给出的结果通常只是一个估算值。真正的浮点运算次数还取决于底层实现是否做了算子融合、是否使用Winograd算法等。比如某些推理引擎会把卷积和BatchNorm融合成一个算子实际运算量会比理论统计少不少。所以FLOPs更适合做相对比较不适合当绝对真理。3.3 手算卷积层FLOPs的底层逻辑光会用工具不够有时候模型里塞了一个不太常见的算子工具统计不了就得自己手算。理解卷积的FLOPs公式非常有用。假设输入是(N, C_in, H, W)卷积核尺寸是K x K输出通道数是C_out输出特征图尺寸是H_out x W_out那么一次卷积操作的计算过程是对输出特征图上的每一个点都要做C_in × K × K次乘法然后把这C_in × K × K个乘积加起来。加法的次数大致等于乘法的次数减1但为了简化一般把乘法加法的总数当作C_in × K × K × 2次浮点运算。整张输出特征图共有C_out × H_out × W_out个点。所以单层卷积的FLOPs近似为FLOPs 2 × C_in × K × K × C_out × H_out × W_out如果卷积层还带bias每个输出点会额外多一次加法但和巨大的乘加总数相比基本可以忽略所以很多工具直接忽略bias的加法次数。不过thop默认会把bias计入FLOPs这就是同一模型不同工具结果有细微差别的一个来源。自己手算一遍再拿工具跑一遍能很快验证工具统计是否合理。我经常这么做尤其是在怀疑自定义算子统计出错的时候。4. 卷积、全连接、BN对统计的影响模型轻量化必须懂的细节4.1 卷积层参数量和FLOPs计算公式卷积层的参数量计算比全连接层简单一点。对于一个Conv2d如果输入通道C_in、输出通道C_out、卷积核K x K且带bias那么参数量 C_in × C_out × K × K C_out注意PyTorch默认卷积是C_out个卷积核每个卷积核的形状是(C_in, K, K)所以乘积正好对上。如果是分组卷积参数量要除以分组数因为每组只处理C_in / groups个输入通道。深度可分离卷积本质上是分组卷积的特例参数量明显减少这也是MobileNet系列轻量化的基础。FLOPs公式前面给过这里要强调输出尺寸H_out和W_out受padding、stride、dilation影响。你用一个工具统计FLOPs时其实已经通过输入尺寸“隐形”指定了这些参数。4.2 BatchNorm层的参数与推理时行为BatchNorm层经常被参数统计搞得一头雾水。它一共有四样东西weight、bias、running_mean、running_var。其中weight和bias是Parameter会出现在parameters()里running_mean和running_var是buffer不会出现在parameters()里但在state_dict()里。BN层的参数量很好算BN参数量 2 × 通道数因为每个通道有一个weight和一个bias。但在推理时BN层真正的行为是做一个逐通道线性变换output (x - running_mean) / sqrt(running_var eps) × weight bias如果模型已经训练好并且把BN融合进卷积层那么BN在推理时理论上不产生独立的FLOPs因为可以把它吸收进卷积的权重和bias里。很多工具统计FLOPs时不统计BN层也是基于这个原理。所以你在统计模型文件大小时通常用state_dict()里的全部张量去算而不是只算parameters()。判断标准取决于用途算可学习参数量用于论文对比那就只看parameters()算部署体积就得把所有buffer都加上。4.3 自定义层和多输入场景怎么处理现实中的模型不会总是干净的nn.Sequential尤其是有多输入、多分支、自定义算子的时候工具很容易失效。拿thop举例子它内置了对常见PyTorch算子的统计规则但如果你写了一个自定义autograd.Function或者用到了某个第三方库的特殊算子hook可能拿不到想要的统计信息最后算出来的FLOPs会偏低。我的做法是分三步先用工具整体跑一遍拿到常规层的统计结果。对自定义算子单独估算FLOPs必要时参考官方实现文档里的计算量。把自定义算子的估算结果加到工具结果上。多输入模型也一样。thop的profile只接受单个inputs但你可以把多个输入张量放在一个tuple里传入input1 torch.randn(1, 3, 224, 224) input2 torch.randn(1, 256) flops, params profile(model, inputs(input1, input2))只要你的模型forward能正常处理这两个输入工具就能跑通。需要注意的是有些模型在forward里会根据输入做动态分支比如判断self.training或某个条件走不同路径这种动态结构用hook统计不一定能覆盖全部分支。最好的办法是保证统计时的前向路径和实际部署路径一致。5. 查看框架结构不止是print(model)5.1 print(model)能看到什么每个PyTorch开发者最早接触的“看结构”方法一定是print(model)。它确实能展示出模块的嵌套关系和每层的配置参数比如卷积的通道数、kernel size、stride全连接的输入输出维度。但print(model)有两个明显短板第一它不显示真实输入流过模型后的输出shape你只能从线性层的in_features和out_features里猜维度变化对卷积层来说很难直接知道特征图在哪个位置被下采样。第二它对复杂分支结构的展示不够直观顶层模块套子模块再套子模块打印出来是密密的一棵树读起来费劲。所以print(model)适合快速确认模块配置不适合做结构分析。5.2 torchinfo与torchsummary的逐层汇总要看清每一层实际处理张量的shape变化还得靠torchinfo。它给出的表格里“Output Shape”一列非常关键能让你一眼看到特征图在哪里从(1, 64, 112, 112)变成(1, 128, 56, 56)卷积层前后通道是怎么变化的。实际项目中我最常用的操作是这样的拿到一个新模型后先构造一个符合真实输入的随机张量然后调用summary把输出截图丢到讨论组里。大家只需要看那一列shape就能快速定位到维度对不上的层或者找到计算瓶颈。一个附加技巧如果模型里某个层的输出shape是[None, ...]或者一堆奇怪的torch.Size([])多半说明你的输入尺寸和模型不匹配或者某些层在追踪时没有正确执行到。这时候可以把input_size改成你实际会用到的分辨率再试一次。5.3 用可视化手段把结构画出来比表格更直观的是模型结构图。常见有两种路线一种是用torchviz的make_dot它会基于反向图生成一个计算图然后配合Graphviz渲染成图片。这个方案对简单的网络很清晰每个张量和每个计算节点都是一个节点能看出数据流方向。但大模型的计算图会很庞大容易糊成一团。另一种是把模型导出成ONNX格式再用通用的可视化工具打开。做法很简单torch.onnx.export( model, torch.randn(1, 3, 224, 224), model.onnx, input_names[input], output_names[output], )导出后的ONNX文件可以用Netron这类工具查看交互体验更好可以点击每一层看到输入输出维度、op类型和参数。这个方案用来跟不懂代码的同事沟通结构是最合适的我经常导出一张ONNX图贴到文档里比贴一堆代码清晰得多。提示多输入模型在导出ONNX时第二个参数要传一个tuple按forward定义的顺序给出所有输入。6. 一个能直接用起来的统计脚本与我的避坑经验6.1 通用统计函数支持单输入、多输入、自定义层说了这么多最后放一套我项目里经常用到的统计模板。它同时输出Params、FLOPs和逐层结构遇到多输入模型也基本能跑import torch from torchinfo import summary from thop import profile, clever_format def describe_model(model, input_shape, batch_size1, devicecpu): model.to(device) model.eval().to(device) # 1) 用 torchinfo 看完整结构和参数量 summary(model, input_size(batch_size, *input_shape), devicedevice) # 2) 用 thop 统计 FLOPs 和 Params dummy_input torch.randn(batch_size, *input_shape).to(device) flops, params profile(model, inputs(dummy_input,), verboseFalse) flops, params clever_format([flops, params], %.3f) print(fFLOPs: {flops}, Params: {params}) # 用法示例 # model MyModel() # describe_model(model, input_shape(3, 224, 224))如果你要统计的模型是多输入把模板改成dummy_input ( torch.randn(batch_size, 3, 224, 224).to(device), torch.randn(batch_size, 10).to(device), ) flops, params profile(model, inputsdummy_input, verboseFalse)注意torchinfo的input_size不支持直接传tuple嵌套的example_input_array所以多输入模型可以用torchinfo的input_data参数传入实际样本或者干脆只用来打印结构FLOPs用thop。6.2 实际使用中的三个坑设备不一致、输入尺寸、BN状态这三个坑我全踩过先说设备不一致。很多模型默认参数在CPU上而dummy_input如果放在GPU上前向传播时模型会先报错“Expected all tensors on cuda”。统计FLOPs之前确认模型和输入张量都在同一个设备否则工具虽然不会崩但可能触发隐式的设备拷贝导致统计结果很奇怪。第二个坑是输入尺寸。FLOPs高度依赖输入分辨率同一个模型用(1, 3, 224, 224)和(1, 3, 512, 512)统计结果可以差四五倍。所以统计前一定要明确线上推理用的分辨率是多少别拿一个奇怪尺寸来“测着玩”。第三个坑是BN状态。前面说过BN在训练和推理时的计算路径不同。统计FLOPs时我强烈建议先model.eval()再进入torch.no_grad()。一方面减少显存占用另一方面避免某些层的训练逻辑被计入统计导致测量结果失真。虽然BN本身的FLOPs影响不大但像Dropout在训练模式下会随机屏蔽一部分神经元某些工具可能把它的额外操作也统计进去或者因为随机状态导致结果不稳定。6.3 统计结果和实际部署时间的差距在哪最后聊一个容易被忽略的问题FLOPs低不等于跑得快。FLOPs只是理论浮点运算次数实际延迟还和很多因素相关。比如模型并行度高不高、算子是否有融合优化、内存带宽是否吃紧、是否使用低精度推理、GPU的利用率能拉到多少。我在一个模型上就遇到过这种情况FLOPs比较高但结构规整、算子类型集中结果在某个推理引擎上跑得反而比一个FLOPs更低但算子零散、分支多的小模型快。所以我现在养成了一个习惯Params和FLOPs用来做方案初筛和模型对比但最终是否采用一定还要在目标设备上实测延迟和功耗。拿Params和FLOPs当绝对评价标准很容易被数字误导。这套统计流程我基本每个模型都会跑一遍。从手动累加参数到用torchinfo看结构再到用thop算FLOPs最后结合ONNX可视化给上下游同事展示已经成了固定动作。希望这次整理能让你少踩几个我踩过的坑尤其是FLOPs口径和BN参数那两块弄清楚之后至少不会被数字来回折腾。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑