人臉106點(diǎn)Caffe模型如何部署到MsnhNet
?【GiantPandaCV導(dǎo)語(yǔ)】大家好,今天為大家介紹一下如何部署一個(gè)人臉106關(guān)鍵點(diǎn)模型到MsnhNet上,涉及到Caffe和Pytorch,MsnhNet模型轉(zhuǎn)換,融合BN簡(jiǎn)化網(wǎng)絡(luò)和如何編寫(xiě)MsnhNet預(yù)測(cè)代碼等等。
?
1. 前言
之前,MsnhNet主要支持了將Pytorch模型轉(zhuǎn)換為MsnhNet框架可以運(yùn)行的模型文件(*.msnhnet和*.bin),并且我們?cè)谥暗?a style="text-decoration: none;word-wrap: break-word;color: #40B8FA;font-weight: normal;border-bottom: 1px solid #3BAAFA;" data-linktype="2">Pytorch轉(zhuǎn)Msnhnet模型思路分享文章中分享了這個(gè)轉(zhuǎn)換的思路。
最近嘗試部署一個(gè)開(kāi)源的人臉106點(diǎn)Caffe模型(https://github.com/dog-qiuqiu/MobileNet-Yolo/tree/master/yoloface50k-landmark106)到MsnhNet中,所以這篇文章就記錄了我是如何將這個(gè)Caffe模型轉(zhuǎn)換到MsnhNet并進(jìn)行部署的。
2. 通用的轉(zhuǎn)換思路
由于我們已經(jīng)在Pytroch2Msnhnet這個(gè)過(guò)程上花費(fèi)了比較大的精力,所以最直接的辦法就是直接將Caffe模型轉(zhuǎn)為Pytorch模型,然后調(diào)用已有的Pytorch2Msnhnet工具完成轉(zhuǎn)換,這樣是比較快捷省事的。
我參考https://github.com/UltronAI/pytorch-caffe這個(gè)工程里面的caffe2pytorch工具新增了一些上面提到的yoloface50k-landmark106關(guān)鍵點(diǎn)模型要用到的OP,如PReLU,nn.BatchNorm1D以及只有2個(gè)維度的Scale層等,例如將Scale層重寫(xiě)為:
class?Scale(nn.Module):
????def?__init__(self,?channels):
????????super(Scale,?self).__init__()
????????self.weight?=?Parameter(torch.Tensor(channels))
????????self.bias?=?Parameter(torch.Tensor(channels))
????????self.channels?=?channels
?#?Python?有一個(gè)內(nèi)置的函數(shù)叫?repr,它能把一個(gè)對(duì)象用字符串的形式表達(dá)出來(lái)以便辨認(rèn),這就是“字符串表示形式”
????def?__repr__(self):
????????return?'Scale(channels?=?%d)'?%?self.channels
????def?forward(self,?x):
??#?landmark網(wǎng)絡(luò)最后的全連接層后面接了Scale,所以需要考慮Scale層輸入為2維的情況
????????if?x.dim()?==?2:
????????????nB?=?x.size(0)
????????????nC?=?x.size(1)
????????????x?=?x?*?self.weight.view(1,?nC).expand(nB,?nC)?+?\
????????????????self.bias.view(1,?nC).expand(nB,?nC)
????????else:
????????????nB?=?x.size(0)
????????????nC?=?x.size(1)
????????????nH?=?x.size(2)
????????????nW?=?x.size(3)
????????????x?=?x?*?self.weight.view(1,?nC,?1,?1).expand(nB,?nC,?nH,?nW)?+?\
????????????????self.bias.view(1,?nC,?1,?1).expand(nB,?nC,?nH,?nW)
????????return?x
可以看到這個(gè)Caffe里面的Scale層Pytorch是不原生支持的,這是Caffe特有的層,所以這里寫(xiě)一個(gè)Scale類繼承nn.Module來(lái)拼出一個(gè)Scale層。除了Scale層還有其它的很多層是這種做法,例如Eletwise層可以這樣來(lái)拼:
class?Eltwise(nn.Module):
????def?__init__(self,?operation='+'):
????????super(Eltwise,?self).__init__()
????????self.operation?=?operation
????def?__repr__(self):
????????return?'Eltwise?%s'?%?self.operation
????def?forward(self,?*inputs):
????????if?self.operation?==?'+'?or?self.operation?==?'SUM':
????????????x?=?inputs[0]
????????????for?i?in?range(1,len(inputs)):
????????????????x?=?x?+?inputs[i]
????????elif?self.operation?==?'*'?or?self.operation?==?'MUL':
????????????x?=?inputs[0]
????????????for?i?in?range(1,len(inputs)):
????????????????x?=?x?*?inputs[i]
????????elif?self.operation?==?'/'?or?self.operation?==?'DIV':
????????????x?=?inputs[0]
????????????for?i?in?range(1,len(inputs)):
????????????????x?=?x?/?inputs[i]
????????elif?self.operation?==?'MAX':
????????????x?=?inputs[0]
????????????for?i?in?range(1,len(inputs)):
????????????????x?=torch.max(x,?inputs[i])
????????else:
????????????print('forward?Eltwise,?unknown?operator')
????????return?x
介紹了如何在Pytorch中拼湊出Caffe的特有層之后,我們就可以對(duì)Caffe模型進(jìn)行解析,然后利用解析后的層關(guān)鍵信息完成Caffe模型到Pytorch模型的轉(zhuǎn)換了。解析Caffe模型的代碼實(shí)現(xiàn)在https://github.com/msnh2012/Msnhnet/blob/master/tools/caffe2Msnhnet/prototxt.py文件,我們截出一個(gè)核心部分說(shuō)明一下,更多細(xì)節(jié)讀者可以親自查看。
我們以一個(gè)卷積層為例,來(lái)理解一下這個(gè)Caffe模型中的prototxt解析函數(shù):
layer?{
??name:?"conv1_conv2d"
??type:?"Convolution"
??bottom:?"data"
??top:?"conv1_conv2d"
??convolution_param?{
????num_output:?8
????bias_term:?false
????group:?1
????stride:?2
????pad_h:?1
????pad_w:?1
????kernel_h:?3
????kernel_w:?3
??}
}
解析prototxt文件的代碼實(shí)現(xiàn)如下,結(jié)合上面卷積層的prototxt表示和下面代碼的注釋?xiě)?yīng)該很好理解:
def?parse_prototxt(protofile):
?#?caffe的每個(gè)layer以{}包起來(lái)
????def?line_type(line):
????????if?line.find(':')?>=?0:
????????????return?0
????????elif?line.find('{')?>=?0:
????????????return?1
????????return?-1
????def?parse_block(fp):
????????#?使用OrderedDict會(huì)根據(jù)放入元素的先后順序進(jìn)行排序,所以輸出的值是排好序的
????????block?=?OrderedDict()
????????line?=?fp.readline().strip()
????????while?line?!=?'}':
????????????ltype?=?line_type(line)
????????????if?ltype?==?0:?#?key:?value
????????????????#print?line
????????????????line?=?line.split('#')[0]
????????????????key,?value?=?line.split(':')
????????????????key?=?key.strip()
????????????????value?=?value.strip().strip('"')
????????????????if?key?in??block:
????????????????????if?type(block[key])?==?list:
????????????????????????block[key].append(value)
????????????????????else:
????????????????????????block[key]?=?[block[key],?value]
????????????????else:
????????????????????block[key]?=?value
????????????elif?ltype?==?1:?#?獲取塊名,以卷積層為例返回[layer,?convolution_param]
????????????????key?=?line.split('{')[0].strip()
????????????????#?遞歸
????????????????sub_block?=?parse_block(fp)
????????????????block[key]?=?sub_block
????????????line?=?fp.readline().strip()
????????????#?忽略注釋
????????????line?=?line.split('#')[0]
????????return?block
????fp?=?open(protofile,?'r')
????props?=?OrderedDict()
????layers?=?[]
????line?=?fp.readline()
????counter?=?0
????while?line:
????????line?=?line.strip().split('#')[0]
????????if?line?==?'':
????????????line?=?fp.readline()
????????????continue
????????ltype?=?line_type(line)
????????if?ltype?==?0:?#?key:?value
????????????key,?value?=?line.split(':')
????????????key?=?key.strip()
????????????value?=?value.strip().strip('"')
????????????if?key?in??props:
???????????????if?type(props[key])?==?list:
???????????????????props[key].append(value)
???????????????else:
???????????????????props[key]?=?[props[key],?value]
????????????else:
????????????????props[key]?=?value
????????elif?ltype?==?1:?#?獲取塊名,以卷積層為例返回[layer,?convolution_param]
????????????key?=?line.split('{')[0].strip()
????????????if?key?==?'layer':
????????????????layer?=?parse_block(fp)
????????????????layers.append(layer)
????????????else:
????????????????props[key]?=?parse_block(fp)
????????line?=?fp.readline()
????if?len(layers)?>?0:
????????net_info?=?OrderedDict()
????????net_info['props']?=?props
????????net_info['layers']?=?layers
????????return?net_info
????else:
????????return?props
然后解析CaffeModel比較簡(jiǎn)單,直接調(diào)用caffe提供的接口即可,代碼實(shí)現(xiàn)如下:
def?parse_caffemodel(caffemodel):
????model?=?caffe_pb2.NetParameter()
????print?('Loading?caffemodel:?'),?caffemodel
????with?open(caffemodel,?'rb')?as?fp:
????????model.ParseFromString(fp.read())
????return?model
解析完Caffe模型之后,我們就拿到了所有Layer的參數(shù)信息和權(quán)重,我們只需要將其對(duì)應(yīng)放到Pytorch實(shí)現(xiàn)的Layer就可以了,這部分的代碼實(shí)現(xiàn)就是https://github.com/msnh2012/Msnhnet/blob/master/tools/caffe2Msnhnet/caffenet.py#L332這里的CaffeNet類這里就不再過(guò)多解釋了,因?yàn)檫@僅僅是一個(gè)構(gòu)件Pytorch模型并加載權(quán)重的過(guò)程,相信熟悉Pytorch的同學(xué)不難看懂和寫(xiě)出這部分代碼。執(zhí)行完這個(gè)過(guò)程之后我們就可以獲得Caffe模型對(duì)應(yīng)的Pytorch模型了。
3. 精簡(jiǎn)網(wǎng)絡(luò)
為了讓Pytorch模型轉(zhuǎn)出來(lái)的MsnhNet模型推理更快,我們可以考慮在Caffe轉(zhuǎn)到Pytorch模型時(shí)就精簡(jiǎn)一些網(wǎng)絡(luò)層,比如常規(guī)的Convolution+BN+Scale可以融合為一個(gè)層。我們發(fā)現(xiàn)這里還存在一個(gè)FC+BN+Scale的結(jié)構(gòu),我們也可以一并融合了。這里可以再簡(jiǎn)單回顧一下融合的原理。
3.1 融合BN原理介紹
「我們知道卷積層的計(jì)算可以表示為:」
「然后BN層的計(jì)算可以表示為:」
「我們把二者組合一下,公式如下:」
然后令
「那么,合并BN層后的卷積層的權(quán)重和偏置可以表示為:」
這個(gè)公式同樣可以用于反卷積,全連接和BN+Scale的組合情況。
3.2 融合BN
基于上面的理論,我們可以在轉(zhuǎn)Caffe模型之前就把BN融合掉,這樣我們?cè)贛snhNet上推理更快(另外一個(gè)需要融合的原因是目前MsnhNet的圖優(yōu)化工具還在開(kāi)發(fā)中,暫時(shí)不支持帶BN+Scale層的融合)。Caffe模型融合的代碼我放在https://github.com/msnh2012/Msnhnet/blob/master/tools/caffe2Msnhnet/caffeOptimize/caffeOptimize.py這里了,簡(jiǎn)要介紹如下:

4. MsnhNet推理
精簡(jiǎn)網(wǎng)絡(luò)之后我們就可以重新將沒(méi)有BN的Caffe模型轉(zhuǎn)到Pytorch再轉(zhuǎn)到MsnhNet了,這部分的示例如下:
#?-*-?coding:?utf-8
#?from?pytorch2caffe?import?plot_graph,?pytorch2caffe
import?sys
import?cv2
import?caffe
import?numpy?as?np
import?os
from?caffenet?import?*
import?argparse
import?torch
from?PytorchToMsnhnet?import?*
################################################################################################???
parser?=?argparse.ArgumentParser(description='Convert?Caffe?model?to?MsnhNet?model.',
?????????????????????????????????formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--model',?type=str,?default=None)
parser.add_argument('--weights',?type=str,?default=None)
parser.add_argument('--height',?type=int,?default=None)
parser.add_argument('--width',?type=int,?default=None)
parser.add_argument('--channels',?type=int,?default=None)
args?=?parser.parse_args()
model_def?=?args.model
model_weights?=?args.weights
name?=?model_weights.split('/')[-1].split('.')[0]
width?=?args.width
height?=?args.height
channels?=?args.channels
net?=?CaffeNet(model_def,?width=width,?height=height,?channels=channels)
net.load_weights(model_weights)
net.to('cpu')
net.eval()
input=torch.ones([1,channels,height,width])
model_name?=?name?+?".msnhnet"
model_bin?=?name?+?".msnhbin"
trans(net,?input,model_name,model_bin)
獲得了MsnhNet的模型文件之后,我們就可以使用MsnhNet進(jìn)行推理了,推理部分的代碼在https://github.com/msnh2012/Msnhnet/blob/master/examples/landmark106/landmark106.cpp。
我們來(lái)看看效果,隨便拿一張人臉圖片來(lái)測(cè)試一下:


landmark的結(jié)果還是比較正確的,另外我們對(duì)比了Caffe/Pytorch/MsnhNet的每層特征值,F(xiàn)loat32情況下相似度均為100%,證明我們的轉(zhuǎn)換過(guò)程是正確的。
我們?cè)?strong style="color: #3594F7;font-weight: bold;">「X86 CPU」 i7 10700F上測(cè)一下速度(float32推理),結(jié)果如下:
| 分辨率 | 線程數(shù) | 時(shí)間 |
|---|---|---|
| 112x112 | 1 | 5ms |
| 112x112 | 2 | 3.5ms |
| 112x112 | 4 | 2.7ms |
速度還是挺快的,由于本框架目前在x86沒(méi)有太多優(yōu)化,所以這個(gè)速度后面會(huì)進(jìn)一步優(yōu)化的。感興趣的讀者也可以測(cè)試在其它平臺(tái)上這個(gè)模型的速度。
5. 轉(zhuǎn)換工具支持的OP和用法
5.1 介紹
Caffe2msnhnet工具首先將你的Caffe模型轉(zhuǎn)換為Pytorch模型,然后調(diào)用Pytorch2msnhnet工具將Caffe模型轉(zhuǎn)為*.msnhnet和*.bin。
5.2 依賴
Pycaffe Pytorch
5.3 計(jì)算圖優(yōu)化
在調(diào)用
caffe2msnhnet.py之前建議使用caffeOPtimize文件夾中的caffeOptimize.py對(duì)原始的Caffe模型進(jìn)行圖優(yōu)化,目前已支持的操作有:Conv+BN+Scale 融合到 Conv
Deconv+BN+Scale 融合到Deconv
InnerProduct+BN+Scale 融合到InnerProduct
5.4 Caffe2Pytorch支持的OP
Convolution 轉(zhuǎn)為 nn.Conv2dDeconvolution 轉(zhuǎn)為 nn.ConvTranspose2dBatchNorm 轉(zhuǎn)為 nn.BatchNorm2d或者nn.BatchNorm1dScale 轉(zhuǎn)為 乘/加ReLU 轉(zhuǎn)為 nn.ReLULeakyReLU 轉(zhuǎn)為 nn.LeakyReLUPReLU 轉(zhuǎn)為 nn.PReLUMax Pooling 轉(zhuǎn)為 nn.MaxPool2dAVE Pooling 轉(zhuǎn)為 nn.AvgPool2dEltwise 轉(zhuǎn)為 加/減/乘/除/torch.maxInnerProduct 轉(zhuǎn)為 nn.LinearNormalize 轉(zhuǎn)為 pow/sum/sqrt/加/乘/除拼接Permute 轉(zhuǎn)為 torch.permuteFlatten 轉(zhuǎn)為 torch.viewReshape 轉(zhuǎn)為 numpy.reshape/torch.from_numpy拼接Slice 轉(zhuǎn)為 torch.index_selectConcat 轉(zhuǎn)為 torch.catCrop 轉(zhuǎn)為 torch.arange/torch.resize_拼接Softmax 轉(zhuǎn)為 torch.nn.function.softmax
5.5 Pytorch2Msnhnet支持的OP
conv2d max_pool2d avg_pool2d adaptive_avg_pool2d linear flatten dropout batch_norm interpolate(nearest, bilinear) cat elu selu relu relu6 leaky_relu tanh softmax sigmoid softplus abs acos asin atan cos cosh sin sinh tan exp log log10 mean permute view contiguous sqrt pow sum pad +|-|x|/|+=|-=|x=|/=|
5.6 使用方法舉例
python caffe2msnhnet --model landmark106.prototxt --weights landmark106.caffemodel --height 112 --width 112 --channels 3,執(zhí)行完之后會(huì)在當(dāng)前目錄下生成lanmark106.msnhnet和landmark106.bin文件。
6. 總結(jié)
至此,我們完成了yoloface50k-landmark106在MsnhNet上的模型轉(zhuǎn)換和部署測(cè)試,如果對(duì)本框架感興趣可以嘗試部署自己的一個(gè)模型試試看,如果轉(zhuǎn)換工具有問(wèn)題請(qǐng)?jiān)趃ithub提出issue或者直接聯(lián)系我們。點(diǎn)擊閱讀原文可以快速關(guān)注MsnhNet,這是我們業(yè)余開(kāi)發(fā)的一個(gè)輕量級(jí)推理框架,如果對(duì)模型部署和算法優(yōu)化感興趣的讀者可以看看,我們也會(huì)在GiantPandaCV公眾號(hào)分享我們的框架開(kāi)發(fā)和算子優(yōu)化相關(guān)的經(jīng)歷。
7. 參考
https://github.com/UltronAI/pytorch-caffe https://github.com/msnh2012/Msnhnet
為了感謝讀者們的長(zhǎng)期支持,我們今天將送出3本由人民郵電出版社提供的《深入淺出 GAN 生成對(duì)抗網(wǎng)絡(luò)》書(shū)籍,感興趣的小伙伴可以在下方留言板留言,我們將從中抽取幾位小伙伴分別送出一本正版書(shū)籍。

沒(méi)中獎(jiǎng)并且對(duì)本書(shū)也感興趣的小伙伴可以考慮點(diǎn)擊下方的當(dāng)當(dāng)網(wǎng)鏈接自行購(gòu)買:
歡迎關(guān)注GiantPandaCV, 在這里你將看到獨(dú)家的深度學(xué)習(xí)分享,堅(jiān)持原創(chuàng),每天分享我們學(xué)習(xí)到的新鮮知識(shí)。( ? ?ω?? )?
有對(duì)文章相關(guān)的問(wèn)題,或者想要加入交流群,歡迎添加BBuf微信:
為了方便讀者獲取資料以及我們公眾號(hào)的作者發(fā)布一些Github工程的更新,我們成立了一個(gè)QQ群,二維碼如下,感興趣可以加入。
