《NLP情感分析》(四)——Faster情感分析

語言: CN / TW / HK

3:Faster 情感分析

上一篇文章中我們已經介紹了基於RNN的升級版本的情感分析,在這一小節中,我們將學習一種不使用RNN的方法:我們將實現論文 Bag of Tricks for Efficient Text Classification中的模型。這個簡單的模型實現了與第二篇文章情感分析相當的性能,但訓練速度要快得多。

3.1 數據預處理

FastText分類模型與其他文本分類模型最大的不同之處在於其計算了輸入句子的n-gram,並將n-gram作為一種附加特徵來獲取局部詞序特徵信息添加至標記化列表的末尾。n-gram的基本思想是,將文本里面的內容按照字節進行大小為n的滑動窗口操作,形成了長度是n的字節片段序列,其中每一個字節片段稱為gram。具體而言,在這裏我們使用bi-grams。

例如,在句子“how are you ?”中,bi-grams 是:“how are”、“are you”和“"you ?”。

“generate_bigrams”函數獲取一個已經標註的句子,計算bigrams並將其附加到標記化列表的末尾。

python def generate_bigrams(x): n_grams = set(zip(*[x[i:] for i in range(2)])) for n_gram in n_grams: x.append(' '.join(n_gram)) return x

例子:

python generate_bigrams(['This', 'film', 'is', 'terrible'])

['This', 'film', 'is', 'terrible', 'film is', 'This film', 'is terrible']

TorchText 'Field' 中有一個preprocessing參數。此處傳遞的函數將在對句子進行 tokenized (從字符串轉換為標token列表)之後,但在對其進行數字化(從tokens列表轉換為indexes列表)之前應用於句子。我們將在這裏傳遞generate_bigrams函數。

由於我們沒有使用RNN,所以不需要使用壓縮填充序列,因此我們不需要設置“include_length=True”。

```python import torch from torchtext.legacy import data from torchtext.legacy import datasets

SEED = 1234

torch.manual_seed(SEED) torch.backends.cudnn.deterministic = True

TEXT = data.Field(tokenize = 'spacy', tokenizer_language = 'en_core_web_sm', preprocessing = generate_bigrams)

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

與前面一樣,加載IMDb數據集並創建拆分:

```python import random

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

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

構建vocab並加載預訓練好的詞嵌入:

```python MAX_VOCAB_SIZE = 25_000

TEXT.build_vocab(train_data, max_size = MAX_VOCAB_SIZE, vectors = "glove.6B.100d", unk_init = torch.Tensor.normal_)

LABEL.build_vocab(train_data) ```

創建迭代器:

```python BATCH_SIZE = 64

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) ```

3.2 構建模型

FastText是一種典型的深度學習詞向量的表示方法,通過將Embedding層將單詞映射到稠密空間,然後將句子中所有單詞在Embedding空間中進行平均,進而完成分類。所以這個模型參數量相較於上一章中的模型會減少很多。

具體地,它首先使用'Embedding'層(藍色)計算每個詞嵌入,然後計算所有詞嵌入的平均值(粉紅色),並通過'Linear'層(銀色)將其輸入。

image.png

我們使用二維池化函數“avg_pool2d”實現單詞在Embedding空間中的平均化。我們可以將詞嵌入看作為一個二維網格,其中詞沿着一個軸,詞嵌入的維度沿着另一個軸。下圖是一個轉換為5維詞嵌入的示例句子,詞沿縱軸,嵌入沿橫軸。[4x5] tensor中的每個元素都由一個綠色塊表示。

image.png

“avg_pool2d”使用大小為“embedded.shape[1]”(即句子長度)乘以1的過濾器。下圖中以粉紅色顯示。

image.png

我們計算filter 覆蓋的所有元素的平均值,然後filter 向右滑動,計算句子中每個單詞下一列嵌入值的平均值。

image.png

每個filter位置提供一個值,即所有覆蓋元素的平均值。filter 覆蓋所有嵌入維度後,會得到一個[1x5] 的張量,然後通過線性層進行預測。

```python import torch.nn as nn import torch.nn.functional as F

class FastText(nn.Module): def init(self, vocab_size, embedding_dim, output_dim, pad_idx):

    super().__init__()

    self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)

    self.fc = nn.Linear(embedding_dim, output_dim)

def forward(self, text):

    #text = [sent len, batch size]

    embedded = self.embedding(text)

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

    embedded = embedded.permute(1, 0, 2)

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

    pooled = F.avg_pool2d(embedded, (embedded.shape[1], 1)).squeeze(1)

    #pooled = [batch size, embedding_dim]

    return self.fc(pooled)

```

與前面一樣,創建一個'FastText'類的實例:

```python INPUT_DIM = len(TEXT.vocab) EMBEDDING_DIM = 100 OUTPUT_DIM = 1 PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token]

model = FastText(INPUT_DIM, EMBEDDING_DIM, OUTPUT_DIM, PAD_IDX) ```

查看模型中的參數數量,我們發現該參數與第一節中的標準RNN大致相同,只有前一個模型的一半。

```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,500,301 trainable parameters

預訓練好的向量複製到嵌入層:

```python pretrained_embeddings = TEXT.vocab.vectors

model.embedding.weight.data.copy_(pretrained_embeddings) ```

tensor([[-0.1117, -0.4966,  0.1631,  ...,  1.2647, -0.2753, -0.1325],
        [-0.8555, -0.7208,  1.3755,  ...,  0.0825, -1.1314,  0.3997],
        [-0.0382, -0.2449,  0.7281,  ..., -0.1459,  0.8278,  0.2706],
        ...,
        [-0.1606, -0.7357,  0.5809,  ...,  0.8704, -1.5637, -1.5724],
        [-1.3126, -1.6717,  0.4203,  ...,  0.2348, -0.9110,  1.0914],
        [-1.5268,  1.5639, -1.0541,  ...,  1.0045, -0.6813, -0.8846]])

將未知tokens和填充tokens的初始權重歸零:

```python UNK_IDX = TEXT.vocab.stoi[TEXT.unk_token]

model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM) model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM) ```

3.3 訓練模型

訓練模型與上一節完全相同。

初始化優化器:

```python import torch.optim as optim

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

定義標準並將模型和標準放置在GPU上:

```python criterion = nn.BCEWithLogitsLoss()

model = model.to(device) criterion = criterion.to(device) ```

精度函數的計算:

```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

```

定義了一個函數來訓練模型。

:因為我們不會使用dropout,因此實際上我們不需要使用model.train(),但為了保持良好的代碼習慣,在這還是保留此行代碼。

```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)

```

定義了一個函數來測試訓練好的模型。

注意:同樣,我們也保留model.eval()

```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)

```

通過函數得到一個epoch需要多長時間:

```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 ```

最後,訓練我們的模型:

```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(), 'tut3-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}%')

```

image.png

獲得測試精度(比上一節中的模型訓練時間少很多):

```python model.load_state_dict(torch.load('tut3-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.381 | Test Acc: 85.42%

3.3 模型驗證

```python import spacy nlp = spacy.load('en_core_web_sm')

def predict_sentiment(model, sentence): model.eval() tokenized = generate_bigrams([tok.text for tok in nlp.tokenizer(sentence)]) indexed = [TEXT.vocab.stoi[t] for t in tokenized] tensor = torch.LongTensor(indexed).to(device) tensor = tensor.unsqueeze(1) prediction = torch.sigmoid(model(tensor)) return prediction.item() ```

負面評論的例子:

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

2.1313092350011553e-12

正面評論的例子:

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

1.0

小結

在下一節中,我們將使用卷積神經網絡(CNN)進行情感分析。