《NLP情感分析》(四)——Faster情感分析
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'層(銀色)將其輸入。
我們使用二維池化函式“avg_pool2d”實現單詞在Embedding空間中的平均化。我們可以將詞嵌入看作為一個二維網格,其中詞沿著一個軸,詞嵌入的維度沿著另一個軸。下圖是一個轉換為5維詞嵌入的示例句子,詞沿縱軸,嵌入沿橫軸。[4x5] tensor中的每個元素都由一個綠色塊表示。
“avg_pool2d”使用大小為“embedded.shape[1]”(即句子長度)乘以1的過濾器。下圖中以粉紅色顯示。
我們計算filter 覆蓋的所有元素的平均值,然後filter 向右滑動,計算句子中每個單詞下一列嵌入值的平均值。
每個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}%')
```
獲得測試精度(比上一節中的模型訓練時間少很多):
```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)進行情感分析。