NLP文字分類學習筆記7:基於預訓練模型的文字分類
預訓練模型
預訓練是一種遷移學習的思想,在一個大資料集上訓練大模型,之後可以利用這個訓練好的模型處理其他任務。預訓練模型的使用方法一般有:
- 用作特徵提取:利用預訓練模型提取資料特徵,再將這些特徵用作自己模型的訓練,如word2vec,GLOVE
- 使用模型結構引數:使用預訓練模型的結構和引數,再輸入自己的資料對模型引數進行訓練調整
- 使用模型結構引數,但凍結部分層的引數:使用預訓練模型的結構和引數,再輸入自己的資料,但不改變凍結層的引數
ELMO
在NLP文字分類學習筆記1中,介紹了one-hot編碼及之後的word2vec詞嵌入向量,但是這種詞的表示使得一個詞只對應一個詞向量,不能能理解同一單詞在不同語境下的同一含義
ELMO則提供一個預訓練模型,應用時根據輸入動態調整原先詞向量(主要是調整下面第三步所說的權重),生成新詞向量,更能反映上下文資訊,解決一詞多義的問題
該模型主要包含三部分:
1、字元編碼層。利用卷積對字元訓練,提取特徵向量
2、使用基於深層的(縱向和橫向有很多層)雙向的(兩個獨立的單向)LSTM
3、利用LSTM每一層的輸出和開始提取的特徵向量通過加權操作得到最後的詞嵌入向量。在實際使用時,需要再次訓練去得到這個新權重
GPT
GPT實際上是一個更大的transformer的解碼器decoder(但是去掉了中間的多頭自注意力層)
預訓練
預訓練任務是給定句子前面的詞來預測之後的詞(因為transformer的decoder是使用帶mask多頭注意力機制,所以自然預訓練採用這樣的任務,因此GPT模型只考慮了單向語言模型)
如下公式所示,句子序列U中詞分別化為嵌入向量並加上位置嵌入向量\(W_p\)
微調
微調時基本相同,任務變為了輸入一個句子序列來預測一個標籤。
所以之後我們使用該預訓練模型來進行微調,都要先通過這種形式來使用,如下圖所示,在做分類時,在句子前後各加兩個記號start與extract,再輸入到模型中,最後使用extract對應的輸出(也就是最後一個輸出)通過Linear層來進行分類
BERT
BERT實際上是一個更大的transformer的編碼器encoder,輸入是一個句子對。
- 為了區分句子對,每一對加入segment embeddings,前一句和後一句分別用0和1表示
- 句子對開始加入<cls>標記,句子間和尾部加入<sep>標記
- 原本的位置嵌入position emebeddings變為可學習的引數
將這三部分相加後作為最終的輸入,如下所示
預訓練
模型通過兩個任務進行預訓練:
1、帶掩碼的語言模型
- 每次隨機以一定概率選中句子中一些詞,以80%概率將選中的詞替換為標記<mask>,以10%的概率替換為一個隨機的詞,10%的概率保持不變
- 採取在句子中挖空,再預測空中的詞填空,使得它考慮了句子中前後的聯絡,成為雙向的模型
- 不百分百挖空是由於之後的微調任務中不會出現<mask>標記,避免之後因此出現的誤差
因此bert與GPT相比,bert是一個雙向的語言模型(掩碼語言模型,MLM),通過左右的語義來填空,而GPT是單向的,只用現在預測未來。而Elmo雖然考慮了雙向,但是基於LSTM的架構,最後提取的雙向特徵只是簡單拼在一起使用,直觀上效果不如bert好(實際還是單向語言模型)
2、預測下一句
- 50%的概率輸入相鄰的句子對,50%的概率輸入不相鄰的句子對
- 最後將<cls>標記對應的輸出,放到全連線層進行預測訓練
微調
微調時與GPT相同,也需要根據不同任務進行改造,對於分類任務,輸入句子在開始加入<cls>標記,句子間和尾部加入<sep>標記。最後的輸出的第一個序列(也就是<cls>對應的輸出)輸入到其它結構,或者直接連線全連線層進行分類。
ALBert
對與Bert主要有以下改變:
- 針對嵌入層引數因式分解,原始bert的嵌入層和隱藏層引數維數相同,但是隱藏層要學習上下文之間的聯絡,因此應該變大,但變大了會導致詞嵌入矩陣變大,影響反向傳播,所以先將詞嵌入矩陣乘以一個矩陣進行降維,再乘以矩陣提升到隱藏層所需要的維度
- 允許各層共享自注意力層和全連線層(Feed-Forward Networks)的引數
- 將原始bert中預測下一句的任務變為預測連貫性的任務:輸入兩個連貫的句子和輸入交換前後順序的顛倒的兩個句子
因此ALBert相當於變“寬”了,但是對比bert,Albert的引數更少
Bert-wwm
針對中文對bert的升級,原始的bert對一個token(即一個英文單詞)進行mask沒有問題,但對中文,就是對一個字mask,這樣就產生問題。該模型在預訓練時對一個詞進行mask。
pytorch實現基於BERT的文字分類
使用bert base版本進行訓練,不更新預訓練模型的引數,只加了全連線層用於分類,在10分類的任務中,對於測試集準確率為76.79%。猜測效果不好原因是:資料量不夠,訓練資料只有4萬,引數卻有1億,顯然是不行的。但是目前只有CPU,跑了一天半,暫時證明能跑通吧。
使用ALbert進行訓練,不更新預訓練模型的引數,只加了全連線層用於分類,在10分類的任務中,對於測試集準確率為61.65%,但是更新預訓練模型的引數,準確率達到了86.26%
bert.py
使用Hugging Face預訓練模型中的bert-base-chinese,可以下載下來使用
文件引數參考
import torch
import torch.nn as nn
from transformers import BertModel,BertTokenizer
class Config(object):
def __init__(self):
self.pre_bert_path="C:/Users/DELL/Downloads/bert-base-chinese"
self.train_path = 'data/dataset_train.csv' # 訓練集
self.dev_path = 'data/dataset_valid.csv' # 驗證集
self.test_path = 'data/test.csv' # 測試集
self.class_path = 'data/class.json' # 類別名單
self.save_path ='mymodel/bert.pth' # 模型訓練結果
self.num_classes=10
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 裝置
self.epochs = 10 # epoch數
self.batch_size = 128 # mini-batch大小
self.maxlen = 32 # 每句話處理成的長度(短填長切)
self.learning_rate = 5e-4 # 學習率
self.hidden_size=768
self.tokenizer = BertTokenizer.from_pretrained(self.pre_bert_path)
class Model(nn.Module):
def __init__(self, config):
super(Model, self).__init__()
self.bert=BertModel.from_pretrained(config.pre_bert_path)
#設定不更新預訓練模型的引數
for param in self.bert.parameters():
param.requires_grad = False
self.fc = nn.Linear(config.hidden_size, config.num_classes)
def forward(self, input):
out=self.bert(input_ids =input['input_ids'],attention_mask=input['attention_mask'],token_type_ids=input['token_type_ids'])
#只取最後一層CLS對應的輸出
out = self.fc(out.pooler_output)
return out
ALbert.py
ALbert的結構,使用使用Hugging Face預訓練模型中的clue/albert_chinese_tiny
import torch.nn as nn
import torch
from transformers import BertTokenizer, AlbertModel
class Config(object):
def __init__(self):
self.pre_bert_path="clue/albert_chinese_tiny"
self.train_path = 'data/dataset_train.csv' # 訓練集
self.dev_path = 'data/dataset_valid.csv' # 驗證集
self.test_path = 'data/test.csv' # 測試集
self.class_path = 'data/class.json' # 類別名單
self.save_path ='mymodel/albert.pth' # 模型訓練結果
self.num_classes=10
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 裝置
self.dropout = 0.5 # 隨機失活
self.epochs = 10 # epoch數
self.batch_size = 128 # mini-batch大小
self.maxlen = 32 # 每句話處理成的長度(短填長切)
self.learning_rate = 5e-4 # 學習率
self.hidden_size=312
self.tokenizer = BertTokenizer.from_pretrained(self.pre_bert_path)
class Model(nn.Module):
def __init__(self, config):
super(Model, self).__init__()
self.bert= AlbertModel.from_pretrained(config.pre_bert_path)
for param in self.bert.parameters():
param.requires_grad = False
self.fc = nn.Linear(config.hidden_size, config.num_classes)
def forward(self, input):
out=self.bert(input_ids =input['input_ids'],attention_mask=input['attention_mask'],token_type_ids=input['token_type_ids'])
out = self.fc(out.pooler_output)
return out
runBert.py
用於執行程式碼模型,更多程式碼詳情見NLP學習筆記0
import json
from mymodel import myBert,myAlbertl
import mydataset
import torch
import pandas as pd
from torch import nn,optim
from torch.utils.data import DataLoader
# config=myBert.Config()
config=myAlbertl.Config()
label_dict=json.load(open(config.class_path,'r',encoding='utf-8'))
# 載入訓練,驗證,測試資料集
train_df = pd.read_csv(config.train_path)
#這裡將標籤轉化為數字
train_ds=mydataset.GetLoader(train_df['review'],[label_dict[i] for i in train_df['cat']])
train_dl=DataLoader(train_ds,batch_size=config.batch_size,shuffle=True)
valid_df = pd.read_csv(config.dev_path)
valid_ds=mydataset.GetLoader(valid_df['review'],[label_dict[i] for i in valid_df['cat']])
valid_dl=DataLoader(valid_ds,batch_size=config.batch_size,shuffle=True)
test_df = pd.read_csv(config.test_path)
test_ds=mydataset.GetLoader(test_df['review'],[label_dict[i] for i in test_df['cat']])
test_dl=DataLoader(test_ds,batch_size=config.batch_size,shuffle=True)
#計算準確率
def accuracys(pre,label):
pre=torch.max(pre.data,1)[1]
accuracy=pre.eq(label.data.view_as(pre)).sum()
return accuracy,len(label)
#匯入網路結構
# model=myBert.Model(config).to(config.device)
model=myAlbertl.Model(config).to(config.device)
#訓練
criterion=nn.CrossEntropyLoss()
optimizer=optim.Adam(model.parameters(),lr=config.learning_rate)
best_loss=float('inf')
for epoch in range(config.epochs):
train_acc = []
for batch_idx,(data,target)in enumerate(train_dl):
inputs = config.tokenizer(data,truncation=True, return_tensors="pt",padding=True,max_length=config.maxlen)
model.train()
out = model(inputs)
loss=criterion(out,target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_acc.append(accuracys(out,target))
train_r = (sum(tup[0] for tup in train_acc), sum(tup[1] for tup in train_acc))
print('當前epoch:{}\t[{}/{}]{:.0f}%\t損失:{:.6f}\t訓練集準確率:{:.2f}%\t'.format(
epoch, batch_idx, len(train_dl), 100. * batch_idx / len(train_dl), loss.data,
100. * train_r[0].numpy() / train_r[1]
))
#每100批次進行一次驗證
if batch_idx%100==0 and batch_idx!=0:
model.eval()
val_acc=[]
loss_total=0
with torch.no_grad():
for (data,target) in valid_dl:
inputs = config.tokenizer(data, truncation=True, return_tensors="pt", padding=True,
max_length=config.maxlen)
out = model(inputs)
loss_total = criterion(out, target).data+loss_total
val_acc.append(accuracys(out,target))
val_r = (sum(tup[0] for tup in val_acc), sum(tup[1] for tup in val_acc))
print('損失:{:.6f}\t驗證集準確率:{:.2f}%\t'.format(loss_total/len(valid_dl),100. * val_r[0].numpy() / val_r[1]))
#如果驗證損失低於最好損失,則儲存模型
if loss_total < best_loss:
best_loss = loss_total
torch.save(model.state_dict(), config.save_path)
#測試
model.load_state_dict(torch.load(config.save_path))
model.eval()
test_acc=[]
with torch.no_grad():
for (data, target) in test_dl:
inputs = config.tokenizer(data,truncation=True, return_tensors="pt",padding=True,max_length=config.maxlen)
out = model(inputs)
test_acc.append(accuracys(out, target))
test_r = (sum(tup[0] for tup in test_acc), sum(tup[1] for tup in test_acc))
print('測試集準確率:{:.2f}%\t'.format(100. * test_r[0].numpy() / test_r[1]))
參考
https://zhuanlan.zhihu.com/p/49271699(講的很好)
http://www.sniper97.cn/index.php/note/deep-learning/bert/3836/
https://zhuanlan.zhihu.com/p/56382372
https://blog.csdn.net/libaominshouzhang/article/details/102995213
https://blog.csdn.net/lch551218/article/details/116243502
https://www.cnblogs.com/sandwichnlp/p/11947627.html