《NLP情感分析》(七)——Transformer情感分析

語言: CN / TW / HK

6:使用Transformer進行情感分析

在本notebook中,我們將使用在 Attention is all you need 論文中首次引入的Transformer模型。 具體來説,我們將使用 BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding 論文中的 BERT模型。

Transformer 模型比本教程中涵蓋的其他模型都要大得多。 因此,我們將使用 transformers library 來獲取預訓練的Transformer並將它們用作我們的embedding層。 我們將固定(而不訓練)transformer,只訓練從transformer產生的表示中學習的模型的其餘部分。 在這種情況下,我們將使用雙向GRU繼續提取從Bert embedding後的特徵。最後在fc層上輸出最終的結果。

6.1 數據準備

首先,像往常一樣,我們導入庫,然後設置隨機種子

```python import torch

import random import numpy as np

SEED = 1234

random.seed(SEED) np.random.seed(SEED) torch.manual_seed(SEED) torch.backends.cudnn.deterministic = True ```

Transformer 已經用特定的詞彙進行了訓練,這意味着我們需要使用完全相同的詞彙進行訓練,並以與 Transformer 最初訓練時相同的方式標記我們的數據。

幸運的是,transformers 庫為每個提供的transformer 模型都有分詞器。 在這種情況下,我們使用忽略大小寫的 BERT 模型(即每個單詞都會小寫)。 我們通過加載預訓練的“bert-base-uncased”標記器來實現這一點。

```python from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased') ```

image.png

tokenizer 有一個 vocab 屬性,它包含我們將使用的實際詞彙。 我們可以通過檢查其長度來檢查其中有多少單詞。

python len(tokenizer.vocab)

30522

使用tokenizer.tokenize方法對字符串進行分詞,並統一大小寫。

```python tokens = tokenizer.tokenize('Hello WORLD how ARE yoU?')

print(tokens) ```

['hello', 'world', 'how', 'are', 'you', '?']

我們可以使用我們的詞彙表使用 tokenizer.convert_tokens_to_ids 來數字化標記。下面的tokens是我們之前上面進行了分詞和統一大小寫之後的list。

```python indexes = tokenizer.convert_tokens_to_ids(tokens)

print(indexes) ```

[7592, 2088, 2129, 2024, 2017, 1029]

Transformer還接受了特殊tokens的訓練,以標記句子的開頭和結尾, 詳細信息。 就像我們標準化padding和未知的token一樣,我們也可以從tokenizer中獲取這些。

注意tokenizer 確實有序列開始和序列結束屬性(bos_tokeneos_token),但我們沒有對此進行設置,並且不適用於我們本次訓練的transformer。

```python init_token = tokenizer.cls_token eos_token = tokenizer.sep_token pad_token = tokenizer.pad_token unk_token = tokenizer.unk_token

print(init_token, eos_token, pad_token, unk_token) ```

[CLS] [SEP] [PAD] [UNK]

我們可以通過反轉詞彙表來獲得特殊tokens的索引

```python init_token_idx = tokenizer.convert_tokens_to_ids(init_token) eos_token_idx = tokenizer.convert_tokens_to_ids(eos_token) pad_token_idx = tokenizer.convert_tokens_to_ids(pad_token) unk_token_idx = tokenizer.convert_tokens_to_ids(unk_token)

print(init_token_idx, eos_token_idx, pad_token_idx, unk_token_idx) ```

101 102 0 100

或者通過tokenizer的方法直接獲取它們

```python init_token_idx = tokenizer.cls_token_id eos_token_idx = tokenizer.sep_token_id pad_token_idx = tokenizer.pad_token_id unk_token_idx = tokenizer.unk_token_id

print(init_token_idx, eos_token_idx, pad_token_idx, unk_token_idx) ```

101 102 0 100

我們需要處理的另一件事是模型是在具有定義的最大長度的序列上訓練的——它不知道如何處理比訓練更長的序列。 我們可以通過檢查我們想要使用的轉換器版本的 max_model_input_sizes 來獲得這些輸入大小的最大長度。

```python max_input_length = tokenizer.max_model_input_sizes['bert-base-uncased']

print(max_input_length) ```

512

之前我們使用了 spaCy 標記器來標記我們的示例。 然而,我們現在需要定義一個函數,我們將把它傳遞給我們的 TEXT 字段,它將為我們處理所有的標記化。 它還會將令牌的數量減少到最大長度。 請注意,我們的最大長度比實際最大長度小 2。 這是因為我們需要向每個序列附加兩個標記,一個在開頭,一個在結尾。

python def tokenize_and_cut(sentence): tokens = tokenizer.tokenize(sentence) tokens = tokens[:max_input_length-2] return tokens

現在我們開始定義我們的字段,transformer期望將batch維度放在第一維上,所以我們設置了 batch_first = True。 現在我們已經有了文本的詞彙數據,由transformer提供,我們設置 use_vocab = False 來告訴 torchtext 已經不需要切分數據了。 我們將 tokenize_and_cut 函數作為標記器傳遞。 preprocessing 參數是一個函數,這是我們將token轉換為其索引的地方。 最後,我們定義特殊的token——注意我們將它們定義為它們的索引值而不是它們的字符串值,即“100”而不是“[UNK]”這是因為序列已經被轉換為索引。

我們像以前一樣定義標籤字段。

```python from torchtext.legacy import data

TEXT = data.Field(batch_first = True, use_vocab = False, tokenize = tokenize_and_cut, preprocessing = tokenizer.convert_tokens_to_ids, init_token = init_token_idx, eos_token = eos_token_idx, pad_token = pad_token_idx, unk_token = unk_token_idx)

LABEL = data.LabelField(dtype = torch.float) ```

加載數據,拆分成訓練集和驗證集

```python from torchtext.legacy import datasets

train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)

train_data, valid_data = train_data.split(random_state = random.seed(SEED)) ```

python print(f"Number of training examples: {len(train_data)}") print(f"Number of validation examples: {len(valid_data)}") print(f"Number of testing examples: {len(test_data)}")

Number of training examples: 17500
Number of validation examples: 7500
Number of testing examples: 25000

隨便看一個例子,看下具體效果如何,輸出其中一個句子的one-hot向量。

python print(vars(train_data.examples[6]))

{'text': [1042, 4140, 1996, 2087, 2112, 1010, 2023, 3185, 5683, 2066, 1037, 1000, 2081, 1011, 2005, 1011, 2694, 1000, 3947, 1012, 1996, 3257, 2003, 10654, 1011, 28273, 1010, 1996, 3772, 1006, 2007, 1996, 6453, 1997, 5965, 1043, 11761, 2638, 1007, 2003, 2058, 13088, 10593, 2102, 1998, 7815, 2100, 1012, 15339, 14282, 1010, 3391, 1010, 18058, 2014, 3210, 2066, 2016, 1005, 1055, 3147, 3752, 2068, 2125, 1037, 16091, 4003, 1012, 2069, 2028, 2518, 3084, 2023, 2143, 4276, 3666, 1010, 1998, 2008, 2003, 2320, 10012, 3310, 2067, 2013, 1996, 1000, 7367, 11368, 5649, 1012, 1000, 2045, 2003, 2242, 14888, 2055, 3666, 1037, 2235, 2775, 4028, 2619, 1010, 1998, 2023, 3185, 2453, 2022, 2062, 2084, 2070, 2064, 5047, 2074, 2005, 2008, 3114, 1012, 2009, 2003, 7078, 5923, 1011, 27017, 1012, 2023, 2143, 2069, 2515, 2028, 2518, 2157, 1010, 2021, 2009, 21145, 2008, 2028, 2518, 2157, 2041, 1997, 1996, 2380, 1012, 4276, 3773, 2074, 2005, 1996, 2197, 2184, 2781, 2030, 2061, 1012], 'label': 'neg'}

我們可以使用 convert_ids_to_tokens 將這些索引轉換回可讀的tokens。

```python tokens = tokenizer.convert_ids_to_tokens(vars(train_data.examples[6])['text'])

print(tokens) ```

['f', '##ot', 'the', 'most', 'part', ',', 'this', 'movie', 'feels', 'like', 'a', '"', 'made', '-', 'for', '-', 'tv', '"', 'effort', '.', 'the', 'direction', 'is', 'ham', '-', 'fisted', ',', 'the', 'acting', '(', 'with', 'the', 'exception', 'of', 'fred', 'g', '##wyn', '##ne', ')', 'is', 'over', '##wr', '##ough', '##t', 'and', 'soap', '##y', '.', 'denise', 'crosby', ',', 'particularly', ',', 'delivers', 'her', 'lines', 'like', 'she', "'", 's', 'cold', 'reading', 'them', 'off', 'a', 'cue', 'card', '.', 'only', 'one', 'thing', 'makes', 'this', 'film', 'worth', 'watching', ',', 'and', 'that', 'is', 'once', 'gage', 'comes', 'back', 'from', 'the', '"', 'se', '##met', '##ary', '.', '"', 'there', 'is', 'something', 'disturbing', 'about', 'watching', 'a', 'small', 'child', 'murder', 'someone', ',', 'and', 'this', 'movie', 'might', 'be', 'more', 'than', 'some', 'can', 'handle', 'just', 'for', 'that', 'reason', '.', 'it', 'is', 'absolutely', 'bone', '-', 'chilling', '.', 'this', 'film', 'only', 'does', 'one', 'thing', 'right', ',', 'but', 'it', 'knocks', 'that', 'one', 'thing', 'right', 'out', 'of', 'the', 'park', '.', 'worth', 'seeing', 'just', 'for', 'the', 'last', '10', 'minutes', 'or', 'so', '.']

儘管我們已經處理了文本的詞彙表,當然也需要為標籤構建詞彙表。

python LABEL.build_vocab(train_data)

python print(LABEL.vocab.stoi)

defaultdict(None, {'neg': 0, 'pos': 1})

像之前一樣,我們創建迭代器。根據以往經驗,使用最大的batch size可以使transformer獲得最好的效果,當然,你也可以嘗試一下使用其他的batch size,如果你的顯卡比較好的話。

```python BATCH_SIZE = 128

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

train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits( (train_data, valid_data, test_data), batch_size = BATCH_SIZE, device = device) ```

6.2 構建模型

接下來,我們導入預訓練模型。

```python from transformers import BertTokenizer, BertModel

bert = BertModel.from_pretrained('bert-base-uncased') ```

image.png

接下來,我們將定義我們的實際模型。

我們將使用預訓練的 Transformer 模型,而不是使用embedding層來獲取文本的embedding。然後將這些embedding輸入GRU以生成對輸入句子情緒的預測。我們通過其 config 屬性從transformer中獲取嵌入維度大小(稱為hidden_size)。其餘的初始化是標準的。

在前向傳遞中,我們將transformer包裝在一個no_grad中,以確保不會在模型的這部分計算梯度。transformer實際上返回整個序列的embedding以及 pooled 輸出。 Bert模型文檔 指出,彙集的輸出“通常不是輸入語義內容的一個很好的總結,你通常更好對整個輸入序列的隱藏狀態序列進行平均或合併”,因此我們不會使用它。前向傳遞的其餘部分是循環模型的標準實現,我們在最後的時間步長中獲取隱藏狀態,並將其傳遞給一個線性層以獲得我們的預測。

```python import torch.nn as nn

class BERTGRUSentiment(nn.Module): def init(self, bert, hidden_dim, output_dim, n_layers, bidirectional, dropout):

    super().__init__()

    self.bert = bert

    embedding_dim = bert.config.to_dict()['hidden_size']

    self.rnn = nn.GRU(embedding_dim,
                      hidden_dim,
                      num_layers = n_layers,
                      bidirectional = bidirectional,
                      batch_first = True,
                      dropout = 0 if n_layers < 2 else dropout)

    self.out = nn.Linear(hidden_dim * 2 if bidirectional else hidden_dim, output_dim)

    self.dropout = nn.Dropout(dropout)

def forward(self, text):

    #text = [batch size, sent len]

    with torch.no_grad():
        embedded = self.bert(text)[0]

    #embedded = [batch size, sent len, emb dim]

    _, hidden = self.rnn(embedded)

    #hidden = [n layers * n directions, batch size, emb dim]

    if self.rnn.bidirectional:
        hidden = self.dropout(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim = 1))
    else:
        hidden = self.dropout(hidden[-1,:,:])

    #hidden = [batch size, hid dim]

    output = self.out(hidden)

    #output = [batch size, out dim]

    return output

```

我們使用標準超參數創建模型的實例。

```python HIDDEN_DIM = 256 OUTPUT_DIM = 1 N_LAYERS = 2 BIDIRECTIONAL = True DROPOUT = 0.25

model = BERTGRUSentiment(bert, HIDDEN_DIM, OUTPUT_DIM, N_LAYERS, BIDIRECTIONAL, DROPOUT) ```

我們可以檢查模型有多少參數,我們的標準型號有不到5M的參數,但這個模型有112M 幸運的是,而且這些參數中有 110M 來自transformer,我們不必再訓練它們。

```python def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f'The model has {count_parameters(model):,} trainable parameters') ```

The model has 112,241,409 trainable parameters

為了固定參數(不需要訓練它們),我們需要將它們的 requires_grad 屬性設置為 False。 為此,我們只需遍歷模型中的所有 named_parameters,如果它們是 bert 轉換器模型的一部分,我們設置 requires_grad = False,如微調的話,需要將requires_grad設置為True

python for name, param in model.named_parameters(): if name.startswith('bert'): param.requires_grad = False

我們現在可以看到我們的模型有不到3M的可訓練參數,這使得它幾乎可以與FastText模型相媲美。 然而,文本仍然必須通過transformer傳播,這導致訓練需要更長的時間。

```python def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f'The model has {count_parameters(model):,} trainable parameters') ```

The model has 2,759,169 trainable parameters

我們可以仔細檢查可訓練參數的名稱,確保它們有意義。 我們可以看到,它們都是 GRU(rnn)和線性層(out)的參數。

python for name, param in model.named_parameters(): if param.requires_grad: print(name)

rnn.weight_ih_l0
rnn.weight_hh_l0
rnn.bias_ih_l0
rnn.bias_hh_l0
rnn.weight_ih_l0_reverse
rnn.weight_hh_l0_reverse
rnn.bias_ih_l0_reverse
rnn.bias_hh_l0_reverse
rnn.weight_ih_l1
rnn.weight_hh_l1
rnn.bias_ih_l1
rnn.bias_hh_l1
rnn.weight_ih_l1_reverse
rnn.weight_hh_l1_reverse
rnn.bias_ih_l1_reverse
rnn.bias_hh_l1_reverse
out.weight
out.bias

6.3 訓練模型

按照慣例,我們構建自己的模型評價標準(損失函數),仍然是二分類

```python import torch.optim as optim

optimizer = optim.Adam(model.parameters()) ```

python criterion = nn.BCEWithLogitsLoss()

將模型和評價標準(損失函數)放在 GPU 上,如果你有GPU的話

python model = model.to(device) criterion = criterion.to(device) 接下來,我們將定義函數用於:計算準確度、定義train、evalute函數以及計算訓練/評估時期每一個epoch所需的時間。

```python def binary_accuracy(preds, y): """ Returns accuracy per batch, i.e. if you get 8/10 right, this returns 0.8, NOT 8 """

#round predictions to the closest integer
rounded_preds = torch.round(torch.sigmoid(preds))
correct = (rounded_preds == y).float() #convert into float for division 
acc = correct.sum() / len(correct)
return acc

```

```python def train(model, iterator, optimizer, criterion):

epoch_loss = 0
epoch_acc = 0

model.train()

for batch in iterator:

    optimizer.zero_grad()

    predictions = model(batch.text).squeeze(1)

    loss = criterion(predictions, batch.label)

    acc = binary_accuracy(predictions, batch.label)

    loss.backward()

    optimizer.step()

    epoch_loss += loss.item()
    epoch_acc += acc.item()

return epoch_loss / len(iterator), epoch_acc / len(iterator)

```

```python def evaluate(model, iterator, criterion):

epoch_loss = 0
epoch_acc = 0

model.eval()

with torch.no_grad():

    for batch in iterator:

        predictions = model(batch.text).squeeze(1)

        loss = criterion(predictions, batch.label)

        acc = binary_accuracy(predictions, batch.label)

        epoch_loss += loss.item()
        epoch_acc += acc.item()

return epoch_loss / len(iterator), epoch_acc / len(iterator)

```

```python import time

def epoch_time(start_time, end_time): elapsed_time = end_time - start_time elapsed_mins = int(elapsed_time / 60) elapsed_secs = int(elapsed_time - (elapsed_mins * 60)) return elapsed_mins, elapsed_secs ```

最後,我們將訓練我們的模型。 由於transformer的尺寸的原因,這比以前的任何型號都要長得多。 即使我們沒有訓練任何transformer的參數,我們仍然需要通過模型傳遞數據,這在標準 GPU 上需要花費大量時間。

```python N_EPOCHS = 5

best_valid_loss = float('inf')

for epoch in range(N_EPOCHS):

start_time = time.time()

train_loss, train_acc = train(model, train_iterator, optimizer, criterion)
valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)

end_time = time.time()

epoch_mins, epoch_secs = epoch_time(start_time, end_time)

if valid_loss < best_valid_loss:
    best_valid_loss = valid_loss
    torch.save(model.state_dict(), 'tut6-model.pt')

print(f'Epoch: {epoch+1:02} | Epoch Time: {epoch_mins}m {epoch_secs}s')
print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%')
print(f'\t Val. Loss: {valid_loss:.3f} |  Val. Acc: {valid_acc*100:.2f}%')

```

Epoch: 01 | Epoch Time: 7m 13s
    Train Loss: 0.502 | Train Acc: 74.41%
     Val. Loss: 0.270 |  Val. Acc: 89.15%
Epoch: 02 | Epoch Time: 7m 7s
    Train Loss: 0.281 | Train Acc: 88.49%
     Val. Loss: 0.224 |  Val. Acc: 91.32%
Epoch: 03 | Epoch Time: 7m 17s
    Train Loss: 0.239 | Train Acc: 90.67%
     Val. Loss: 0.211 |  Val. Acc: 91.91%
Epoch: 04 | Epoch Time: 7m 14s
    Train Loss: 0.206 | Train Acc: 91.81%
     Val. Loss: 0.206 |  Val. Acc: 92.01%
Epoch: 05 | Epoch Time: 7m 15s
    Train Loss: 0.188 | Train Acc: 92.63%
     Val. Loss: 0.211 |  Val. Acc: 91.92%

我們將加載為我們提供最佳驗證集上損失值的參數,並在測試集上應用這些參數 - 並在測試集上達到了最優的結果。

```python model.load_state_dict(torch.load('tut6-model.pt'))

test_loss, test_acc = evaluate(model, test_iterator, criterion)

print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc*100:.2f}%') ```

Test Loss: 0.209 | Test Acc: 91.58%

6.4 模型驗證

然後我們將使用該模型來測試一些序列的情緒。 我們對輸入序列進行標記,將其修剪到最大長度,將特殊token添加到任一側,將其轉換為張量,使用unsqueeze函數增加一維,然後將其傳遞給我們的模型。

python def predict_sentiment(model, tokenizer, sentence): model.eval() tokens = tokenizer.tokenize(sentence) tokens = tokens[:max_input_length-2] indexed = [init_token_idx] + tokenizer.convert_tokens_to_ids(tokens) + [eos_token_idx] tensor = torch.LongTensor(indexed).to(device) tensor = tensor.unsqueeze(0) prediction = torch.sigmoid(model(tensor)) return prediction.item()

python predict_sentiment(model, tokenizer, "This film is terrible")

0.03391794115304947

python predict_sentiment(model, tokenizer, "This film is great")

0.8869886994361877