使用keras-bert进行中文文本分类+Google colab运行源码
前文介紹了BERT的原理。在實際應用中,BERT要比其理論本身要簡單的多。這里我們利用Github的中文BERT預訓練的結果(地址),進行實際的文檔分類。
數據集
為了便于進行比較,文檔分類的數據集來自Github的這個地址。該數據集使用了THUCNews的一個子集,使用了其中的10個分類,每個分類6500條數據。
類別如下:
體育, 財經, 房產, 家居, 教育, 科技, 時尚, 時政, 游戲, 娛樂
數據集劃分如下:
- 訓練集: 5000*10
- 驗證集: 500*10
Github上該數據集的地址在百度網盤上,可以用multicloud直接將百度網盤的文件傳到google drive上。
基本結構
利用BERT的預訓練模型進行文檔分類,其原理如下:
其原理是在BERT的Transform Layer的最高一層的第一個輸出,添加Dense+Softmax對文檔進行分類,loss函數取cross entropy函數。因此,真正的核心代碼也就如下幾行:
預訓練的模型采用了小參量的模型RBT3,RBT3實際上只有3個transform layer,而不是完整的12個transform layer。因此,只能算是“低配窮人版”的BERT,但從后面的結果來看表現已經相當不錯了。
框架
為了實現上述功能,本文采用了Keras框架。Keras框架的好處是糙快猛,便于深度學習的入門者快速上手,而針對BERT模型,也已經有開源的keras-bert可以直接拿來使用。
平臺
源代碼在Google colab平臺上,使用Google免費提供的GPU跑通。本來本人想試TPU的,但是發現依據keras-bert建議的TPU訓練代碼,模型無法收斂,因此還有待牛人進一步解決了…
代碼
接下來直接上代碼。
下載數據到Colab目錄
首先將RBT3對應的google drive的地址,直接add到自己的google drive賬號里,這樣就可以不用下載,直接在colab上將文件解壓就可以。
!cp drive/My\ Drive/chinese_rbt3_L-3_H-768_A-12.zip . !mkdir cnews !mkdir model !cp drive/My\ Drive/cnews/* cnews/ !rm -rf model !unzip chinese_rbt3_L-3_H-768_A-12.zip -d model/輸出
Archive: chinese_rbt3_L-3_H-768_A-12.zipinflating: model/bert_config_rbt3.json inflating: model/bert_model.ckpt.data-00000-of-00001 inflating: model/__MACOSX/._bert_model.ckpt.data-00000-of-00001 inflating: model/bert_model.ckpt.index inflating: model/__MACOSX/._bert_model.ckpt.index inflating: model/bert_model.ckpt.meta inflating: model/__MACOSX/._bert_model.ckpt.meta inflating: model/vocab.txt inflating: model/__MACOSX/._vocab.txt安裝庫
!pip install --upgrade --force-reinstall keras-bert keras-rectified-adam導入模塊
import numpy as np import os import keras # from tensorflow.python import keras from keras_bert import load_trained_model_from_checkpoint #用于加載預訓練的bert from keras_bert import get_pretrained, PretrainedList, get_checkpoint_paths from keras_bert import Tokenizer #用于對輸入的文章“分詞” import codecs from keras.preprocessing.sequence import pad_sequences注意到keras-bert引用了兩種keras庫:
- tensorflow.python.keras:
- keras:
keras-bert默認使用的是keras庫,因此模型也要對應import keras。如果在python里執行
那么接下來模型要import tensorflow.python.keras。如果弄混了,在build model的時候就會有千奇百怪的錯誤。
如果采用colab的TPU,就需要用tensorflow.python.keras包了,因為在keras本身對TPU支持目前還有限,對TPU的操作主要依賴于tensorflow,而且還必須是tensorflow 1.x的版本,tensorflow 2.x版本目前對TPU的支持不好。目前colab默認的還是tensorflow 1.x的版本。
配置數據的路徑
# 與訓練的bert model的路徑 model_path="model/" paths = get_checkpoint_paths(model_path) print(paths.config, paths.checkpoint, paths.vocab) # 下載的數據集的路徑 base_dir = 'cnews' train_dir = os.path.join(base_dir, 'cnews.train.txt') test_dir = os.path.join(base_dir, 'cnews.test.txt') val_dir = os.path.join(base_dir, 'cnews.val.txt') vocab_dir = os.path.join(base_dir, 'cnews.vocab.txt')數據預處理
def read_file(filename):"""讀取文件數據"""contents, labels = [], []with open(filename, encoding='utf-8') as f:for line in f:try:label, content = line.strip().split('\t')if content:contents.append(content)labels.append(label)except:passreturn contents, labels # 從文件中讀取文本內容和對應的分類標簽 train_contents, train_labels=read_file(train_dir) validate_contents, validate_labels=read_file(val_dir)# 讀取預訓練模型的“分詞器” token_dict = {} with codecs.open(paths.vocab, 'r', 'utf8') as reader:for line in reader:token = line.strip()token_dict[token] = len(token_dict) tokenizer = Tokenizer(token_dict) tokenizer.tokenize(u'今天天氣不錯')注意到“分詞器”針對“今天天氣不錯”會輸出:
['[CLS]', '今', '天', '天', '氣', '不', '錯', '[SEP]']這里也可以看到BERT的“分詞”實際上是將句子中的每一個漢字都拆成了單獨的一個字符。這實際上也是前面提到的“基于字符的”神經網絡模型。2019年已經有文獻證明,針對漢字,基于字符的NLP要比基于分詞的NLP訓練效果要更好,因此BERT的中文NLP也采用了這種技術。
def get_id_segments(contents):ids, segments = [],[]for sent in contents:id, segment = tokenizer.encode(sent)ids.append(id)segments.append(segment)return ids, segments train_ids,train_segments = get_id_segments(train_contents) validate_ids, validate_segments = get_id_segments(validate_contents) print(train_contents[0], train_ids[0])輸出
馬曉旭意外受傷讓國奧警惕 無奈大雨格外青睞殷家軍記者傅亞雨沈陽報道 來到沈陽,國奧隊依然沒有擺脫雨水的困擾。7月31日下午6點,國奧隊的日常訓練再度受到大雨的干擾,無奈之下隊員們只慢跑了25分鐘就草草收場。31日上午10點,國奧隊在奧體中心外場訓練的時候,天就是陰沉沉的,氣象預報顯示當天下午沈陽就有大雨,但幸好隊伍上午的訓練并沒有受到任何干擾。下午6點,當球隊抵達訓練場時,大雨已經下了幾個小時,而且絲毫沒有停下來的意思。抱著試一試的態度,球隊開始了當天下午的例行訓練,25分鐘過去了,天氣沒有任何轉好的跡象,為了保護球員們,國奧隊決定中止當天的訓練,全隊立即返回酒店。在雨中訓練對足球隊來說并不是什么稀罕事,但在奧運會即將開始之前,全隊變得“嬌貴”了。在沈陽最后一周的訓練,國奧隊首先要保證現有的球員不再出現意外的傷病情況以免影響正式比賽,因此這一階段控制訓練受傷、控制感冒等疾病的出現被隊伍放在了相當重要的位置。而抵達沈陽之后,中后衛馮蕭霆就一直沒有訓練,馮蕭霆是7月27日在長春患上了感冒,因此也沒有參加29日跟塞爾維亞的熱身賽。隊伍介紹說,馮蕭霆并沒有出現發燒癥狀,但為了安全起見,這兩天還是讓他靜養休息,等感冒徹底好了之后再恢復訓練。由于有了馮蕭霆這個例子,因此國奧隊對雨中訓練就顯得特別謹慎,主要是擔心球員們受涼而引發感冒,造成非戰斗減員。而女足隊員馬曉旭在熱身賽中受傷導致無緣奧運的前科,也讓在沈陽的國奧隊現在格外警惕,“訓練中不斷囑咐隊員們要注意動作,我們可不能再出這樣的事情了。”一位工作人員表示。從長春到沈陽,雨水一路伴隨著國奧隊,“也邪了,我們走到哪兒雨就下到哪兒,在長春幾次訓練都被大雨給攪和了,沒想到來沈陽又碰到這種事情。”一位國奧球員也對雨水的“青睞”有些不解。 [ 101 7716 3236 3195 2692 1912 1358 839 6375 1744 1952 6356 2664 31871937 1920 7433 3419 1912 7471 4712 3668 2157 1092 6381 5442 987 7627433 3755 7345 2845 6887 3341 1168 3755 7345 8024 1744 1952 7339 8984197 3766 3300 3030 5564 7433 3717 4638 1737 2817 511 128 3299 81763189 678 1286 127 4157 8024 1744 1952 7339 4638 3189 2382 6378 52981086 2428 1358 1168 1920 7433 4638 2397 2817 8024 3187 1937 722 6787339 1447 812 1372 2714 6651 749 8132 1146 7164 2218 5770 5770 31191767 511 8176 3189 677 1286 8108 4157 8024 1744 1952 7339 1762 1952860 704 2552 1912 1767 6378 5298 4638 3198 952 8024 1921 2218 32217346 3756]這里主要是將讀取到的文章的文本轉換成id,這個id才是BERT模型需要的真正的輸入。
同時每一個文章長度都是不一樣的,這里簡單的畫了下各文章經過tokenizer后長度的分布:
其中橫軸是文章數,縱軸是樣本的個數。這里可以看到有相當的文章的"字“數都超過了1000字。但是BERT模型最長也就只能輸入512,同時考慮到Colab上GPU的內存的限制,真正輸入的文章長度要更短。
2019年已經有論文對文本分類要截取的“字數”進行了討論(原文),論文針對IMDB上的影評,考慮了三種情況:
- 截取文章頭部510個token
- 截取文章尾部510個token
- 截取文章頭部128個token和尾部382個token。
發現第三種情況效果是最好的。
由于我們處理的新聞文章,新聞文章的特點一般都是開宗明義,所以我這里只取了文章頭部128個token,因此代碼:
這里用了keras自帶的pad_sequences函數。這個函數對超過指定長度的會截斷,沒到指定長度的會補0,返回numpy數組。這樣就不用自己hard code了。
此外,還要將讀取到的文章分類也要轉為數字:
加載BERT預訓練模型
加載BERT預訓練模型,實際上調用的就是keras-bert的函數就可以。
bert_model = load_trained_model_from_checkpoint(paths.config.replace('.json','_rbt3.json'), paths.checkpoint, seq_len=None) for i,layer in enumerate(bert_model.layers):layer.trainable = True調用完后,BERT各層默認是constant,即不參與接下來的訓練。從實際使用的情況看,如果只訓練自己添加的那幾層,幾乎達不到分類的效果,因此這里我們采用了所有層都參與訓練。訓練的層越多,理論上效果越好,但也要考慮到過擬合、以及對硬件資源占用的問題。(之前嘗試采用“高配旗艦版”的24個transformer layer的BERT,把gpu搞崩了好幾次)由于我們的這個BERT的模型比較精簡,因此所有層參與訓練的問題不大。
修改BERT模型
修改BERT模型,即前面提到的代碼
inputs = bert_model.inputs[:2] x=bert_model.layers[-1].output # if returns tuple, then we are using keras lib # if returns KerasHistory, then we are using tensorflow.python.keras lib print(type(x._keras_history)) x=keras.layers.Lambda(lambda x: x[:, 0], name='slice')(x) x=keras.layers.Dense(units=3072, activation=keras.backend.tanh)(x) x=keras.layers.Dropout(rate=0.1,seed=2019)(x) x=keras.layers.Dense(units=num_labels, activation=keras.backend.softmax)(x) model=keras.Model(inputs, x) model.compile(optimizer=keras.optimizers.Adam(1e-4),loss=keras.losses.sparse_categorical_crossentropy,metrics=[keras.metrics.sparse_categorical_accuracy]) model.summary()會輸出
__________________________________________________________________________________________________ Layer (type) Output Shape Param # Connected to ================================================================================================== Input-Token (InputLayer) (None, None) 0 __________________________________________________________________________________________________ Input-Segment (InputLayer) (None, None) 0 __________________________________________________________________________________________________ Embedding-Token (TokenEmbedding [(None, None, 768), 16226304 Input-Token[0][0] __________________________________________________________________________________________________ Embedding-Segment (Embedding) (None, None, 768) 1536 Input-Segment[0][0] __________________________________________________________________________________________________ Embedding-Token-Segment (Add) (None, None, 768) 0 Embedding-Token[0][0] Embedding-Segment[0][0] __________________________________________________________________________________________________ Embedding-Position (PositionEmb (None, None, 768) 393216 Embedding-Token-Segment[0][0] __________________________________________________________________________________________________ Embedding-Dropout (Dropout) (None, None, 768) 0 Embedding-Position[0][0] __________________________________________________________________________________________________ Embedding-Norm (LayerNormalizat (None, None, 768) 1536 Embedding-Dropout[0][0] __________________________________________________________________________________________________ Encoder-1-MultiHeadSelfAttentio (None, None, 768) 2362368 Embedding-Norm[0][0] __________________________________________________________________________________________________ Encoder-1-MultiHeadSelfAttentio (None, None, 768) 0 Encoder-1-MultiHeadSelfAttention[ __________________________________________________________________________________________________ Encoder-1-MultiHeadSelfAttentio (None, None, 768) 0 Embedding-Norm[0][0] Encoder-1-MultiHeadSelfAttention- __________________________________________________________________________________________________ Encoder-1-MultiHeadSelfAttentio (None, None, 768) 1536 Encoder-1-MultiHeadSelfAttention- __________________________________________________________________________________________________ Encoder-1-FeedForward (FeedForw (None, None, 768) 4722432 Encoder-1-MultiHeadSelfAttention- __________________________________________________________________________________________________ Encoder-1-FeedForward-Dropout ( (None, None, 768) 0 Encoder-1-FeedForward[0][0] __________________________________________________________________________________________________ Encoder-1-FeedForward-Add (Add) (None, None, 768) 0 Encoder-1-MultiHeadSelfAttention-Encoder-1-FeedForward-Dropout[0][ __________________________________________________________________________________________________ Encoder-1-FeedForward-Norm (Lay (None, None, 768) 1536 Encoder-1-FeedForward-Add[0][0] __________________________________________________________________________________________________ Encoder-2-MultiHeadSelfAttentio (None, None, 768) 2362368 Encoder-1-FeedForward-Norm[0][0] __________________________________________________________________________________________________ Encoder-2-MultiHeadSelfAttentio (None, None, 768) 0 Encoder-2-MultiHeadSelfAttention[ __________________________________________________________________________________________________ Encoder-2-MultiHeadSelfAttentio (None, None, 768) 0 Encoder-1-FeedForward-Norm[0][0] Encoder-2-MultiHeadSelfAttention- __________________________________________________________________________________________________ Encoder-2-MultiHeadSelfAttentio (None, None, 768) 1536 Encoder-2-MultiHeadSelfAttention- __________________________________________________________________________________________________ Encoder-2-FeedForward (FeedForw (None, None, 768) 4722432 Encoder-2-MultiHeadSelfAttention- __________________________________________________________________________________________________ Encoder-2-FeedForward-Dropout ( (None, None, 768) 0 Encoder-2-FeedForward[0][0] __________________________________________________________________________________________________ Encoder-2-FeedForward-Add (Add) (None, None, 768) 0 Encoder-2-MultiHeadSelfAttention-Encoder-2-FeedForward-Dropout[0][ __________________________________________________________________________________________________ Encoder-2-FeedForward-Norm (Lay (None, None, 768) 1536 Encoder-2-FeedForward-Add[0][0] __________________________________________________________________________________________________ Encoder-3-MultiHeadSelfAttentio (None, None, 768) 2362368 Encoder-2-FeedForward-Norm[0][0] __________________________________________________________________________________________________ Encoder-3-MultiHeadSelfAttentio (None, None, 768) 0 Encoder-3-MultiHeadSelfAttention[ __________________________________________________________________________________________________ Encoder-3-MultiHeadSelfAttentio (None, None, 768) 0 Encoder-2-FeedForward-Norm[0][0] Encoder-3-MultiHeadSelfAttention- __________________________________________________________________________________________________ Encoder-3-MultiHeadSelfAttentio (None, None, 768) 1536 Encoder-3-MultiHeadSelfAttention- __________________________________________________________________________________________________ Encoder-3-FeedForward (FeedForw (None, None, 768) 4722432 Encoder-3-MultiHeadSelfAttention- __________________________________________________________________________________________________ Encoder-3-FeedForward-Dropout ( (None, None, 768) 0 Encoder-3-FeedForward[0][0] __________________________________________________________________________________________________ Encoder-3-FeedForward-Add (Add) (None, None, 768) 0 Encoder-3-MultiHeadSelfAttention-Encoder-3-FeedForward-Dropout[0][ __________________________________________________________________________________________________ Encoder-3-FeedForward-Norm (Lay (None, None, 768) 1536 Encoder-3-FeedForward-Add[0][0] __________________________________________________________________________________________________ slice (Lambda) (None, 768) 0 Encoder-3-FeedForward-Norm[0][0] __________________________________________________________________________________________________ dense_11 (Dense) (None, 3072) 2362368 slice[0][0] __________________________________________________________________________________________________ dropout_6 (Dropout) (None, 3072) 0 dense_11[0][0] __________________________________________________________________________________________________ dense_12 (Dense) (None, 10) 30730 dropout_6[0][0] ================================================================================================== Total params: 40,279,306 Trainable params: 40,279,306 Non-trainable params: 0注意最后三行的提示,即所有的參數都參與了訓練。
這里還有個tip,構建模型時,如果直接將代碼
替換成
x=bert_model(inputs)這樣不會對model實際訓練造成影響,但是在打印model.summary()時,bert內部的各層只會顯示為一層bert_model。這樣也就不方便查看了。
開始訓練
batch_size = 64 train_size = (train_ids.shape[0] // batch_size) * batch_size validate_size = (validate_ids.shape[0] // batch_size) * batch_sizeclass TrainHistory(keras.callbacks.Callback):def on_train_begin(self, logs={}):self.train_loss = []self.train_acc = []def on_batch_end(self, batch, logs={}):self.train_loss.append(logs.get('loss'))self.train_acc.append(logs.get('sparse_categorical_accuracy'))history = TrainHistory()model.fit(x=[train_ids[:train_size], train_segments[:train_size]],y=np.array(train_labelids[:train_size]),validation_data=[[validate_ids[:validate_size], validate_segments[:validate_size]],np.array(validate_labelids[:validate_size])],batch_size = batch_size,callbacks=[history])Keras最讓人感到激動的一點是,訓練只需要一行代碼fit就解決了。這里實際上可以直接將訓練集和測試集扔進去也沒有問題。之所以我把輸入截斷成batch_size的整數倍,是由于TPU的輸入是這樣要求的。但由于TPU試驗失敗,但是代碼還是保留了。
另外,為了記錄訓練時的loss和accuracy,自定義了TransHistory類。在完成訓練后,就可以畫圖表示loss和accuracy的收斂過程了。
訓練結果輸出
Train on 49984 samples, validate on 4992 samples Epoch 1/1 49984/49984 [==============================] - 199s 4ms/step - loss: 0.1682 - sparse_categorical_accuracy: 0.9495 - val_loss: 0.1362 - val_sparse_categorical_accuracy: 0.9619 <keras.callbacks.callbacks.History at 0x7f6e82390080>這里為了節省時間,只訓練了一個epoch,使用google colab的GPU 3分多鐘就跑完了,測試集上準確率達到96.19%,而github上采用cnn訓練了3個epoch,才最多達到94.12%,效果還是很好的。
同時圖像上也做了比較:
從圖像上看,模型在100個batch左右,即輸入6400個樣本的時候,就可以訓練到準確率90%左右,這樣也說明了BERT在下游訓練的優勢還是很明顯的。這也是最近兩年bert如此火的原因吧。
總結
以上是生活随笔為你收集整理的使用keras-bert进行中文文本分类+Google colab运行源码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【C++复习总结回顾】—— 【五】数组与
- 下一篇: 新旧电脑文件转移