2: Обновленный анализ настроений
В предыдущей статье мы изучили базовый рабочий процесс анализа настроений, далее мы узнаем, как улучшить модель оптимизации: она включает в себя, как использовать сжатие для заполнения последовательностей, загружать и использовать предварительно обученные векторы слов, использовать разные оптимизаторы, выбирать разные Архитектура RNN (включая двунаправленную RNN, многоуровневую RNN) и регуляризация.
Основное содержание этой главы состоит в следующем:
- заполнение последовательности
- Предварительно обученные вложения слов
- LSTM
- Двунаправленный RNN
- Многослойная РНС
- Регуляризация
- оптимизация
2.1 Подготовка данных
Сначала настройте начальное число и классифицируйте его на наборы для обучения, тестирования и проверки.
При подготовке данных следует отметить, что, поскольку RNN может обрабатывать только незаполненные элементы в последовательности (то есть ненулевые данные), вывод для любого дополненного элемента равен 0 . Обратите внимание, что мы устанавливаем для include_length значение True при подготовке данных, чтобы получить фактическую длину предложения, которую необходимо использовать позже.
import torch
from torchtext.legacy import data
SEED = 1234
torch.manual_seed(SEED)
torch.backends.cudnn.deterministic = True
TEXT = data.Field(tokenize = 'spacy',
tokenizer_language = 'en_core_web_sm',
include_lengths = True)
LABEL = data.LabelField(dtype = torch.float)
Загрузите набор данных IMDb
from torchtext.legacy import datasets
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
Выберите часть из тренировочного набора, который будет проверочным.
import random
train_data, valid_data = train_data.split(random_state = random.seed(SEED))
2.2 Вектор слова
Далее используйте для инициализации предварительно обученные векторы слов, которые получаются путем передачи заданных параметров в build_vocab.
Здесь мы выбираем вектор слова GloVe Полное имя GloVe: Global Vectors for Word Representation.здесьСуществует подробное введение и много ресурсов об этом. В этом руководстве не будет подробно рассказано, как получается вектор слов, а только кратко описано, как использовать этот вектор слов.Здесь мы используем «glove.6B.100d», где 6B означает, что вектор слов состоит из 6 миллиардов токенов. Из приведенного выше обучения 100d означает, что вектор слов является 100-мерным (обратите внимание, что этот вектор слов имеет более 800 триллионов)
Конечно, вы также можете выбратьдругие слова векторов. Теоретически расстояние этих предварительно обученных векторов слов в векторном пространстве встраивания слов представляет собой семантическую связь между словами в определенной степени, например, «ужасный», «ужасный», «ужасный», их векторное пространство встраивания слов будет очень близко.
TEXT.build_vocab означает извлечение вектора слов словаря в текущих обучающих данных из предварительно обученного вектора слов для формирования Vocab (словаря) текущего обучающего набора. Для слов, которые не появляются в текущем корпусе векторов слов (обозначаются как UNK, неизвестные), инициализируйте случайным образом через распределение Гаусса (unk_init = torch.Tensor.normal_).
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)
2.3 Создать итератор + выбрать GPU
BATCH_SIZE = 64
# 根据当前环境选择是否调用GPU进行训练
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,
sort_within_batch = True,
device = device)
2.4 Построение модели
LSTM
LSTM - это вариант стандартной RNN, который добавляет метод переноса информации через несколько временных шагов, в некоторой степени преодолевая существование стандартной RNN.Градиент исчезаетПроблема. В частности, LSTM добавляет ячейки памяти, которую можно рассматривать как «память» LSTM, хранящую моментПамять LSTM, которая может хранить память от прошлого до настоящего момента.всю необходимую информацию для управления потоком информации в память и из памяти с использованием нескольких вентилей одновременно, см.здесь. Таким образом, мы можем думать о LSTM как о,ифункцию, а не толькои.
Таким образом, структура модели с использованием LSTM выглядит следующим образом (с опущенным слоем внедрения):
Как и начальное скрытое состояние, начальное состояние памятиИнициализируется как тензор со всеми нулями. Важно отметить, что при прогнозировании настроений используется только конечное скрытое состояние, а не конечное состояние ячейки памяти, т.е..
Двунаправленный RNN
Двунаправленные RNN добавляют обратный уровень RNN к предыдущему стандартному уровню RNN. Затем скрытые состояния двух слоев RNN в каждый момент времени соединяются и используются в качестве конечного вектора скрытого состояния. то есть на шаге по времениКогда прямая RNN обрабатывает слова, обратная RNN обрабатывает слова. Благодаря такой двунаправленной обработке скрытое состояние, соответствующее каждому слову, может собирать информацию как с левого, так и с правого направления, так что эти векторы кодируют более сбалансированную информацию.
Мы используем последнее скрытое состояние прямой RNN (полученное из последнего слова предложения)и последнее скрытое состояние обратного RNN (полученное из первого слова предложения)делать прогнозы настроений, т.е., на рисунке ниже показан двунаправленный RNN, где прямой RNN выделен оранжевым цветом, обратный RNN — зеленым, а линейные слои — серебристым.
Многослойная РНС
Многослойная RNN (также известная как глубокая RNN): добавьте еще несколько слоев RNN поверх исходной стандартной RNN. Первая (нижняя) RNN на временном шагеСкрытое состояние вывода будет на временном шагеВвод RNN выше, а затем прогнозируется в соответствии с окончательным скрытым состоянием конечного (самого высокого) слоя. На рисунке ниже показана многослойная однонаправленная RNN, где слои даны в виде. Также обратите внимание, что каждому слою требуется его начальное скрытое состояние,.
Регуляризация
Мы внесли различные улучшения в модель, но мы должны обратить внимание на то, что при постепенном увеличении параметров модели больше вероятность переобучения модели. Чтобы решить эту проблему, мы добавляем регуляризацию отсева. Dropout работает путем случайной генерации нейронов в слое во время прямого распространения.Dropout(установлено на 0). Вероятность отбрасывания каждого нейрона задается гиперпараметром и не зависит от других нейронов.
Одна из теорий, почему отсев работает, заключается в том, что модель с отсевом параметров можно считать «более слабой» (с меньшим количеством параметров) моделью. Следовательно, окончательную модель можно рассматривать как совокупность всех этих более слабых моделей, ни одна из которых не имеет чрезмерной параметризации, что снижает вероятность переобучения.
Implementation Details
1. Небольшое дополнение в процессе обучения модели: В процессе обучения модели модель не должна обучать токен площадки, добавляемый после заполнения каждой выборки, то есть она не будет изучать встроенный тег "
2. Поскольку двунаправленный LSTM, используемый в эксперименте, включает в себя процессы прямого и обратного распространения, окончательный вектор скрытого состояния содержит прямое и обратное скрытые состояния, поэтому вход в следующем слое nn. Линейный слой Форма в два раза больше формы измерение скрытого слоя.
3. Прежде чем вводить вложения (вектор слов) в RNN, нам нужно использоватьnn.utils.rnn.packed_padded_sequence
Они «упакованы», чтобы гарантировать, что RNN будет обрабатывать только токены, которые не являются пэдами. Результат, который мы получаем, включаетpacked_output
(упакованная последовательность) иhidden sate
иcell state
. Если операция «упаковки» не выполняется, то выводhidden state
иcell state
Высокая вероятность — это токен закладки из предложения. Если используются упакованные дополненные предложения, вывод будет последним незаполненным элементом.hidden state
иcell state
.
4. Затем используемnn.utils.rnn.pad_packed_sequence
Преобразуйте выходное предложение «распаковать» в тензор. Обратите внимание, что вывод токенов заполнения представляет собой тензор нулей, и обычно нам нужно «распаковывать» вывод только при его использовании в последующих моделях. Хотя в данном случае это не требуется, здесь просто показаны шаги.
5. окончательное скрытое состояние: то есть скрытое, форма которого [количество слоев * число направлений, размер партии, скрытый тусклый]. Поскольку нам нужны только последние скрытые состояния для прямого и обратного распространения, нам нужны только последние 2 скрытых слоя hidden[-2,:,:] и hidden[-1,:,:], а затем объединить их вместе, затем передать в линейном слое линейный слой. #####Я не знаю, как это объяснить здесь, и это нужно настроить.
import torch.nn as nn
class RNN(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers,
bidirectional, dropout, pad_idx):
super().__init__()
# embedding嵌入层(词向量)
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx = pad_idx)
# RNN变体——双向LSTM
self.rnn = nn.LSTM(embedding_dim, # input_size
hidden_dim, #output_size
num_layers=n_layers, # 层数
bidirectional=bidirectional, #是否双向
dropout=dropout) #随机去除神经元
# 线性连接层
self.fc = nn.Linear(hidden_dim * 2, output_dim) # 因为前向传播+后向传播有两个hidden sate,且合并在一起,所以乘以2
# 随机去除神经元
self.dropout = nn.Dropout(dropout)
def forward(self, text, text_lengths):
#text 的形状 [sent len, batch size]
embedded = self.dropout(self.embedding(text))
#embedded 的形状 [sent len, batch size, emb dim]
# pack sequence
# lengths need to be on CPU!
packed_embedded = nn.utils.rnn.pack_padded_sequence(embedded, text_lengths.to('cpu'))
packed_output, (hidden, cell) = self.rnn(packed_embedded)
#unpack sequence
output, output_lengths = nn.utils.rnn.pad_packed_sequence(packed_output)
#output的形状[sent len, batch size, hid dim * num directions]
#output中的 padding tokens是数值为0的张量
#hidden 的形状 [num layers * num directions, batch size, hid dim]
#cell 的形状 [num layers * num directions, batch size, hid dim]
#concat the final forward (hidden[-2,:,:]) and backward (hidden[-1,:,:]) hidden layers
#and apply dropout
hidden = self.dropout(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim = 1))
#hidden 的形状 [batch size, hid dim * num directions]
return self.fc(hidden)
2.5 Создание экземпляра модели + входящие параметры
Чтобы гарантировать, что предварительно обученные векторы слов могут быть загружены в модель, EMBEDDING_DIM должен быть равен размеру предварительно обученных векторов слов GloVe.
INPUT_DIM = len(TEXT.vocab) # 250002: 之前设置的只取25000个最频繁的词,加上pad_token和unknown token
EMBEDDING_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1
N_LAYERS = 2
BIDIRECTIONAL = True
DROPOUT = 0.5
PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token] #指定参数,定义pad_token的index索引值,让模型不管pad token
model = RNN(INPUT_DIM,
EMBEDDING_DIM,
HIDDEN_DIM,
OUTPUT_DIM,
N_LAYERS,
BIDIRECTIONAL,
DROPOUT,
PAD_IDX)
Просмотр параметров модели
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')
Затем скопируйте предварительно обученные векторы слов, загруженные ранее в слой встраивания в нашей модели, и замените параметры веса, инициализированные исходной моделью, предварительно обученными векторами слов вложений.
Мы извлекаем вложения из словаря поля и проверяем, что они имеют правильный размер,[vocab size, embedding dim]
pretrained_embeddings = TEXT.vocab.vectors
# 检查词向量形状 [vocab size, embedding dim]
print(pretrained_embeddings.shape)
# 用预训练的embedding词向量替换原始模型初始化的权重参数
model.embedding.weight.data.copy_(pretrained_embeddings)
потому что наш<unk>
и<pad>
Токенов нет в предварительно обученном словаре, они уже есть в построении нашего собственного словаря, используйтеunk_init
(an дистрибутив) инициализируется. Итак, лучше явно указать модели, чтобы они инициализировали их 0, они не имеют ничего общего с эмоциями.
Мы делаем это, вручную устанавливая их веса векторов слов равными 0.
Уведомление: Как и при инициализации встраивания, это должно быть сделано на "weight.data", а не на "weight"!
#将unknown 和padding token设置为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)
print(model.embedding.weight.data)
Теперь мы можем видеть, что первые две строки матрицы весов встраивания равны 0. Следует отметить, что вектор слов маркера заполнения никогда не изучается в процессе обучения модели. И будет изучен вектор слова неизвестного токена.
2.6 Обучение модели
Начните обучение модели прямо сейчас!
Мы меняем оптимизатор стохастического градиентного спуска с «SGD» на «Adam». SGD использует скорость обучения, которую мы установили, для синхронного обновления всех параметров, в то время как Adam регулирует скорость обучения каждого параметра, предоставляя параметры, которые обновляются чаще, параметры, которые обновляются реже, и параметры, которые обновляются не часто. Для получения дополнительной информации об «Адаме» (и других оптимизаторах) см.здесь.
Чтобы изменить «SGD» на «Адам», нам просто нужно изменить «optim.SGD» на «optim.Adam», также обратите внимание, что мы не предоставляем начальную скорость обучения Адама, поскольку PyTorch предоставляет начальную скорость обучения по умолчанию.
2.6.1 Настройка оптимизатора
import torch.optim as optim
optimizer = optim.Adam(model.parameters())
2.6.2 Настройка функции потерь и GPU
Остальные этапы обучения модели остаются прежними.
criterion = nn.BCEWithLogitsLoss() # 损失函数. criterion 在本task中时损失函数的意思
model = model.to(device)
criterion = criterion.to(device)
2.6.3 Точность расчета
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
2.6.4 Определение функции обучения для обучения модели
Поскольку мы установили «include_length=True», наш «batch.text» теперь является кортежем, первый элемент — числовой тензор, а второй элемент — фактическая длина каждой последовательности. Прежде чем передать их в модель, мы разбиваем их на соответствующие переменные text и text_length.
Уведомление: Поскольку сейчас мы используем отсев, мы должны помнить об использованииmodel.train()
чтобы гарантировать, что отсев включен во время обучения.
def train(model, iterator, optimizer, criterion):
epoch_loss = 0
epoch_acc = 0
model.train()
for batch in iterator:
optimizer.zero_grad() # 梯度清零
text, text_lengths = batch.text # batch.text返回的是一个元组(数字化的张量,每个句子的长度)
predictions = model(text, text_lengths).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)
2.6.5 Определение тестовой функции
Уведомление: Поскольку сейчас мы используем отсев, мы должны помнить об использованииmodel.eval()
чтобы убедиться, что отсев отключен при оценке.
def evaluate(model, iterator, criterion):
epoch_loss = 0
epoch_acc = 0
model.eval()
with torch.no_grad():
for batch in iterator:
text, text_lengths = batch.text #batch.text返回的是一个元组(数字化的张量,每个句子的长度)
predictions = model(text, text_lengths).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
2.6.6 Формальное обучение модели
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(), 'tut2-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}%')
2.6.7 Окончательные результаты испытаний
model.load_state_dict(torch.load('tut2-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.334 | Test Acc: 85.28%
2.7 Проверка модели
Теперь мы можем использовать эту модель, чтобы предсказать настроение любого предложения, которое мы даем, отметив, что предложение, которое нам нужно дать, относится к рецензиям на фильмы.
При использовании модели для реальных прогнозов модель всегда должна находиться в режиме оценки.
Функция «predict_sentiment» делает следующее:
- Переключите модель в режим оценки
- словесная сегментация предложений
- Преобразовать каждое слово после сегментации слова, соответствующее словарному запасу, в соответствующий индексный индекс,
- Получить длину предложения
- Преобразование индексов из списка в тензор
- Добавьте пакетное измерение, распаковав
- Преобразовать длину в тензор тензор
- Используйте сигмовидную функцию для сжатия прогнозируемых значений между 0-1
- Используйте метод item() для преобразования тензора только с одним значением в целое число.
Отрицательные отзывы возвращают значения, близкие к 0, а положительные отзывы возвращают значения, близкие к 1.
import spacy
nlp = spacy.load('en_core_web_sm')
def predict_sentiment(model, sentence):
model.eval()
tokenized = [tok.text for tok in nlp.tokenizer(sentence)]
indexed = [TEXT.vocab.stoi[t] for t in tokenized]
length = [len(indexed)]
tensor = torch.LongTensor(indexed).to(device)
tensor = tensor.unsqueeze(1)
length_tensor = torch.LongTensor(length)
prediction = torch.sigmoid(model(tensor, length_tensor))
return prediction.item()
Примеры негативных отзывов:
predict_sentiment(model, "This film is terrible")
0.05380420759320259
Примеры положительных отзывов:
predict_sentiment(model, "This film is great")
0.94941645860672
резюме
Теперь мы создали модель анализа настроений для обзоров фильмов. В следующем подразделе мы реализуем модель, которая обеспечивает более высокую точность и более быстрое обучение с меньшим количеством параметров.