我會簡述如何得到v5模型中各層的參數量和計算量(爛大街的參量表),然後再将如何得到各層的計算量FLOPs(基本沒人教怎麽獲得各層FLOPs,花我一番功夫,其實特别簡單,輪子U神都造好了)
文章目錄
- 前言
- 一、參數量param和計算量FLOPs
- 二、YOLOV5中打印各項參數
- 1. 爛大街的參數打印
- 2. 各層的計算量FLOPs
- 3.柳暗花明又一村
- 總結
前言
在側端部署深度學習模型時,我們一直都說說這些模型很小,屬于輕量級網絡。當他人問如何橫向對比這一批輕量級網絡時,我們該如何證明這個網絡比另一個網絡優秀呢?除了mAP外,我們還可以 比對參數量param和計算量FlOPs。
一、參數量param和計算量FLOPs
以yolov5的官方參數表爲例:
mAP:平均精度指标
params:參數量,單位M:minllion 10^6
FLOPs:模型總計算量 ,單位B/G:billion 10^9
注意:FLOPS爲每秒浮點運算次數,常用于描述GPU性能,注意s的大小寫
二、YOLOV5中打印各項參數
其實U神在yolov5的工程文件已經寫好了打印各項參數的代碼,細心的小夥伴也在train或detect時也留意到程序會計算模型信息并打印。
1. 爛大街的參數打印
在 .\yolov5\utils\torch_utilis.py 文件中model_info函數負責打印param和FLOPs信息
def model_info(model, verbose=False, img_size=640): #打印模型參數、計算量 # Model information. img_size may be int or list, i.e. img_size=640 or img_size=[640, 320] #train階段會輸出叠代數,等于param n_p = sum(x.numel() for x in model.parameters()) # number parameters n_g = sum(x.numel() for x in model.parameters() if x.requires_grad) # number gradients if verbose: #if True: print(f"{'layer':>5} {'name':>40} {'gradient':>9} {'parameters':>12} {'shape':>20} {'mu':>10} {'sigma':>10}") for i, (name, p) in enumerate(model.named_parameters()): name = name.replace('module_list.', '') print('%5g %40s %9s %12g %20s %10.3g %10.3g' % (i, name, p.requires_grad, p.numel(), list(p.shape), p.mean(), p.std())) try: # FLOPs # FLOPs 核心計算庫 thop.profile from thop import profile stride = max(int(model.stride.max()), 32) if hasattr(model, 'stride') else 32 img = torch.zeros((1, model.yaml.get('ch', 3), stride, stride), device=next(model.parameters()).device) # input flops = profile(deepcopy(model), inputs=(img,), verbose=False)[0] / 1E9 * 2 # stride GFLOPs img_size = img_size if isinstance(img_size, list) else [img_size, img_size] # expand if int/float fs = ', %.1f GFLOPs' % (flops * img_size[0] / stride * img_size[1] / stride) # 640x640 GFLOPs except (ImportError, Exception): fs = '' name = Path(model.yaml_file).stem.replace('yolov5', 'YOLOv5') if hasattr(model, 'yaml_file') else 'Model' LOGGER.info(f"{name} summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}")
yolo.py會生成一個Model類并調用model_info函數,并結合parse_model函數,我們會得到爛大街的參數表
from n params module arguments 0 -1 1 3520 models.common.Conv [3, 32, 6, 2, 2] 1 -1 1 18560 models.common.Conv [32, 64, 3, 2] 2 -1 1 18816 models.common.C3 [64, 64, 1] 3 -1 1 73984 models.common.Conv [64, 128, 3, 2] 4 -1 2 115712 models.common.C3 [128, 128, 2] 5 -1 1 295424 models.common.Conv [128, 256, 3, 2] 6 -1 3 625152 models.common.C3 [256, 256, 3] 7 -1 1 1180672 models.common.Conv [256, 512, 3, 2] 8 -1 1 1182720 models.common.C3 [512, 512, 1] 9 -1 1 656896 models.common.SPPF [512, 512, 5] 10 -1 1 131584 models.common.Conv [512, 256, 1, 1] 11 -1 1 0 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest'] 12 [-1, 6] 1 0 models.common.Concat [1] 13 -1 1 361984 models.common.C3 [512, 256, 1, False] 14 -1 1 33024 models.common.Conv [256, 128, 1, 1] 15 -1 1 0 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest'] 16 [-1, 4] 1 0 models.common.Concat [1] 17 -1 1 90880 models.common.C3 [256, 128, 1, False] 18 -1 1 147712 models.common.Conv [128, 128, 3, 2] 19 [-1, 14] 1 0 models.common.Concat [1] 20 -1 1 296448 models.common.C3 [256, 256, 1, False] 21 -1 1 590336 models.common.Conv [256, 256, 3, 2] 22 [-1, 10] 1 0 models.common.Concat [1] 23 -1 1 1182720 models.common.C3 [512, 512, 1, False] 24 [17, 20, 23] 1 26970 Detect [5, [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]], [128, 256, 512]] YOLOv5s summary: 270 layers, 7033114 parameters, 7033114 gradients, 15.9 GFLOPs
2. 各層的計算量FLOPs
筆者發現FLOPs的核心計算是利用profile庫完成的
from thop import profile flops = profile(deepcopy(model), inputs=(img,), verbose=False)[0] / 1E9 * 2 # stride GFLOPs
當時筆者進一步進入profile.py的底層邏輯中進行修改,想簡單的獲得各層的計算量或者是計算量的峰值,後續發現其實U神在torch_utilis.py中重構了一個profile函數。
該函數用于輸出訓練過程中的一些相關信息,如前向傳播時間、反向傳播時間、輸入變量的shape、輸出變量的shape等
def profile(input, ops, n=10, device=None): # YOLOv5 speed/memory/FLOPs profiler # # Usage: # input = torch.randn(16, 3, 640, 640) # m1 = lambda x: x * torch.sigmoid(x) # m2 = nn.SiLU() # profile(input, [m1, m2], n=100) # profile over 100 iterations results = [] device = device or select_device() print(f"{'Params':>12s}{'GFLOPs':>12s}{'GPU_mem (GB)':>14s}{'forward (ms)':>14s}{'backward (ms)':>14s}" f"{'input':>24s}{'output':>24s}") for x in input if isinstance(input, list) else [input]: x = x.to(device) x.requires_grad = True for m in ops if isinstance(ops, list) else [ops]: m = m.to(device) if hasattr(m, 'to') else m # device m = m.half() if hasattr(m, 'half') and isinstance(x, torch.Tensor) and x.dtype is torch.float16 else m tf, tb, t = 0, 0, [0, 0, 0] # dt forward, backward try: flops = thop.profile(m, inputs=(x,), verbose=False)[0] / 1E9 * 2 # GFLOPs except Exception: flops = 0 try: for _ in range(n): t[0] = time_sync() y = m(x) t[1] = time_sync() try: _ = (sum(yi.sum() for yi in y) if isinstance(y, list) else y).sum().backward() t[2] = time_sync() except Exception: # no backward method # print(e) # for debug t[2] = float('nan') tf += (t[1] - t[0]) * 1000 / n # ms per op forward tb += (t[2] - t[1]) * 1000 / n # ms per op backward mem = torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0 # (GB) s_in = tuple(x.shape) if isinstance(x, torch.Tensor) else 'list' s_out = tuple(y.shape) if isinstance(y, torch.Tensor) else 'list' p = sum(list(x.numel() for x in m.parameters())) if isinstance(m, nn.Module) else 0 # parameters print(f'{p:12}{flops:12.4g}{mem:>14.3f}{tf:14.4g}{tb:14.4g}{str(s_in):>24s}{str(s_out):>24s}') results.append([p, flops, mem, tf, tb, s_in, s_out]) except Exception as e: print(e) results.append(None) torch.cuda.empty_cache() return results
當時就想利用這個函數進行修改,嵌入到model_info函數中,達到輸出每一層網絡的FLOPs的目的,曆經嘗試失敗了。
3.柳暗花明又一村
神奇的發現U神其實都寫好了造好輪子的,隻是我們不知道哪裏去用,怎麽用,真的郁悶。
AnyWay,自己琢磨的過程也是學習的過程吧。
yolo.py文件中,U神寫好了用法:
if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--cfg', type=str, default='yolov5s.yaml', help='model.yaml') parser.add_argument('--batch-size', type=int, default=1, help='total batch size for all GPUs') parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument('--profile', action='store_true', help='profile model speed') parser.add_argument('--line-profile', action='store_true', help='profile model speed layer by layer') parser.add_argument('--test', action='store_true', help='test all yolo*.yaml') opt = parser.parse_args() opt.cfg = check_yaml(opt.cfg) # check YAML print_args(vars(opt)) device = select_device(opt.device) # Create model im = torch.rand(opt.batch_size, 3, 640, 640).to(device) model = Model(opt.cfg).to(device) # Options if opt.line_profile: # profile layer by layer _ = model(im, profile=True) elif opt.profile: # profile forward-backward results = profile(input=im, ops=[model], n=3) elif opt.test: # test all models for cfg in Path(ROOT / 'models').rglob('yolo*.yaml'): try: _ = Model(cfg) except Exception as e: print(f'Error in {cfg}: {e}')
parser.add_argument('--profile', action='store_true', help='profile model speed') parser.add_argument('--line-profile', action='store_true', help='profile model speed layer by layer') #其實這裏就明明白白寫着怎麽輸出各層FLOPs
稍微修改代碼,然後
終端行輸入python .\models\yolo.py --line-profile我們得到:
time (ms) GFLOPs params module 22.01 0.72 3520 models.common.Conv 16.48 0.95 18560 models.common.Conv 41.61 0.96 18816 models.common.C3 10.80 0.95 73984 models.common.Conv 33.24 1.48 115712 models.common.C3 8.00 0.95 295424 models.common.Conv 26.01 2.00 625152 models.common.C3 6.80 0.94 1180672 models.common.Conv 10.16 0.95 1182720 models.common.C3 11.60 0.53 656896 models.common.SPPF 1.20 0.11 131584 models.common.Conv 0.40 0.00 0 torch.nn.modules.upsampling.Upsample 0.40 0.00 0 models.common.Concat 16.00 1.16 361984 models.common.C3 1.60 0.11 33024 models.common.Conv 0.80 0.00 0 torch.nn.modules.upsampling.Upsample 1.20 0.00 0 models.common.Concat 26.04 1.16 90880 models.common.C3 4.00 0.47 147712 models.common.Conv 0.40 0.00 0 models.common.Concat 14.44 0.95 296448 models.common.C3 4.09 0.47 590336 models.common.Conv 0.00 0.00 0 models.common.Concat 10.40 0.95 1182720 models.common.C3 2.80 0.09 26970 Detect 270.49 15.88 7033114.00 Total
得到完整的各層參量和FLOPs表,簡潔多了
如果你看到這裏,我希望你研究下profile函數,這個函數可移植性高,可以适用大部分模型。
總結
最初我是想自己調用U神寫的profile函數,發現真的好難實現,輸入比較難寫,後續也發現了U神在yolo.py留下的彩蛋。
其實獲取各層的計算量很簡單,一行代碼就可以了,但爲什麽網上基本就沒有資料沒人記錄呢,哪怕用來指導後來人也很好啊,這個profile函數其實可以使用于大部分模型,可移植。
我就邊絮絮叨叨記錄踩坑過程,希望後來人不用踩坑吧,畢竟大部分都是像我這樣的萌新。雖然後面是用了U神的代碼,但前期自己琢磨的過程也很有收獲,笑