實驗背景
在前面的深度學習之用CelebA_Spoof數據集搭建一個活體檢測-模型搭建和訓練,我們基于CelebA_Spoof數據集構建了一個用SqueezeNe框架進行訓練的活體2D模型,采用了蒸餾法進行了一些簡單的工作。在前面提供的訓練參數中,主要用了以下幾個參數:
lr=0.001, step_size=10, gamma=0.1, alpha=0.5, T=3.0, epochs=300, log_interval=5, batch_size=128, save_interval=10, eval_interval=5
但是,效果并不是很好,得到的評估指標為:
{'Accuracy': 0.8841447074586869, 'Precision': 0.9865851252712566, 'Recall': 0.8468018456588917, 'F1': 0.9113647235700131, 'FPR': 0.027303754266211604, 'ROC_AUC': 0.9661331014932475, 'EER': 0.076691427424212, 'PR_AUC': 0.9862052738456543, 'AP': 0.9862056134292535}
對于一個好的活體檢測模型來說,各項指標都不是很好。對于這個指標,就需要進一步進行全面的分析了,如:預處理、訓練的各個參數的玄學調整,模型結構深度,蒸餾中的權重比等等之類。在各種折騰后都得不到比較好的改變,于是想在特征上進行改進,如果人工再加點特征試試,會是怎樣?這突發奇想就想到了:傅里葉變換。為什么用它,因為非活體的照片很多都是翻拍的,那么因為相機或者屏幕的閃爍,可能會出現一些條紋或者頻域上的特征,這些就有可能很好的區分這兩類圖片。為了提升模型對偽造攻擊的識別能力,我們嘗試在訓練過程中加入傅里葉變換作為輔助特征。
方法對比
基線模型訓練時候的訓練過程(無傅里葉變換)
直接是普通的蒸餾訓練過程,正常的損失計算。
# 傳統RGB圖像預處理
def _compute_loss(self, student_out, teacher_out, targets):current_T = max(1.0, self.args.T * (0.95 ** (self.current_epoch/10)))"""計算蒸餾損失"""# KL散度損失kl_loss = nn.KLDivLoss(reduction='batchmean')(torch.log_softmax(student_out/self.args.T, dim=1),torch.softmax(teacher_out/self.args.T, dim=1)) * (current_T ** 2)# 交叉熵損失ce_loss = self.criterion(student_out, targets)total_loss = self.args.alpha * kl_loss + (1 - self.args.alpha) * ce_lossreturn total_lossdef train_epoch(self, train_loader, epoch):try:"""完整訓練邏輯"""self.student.train()self.current_epoch = epochtotal_loss = 0.0correct = 0total = 0for batch_idx, (data, target) in enumerate(train_loader):data, target = data.to(self.device), target.to(self.device)if self.gpu == 0 and batch_idx % 200 == 0:current_lr = self.optimizer.param_groups[0]['lr']print(f'當前學習率: {current_lr:.6f}') # 添加這行打印學習率self.optimizer.zero_grad()# 前向傳播student_out = self.student(data)with torch.no_grad():teacher_out = self.teacher(data) # 計算損失loss = self._compute_loss(student_out, teacher_out, target)# 反向傳播loss.backward()self.optimizer.step()# 統計指標total_loss += loss.item()_, predicted = student_out.max(1)total += target.size(0)correct += predicted.eq(target).sum().item()# 主進程打印日志if self.gpu == 0 and batch_idx % 200 == 0:avg_loss = total_loss / (batch_idx + 1)acc = 100. * correct / totalprint(f'Epoch {epoch} Batch {batch_idx}/{len(train_loader)} 'f'Loss: {avg_loss:.4f} | Acc: {acc:.2f}%')self.scheduler.step()return {'loss':total_loss / len(train_loader),'accuracy': 100.0 * correct / total}except Exception as e:if "NCCL" in str(e):print(f"NCCL錯誤發生,嘗試恢復訓練...")torch.distributed.destroy_process_group()torch.distributed.init_process_group(backend='nccl')return {'loss': 0, 'accuracy': 0}else:print(f"訓練過程中發生錯誤: {str(e)}")raise e
改進模型(加入傅里葉變換)
加入的傅里葉變換該怎么加呢,我們只在訓練過程中加入,那么得到的特征中具有較好區分性就行,所以不需要將輸入圖像數據都進行傅里葉變換,這樣也防止在后續的推理過程中都需要進行傅里葉變換,增加無畏的動作和減少更多的特征內卷。
訓練過程中,正常的輸入圖像數據,正常的教師學生模型的特征求取,但是同時采用傅里葉變換對圖像數據進行預處理,用于后續的損失函數加入。
訓練參數為:
lr=0.001, step_size=10, gamma=0.9, alpha=0.5, T=3.0, epochs=300, log_interval=5, batch_size=128, save_interval=10, eval_interval=5
def _fourier_transform(self, x):x = torch.fft.fftshift(torch.fft.fft2(x, dim=(-2, -1))) # 中心化頻譜x = torch.abs(x)# 動態調整濾波區域h, w = x.shape[-2:]crow, ccol = h//2, w//2mask = torch.ones_like(x)mask[..., crow-10:crow+10, ccol-10:ccol+10] = 0.3 # 部分保留中心低頻return torch.log(1 + 10*x*mask) # 增強高頻特征
def _compute_loss(self, student_out, teacher_out, targets):current_T = max(1.0, self.args.T * (0.95 ** (self.current_epoch/10)))"""計算蒸餾損失"""# KL散度損失kl_loss = nn.KLDivLoss(reduction='batchmean')(torch.log_softmax(student_out/self.args.T, dim=1),torch.softmax(teacher_out/self.args.T, dim=1)) * (current_T ** 2)# 交叉熵損失ce_loss = self.criterion(student_out, targets)total_loss = self.args.alpha * kl_loss + (1 - self.args.alpha) * ce_loss#if self.gpu == 0: # 僅主進程打印# print(f"原始損失 - KL: {kl_loss.item():.4f} | CE: {ce_loss.item():.4f}")# 添加頻域分支損失if self.use_freq:base_weight = self.freq_weight # 基礎權重dynamic_weight = min(0.25, 0.15 + self.current_epoch*0.001)freq_loss = self.criterion(self.freq_pred, targets) * base_weight * dynamic_weighttotal_loss += freq_loss#if self.gpu == 0:# print(f"頻域分支損失: {freq_loss.item():.4f} (權重: {self.freq_weight})")return total_lossdef train_epoch(self, train_loader, epoch):try:"""完整訓練邏輯"""self.student.train()self.current_epoch = epochtotal_loss = 0.0correct = 0total = 0for batch_idx, (data, target) in enumerate(train_loader):data, target = data.to(self.device), target.to(self.device)if self.gpu == 0 and batch_idx % 200 == 0:current_lr = self.optimizer.param_groups[0]['lr']print(f'當前學習率: {current_lr:.6f}') # 添加這行打印學習率self.optimizer.zero_grad()# 前向傳播student_out = self.student(data)with torch.no_grad():teacher_out = self.teacher(data)if self.use_freq:# 頻域處理with torch.no_grad():freq_data = self._fourier_transform(data)self.freq_pred = self.freq_branch(freq_data).squeeze() # 計算損失loss = self._compute_loss(student_out, teacher_out, target)# 反向傳播loss.backward()self.optimizer.step()# 統計指標total_loss += loss.item()_, predicted = student_out.max(1)total += target.size(0)correct += predicted.eq(target).sum().item()# 主進程打印日志if self.gpu == 0 and batch_idx % 200 == 0:avg_loss = total_loss / (batch_idx + 1)acc = 100. * correct / totalprint(f'Epoch {epoch} Batch {batch_idx}/{len(train_loader)} 'f'Loss: {avg_loss:.4f} | Acc: {acc:.2f}%')self.scheduler.step()return {'loss':total_loss / len(train_loader),'accuracy': 100.0 * correct / total}except Exception as e:if "NCCL" in str(e):print(f"NCCL錯誤發生,嘗試恢復訓練...")torch.distributed.destroy_process_group()torch.distributed.init_process_group(backend='nccl')return {'loss': 0, 'accuracy': 0}else:print(f"訓練過程中發生錯誤: {str(e)}")raise e
性能指標對比
指標 | 基線模型 | 傅里葉增強模型 | 提升幅度 |
---|---|---|---|
Accuracy | 88.41% | 93.40% | +4.99% |
Precision | 98.66% | 98.52% | -0.14% |
Recall | 84.68% | 91.86% | +7.18% |
F1 Score | 91.14% | 95.08% | +3.94% |
ROC AUC | 96.61% | 97.83% | +1.22% |
EER | 7.67% | 5.84% | -1.83% |
關鍵發現
-
召回率顯著提升:傅里葉變換幫助模型更好地捕捉偽造痕跡,使召回率提高了7.18%
-
等錯誤率降低:EER從7.67%降至5.84%,表明系統整體性能更均衡
-
特征互補性:雖然單獨看頻域特征效果有限,但與空間特征結合產生了協同效應
實現建議
在本次實驗,我是保留了中心低頻,增強了高頻特征,當然也可以不這么干,畢竟有些低頻的信息也有用,需要多次驗證采取最好的。代碼中添加的過濾如下:
# 動態調整濾波區域h, w = x.shape[-2:]crow, ccol = h//2, w//2mask = torch.ones_like(x)mask[..., crow-10:crow+10, ccol-10:ccol+10] = 0.3 # 部分保留中心低頻return torch.log(1 + 10*x*mask) # 增強高頻特征
結論
傅里葉變換的引入使模型在保持高精確度的同時,顯著提升了召回能力。這只是在調整模型過程中的一點小改善,當然還有其他更好的方法,SqueezeNe的模型結構還是淺,如果沒有更多的限制,可以加深加大,這樣效果會更好。