pytorch+huggingface實現基於bert模型的文本分類(附代碼)


從RNN到BERT

一年前的這個時候,我逃課了一個星期,從澳洲飛去上海觀看電競比賽,也順便在上海的一個公司聯系了面試。當時,面試官問我對RNN的了解程度,我回答“沒有了解”。但我把這個問題帶回了學校,從此接觸了RNN,以及它的加強版-LSTM。

時隔一年,LSTM好像已經可以退出歷史舞台。BERT站在了舞台中間,它可以更快且更好的解決NLP問題。我打算以邊學習邊分享的方式,用BERT(GTP-2)過一遍常見的NLP問題。這一篇博客是文本分類的baseline system。

BERT

如果你熟悉transformer,相信理解bert對你來說沒有任何難度。bert就是encoder的堆疊。

如果你不熟悉transformer,這篇文章是我見過的最棒的transformer圖解,可以幫助你理解:http://jalammar.github.io/illustrated-transformer/ 

當然這個作者也做出了很棒的bert圖解,鏈接在此:http://jalammar.github.io/illustrated-bert/

BERT做文本分類

bert是encoder的堆疊。當我們向bert輸入一句話,它會對這句話里的每一個詞(嚴格說是token,有時也被稱為word piece)進行並列處理,並為每個詞輸出對應的向量。我們給輸入文本的句首添加一個[CLS] token(CLS為classification的縮寫),然后我們只考慮這個CLS token的對應輸出,用它來做classifier的輸入,最終輸出具體的分類。

 

 

使用Huggingface

Huggingface可以幫助我們輕易的完成文本分類任務。 

通過它,我們可以輕松的讀取預訓練語言模型,以及使用它自帶的文本分類bert模型-BertForSequenceClassification

 

 

 

 

 

正式開始解決問題

數據介紹

數據來自Kaggle的competition:Real or Not? NLP with Disaster Tweets  鏈接:https://www.kaggle.com/c/nlp-getting-started

這是推特的數據集,數據的格式如下:

id location keyword text target
1 聖地亞哥 大火 聖地亞哥國家公園出現嚴重森林大火 1
2 硅谷 沙灘 今天在硅谷的沙灘曬太陽真開心 0

 

 

 

 

我們需要做的,就是根據推文的location、keyword 以及 text 來判斷這篇推文是否和災難有關。

它的現實意義在於,如果我們能夠根據推文來第一時間發現災難,有關部門就可以快速做出反應,將災難的損失降低到最小。就像前段時間溫嶺油罐車爆炸,群眾第一時間就把信息、視頻上傳到了微博,消防部門可以通過微博獲取信息。

探索式資料分析(EDA)與數據清理

在拿到數據后,我們需要進行探索式資料分析。由於這不是本篇博客最重要的部分,這里我只給出大體輪廓和結論。在我的kaggle notebook上有詳細的代碼及plot。https://www.kaggle.com/jianweitang/nlp-with-disaster-tweets-eda

我們保留keyword這一列,摒棄location這一列。

有標簽的訓練數據有7613條,無標簽的測試數據有3263條

Training Set Shape: (7613, 5)
Test Set Shape: (3263, 4)

對於location這一列,它具有較多的缺失值,並且有非常多的unique values,暫且認為很難將他與災難直接聯系到一起,我們直接把location這一列摒棄。

Number of unique values in keyword = 222 (Training) - 222 (Test)
Number of unique values in location = 3342 (Training) - 1603 (Test)

 

 

 而對於keyword這一列,它的缺失值很少,unique values有222個。同時它與label之間有可見的相關性,有些詞只在災難推文中出現,有些詞只在非災難推文中出現。如下圖:

 

標簽的分布是均勻的,這意味着我們可以直接把它拿來訓練模型

 

 

 

 

 文本清潔

  • 去除特殊符號
  • 把縮寫及網絡用語展開,例如把 he's 展開為 he is, lmao 展開為 laughing my ass off
  • 把hashtags和usernames展開
  • 糾正錯誤拼寫

推文錯誤標記

在數據中我們發現了重復的text被標記成了不同的標簽,大概有十幾個樣本。這些樣本可能是有爭議,也可能是單純的標記錯誤,在這里我們直接刪掉這些樣本。

 

BERT預處理

import random
import torch
from torch.utils.data import TensorDataset, DataLoader, random_split
from transformers import BertTokenizer
from transformers import BertForSequenceClassification, AdamW
from transformers import get_linear_schedule_with_warmup

seed = 42
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True

device = torch.device('cuda')

 

我們先讀取預訓練的 bert-base-uncased 模型,用來進行分詞,以及詞向量轉化

# Get text values and labels
text_values = train['final_text'].values
labels = train['target'].values

# Load the pretrained Tokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True)  

 

來用這個tokenizer切分數據里的第一條推文試試看

print('Original Text : ', text_values[1])
print('Tokenized Text: ', tokenizer.tokenize(text_values[1]))
print('Token IDs     : ', tokenizer.convert_tokens_to_ids(tokenizer.tokenize(text_values[1])))

輸出:

Original Text :   Forest fire near La Ronge Sask. Canada
Tokenized Text:  ['forest', 'fire', 'near', 'la', 'ron', '##ge', 'sas', '##k', '.', 'canada']
Token IDs     :  [3224, 2543, 2379, 2474, 6902, 3351, 21871, 2243, 1012, 2710]

 

除了分詞以外,我們需要給它添加[CLS]和[SEP],以及[PAD],其中CLS在句首,SEP在句尾,PAD為統一句子長度的padding。這里看看tokenizer會給他們分別怎樣的index。

text = '[CLS]'
print('Original Text : ', text)
print('Tokenized Text: ', tokenizer.tokenize(text))
print('Token IDs     : ', tokenizer.convert_tokens_to_ids(tokenizer.tokenize(text)))
print('\n')

text = '[SEP]'
print('Original Text : ', text)
print('Tokenized Text: ', tokenizer.tokenize(text))
print('Token IDs     : ', tokenizer.convert_tokens_to_ids(tokenizer.tokenize(text)))
print('\n')

text = '[PAD]'
print('Original Text : ', text)
print('Tokenized Text: ', tokenizer.tokenize(text))
print('Token IDs     : ', tokenizer.convert_tokens_to_ids(tokenizer.tokenize(text)))

輸出:

Original Text :  [CLS]
Tokenized Text:  ['[CLS]']
Token IDs     :  [101]


Original Text :  [SEP]
Tokenized Text:  ['[SEP]']
Token IDs     :  [102]


Original Text :  [PAD]
Tokenized Text:  ['[PAD]']
Token IDs     :  [0]

實際使用中,我們用tokenizer.encode()這個function來直接把文本轉化為token_id 並添加special tokens。

我們定義一個encode_fn把數據集的整個文本都轉化為tokens。

# Function to get token ids for a list of texts 
def encode_fn(text_list):
    all_input_ids = []    
    for text in text_list:
        input_ids = tokenizer.encode(
                        text,                      
                        add_special_tokens = True,  # 添加special tokens, 也就是CLS和SEP
                        max_length = 160,           # 設定最大文本長度
                        pad_to_max_length = True,   # pad到最大的長度  
                        return_tensors = 'pt'       # 返回的類型為pytorch tensor
                   )
        all_input_ids.append(input_ids)    
    all_input_ids = torch.cat(all_input_ids, dim=0)
    return all_input_ids

all_input_ids = encode_fn(text_values) labels = torch.tensor(labels)

接下來,我們把數據分為訓練集與驗證集,並構建dataloader。

epochs = 4
batch_size = 32

# Split data into train and validation
dataset = TensorDataset(all_input_ids, labels)
train_size = int(0.90 * len(dataset))
val_size = len(dataset) - train_size
train_dataset, val_dataset = random_split(dataset, [train_size, val_size])

# Create train and validation dataloaders
train_dataloader = DataLoader(train_dataset, batch_size = batch_size, shuffle = True)
val_dataloader = DataLoader(val_dataset, batch_size = batch_size, shuffle = False)

加載與訓練的bert模型, 並定義optimizer與learning rate scheduler

# Load the pretrained BERT model
model = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=2, output_attentions=False, output_hidden_states=False)
model.cuda()

# create optimizer and learning rate schedule
optimizer = AdamW(model.parameters(), lr=2e-5)
total_steps = len(train_dataloader) * epochs
scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=0, num_training_steps=total_steps)

定義一個計算accuracy的方法,方便在訓練的時候print出精確度的變化

from sklearn.metrics import f1_score, accuracy_score

def flat_accuracy(preds, labels):
    
    """A function for calculating accuracy scores"""
    
    pred_flat = np.argmax(preds, axis=1).flatten()
    labels_flat = labels.flatten()
    return accuracy_score(labels_flat, pred_flat)

BERT的訓練與驗證

for epoch in range(epochs):
    model.train()
    total_loss, total_val_loss = 0, 0
    total_eval_accuracy = 0
    for step, batch in enumerate(train_dataloader):
        model.zero_grad()
        loss, logits = model(batch[0].to(device), token_type_ids=None, attention_mask=(batch[0]>0).to(device), labels=batch[1].to(device))
        total_loss += loss.item()
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        optimizer.step() 
        scheduler.step()
        
    model.eval()
    for i, batch in enumerate(val_dataloader):
        with torch.no_grad():
            loss, logits = model(batch[0].to(device), token_type_ids=None, attention_mask=(batch[0]>0).to(device), labels=batch[1].to(device))
                
            total_val_loss += loss.item()
            
            logits = logits.detach().cpu().numpy()
            label_ids = batch[1].to('cpu').numpy()
            total_eval_accuracy += flat_accuracy(logits, label_ids)
    
    avg_train_loss = total_loss / len(train_dataloader)
    avg_val_loss = total_val_loss / len(val_dataloader)
    avg_val_accuracy = total_eval_accuracy / len(val_dataloader)
    
    print(f'Train loss     : {avg_train_loss}')
    print(f'Validation loss: {avg_val_loss}')
    print(f'Accuracy: {avg_val_accuracy:.2f}')
    print('\n')

輸出:

Train loss     : 0.441781023875452
Validation loss: 0.34831519580135745
Accuracy: 0.86


Train loss     : 0.3275374324204257
Validation loss: 0.3286557973672946
Accuracy: 0.88


Train loss     : 0.2503694619696874
Validation loss: 0.355623895690466
Accuracy: 0.86


Train loss     : 0.19663514375973207
Validation loss: 0.3806843503067891
Accuracy: 0.86

這里比較特別的一點是,即使只有4個epochs,validation loss也是一直在增大的。我看了下其他人使用pytorch和huggingface的訓練部分,也存在這個問題,反而使用tensorflow和TFhub的稍微好一點。我猜測這里的原因是過擬合。

 

模型預測

這里與訓練類似,把測試集構建為dataloade,然后將預測結果輸出到csv文件。到這里整個流程就結束了。

# Create the test data loader
text_values = df_test['final_text'].values
all_input_ids = encode_fn(text_values)
pred_data = TensorDataset(all_input_ids)
pred_dataloader = DataLoader(pred_data, batch_size=batch_size, shuffle=False)
model.eval()
preds = []
for i, (batch,) in enumerate(pred_dataloader):
    with torch.no_grad():
        outputs = model(batch.to(device), token_type_ids=None, attention_mask=(batch>0).to(device))
        logits = outputs[0]
        logits = logits.detach().cpu().numpy()
        preds.append(logits)

final_preds = np.concatenate(preds, axis=0)
final_preds = np.argmax(final_preds, axis=1) 
# Create submission file
submission = pd.DataFrame()
submission['id'] = df_test['id']
submission['target'] = final_preds
submission.to_csv('submission.csv', index=False) 

小結

我把預測結果上傳后,score是0.83,在kaggle上排名100多。考慮到排名靠前的60位使用的不是NLP方法,他們找到了正確答案並直接上傳得到了100%的正確率,我對這個簡單模型的結果還是挺滿意的。

也希望我對這個學習過程的分享,能夠幫助到一同學習NLP的人。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM