R-Drop論文復現與理論講解

華為雲開發者聯盟發表於2023-03-07
摘要:基於 Dropout 的這種特殊方式對網路帶來的隨機性,研究員們提出了 R-Drop 來進一步對(子模型)網路的輸出預測進行了正則約束。

本文分享自華為雲社群《R-Drop論文復現與理論講解》,作者: 李長安。

R-Drop: Regularized Dropout for Neural Networks

由於深度神經網路非常容易過擬合,因此 Dropout 方法採用了隨機丟棄每層的部分神經元,以此來避免在訓練過程中的過擬合問題。正是因為每次隨機丟棄部分神經元,導致每次丟棄後產生的子模型都不一樣,所以 Dropout 的操作一定程度上使得訓練後的模型是一種多個子模型的組合約束。基於 Dropout 的這種特殊方式對網路帶來的隨機性,研究員們提出了 R-Drop 來進一步對(子模型)網路的輸出預測進行了正則約束。論文透過實驗得出一種改進的正則化方法R-dropout,簡單來說,它透過使用若干次(論文中使用了兩次)dropout,定義新的損失函式。實驗結果表明,儘管結構非常簡單,但是卻能很好的防止模型過擬合,進一步提高模型的正確率。模型主體如下圖所示。

R-Drop論文復現與理論講解

論文貢獻

由於深度神經網路非常容易過擬合,因此 Dropout 方法採用了隨機丟棄每層的部分神經元,以此來避免在訓練過程中的過擬合問題。正是因為每次隨機丟棄部分神經元,導致每次丟棄後產生的子模型都不一樣,所以 Dropout 的操作一定程度上使得訓練後的模型是一種多個子模型的組合約束。基於 Dropout 的這種特殊方式對網路帶來的隨機性,研究員們提出了 R-Drop 來進一步對(子模型)網路的輸出預測進行了正則約束。

實現思路

與傳統作用於神經元(Dropout)或者模型引數(DropConnect)上的約束方法不同,R-Drop 作用於模型的輸出層,彌補了 Dropout 在訓練和測試時的不一致性。簡單來說就是在每個 mini-batch 中,每個資料樣本過兩次帶有 Dropout 的同一個模型,R-Drop 再使用 KL-divergence 約束兩次的輸出一致。既約束了由於 Dropout 帶來的兩個隨機子模型的輸出一致性。

R-Drop論文復現與理論講解

論文公式

模型的訓練目標包含兩個部分,一個是兩次輸出之間的KL散度,如下:

R-Drop論文復現與理論講解

另一個是模型自有的損失函式交叉熵,如下:

R-Drop論文復現與理論講解

總損失函式為:

R-Drop論文復現與理論講解

程式碼實現

與傳統的訓練方法相比,R- Drop 只是簡單增加了一個 KL-divergence 損失函式項,並沒有其他任何改動。其PaddlePaddle版本對應的程式碼實現如下所示。

  • 散度損失

交叉熵=熵+相對熵(KL散度) 其與交叉熵的關係如下:

R-Drop論文復現與理論講解

程式碼實現示意

import paddle.nn.functional as F
# define your task model, which outputs the classifier logits
model = TaskModel()
def compute_kl_loss(self, p, q, pad_mask=None):
 p_loss = F.kl_div(F.log_softmax(p, axis=-1), F.softmax(q, axis=-1), reduction='none')
 q_loss = F.kl_div(F.log_softmax(q, axis=-1), F.softmax(p, axis=-1), reduction='none')
    # pad_mask is for seq-level tasks
 if pad_mask is not None:
 p_loss.masked_fill_(pad_mask, 0.)
 q_loss.masked_fill_(pad_mask, 0.)
    # You can choose whether to use function "sum" and "mean" depending on your task
 p_loss = p_loss.sum()
 q_loss = q_loss.sum()
    loss = (p_loss + q_loss) / 2
 return loss
# keep dropout and forward twice
logits = model(x)
logits2 = model(x)
# cross entropy loss for classifier
ce_loss = 0.5 * (cross_entropy_loss(logits, label) + cross_entropy_loss(logits2, label))
kl_loss = compute_kl_loss(logits, logits2)
# 論文中對於CV任務的超引數
α = 0.6
# carefully choose hyper-parameters
loss = ce_loss + α * kl_loss

程式碼實現實戰

專案說明

本次實驗以白菜生長的四個週期為例,進行生長情況識別實驗。資料來自於訊飛的比賽。資料展示如下:發芽期、幼苗期、蓮座期、結球期。

R-Drop論文復現與理論講解R-Drop論文復現與理論講解R-Drop論文復現與理論講解R-Drop論文復現與理論講解
!cd 'data/data107306' && unzip -q img.zip
!cd 'data/data106868' && unzip -q pdweights.zip

 

# 匯入所需要的庫
from sklearn.utils import shuffle
import os
import pandas as pd
import numpy as np
from PIL import Image
import paddle
import paddle.nn as nn
from paddle.io import Dataset
import paddle.vision.transforms as T
import paddle.nn.functional as F
from paddle.metric import Accuracy
import warnings
warnings.filterwarnings("ignore")
# 讀取資料
train_images = pd.read_csv('data/data107306/img/df_all.csv')
train_images = shuffle(train_images)
# 劃分訓練集和校驗集
all_size = len(train_images)
# print(all_size)
train_size = int(all_size * 0.9)
train_image_list = train_images[:train_size]
val_image_list = train_images[train_size:]
train_image_path_list = train_image_list['image'].values
label_list = train_image_list['label'].values
train_label_list = paddle.to_tensor(label_list, dtype='int64')
val_image_path_list = val_image_list['image'].values
val_label_list1 = val_image_list['label'].values
val_label_list = paddle.to_tensor(val_label_list1, dtype='int64')
# 定義資料預處理
data_transforms = T.Compose([
 T.Resize(size=(256, 256)),
 T.Transpose(), # HWC -> CHW
 T.Normalize(
        mean = [0, 0, 0],
        std = [255, 255, 255],
 to_rgb=True) 
])
# 構建Dataset
class MyDataset(paddle.io.Dataset):
 """
 步驟一:繼承paddle.io.Dataset類
    """
 def __init__(self, train_img_list, val_img_list,train_label_list,val_label_list, mode='train'):
 """
 步驟二:實現建構函式,定義資料讀取方式,劃分訓練和測試資料集
        """
 super(MyDataset, self).__init__()
 self.img = []
 self.label = []
 self.valimg = []
 self.vallabel = []
 # 藉助pandas讀csv的庫
 self.train_images = train_img_list
 self.test_images = val_img_list
 self.train_label = train_label_list
 self.test_label = val_label_list
 # self.mode = mode
 if mode == 'train':
 # 讀train_images的資料
 for img,la in zip(self.train_images, self.train_label):
 self.img.append('data/data107306/img/imgV/'+img)
 self.label.append(la)
 else :
 # 讀test_images的資料
 for img,la in zip(self.test_images, self.test_label):
 self.img.append('data/data107306/img/imgV/'+img)
 self.label.append(la)
 def load_img(self, image_path):
 # 實際使用時使用Pillow相關庫進行圖片讀取即可,這裡我們對資料先做個模擬
        image = Image.open(image_path).convert('RGB')
        image = np.array(image).astype('float32')
 return image
 def __getitem__(self, index):
 """
 步驟三:實現__getitem__方法,定義指定index時如何獲取資料,並返回單條資料(訓練資料,對應的標籤)
        """
        image = self.load_img(self.img[index])
        label = self.label[index]
 return data_transforms(image), label
 def __len__(self):
 """
 步驟四:實現__len__方法,返回資料集總數目
        """
 return len(self.img)
#train_loader
train_dataset = MyDataset(train_img_list=train_image_path_list, val_img_list=val_image_path_list, train_label_list=train_label_list, val_label_list=val_label_list, mode='train')
train_loader = paddle.io.DataLoader(train_dataset, places=paddle.CPUPlace(), batch_size=8, shuffle=True, num_workers=0)
#val_loader
val_dataset = MyDataset(train_img_list=train_image_path_list, val_img_list=val_image_path_list, train_label_list=train_label_list, val_label_list=val_label_list, mode='test')
val_loader = paddle.io.DataLoader(val_dataset, places=paddle.CPUPlace(), batch_size=8, shuffle=True, num_workers=0)
from work.senet154 import SE_ResNeXt50_vd_32x4d
from work.res2net import Res2Net50_vd_26w_4s
from work.se_resnet import SE_ResNet50_vd
# 模型封裝
# model_re2 = SE_ResNeXt50_vd_32x4d(class_num=4)
model_re2 = Res2Net50_vd_26w_4s(class_dim=4)
model_ss = SE_ResNet50_vd(class_num=4)
model_ss.train()
model_re2.train()
epochs = 2
optim1 = paddle.optimizer.Adam(learning_rate=3e-4, parameters=model_re2.parameters())
optim2 = paddle.optimizer.Adam(learning_rate=3e-4, parameters=model_ss.parameters())
import paddle.nn.functional as F
def compute_kl_loss(p, q, pad_mask=None):
 p_loss = F.kl_div(F.log_softmax(p, axis=-1), F.softmax(q, axis=-1), reduction='none')
 q_loss = F.kl_div(F.log_softmax(q, axis=-1), F.softmax(p, axis=-1), reduction='none')
 # pad_mask is for seq-level tasks
 if pad_mask is not None:
 p_loss.masked_fill_(pad_mask, 0.)
 q_loss.masked_fill_(pad_mask, 0.)
 # You can choose whether to use function "sum" and "mean" depending on your task
 p_loss = p_loss.sum()
 q_loss = q_loss.sum()
    loss = (p_loss + q_loss) / 2
 return loss
# 用Adam作為最佳化函式
for epoch in range(epochs):
 for batch_id, data in enumerate(train_loader()):
 x_data = data[0]
 y_data = data[1]
        predicts1 = model_re2(x_data)
        predicts2 = model_ss(x_data)
        loss1 = F.cross_entropy(predicts1, y_data, soft_label=False)
        loss2 = F.cross_entropy(predicts2, y_data, soft_label=False)
 # cross entropy loss for classifier
 ce_loss = 0.5 * (loss1 + loss2)
 kl_loss = compute_kl_loss(predicts1, predicts2)
 # 論文中對於CV任務的超引數
        α = 0.6
 # carefully choose hyper-parameters
        loss = ce_loss + α * kl_loss
 # 計算損失
        acc1 = paddle.metric.accuracy(predicts1, y_data)
        acc2 = paddle.metric.accuracy(predicts2, y_data)
 loss.backward()
 if batch_id % 50 == 0:
 print("epoch: {}, batch_id: {}, loss1 is: {}".format(epoch, batch_id, loss.numpy()))
        optim1.step()
        optim1.clear_grad()
        optim2.step()
        optim2.clear_grad()

總結

本文介紹了R-Drop,它將“Dropout兩次”的思想用到了有監督任務中,每個實驗結果幾乎都取得了明顯的提升,並以白菜生長情況識別為例對R-Drop進行了實戰。

R-Drop論文的實現思路實際上非常簡單,在論文中,作者對CV以及NLP兩大任務進行了實驗,但是幾乎用的都是Transformer的模型,深度神經網路是深度學習的基礎,但其在訓練模型時會出現過擬合的問題,而簡單易用的 Dropout 正則化技術可以防止這種問題的發生。然而 Dropout 的操作在一定程度上會使得訓練後的模型成為一種多個子模型的組合約束。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章