中文情感分類

2023-08-30 06:01:22

本文通過ChnSentiCorp資料集介紹了文字分類任務過程,主要使用預訓練語言模型bert-base-chinese直接在測試集上進行測試,也簡要介紹了模型訓練流程,不過最後沒有儲存訓練好的模型。

一.任務和資料集介紹
1.任務
中文情感分類本質還是一個文字分類問題。
2.資料集
本文使用ChnSentiCorp情感分類資料集,每條資料中包括一句購物評價,以及一個標識,表明這條評價是一條好評還是一條差評。被評價的商品主要是書籍、酒店、計算機配件等。一些例子如下所示:

二.模型架構
基本思路是先特徵抽取,然後進行下游任務。前者主要是RNN、LSTM、GRU、BERT、GPT、Transformers等模型,後者本質就是分類模型,比如全連線神經網路等。

三.實現程式碼
1.準備資料集
(1)使用編碼工具

def load_encode_tool(pretrained_model_name_or_path):
    # 載入編碼工具bert-base-chinese
    token = BertTokenizer.from_pretrained(Path(f'{pretrained_model_name_or_path}'))
    # print(token)
    return token
if __name__ == '__main__':
    # 測試編碼工具
    pretrained_model_name_or_path = r'L:\20230713_HuggingFaceModel\bert-base-chinese'
    token = load_encode_tool(pretrained_model_name_or_path)
    print(token)

輸出結果如下所示:

BertTokenizer(name_or_path='L:\20230713_HuggingFaceModel\bert-base-chinese', vocab_size=21128, model_max_length=1000000000000000019884624838656, is_fast=False, padding_side='right', truncation_side='right', special_tokens={'unk_token''[UNK]''sep_token''[SEP]''pad_token''[PAD]''cls_token''[CLS]''mask_token''[MASK]'}, clean_up_tokenization_spaces=True)

其中,vocab_size=21128表示bert-base-chinese模型的字典中有21128個詞,特殊token主要是UNK、SEP、PAD、CLS、MASK。需要說明的是model_max_length,定義為self.model_max_length = model_max_length if model_max_length is not None else VERY_LARGE_INTEGER。因為從本地載入模型,不滿足如下條件,所以給model_max_length賦了一個很大的數值,如下所示:

返回值token總的資料結構如下所示: 接下來測試編碼工具如下所示:

if __name__ == '__main__':
    # 測試編碼工具
    pretrained_model_name_or_path = r'L:\20230713_HuggingFaceModel\bert-base-chinese'
    token = load_encode_tool(pretrained_model_name_or_path)
    out = token.batch_encode_plus(
        batch_text_or_text_pairs=['從明天起,做一個幸福的人。''餵馬,劈柴,周遊世界。'],
        truncation=True,  # 是否截斷
        padding='max_length',  # 是否填充
        max_length=17,  # 最大長度,如果不足,那麼填充,如果超過,那麼截斷
        return_tensors='pt',  # 返回的型別
        return_length=True  # 返回長度
    )
    # 檢視編碼輸出
    for key, value in out.items():
        print(key, value.shape)
        # 把編碼還原成文字
        print(token.decode(out['input_ids'][0]))

輸出結果如下所示:

input_ids torch.Size([2, 17])
[CLS] 從 明 天 起 , 做 一 個 幸 福 的 人 。 [SEP] [PAD] [PAD]
token_type_ids torch.Size([2, 17])
[CLS] 從 明 天 起 , 做 一 個 幸 福 的 人 。 [SEP] [PAD] [PAD]
length torch.Size([2])
[CLS] 從 明 天 起 , 做 一 個 幸 福 的 人 。 [SEP] [PAD] [PAD]
attention_mask torch.Size([2, 17])
[CLS] 從 明 天 起 , 做 一 個 幸 福 的 人 。 [SEP] [PAD] [PAD]

其中,out資料結構如下所示: 因此,out['input_ids'][0]表示第一個句子,token.decode(out['input_ids'][0])表示對第一個句子進行解碼。input_idstoken_type_idsattention_mask編碼結果示意圖如下所示: 說明:bert-base-chinese編碼工具是以字為詞,即把每個字都作為一個詞進行處理。如果對input_idstoken_type_idsattention_mask物理意義不清楚的,那麼參考使用編碼工具

(2)定義資料集

class Dataset(torch.utils.data.Dataset):
    def __init__(self, split):
        mode_name_or_path = r'L:\20230713_HuggingFaceModel\ChnSentiCorp'
        self.dataset = load_from_disk(mode_name_or_path)[split]

    def __len__(self):
        return len(self.dataset)

    def __getitem__(self, i):
        text = self.dataset[i]['text']
        label = self.dataset[i]['label']
        return text, label
if __name__ == '__main__':
    # 載入訓練資料集
    dataset = Dataset('train')
    print(len(dataset), dataset[20])

輸出結果如下所示:

9600 ('非常不錯,服務很好,位於市中心區,交通方便,不過價格也高!', 1)

(3)定義計算裝置
通常做深度學習都會有個N卡,設定CUDA如下所示:

device = 'cpu'
if torch.cuda.is_available():
   device = 'cuda'

(4)定義資料整理函數
主要對輸入data進行batch_encode_plus(),然後返回input_ids、attention_mask、token_type_ids和labels:

# 資料整理函數
def collate_fn(data):
    sents = [i[0] for i in data]
    labels = [i[1] for i in data]
    # 編碼
    data = token.batch_encode_plus(batch_text_or_text_pairs=sents, truncation=True, padding='max_length', max_length=500, return_tensors='pt', return_length=True)
    # input_ids:編碼之後的數位
    # attention_mask:補零的位置是0, 其他位置是1
    input_ids = data['input_ids']
    attention_mask = data['attention_mask']
    token_type_ids = data['token_type_ids']
    labels = torch.LongTensor(labels)
    # 把資料移動到計算裝置上
    input_ids = input_ids.to(device)
    attention_mask = attention_mask.to(device)
    token_type_ids = token_type_ids.to(device)
    labels = labels.to(device)
    return input_ids, attention_mask, token_type_ids, labels
if __name__ == '__main__':
    # 測試編碼工具
    pretrained_model_name_or_path = r'L:\20230713_HuggingFaceModel\bert-base-chinese'
    token = load_encode_tool(pretrained_model_name_or_path)
    
    # 定義計算裝置
    device = 'cpu'
    if torch.cuda.is_available():
        device = 'cuda'
    
    # 測試資料整理函數
    data = [
        ('你站在橋上看風景', 1),
        ('看風景的人在樓上看你', 0),
        ('明月裝飾了你的窗子', 1),
        ('你裝飾了別人的夢', 0),
    ]
    input_ids, attention_mask, token_type_ids, labels = collate_fn(data)
    print(input_ids.shape, attention_mask.shape, token_type_ids.shape, labels)

結果輸出如下所示:

torch.Size([4, 500]) torch.Size([4, 500]) torch.Size([4, 500]) tensor([1, 0, 1, 0], device='cuda:0')

(5)定義資料集載入器
定義資料集載入器使用資料整理函數批次處理資料集中的資料如下所示:

loader = torch.utils.data.DataLoader(dataset=dataset, batch_size=16, collate_fn=collate_fn, shuffle=True, drop_last=True)
  • dataset:如果資料集是train dataset,那麼就是訓練集資料載入器;如果資料集是test dataset,那麼就是測試集資料載入器,上述定義的dataset是訓練集資料載入器。
  • batch_size=16:每個batch包括16條資料。
  • collate_fn=collate_fn:使用的資料整理函數
  • shuffle=True:打亂各個batch間的順序,讓資料隨機
  • drop_last=True:當剩餘資料不足16條時,丟棄這些尾數

2.定義模型
(1)載入預訓練模型

# 檢視資料樣例
for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(loader):
    break
print(input_ids.shape, attention_mask.shape, token_type_ids.shape, labels)
    
pretrained_model_name_or_path = r'L:\20230713_HuggingFaceModel\bert-base-chinese'
pretrained = BertModel.from_pretrained(Path(f'{pretrained_model_name_or_path}'))
# 不訓練預訓練模型,不需要計算梯度
for param in pretrained.parameters():
    param.requires_grad_(False)
pretrained.to(device)

out = pretrained(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
print(out.last_hidden_state.shape)

輸出結果如下所示:

torch.Size([16, 500]) torch.Size([16, 500]) torch.Size([16, 500]) tensor([1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0], device='cuda:0'#資料整理函數計算結果
torch.Size([16, 500, 768]) #16表示batch size,500表示句子包含詞數,768表示向量維度

其中,out是BaseModelOutputWithPoolingAndCrossAttentions物件,包括last_hidden_state和pooler_output兩個欄位。資料結構如下所示:

(2)定義下游任務模型
該模型是權重為768×2的全連線神經網路,本質就是把768維向量轉換為2維。計算過程就是通過模型提取特徵矩陣(16×500×768),然後取第1個字[CLS]代表整個文字語意特徵,用於下游分類任務等。如下所示:

class Model(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = torch.nn.Linear(768, 2)

    def forward(self, input_ids, attention_mask, token_type_ids):
        # 使用預訓練模型抽取資料特徵
        with torch.no_grad():
            out = pretrained(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
        # 對抽取的特徵只取第1個字的結果做分類即可
        out = self.fc(out.last_hidden_state[:, 0])
        out = out.softmax(dim=1)
        return out

3.訓練和測試
(1)訓練

def train():
    # 定義優化器
    optimizer = AdamW(model.parameters(), lr=5e-4)
    # 定義1oss函數
    criterion = torch.nn.CrossEntropyLoss()
    # 定義學習率調節器
    scheduler = get_scheduler(name='linear', num_warmup_steps=0, num_training_steps=len(loader), optimizer=optimizer)
    # 將模型切換到訓練模式
    model.train()
    # 按批次遍歷訓練集中的資料
    for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(loader):
        # 模型計算
        out = model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
        # 計算loss並使用梯度下降法優化模型引數
        loss = criterion(out, labels)
        loss.backward()
        optimizer.step()
        scheduler.step()
        optimizer.zero_grad()
        # 輸出各項資料的情況,便於觀察
        if i % 10 == 0:
            out = out.argmax(dim=1)
            accuracy = (out == labels).sum().item() / len(labels)
            lr = optimizer.state_dict()['param_groups'][0]['lr']
            print(i, loss.item(), lr, accuracy)

(2)測試

def test():
    # 定義測試資料集載入器
    loader_test = torch.utils.data.DataLoader(dataset=Dataset('test'), batch_size=32, collate_fn=collate_fn, shuffle=True, drop_last=True)
    # 將下游任務模型切換到執行模式
    model.eval()
    correct = 0
    total = 0
    # 按批次遍歷測試集中的資料
    for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(loader_test):
        # 計算5個批次即可,不需要全部遍歷
        if i == 5:
            break
        print(i)
        # 計算
        with torch.no_grad():
            out = model(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids)
        # 統計正確率
        out = out.argmax(dim=1)
        correct += (out == labels).sum().item()
        total += len(labels)
    print(correct / total)

在PyTorch中,model.train()和model.eval()是用於切換模型訓練和評估模式的方法:

  • model.train()方法:將模型切換到訓練模式,這會啟用一些特定的行為,例如梯度下降、權重更新等。在訓練模式下,模型會使用訓練資料對模型引數進行更新,以最小化損失函數。
  • model.eval()方法:將模型切換到評估模式,這會禁用一些在訓練模式下啟用的行為,例如梯度下降、權重更新等。在評估模式下,模型通常用於對測試資料進行預測,以評估模型的效能。
  • param.requires_grad_(False)方法:它和model.eval都可以關閉梯度計算,但兩者區別在於param.requires_grad_(False)只關閉單個引數的梯度計算,而model.eval關閉整個模型的梯度計算。

參考文獻:
[1]HuggingFace自然語言處理詳解:基於BERT中文模型的任務實戰
[2]https://huggingface.co/bert-base-chinese/tree/main