«Анализ настроений НЛП» (6) — многотипный анализ настроений

NLP
«Анализ настроений НЛП» (6) — многотипный анализ настроений

5: Мультитипный анализ настроений

Во всех предыдущих исследованиях наш набор данных имел только две категории для анализа настроений: положительные или отрицательные. Когда у нас есть только два класса, наш вывод может быть одним скаляром в диапазоне от 0 до 1, указывающим, к какому классу принадлежит пример. Когда у нас есть более 2 примеров, наш вывод должен бытьCCразмерный вектор, гдеCCэто количество классов.

В этом исследовании мы выполним классификацию набора данных с 6 классами. Обратите внимание, что этот набор данных на самом деле не является набором данных анализа настроений, а представляет собой набор данных вопросов, и задача состоит в том, чтобы классифицировать категорию, к которой относится вопрос. Однако все, что описано в этом исследовании, применимо к любомуCCНабор данных примеров входных последовательностей для одного из классов.

Далее мы устанавливаем поля и загружаем набор данных, в отличие от предыдущего:

Во-первых, нам не нужноLABELустановить в полеdtype. При решении проблем с несколькими классами PyTorch ожидает, что метки будут оцифрованы какLongTensor.

Во-вторых, на этот раз мы используемTRECнабор данных вместоIMDBнабор данных.fine_grainedПараметры позволяют нам использовать детализированные метки (которых 50 классов) или нет (в данном случае их будет 6 классов).

import torch
from torchtext.legacy import data
from torchtext.legacy import datasets
import random

SEED = 1234

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

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

LABEL = data.LabelField()

train_data, test_data = datasets.TREC.splits(TEXT, LABEL, fine_grained=False)

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

image.png

Рассмотрим пример обучающего набора

vars(train_data[-1])
{'text': ['What', 'is', 'a', 'Cartesian', 'Diver', '?'], 'label': 'DESC'}

Далее пополняем словарный запас. Поскольку этот набор данных небольшой (всего около 3800 обучающих выборок), он также имеет очень маленький словарный запас (около 7500 различных слов, т.е. однократный вектор имеет размерность 7500), что означает, что нам не нужно писать в словаре как до Установить "max_size" на столе.

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)

Далее мы можем проверить метки.

6 меток (для недетализированного случая) соответствуют 6 классам вопросов в наборе данных:

  • HUM: Вопросы о людях
  • ENTY: Вопросы о сущностях
  • DESC: По запросу описание проблемы
  • NUM: Вопросы об ответах с цифрами
  • LOC: Вопрос об ответе - местоположение
  • ABBR: Вопрос об аббревиатурах
print(LABEL.vocab.stoi)
defaultdict(None, {'HUM': 0, 'ENTY': 1, 'DESC': 2, 'NUM': 3, 'LOC': 4, 'ABBR': 5})

Как всегда, настраиваем итератор.

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)

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

import torch.nn as nn
import torch.nn.functional as F

class CNN(nn.Module):
    def __init__(self, vocab_size, embedding_dim, n_filters, filter_sizes, output_dim, 
                 dropout, pad_idx):
        
        super().__init__()
        
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        
        self.convs = nn.ModuleList([
                                    nn.Conv2d(in_channels = 1, 
                                              out_channels = n_filters, 
                                              kernel_size = (fs, embedding_dim)) 
                                    for fs in filter_sizes
                                    ])
        
        self.fc = nn.Linear(len(filter_sizes) * n_filters, output_dim)
        
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, text):
        
        #text = [sent len, batch size]
        
        text = text.permute(1, 0)
                
        #text = [batch size, sent len]
        
        embedded = self.embedding(text)
                
        #embedded = [batch size, sent len, emb dim]
        
        embedded = embedded.unsqueeze(1)
        
        #embedded = [batch size, 1, sent len, emb dim]
        
        conved = [F.relu(conv(embedded)).squeeze(3) for conv in self.convs]
            
        #conv_n = [batch size, n_filters, sent len - filter_sizes[n]]
        
        pooled = [F.max_pool1d(conv, conv.shape[2]).squeeze(2) for conv in conved]
        
        #pooled_n = [batch size, n_filters]
        
        cat = self.dropout(torch.cat(pooled, dim = 1))

        #cat = [batch size, n_filters * len(filter_sizes)]
            
        return self.fc(cat)

Мы определяем нашу модель, обязательно выводя размеры:OUTPUT_DIMУстановить какCC. Мы можем использоватьLABELЛегкий доступ к размеру словарного запасаCC, как мы используемTEXTДлина словаря, чтобы получить тот же размер, что и входной словарь.

Примеры в этом наборе данных намного меньше, чем примеры в наборе данных IMDb, поэтому мы будем использовать меньшийfilterразмер.

INPUT_DIM = len(TEXT.vocab)
EMBEDDING_DIM = 100
N_FILTERS = 100
FILTER_SIZES = [2,3,4]
OUTPUT_DIM = len(LABEL.vocab)
DROPOUT = 0.5
PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token]

model = CNN(INPUT_DIM, EMBEDDING_DIM, N_FILTERS, FILTER_SIZES, OUTPUT_DIM, DROPOUT, PAD_IDX)

Проверяя количество параметров, мы видим, что чем меньшеfilterРазмер означает, что наши параметры составляют одну треть размера модели CNN в наборе данных IMDb.

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 841,806 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.1638,  0.6046,  1.0789,  ..., -0.3140,  0.1844,  0.3624],
        ...,
        [-0.3110, -0.3398,  1.0308,  ...,  0.5317,  0.2836, -0.0640],
        [ 0.0091,  0.2810,  0.7356,  ..., -0.7508,  0.8967, -0.7631],
        [ 0.5831, -0.2514,  0.4156,  ..., -0.2735, -0.8659, -1.4063]])

Затем неизвестные веса и параметры заполнения будут инициализированы 0.

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)

Еще одно отличие от предыдущего блокнота — наша функция потерь.BCEWithLogitsLossОн обычно используется для бинарной классификации, в то время какCrossEntropyLossдля мультиклассификации,CrossEntropyLossВыполнить на выходе нашей моделиsoftmaxфункция, потеря определяется *перекрестной энтропией* между этой функцией и меткой.

Вообще говоря:

  • Когда наш пример принадлежит толькоCCпервый класс при использованииCrossEntropyLoss
  • Используется, когда наш пример принадлежит только 2 классам (0 и 1)BCEWithLogitsLoss, а также для нашего примера 0 иCCмежду классами (также известная как классификация с несколькими метками).
import torch.optim as optim

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

criterion = nn.CrossEntropyLoss()

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

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

Например, в наших метках мы имеем: «HUM» = 0, «ENTY» = 1, «DESC» = 2, «NUM» = 3, «LOC» = 4 и «ABBR» = 5. Если наша выходная модель выглядит так:[5.1, 0.3, 0.1, 2.1, 0.2, 0.6]Это означает, что модель уверена, что пример относится к классу 0: это вопрос о людях, и немного более уверена, что пример принадлежит к классу 3: это вопрос о числах.

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

def categorical_accuracy(preds, y):
    """
    Returns accuracy per batch, i.e. if you get 8/10 right, this returns 0.8, NOT 8
    """
    top_pred = preds.argmax(1, keepdim = True)
    correct = top_pred.eq(y.view_as(top_pred)).sum()
    acc = correct.float() / y.shape[0]
    return acc

Тренировочный цикл аналогичен предыдущему,CrossEntropyLossОжидаемые входные данные[batch size, n classes], помеченный как[batch size].

Тег должен быть по умолчанию по умолчаниюLongTensorтип данных, так как мы неdtypeУстановить какFloatTensor.

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)
        
        loss = criterion(predictions, batch.label)
        
        acc = categorical_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)

Оцените цикл, как и раньше

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)
            
            loss = criterion(predictions, batch.label)
            
            acc = categorical_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(), 'tut5-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('tut5-model.pt'))

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

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

image.png

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

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

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

def predict_class(model, sentence, min_len = 4):
    model.eval()
    tokenized = [tok.text for tok in nlp.tokenizer(sentence)]
    if len(tokenized) < min_len:
        tokenized += ['<pad>'] * (min_len - len(tokenized))
    indexed = [TEXT.vocab.stoi[t] for t in tokenized]
    tensor = torch.LongTensor(indexed).to(device)
    tensor = tensor.unsqueeze(1)
    preds = model(tensor)
    max_preds = preds.argmax(dim = 1)
    return max_preds.item()

Теперь давайте попробуем это на нескольких разных задачах...

pred_class = predict_class(model, "Who is Keyser Söze?")
print(f'Predicted class is: {pred_class} = {LABEL.vocab.itos[pred_class]}')
Predicted class is: 0 = HUM
pred_class = predict_class(model, "How many minutes are in six hundred and eighteen hours?")
print(f'Predicted class is: {pred_class} = {LABEL.vocab.itos[pred_class]}')
Predicted class is: 3 = NUM
pred_class = predict_class(model, "What continent is Bulgaria in?")
print(f'Predicted class is: {pred_class} = {LABEL.vocab.itos[pred_class]}')
Predicted class is: 4 = LOC
pred_class = predict_class(model, "What does WYSIWYG stand for?")
print(f'Predicted class is: {pred_class} = {LABEL.vocab.itos[pred_class]}')
Predicted class is: 5 = ABBR