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

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

1.1 Введение

В этой части мы будем использовать pytorch и torchtext, чтобы построить простую модель машинного обучения для прогнозирования настроения предложения (т. е. будет ли настроение, выраженное предложением, положительным или отрицательным). Эта серия руководств будет работать с набором данных обзора фильмов:Набор данных IMDbзавершено на.

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

MDb数据集来源:
@InProceedings{maas-EtAl:2011:ACL-HLT2011,
  author    = {Maas, Andrew L.  and  Daly, Raymond E.  and  Pham, Peter T.  and  Huang, Dan  and  Ng, Andrew Y.  and  Potts, Christopher},
  title     = {Learning Word Vectors for Sentiment Analysis},
  booktitle = {Proceedings of the 49th Annual Meeting of the Association for Computational Linguistics: Human Language Technologies},
  month     = {June},
  year      = {2011},
  address   = {Portland, Oregon, USA},
  publisher = {Association for Computational Linguistics},
  pages     = {142--150},
  url       = {http://www.aclweb.org/anthology/P11-1015}
}

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

Одной из основных концепций TorchText являетсяField, они определяют, как следует обрабатывать данные. Наш набор данных представляет собой помеченный набор данных, т. е. данные состоят из исходной строки комментариев и настроений, «pos» для положительных настроений и «neg» для отрицательных настроений.

FieldПараметр указывает, как следует обрабатывать данные.

Мы используемTEXTполя, чтобы определить, как следует обрабатывать комментарии, и использоватьLABELПоле управления эмоциями.

нашTEXTполе имеетtokenize='spacy'как параметр. Это определяет, что «токенизация» (действие разделения строки на дискретные «токены») должна использоватьspaCyМаркер готов. Если не установленоtokenizeпараметр, по умолчанию строка разделяется пробелами. Нам также необходимо указатьtokenizer_languageчтобы сообщить torchtext, какую модель spaCy использовать. Мы используемen_core_web_smМодель.

скачатьen_core_web_smМетод модели:python -m spacy download en_core_web_sm

LABELЗависит отLabelFieldопределение, предназначенное для обработки теговFieldОсобое подмножество классов. Мы объясним позжеdtypeпараметр.

СвязанныйFieldДля получения дополнительной информации, пожалуйста, посетитездесь.

import torch
from torchtext.legacy import data

# 设置随机种子数,该数可以保证随机数是可重复的
SEED = 1234

# 设置种子
torch.manual_seed(SEED)
# 将这个 flag 置为True的话,每次返回的卷积算法将是确定的,即默认算法。如果配合上设置 Torch 的随机种子为固定值的话,应该可以保证每次运行网络的时候相同输入的输出是固定的
torch.backends.cudnn.deterministic = True  

# 读取数据和标签
TEXT = data.Field(tokenize = 'spacy', tokenizer_language = 'en_core_web_sm')
LABEL = data.LabelField(dtype = torch.float)

TorchTextЕще одна удобная функция заключается в том, что он поддерживает общие наборы данных, используемые в обработке естественного языка (NLP).

Следующий код автоматически загружает и разбивает набор данных IMDb на канонические наборы поездов и тестов какtorchtext.datasetsобъект. Он использует ранее определенныйFieldsОбработка данных. Набор данных IMDb содержит 50 000 обзоров фильмов, каждый из которых отмечен как положительный или отрицательный.

from torchtext.legacy import datasets

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

image.pngОзнакомьтесь с размерами наших поездов и тестовых наборов:

print(f'Number of training examples: {len(train_data)}')
print(f'Number of testing examples: {len(test_data)}')

image.png

Взгляните на пример данных:

print(vars(train_data.examples[0]))

image.png

Набор данных IMDb разделен на обучающий набор и тестовый набор, здесь нам также нужно создать проверочный набор. можно использовать.split()способ сделать.

По умолчанию данные будут разделены на набор для обучения и набор для проверки в соответствии с соотношением 70% и 30%, которое можно установить, настроивsplit_ratioпараметры для установки соотношения обучающего набора и проверочного набора, т.е.split_ratioЗначение 0,8 означает, что 80 % примеров составляют обучающий набор, а 20 % — проверочный набор.

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

import random

train_data, valid_data = train_data.split(split_ratio=0.8 , 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)}')

image.png

Далее нам нужно построитьГлоссарий. Это таблица поиска, в которой каждому слову в наборе данных соответствует уникальныйindex(целое число).

Мы делаем это, потому что наша модель не может работать со строками, а только с числами. каждыйindexдля каждого слова построитьone-hotвектор, обычно сVVВыражать.

image.png

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

Есть два способа оптимизировать наш однократный вектор: один состоит в том, чтобы взять только верхние n слов с наибольшим количеством вхождений в качестве основы однократного, другой — игнорировать слова с менее чем m вхождениями. В этом примере мы используем первый метод: используем 25 000 наиболее распространенных слов в качестве одноразовых кодировок.

На этом пути есть проблема: некоторые слова появляются в наборе данных, но не могут быть напрямую закодированы горячим способом. Здесь мы используем спец.<unk>для их кодирования. Например, если наше предложение звучит так: «Этот фильм великолепен, и я люблю его», но слова «любовь» нет в словаре, мы преобразуем предложение в следующее: «Этот фильм великолепен, и я<unk>Это".

Ниже мы строим словарь, оставляя только самые употребительныеmax_sizeотметка.

MAX_VOCAB_SIZE = 25000

TEXT.build_vocab(train_data, max_size = MAX_VOCAB_SIZE)
LABEL.build_vocab(train_data)

Зачем строить словарный запас только на тренировочном наборе? Потому что при тестировании модели ни один из них никак не может повлиять на тестовый набор. Конечно, проверочный набор также не включен, поскольку ожидается, что проверочный набор будет максимально точно отражать тестовый набор.

print(f"Unique tokens in TEXT vocabulary: {len(TEXT.vocab)}")
print(f"Unique tokens in LABEL vocabulary: {len(LABEL.vocab)}")

image.png

Почему размер словаря 25002 вместо 25000? Два других дополнительных токена<unk>и<pad>.

Когда мы загружаем предложения в нашу модель, мы загружаем их по одному пакету за раз, и все предложения в пакете должны быть одинаковой длины. Следовательно, должна быть установлена ​​максимальная длина. Чтобы гарантировать, что каждое предложение в пакете имеет одинаковый размер, любое предложение короче, чем максимальная длина, дополняется, а дополненная часть устанавливается на 0, а часть, превышающая максимальную длину, перехватывается напрямую.

image.png

Мы также можем посмотреть на самые распространенные слова в словаре и на то, сколько раз они встречаются в наборе данных.

print(TEXT.vocab.freqs.most_common(20))

image.png

также можно использоватьstoi (string to int) or itos (int to sstring) следующий метод выводит первые 10 слов text-vocab.

print(TEXT.vocab.itos[:10])

image.png

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

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

Если у вас есть gpu, конечно, вы можете поместить тензор, возвращаемый итератором, на GPU, вы можете использовать torch.device, а можете поместить тензор на gpu или cpu.

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)

1.3 Построение модели

Следующим шагом является построение модели, которую мы будем обучать и оценивать далее.

При использовании модели RNN в PyTorch при создании RNN используется не класс RNN, а подкласс nn.module.

существует__init__Количество слоев модели, которые мы определяем.Трехслойная модель-это слой встраивания, слой RNN и, наконец, полностью связанный слой.Инициализация параметров всех слоев является случайной, если некоторые параметры не установлены специально. Слой встраивания преобразует разреженный однократный в вектор, плотно вложенный в пространство. Слой встраивания представляет собой простой однослойный полносвязный слой. Это также уменьшает размерность входных данных для RNN и снижает вычислительную сложность операции с данными. Одна из теорий здесь заключается в том, что слова, оказывающие одинаковое влияние на настроение отзыва, отображаются близко друг к другу в векторном пространстве. Для получения дополнительной информации см.here.

Уровень RNN принимает предыдущее состояниеht1h_{t-1}Вектор плотного вложения, соответствующий текущему входу, эти две части используются для вычисления состояния скрытого слоя следующего слоя,hth_t.

image.png

Наконец, последний линейный слой получит состояние последнего скрытого слоя вывода RNN. Состояние скрытого слоя этого слоя содержит всю предыдущую информацию.Введите состояние скрытого слоя последнего слоя RNN в полносвязный слой, чтобы получитьf(hT)f(h_T), и, наконец, преобразуется в batch_size*num_classes. Прямой метод заключается в том, что когда мы вводим обучающие данные, набор данных проверки и набор тестовых данных в модель, данные будут переданы прямому методу, и будет получен результат вывода модели.

В каждой партии,text, имеет размер _[sentence length, batch size]_ тензор, Это получается путем преобразования однократного вектора, соответствующего каждому предложению.

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

Каждый входной пакет проходит через слой внедрения и будетembedded, чтобы получить плотное векторное представление каждого предложения.embeddedПосле того, как размер вектора[sentence length, batch size, embedding dim].

В некоторых фреймворках использование RNN требует инициализации.h0h_0, но не используется в pytorch, по умолчанию все 0. Использование RNN вернет 2 тензора,outputиhidden. Размер вывода _[sentence length, batch size, hidden dim]_ and hiddenРазмер _[1, batch size, hidden dim]_. outputскрытое состояние каждого слоя, иhiddenявляется скрытым состоянием последнего слоя. Примечания:squeezeметод, который может устранить размерность размерности 1.

На самом деле мы обычно используемhiddenВот и все, не волнуйсяoutput. Наконец, через линейный слойfc, который производит окончательный прогноз.

import torch.nn as nn

class RNN(nn.Module):
    def __init__(self, input_dim, embedding_dim, hidden_dim, output_dim):
        
        super().__init__()
        
        self.embedding = nn.Embedding(input_dim, embedding_dim)
        
        self.rnn = nn.RNN(embedding_dim, hidden_dim)
        
        self.fc = nn.Linear(hidden_dim, output_dim)
        
    def forward(self, text):

        #text = [sent len, batch size]
        
        embedded = self.embedding(text)
        
        #embedded = [sent len, batch size, emb dim]
        
        output, hidden = self.rnn(embedded)
        
        #output = [sent len, batch size, hid dim]
        #hidden = [1, batch size, hid dim]
        
        assert torch.equal(output[-1,:,:], hidden.squeeze(0))
        
        return self.fc(hidden.squeeze(0))

Ниже мы можем сделать пример построения RNN.

Входное измерение — это измерение, соответствующее однократному вектору, которое также эквивалентно размерности словаря.

Измерение встраивания — это гиперпараметр, который можно установить, обычно он составляет 50-250 измерений, что также в некоторой степени связано с размером словаря.

Размерность скрытого слоя — это размер последнего скрытого слоя, обычно его можно установить в 100-500 измерений, что также является размером словаря и сложностью задачи.

Размерность выходных данных — это количество категорий для классификации.

INPUT_DIM = len(TEXT.vocab) #词典大小
EMBEDDING_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1

model = RNN(INPUT_DIM, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM)

Вы также можете вывести количество параметров для обучения.

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

image.png

1.4 Обучение модели

Перед обучением модели сначала необходимо установить оптимизатор Здесь мы выбираем SGD, расчет стохастического градиентного спуска, model.parameters () указывает параметры, которые необходимо обновить, а lr — скорость обучения

import torch.optim as optim

optimizer = optim.SGD(model.parameters(), lr=1e-3)

Затем определите функцию потерь, BCEWithLogitsLoss обычно используется для двоичной классификации.

criterion = nn.BCEWithLogitsLoss()

использовать.to, вы можете поставить тензор на gpu для расчета.

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

Функция потерь используется для расчета величины потерь, а также функция для расчета точности.

Введите результат прогнозирования, выдаваемый сигмовидным слоем, в функцию, которая вычисляет точность, и округлите до ближайшего целого числа, если оно больше 0,5, возьмите 1. В противном случае берите 0.

Вычислите значение прогнозируемого результата, соответствующего метке, и разделите на все значения, чтобы получить точность.

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

trainФункция выполняет итерацию по всем образцам, каждый раз по одному пакету.

model.train()Ставим модель в "тренировочный режим", тоже открываемdropoutиbatch normalization. В каждом пакете сначала очищайте градиент до 0. Каждый параметр модели имеетgradСвойство, в котором хранится значение градиента, вычисленное функцией потерь. PyTorch не удаляет автоматически (или «обнуляет») градиент, вычисленный из последнего вычисления градиента, поэтому его необходимо обнулить вручную.

каждый раз, когда вы входите,batch.text, в модель. Просто вызовите модель.

использоватьloss.backward()Чтобы рассчитать градиент, обновите параметры, используяoptimizer.step().

Величина потерь и точность накапливаются за всю эпоху,.item()Извлеките значение в тензоре, который содержит только одно значение в тензоре.

Наконец, мы возвращаем потери и точность, усредненные по всей эпохе.lenВы можете получить количество партий в эпоху

Конечно, при расчете не забывайтеLongTensorпревратиться вtorch.float. Это связано с тем, что TorchText по умолчанию устанавливает тензоры какLongTensor.

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) #计算Loss
        
        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)  #返回整个epoch中损失和准确率的平均值

evaluateиtrainАналогично, если функция поезда немного изменена.

model.eval()Поместите модель в «режим оценки», который выключается.dropoutиbatch normalization.

существуетwith no_grad(), вычисления градиента не выполняются, что приводит к меньшему использованию памяти и более быстрым вычислениям.

другие функции вtrainАналогично в оценке, удалено в оценкеoptimizer.zero_grad(), loss.backward() and optimizer.step(), так как больше нет необходимости обновлять параметры

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(), 'tut1-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('tut1-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

1.5 Резюме

В следующей статье будут следующие оптимизации:

  • Сжатый тензор заполнения
  • Предварительно обученные вложения слов
  • Различные архитектуры RNN
  • Двунаправленный RNN
  • Многослойная РНС
  • Регуляризация
  • разные оптимизаторы

В конечном итоге улучшенная точность (84%)