一、賽題背景
在NLP任務中,經常會出現Multi-Task Learning(多任務學習)這一問題。多任務學習是一種聯合學習,多個任務並行學習,結果相互影響。在實際問題中,就是將多個學習任務融合到一個模型中完成。不同的任務會關注到不同的文本分析特征,將多任務聯合起來有利於進行模型泛化,緩解深度學習模型容易過擬合的現象。
多任務學習的出發點是多種多樣的:
(1)從生物學來看,我們將多任務學習視為對人類學習的一種模擬。為了學習一個新的任務,我們通常會使用學習相關任務中所獲得的知識。例如,嬰兒先學會識別臉,然后將這種知識用來識別其他物體。
(2)從教學法的角度來看,我們首先學習的任務是那些能夠幫助我們掌握更復雜技術的技能。這一點對於學習武術和編程來講都是非常正確的方法。具一個脫離大眾認知的例子,電影Karate Kid中Miyagi先生教會學空手道的小孩磨光地板以及為汽車打蠟這些表明上沒關系的任務。然而,結果表明正是這些無關緊要的任務使得他具備了學習空手道的相關的技能。
(3)從機器學習的角度來看,我們將多任務學習視為一種歸約遷移(inductive transfer)。歸約遷移(inductive transfer)通過引入歸約偏置(inductive bias)來改進模型,使得模型更傾向於某些假設。舉例來說,常見的一種歸約偏置(Inductive bias)是L1正則化,它使得模型更偏向於那些稀疏的解。在多任務學習場景中,歸約偏置(Inductive bias)是由輔助任務來提供的,這會導致模型更傾向於那些可以同時解釋多個任務的解。接下來我們會看到這樣做會使得模型的泛化性能更好。
深度學習中有兩種多任務學習模式:參數硬共享機制與參數軟共享機制。本文基礎參數硬共享機制構建算法模型,並從軟件工程的角度構建代碼,完成算法的設計方案與落地實施。本次算法是為了完成天池比賽提出的,比賽詳細內容請點擊這里。我們先對比賽內容做一個簡單的介紹:
這次的NLP比賽有三個任務,分別是預測文本是否相關、預測新聞文本分類與情感分析。本次比賽只能使用單模型(單模型的定義:一個任務只能有一個預測函數,所有任務只能使用同一個bert,在計算圖中只能有一個bert)完成任務,不能集成多個模型進行預測。即不能使用不同的模型提取文本特征。在此基礎上,我們實現基於參數硬共享機制的算法模型,並結合軟件工程的思想構建代碼,
二、算法設計方案與結構特點
算法最終要落地到具體的代碼實現,而在實現過程中就會遇到各種各樣的問題。我們的算法模型可以分為兩個層,數據層與算法層。數據層向算法層提供接口調取數據,而算法層經過訓練與優化產生最終的預測結果。所以我們的設計方案是沒有界面層的三層架構,或者說這樣可以稱作兩層架構。算法層是業務邏輯層而數據層是數據訪問層,將數據庫替換為文件讀取,也不妨是一種返璞歸真。在兩個層的內部,我們使用建造者模式來完成設計。在數據層,由於有三個分類任務,我們需要從三個文件中讀取數據,不同文件的數據格式是不一樣的,所以我們需要對三個文件做單獨處理,這就會產生三個子層。而算法層需要我們提供格式一直的數據,所以我們在數據層需要對這些數據進行整理,將三個部分統一為一個整體,再通過接口傳遞給算法層。在算法層,又可以分為三個子層,分別是bert、pool與predict,分別完成特征提取、特征池化與分類預測的任務。在算法層我們將分為三個子層去實現,每個子層各自獨立實現,再將三個子層結合到一起形成一個整體模型,方便torch框架對模型進行反向傳播與優化。在算法層中,每個子層都有不同的實現方案,比如bert可以利用不同的模型實現,不同模型的參數設定與輸出不一定完全相同;pool有max、mean、attention等不同的實現方法,這需要不停地調整優化才能得出最終的模型。這要求算法層各個子層相互獨立,三個子層組成的算法鏈完成訓練與預測。而在調用中,只會看到一個整理的模型。除了最主要的數據層與算法層,系統中還有日志記錄、配置文件讀寫、參數解析、模型檢查點記錄等輔助功能,這些功能采取單例的模式,在整個系統中各自只擁有一個實例。
三、算法接口API及視圖
我們分為幾個類來介紹API接口:
(1)Logger類,繼承logging,提供info、debug、warning等接口記錄日志。
(2)Config類,提供讀寫配置文件、配置參數的檢查接口。
(3)CheckpointManager類:提供記錄模型檢查點接口。
(4)JointDataset類:提供數據讀取、batch抽取接口。
(5)Ocnli、Tnews、Ocemotion類:提供三個數據集的讀取、解析與填充接口。
(6)NLPModel類:提供模型構建、模型訓練、模型預測接口。
算法依賴視圖:
算法執行流程圖:
四、算法核心數據結構設計與源代碼的目錄文件結構
為了能夠最大化利用torch帶來的便利,我們在數據層繼承了torch中的Dataset類,並實現了__len__()與__getitem__()方法。這樣我們就能夠繼續利用torch的DataLoader類,能夠自動產生隨機抽取的batch進行算法訓練。在batch中,每一條數據由一個dict組成,dict的具體內容如下所示:
{
'id':數據ID,
'input_ids':AutoTokenizer后的向量,
'token_type_ids':標明input_ids中padding的部分,與input_ids長度一致,0表示為paddnig,1表示為原始數據,
'length':長度,
'target':標簽,
'task_type_id':任務類型
}
源代碼的文件結構目錄如下:
五、算法運行環境和技術選型說明
Python:3.8.5
torch: 1.7.1+cu110
cuda: 11.0
tensorboard:2.4.0
tensorflow: 2.3.1
transformers: 4.0.1
硬件環境:建議使用NVIDIA2070以上顯卡,2070上運行大概需要一個小時;
操作系統:win10/linux皆可,只要版本較新,可以支持cuda等。
運行環境算是標配沒有可以選擇的余地,具體的bert模型與pool方法選擇還在嘗試尋找最優解。
六、舉例說明算法運行流程
我們有三個數據集:OCEMOTION_train1128.csv、OCNLI_train1128.csv與TNEWS_train1128.csv,分別有35693、55386與63359條數據。我們簡單地從三個數據集中各抽取4條數據作為我們的訓練數據來舉例說明我們的運行流程。因為OCNLI_train1128.csv數據是對比兩句話的相關程度,比另外兩個數據集多一句話,所以我們把12條數據AutoTokenizer后,全部padding到統一的長度,即OCNLI_train1128.csv中最長的長度,padding的數字為0,並且使用一個等長的向量標注哪些是原始數據哪些是padding的數據。在完成數據處理之后,將數據組成一個batch交給算法層的模型去處理。首先,bert層會將每一條輸入的數據轉化為一個768維的特征向量交給pool層。pool層采取self-attention機制,先產生三個12*768維的query、key與value矩陣。query是將batch標簽通過一個nn產生,key與value是batch特征通過一個nn產生。然后將12*768維的矩陣升維成12*8*96的矩陣再轉化為8*12*96的矩陣作為8個MultiHeadAttentionHeads。接着,基於self-attention機制,通過計算query與key的乘積計算value作為該條數據的特征,交給predict層進行預測。三個任務有各自的predict層,最后的loss通過計算平均loss再進行反向傳播。在訓練中,學習率通過warm-up的方法進行調整,參數會進行AdamW優化。