5: Мультитипный анализ настроений
Во всех предыдущих исследованиях наш набор данных имел только две категории для анализа настроений: положительные или отрицательные. Когда у нас есть только два класса, наш вывод может быть одним скаляром в диапазоне от 0 до 1, указывающим, к какому классу принадлежит пример. Когда у нас есть более 2 примеров, наш вывод должен бытьразмерный вектор, гдеэто количество классов.
В этом исследовании мы выполним классификацию набора данных с 6 классами. Обратите внимание, что этот набор данных на самом деле не является набором данных анализа настроений, а представляет собой набор данных вопросов, и задача состоит в том, чтобы классифицировать категорию, к которой относится вопрос. Однако все, что описано в этом исследовании, применимо к любомуНабор данных примеров входных последовательностей для одного из классов.
Далее мы устанавливаем поля и загружаем набор данных, в отличие от предыдущего:
Во-первых, нам не нужно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))
Рассмотрим пример обучающего набора
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
даизмерение вместоизмерение.
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
Установить как. Мы можем использоватьLABEL
Легкий доступ к размеру словарного запаса, как мы используем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функция, потеря определяется *перекрестной энтропией* между этой функцией и меткой.
Вообще говоря:
- Когда наш пример принадлежит толькопервый класс при использовании
CrossEntropyLoss
- Используется, когда наш пример принадлежит только 2 классам (0 и 1)
BCEWithLogitsLoss
, а также для нашего примера 0 имежду классами (также известная как классификация с несколькими метками).
import torch.optim as optim
optimizer = optim.Adam(model.parameters())
criterion = nn.CrossEntropyLoss()
model = model.to(device)
criterion = criterion.to(device)
Ранее у нас была функция, вычисляющая точность в случае бинарных меток, и мы сказали, что если значение превышает 0,5, то мы будем считать его положительным. В случае, когда у нас более 2 классов, наша модель выводитМногомерный вектор, где значение каждого элемента — это уверенность в том, что пример принадлежит классу.
Например, в наших метках мы имеем: «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}%')
Наконец, запустите нашу модель на тестовом наборе.
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}%')
Подобно тому, как мы создали функцию для прогнозирования тональности любого заданного предложения, теперь мы можем создать функцию для прогнозирования класса заданного вопроса.
Единственная разница здесь в том, что вместо использования сигмовидной функции для сжатия входных данных между 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