在工程中,模型的運行速度與精度是同樣重要的,本文中,我會運用不同的方法去優(yōu)化比較模型的性能,希望能給大家?guī)硪恍嵱玫膖rick與經驗。
有關鍵點檢測相關經驗的同學應該知道,關鍵點主流方法分為Heatmap-based與Regression-based。
其主要區(qū)別在于監(jiān)督信息的不同,Heatmap-based方法監(jiān)督模型學習的是高斯概率分布圖,即把GroundTruth中每個點渲染成一張高斯熱圖,最后網絡輸出為K張?zhí)卣鲌D對應K個關鍵點,然后通過argmax來獲取最大值點作為估計結果。這種方法由于需要渲染高斯熱圖,且由于熱圖中的最值點直接對應了結果,不可避免地需要維持一個相對高分辨率的熱圖(常見的是64x64,再小的話誤差下界過大會造成嚴重的精度損失),因此也就自然而然導致了很大的計算量和內存開銷。
Regression-based方法則非常簡單粗暴,直接監(jiān)督模型學習坐標值,計算坐標值的L1或L2 loss。由于不需要渲染高斯熱圖,也不需要維持高分辨率,網絡輸出的特征圖可以很?。ū热?4x14甚至7x7),拿Resnet-50來舉例的話,F(xiàn)LOPs是Heatmap-based方法的兩萬分之一,這對于計算力較弱的設備(比如手機)是相當友好的,在實際的項目中,也更多地是采用這種方法。
但是Regression在精度方面始終被Heatmap碾壓,Heatmap全卷積的結構能夠完整地保留位置信息,因此高斯熱圖的空間泛化能力更強。而回歸方法因為最后需要將圖片向量展開成一維向量,reshape過程中會對位置信息有所丟失。除此之外,Regression中的全連接網絡需要將位置信息轉化為坐標值,對于這種隱晦的信息轉化過程,其非線性是極強的,因此不好訓練和收斂。
為了更好的提高Regression的精度,我將對其做出一系列優(yōu)化,并記錄于此。
github地址
1.regression
我將以mobilenetv3作為所有實驗的backbone,搭建MobileNetv3+Deeppose的Baseline。訓練數(shù)據來自項目,config如下所示。
model = dict(
type='TopDown',
pretrained=None,
backbone=dict(type='MobileNetV3'),
neck=dict(type='GlobalAveragePooling'),
keypoint_head=dict(
type='DeepposeRegressionHead',
in_channels=96,
num_joints=channel_cfg['num_output_channels'],
loss_keypoint=dict(type='SmoothL1Loss', use_target_weight=True)),
train_cfg=dict(),
test_cfg=dict(flip_test=True))
cpu端,模型速度是基于ncnn測試出來的,結論如下:
方法 | input size | AP50:95 | acc_pse | time |
---|---|---|---|---|
Deeppose | 192*256 | 41.3% | 65% | 2.5ms |
2.Heatmap
同樣以mobilenetv3作為backbone,與Regression不同的是,為了獲得尺寸為(48,64)的熱圖特征,我們需要在head添加3個deconv層,將backbone尺寸為(6,8)的特征圖上采樣至(48,64)。
model = dict(
type='TopDown',
backbone=dict(type='MobileNetV3'),
keypoint_head=dict(
type='TopdownHeatmapSimpleHead',
in_channels=96,
out_channels=channel_cfg['num_output_channels'],
loss_keypoint=dict(type='JointsMSELoss', use_target_weight=True)),
train_cfg=dict(),
test_cfg=dict(
flip_test=True,
post_process='default',
shift_heatmap=True,
modulate_kernel=11))
cpu端,模型速度是基于ncnn測試出來的,結論如下:
方法 | input size | AP50:95 | acc_pse | time |
---|---|---|---|---|
Deeppose | 192*256 | 41.3% | 65% | 2.5ms |
Heatmap | 192*256 | 67.5% | 93% | 60ms |
由于head層結構不同,參數(shù)量變大,導致推理時間劇增。Heatmap全卷積的結構能夠完整地保留位置信息,因此高斯熱圖的空間泛化能力更強。而回歸方法因為最后需要將圖片向量展開成一維向量,reshape過程中會對位置信息有所丟失。除此之外,Regression中的全連接網絡需要將位置信息轉化為坐標值,對于這種隱晦的信息轉化過程,其非線性是極強的,因此不好訓練和收斂。
3.RLE
Regression只關心離散概率分布的均值(只預測坐標值,一個均值可以對應無數(shù)種分布),丟失了 μ \mu μ周圍分布的信息,相較于heatmap顯示地將GT分布(人為設置方差 σ \sigma σ)標注成高斯熱圖并作為學習目標,RLE隱性的極大似然損失可以幫助regression確定概率分布均值與方差,構造真實誤差概率分布,從而更好的回歸坐標。
model = dict(
type='TopDown',
backbone=dict(type='MobileNetV3'),
neck=dict(type='GlobalAveragePooling'),
keypoint_head=dict(
type='DeepposeRegressionHead',
in_channels=96,
num_joints=channel_cfg['num_output_channels'],
loss_keypoint=dict(
type='RLELoss',
use_target_weight=True,
size_average=True,
residual=True),
out_sigma=True),
train_cfg=dict(),
test_cfg=dict(flip_test=True, regression_flip_shift=True))
mmpose已經實現(xiàn)了RLE loss,我們只需要在config上添加loss_keypoint=RLELoss就能夠運行。
方法 | input size | AP50:95 | acc_pse | time |
---|---|---|---|---|
Deeppose | 192*256 | 41.3% | 65% | 2.5ms |
Heatmap | 192*256 | 67.5% | 93% | 60ms |
RLE | 192*256 | 67.3% | 90% | 2.5ms |
從上表中,我們可以發(fā)現(xiàn),當引入RLE損失后,AP提升至67.3%與heatmap相近,同時推理時間仍然保持2.5ms。RLE詳細講解請參考。
4.Integral Pose Regression
我們知道Heatmap推理時,是通過argmax來獲取特征圖中得分最高的索引,但argmax本身不可導。為了解決這個問題,IPR采用了Soft-Argmax方式解碼,先用Softmax對概率熱圖進行歸一化,然后用求期望的方式得到預測坐標。我們在deeppose上引入IPR機制,將最后的fc換成conv層,保留backbone最后層的特征尺寸,并對該特征Softmax,利用期望獲得預測坐標。這樣做的一大好處是,能夠將更多的監(jiān)督信息引入訓練中。
我在mmpose上寫了IPRhead代碼
@HEADS.register_module()
class IntegralPoseRegressionHead(nn.Module):
def __init__(self,
in_channels,
num_joints,
feat_size,
loss_keypoint=None,
out_sigma=False,
debias=False,
train_cfg=None,
test_cfg=None):
super().__init__()
self.in_channels = in_channels
self.num_joints = num_joints
self.loss = build_loss(loss_keypoint)
self.train_cfg = {} if train_cfg is None else train_cfg
self.test_cfg = {} if test_cfg is None else test_cfg
self.out_sigma = out_sigma
self.conv = build_conv_layer(
dict(type='Conv2d'),
in_channels=in_channels,
out_channels=num_joints,
kernel_size=1,
stride=1,
padding=0)
self.size = feat_size
self.wx = torch.arange(0.0, 1.0 * self.size, 1).view([1, self.size]).repeat([self.size, 1]) / self.size
self.wy = torch.arange(0.0, 1.0 * self.size, 1).view([self.size, 1]).repeat([1, self.size]) / self.size
self.wx = nn.Parameter(self.wx, requires_grad=False)
self.wy = nn.Parameter(self.wy, requires_grad=False)
if out_sigma:
self.gap = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(self.in_channels, self.num_joints * 2)
if debias:
self.softmax_fc = nn.Linear(64, 64)
def forward(self, x):
"""Forward function."""
if isinstance(x, (list, tuple)):
assert len(x) == 1, ('DeepPoseRegressionHead only supports '
'single-level feature.')
x = x[0]
featmap = self.conv(x)
s = list(featmap.size())
featmap = featmap.view([s[0], s[1], s[2] * s[3]])
featmap = F.softmax(16 * featmap, dim=2)
featmap = featmap.view([s[0], s[1], s[2], s[3]])
scoremap_x = featmap.mul(self.wx)
scoremap_x = scoremap_x.view([s[0], s[1], s[2] * s[3]])
soft_argmax_x = torch.sum(scoremap_x, dim=2, keepdim=True)
scoremap_y = featmap.mul(self.wy)
scoremap_y = scoremap_y.view([s[0], s[1], s[2] * s[3]])
soft_argmax_y = torch.sum(scoremap_y, dim=2, keepdim=True)
output = torch.cat([soft_argmax_x, soft_argmax_y], dim=-1)
if self.out_sigma:
x = self.gap(x).reshape(x.size(0), -1)
pred_sigma = self.fc(x)
pred_sigma = pred_sigma.reshape(pred_sigma.size(0), self.num_joints, 2)
output = torch.cat([output, pred_sigma], dim=-1)
return output, featmap
我們引入IPR后實際輸出的特征與Heatmap方法輸出的特征形式類似,Heatmap方法有人造的概率分布即高斯熱圖,而在deeppose中引入IPR則是將期望作為坐標,并通過坐標GT直接監(jiān)督的,因此,只要期望接近GT,loss就會降低。這就帶來一個問題,我們通過期望獲得的預測坐標無法對概率分布進行約束。
如上圖所示,上下兩個分布的期望都是mean,但是分布卻是完全不同。RLE已經論證一個合理的概率分布是至關重要的,為了提高模型性能,對概率分布加以監(jiān)督是必要的。DSNT提出了利用JS散度將模型的概率分布向自制的高斯分布靠攏,這里有一個問題,高斯分布的方差只能通過經驗值設定,無法針對每個樣本自適應的給出,同時高斯分布也未必是最優(yōu)選擇。
@LOSSES.register_module()
class RLE_DSNTLoss(nn.Module):
"""RLE_DSNTLoss loss.
"""
def __init__(self,
use_target_weight=False,
size_average=True,
residual=True,
q_dis='laplace',
sigma=2.0):
super().__init__()
self.dsnt_loss = DSNTLoss(sigma=sigma, use_target_weight=use_target_weight)
self.rle_loss = RLELoss(use_target_weight=use_target_weight,
size_average=size_average,
residual=residual,
q_dis=q_dis)
self.use_target_weight = use_target_weight
def forward(self, output, heatmap, target, target_weight=None):
assert target_weight is not None
loss1 = self.dsnt_loss(heatmap, target, target_weight)
loss2 = self.rle_loss(output, target, target_weight)
loss = loss1 + loss2 # 這里權重可以調參
return loss
@LOSSES.register_module()
class DSNTLoss(nn.Module):
def __init__(self,
sigma,
use_target_weight=False,
size_average=True,
):
super(DSNTLoss, self).__init__()
self.use_target_weight = use_target_weight
self.sigma = sigma
self.size_average = size_average
def forward(self, heatmap, target, target_weight=None):
"""Forward function.
Note:
- batch_size: N
- num_keypoints: K
- dimension of keypoints: D (D=2 or D=3)
Args:
output (torch.Tensor[N, K, D*2]): Output regression,
including coords and sigmas.
target (torch.Tensor[N, K, D]): Target regression.
target_weight (torch.Tensor[N, K, D]):
Weights across different joint types.
"""
loss = dsntnn.js_reg_losses(heatmap, target, self.sigma)
if self.size_average:
loss /= len(loss)
return loss.sum()
從下表可以看出,引入IPR+DSNT后模型性能提升。
方法 | input size | AP50:95 | acc_pse | time |
---|---|---|---|---|
Deeppose | 192*256 | 41.3% | 65% | 2.5ms |
Heatmap | 192*256 | 67.5% | 93% | 60ms |
RLE | 192*256 | 67.3% | 90% | 2.5ms |
RLE+IPR+DSNT | 256*256 | 70.2% | 95% | 3.5ms |
5.Removing the Bias of Integral Pose Regression
我們引入IPR后,可以使用Softmax來計算期望獲得坐標值,但是,利用Softmax計算期望會引入誤差。因為Softmax有一個特性讓每一項值都非零。對于一個本身非常尖銳的分布,Softmax會將其軟化,變成一個漸變的分布。這個性質導致的結果是,最后計算得到的期望值會不準確。只有響應值足夠大,分布足夠尖銳的時候,期望值才接近Argmax結果,一旦響應值小,分布平緩,期望值會趨近于中心位置。 這種影響會隨著特征尺寸的變大而更劇烈。
Removing the Bias of Integral Pose Regression提出debias方法消除Softmax軟化產生的影響。具體而言,假設響應值是符合高斯分布的,我們可以根據響應最大值點兩倍的寬度,把特征圖劃分成四個區(qū)域:
我們知道一旦經過Softmax,原本都是0值的2、3、4象限區(qū)域瞬間就會被長長的尾巴填滿,而對于第1象限區(qū)域,由于響應值正處于區(qū)域的中央,因此不論響應值大小,該區(qū)域的估計期望值都會是準確的。
讓我們回到Softmax公式:
為了簡潔,我們先把分母部分用C來表示:
由于假設2、3、4區(qū)域的響應值都為0,因而分子部分計算出來為1,劃分區(qū)域后的Softmax結果可以表示成:
然后繼續(xù)按照Soft-Argmax的計算公式帶入,期望值的計算可以表示為:
即:第一區(qū)域的期望值,加上另外三個區(qū)域的期望值。已知2,3,4趨于 H ~ ( P ) = 1 / c \tilde{H}(P)=1/c H~(P)=1/c,因此這三個區(qū)域的期望值可以把1/c提出來,只剩下
而這里的求和,在幾何意義上等價于該區(qū)域的中心點坐標乘以該區(qū)域的面積,我給一個簡單的演示,對于[n, m]區(qū)間:
因而對于整塊特征圖的期望值,又可以看成四個區(qū)域中心點坐標的加權和:
由于四個區(qū)域的中心點存在對稱性,假設第一區(qū)域中心點坐標為 J 1 = ( x 0 , y 0 ) J_1=(x_0,y_0) J1?=(x0?,y0?),那么剩下三個區(qū)域中心點坐標為 J 2 = ( x 0 , y 0 + w / 2 ) , J 3 = ( x 0 + h / 2 , y 0 ) , J 4 = ( x 0 + h / 2 , y 0 + w / 2 ) J_2=(x_0,y_0+w/2),J_3=(x_0+h/2,y_0), J_4=(x_0+h/2,y_0+w/2) J2?=(x0?,y0?+w/2),J3?=(x0?+h/2,y0?),J4?=(x0?+h/2,y0?+w/2)
對應上面我們得出的1/c乘以中心點坐標乘以面積,就得到了每個加權值:
帶入上面的加權和公式(6),整張?zhí)卣鲌D的期望值可以表示為:
由于已知四個區(qū)域權重相加為1,所以有
w
1
=
1
?
w
2
?
w
3
?
w
4
w_1=1-w_2-w_3-w_4
w1?=1?w2??w3??w4?,因此整張?zhí)卣鲌D期望值化簡成如下形式:
由于
J
r
J^r
Jr值可以很容易通過對整張圖計算Soft-Argmax得到,因此對公式(9)移項就能得到準確的第一區(qū)域中心點坐標:
這一步就相當于將原本多余的長尾從期望值中減去了,對該公式我們還可以進一步分析,整張圖的期望估計值相當于第一區(qū)域期望值的一個偏移。文章來源:http://www.zghlxwxcb.cn/news/detail-479064.html
@HEADS.register_module()
class IntegralPoseRegressionHead(nn.Module):
def __init__(self,
in_channels,
num_joints,
feat_size,
loss_keypoint=None,
out_sigma=False,
debias=False,
train_cfg=None,
test_cfg=None):
super().__init__()
self.in_channels = in_channels
self.num_joints = num_joints
self.loss = build_loss(loss_keypoint)
self.train_cfg = {} if train_cfg is None else train_cfg
self.test_cfg = {} if test_cfg is None else test_cfg
self.out_sigma = out_sigma
self.debias = debias
self.conv = build_conv_layer(
dict(type='Conv2d'),
in_channels=in_channels,
out_channels=num_joints,
kernel_size=1,
stride=1,
padding=0)
self.size = feat_size
self.wx = torch.arange(0.0, 1.0 * self.size, 1).view([1, self.size]).repeat([self.size, 1]) / self.size
self.wy = torch.arange(0.0, 1.0 * self.size, 1).view([self.size, 1]).repeat([1, self.size]) / self.size
self.wx = nn.Parameter(self.wx, requires_grad=False)
self.wy = nn.Parameter(self.wy, requires_grad=False)
if out_sigma:
self.gap = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(self.in_channels, self.num_joints * 2)
if debias:
self.softmax_fc = nn.Linear(64, 64)
def forward(self, x):
"""Forward function."""
if isinstance(x, (list, tuple)):
assert len(x) == 1, ('DeepPoseRegressionHead only supports '
'single-level feature.')
x = x[0]
featmap = self.conv(x)
s = list(featmap.size())
featmap = featmap.view([s[0], s[1], s[2] * s[3]])
if self.debias:
mlp_x_norm = torch.norm(self.softmax_fc.weight, dim=-1)
norm_feat = torch.norm(featmap, dim=-1, keepdim=True)
featmap = self.softmax_fc(featmap)
featmap /= norm_feat
featmap /= mlp_x_norm.reshape(1, 1, -1)
featmap = F.softmax(16 * featmap, dim=2)
featmap = featmap.view([s[0], s[1], s[2], s[3]])
scoremap_x = featmap.mul(self.wx)
scoremap_x = scoremap_x.view([s[0], s[1], s[2] * s[3]])
soft_argmax_x = torch.sum(scoremap_x, dim=2, keepdim=True)
scoremap_y = featmap.mul(self.wy)
scoremap_y = scoremap_y.view([s[0], s[1], s[2] * s[3]])
soft_argmax_y = torch.sum(scoremap_y, dim=2, keepdim=True)
# output = torch.cat([soft_argmax_x, soft_argmax_y], dim=-1)
if self.debias:
C = featmap.reshape(s[0], s[1], s[2] * s[3]).exp().sum(dim=2).unsqueeze(dim=2)
soft_argmax_x = C / (C - 1) * (soft_argmax_x - 1 / (2 * C))
soft_argmax_y = C / (C - 1) * (soft_argmax_y - 1 / (2 * C))
output = torch.cat([soft_argmax_x, soft_argmax_y], dim=-1)
if self.out_sigma:
x = self.gap(x).reshape(x.size(0), -1)
pred_sigma = self.fc(x)
pred_sigma = pred_sigma.reshape(pred_sigma.size(0), self.num_joints, 2)
output = torch.cat([output, pred_sigma], dim=-1)
return output, featmap
方法 | input size | AP50:95 | acc_pse | time |
---|---|---|---|---|
Deeppose | 192*256 | 41.3% | 65% | 2.5ms |
Heatmap | 192*256 | 67.5% | 93% | 60ms |
RLE | 192*256 | 67.3% | 90% | 2.5ms |
RLE+IPR+DSNT | 256*256 | 70.2% | 95% | 3.5ms |
RLE+IPR+DSNT+debias | 256*256 | 71% | 95% | 3.5ms |
非常感謝知乎作者鏡子文章給予的指導,在這里借鑒了很多,有興趣的朋友可以查看知乎地址。文章來源地址http://www.zghlxwxcb.cn/news/detail-479064.html
到了這里,關于mmpose關鍵點(四):優(yōu)化關鍵點模型(原理與代碼講解,持續(xù)更新)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!