企业隐患排查文本挖掘比赛(二):算法篇(从词向量到BERT)
1、文本挖掘的歷程
對于NLP問題,首先要解決的是文本表示的問題。雖然人能夠清楚地了解文本的含義,但是計算機只能處理數值運算,因此首先要考慮如何將文本轉化為數值。
1.1 向量表示
1.1.1 詞袋模型
最初的方案是通過詞袋模型把一個句子轉化為向量表示。它不考慮句子中單詞的順序,只考慮詞表(vocabulary)中單詞在這個句子中的出現次數。
如果是表示一個詞,那就是one-hot的方式,比如我們想表示apple這個詞,就在對應位置設置1,其他位置設置為0,如下:
如果是表示一個句子,比如:
"John likes to watch movies, Mary likes movies too"
"John also likes to watch football games"
——來自wiki
對于這兩個句子,我們要用詞袋模型把它轉化為向量表示,這兩個句子形成的詞表(不去停用詞)為:
[‘also’, ‘football’, ‘games’, ‘john’, ‘likes’, ‘mary’, ‘movies’, ‘to’, ‘too’, ‘watch’]
因此,它們的向量表示為:
scikit-learn中的CountVectorizer()函數實現了BOW模型,如下:
from sklearn.feature_extraction.text import CountVectorizer corpus = ["John likes to watch movies, Mary likes movies too","John also likes to watch football games", ] vectorizer = CountVectorizer() X = vectorizer.fit_transform(corpus) print(vectorizer.get_feature_names()) print(X.toarray())#輸出結果: #['also', 'football', 'games', 'john', 'likes', 'mary', 'movies', 'to', 'too', 'watch'] #[[0 0 0 1 2 1 2 1 1 1] # [1 1 1 1 1 0 0 1 0 1]]1.1.2 TF-IDF
使用BOW模型有很多缺點,首先它沒有考慮單詞之間的順序,其次它無法反應出一個句子的關鍵詞,比如下面這個句子:
"John likes to play football, Mary likes too"
這個句子若用BOW模型,它的詞表為:[‘football’, ‘john’, ‘likes’, ‘mary’, ‘play’, ‘to’, ‘too’],則詞向量表示為:[1 1 2 1 1 1 1]。若根據BOW模型提取這個句子的關鍵詞,則為 “like”,但是顯然這個句子的關鍵詞應該為 “football”。而TF-IDF則可以解決這個問題。TF-IDF看名字也知道包括兩部分TF和IDF,TF(Term Frequency,詞頻)的公式為:
TF(w)=單詞w在文章中出現的次數文章的單詞總數TF(w)={{單詞w在文章中出現的次數}\over{文章的單詞總數}} TF(w)=文章的單詞總數單詞w在文章中出現的次數?
而IDF(inverse document frequency,逆文本頻率)的公式為:
IDF(w)=log(語料庫中文檔的總數包含詞w的文檔數+1)IDF(w)=log({{語料庫中文檔的總數}\over{包含詞w的文檔數+1}}) IDF(w)=log(包含詞w的文檔數+1語料庫中文檔的總數?)
其中,分母之所以加1是為了防止分母為0。所以,TF-IDF的公式為:
TF?IDF(w)=TF(w)?IDF(w)TF-IDF(w)=TF(w)*IDF(w) TF?IDF(w)=TF(w)?IDF(w)
TF-IDF值越大說明這個詞越重要,也可以說這個詞是關鍵詞。
實際使用中,sklearn也封裝TF-IDF方法,如下:
from sklearn.feature_extraction.text import TfidfVectorizer corpus = ['This is the first document.','This document is the second document.','And this is the third one.','Is this the first document?', ] vectorizer = TfidfVectorizer() X = vectorizer.fit_transform(corpus) print(vectorizer.get_feature_names()) print(X.toarray())""" ['and', 'document', 'first', 'is', 'one', 'second', 'the', 'third', 'this'][[0. 0.46979139 0.58028582 0.38408524 0. 0.0.38408524 0. 0.38408524][0. 0.6876236 0. 0.28108867 0. 0.538647620.28108867 0. 0.28108867][0.51184851 0. 0. 0.26710379 0.51184851 0.0.26710379 0.51184851 0.26710379][0. 0.46979139 0.58028582 0.38408524 0. 0.0.38408524 0. 0.38408524]] """1.1.3 Word Embedding
后來又出現了詞向量,word embedding,用一個低維稠密的向量去表示一個詞,比如:
通常這個向量的維度在幾百到上千之間,相比one hot幾千幾萬的維度就低了很多。詞與詞之間可以通過相似度或者距離來表示關系,相關的詞向量相似度比較高,或者距離比較近,不相關的詞向量相似度低,或者距離比較遠,這樣詞向量本身就有了含義。文本的表示問題就得到解決。
詞向量可以通過一些無監督的方法學習得到,比如Word2Vec中的CBOW或者Skip-Gram,可以預先在語料庫上訓練出詞向量,以供后續的使用。
1.2 解決具體任務
NLP中有各種各樣的任務,比如分類(Classification),問答(QA),實體命名識別(NER)等。
對于這些不同的任務,最早的做法是根據每類任務定制不同的模型,輸入預訓練好的embedding,然后利用特定任務的數據集對模型進行訓練,如下:
這里存在的問題就是,不是每個特定任務都有大量的標簽數據可供訓練,對于那些數據集非常小的任務,恐怕就難以得到一個理想的模型。
為了解決這個問題,NLP領域借鑒了CV領域的解決方案。首先用一個通用的網絡模型,比如Vgg,ResNet或者GoogleNet,在ImageNet上做預訓練(pre-training)。ImageNet有1400萬張有標注的圖片,包含1000個類別,這樣的數據規模足以訓練出一個規模龐大的模型。在訓練過程中,模型會不斷的學習如何提取特征,底層的CNN網絡結構會提取邊緣,角,點等通用特征,模型越往上走,提取的特征也越抽象,與特定的任務更加相關。當完成預訓練之后,根據我自己的分類任務,調整最上層的網絡結構,然后在小數據集里對模型進行訓練。
在訓練時,可以固定住底層的模型參數只訓練頂層的參數,也可以對整個模型進行訓練,這個過程叫做微調(fine-tuning),最終得到一個可用的模型。
總結一下,整個過程包括兩步,拿一個通用模型在ImageNet上做預訓練(pre-training),然后針對特定任務進行微調(fine-tuning),完美解決了特定任務數據不足的問題。
還有一個好處是,對于各種各樣的任務都不再需要從頭開始訓練網絡,可以直接拿預訓練好的結果進行微調,既減少了訓練計算量的負擔,也減少了人工標注數據的負擔。
NLP領域引入了這種做法,用一個通用模型,在非常大的語料庫上進行預訓練,然后在特定任務上進行微調,BERT就是這套方案的集大成者。BERT不是第一個,但目前為止,是效果最好的方案。BERT用了一個已有的模型結構,提出了一整套的預訓練方法和微調方法。
2、Attention機制
Attention(注意力)機制的核心邏輯是「從關注全部到關注重點」,將有限的注意力集中在重點信息上,從而節省資源,快速獲得最有效的信息。
之所以要引入 Attention 機制,主要是Attention 有幾個優點:
- 參數少
模型復雜度跟 CNN、RNN 相比,復雜度更小,參數也更少,所以對算力的要求也就更小。
- 速度快
Attention 解決了 RNN 不能并行計算的問題。Attention機制每一步計算不依賴于上一步的計算結果,因此可以和CNN一樣并行處理。
- 效果好
在 Attention 機制引入之前,有一個問題大家一直很苦惱:長距離的信息會被弱化,就好像記憶能力弱的人,記不住過去的事情是一樣的。
Attention 是挑重點,就算文本比較長,也能從中間抓住重點,不丟失重要的信息,從而達到“天涯若比鄰”的效果。
Attention 原理可以分解為三步:
第一步: query 和 key 進行相似度計算,得到權值;
第二步:將權值進行歸一化,得到直接可用的權重;
第三步:將權重和 value 進行加權求和;
從上面的建模,我們可以大致感受到 Attention 的思路簡單,四個字“帶權求和”就可以高度概括,大道至簡。做個不太恰當的類比,人類學習一門新語言基本經歷四個階段:死記硬背(通過閱讀背誦學習語法練習語感)->提綱挈領(簡單對話靠聽懂句子中的關鍵詞匯準確理解核心意思)->融會貫通(復雜對話懂得上下文指代、語言背后的聯系,具備了舉一反三的學習能力)->登峰造極(沉浸地大量練習)。
這也如同attention的發展脈絡,RNN 時代是死記硬背的時期,attention 的模型學會了提綱挈領,進化到 transformer,融匯貫通,具備優秀的表達學習能力,再到 GPT、BERT,通過多任務大規模學習積累實戰經驗,戰斗力爆棚。
要回答為什么 attention 這么優秀?是因為它讓模型開竅了,懂得了提綱挈領,學會了融會貫通。
——阿里技術
3、Transformer的原理
Bert中的核心結構是Transformer,Transformer包括兩個部分encoder和decoder,首先看一下encoder部分的處理:
首先是輸入word embedding,這里是直接輸入一整句話的所有embedding。如下圖所示,假設我們的輸入是Thinking Machines,每個詞對應一個embedding,就有2個embedding。輸入embedding需要加上位置編碼(Positional Encoding),為什么要加位置編碼,后文會做詳細介紹。然后經過一個Multi-Head Attention結構,這個結構是算法單元中最重要的部分,我們會在后邊詳細介紹。之后是做了一個shortcut的處理,就是把輸入和輸出按照對應位置加起來,如果了解殘差網絡(ResNet)的同學,會對這個結構比較熟悉,這個操作有利于加速訓練。然后經過一個歸一化normalization的操作。接著經過一個兩層的全連接網絡,最后同樣是shortcut和normalization的操作。可以看到,除了Multi-Head Attention,都是常規操作,沒有什么難理解的。這里需要注意的是,每個小模塊的輸入和輸出向量,維度都是相等的,比如,Multi-Head Attention的輸入和輸出向量維度是相等的,否則無法進行shortcut的操作;Feed Forward的輸入和輸出向量維度也是相等的;最終的輸出和輸入向量維度也是相等的。但是Multi-Head Attention和Feed Forward內部,向量維度會發生變化。
我們來詳細看一下Multi-Head Attention的結構。這個Multi-Head表示多頭的意思,先從最簡單的看起,看看單頭Attention是如何操作的。從下圖的橙色方塊可以看到,embedding在進入到Attention之前,有3個分叉,那表示說從1個向量,變成了3個向量。具體是怎么算的呢?我們看圖中,定義了一個WQW^QWQ矩陣(這個矩陣隨機初始化,通過訓練得到),將embedding和WQW^QWQ矩陣做乘法,得到查詢向量q,假設輸入embedding是512維,在圖中我們用4個小方格表示,輸出的查詢向量是64維,圖中用3個小方格以示不同。然后類似地,定義WKW^KWK和WVW^VWV矩陣,將embedding和WKW^KWK做矩陣乘法,得到鍵向量k;將embeding和WVW^VWV做矩陣乘法,得到值向量v。對每一個embedding做同樣的操作,那么每個輸入就得到了3個向量:查詢向量,鍵向量和值向量。需要注意的是,查詢向量和鍵向量要有相同的維度,值向量的維度可以相同,也可以不同,但一般也是相同的。
接下來我們計算每一個embedding的輸出,以第一個詞Thinking為例,參看下圖。用查詢向量q1跟鍵向量k1和k2分別做點積,得到112和96兩個數值。這也是為什么前文提到查詢向量和鍵向量的維度必須要一致,否則無法做點積。然后除以常數8,得到14和12兩個數值。這個常數8是鍵向量的維度的開方,鍵向量和查詢向量的維度都是64,開方后是8。做這個尺度上的調整目的是為了易于訓練。然后把14和12丟到softmax函數中,得到一組加和為1的系數權重,算出來是大約是0.88和0.12。將0.88和0.12對兩個值向量v1和v2做加權求和,就得到了Thinking的輸出向量z1。類似的,可以算出Machines的輸出z2。如果一句話中包含更多的詞,也是相同的計算方法。
通過這樣一系列的計算,可以看到,現在每個詞的輸出向量z都包含了其他詞的信息,每個詞都不再是孤立的了。而且每個位置中,詞與詞的相關程度,可以通過softmax輸出的權重進行分析。如下圖所示,這是某一次計算的權重,其中線條顏色的深淺反映了權重的大小,可以看到it中權重最大的兩個詞是The和animal,表示it跟這兩個詞關聯最大。這就是attention的含義,輸出跟哪個詞關聯比較強,就放比較多的注意力在上面。
上面我們把每一步計算都拆開了看,實際計算的時候,可以通過矩陣來計算,如圖所示。
講完了attention,再來講Multi-Head。對于同一組輸入embedding,我們可以并行做若干組上面的操作,例如,我們可以進行8組這樣的運算,每一組都有WQ,WK,WVW^Q, W^K, W^VWQ,WK,WV矩陣,并且不同組的矩陣也不相同。這樣最終會計算出8組輸出,我們把8組的輸出連接起來,并且乘以矩陣WOW^OWO做一次線性變換得到輸出,WOW^OWO也是隨機初始化,通過訓練得到,計算過程如下圖所示。這樣的好處,一是多個組可以并行計算,二是不同的組可以捕獲不同的子空間的信息。
到這里就把Transformer的結構講完了,同樣都是做NLP任務,我們來和RNN做個對比。下圖是個最基本的RNN結構,還有計算公式。當計算隱向量h4時,用到了輸入x4,和上一步算出來的隱向量h3,h3包含了前面所有節點的信息。h4中包含最多的信息是當前的輸入x4,越往前的輸入,隨著距離的增加,信息衰減得越多。對于每一個輸出隱向量h都是如此,包含信息最多得是當前的輸入,隨著距離拉遠,包含前面輸入的信息越來越少。但是Transformer這個結構就不存在這個問題,不管當前詞和其他詞的空間距離有多遠,包含其他詞的信息不取決于距離,而是取決于兩者的相關性,這是Transformer的第一個優勢。第二個優勢在于,對于Transformer來說,在對當前詞進行計算的時候,不僅可以用到前面的詞,也可以用到后面的詞。而RNN只能用到前面的詞,這并不是個嚴重的問題,因為這可以通過雙向RNN來解決。第三點,RNN是一個順序的結構,必須要一步一步地計算,只有計算出h1,才能計算h2,再計算h3,隱向量無法同時并行計算,導致RNN的計算效率不高,這是RNN的固有結構所造成的,之前有一些工作就是在研究如何對RNN的計算并行化。通過前文的介紹,可以看到Transformer不存在這個問題。通過這里的比較,可以看到Transformer相對于RNN有巨大的優勢,因此我看到有人說RNN以后會被取代。
關于上面的第三點優勢,可能有人會不認可,RNN的結構包含了序列的時序信息,而Transformer卻完全把時序信息給丟掉了。為了解決時序的問題,Transformer的作者用了一個絕妙的辦法,這就是我在前文提到的位置編碼(Positional Encoding)。位置編碼是和word embedding同樣維度的向量,將位置embedding和詞embedding加在一起,作為輸入embedding,如下圖所示。位置編碼可以通過學習得到,也可以通過設置一個跟位置或者時序相關的函數得到,比如設置一個正弦或者余弦函數,這里不再多說。
我們把Transformer的結構作為一個基本單元,把N個這樣的基本單元順序連起來,就是BERT的算法模型,如下圖所示:
從前面的描述中可以看到,當輸入有多少個embedding,那么輸出也就有相同數量的embedding,可以采用和RNN相同的叫法,把輸出叫做隱向量。在做具體NLP任務的時候,只需要從中取對應的隱向量作為輸出即可。
Transformer的詳細內容參見:自然語言處理中的Transformer
4、BERT原理
實際BERT并不是第一個提出預訓練加微調的方案,此前還有一套方案叫GPT。GPT的模型結構和BERT是相同的,都是上圖的結構,只是BERT的模型規模更加龐大。
GPT是這么預訓練的,在一個8億單詞的語料庫上做訓練,給出前文,不斷地預測下一個單詞。比如這句話,Winter is coming,當給出第一個詞Winter之后,預測下一個詞is,之后再預測下一個詞coming。不需要標注數據,通過這種無監督訓練的方式,得到一個預訓練模型。
我們再來看看BERT有什么不同。BERT來自于Bidirectional Encoder Representations from Transformers首字母縮寫,這里提到了一個雙向(Bidirectional)的概念。BERT在一個33億單詞的語料庫上做預訓練,語料庫就要比GPT大了幾倍。預訓練包括了兩個任務:
- 第一個任務是隨機地扣掉15%的單詞,用一個掩碼MASK代替,讓模型去猜測這個單詞;
- 第二個任務是,每個訓練樣本是一個上下句,有50%的樣本,下句和上句是真實的,另外50%的樣本,下句和上句是無關的,模型需要判斷兩句的關系。
這兩個任務各有一個loss,將這兩個loss加起來作為總的loss進行優化。
對于loss1,其計算過程如下:
對于loss2,其計算過程如下:
當然現在也有一些文獻說Next Sentence Prediction沒有用。
下面兩行是一個小栗子,用括號標注的是扣掉的詞,用[MASK]來代替。
正樣本:我[MASK](是)個算法工程師,我服務于WiFi萬能鑰匙這家[MASK](公司)。
負樣本:我[MASK](是)個算法工程師,今天[MASK](股票)又跌了。
我們來對比下GPT和BERT兩種預訓練方式的優劣。GPT在預測詞的時候,只預測下一個詞,因此只能用到上文的信息,無法利用到下文的信息。而BERT是預測文中扣掉的詞,可以充分利用到上下文的信息,這使得模型有更強的表達能力,這也是BERT中Bidirectional的含義。在一些NLP任務中需要判斷句子關系,比如判斷兩句話是否有相同的含義。BERT有了第二個任務,就能夠很好的捕捉句子之間的關系。
講完了這兩個任務,我們再來看看,如何表達這么復雜的一個訓練樣本,讓計算機能夠明白。下圖表示“my dog is cute, he likes playing.”的輸入形式。每個符號的輸入由3部分構成:
- 第一個是詞本身的embedding;
- 第二個是表示上下句的embedding,如果是上句,就用A embedding,如果是下句,就用B embedding;
- 最后,根據Transformer模型的特點,還要加上位置embedding,這里的位置embedding是通過學習的方式得到的,BERT設計一個樣本最多支持512個位置;
將3個embedding相加,作為輸入。需要注意的是,在每個句子的開頭,需要加一個Classification(CLS)符號,后文中會進行介紹,其他的一些小細節就不說了。
圖3.2
完成預訓練之后,就要針對特定任務就行微調了,這里只說下分類任務,分類任務包括對單句子的分類任務,比如判斷電影評論是喜歡還是討厭;多句子分類,比如判斷兩句話是否表示相同的含義。
下圖(a)(b)是對這類任務的一個示例,左邊表示兩個句子的分類,右邊是單句子分類:
在輸出的隱向量中,取出CLS對應的向量C,加一層網絡W,并丟給softmax進行分類,得到預測結果P,計算過程如下:
P=softmax(C?WT)P=softmax(C·W^T) P=softmax(C?WT)
在特定任務數據集中對Transformer模型的所有參數(固定)和網絡W(fine-tuning)共同訓練,直到收斂。新增加的網絡W是H*K維,H表示隱向量的維度,K表示分類數量,W的參數數量相比預訓練模型的參數少得可憐。我們只需要新增少量的參數,然后在特定數據集上進行訓練即可。從實驗結果來看,即便是很小的數據集,也能取得不錯的效果。
5、BERT的應用
5.1 simpletransformers
Simple Transformers是專為需要簡單快速完成某項文本工作而設計。它是AI創業公司Hugging Face在Transformers庫的基礎上構建的。基于該工具,我們不必拘泥于源代碼,也不用費時費力地去弄清楚各種設置,把文本分類問題變得簡單易操作。
首先,我們需要安裝simpletransformers:
pip install simpletransformers對于不同的任務,模型的訓練流程遵循相同的模式,總共四步:
下面以分類模型為例:
5.1.1 準備數據
Simple Transformers要求數據必須包含在至少兩列的DataFrames中。我們只需為列的文本和標簽命名,SimpleTransformers就會處理數據。或者我們也可以遵循以下約定:
-
第一列包含文本,類型為str。
-
第二列包含標簽,類型為int。
對于多類分類,標簽應該是從0開始的整數。如果數據具有其他標簽,則可以使用dict保留從原始標簽到整數標簽的映射。
# 讀取訓練集和驗證集數據 train_df = train.iloc[trn_idx][['content', 'label']] valid_df = train.iloc[val_idx][['content', 'label']] # 為文本和標簽命名(如果不命名,默認將第一列作為text,第二列作為labels) train_df.columns = ['text', 'labels'] valid_df.columns = ['text', 'labels']5.1.2 模型
導入針對特定任務的模型:
from simpletransformers.classification import ClassificationModel由于Transformers是預訓練模型,所以我們可以先load一下預訓練好的模型:
- load標準預訓練模型:
標準預訓練模型可參見:Pretrained models
- load社區預訓練模型
社區預訓練模型可參見:Hugging Face
- load本地預訓練模型
outputs/best_model為本地保存模型的路徑。
此外,load模型的時候,可以修改模型的配置選項,使得我們可以很容易的定制模型。配置選項分為兩種類型:所有任務通用的選項、針對特定任務的選項。配置方式如下:
model_args = ClassificationArgs() model_args.num_train_epochs = 5 model_args.learning_rate = 1e-4model = ClassficationModel("bert", "bert-base-cased", use_cuda=True, # 是否使用CUDAargs=model_args) # 模型配置通用配置說明參見:Configuring a Simple Transformers Model
5.1.3 訓練
# Train the model model.train_model(train_df)這就是訓練模型的全部操作,我們還可以通過將包含相關屬性的字典傳遞給train_model方法來更改超參數。需要注意的是,即使完成訓練,這些修改也將保留。
train_model方法將在第n個step(其中n為self.args [‘save_steps’])創建模型的檢查點(保存)。訓練完成后,最終模型將保存到self.args [‘output_dir’]。
5.1.4 評估
result, model_outputs, wrong_predictions = model.eval_model(eval_df)要評估模型,只需調用eval_model。此方法具有三個返回值:
? result:dict形式的評估結果。默認情況下,僅對多類分類計算馬修斯相關系數(MCC)。
? model_outputs:評估數據集中每個項目的模型輸出list。用softmax函數來計算預測值,輸出每個類別的概率而不是單個預測。
? wrong_predictions:每個錯誤預測的InputFeature列表。可以從InputFeature.text_a屬性獲取文本。(可以在存儲庫 ThilinaRajapakse/simpletransformers 的utils.py文件中找到InputFeature類)
我們還可以在評估中使用其他指標,只需將指標函數作為關鍵字參數傳遞給eval_model方法。指標功能應包含兩個參數,第一個是真實標簽,第二個是預測,這遵循sklearn的標準。
對于任何需要附加參數的度量標準函數(在sklearn中為f1_score),你可以在添加了附加參數的情況下將其包裝在自己的函數中,然后將函數傳遞給eval_model。
from sklearn.metrics import f1_score, accuracy_scoredef f1_multiclass(labels, preds):return f1_score(labels, preds, average='micro')result, model_outputs, wrong_predictions = model.eval_model(eval_df, f1=f1_multiclass, acc=accuracy_score)作為參考,我使用這些超參數獲得的結果如下:
{'mcc': 0.937104098029913, 'f1': 0.9527631578947369, 'acc': 0.9527631578947369}5.1.5 預測
在實際應用中,我們常常不知道什么是真正的標簽。要對任意樣本執行預測,可以使用predict方法。此方法與eval_model方法非常相似,不同之處在于,該方法采用簡單的文本列表并返回預測列表和模型輸出列表。
predictions, raw_outputs = model.predict(['Some arbitary sentence'])參考文檔:Documentation
5.2 構建中文文本分類任務
5.2.1 企業隱患排查文本訓練和預測
(1)讀取數據
import warnings warnings.simplefilter('ignore')import gc import torch import numpy as np import pandas as pd from sklearn.metrics import f1_score from sklearn.model_selection import StratifiedKFoldfrom simpletransformers.classification import ClassificationModel, ClassificationArgs# 讀取數據 train = pd.read_csv('./drive/MyDrive/data/train.csv') test = pd.read_csv('./drive/MyDrive/data/test.csv') train['content'].fillna('', inplace=True) test['content'].fillna('', inplace=True)cuda_available = torch.cuda.is_available()(2)single sentence classification
單句子的文本分類任務
oof = [] prediction = test[['id']] prediction['bert_pred'] = 0kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=2021) for fold_id, (trn_idx, val_idx) in enumerate(kfold.split(train, train['label'])):train_df = train.iloc[trn_idx][['content', 'label']]valid_df = train.iloc[val_idx][['content', 'label']]train_df.columns = ['text', 'labels']valid_df.columns = ['text', 'labels']def get_model_args(dir):model_args = ClassificationArgs()model_args.max_seq_length = 128 # 截取文本長度為128model_args.train_batch_size = 16 model_args.num_train_epochs = 2 # 跑2epochmodel_args.sliding_window=True # 使用滑動窗口model_args.evaluate_during_training = True # 訓練過程中做評估model_args.evaluate_during_training_verbose = Truemodel_args.fp16 = False model_args.no_save = True # 不保存模型model_args.save_steps = -1 # 不根據step保存檢查點model_args.overwrite_output_dir = True # 覆蓋輸出路徑model_args.output_dir = dir # 模型輸出路徑,默認為/outputsreturn model_argsmodel_args = get_model_args('./drive/MyDrive/models/bert_fold_' + str(fold_id))model = ClassificationModel('bert','hfl/chinese-roberta-wwm-ext', # 中文文本train的社區預訓練模型use_cuda=cuda_available,args=model_args)model.train_model(train_df, eval_df=valid_df)_, vaild_outputs, _ = model.eval_model(valid_df)df_oof = train.iloc[val_idx][['id', 'label']].copy()# df_oof['bert_pred'] = vaild_outputs[:,1]df_oof['bert_pred'] = [np.mean(output, axis=0)[1] for output in vaild_outputs]oof.append(df_oof)_, test_outputs = model.predict([text for text in test['content']])# prediction['bert_pred'] += test_outputs[:, 1] / kfold.n_splitsprediction['bert_pred'] += np.array([np.mean(output, axis=0)[1] for output in test_outputs]) / kfold.n_splitsdel model, train_df, valid_df, vaild_outputs, test_outputsgc.collect()(3)sentence pair classification
多句子的文本分類任務
oof = [] prediction = test[['id']] prediction['bert_pred'] = 0kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=2021) for fold_id, (trn_idx, val_idx) in enumerate(kfold.split(train, train['label'])):train_df = train.iloc[trn_idx][['level_4', 'content', 'label']]valid_df = train.iloc[val_idx][['level_4', 'content', 'label']]train_df.columns = ['text_a', 'text_b', 'labels']valid_df.columns = ['text_a', 'text_b', 'labels']def get_model_args(dir):model_args = ClassificationArgs()model_args.max_seq_length = 256model_args.train_batch_size = 16model_args.num_train_epochs = 3model_args.fp16 = Falsemodel_args.no_save = Truemodel_args.overwrite_output_dir = Truemodel_args.output_dir = dirreturn model_argsmodel_args = get_model_args('./drive/MyDrive/models/bert_pair_fold_' + str(fold_id)) model = ClassificationModel('bert','hfl/chinese-roberta-wwm-ext',num_labels=2,use_cuda=True,cuda_device=0,args=model_args)model.train_model(train_df, eval_df=valid_df)_, vaild_outputs, _ = model.eval_model(valid_df)df_oof = train.iloc[val_idx][['id', 'label']].copy()df_oof['bert_pred'] = vaild_outputs[:,1]oof.append(df_oof)_, test_outputs = model.predict([list(text) for text in test[['level_4', 'content']].values])prediction['bert_pred'] += test_outputs[:, 1] / kfold.n_splitsdel model, train_df, valid_df, vaild_outputs, test_outputsgc.collect()使用五折交叉驗證,減小模型過擬合的風險。
5.2.2 選擇合適的切分點
i_bst = 0 bst = 0 df_oof = pd.concat(oof) for i in np.arange(df_oof.bert_pred.min(), df_oof.bert_pred.max(), 0.1):df_oof['pred_label'] = df_oof['bert_pred'].apply(lambda x: 1 if x >= i else 0)score = f1_score(df_oof['label'], df_oof['pred_label'])# print(i, 'f1_score:', score)if score> bst:i_bst = ibst = score print(i_bst, 'f1_score:', bst) # 0.9234375對于預測結果,在訓練集上選擇一個最優切分點,使得訓練集上的f1分數最高,然后使用這個切分點在測試集上劃分正負樣本。
5.2.3 預測結果
prediction['label'] = prediction['bert_pred'].apply(lambda x: 1 if x >= i_bst else 0) prediction['label'].value_counts() df_oof[['id', 'bert_pred']].to_csv('./drive/MyDrive/output/roberta_pred_oof2.csv', index=False) prediction[['id', 'bert_pred']].to_csv('./drive/MyDrive/output/roberta_pred_test2.csv', index=False)bert的預測結果有兩種使用方式:
-
在訓練集上選擇合適的切分點,使用切分點直接在測試集上劃分正負樣本;
-
將模型的預測結果作為子模型分,同其他統計類特征一起進入LGB模型,輸出預測概率;
6、sbert雙塔模型
6.1 雙塔SBERT模型
塔式結構在深度學習模型中是比較常見的,比較著名的是微軟的DSSM(Deep Structured Semantic Models )基于深度網絡的語義模型,其結構如下:
DSSM模型通過一層一層堆疊的網絡提取隱含語義,通過semantic特征向量(128維)的兩兩余弦計算,得到各文本之間的相似度R(q, d),最后優化相似度與樣本的距離。其中128維的semantic feature,即為通過該模型學習到的對應文本的隱含語義。
而SBert與dssm思想上比較類似,都是通過獨立的子模塊進行高維信息抽取,通過各文本特征向量的余弦距離,來表征文本語義相似度。sbert結構圖如下:
semantic feature(上圖的U、V) 在實際生產上非常有意義,它能夠預先通過sbert計算得到。然后,通過向量搜索引擎處理這些向量,檢索到最相似語義的文本。這種方式能非常快速實現海量相似文本的查詢、排序,而無需再進行高延遲的模型預測。類似的,推薦系統也是預先學習到用戶、商品的表征向量,從而進行召回、排序、推薦等任務。
6.2 sentence-transformer
sentence-transformer框架提供了一種簡便的方法來計算句子和段落的向量表示(也稱為句子嵌入)。這些模型基于諸如BERT / RoBERTa / XLM-RoBERTa等模型,并且經過專門調整以優化向量表示,以使具有相似含義的句子在向量空間中接近。
首先,安裝sentence-transformers包:
pip install -U sentence-transformers比如我們的訓練數據集如下,任務為:對于給定句子可以查詢最相似的文本。
[(‘為什么我無法看到額度’, ‘為什么開通了卻沒有額度’, 0),
(‘為啥換不了’, ‘為兩次還都提示失敗呢’, 0),
(‘借了錢,但還沒有通過,可以取消嗎?’, ‘可否取消’, 1),
(‘為什么我申請額度輸入密碼就一直是那個頁面’, ‘為什么要輸入支付密碼來驗證’, 0),
(‘今天借 明天還款可以?’, ‘今天借明天還要手續費嗎’, 0),
(‘你好!今下午咱沒有扣我款?’, ‘你好 今天怎么沒有扣款呢’, 1),
我們可以使用如下代碼對預訓練模型進行微調:
# 先安裝sentence_transformers from sentence_transformers import SentenceTransformer# Define the model. Either from scratch of by loading a pre-trained model model = SentenceTransformer('distiluse-base-multilingual-cased') # distiluse-base-multilingual-cased 蒸餾得到的,官方預訓練好的模型# 加載數據集 def load_data(filename):D = []with open(filename, encoding='utf-8') as f:for l in f:try:text1, text2, label = l.strip().split(',')D.append((text1, text2, int(label)))except ValueError:_return Dtrain_data = load_data('text_matching/input/train.csv') valid_data = load_data('text_matching/input/dev.csv') test_data = load_data('text_matching/input/test.csv')from sentence_transformers import SentenceTransformer, SentencesDataset from sentence_transformers import InputExample, evaluation, losses from torch.utils.data import DataLoader# Define your train examples. train_datas = [] for i in train_data:train_datas.append(InputExample(texts=[i[0], i[1]], label=float(i[2])))# Define your evaluation examples sentences1,sentences2,scores = [],[],[] for i in valid_data:sentences1.append(i[0])sentences2.append(i[1])scores.append(float(i[2]))evaluator = evaluation.EmbeddingSimilarityEvaluator(sentences1, sentences2, scores)# Define your train dataset, the dataloader and the train loss train_dataset = SentencesDataset(train_datas, model) train_dataloader = DataLoader(train_dataset, shuffle=True, batch_size=16) train_loss = losses.CosineSimilarityLoss(model)# Tune the model model.fit(train_objectives=[(train_dataloader, train_loss)], epochs=3, warmup_steps=100, evaluator=evaluator, evaluation_steps=200, output_path='./two_albert_similarity_model')向量相似度的測評:
# Define your evaluation examples sentences1,sentences2,scores = [],[],[] for i in test_data:sentences1.append(i[0])sentences2.append(i[1])scores.append(float(i[2]))evaluator = evaluation.EmbeddingSimilarityEvaluator(sentences1, sentences2, scores) model.evaluate(evaluator) ''' 0.68723840499831 '''模型準確度的測評:
''' Evaluate a model based on the similarity of the embeddings by calculating the accuracy of identifying similar and dissimilar sentences. The metrics are the cosine similarity as well as euclidean and Manhattan distance The returned score is the accuracy with a specified metric. ''' evaluator = evaluation.BinaryClassificationEvaluator(sentences1, sentences2, scores) model.evaluate(evaluator) ''' 0.8906211331111515 '''模型獲取向量:
from sentence_transformers import SentenceTransformer, utilmodel = SentenceTransformer('./two_albert_similarity_model')# Sentences are encoded by calling model.encode() emb1 = model.encode('什么情況導致評估不過') emb2 = model.encode("個人信用怎么評估出來的") print(emb1) print(emb2)cos_sim = util.pytorch_cos_sim(emb1, emb2) print("Cosine-Similarity:", cos_sim) ''' emb1:[ 2.98918579e-02 -1.61327735e-01 -2.11362451e-01 -8.07176754e-02 8.48087892e-02 -1.54550061e-01 -1.11961491e-01 -7.36757461e-03 。。。 -1.64973773e-02 -6.62902296e-02 7.76088312e-02 -5.86621352e-02] emb2:[-0.00474379 0.01176221 -0.12961781 0.03897651 -0.08082788 0.02274037 -0.01527447 -0.03132218 0.04967966 -0.11125126 0.03260884 -0.08910057。。。 0.04023521 0.07583613 -0.01950659 -0.04246246 0.03055439 0.0451214] Cosine-Similarity: tensor([[-0.0515]]) '''模型向量召回:
from tqdm import tqdm import numpy as np import faiss # make faiss availableALL = [] for i in tqdm(test_data):ALL.append(i[0])ALL.append(i[1]) ALL = list(set(ALL))DT = model.encode(ALL) DT = np.array(DT, dtype=np.float32)# https://waltyou.github.io/Faiss-Introduce/ index = faiss.IndexFlatL2(DT[0].shape[0]) # build the index print(index.is_trained) index.add(DT) # add vectors to the index print(index.ntotal)查詢最相似的文本:
k = 10 # we want to see 10 nearest neighbors aim = 220 D, I = index.search(DT[aim:aim+1], k) # sanity check print(I) print(D) print([ALL[i]for i in I[0]]) ''' [[ 220 4284 3830 2112 1748 639 5625 6062 1515 1396]] [[0. 0.04789903 0.04982989 0.07678283 0.08252098 0.08306326 0.08532818 0.11053496 0.11116458 0.11140325]] ['4500元一天要多少息', '借一萬一天利息多少錢', '萬元日利息多少', '代五萬元,一天的息是多少', '一萬元的日息是多少?', '一萬元每天多少錢利息', '1千元日息是多少', '一天利息好多錢', '10000塊日利息是多少', '借1萬一天多少錢利息啊'] '''先獲取特征再查詢最相似的文本:
query = [model.encode('1萬元的每天利息是多少')] query = np.array(query, dtype=np.float32) D, I = index.search(query, 10) # sanity check print(I) print(D) print([ALL[i]for i in I[0]]) ''' [[3170 1476 639 2112 1826 3193 1396 4332 5360 1748]] [[0.03987426 0.05244656 0.05858241 0.05872107 0.07376505 0.08907703 0.0905426 0.09842405 0.10062639 0.10626719]] ['20000每天利息多少', '1萬元日利息多少', '一萬元每天多少錢利息', '代五萬元,一天的息是多少', '1萬元日息是多少啊!', '100000元一天的利息是5000嗎', '借1萬一天多少錢利息啊', '借一萬八,那一天是多少利息', '28000的日息是多少', '一萬元的日息是多少?'] '''6.3 文本相似度提取
前面介紹了sentence-transformers的使用方法,對于該比賽的數據,剛開始考慮的一個想法是將level_4和content作為雙塔的輸入,label作為標簽,在預訓練模型上進行微調。后來覺得場景有些不適用,因為sbert更適合表征文本語義相似度的任務,而比賽中的數據,標簽為1表示的是針對level_4的標準,content填報不合規,采用這樣的數據進行微調顯然有些不合理。
后來干脆就不作微調了,直接將level和content文本送入預訓練模型,獲取文本的sentence embedding,然后計算(level_1, content),(level_2, content),(level_3, content)和(level_4, content)之間的cos similar,將sentence embedding降維后同cos similar一起作為最終LGB模型的輸入,結果發現cos similar還是有些用的,但是embedding不是太好用,最終模型只使用了level和content的similar特征。
計算sentence embedding和similar的代碼如下:
def get_embedding(data):"""獲取文本的sentence embedding和cos similar"""sentences1, sentences2 = [], []for i in data:sentences1.append(i[0])sentences2.append(i[1])# Sentences are encoded by calling model.encode()emb1 = model.encode(sentences1, convert_to_tensor=True)emb2 = model.encode(sentences2, convert_to_tensor=True)cos_sim = torch.cosine_similarity(emb1, emb2, dim=1)return emb1, emb2, cos_simtr_emb1, tr_emb2, tr_cos_sim = get_embedding(train_all_data) ts_emb1, ts_emb2, ts_cos_sim = get_embedding(test_data)emb1_cat = np.concatenate((tr_emb1, ts_emb1), axis=0) emb2_cat = np.concatenate((tr_emb2, ts_emb2), axis=0)# 保存embedding和cosine-similarity from sklearn.decomposition import TruncatedSVDdf_tr = tr_data[['id']].copy() df_ts = ts_data[['id']].copy() n_components = 16 svd = TruncatedSVD(n_components=n_components)svd.fit(emb1_cat) emb1_cat_svd = svd.transform(emb1_cat) emb1_tr_svd = emb1_cat_svd[:tr_data.shape[0], :] emb1_ts_svd = emb1_cat_svd[tr_data.shape[0]:, :]for i in range(n_components):df_tr[f'l4_emb_{i}'] = emb1_tr_svd[:, i]df_ts[f'l4_emb_{i}'] = emb1_ts_svd[:, i]svd.fit(emb2_cat) emb2_cat_svd = svd.transform(emb2_cat) emb2_tr_svd = emb2_cat_svd[:tr_data.shape[0], :] emb2_ts_svd = emb2_cat_svd[tr_data.shape[0]:, :]for i in range(n_components):df_tr[f'ct_emb_{i}'] = emb2_tr_svd[:, i]df_ts[f'ct_emb_{i}'] = emb2_ts_svd[:, i]df_tr['cos_sim'] = tr_cos_sim df_ts['cos_sim'] = ts_cos_sim如上介紹了使用bert提取文本特征的方法,最終選擇進入LGB模型的特征有:
- single sentence classification的預測結果
- sentence pair classification的預測結果
- sbert提取的level和sentence之間的相似度
數據和代碼參見sodic_enterprise_hidden_dangers。
參考和引用:
[1] 自然語言處理中的Transformer和BERT
[2] 基于雙塔SBERT模型的智能語義計算實驗
[3] 一文看懂 Attention
[4] 文本預處理:詞袋模型(bag of words,BOW)、TF-IDF
[5] Simple Transformer:用BERT、RoBERTa、XLNet、XLM進行多類文本分類
[6] simpletransformers Documentation
總結
以上是生活随笔為你收集整理的企业隐患排查文本挖掘比赛(二):算法篇(从词向量到BERT)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CPython
- 下一篇: 模拟彩票摇号的小游戏(31选7)