【人人都能學得會的NLP - 文字分類篇 03】長文字多標籤分類分類如何做?
NLP Github 專案:
-
NLP 專案實踐:fasterai/nlp-project-practice
介紹:該倉庫圍繞著 NLP 任務模型的設計、訓練、最佳化、部署和應用,分享大模型演算法工程師的日常工作和實戰經驗
-
AI 藏經閣:https://gitee.com/fasterai/ai-e-book
介紹:該倉庫主要分享了數百本 AI 領域電子書
-
AI 演算法面經:fasterai/nlp-interview-handbook#面經
介紹:該倉庫一網打盡網際網路大廠NLP演算法面經,演算法求職必備神器
-
NLP 劍指Offer:https://gitee.com/fasterai/nlp-interview-handbook
介紹:該倉庫彙總了 NLP 演算法工程師高頻面題
1、引言
文字分類任務在二分類、多分類等簡單場景下的應用不勝列舉,當探討其多標籤分類、長文字分類、模型壓縮加速等複雜場景的應用時,傳統的文字策略出現了不盡匹配的情況,亟需進行最佳化迭代,因此,我們在本文帶來複雜場景下文字分類任務的介紹。
2、多標籤任務
任務簡介:
由前文文字分類(一)分類任務與模型介紹的文章我們能夠從定義層面釐清多分類和多標籤任務的區別:
- 多分類任務中一條資料只有一個label,但是label有可能存在多個值。
- 多標籤分類任務指的是一條資料可能有一個或者多個label。
本節,我們重點分析二者模型架構層面存在的差別:
演算法架構:
(1)模型輸入輸出
本節仍以新聞分類為例來進行兩種任務的探討
模型輸入是新聞文字,如下所示:
- “今天大盤漲了3%,地產、傳媒板塊領漲” [金融]
- “汪峰今天釋出了新歌,娛樂圈又將有大事發生”[娛樂、音樂]
假設新聞文字只有[金融、體育、娛樂、音樂]四類,那模型輸出便採用one-hot思路對[金融、體育、娛樂、音樂]進行有序排列,二者區別如下表所示:
(2)模型細節闡述
- 關於多分類任務,模型只需要在輸出層階段執行一次分類器判斷,此時輸出層的啟用函式是softmax,這種輸出是一種基於分佈的形式,判斷哪一類的可能性最大;
- 關於多標籤任務,我們當然也可以採取多分類任務的思路,對每個label進行執行二分類判斷,以上面新聞分類為例,就需要訓練4個二分類器,這樣子不僅費時費力,也在一定程度上損壞了label之間的依賴關係。所以,我們一般採用下面的思路:將傳統多分類任務中的輸出層的softmax啟用函式變換為sigmoid啟用函式,對每個節點的值進行一次啟用,對單個節點執行0-1判斷。
同樣,根據上述任務實現思路,可以將損失函式進行調整,由多分類任務的多類別交叉熵損失函式(Categorical_crossentropy)調整為更適合多標籤任務的二分類交叉熵損失函式(Binary_crossentropy)
二者區別如下表所示:
在文字分類的複雜場景中,樣本總會存在多標籤情況,本節主要對多類別和多標籤任務的區別進行釐清,方便未來應用。
3、長文字分類
諸如BERT等各種預訓練模型目前已經廣泛應用於文字分類任務,但是模型仍存在一定的侷限性,即它對於輸入文字的最大長度有一定的限制,除去[cls]、[sep]標籤外,文字最多隻能再輸入510個token(下文統一把[cls]、[sep]也算作token,即512),但是現實場景中,長於512個token的文字比比皆是,那麼如何實現預訓練模型在這些長文字分類任務中的應用呢?
實現思路:
首先對於長文字分類,有兩個思路,
- 第一個從資料層面進行解決,即改造我們的文字,使之符合模型的要求;
- 第二個,從模型層面進行解決,即迭代模型,使之能夠容納更長的文字。
資料層面一般有如下做法:
- 截斷法:前或者後截斷,使文字滿足512個字以內
- 分段法:分為多個512個字的段
- 壓縮法:裁剪無意義的句子
模型層面則有如下模型:
Transformer-XL、Longformer等
下面分別對它們進行詳細介紹:
首先對資料層面的各個方法進行分析
(1)截斷法
截斷法主要採取如下方式:
- 頭截斷, 只保留最前面N(如512)個字;
- 尾截斷, 只保留最後面N個字;
- 頭+尾截斷, 開頭結尾各保留一部分;
截斷法的特點:
- 儘管要求最大長度是512個token, 但去除[cls]、[sep]後, 實際是510個token;
- 選擇頭截斷、還是尾截斷、還是兩者結合,主要看資料的關鍵資訊分佈;
- 截斷法適合大量幾百字的文字, 如果文字幾千字, 粗暴截斷會丟失重要資訊;
(2)分段法
分段法主要採取如下方式:
- 將長文字依次劃分為n個不超過512字的段(為避免語義丟失,最好考慮斷句);
- 針對n個段分別進行BERT編碼;
- 將n段經過BERT後的[CLS]向量進行max-pooling或mean-pooling;
- 然後再接一個全連線層做分類;
分段法特點:
- 考慮到全域性資訊, 相比截斷法, 對幾千字的長文字效果較好;
- 效能較差, 每個段都要encode一次, 文字越長,速度越慢;
- 段落之間聯絡會丟失, 易出現badcase;
(3)壓縮法
壓縮法主要採取如下方式:
其核心是裁減掉一些無意義的句子,例如:
- 一些文章開頭或結尾有一些無用“套路話術”, 這些可以刪除掉;
- 去除url;
- 句子篩選,只保留最重要的N個句子,如:計算句子和標題的相似度;
接著分析模型層面的迭代最佳化:
(1)transformer-xl模型
當資料過長時,如果使用截斷法,它沒有考慮句子的自然邊界,而是根據固定的長度來劃分序列,導致分割出來的文字在語義上是不完整的;如果使用分段法,每個句子之間獨立訓練,不同的token之間最長的依賴關係,就取決於句子的長度。
- 模型介紹
transformer-xl提出了一個狀態複用的塊級別迴圈用以解決長序列問題,雖然這個模型的提出主要是為了解決文字生成任務,但我們可以參考其解決長序列問題的思路。
- 塊級別迴圈訓練階段介紹
- 依然文字是分塊(句子)輸入, 但在計算當前塊的輸出時, 會快取並利用上一個segment中所有layer的隱向量序列
- 其中,所有隱向量序列只參與前向計算,不再進行反向傳播。
(2)Longformer模型
注意力機制能夠快速便捷地從整個文字序列中捕獲重要資訊。然而傳統的注意力機制的時空複雜度與文字的序列長度呈平方的關係,這在很大程度上限制了模型的輸入不能太長。
- 模型介紹
基於這些考慮,Longformer被提出來擴充模型在長序列建模的能力,它提出了一種時空複雜度同文字序列長度呈線性關係的注意力機制,用以保證模型使用更低的時空複雜度建模長文件,並將文字處理長度擴充到了4096。
- 提出新的注意力機制
下圖展示了經典的注意力機制和Longformer提出的注意力機制,其中a是經典的注意力機制,它是一種“全關注”的注意力機制,即每個token都要和序列中的其他所有token進行互動,因此它的時空複雜度是O(n²) 。右邊的三種模式是Longformer提出來的注意力機制,分別是滑動視窗注意力(Sliding Window Attention)、擴張滑動視窗注意力(Dilated Sliding Window) 和全域性+滑動視窗注意力(Global+Sliding Window)
下面對其進行詳細解釋:
①滑動視窗注意力
- 引入固定長度的滑動視窗,即當前詞只與相鄰的k個詞關聯
- 注意力複雜度從0(n²)降到0(nk);
- 操作類似於卷積操作,單層感受野是k,L層感受野能達到L*k;
②擴張滑動視窗注意力
- 在滑動視窗注意力基礎上引入膨脹卷積, 類似IDCNN,在卷積核中增加空洞,擴充單層感受野,關注到更多上下文。從下圖可以看到,同樣是尺寸為 3 的卷積核,同樣是兩層卷積層,傳統卷積上下文大小為 5,而膨脹卷積的上下文大小為 7。
③全域性+滑動視窗注意力
- 首先需要全域性注意力來關注一些預先設定的位置,即設定某些位置的token能夠看見全部的token,同時其他的所有token也能看見這些位置的token,相當於是將這些位置的token“暴露”在最外面。
- 同時,這些位置的確定和具體的任務有關,例如對於分類任務,這個帶有全域性視角的token是“CLS”,確保其能Attention到整個序列;對於問答任務,這些帶有全域性視角的token是Question對應的這些token。
4、模型壓縮加速策略—模型蒸餾
BERT引數過多導致模型笨重,硬體受限下,如何實現模型壓縮與加速?除了onnx推理加速, 知識蒸餾(Knowledge Distillation) 也是一種非常常用的方法,本節將帶來蒸餾的技術的介紹。
- 蒸餾定義:
蒸餾是用teacher模型指導student模型訓練,以期提升student模型精度。一般來說,teacher模型精度高,不過計算複雜度也大,不適合在終端裝置部署,而student模型計算複雜度雖符合終端裝置要求,但精度不夠,所以可以採取模型蒸餾(distillation)解決這一問題。
- 蒸餾目標:
用推理效率更高的、輕量的學生模型, 近似達到老師的大模型的效果,一般老師的模型size(引數量)要大過學生, 比如用BERT-large去教BERT-base。
- 蒸餾過程:
蒸餾,即老師將知識(Embedding/hidden/logits) 教給學生的過程。
下面將以經典完成蒸餾的預訓練模型進行介紹:
(1)DistilBERT
- 基本介紹:
DistilBERT是一個6層的BERT, 由12層的BERT_Base當老師, 在預訓練階段蒸餾得到。
- 蒸餾流程:
- DistilBERT直接使用老師模型的前6層進行初始化(各層之間維度相同)
- DistilBERT只進行MLM任務,沒有進行NSP任務(該任務被認為是無效策略)
- 另外注意的是,學生模型在學習時,除了要利用真實的label,還需要學習老師模型的隱層輸出(hidden)和輸出機率(soft_label)
流程如下圖所示:
- 模型細節:
其蒸餾過程中最重要的就是loss的學習,下面我們將分析蒸餾的loss如何定義:
其中,
第一項為有監督MLM損失:
被mask的部分作為label,與學生輸出計算交叉熵
第二項為蒸餾MLM損失:
學生的輸出 �� 向老師輸出��看齊,兩者計算交叉熵:
蒸餾時,老師的輸出��也稱作soft_label,它是logits經過softmax後的機率
並且需要注意的是,這裡的softmax一般帶溫度係數T,訓練時設定T=8,推理時設定T=1
第三項為輸出層餘弦損失:
學生的lasthidden向老師的lasthidden看齊,計算餘弦距離
- 模型效果:
- 從模型大小來看,DistilBERT模型引數由BERT-base的110M降為66M
- 從推理速度來看,推理速度獲得40%的提升
- 從模型效果來看,下游任務直接微調時, 獲得97%的BERT-base效果
我們看到DistilBERT模型僅學習老師模型的最後部分,那麼是否可以向老師模型學習到更多的結構呢?
(2)TinyBERT
TinyBERT能夠很好的解決上述問題,
- 首先,在模型結構層面,它對於模型學習得更徹底,基於 transformer 的模型專門設計的知識蒸餾方法,即將Embedding層和中間層都進行蒸餾,如下圖所示。
- 其次,在學習階段層面,它使用了兩階段蒸餾,即在預訓練和微調階段均進行了蒸餾
- 蒸餾流程:
- TinyBERT提出了一種兩階段學習框架,包括通用形式蒸餾和特定任務的蒸餾,如下圖所示。
- 在通用蒸餾階段,使用原始BERT,無需進行任何微調即可將其用作teacher,並使用大型文字語料庫作為訓練資料。透過對來自一般領域的文字執行Transformer蒸餾,獲得了可以針對下游任務進行微調的常規TinyBERT,通用形式蒸餾幫助TinyBERT學習預訓練BERT中嵌入的豐富知識,這在改進TinyBERT的泛化能力中起著重要作用。
- 在特定任務的蒸餾階段,使用增強的特定任務的資料集,重新執行Transformer蒸餾,特定任務的蒸餾進一步向TinyBERT教學了經過微調的BERT的知識。
- 模型細節:
下面我們將分析TinyBERT的loss如何定義:
其中,
第一項為詞向量損失:
計算學生詞向量和老師詞向量的均方誤差,因為兩者維度未必一致,所以需要引入對映 ��
第二項為中間層損失:
若學生4層,老師12層,則老師的(3,6,9,12)層分別蒸餾到學生的(1,2,3,4)層,中間層的損失由隱層均方誤差損失和注意力損失組成:
其中隱層均方誤差損失:
學生的第i層隱層輸出和老師的第j層隱層輸出計算MSE,用 �ℎ 做對映
其中注意力損失:
學生第i層多頭注意力矩陣 和老師第j層多頭注意力矩陣計算MSE,K為head數
第三項為預測層損失:
和DistilBERT一樣,學生學習老師的soft_label並計算交叉熵:
模型效果:
- 4層的TinyBERT, 能夠達到老師(BERT-base)效果的96.8%、引數量縮減為原來的 13.3%、僅需要原來10.6%的推理時間
- DistilBERT可以不微調蒸餾, 但 TinyBERT最好要做微調蒸餾, 僅4 層的它直接微調效果可能下降明顯
- 預訓練蒸餾時TinyBERT沒有使用預測層損失,主要因為預訓練階段主要學習文字表示
進一步思考:
DistilBERT和TinyBERT主要將模型變淺,已有研究證實,相比於模型變窄,模型變淺讓精度損失更大,那麼能否透過降低模型寬度來實現蒸餾?
(3)MobileBERT
- 模型結構
MobileBERT為上述問題的解決提供了思路,直接對其進行微調, 便可以達到BERT-Base 99.2%的效果、引數量小了4倍、推理 速度快了5.5倍,形象結構如下圖所示:
(a)圖是標準的BERT, L層 transformer;
(b)圖是Teacher模型, 是一個 Inverted-Bottleneck BERT_Large;Bottleneck結構是一個線性層, 主要將模型加寬;
(c)圖是MobileBERT學生模型, 它的Bottleneck結構主要將模型變窄;
- 模型對比,如下表所示:
- IB-BERT將521的 hidden加寬到1024來,近似標準的BERT_Large
- MobileBERT的細節則是:
- 將512的 hidden變窄到128
- 堆了更多的Feed Forward層,防止FFN的HHA的引數數量比例失衡;
- 移除了LayerNorm,替換Gelu為Relu啟用;
- Embedding層為128,透過kernel size為3的1維卷積轉化為512維;
- 蒸餾流程
MobileBERT使用漸進式知識遷移蒸餾
- 最開始的Embedding層和最後的分類層直接從老師複製到學生
- 由於老師學生層數相同,學生逐層學習老師的hidden和attention
- 當學生在學習i層時,前面的所有層 (小於i層)引數均不更新
- 模型細節:
下面我們將分析其蒸餾的loss如何定義:
主要圍繞四個損失進行計算:第一項為有監督MLM損失;第二項為有監督NSP損失;
第三項為隱層蒸餾損失;第四項為注意力矩陣損失。
各項損失的計算方法基本與前面一致, 除了注意力矩陣損失,使用KL散度替代MSE
(4)蒸餾工具的使用
我們可以透過TextBrewer工具,自定義各種蒸餾策略。
- 特點如下:
- 適用範圍廣:支援多種模型結構(如Transformer、 RNN)和多種NLP任務(如文字分類、閱 讀理解和序列標註等) ;
- 配置方便靈活:知識蒸餾過程由配置物件(Configurations) 配置。透過配置物件可自由組合 多種知識蒸餾方法;
- 多種蒸餾方法與策略:TextBrewer不僅提供了標準和常見的知識蒸餾方法,也包括了計算 機視覺(CV) 領域中的一些蒸餾技術。
- 簡單易用:為了使用TextBrewer蒸餾模型, 使用者無須修改模型部分的程式碼,並且可複用已 有訓練指令碼的大部分程式碼, 如模型初始化、資料處理和任務評估, 僅需額外完成一些準備工作。
- 架構如下:
其架構主要圍繞Distillers和Configurations展開:
- Distillers 是TextBrewer的核心,用來訓練蒸餾模型、儲存模型和呼叫回撥函式。目前,工具包中提供了五種 Distillers。
- BasicDistiller:進行最基本的知識蒸餾;
- GeneralDistiller:相比於BasicDistiller,額外 提供中間層損失函式(Interme- diate Loss Functions)的支援;
- MultiTeacherDistiller:多教師單任務知識蒸餾, 將多個同任務的教師模型蒸餾到一一個學生模型;
- MultiTaskDistiller:多教師多工知識蒸餾, 將多個不同任務的教師模型蒸餾到一個學生模型;
- BasicTrainer:用於在有標籤資料上有監督地訓 備、模型儲存頻率和評測頻率等; 練教師模型
- Configurations:Distillers訓練 或蒸餾模型的具體方式由兩個配 置物件——TrainingConfig和DistillationConfig指定。
- TrainingConfig:定義了深度學習實驗的通用配置,如日誌目錄與模型儲存目錄、執行裝置、模型儲存頻率和評測頻率等。
- DistillationConfig:定義了和知識蒸餾密切相關的配置,如知識蒸餾損失的型別、知識蒸餾溫度、硬標籤損失的權重、調節器和中間隱含層狀態損失函式等。調節器用於動態調整損失權重和溫度。
程式碼參考:
Longformer可參考huggingface中的longformer-chinese-base-4096,呼叫正確介面即可:
elif 'longformer' in bert_base_model_dir.lower(): # # 自動載入longformer模型
self.bert_tokenizer = BertTokenizer.from_pretrained(bert_base_model_dir)
# # longformer-chinese-base-4096模型引數prefix為bert而非標準的longformer,這是個坑
LongformerModel.base_model_prefix = 'bert'
self.bert_model = LongformerModel.from_pretrained(bert_base_model_dir)
關於蒸餾的核心程式碼如下:
from textbrewer import DistillationConfig, TrainingConfig, GeneralDistiller
# 獲取老師模型、啟用return_extra
# 透過BertFCPredictor獲取teacher model
teacher_predictor = BertFCPredictor(
'../model/chinese-roberta-wwm-ext', '../tmp/bertfc', enable_parallel=enable_parallel
)
teacher_model = teacher_predictor.model
teacher_model.forward = partial(teacher_model.forward, return_extra=True) # 啟用return_extra
print('teacher模型載入成功,label mapping:', teacher_predictor.vocab.id2tag)
# 獲取學生模型、啟用return_extra
# 透過BertFCTrainer獲取student model
pretrained_model, model_dir = './model/TinyBERT_4L_zh', './tmp/bertfc'
student_trainer = BertFCTrainer(pretrained_model, model_dir, enable_parallel=enable_parallel)
student_trainer.vocab.build_vocab(labels=train_labels, build_texts=False, with_build_in_tag_id=False)
student_trainer._build_model()
student_trainer.vocab.save_vocab('{}/{}'.format(student_trainer.model_dir, student_trainer.vocab_name))
student_trainer._save_config()
student_model = student_trainer.model
student_model.forward = partial(student_model.forward, return_extra=True) # 啟用return_extra
print('student模型載入成功,label mapping:', student_trainer.vocab.id2tag) # 確保學生老師label mapping要一致
# 蒸餾配置
distill_config = DistillationConfig(
# 設定溫度係數temperature, tiny-bert論文作者使用1表現最好,一般大於1比較好
temperature=4,
# 設定ground truth loss權重
hard_label_weight=1,
# 設定預測層蒸餾loss(即soft label損失)為交叉熵,並稍微放大其權重
kd_loss_type='ce', kd_loss_weight=1.2,
# 配置中間層蒸餾對映
intermediate_matches=[
# 配置hidden蒸餾對映、維度對映
{'layer_T': 0, 'layer_S': 0, 'feature': 'hidden', 'loss': 'hidden_mse', 'weight': 1,
'proj': ['linear', 312, 768]}, # embedding層輸出
{'layer_T': 3, 'layer_S': 1, 'feature': 'hidden', 'loss': 'hidden_mse', 'weight': 1,
'proj': ['linear', 312, 768]},
{'layer_T': 6, 'layer_S': 2, 'feature': 'hidden', 'loss': 'hidden_mse', 'weight': 1,
'proj': ['linear', 312, 768]},
{'layer_T': 9, 'layer_S': 3, 'feature': 'hidden', 'loss': 'hidden_mse', 'weight': 1,
'proj': ['linear', 312, 768]},
{'layer_T': 12, 'layer_S': 4, 'feature': 'hidden', 'loss': 'hidden_mse', 'weight': 1,
'proj': ['linear', 312, 768]},
# 配置attention矩陣蒸餾對映,注意layer序號從0開始
{"layer_T": 2, "layer_S": 0, "feature": "attention", "loss": "attention_mse", "weight": 1},
{"layer_T": 5, "layer_S": 1, "feature": "attention", "loss": "attention_mse", "weight": 1},
{"layer_T": 8, "layer_S": 2, "feature": "attention", "loss": "attention_mse", "weight": 1},
{"layer_T": 11, "layer_S": 3, "feature": "attention", "loss": "attention_mse", "weight": 1},
]
)
# 訓練配置
epoch = 20 # 使用大一點的epoch
optimizer = AdamW(student_model.parameters(), lr=1e-4) # 使用大一點的lr
train_config = TrainingConfig(
output_dir=model_dir, log_dir='./log',
data_parallel=enable_parallel, ckpt_frequency=1 # 一個epoch存1次模型
)
# 配置model中logits hiddens attentions losses的獲取方法
def simple_adaptor(batch, model_outputs):
return {
'logits': model_outputs[-1]['logits'], 'hidden': model_outputs[-1]['hiddens'],
'attention': model_outputs[-1]['attentions'], 'losses': model_outputs[1],
}
# 蒸餾
distiller = GeneralDistiller(
train_config=train_config, distill_config=distill_config,
model_T=teacher_model, model_S=student_model,
adaptor_T=simple_adaptor, adaptor_S=simple_adaptor
)
with distiller:
print('開始蒸餾')
distiller.train(optimizer, train_dataloader, num_epochs=epoch)
print('蒸餾結束')
【動手學 RAG】系列文章:
- 【RAG 專案實戰 01】在 LangChain 中整合 Chainlit
- 【RAG 專案實戰 02】Chainlit 持久化對話歷史
- 【RAG 專案實戰 03】優雅的管理環境變數
- 【RAG 專案實戰 04】新增多輪對話能力
- 【RAG 專案實戰 05】重構:封裝程式碼
- 【RAG 專案實戰 06】使用 LangChain 結合 Chainlit 實現文件問答
- 【RAG 專案實戰 07】替換 ConversationalRetrievalChain(單輪問答)
- 【RAG 專案實戰 08】為 RAG 新增歷史對話能力
- More...
【動手部署大模型】系列文章:
- 【模型部署】vLLM 部署 Qwen2-VL 踩坑記 01 - 環境安裝
- 【模型部署】vLLM 部署 Qwen2-VL 踩坑記 02 - 推理加速
- 【模型部署】vLLM 部署 Qwen2-VL 踩坑記 03 - 多圖支援和輸入格式問題
- More...
【人人都能學得會的NLP】系列文章:
- 【人人都能學得會的NLP - 文字分類篇 01】使用ML方法做文字分類任務
- 【人人都能學得會的NLP - 文字分類篇 02】使用DL方法做文字分類任務
- 【人人都能學得會的NLP - 文字分類篇 03】長文字多標籤分類分類如何做?
- 【人人都能學得會的NLP - 文字分類篇 04】層次化多標籤文字分類如何做?
- 【人人都能學得會的NLP - 文字分類篇 05】使用LSTM完成情感分析任務
- 【人人都能學得會的NLP - 文字分類篇 06】基於 Prompt 的小樣本文字分類實踐
- More...
本文由mdnice多平臺釋出