⚠️這個方法還可以用在別的地方,比如要判別一個人不同年齡的照片是不是同一個人,這樣這里的yaw coefficient參數就是用來調整照片年齡的不同,而不是人臉角度的不同了!!!!!!!!!
Pose-Robust Face Recognition via Deep Residual Equivariant Mapping
Abstract
由於深度學習的出現,人臉識別獲得了非凡的成功。可是,很多當代的人臉檢測模型,對比於處理正面人臉,在處理側面人臉時仍表現得比較差。主要的原因是正面和側面訓練人臉數量的高度不平衡 —— 對比於側面訓練樣本,這里有着極其多的正面訓練樣本。除此之外,本質上來說,訓練一個對於大量姿勢變量來說是幾何不變的深度表征是非常困難的(即不同姿勢的人臉的輸入經過CNN網絡都能對應得到同個表征)。在該論文中,我們假設在正面和側面人臉中有一個固有的映射,並且最后,它們在深度表征空間中的差異都能夠通過一個等價映射連接。為了探究該映射方法,我們制定了一個新的Deep Residual EquivAriant Mapping (DREAM) 塊,其有着能夠通過適當地添加residuals到輸入的深度表征來將一個側面人臉表征轉換為一個規范的姿勢來簡化識別的能力。對於很多很強的深度網絡,如ResNet模型,DREAM塊都能夠增強它們側面人臉識別的性能,不需要故意增強側面人臉的訓練數據。該塊很容易使用、是輕量級的並有着可忽略的計算開銷
1.大概說明

通過上面的圖可以了解該DREAM的實現理論就是將人臉輸入到人臉識別CNN模型中,得到人臉的一個特征向量,可見正面人臉的特征向量在一個空間中,而側臉人臉的特征向量在別的空間中,DREAM的目的就是希望經過一定地變換,能夠將側面人臉的特征向量變換到與正面人臉相同的特征向量空間中
為了能夠可視化變換后的效果,作者在訓練模型同時訓練一個GANs網絡,使得能夠從特征向量重構人臉圖像,因此從最下面一層的圖像我們可以看見,變換前的側面人臉重構后為右邊的樣子,經過DREAM變換后重構的人臉就是一個正面人臉了
其他的論文都是在圖像層面進行正面-側面的轉換,而該論文是在特征層面進行的正面-側面的轉換
2.公式說明
該DREAM的公式實現為:
假設g為側面到正面的變換,在圖像層面的變換為gX;Φ表示為人臉識別得到特征向量的CNN網絡,因此Φ(X)表示的就是特征向量,Mg則表示在特征層面的側面到正面的變換。所以我們希望得到的效果就是在圖像層面上進行變換后經過CNN網絡得到的特征向量與先經過CNN網絡得到特征向量,然后再在特征層面進行變換得到的特征向量是相似的:![]()
在該論文中實現右邊,將其定義為更簡單的形式:
即Xp表示側面人臉輸入,Xf表示正面人臉輸入,都是同一個人。
- 先將側面人臉Xp輸入CNN網絡得到特征向量Φ(Xp)
- 然后拉出另一個分支,使用DREAM塊的residual函數對其進行操作R(Φ(Xp))
- 接着這里有一個參數名為yaw coefficient —— Y(X),這個參數是通過對每張側面人臉得到的21個landmarks進行計算得到的。將這個參數和上面的分支得到的R(Φ(Xp))相乘得到Y(X)*R(Φ(Xp))
- 然后將這個相乘得到的結果與Φ(Xp)相加即得到了變換后的特征向量了
如下面的公式所示:

這里yaw coefficient —— Y(X)的范圍為[0,1],其作為分支residuals的soft gate,即保證當人臉是正面時,Y(X)=0;否則當人臉慢慢地從正面轉向完全地側面時,Y(X)的值就會逐漸地從0增長到1。因此在完全的側面姿勢時,residuals的大小是最大的。沒有這個參數,residuals—— R(Φ(X))將盲目地被添加到任意姿勢的輸入人臉中,影響最后的人臉識別性能
yaw coefficient —— Y(X)該參數是通過計算head rotation estimator得到的,如下面所示:

使用論文[37](即Appearance-based gaze estimation in the wild)中的算法來估計head rotation。具體來說采用的是論文[28](即視線檢測Learning-by-synthesis for appearance-based 3d gaze estimation)中的人臉模型和頭部坐標體系定義。因此參數yaw coefficient —— Y(X)也是通過一個模型計算得到的。不過該論文中有一點點不同是,其輸入不是3D人臉的6個點(即4個眼角點和2個嘴角點),而是使用了21個landmarks,因為這樣的到的性能更好
然后我們通過使用EPnP算法[16]估計初始解來擬合模型,並通過非線性優化進一步細化姿勢。最后一步的非線性優化使得其范圍在[0,1]間,公式為:
σ表示sigmoid函數,該式子的意思是當側面人臉的旋轉角度大於45度時,(4/Π)*y-1就大於0,那么在sigmoid函數中就會快速趨近於1,越大越接近1。yaw角度為[-90o, 90o]
⚠️yaw的計算方法該論文中並沒有給出,所以可能需要自己去查看相應論文理解
3.損失函數
該模型的訓練損失函數為:

這里的ΘR表示R(.)的參數。上面的說明知道參數yaw coefficient —— Y(X)也是通過網絡得到的結果,但是我們並不要訓練這個網絡,所以會將yaw網絡的參數固定
使用SGD隨機梯度下降方法,損失計算的是側面人臉變換后的特征向量和同一個人正面人臉的特征向量的歐氏距離
4.訓練
DREAM的使用方式有三種:
1)stitching
即對於給定的基本網絡,我們只需將DREAM塊縫合到基本網絡的最終特征層上,而不需要改變原始模型的任何已知參數。這種方法最簡單
2)End-to-End
該提出的輕量級塊也可以以端到端的方式與stem CNN一起訓練。給定一個簡單的基本網絡,插入DREAM塊,並直接對新網絡的所有參數進行隨機初始化后進行訓練。如果stem CNN不是普通的,而是之前訓練過的,我們可以先微調stem CNN,同時使用現有的人臉識別loss(例如,verification loss,identification loss,或者兩者都有),以端到端的方式訓練DREAM 塊。我們將該策略命名為“end2end”。使用這種策略,在側臉上的表現不能得到保證,因為DREAM塊可能無法區分正面和側臉的情況,因為沒有特定的正面-側臉數據對用於訓練該塊。
3)End-to-end+retrain
我們一起訓練stem CNN和DREAM塊,然后分別使用用正臉-側臉對訓練DREAM塊。這種方法效果最好
5.對應代碼實現:
詳細可見:https://github.com/penincillin/DREAM
DREAM塊是如何加入模型的,src/end2end/ResNet.py:
class ResNet(nn.Module): def __init__(self, block, layers, num_classes=1000, end2end=True): self.inplanes = 64 self.end2end = end2end super(ResNet, self).__init__() self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False) self.bn1 = nn.BatchNorm2d(64) self.relu = nn.ReLU(inplace=True) self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) self.layer1 = self._make_layer(block, 64, layers[0]) self.layer2 = self._make_layer(block, 128, layers[1], stride=2) self.layer3 = self._make_layer(block, 256, layers[2], stride=2) self.layer4 = self._make_layer(block, 512, layers[3], stride=2) self.avgpool = nn.AvgPool2d(7) self.feature = nn.Linear(512 * block.expansion, 256) if self.end2end: self.fc1 = nn.Linear(256, 256) self.fc2 = nn.Linear(256, 256) self.fc = nn.Linear(256, num_classes) for m in self.modules(): if isinstance(m, nn.Conv2d): n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels m.weight.data.normal_(0, math.sqrt(2. / n)) elif isinstance(m, nn.BatchNorm2d): m.weight.data.fill_(1) m.bias.data.zero_() def _make_layer(self, block, planes, blocks, stride=1): downsample = None if stride != 1 or self.inplanes != planes * block.expansion: downsample = nn.Sequential( nn.Conv2d(self.inplanes, planes * block.expansion, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(planes * block.expansion), ) layers = [] layers.append(block(self.inplanes, planes, stride, downsample)) self.inplanes = planes * block.expansion for i in range(1, blocks): layers.append(block(self.inplanes, planes)) return nn.Sequential(*layers) def forward(self, x, yaw): x = self.conv1(x) x = self.bn1(x) x = self.relu(x) x = self.maxpool(x) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) x = self.avgpool(x) x = x.view(x.size(0), -1) mid_feature = self.feature(x) # code above is the original arch of resnet if self.end2end: raw_feature = self.fc1(mid_feature) raw_feature = self.relu(raw_feature) raw_feature = self.fc2(raw_feature) raw_feature = self.relu(raw_feature) yaw = yaw.view(yaw.size(0),1) yaw = yaw.expand_as(raw_feature) feature = yaw * raw_feature + mid_feature else: feature = mid_feature feature = F.dropout(feature, p=0.7, training=self.training) pred_score = self.fc(feature) return pred_score
這個代碼的作者已經將圖片的yaw參數都計算出來了,所以在訓練中該參數就作為一個常量輸入了
讀取該值的函數src/end2end/selfDefine.py:
def load_imgs(img_dir, image_list_file, label_file): imgs = list() max_label = 0 with open(image_list_file, 'r') as imf: with open(label_file, 'r') as laf: record = laf.readline().strip().split() #label_file第一行的數據記錄了圖片的總量total_num,以及種類數label_num total_num, label_num = int(record[0]), int(record[1]) for line in imf: img_path = os.path.join(img_dir, line.strip())#.strip()移除了頭尾的空格 record = laf.readline().strip().split() label,yaw = int(record[0]), float(record[1]) #label_file后面每一行記錄的分別是該圖片的標簽類別和yaw系數 max_label = max(max_label, label) #查看最后得到的最大類別標簽是不是總類別數-1 imgs.append((img_path, label, yaw)) assert(total_num == len(imgs)) assert(label_num == max_label+1) return imgs, max_label
這個作者得到yaw運行的是src/test_process_align這個二進制執行文件得到的
