理性分析不同模型的性能指标

发布时间 2023-08-18 11:12:44作者: 信海

性能指标

FLOPS:浮点运算次数。
MADD:表示一次乘法和一次加法,这可以粗略认为:MADD=2 * Flops,即((输出一个元素所经历的乘法次数)+(输出一个元素所经历的加法的个数)) * (输出总共的元素的个数)
MEMREAD:网络运行时,从内存中读取的大小,即输入的特征图大小 + 网络参数的大小
MEMWRITE:网络运行时,写入到内存中的大小,即输出的特征图大小

模型类型

卷积神经网络

在经典卷积神经网络中,输入特征图通常为\(H\times W\)(这里假设\(H,W\)一致),输入通道数目为\(C_i\),卷积核输入深度通常和输入特征图的输入通道保持一致,其核大小通常为\(K\),输出通道(可以理解为输出深度)通常为\(C_{out}\),除此以外需要考虑填充大小\(P\)和步长\(S\)。基于上述条件,特征图输出大小\(M\)满足:

\[M=(H-K+2\times P)/ S + 1 \]

在每一次卷积过程中,每个卷积核与输入特征图上当前步对应的窗口内元素进行逐通道、逐元素相乘并累加;重复多次操作,直至卷积核完整遍历输入特征图,每个卷积层运算次数flops满足下述关系:

\[Flops = C_i \times K^2 \times M^2 \times C_{out} \]

依据层内相乘,层间相加的准则,经典卷积神经网络模型所有卷积层的时间复杂度满足关系如下(\(N\)为卷积层的数目):

\[\sum_j^N C_{ij} \times K_j^2 \times M_j^2 \times C_{out,j} \]

下面通过一个案例,来具体分析:

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 2, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(2)
        self.relu1 = nn.ReLU()

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu1(x)

        return x
net = Net()
stat(net, (3, 500, 500)) 

# 输入通道为3 输入特征图大小为500
# 输出特征图大小为(500-7+6)/2+1=250
# Flops=250*250*3*2*7*7=18375000
# MADD=(250*250*2)*((7*7*3)+(7*7*3-1))=36625000
# MemREAD= ((500 * 500 * 3) + (7 * 7 * 3 * 2)) * 4 = 3001176.0 【认为每个参数为FP32,满足四个字节,因此乘4】
# MEMW:250 * 250 * 2 * 4 = 500000

FLOPS/PARAMS参数计算工具

下面给出thop的使用方法,thop只能计算输入为一个矩阵的模型的参数量和FLOPs,实际中的模型可能存在多个输入,如以下案例:

out = model(detections,boxs,grids,masks,captions)

# 其中作为模型传入参数的是五个矩阵,其尺寸为:
detections.shape = (bs,50,2048)
boxs.shape = (bs,50,4)
grids.shape = (bs,49,2048)
masks.shape = (bs,50,49)
captions.shape = (bs,19) 

想要计算这样一个模型的参数量和计算量,需要考虑一种新的方式。我在这里给出的方案是:构建一个新的,接收单矩阵输入的类。代码如下:

import torch.nn as nn

class func(nn.Module):
    def __init__(self,object):
        super(func,self).__init__()
        self.model = object
    def forward(self,a):
        detections = torch.randn(1,50,2048).float()
        boxs = torch.randn(1,50,4).float()
        grids = torch.randn(1,49,2048).float()
        masks = torch.randn(1,50,49).float()
        captions = torch.tensor([0,100,105,2,1,1,1,1,
                          1,1,1,1,1,1,1,1,1,1,1]).unsqueeze(0)
        print(captions.shape)
        out = self.model(detections,boxs,grids,masks,captions)
        return out

可以看到,该类继承自nn.Module,以确保该类可以被声明为一个模型。接着在类的初始化函数中,将待计算模型的对象作为参数传入,并赋值给self.model;在forward函数中,规定其必须接收一个参数(实际上我们并不会使用这个接收到的参数),并通过torch内置的随机函数产生需要形状的张量(之所以特殊对待captions是因为model要求该参数的元素为整型)。

最后,就可以进行参数量和FLOPs的计算,代码如下:

model = Transformer(text_field.vocab.stoi['<bos>'],
                    encoder, decoder, args=args)
use_model = func(model)
input = torch.randn(1, 3, 224, 224)
flops, params = profile(use_model, inputs=(input))
print("FLOPs=", str(flops/1e9) + '{}'.format("G"))
print("params=", str(params/1e6) + '{}'.format("M"))

其中,第一行声明Transformer类,并将其对象命名为model;第二行声明上面定义的func类,将model作为声明类时的传入参数;第三行随机生成一个矩阵;第四行调用thop中的profile方法对func类的参数量和运算量进行计算,实际上等价于对Transformer类的参数量和运算量计算。

吞吐和时延

吞吐量:完成一个特定任务的速率(单位时间内完成的任务数目,衡量指标为bits/second,Bytes/second)。对神经网络而言,吞吐量可以设置为每秒处理的图片数量或语音数量。
延时:完成一个任务需要花费的时间。

吞吐量和延时并不是严格的反比关系。吞吐量可以认为是一个系统并行处理的任务量,延时是一个系统串行处理一个任务所花费的时间

参考文献

[1] 面试宝典笔记:卷积计算过程中的FLOPs
[2] 通过thop计算模型的参数量与运算量(FLOPs)
[3] THOP+torchstat 计算PyTorch模型的FLOPs,问题记录与解决
[4]吞吐和时延