尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

手把手推导CNN与ViT的FLOPs与参数量:从原理到代码实战

手把手推导CNN与ViT的FLOPs与参数量:从原理到代码实战 1. 项目概述为什么我们需要亲手计算FLOPs和参数量在模型设计、论文复现或者面试准备时我们经常会听到两个词FLOPs和参数量。前者衡量模型的计算复杂度后者衡量模型的空间占用和优化潜力。很多朋友习惯直接用现成的工具库比如thop、ptflops跑一下得到一个数字就完事了。但说实话如果你不理解这个数字是怎么来的它对你的价值就大打折扣。当工具报错、结果异常或者你需要为一个全新的模块估算复杂度时就会束手无策。这个内容就是带你回到最根本的数学和代码层面亲手推导和计算卷积神经网络CNN和视觉TransformerViT这两大主流视觉架构的FLOPs与参数量。我们不止看公式更会结合具体的PyTorch代码从张量的维度出发一步步拆解。你会发现一旦掌握了核心原理无论是面对ResNet、EfficientNet这样的CNN变体还是Swin Transformer、DeiT等ViT改进型你都能快速分析其计算开销从而在模型选型、轻量化设计时做出更明智的决策。2. 核心概念与计算基础拆解在动手计算之前我们必须统一对几个核心概念的理解。这就像木匠干活前要先认识尺子和锯子一样概念清晰了后面的计算才不会出错。2.1 参数量模型究竟记住了多少东西参数量顾名思义就是模型中所有需要学习的参数的总数。在深度学习中这些参数通常是权重矩阵和偏置向量中的每一个标量值。计算原则对于模型中任何一个具有可学习参数的层其参数量等于该层所有参数张量中元素的总和。更具体地说全连接层Linear Layer如果输入特征维度是in_features输出是out_features并且使用偏置biasTrue那么参数量 in_features * out_features out_features。不加偏置则减去后面的out_features。卷积层Conv Layer这是重点。一个卷积核的参数量由其尺寸、输入通道数和输出通道数决定。假设卷积核尺寸为(kernel_h, kernel_w)输入通道数为C_in输出通道数为C_out并且使用偏置。那么一个卷积核的参数量是kernel_h * kernel_w * C_in。因为有C_out个这样的卷积核所以总参数量 kernel_h * kernel_w * C_in * C_out C_out加上偏置。如果不加偏置同样去掉C_out。注意参数量计算与输入图片的大小H, W完全无关。它只由网络结构本身决定。所以参数量是一个静态的、描述模型容量的指标。2.2 FLOPs完成一次前向传播需要多少“力气”FLOPs即浮点运算次数是衡量模型计算复杂度的关键指标。它特指完成一次前向传播推理所需要进行浮点数乘加运算的次数。通常一次乘加运算Multiply-ACCumulate MAC被视为一次浮点运算。但更严谨的学术文献中有时会将一次乘法和一次加法计为两次单独的FLOP。为了与主流工具如thop保持一致我们这里采用“一次乘加算作一次FLOP”的约定这在业界通常称为“MACs”。当你看到论文中的FLOPs时需要留意其具体定义。计算原则FLOPs的计算与具体的操作和输入数据尺寸强相关。全连接层对于输入向量(in_features,)到输出向量(out_features,)的运算可以看作是一个矩阵乘法(out_features, in_features) * (in_features,)。其FLOPs为in_features * out_features次乘法每个连接一次和同样数量的加法累加如果按一次乘加为1 FLOPs计就是in_features * out_features。如果考虑偏置加法还需要加上out_features次加法但通常偏置加法的计算量很小在估算时有时被忽略。卷积层这是计算的大头。其FLOPs由卷积操作在每个空间位置、每个输出通道上的计算量累加而来。一个广泛使用的公式是FLOPs H_out * W_out * C_in * kernel_h * kernel_w * C_out其中(H_out, W_out)是输出特征图的空间尺寸。这个公式怎么来的我们可以这样理解对于输出特征图上的每一个点共H_out * W_out * C_out个都需要与一个(kernel_h, kernel_w, C_in)的输入块做内积即逐元素相乘后求和这个内积操作包含了kernel_h * kernel_w * C_in次乘法和同样次数的加法减一因此每个输出点对应kernel_h * kernel_w * C_in次乘加运算。重要心得FLOPs严重依赖于输入分辨率。这也是为什么比较模型复杂度时必须指明输入图像尺寸如224x224。同样一个网络处理320x320的图片比处理224x224的图片FLOPs要高出一倍多。2.3 参数量 vs. FLOPs vs. 实际推理速度这是一个常见的误区参数量小的模型一定跑得快吗不一定。参数量主要影响模型大小存储占用和优化器状态内存训练时与计算速度没有直接关系。一个参数量大但计算模式规整如大矩阵乘法的模型可能比参数量小但计算模式复杂如大量分支、小卷积的模型在GPU上跑得更快。FLOPs是理论计算量是速度的一个主要影响因素但非唯一因素。内存访问成本Memory Access Cost, MAC常常是瓶颈。例如分组卷积Group Convolution或深度可分离卷积Depthwise Separable Conv通过降低FLOPs来提升效率但其内存访问模式可能不如标准卷积高效在特定硬件上可能无法达到FLOPs降低比例的加速效果。实际推理速度由FLOPs、MAC、硬件计算单元利用率、算子优化程度、框架开销等共同决定。最可靠的方式仍然是在目标硬件和部署环境下进行实际测速。我们的计算是为了在理论层面建立直觉和进行初步筛选避免设计出明显计算冗余的结构。3. CNN核心层FLOPs与参数量计算实战让我们以最经典的二维卷积层为例用代码和公式彻底把它算明白。假设我们使用PyTorch框架。3.1 标准卷积层计算详解我们定义一个卷积层nn.Conv2d(in_channels3, out_channels64, kernel_size3, stride1, padding1)。输入一张(224, 224)的RGB图片。第一步确定输出尺寸公式H_out floor((H_in 2*padding - kernel_size) / stride 1)代入H_out W_out floor((224 2*1 - 3) / 1 1) floor(224 /1) 224。所以输出特征图形状为(64, 224, 224)。第二步计算参数量权重形状(out_channels, in_channels, kernel_h, kernel_w) (64, 3, 3, 3)参数量含偏置64 * 3 * 3 * 3 64 1728 64 1792可以看到参数量与输入图片尺寸无关。第三步计算FLOPs使用公式FLOPs H_out * W_out * C_in * kernel_h * kernel_w * C_out代入FLOPs 224 * 224 * 3 * 3 * 3 * 64让我们分步计算每个输出点的计算量3 * 3 * 3 27次乘加。输出点总数224 * 224 * 64 3,211,264个。总FLOPs27 * 3,211,264 86,704,128。换算成常用单位约86.7 MFLOPs百万次浮点运算。import torch import torch.nn as nn # 定义卷积层和输入 conv nn.Conv2d(in_channels3, out_channels64, kernel_size3, stride1, padding1) input_tensor torch.randn(1, 3, 224, 224) # (batch, channel, height, width) # 手动计算参数量 def manual_params(conv_layer): # 权重参数 weight_params conv_layer.out_channels * conv_layer.in_channels * conv_layer.kernel_size[0] * conv_layer.kernel_size[1] # 偏置参数 bias_params conv_layer.out_channels if conv_layer.bias is not None else 0 return weight_params bias_params print(f手动计算参数量{manual_params(conv)}) print(fPyTorch参数量{sum(p.numel() for p in conv.parameters())}) # 应一致 # 手动计算FLOPs (一次前向) def manual_flops_conv2d(conv_layer, input_shape): batch, C_in, H_in, W_in input_shape _, C_out, H_out, W_out torch.randn(batch, conv_layer.out_channels, H_in, H_in).shape # 简单估算输出尺寸严谨起见应用公式计算 # 严谨输出尺寸计算 H_out (H_in 2*conv_layer.padding[0] - conv_layer.dilation[0]*(conv_layer.kernel_size[0]-1) -1)//conv_layer.stride[0] 1 W_out (W_in 2*conv_layer.padding[1] - conv_layer.dilation[1]*(conv_layer.kernel_size[1]-1) -1)//conv_layer.stride[1] 1 flops_per_output C_in * conv_layer.kernel_size[0] * conv_layer.kernel_size[1] total_flops H_out * W_out * C_out * flops_per_output # 如果考虑偏置加法加上 H_out * W_out * C_out if conv_layer.bias is not None: total_flops H_out * W_out * C_out return total_flops print(f手动估算FLOPs{manual_flops_conv2d(conv, input_tensor.shape)})3.2 深度可分离卷积计算与对比深度可分离卷积是MobileNet等轻量级网络的核心它显著降低了FLOPs和参数量。它分为两步深度卷积Depthwise Conv每个输入通道单独使用一个二维卷积核进行滤波。groupsin_channels。逐点卷积Pointwise Conv使用1x1卷积来组合深度卷积的输出通道。我们对比一下标准卷积和深度可分离卷积。假设输入为(C_in, H, W)输出为(C_out, H, W)卷积核大小K且stride1, padding1保持尺寸不变。标准卷积参数量C_in * K * K * C_out C_outFLOPsH * W * C_in * K * K * C_out深度可分离卷积深度卷积参数量C_in * K * K每个输入通道一个KxK核FLOPsH * W * C_in * K * K逐点卷积1x1 Conv参数量C_in * 1 * 1 * C_out C_out C_in * C_out C_outFLOPsH * W * C_in * C_out总计参数量C_in * K * K C_in * C_out C_outFLOPsH * W * C_in * (K * K C_out)对比与简化 将总FLOPs与标准卷积的FLOPs相除得到比例约为1/C_out 1/(K*K)。当C_out较大如256且K3时这个比例大约为1/256 1/9 ≈ 0.12FLOPs降低为原来的约1/8参数量也有类似比例的下降。这就是深度可分离卷积威力巨大的原因。3.3 池化层、激活层与归一化层的计算考量这是一个容易混淆的点像ReLU、MaxPooling、BatchNorm这样的层它们有FLOPs吗它们影响参数量吗池化层MaxPool/AvgPool通常不计入主要的FLOPs。虽然它们有比较或加法操作但相对于卷积和全连接层的密集计算其计算量可以忽略不计。它们没有可学习的参数所以参数量为0。激活层ReLU, Sigmoid, GELU等包含简单的元素级操作计算量很小。在粗略估算时通常忽略其FLOPs。它们也没有参数。归一化层BatchNorm, LayerNorm训练阶段BatchNorm需要计算批次的均值和方差有额外的计算量。但在推理阶段其参数缩放因子gamma和偏移因子beta以及运行统计的均值和方差是固定的它执行的是一个固定的仿射变换y gamma * (x - running_mean) / sqrt(running_var eps) beta。这个变换包含减、除、乘、加计算量不可忽略尤其是通道数多的时候。FLOPs对于形状为(C, H, W)的特征图BatchNorm在推理时的FLOPs约为C * H * W * 4四次元素运算。在严谨的模型分析中需要计入。参数量BatchNorm有2*C个可学习参数gamma和beta以及2*C个运行时统计参数均值和方差但后者不算在通常的“可训练参数量”中。所以其可训练参数量是2*C。实操心得在使用thop等工具时它们通常会计入BatchNorm的FLOPs。如果你手动计算一个包含BN的卷积块记得加上这部分。一个常见的做法是在模型部署时将BN层与前一层的卷积或全连接层进行“融合”将BN的参数吸收到卷积的权重和偏置中这样既能保持数值等效又能消除BN层的计算开销提升推理速度。4. ViT核心模块FLOPs与参数量计算实战视觉Transformer的结构与CNN截然不同其计算核心从卷积变成了自注意力Self-Attention和前馈网络FFN。理解它的计算开销是分析ViT及其变体如Swin Transformer的关键。4.1 Patch Embedding层计算ViT的第一步是将图像分割成固定大小的块Patch并将每个块展平、投影到一个嵌入向量。这通常通过一个卷积层来实现。假设输入图像(3, 224, 224)Patch大小P16嵌入维度D768计算过程图像被分割成(224/16) * (224/16) 14 * 14 196个Patch。每个Patch是(3, 16, 16)的张量展平后长度为3*16*16768。Patch Embedding层实质是一个输入为768输出为768的线性投影如果展平后长度等于D或者更常见的是用一个卷积核大小步长P的卷积层nn.Conv2d(3, D, kernel_sizeP, strideP)。对于卷积实现conv nn.Conv2d(3, 768, kernel_size16, stride16)参数量3 * 16 * 16 * 768 768 589,824 768 590,592FLOPs输出特征图形状为(768, 14, 14)。FLOPs 14 * 14 * 3 * 16 * 16 * 768 14*14 * 768 * 768。注意因为stridekernel_size每个输出位置对应输入的一个不重叠的块计算量与线性层等价。4.2 自注意力机制计算分解自注意力是Transformer的计算核心也是复杂度最高的部分。对于一个包含N个令牌Token即Patch每个令牌维度为D的序列计算多头自注意力MSA。单头自注意力计算生成Q, K, V通过三个线性层将输入X (N, D)映射到Q, K, V (N, D_k)通常D_k D / num_heads。参数量3 * D * D_k忽略偏置FLOPs3 * N * D * D_k矩阵乘法计算注意力分数A softmax(Q * K^T / sqrt(D_k))形状为(N, N)。Q * K^T的FLOPsN * N * D_k每个元素是D_k维向量的内积。Softmax等操作计算量相对较小可粗略估算或忽略。加权求和Output A * V形状为(N, D_k)。FLOPsN * N * D_k每个输出元素是N个V向量的加权和。单头总FLOPs≈3 * N * D * D_k 2 * N * N * D_k。 对于h个头因为D h * D_k所以多头总FLOPs≈3 * N * D * D_k * h 2 * N * N * D_k * h 3 * N * D^2 2 * N^2 * D。关键洞察公式2 * N^2 * D揭示了自注意力的计算复杂度与序列长度N的平方成正比。这就是原生ViT处理高分辨率图像N很大时计算量爆炸的原因。Swin Transformer提出的窗口注意力Window Attention和移位窗口Shifted Window就是为了将全局的N限制在一个窗口大小M内使复杂度从O(N^2)降为O(M^2)而M是固定值如7。4.3 前馈网络与整体ViT块计算一个标准的Transformer编码器块包含层归一化LN1、多头自注意力MSA、残差连接、层归一化LN2、前馈网络FFN、残差连接。前馈网络通常是一个两层MLP中间有一个扩展因子。公式FFN(x) Linear_up(GeLU(Linear(x)))。假设输入维度D中间层维度扩展为D_ff 4*D常见设置。第一个线性层(N, D) - (N, 4D) FLOPs ≈N * D * 4D 4N D^2。第二个线性层(N, 4D) - (N, D) FLOPs ≈N * 4D * D 4N D^2。FFN总FLOPs≈8N D^2。一个ViT块的FLOPs估算MSA部分3N D^2 2N^2 DFFN部分8N D^2总计忽略LN和残差加法的少量计算≈ 11N D^2 2N^2 D参数量估算MSA中的3个QKV投影矩阵3 * D * DMSA后的输出投影矩阵D * DFFN的第一个线性层D * 4DFFN的第二个线性层4D * D总计忽略偏置和LN参数≈ 12 D^2以一个ViT-Base为例D768, N197 (196 patches 1 cls token), heads12单块参数量 ≈12 * 768^2 ≈ 7.08M。单块FLOPs对于输入197个token11N D^2 11 * 197 * 589,824 ≈ 1.28e92N^2 D 2 * 38,809 * 768 ≈ 59.6e6总计 ≈1.34 GFLOPs。一个包含12个这样的块的ViT-Base仅编码器的FLOPs就高达16 GFLOPs以上这解释了为什么ViT需要更大的算力。5. 常见问题、工具使用与避坑指南理论懂了一上手还是容易出错。这部分记录了我踩过的坑和总结的技巧。5.1 手动计算与工具结果对不上排查清单当你用自己写的函数计算出的FLOPs和thop、ptflops等工具结果不一致时可以按以下清单排查FLOPs定义是否统一这是最大的坑确认工具使用的是“乘加算一次”还是“乘和加分开算两次”。thop默认使用“乘加算一次”即MACs。有些论文报告的是“乘加分开”的FLOPs数值会是前者的两倍。务必查看工具的文档。输入尺寸是否正确确保你手动计算时使用的输入张量形状尤其是batch_size与传递给工具的形状完全一致。FLOPs与batch_size是线性关系。是否包含了所有层你的手动计算是否漏掉了某些层例如BatchNorm/LayerNorm如前所述推理时有计算量。偏置加法你的计算是否包含了卷积和线性层偏置的加法这部分通常占比很小但严格来说应该计入。激活函数thop通常不计入ReLU等简单激活的FLOPs但复杂的如GELU、Swish可能会被估算。跳跃连接中的加法残差连接中的张量加法操作。卷积的细节参数你的计算是否考虑了dilation空洞卷积、groups分组卷积对于空洞卷积有效核尺寸会变大。对于分组卷积计算量会除以groups。ViT中的Class Token和位置编码Class Token是一个额外的可学习向量会增加序列长度N从num_patches变为num_patches1。位置编码如果是可学习的也会增加参数量。这些在你的计算中考虑了吗5.2 使用thop.profile的正确姿势与陷阱thop是一个常用的PyTorch模型复杂度分析工具。基本用法很简单但有些细节需要注意。import torch import torchvision.models as models from thop import profile, clever_format model models.resnet50() input torch.randn(1, 3, 224, 224) flops, params profile(model, inputs(input, )) flops, params clever_format([flops, params], %.3f) print(fFLOPs: {flops}, Params: {params})常见陷阱模型模式确保模型在.eval()模式下进行profile。某些层如Dropout、BatchNorm在训练和评估模式下的计算图可能不同。自定义层/操作thop通过注册钩子来统计已知模块如nn.Conv2d,nn.Linear的计算量。如果你有自定义的层例如一个手工实现的注意力模块thop可能无法识别导致计算结果偏低。你需要为自定义层手动注册FLOPs计算函数。def my_attention_flops_counter_hook(module, input, output): # 根据你的实现计算FLOPs module.__flops__ calculated_flops my_custom_layer.register_forward_hook(my_attention_flops_counter_hook) # 然后在profile前还需要处理一下比较麻烦。更推荐使用fvcore等更灵活的工具。动态控制流如果模型的前向传播包含条件判断if-else或循环thop可能只统计实际执行路径的计算量但这依赖于输入。对于动态网络需要谨慎分析。内存访问成本未计入再次强调thop只统计理论浮点运算次数不反映内存带宽带来的实际延迟。5.3 模型轻量化设计中的计算量权衡思路当你需要设计一个高效的模型时仅仅看FLOPs和参数量是不够的但它们是重要的起点。以下是一些权衡思路FLOPs vs. MAC内存访问成本1x1卷积是“便宜”的它的FLOPs低H*W*C_in*C_out并且具有良好的内存访问局部性在GPU上非常高效。分组卷积的权衡虽然大幅降低FLOPs和参数量但可能破坏数据连续性增加MAC在某些硬件上加速比不如理论。激活函数与数据精度使用ReLU6代替ReLU使用半精度FP16或整型INT8量化这能大幅降低内存占用和带宽压力从而提升实际速度但FLOPs统计上看不出来。ViT中的计算优化序列长度是敌人一切降低有效序列长度N的方法都能平方级地降低注意力计算量。这就是窗口注意力Swin、池化注意力Pooling、下采样Downsampling等技术的核心动机。线性注意力近似研究线性复杂度的注意力变体如Linformer, Performer用低秩分解或核函数近似原始注意力矩阵将复杂度从O(N^2)降为O(N)。蒸馏与剪枝用一个大模型教师指导一个小模型学生训练或者直接剪掉模型中不重要的权重/注意力头。实践建议在设计初期用FLOPs和参数量进行快速筛选和迭代。但在最终决定前一定要在目标硬件手机CPU、GPU、NPU上做端到端的基准测试。使用TensorRT、OpenVINO、MNN等部署框架进行优化和测速因为框架优化和算子融合可能彻底改变性能表现。计算FLOPs和参数量不是目的而是手段。目的是为了建立对模型复杂度的直觉在模型设计、论文阅读和性能调优时能快速定位计算瓶颈做出更优的工程决策。亲手推导一遍公式再用代码验证一遍这种理解远比单纯记住几个数字要深刻得多。下次当你看到一个新的SOTA模型时试着先估算一下它的计算开销你会发现自己对模型的理解又上了一个台阶。
返回列表