• 神经网络模型的参数量和计算量


    其实模型的参数量好算,但浮点运算数并不好确定,我们一般也就根据参数量直接估计计算量了。但是像卷积之类的运算,它的参数量比较小,但是运算量非常大,它是一种计算密集型的操作。反观全连接结构,它的参数量非常多,但运算量并没有显得那么大。

    FLOPs(Floating-point Operations):浮点运算次数,理解为计算量,可以用来衡量算法的复杂度。一个乘法或一个加法都是一个FLOPs

    FLOPS(Floating-point Operations Per Second):每秒浮点运算次数,理解为计算速度,是一个衡量硬件性能的指标。

    MACCs(multiply-accumulate operations):乘-加操作次数,MACCs 大约是 FLOPs 的一半。将$w*x+b$视为一个乘法累加,也称为1 个 MACC。

    MAC(Memory Access Cost):内存访问成本

    Params:是指模型训练中需要训练的参数总数

    注意了:下面的阐述如果没有特别说明,默认都是batch为1。

    全连接层

      全连接 权重$W$矩阵为$(C_{in}, C_{out})$,输入$(B, F, C_{in})$,输出$(B, F, C_{out})$。 全连接层执行的计算为:$y=matmul(x,W)+b$

    Params=CinCout+Cout

    FLOPs=FCinCout+Cout

    MACCs=FCinCout

    (目前全连接层已经逐渐被 Global Average Pooling 层取代了) 注意,全连接层的权重参数量(内存占用)远远大于卷积层。

    一维卷积层

      一维卷积 kernel大小为$K$,输入通道$C_{in}$,输出通道$C_{out}$。输入$(B, C_{in}, F_{in})$,输出$(B, C_{out}, F_{out})$。

    Params=KCinCout+Cout(bias)

    输出特征图有$(F_{out}, C_{out})$个像素

    每个像素对应一个立体卷积核$k∗C_{in}$在输入特征图上做立体卷积卷积出来的;

    FLOPs=CinKFoutCout+Cout(bias)

    二维卷积层

      卷积层卷积核(Kernel)的高和宽:$K[0]$和$K[1]$ 。输入为$(N,C_{in},H_{in},W_{in})$。输出为 $(N,C_{out},H_{out},W_{out})$,其中$H_{\text{out}}$和$W_{\text{out}}$ 分别为特征图的高度和宽度。

    Params=K[0]K[1]CinCout+Cout(bias)

    • 输出特征图中有$H_{out}*W_{out}*C_{out}$个像素;
    • 每个像素对应一个立体卷积核$k[0]*k[1]*C_{in}$在输入特征图上做立体卷积卷积出来的;

    MACCs=(CinK[0]K[1])HoutWoutCout(bias)

    其中输出特征图尺寸$H_{out},W_{out}$本身又由输入矩阵$H_{in},W_{in}$,卷积尺寸K,Padding,Stride这是个参数决定:

    Hout =Hin+2× padding [0] dilation [0]×( kernel_size [0]1)1 stride [0]+1

    Wout =Win+2× padding [1] dilation [1]×( kernel_size [1]1)1 stride [1]+1

    那我们现在来计算一下参数量,如果了解卷积的原理,应该也不难算出它的参数量(可能有人会说卷积原理怎么理解,这里推荐一篇写得通俗易懂的文章:CNN基础知识——卷积(Convolution)、填充(Padding)、步长(Stride) - 知乎

    分组卷积

    对于尺寸为$H_1×W_1×C_1$的输入矩阵,当标准卷积核的大小为$K[0], K[1], C_{in}$ ,共有$C_{out}$个卷积核时,标准卷积会对完整的输入数据进行运算,最终得到的输出矩阵尺寸为$(H_{out}, W_{out}, C_{out})$。这里我们假设卷积运算前后的特征图尺寸保持不变,则上述过程可以展示为下图。

    图* 标准卷积示意图

      分组卷积中,通过指定组数$g$将输入数据分成$g$组。需要注意的是,这里的分组指的是在深度上进行分组,输入的宽和高保持不变,即将每$C_{in}/g$个通道分为一组。因为输入数据发生了改变,相应的卷积核也需要进行对应的变化,即每个卷积核的输入通道数也就变为了$C_{in}/g$,而卷积核的大小是不需要改变的。同时,每组的卷积核个数也由原来的$C_{out}$变为$C_{out}/g$。对于每个组内的卷积运算,同样采用标准卷积运算的计算方式,这样就可以得到$g$组尺寸为$H_{out}, W_{out},C_{out}/g$的输出矩阵,最终将这$g$组输出矩阵进行拼接就可以得到最终的结果。这样拼接完成后,最终的输出尺寸就可以保持不变,仍然是$H_{out}, W_{out}, C_{out}$。分组卷积的运算过程如下图所示。

    图 分组卷积示意图

    使用分组卷积后,参数和计算量则变为:

    Params=K[0]K[1]CingCoutgg=K[0]K[1]CinCout1g

    $$MACCs=(\frac{C_{in}}{g}*K[0]*K[1])*H_{out}·W_{out}*\frac{C_{out}}{g}*g\\

    =(C_{in}*K[0]·K[1])*H_{out}·W_{out}*C_{out}*\frac{1}{g}$$

    深度可分离卷积层

      深度可分离卷积是将常规卷积因式分解为两个较小的运算,它们在一起占用的内存更少(权重更少),并且速度更快。深度可分离卷积中,

    1. 先进行 深度卷积,与常规卷积相似,不同之处在于将输入通道分groups组,groups等于输入通道数。深度卷积输入通道数和输出通道数相等
    2. 在进行 逐点卷积,也就是1x1卷积
    1. class DepthwiseSeparableConv(nn.Module):
    2. def __init__(self, in_channels, out_channels, kernel_size,
    3. stride, padding, dilation, bias):
    4. super(DepthwiseSeparableConv, self).__init__()
    5. # Use `groups` option to implement depthwise convolution
    6. depthwise_conv = nn.Conv1d(in_channels, in_channels, kernel_size,
    7. stride=stride, padding=padding,
    8. dilation=dilation, groups=in_channels,
    9. bias=bias)
    10. pointwise_conv = nn.Conv1d(in_channels, out_channels, 1, bias=bias)
    11. self.net = nn.Sequential(depthwise_conv, pointwise_conv)
    12. def forward(self, x):
    13. return self.net(x)

    标准卷积为:

    深度卷积,将输入分成$C_{in}$组,$C_{in}=C_{out}$

    逐点卷积

    所以深度可分离卷积的参数量和计算量为:

    Params=K[0]K[1]CinCout1Cin+11CinCout=K[0]K[1]Cout+CinCout

    MACC=MACCs=(Cin K[0]K[1])Hout Wout Cout 1Cin +(Cin 11)Hout Wout Cout =K[0]K[1]Hout Wout Cout +Cin Hout Wout Cout 

    LSTM层

    关于LSTM的原理可以参考这一篇文章:循环神经网络(RNN)及衍生LSTM、GRU详解,如果想要算清楚,请务必要看,由于相似内容太多我就不搬移过来了

    Params=Cin(hidden_size4)+hidden_sizehidden_size4

    一个time_step的LSTM计算量为:

    MACCs=1Cinhidden_size4+hidden_sizehidden_size4+hidden_sizehidden_size

    第三库计算工具

    模型参数数量(params):指模型含有多少参数,直接决定模型的大小,也影响推断时对内存的占用量,单位通常为 M,GPU 端通常参数用 float32 表示,所以模型大小是参数数量的 4 倍。

    以AlexNet模型为例

    1. import torch
    2. import torch.nn as nn
    3. import torchvision
    4. class AlexNet(nn.Module):
    5. def __init__(self,num_classes=1000):
    6. super(AlexNet,self).__init__()
    7. self.feature_extraction = nn.Sequential(
    8. nn.Conv2d(in_channels=3,out_channels=96,kernel_size=11,stride=4,padding=2,bias=False),
    9. nn.ReLU(inplace=True),
    10. nn.MaxPool2d(kernel_size=3,stride=2,padding=0),
    11. nn.Conv2d(in_channels=96,out_channels=192,kernel_size=5,stride=1,padding=2,bias=False),
    12. nn.ReLU(inplace=True),
    13. nn.MaxPool2d(kernel_size=3,stride=2,padding=0),
    14. nn.Conv2d(in_channels=192,out_channels=384,kernel_size=3,stride=1,padding=1,bias=False),
    15. nn.ReLU(inplace=True),
    16. nn.Conv2d(in_channels=384,out_channels=256,kernel_size=3,stride=1,padding=1,bias=False),
    17. nn.ReLU(inplace=True),
    18. nn.Conv2d(in_channels=256,out_channels=256,kernel_size=3,stride=1,padding=1,bias=False),
    19. nn.ReLU(inplace=True),
    20. nn.MaxPool2d(kernel_size=3, stride=2, padding=0),
    21. )
    22. self.classifier = nn.Sequential(
    23. nn.Dropout(p=0.5),
    24. nn.Linear(in_features=256*6*6,out_features=4096),
    25. nn.ReLU(inplace=True),
    26. nn.Dropout(p=0.5),
    27. nn.Linear(in_features=4096, out_features=4096),
    28. nn.ReLU(inplace=True),
    29. nn.Linear(in_features=4096, out_features=num_classes),
    30. )
    31. def forward(self,x):
    32. x = self.feature_extraction(x)
    33. x = x.view(x.size(0),256*6*6)
    34. x = self.classifier(x)
    35. return x
    36. if __name__ =='__main__':
    37. # model = torchvision.models.AlexNet()
    38. model = AlexNet()
    39. # 打印模型参数
    40. #for param in model.parameters():
    41. #print(param)
    42. #打印模型名称与shape
    43. for name,parameters in model.named_parameters():
    44. print(name,':',parameters.size())
    View Code

    计算参数量与可训练参数量

    1. def get_parameter_number(model):
    2. total_num = sum(p.numel() for p in model.parameters())
    3. trainable_num = sum(p.numel() for p in model.parameters() if p.requires_grad)
    4. return {'Total': total_num, 'Trainable': trainable_num}
    5. total_num, trainable_num = get_parameter_number(model)
    6. print("trainable_num/total_num: %.2fM/%.2fM" % (trainable_num / 1e6, total_num / 1e6))

    torchsummary

    1. import torchsummary as summary
    2. summary.summary(model, (3, 224, 224))

    打印结果

    1. ----------------------------------------------------------------
    2. Layer (type) Output Shape Param #
    3. ================================================================
    4. Conv2d-1 [-1, 96, 55, 55] 34,848
    5. ReLU-2 [-1, 96, 55, 55] 0
    6. MaxPool2d-3 [-1, 96, 27, 27] 0
    7. Conv2d-4 [-1, 192, 27, 27] 460,800
    8. ReLU-5 [-1, 192, 27, 27] 0
    9. MaxPool2d-6 [-1, 192, 13, 13] 0
    10. Conv2d-7 [-1, 384, 13, 13] 663,552
    11. ReLU-8 [-1, 384, 13, 13] 0
    12. Conv2d-9 [-1, 256, 13, 13] 884,736
    13. ReLU-10 [-1, 256, 13, 13] 0
    14. Conv2d-11 [-1, 256, 13, 13] 589,824
    15. ReLU-12 [-1, 256, 13, 13] 0
    16. MaxPool2d-13 [-1, 256, 6, 6] 0
    17. Dropout-14 [-1, 9216] 0
    18. Linear-15 [-1, 4096] 37,752,832
    19. ReLU-16 [-1, 4096] 0
    20. Dropout-17 [-1, 4096] 0
    21. Linear-18 [-1, 4096] 16,781,312
    22. ReLU-19 [-1, 4096] 0
    23. Linear-20 [-1, 1000] 4,097,000
    24. ================================================================
    25. Total params: 61,264,904
    26. Trainable params: 61,264,904
    27. Non-trainable params: 0
    28. ----------------------------------------------------------------
    29. Input size (MB): 0.57
    30. Forward/backward pass size (MB): 9.96
    31. Params size (MB): 233.71
    32. Estimated Total Size (MB): 244.24
    33. ----------------------------------------------------------------
    View Code

    torchstat

    1. from torchstat import stat
    2. stat(model, (3, 224, 224))
    3. # Total params: 61,264,904
    4. # ------------------------------------------
    5. # Total memory: 4.98MB
    6. # Total MAdd: 1.72GMAdd
    7. # Total Flops: 862.36MFlops
    8. # Total MemR+W: 244.14MB

    thop

    1. from thop import profile
    2. input = torch.randn(1, 3, 224, 224)
    3. flops, params = profile(model, inputs=(input, ))
    4. print(flops, params) # 861301280.0 61264904.0

    ptflops

    1. from ptflops import get_model_complexity_info
    2. flops, params = get_model_complexity_info(model, (3, 224, 224), as_strings=True, print_per_layer_stat=True)
    3. print('Flops: ' + flops)
    4. print('Params: ' + params)

    复杂度对模型的影响

    • 时间复杂度决定了模型的训练/预测时间。如果复杂度过高,则会导致模型训练和预测耗费大量时间,既无法快速的验证想法和改善模型,也无法做到快速的预测。
    • 空间复杂度决定了模型的参数数量。由于维度诅咒的限制,模型的参数越多,训练模型所需的数据量就越大,而现实生活中的数据集通常不会太大,这会导致模型的训练更容易过拟合。
    • 当我们需要裁剪模型时,由于卷积核的空间尺寸通常已经很小(3x3),而网络的深度又与模型的表征能力紧密相关,不宜过多削减,因此模型裁剪通常最先下手的地方就是通道数

    Inception 系列模型是如何优化复杂度的

    Inception V1中的 1*1 卷积降维同时优化时间复杂度和空间复杂度

    Inception V1中使用 GAP 代替 Flatten

    Inception V2中使用 两个3*3卷积级联代替5*5卷积分支

    Inception V3中使用 N*1与1*N卷积级联代替N*N卷积

    Xception 中使用 深度可分离卷积(Depth-wise Separable Convolution)

    参考文献

    【知乎】卷积神经网络的复杂度分析

    【知乎】神经网络模型复杂度分析

    【知乎】深度学习模型参数量/计算量和推理速度计算

    【知乎】教你如何估计各种神经网络的计算量和参数量

    【飞桨】分组卷积(Group Convolution)

    【知乎】深度可分离卷积

  • 相关阅读:
    链表-真正的动态数据结构
    【笔记】Splay
    要有自己的科研笔记
    hive01--hive的安装及配置
    pytest(8)-参数化
    手把手教你写一个JSON在线解析的前端网站1
    Cannot find module ‘./assets/empty-module.js‘
    flask连接sqllite,并进行一些基本的增删改查操作
    python读取excel数据,某行、某列、某单元
    Intel Locked Atomic Operations
  • 原文地址:https://blog.csdn.net/qq_34218078/article/details/126766781