«Анализ настроений НЛП» (4) — более быстрый анализ настроений

NLP
«Анализ настроений НЛП» (4) — более быстрый анализ настроений

3. Ускоренный анализ настроений

В предыдущей статье мы представили обновленную версию анализа настроений на основе RNN, в этом подразделе мы изучим метод, который не использует RNN: мы реализуем статьюBag of Tricks for Efficient Text Classificationмодель в . Эта простая модель обеспечивает производительность, сравнимую с анализом тональности второй статьи, но обучается намного быстрее.

3.1 Предварительная обработка данных

Самая большая разница между моделью классификации FastText и другими моделями классификации текста заключается в том, что она вычисляет n-грамму входного предложения и использует n-грамму в качестве дополнительной функции для получения информации о локальном порядке слов и добавления ее в конец предложения. список токенизации. Основная идея n-граммы состоит в том, чтобы выполнять операцию скользящего окна размера n над содержимым текста по байтам, формируя последовательность байтовых фрагментов длины n, каждый из которых называется граммом. В частности, здесь мы используем биграммы.

Например, в предложении «как дела?» биграммы: «как дела», «ты» и «ты?».

Функция «generate_bigrams» берет уже помеченное предложение, подсчитывает биграммы и добавляет их в конец токенизированного списка.

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

пример:

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

TorchText 'Поле' имеетpreprocessingпараметр. Переданная здесь функция будет применена к предложению после его токенизации (из строки в список токенов), но до его оцифровки (из списка токенов в список индексов). мы пройдем здесьgenerate_bigramsфункция.

Поскольку мы не используем RNN, нам не нужно использовать сжатую последовательность заполнения, поэтому нам не нужно устанавливать «include_length=True».

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 и создайте разбиения:

import random

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

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

Создайте словарный запас и загрузите предварительно обученные вложения слов:

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)

Создайте итератор:

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 — это типичный метод представления векторов слов для глубокого обучения.Слой встраивания сопоставляет слова с плотным пространством, а затем усредняет все слова в предложении в пространстве встраивания для завершения классификации. Поэтому количество параметров в этой модели будет намного меньше по сравнению с моделью из предыдущей главы.

В частности, он сначала вычисляет встраивание каждого слова, используя слой «Внедрение» (синий), затем вычисляет среднее значение всех вложений слов (розовый) и передает его через слой «Линейный» (серебристый).

image.png

Мы используем двумерную функцию объединения «avg_pool2d» для усреднения слов в пространстве встраивания. Мы можем думать о встраиваниях слов как о двумерной сетке, в которой слова расположены вдоль одной оси, а размеры вложений слов — вдоль другой оси. Изображение ниже представляет собой пример предложения, преобразованного в 5-мерное вложение слов, со словами по вертикальной оси и вложениями по горизонтальной оси. Каждый элемент в тензоре [4x5] представлен зеленым блоком.

image.png

«avg_pool2d» использует фильтр размера «embedded.shape[1]» (т.е. длина предложения), умноженный на 1. На изображении ниже показано розовым цветом.

image.png

Мы вычисляем среднее значение всех элементов, покрываемых фильтром, затем сдвигаем фильтр вправо и вычисляем среднее значение следующего столбца вложений для каждого слова в предложении.

image.png

Каждая позиция фильтра предоставляет значение, среднее значение всех покрытых элементов. После того, как фильтр охватывает все размеры встраивания, вы получаете тензор [1x5], который затем предсказывается линейным слоем.

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:

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 в разделе 1, и только половина предыдущей модели.

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

Предварительно обученный вектор копируется в слой внедрения:

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

Обнулите начальные веса неизвестных токенов и заполненных токенов:

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 Обучение модели

Обучение модели точно такое же, как и в предыдущем разделе.

Инициализируйте оптимизатор:

import torch.optim as optim

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

Определите критерии и поместите модель и критерии на GPU:

criterion = nn.BCEWithLogitsLoss()

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

Расчет функции точности:

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

Определена функция для обучения модели.

Примечание: Поскольку мы не будем использовать отсев, нам не нужно использоватьmodel.train(), но для того, чтобы поддерживать хорошие привычки кодирования, эта строка кода сохранена здесь.

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

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)

Сколько времени требуется, чтобы получить эпоху через функцию:

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

Наконец, обучите нашу модель:

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

Получите точность теста (намного меньше, чем время обучения модели в предыдущем разделе):

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 Проверка модели

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

Примеры негативных отзывов:

predict_sentiment(model, "This film is terrible")
2.1313092350011553e-12

Примеры положительных отзывов:

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

резюме

В следующем разделе мы будем использовать сверточные нейронные сети (CNN) для анализа настроений.