Предыдущий ‹‹ Введение в обработку естественного языка с помощью PyTorch (1/5)

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

Для начала давайте загрузим набор данных AG_News и создадим словарь.
Чтобы упростить задачу, все эти операции объединены в функцию load_dataset соответствующего модуля Python:

!pip install -r https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/nlp-pytorch/requirements.txt
!wget -q https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/nlp-pytorch/torchnlp.py
import torch
import torchtext
import os
import collections
from torchnlp import *
train_dataset, test_dataset, classes, vocab = load_dataset()
vocab_size = len(vocab)
print("Vocab size = ",vocab_size)
Loading dataset...
Building vocab...
Vocab size =  95812

Представление «Мешок слов» (BoW)

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

Векторное представление «Мешок слов» (BoW) является наиболее часто используемым традиционным векторным представлением. Каждое слово связано с индексом вектора, элемент вектора содержит количество вхождений слова в данный документ.

Примечание. Вы также можете думать о BoW как о сумме всех векторов горячего кодирования для отдельных слов в тексте. Ниже приведен пример того, как создать представление BoW для текста с использованием векторизации, определенной ранее:

def to_bow(text,bow_vocab_size=vocab_size):
    res = torch.zeros(bow_vocab_size,dtype=torch.float32)
    for i in encode(text):
        if i<bow_vocab_size:
            res[i] += 1
    return res

print(f"sample text:\n{train_dataset[0][1]}")
print(f"\nBoW vector:\n{to_bow(train_dataset[0][1])}")
sample text:
Wall St. Bears Claw Back Into the Black (Reuters) Reuters - Short-sellers, Wall Street's dwindling\band of ultra-cynics, are seeing green again.

BoW vector:
tensor([0., 0., 2.,  ..., 0., 0., 0.])

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

Классификатор BoW для поездов

Теперь, когда мы узнали, как построить BoW-представление нашего текста, давайте на его основе обучим классификатор. Во-первых, нам нужно преобразовать наш набор данных для обучения таким образом, чтобы все представления позиционных векторов были преобразованы в представление BoW. Этого можно добиться, передав функцию bowify в качестве параметра collate_fn в стандартный DataLoader Torch.
collate_fn дает вам возможность применить собственную функцию к набору данных, загруженному Dataloader:

from torch.utils.data import DataLoader
import numpy as np 

# this collate function gets list of batch_size tuples, and needs to 
# return a pair of label-feature tensors for the whole minibatch
def bowify(b):
    return (
            torch.LongTensor([t[0]-1 for t in b]),
            torch.stack([to_bow(t[1]) for t in b])
    )

train_loader = DataLoader(train_dataset, batch_size=16, collate_fn=bowify, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=16, collate_fn=bowify, shuffle=True)

Теперь давайте определим простую нейронную сеть-классификатор, содержащую один линейный слой. Размер входного вектора равен vocab_size, а выходной размер соответствует количеству классов (4). Поскольку мы решаем задачу классификации, последней функцией активации является LogSoftmax().

net = torch.nn.Sequential(torch.nn.Linear(vocab_size,4),torch.nn.LogSoftmax(dim=1))

Теперь мы определим стандартный цикл обучения PyTorch. Поскольку наш набор данных довольно велик, для целей обучения мы будем обучать только одну эпоху, а иногда даже меньше эпохи (указание параметра epoch_size позволяет нам ограничить обучение). Мы также сообщим о накопленной точности обучения во время обучения; частота отправки отчетов указывается с помощью параметра report_freq.

def train_epoch(net,dataloader,lr=0.01,optimizer=None,loss_fn = torch.nn.NLLLoss(),epoch_size=None, report_freq=200):
    optimizer = optimizer or torch.optim.Adam(net.parameters(),lr=lr)
    net.train()
    total_loss,acc,count,i = 0,0,0,0
    for labels,features in dataloader:
        optimizer.zero_grad()
        out = net(features)
        loss = loss_fn(out,labels) #cross_entropy(out,labels)
        loss.backward()
        optimizer.step()
        total_loss+=loss
        _,predicted = torch.max(out,1)
        acc+=(predicted==labels).sum()
        count+=len(labels)
        i+=1
        if i%report_freq==0:
            print(f"{count}: acc={acc.item()/count}")
        if epoch_size and count>epoch_size:
            break
    return total_loss.item()/count, acc.item()/count

Давайте посмотрим, как классификатор работает с обучающим набором данных.

train_epoch(net,train_loader,epoch_size=15000)
3200: acc=0.80125
6400: acc=0.83984375
9600: acc=0.8560416666666667
12800: acc=0.8628125

(0.025899573938170477, 0.8665378464818764)

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

Частота термина — обратное представление частоты документа (TF-IDF)

В BoW вхождения слов имеют одинаковый вес, независимо от самого слова. Однако ясно, что часто встречающиеся слова, такие как «а», «в», «the» и т. д., гораздо менее важны для классификации, чем специализированные термины. Фактически, в большинстве задач НЛП одни слова более уместны, чем другие.

TF-IDF означает термин «частота – обратная частота документов». Это разновидность сумки слов, где вместо двоичного значения 0/1, указывающего появление слова в документе, используется значение с плавающей запятой, которое связано с частотой появления слова в корпусе.

Формула для расчета TF-IDF: w_ij = tf_ij × log(N/df_i)

Вот значение каждого параметра в формуле:
 – i – слово
 – j – документ
 – w_ij — вес или важность слова в документе
- tf_ij — количество вхождений слова i в документ j,
т. е. значение BoW, которое мы видели ранее.
- N — количество документов в коллекции
df_i — количество документов, содержащих слово i во всей коллекции.

Значение TF-IDF w_ij увеличивается пропорционально количеству раз, когда слово появляется в документе, и компенсируется количеством документов в корпусе, содержащих это слово, что помогает скорректировать тот факт, что некоторые слова появляются чаще других. Например, если слово встречается в каждом документе в коллекции, df_i = N и w_ij = 0, эти термины будут полностью игнорироваться.

Сначала давайте посчитаем частоту появления документовdf_i для каждого слова i. Мы можем представить его как тензор размера vocab_size. Мы ограничим количество документов до N=1000, чтобы ускорить обработку. Для каждого входного предложения мы вычисляем набор слов (представленный их номерами) и увеличиваем соответствующий счетчик:

N = 1000
df = torch.zeros(vocab_size)
for _,line in train_dataset[:N]:
    for i in set(encode(line)):
        df[i] += 1

Теперь, когда у нас есть частоты документов для каждого слова, мы можем определить функцию tf_idf, которая будет принимать строку и создавать вектор TF-IDF. Мы будем использовать to_bow, определенный выше, чтобы вычислить вектор частоты термина и умножить его на обратную частоту документа соответствующего термина. Помните, что все тензорные операции выполняются поэлементно, что позволяет нам реализовать все вычисления в виде тензорной формулы:

def tf_idf(s):
    bow = to_bow(s)
    return bow*torch.log((N+1)/(df+1))

print(tf_idf(train_dataset[0][1]))
tensor([0.0000, 0.0000, 0.0363,  ..., 0.0000, 0.0000, 0.0000])

Несмотря на то, что представление TF-IDF вычисляет разные веса для разных слов в зависимости от их важности, оно не может правильно передать смысл, во многом потому, что порядок слов в предложении все еще не учитывается. Цитата известного лингвиста Дж. Р. Ферта (1935):
«Полное значение слова всегда контекстуально, и никакое исследование значения вне контекста не может восприниматься всерьез».

Приятного обучения!
Далее ›› Введение в обработку естественного языка с помощью PyTorch (3/5)