6. Анализ настроений с помощью Transformer
В этом блокноте мы будем использоватьAttention is all you needМодель Transformer впервые представлена в статье. В частности, мы будем использоватьBERT: Pre-training of Deep Bidirectional Transformers for Language UnderstandingМодель BERT в статье.
Модель Transformer намного больше, чем другие модели, описанные в этом уроке. Поэтому мы будем использоватьtransformers libraryчтобы получить предварительно обученные трансформеры и использовать их в качестве слоев для встраивания. Мы исправим (не обучим) преобразователь и будем обучать только остальную часть модели, полученную из представлений, созданных преобразователем. В этом случае мы продолжим извлекать признаки из вложений Берта, используя двунаправленный GRU. Наконец, окончательный результат выводится на слой fc.
6.1 Подготовка данных
Сначала, как обычно, импортируем библиотеку, затем устанавливаем случайное начальное число
import torch
import random
import numpy as np
SEED = 1234
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.backends.cudnn.deterministic = True
Трансформер был обучен с использованием определенного словаря, а это значит, что нам нужно тренироваться с точно таким же словарем и маркировать наши данные так же, как изначально обучался Трансформер.
К счастью, в библиотеке трансформаторов есть токенизаторы для каждой предоставленной модели трансформатора. В этом случае мы используем модель BERT без учета регистра (т. е. каждое слово будет строчным). Мы делаем это, загружая предварительно обученный токенизатор «bert-base-uncased».
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
tokenizer
существует одинvocab
атрибут, который содержит фактический словарь, который мы будем использовать. Мы можем проверить, сколько слов в нем, проверив его длину.
len(tokenizer.vocab)
30522
использоватьtokenizer.tokenize
Метод разбивает строку и объединяет случай.
tokens = tokenizer.tokenize('Hello WORLD how ARE yoU?')
print(tokens)
['hello', 'world', 'how', 'are', 'you', '?']
Мы можем использовать наш глоссарий, чтобы использоватьtokenizer.convert_tokens_to_ids
оцифровать знак. Следующие токены представляют собой список после того, как мы выполнили сегментацию слов и унифицированный регистр выше.
indexes = tokenizer.convert_tokens_to_ids(tokens)
print(indexes)
[7592, 2088, 2129, 2024, 2017, 1029]
Трансформеров также обучают специальными жетонами, чтобы отмечать начало и конец предложений.Детали. Точно так же, как мы нормализуем отступы и неизвестные токены, мы также можем получитьtokenizer
получить их.
Уведомление:tokenizer
имеет свойства начала и конца последовательности (bos_token
иeos_token
), но мы не устанавливали это, и на этот раз это не сработало с трансформатором, который мы обучали.
init_token = tokenizer.cls_token
eos_token = tokenizer.sep_token
pad_token = tokenizer.pad_token
unk_token = tokenizer.unk_token
print(init_token, eos_token, pad_token, unk_token)
[CLS] [SEP] [PAD] [UNK]
Мы можем получить индекс специальных токенов, перевернув словарь
init_token_idx = tokenizer.convert_tokens_to_ids(init_token)
eos_token_idx = tokenizer.convert_tokens_to_ids(eos_token)
pad_token_idx = tokenizer.convert_tokens_to_ids(pad_token)
unk_token_idx = tokenizer.convert_tokens_to_ids(unk_token)
print(init_token_idx, eos_token_idx, pad_token_idx, unk_token_idx)
101 102 0 100
Или получить их напрямую через метод токенизатора
init_token_idx = tokenizer.cls_token_id
eos_token_idx = tokenizer.sep_token_id
pad_token_idx = tokenizer.pad_token_id
unk_token_idx = tokenizer.unk_token_id
print(init_token_idx, eos_token_idx, pad_token_idx, unk_token_idx)
101 102 0 100
Еще одна вещь, с которой нам нужно иметь дело, это то, что модель обучается на последовательностях с определенной максимальной длиной — она не знает, как обрабатывать последовательности длиннее, чем она была обучена. Мы можем сделать это, проверив версию конвертера, которую мы хотим использовать.max_model_input_sizes
чтобы получить максимальную длину этих входных размеров.
max_input_length = tokenizer.max_model_input_sizes['bert-base-uncased']
print(max_input_length)
512
прежде чем мы использовалиspaCy
tokenizer для маркировки наших примеров. Однако теперь нам нужно определить функцию, которую мы передадим нашемуTEXT
поле, которое будет обрабатывать всю токенизацию для нас. Это также уменьшит количество токенов до максимальной длины. Обратите внимание, что наша максимальная длина на 2 меньше фактической максимальной длины. Это потому, что нам нужно добавить два маркера к каждой последовательности, один в начале и один в конце.
def tokenize_and_cut(sentence):
tokens = tokenizer.tokenize(sentence)
tokens = tokens[:max_input_length-2]
return tokens
Теперь мы начинаем определять наши поля, преобразователь ожидает поместить пакетное измерение в первое измерение, поэтому мы устанавливаемbatch_first = True
. Теперь, когда у нас есть лексические данные для текста, предоставленные преобразователем, мы устанавливаемuse_vocab = False
чтобы сообщить torchtext, что нет необходимости разделять данные. мы будемtokenize_and_cut
Функции передаются как токенизаторы.preprocessing
Параметр — это функция, здесь мы конвертируем токен в его индекс. Наконец, мы определяем специальные токены — обратите внимание, что мы определили их как их значения индекса, а не их строковые значения, то есть «100» вместо «[UNK]». Это связано с тем, что последовательности были преобразованы в индексы.
Мы определяем поле метки, как и раньше.
from torchtext.legacy import data
TEXT = data.Field(batch_first = True,
use_vocab = False,
tokenize = tokenize_and_cut,
preprocessing = tokenizer.convert_tokens_to_ids,
init_token = init_token_idx,
eos_token = eos_token_idx,
pad_token = pad_token_idx,
unk_token = unk_token_idx)
LABEL = data.LabelField(dtype = torch.float)
Загрузить данные, разделенные на набор для обучения и набор для проверки
from torchtext.legacy import datasets
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
train_data, valid_data = train_data.split(random_state = random.seed(SEED))
print(f"Number of training examples: {len(train_data)}")
print(f"Number of validation examples: {len(valid_data)}")
print(f"Number of testing examples: {len(test_data)}")
Number of training examples: 17500
Number of validation examples: 7500
Number of testing examples: 25000
Просто посмотрите на пример, посмотрите, как это работает, и выведите горячий вектор одного из предложений.
print(vars(train_data.examples[6]))
{'text': [1042, 4140, 1996, 2087, 2112, 1010, 2023, 3185, 5683, 2066, 1037, 1000, 2081, 1011, 2005, 1011, 2694, 1000, 3947, 1012, 1996, 3257, 2003, 10654, 1011, 28273, 1010, 1996, 3772, 1006, 2007, 1996, 6453, 1997, 5965, 1043, 11761, 2638, 1007, 2003, 2058, 13088, 10593, 2102, 1998, 7815, 2100, 1012, 15339, 14282, 1010, 3391, 1010, 18058, 2014, 3210, 2066, 2016, 1005, 1055, 3147, 3752, 2068, 2125, 1037, 16091, 4003, 1012, 2069, 2028, 2518, 3084, 2023, 2143, 4276, 3666, 1010, 1998, 2008, 2003, 2320, 10012, 3310, 2067, 2013, 1996, 1000, 7367, 11368, 5649, 1012, 1000, 2045, 2003, 2242, 14888, 2055, 3666, 1037, 2235, 2775, 4028, 2619, 1010, 1998, 2023, 3185, 2453, 2022, 2062, 2084, 2070, 2064, 5047, 2074, 2005, 2008, 3114, 1012, 2009, 2003, 7078, 5923, 1011, 27017, 1012, 2023, 2143, 2069, 2515, 2028, 2518, 2157, 1010, 2021, 2009, 21145, 2008, 2028, 2518, 2157, 2041, 1997, 1996, 2380, 1012, 4276, 3773, 2074, 2005, 1996, 2197, 2184, 2781, 2030, 2061, 1012], 'label': 'neg'}
мы можем использоватьconvert_ids_to_tokens
Преобразуйте эти индексы обратно в читаемые токены.
tokens = tokenizer.convert_ids_to_tokens(vars(train_data.examples[6])['text'])
print(tokens)
['f', '##ot', 'the', 'most', 'part', ',', 'this', 'movie', 'feels', 'like', 'a', '"', 'made', '-', 'for', '-', 'tv', '"', 'effort', '.', 'the', 'direction', 'is', 'ham', '-', 'fisted', ',', 'the', 'acting', '(', 'with', 'the', 'exception', 'of', 'fred', 'g', '##wyn', '##ne', ')', 'is', 'over', '##wr', '##ough', '##t', 'and', 'soap', '##y', '.', 'denise', 'crosby', ',', 'particularly', ',', 'delivers', 'her', 'lines', 'like', 'she', "'", 's', 'cold', 'reading', 'them', 'off', 'a', 'cue', 'card', '.', 'only', 'one', 'thing', 'makes', 'this', 'film', 'worth', 'watching', ',', 'and', 'that', 'is', 'once', 'gage', 'comes', 'back', 'from', 'the', '"', 'se', '##met', '##ary', '.', '"', 'there', 'is', 'something', 'disturbing', 'about', 'watching', 'a', 'small', 'child', 'murder', 'someone', ',', 'and', 'this', 'movie', 'might', 'be', 'more', 'than', 'some', 'can', 'handle', 'just', 'for', 'that', 'reason', '.', 'it', 'is', 'absolutely', 'bone', '-', 'chilling', '.', 'this', 'film', 'only', 'does', 'one', 'thing', 'right', ',', 'but', 'it', 'knocks', 'that', 'one', 'thing', 'right', 'out', 'of', 'the', 'park', '.', 'worth', 'seeing', 'just', 'for', 'the', 'last', '10', 'minutes', 'or', 'so', '.']
Хотя мы разобрались со словарным запасом текста, конечно, нам также необходимо создать словарный запас для меток.
LABEL.build_vocab(train_data)
print(LABEL.vocab.stoi)
defaultdict(None, {'neg': 0, 'pos': 1})
Как и раньше, мы создаем итератор. Основываясь на прошлом опыте, использование наибольшего размера партии может обеспечить наилучшие результаты преобразователя, конечно, вы также можете попробовать использовать другие размеры партии, если ваша видеокарта лучше.
BATCH_SIZE = 128
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)
6.2 Построение модели
Далее мы импортируем предварительно обученную модель.
from transformers import BertTokenizer, BertModel
bert = BertModel.from_pretrained('bert-base-uncased')
Далее мы определим нашу фактическую модель.
Вместо использования слоя встраивания мы будем использовать предварительно обученную модель Transformer для получения встраивания текста. Затем эти вложения передаются в ГРУ для генерации прогнозов тональности входного предложения. Мы получаем размер встраивания от преобразователя через его свойство конфигурации (называемоеhidden_size
). В остальном инициализация стандартная.
На прямом проходе мы заворачиваем трансформатор вno_grad
, чтобы в этой части модели не вычислялись градиенты. Преобразователь фактически возвращает всю последовательность вложений иpooledвывод.Документация модели БертаОтметив, что объединенный вывод «обычно не является хорошим обобщением семантического содержания ввода, обычно лучше усреднять или объединять последовательность скрытых состояний во входной последовательности», поэтому мы не будем его использовать. Остальная часть прямого прохода — это стандартная реализация рекуррентных моделей, где мы берем скрытое состояние на последнем временном шаге и передаем его линейному слою, чтобы получить наши прогнозы.
import torch.nn as nn
class BERTGRUSentiment(nn.Module):
def __init__(self,
bert,
hidden_dim,
output_dim,
n_layers,
bidirectional,
dropout):
super().__init__()
self.bert = bert
embedding_dim = bert.config.to_dict()['hidden_size']
self.rnn = nn.GRU(embedding_dim,
hidden_dim,
num_layers = n_layers,
bidirectional = bidirectional,
batch_first = True,
dropout = 0 if n_layers < 2 else dropout)
self.out = nn.Linear(hidden_dim * 2 if bidirectional else hidden_dim, output_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, text):
#text = [batch size, sent len]
with torch.no_grad():
embedded = self.bert(text)[0]
#embedded = [batch size, sent len, emb dim]
_, hidden = self.rnn(embedded)
#hidden = [n layers * n directions, batch size, emb dim]
if self.rnn.bidirectional:
hidden = self.dropout(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim = 1))
else:
hidden = self.dropout(hidden[-1,:,:])
#hidden = [batch size, hid dim]
output = self.out(hidden)
#output = [batch size, out dim]
return output
Мы создаем экземпляр модели, используя стандартные гиперпараметры.
HIDDEN_DIM = 256
OUTPUT_DIM = 1
N_LAYERS = 2
BIDIRECTIONAL = True
DROPOUT = 0.25
model = BERTGRUSentiment(bert,
HIDDEN_DIM,
OUTPUT_DIM,
N_LAYERS,
BIDIRECTIONAL,
DROPOUT)
Мы можем проверить, сколько параметров у модели, наша стандартная модель имеет менее 5 млн параметров, а эта модель имеет 112 млн. К счастью, 110 млн из этих параметров поступают от преобразователя, нам больше не нужно их обучать.
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 112,241,409 trainable parameters
Чтобы исправить параметры (без их обучения), нам нужно преобразовать ихrequires_grad
свойство установлено наFalse
. Для этого мы просто перебираем всеnamed_parameters
, если ониbert
В рамках модели преобразователя задаемrequires_grad = False
, такие как тонкая настройка, она должна бытьrequires_grad
Установить какTrue
for name, param in model.named_parameters():
if name.startswith('bert'):
param.requires_grad = False
Теперь мы можем видеть, что наша модель имеет менее 3 миллионов обучаемых параметров, что делает ее почти такой же хорошей, какFastText
модель сравнима. Однако текст по-прежнему должен передаваться через преобразователь, из-за чего обучение занимает больше времени.
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,759,169 trainable parameters
Мы можем перепроверить имена обучаемых параметров, чтобы убедиться, что они имеют смысл. Мы видим, что они оба ГРУ (rnn
) и линейный слой (out
) параметры.
for name, param in model.named_parameters():
if param.requires_grad:
print(name)
rnn.weight_ih_l0
rnn.weight_hh_l0
rnn.bias_ih_l0
rnn.bias_hh_l0
rnn.weight_ih_l0_reverse
rnn.weight_hh_l0_reverse
rnn.bias_ih_l0_reverse
rnn.bias_hh_l0_reverse
rnn.weight_ih_l1
rnn.weight_hh_l1
rnn.bias_ih_l1
rnn.bias_hh_l1
rnn.weight_ih_l1_reverse
rnn.weight_hh_l1_reverse
rnn.bias_ih_l1_reverse
rnn.bias_hh_l1_reverse
out.weight
out.bias
6.3 Обучение модели
По соглашению мы строим собственные критерии оценки модели (функцию потерь), которая по-прежнему является бинарной классификацией.
import torch.optim as optim
optimizer = optim.Adam(model.parameters())
criterion = nn.BCEWithLogitsLoss()
Поместите модель и критерии оценки (функцию потерь) на GPU, если он у вас есть
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
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)
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(), 'tut6-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}%')
Epoch: 01 | Epoch Time: 7m 13s
Train Loss: 0.502 | Train Acc: 74.41%
Val. Loss: 0.270 | Val. Acc: 89.15%
Epoch: 02 | Epoch Time: 7m 7s
Train Loss: 0.281 | Train Acc: 88.49%
Val. Loss: 0.224 | Val. Acc: 91.32%
Epoch: 03 | Epoch Time: 7m 17s
Train Loss: 0.239 | Train Acc: 90.67%
Val. Loss: 0.211 | Val. Acc: 91.91%
Epoch: 04 | Epoch Time: 7m 14s
Train Loss: 0.206 | Train Acc: 91.81%
Val. Loss: 0.206 | Val. Acc: 92.01%
Epoch: 05 | Epoch Time: 7m 15s
Train Loss: 0.188 | Train Acc: 92.63%
Val. Loss: 0.211 | Val. Acc: 91.92%
Мы загрузим параметры, которые дают нам наилучшие значения потерь на проверочном наборе, и применим эти параметры к тестовому набору — и достигнем оптимальных результатов на тестовом наборе.
model.load_state_dict(torch.load('tut6-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.209 | Test Acc: 91.58%
6.4 Проверка модели
Затем мы будем использовать модель для проверки некоторых последовательностей настроений. Мы токенизируем входную последовательность, обрезаем ее до максимальной длины, добавляем специальный токен с каждой стороны, преобразуем его в тензор, увеличиваем на одно измерение с помощью функции разжатия и передаем в нашу модель.
def predict_sentiment(model, tokenizer, sentence):
model.eval()
tokens = tokenizer.tokenize(sentence)
tokens = tokens[:max_input_length-2]
indexed = [init_token_idx] + tokenizer.convert_tokens_to_ids(tokens) + [eos_token_idx]
tensor = torch.LongTensor(indexed).to(device)
tensor = tensor.unsqueeze(0)
prediction = torch.sigmoid(model(tensor))
return prediction.item()
predict_sentiment(model, tokenizer, "This film is terrible")
0.03391794115304947
predict_sentiment(model, tokenizer, "This film is great")
0.8869886994361877