ЕСИМ для рассуждений

NLP

Я познакомился с более влиятельной моделью в области Inference — ESIM. В то же время шерсть Colab была зачищена.

Введение в модель ESIM

Enhanced LSTM for Natural Language InferenceВ этой статье предлагается модель для вычисления подобия двух предложений. Модель состоит из 3-х частей:

Input Encoding

Во-первых, два входных предложения, словесный вектор предпосылки и гипотезы.a=(a_1,...,a_{l_a})иb=(b_1,...,b_{l_b})После процесса BiLSTM получается новое представление вектора слов.(\bar{a_1}, \dots, \bar{a_{l_a}})и(\bar{b_1}, \dots, \bar{b_{l_b}}).

Local Inference

В документе говорится, что лучший способ вычислить степень корреляции между двумя словами — это вычислить скалярное произведение вектора слов, которое равноe_{ij}=\bar{a_i}^T\bar{b_j}. Таким образом, вычислив сходство (внимание) между всеми парами слов двух предложений, можно получить матрицу

(e_{ij})_{l_a \times l_b} = (\bar{a_i}^T\bar{b_j})_{l_a \times l_b}

Затем возникает очень интересная мысль: поскольку мы хотим судить о сходстве двух предложений, нам нужно посмотреть, могут ли они представлять друг друга. То есть используйте векторы слов в посылке и гипотезе соответственно.\bar{a_i}и\bar{b_i}Представляет вектор слов противника.

Формула в статье следующая:

\widetilde{a_i} = \sum_{j=1}^{l_b}{\frac{exp(e_{ij})}{\sum_{k=1}^{l_b}{exp(e_{ik})}}\bar{b_j}}
\widetilde{b_j} = \sum_{i=1}^{l_a}{\frac{exp(e_{ij})}{\sum_{k=1}^{l_a}{exp(e_{kj})}}\bar{a_i}}

Перевод такой, потому что модель не знает, какая пара должна бытьa_iиb_jЭто похоже или относительно, поэтому операция перечисления выполняется для выражения всех ситуаций. Матрица подобия, рассчитанная ранее, используется для взвешивания. Вес в каждой позиции — это текущая строка матрицы весов (для вычисления\widetilde{a_i}, для вычислений\widetilde{b_j}значение Softmax столбца).

В целях усиления рассуждений (улучшение информации для вывода) в статье собраны промежуточные результаты, полученные ранее.

m_a = [\bar{a};\widetilde{a};\bar{a}-\widetilde{a};\bar{a} \odot \widetilde{a}]
m_b = [\bar{b};\widetilde{b};\bar{b}-\widetilde{b};\bar{b} \odot \widetilde{b}]

Inference Composition

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

После объединения всей информации она отправляется на полносвязный слой для завершения окончательного смешивания.

Импорт необходимых библиотек

import os
import time
import logging
import pickle
from tqdm import tqdm_notebook as tqdm

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

import torchtext
from torchtext import data, datasets
from torchtext.vocab import GloVe

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

import nltk
from nltk import word_tokenize
import spacy
from keras_preprocessing.text import Tokenizer

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
cuda

Смонтировать Google Диск

from google.colab import drive
drive.mount('/content/drive')
Go to this URL in a browser: https://accounts.google.com/o/oauth2/xxxxxxxx

Enter your authorization code:
··········
Mounted at /content/drive
!nvidia-smi
Fri Aug  9 04:45:35 2019       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 418.67       Driver Version: 410.79       CUDA Version: 10.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla K80           Off  | 00000000:00:04.0 Off |                    0 |
| N/A   60C    P0    62W / 149W |   6368MiB / 11441MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
+-----------------------------------------------------------------------------+

Подготовьте данные с помощью torchtext

Использование torchtext относится к ссылке:GitHub.com/py torch/ отвратительно…

GloVe в torchtext можно использовать напрямую, но поскольку он не предоставляет функции прямого чтения исходных файлов, аналогичной torchvision, он может только читать кэш, поэтому лучше всего:

  1. Сначала загрузите GloVe локально
  2. Откройте терминал в каталоге загрузки, а затем используйте torchtext для создания кеша в терминале.
  3. При использовании GloVe в будущем увеличьте параметр cache, чтобы torchtext читался из кеша, а не загружал огромный GloVe в локальный

Но если это шерсть Colab, это легко (~ ̄▽ ̄)~

torchtext также может напрямую загружать набор данных SNLI, но структура каталога загрузки набора данных выглядит следующим образом:

  • root
    • snli_1.0
      • snli_1.0_train.jsonl
      • snli_1.0_dev.jsonl
      • snli_1.0_test.jsonl
TEXT = data.Field(batch_first=True, lower=True, tokenize="spacy")
LABEL = data.Field(sequential=False)

# 分离训练、验证、测试集
tic = time.time()
train, dev, test = datasets.SNLI.splits(TEXT, LABEL)
print(f"Cost: {(time.time() - tic) / 60:.2f} min")

# 加载GloVe预训练向量
tic = time.time()
glove_vectors = GloVe(name='6B', dim=100)
print(f"Creat GloVe done. Cost: {(time.time() - tic) / 60:.2f} min")

# 创建词汇表
tic = time.time()
TEXT.build_vocab(train, dev, test, vectors=glove_vectors)
LABEL.build_vocab(train)
print(f"Build vocab done. Cost: {(time.time() - tic) / 60:.2f} min")

print(f"TEXT.vocab.vectors.size(): {TEXT.vocab.vectors.size()}")
num_words = int(TEXT.vocab.vectors.size()[0])

# 保存分词和词向量的对应字典
if os.path.exists("/content/drive/My Drive/Colab Notebooks"):
    glove_stoi_path = "/content/drive/My Drive/Colab Notebooks/vocab_label_stoi.pkl"
else:
    glove_stoi_path = "./vocab_label_stoi.pkl"
pickle.dump([TEXT.vocab.stoi, LABEL.vocab.stoi], open(glove_stoi_path, "wb"))

batch_sz = 128

train_iter, dev_iter, test_iter = data.BucketIterator.splits(
    datasets=(train, dev, test),
    batch_sizes=(batch_sz, batch_sz, batch_sz),
    shuffle=True,
    device=device
)
Cost: 7.94 min
Creat GloVe done. Cost: 0.00 min
Build vocab done. Cost: 0.12 min
TEXT.vocab.vectors.size(): torch.Size([34193, 100])

Общая конфигурация параметров

При совершенствовании алхимии лучше всего иметь глобальную формулу, которую легко настроить.

class Config:

    def __init__(self):
        # For data
        self.batch_first = True
        try:
            self.batch_size = batch_sz
        except NameError:
            self.batch_size = 512

        # For Embedding
        self.n_embed = len(TEXT.vocab)
        self.d_embed = TEXT.vocab.vectors.size()[-1]

        # For Linear
        self.linear_size = self.d_embed

        # For LSTM
        self.hidden_size = 300

        # For output
        self.d_out = len(LABEL.vocab)  # 表示输出为几维
        self.dropout = 0.5

        # For training
        self.save_path = r"/content/drive/My Drive/Colab Notebooks" if os.path.exists(
            r"/content/drive/My Drive/Colab Notebooks") else "./"
        self.snapshot = os.path.join(self.save_path, "ESIM.pt")

        self.device = device
        self.epoch = 64
        self.scheduler_step = 3
        self.lr = 0.0004
        self.early_stop_ratio = 0.985  # 可以提早结束训练过程


args = Config()

Реализация кода модели ESIM

Ссылка на код:GitHub.com/Пэн Шуан/…

Использование nn.BatchNorm1d

Регуляризация данных может устранить проблему различного распределения данных в разных измерениях.Геометрическое понимание состоит в том, чтобы упорядочить «эллипсоид» в n-мерном пространстве в «сферу», что может упростить сложность обучения модели и улучшить скорость обучения.

Однако, если все входные данные упорядочиваются, это потребует много времени.Пакетная нормализация — это компромиссный метод, который упорядочивает только входные данные размера_пакета. Вероятностное понимание заключается в оценке распределения всех выборок на основе распределения выборок размера партии.

ПиТорчnn.BatchNorm1dКак вы можете понять из названия, это пакетная регуляризация одномерных данных, поэтому здесь есть два ограничения:

  1. обучение (т.е. открытоеmodel.train()), предоставьте размер пакета не менее 2; для тестирования используйте (model.eval()) когда нет ограничения на размер пакета
  2. Предпоследним параметром по умолчанию является «пакет».

И форма каждого пакета данных, полученного моей предыдущей обработкой данных после векторного отображения слов,batch * seq_len * embed_dim, так что здесь 3 измерения. и после тортекстаdata.BucketIterator.splitsобработка, за партиюseq_lenявляется динамическим (той же длины, что и самое длинное предложение в текущем пакете). Таким образом, если никакая обработка не добавляется, она непосредственно вводится вBatchNorm1d, вы обычно увидите следующую ошибку:

RuntimeError: running_mean should contain xxx elements not yyy

Нужно ли добавлять слой BatchNorm1d после внедрения

Реализация эталонного кода очень красивая, видно мастерство автора кода. Однако автор, похоже, не использует предварительно обработанный вектор слов в качестве вектора встраивания, но я использую предварительно обученный вектор слов GloVe и не буду обучать Glove, поэтому необходимо увеличиватьnn.BatchNorm1d?

Поскольку слепое увеличение количества слоев в сети не даст хорошего эффекта, лучше всего сначала посмотреть, является ли вектор слов GloVe «регуляризованным» в каждом измерении.

glove = TEXT.vocab.vectors

means, stds = glove.mean(dim=0).numpy(), glove.std(dim=0).numpy()
dims = [i for i in range(glove.shape[1])]

plt.scatter(dims, means)
plt.scatter(dims, stds)
plt.legend(["mean", "std"])
plt.xlabel("Dims")
plt.ylabel("Features")
plt.show()

print(f"mean(means)={means.mean():.4f}, std(means)={means.std():.4f}")
print(f"mean(stds)={stds.mean():.4f}, std(stds)={stds.std():.4f}")

mean(means)=0.0032, std(means)=0.0809
mean(stds)=0.4361, std(stds)=0.0541

Из рисунка видно, что распределение каждого измерения относительно стабильно, поэтому его не предполагается использовать после слоя Embedding.nn.BatchNorm1d.

Использование nn.LSTM

nn.LSTM(
   input_size, hidden_size, num_layers, bias=True, batch_first=False, dropout=0, bidirectional=False)
)

nn.LSTMПараметр по умолчанию для batch_first:False, мне это будет очень неудобно, кто привык к формату данных CV, так что лучше поставить егоTrue.

Ниже приведен формат ввода/вывода LSTM. Входные данные могут быть опущеныh_0иc_0, в это время LSTM автоматически сгенерирует все 0h_0иc_0.

  • Inputs: input, (h_0, c_0)
  • Outputs: output, (h_n, c_n)
  • input: (seq_len, batch, input_size)
  • output: (seq_len, batch, num_directions * hidden_size)
  • h / c: (num_layers * num_directions, batch, hidden_size)
class ESIM(nn.Module):

    def __init__(self, args):
        super(ESIM, self).__init__()
        self.args = args

        self.embedding = nn.Embedding(
            args.n_embed, args.d_embed)  # 参数的初始化可以放在之后
        # self.bn_embed = nn.BatchNorm1d(args.d_embed)

        self.lstm1 = nn.LSTM(args.d_embed, args.hidden_size,
                             num_layers=1, batch_first=True, bidirectional=True)
        self.lstm2 = nn.LSTM(args.hidden_size * 8, args.hidden_size,
                             num_layers=1, batch_first=True, bidirectional=True)

        self.fc = nn.Sequential(
            nn.BatchNorm1d(args.hidden_size * 8),
            nn.Linear(args.hidden_size * 8, args.linear_size),
            nn.ELU(inplace=True),
            nn.BatchNorm1d(args.linear_size),
            nn.Dropout(args.dropout),
            nn.Linear(args.linear_size, args.linear_size),
            nn.ELU(inplace=True),
            nn.BatchNorm1d(args.linear_size),
            nn.Dropout(args.dropout),
            nn.Linear(args.linear_size, args.d_out),
            nn.Softmax(dim=-1)
        )

    def submul(self, x1, x2):
        mul = x1 * x2
        sub = x1 - x2
        return torch.cat([sub, mul], -1)

    def apply_multiple(self, x):
        # input: batch_size * seq_len * (2 * hidden_size)
        p1 = F.avg_pool1d(x.transpose(1, 2), x.size(1)).squeeze(-1)
        p2 = F.max_pool1d(x.transpose(1, 2), x.size(1)).squeeze(-1)
        # output: batch_size * (4 * hidden_size)
        return torch.cat([p1, p2], 1)

    def soft_attention_align(self, x1, x2, mask1, mask2):
        '''
        x1: batch_size * seq_len * dim
        x2: batch_size * seq_len * dim
        '''
        # attention: batch_size * seq_len * seq_len
        attention = torch.matmul(x1, x2.transpose(1, 2))
        # mask的作用:防止计算Softmax的时候出现异常值
        mask1 = mask1.float().masked_fill_(mask1, float('-inf'))
        mask2 = mask2.float().masked_fill_(mask2, float('-inf'))

        # weight: batch_size * seq_len * seq_len
        weight1 = F.softmax(attention + mask2.unsqueeze(1), dim=-1)
        x1_align = torch.matmul(weight1, x2)
        weight2 = F.softmax(attention.transpose(
            1, 2) + mask1.unsqueeze(1), dim=-1)
        x2_align = torch.matmul(weight2, x1)

        # x_align: batch_size * seq_len * hidden_size
        return x1_align, x2_align

    def forward(self, sent1, sent2):
        """
        sent1: batch * la
        sent2: batch * lb
        """
        mask1, mask2 = sent1.eq(0), sent2.eq(0)
        x1, x2 = self.embedding(sent1), self.embedding(sent2)
        # x1, x2 = self.bn_embed(x1), self.bn_embed(x2)

        # batch * [la | lb] * dim
        o1, _ = self.lstm1(x1)
        o2, _ = self.lstm1(x2)

        # Local Inference
        # batch * [la | lb] * hidden_size
        q1_align, q2_align = self.soft_attention_align(o1, o2, mask1, mask2)

        # Inference Composition
        # batch_size * seq_len * (8 * hidden_size)
        q1_combined = torch.cat([o1, q1_align, self.submul(o1, q1_align)], -1)
        q2_combined = torch.cat([o2, q2_align, self.submul(o2, q2_align)], -1)

        # batch_size * seq_len * (2 * hidden_size)
        q1_compose, _ = self.lstm2(q1_combined)
        q2_compose, _ = self.lstm2(q2_combined)

        # Aggregate
        q1_rep = self.apply_multiple(q1_compose)
        q2_rep = self.apply_multiple(q2_compose)

        # Classifier
        similarity = self.fc(torch.cat([q1_rep, q2_rep], -1))
        return similarity


def take_snapshot(model, path):
    """保存模型训练结果到Drive上,防止Colab重置后丢失"""
    torch.save(model.state_dict(), path)
    print(f"Snapshot has been saved to {path}")


def load_snapshot(model, path):
    model.load_state_dict(torch.load(path))
    print(f"Load snapshot from {path} done.")


model = ESIM(args)
# if os.path.exists(args.snapshot):
#     load_snapshot(model, args.snapshot)

# Embedding向量不训练
model.embedding.weight.data.copy_(TEXT.vocab.vectors)
model.embedding.weight.requires_grad = False

model.to(args.device)
ESIM(
  (embedding): Embedding(34193, 100)
  (lstm1): LSTM(100, 300, batch_first=True, bidirectional=True)
  (lstm2): LSTM(2400, 300, batch_first=True, bidirectional=True)
  (fc): Sequential(
    (0): BatchNorm1d(2400, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (1): Linear(in_features=2400, out_features=100, bias=True)
    (2): ELU(alpha=1.0, inplace)
    (3): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (4): Dropout(p=0.5)
    (5): Linear(in_features=100, out_features=100, bias=True)
    (6): ELU(alpha=1.0, inplace)
    (7): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (8): Dropout(p=0.5)
    (9): Linear(in_features=100, out_features=4, bias=True)
    (10): Softmax()
  )
)

фаза обучения

Вот несколько деталей:

форма партии.этикетка

batch.labelпредставляет собой одномерный вектор формы (пакет); иY_predэто формаbatch \times 42D вектор , используя.topk(1).indicesЭто все еще двумерный вектор после извлечения максимального значения.

Так что, если вы не расширяетеbatch.labelизмерение, которое PyTorch автоматически транслируетbatch.label, окончательный результат больше неbatch \times 1, ноbatch \times batch, то итоговая расчетная точность будет запредельной. Вот что означает приведенный ниже код:

(Y_pred.topk(1).indices == batch.label.unsqueeze(1))

Разделение тензора и скаляра

В Python 3.6 символ деления/Результатом по умолчанию является float, но это не относится к PyTorch, и это еще одна деталь, которую легко упустить из виду.

(Y_pred.topk(1).indices == batch.label.unsqueeze(1))

Результат приведенного выше кода можно рассматривать как тип bool (на самом делеtorch.uint8). перечислить.sum()Тип результата после суммированияtorch.LongTensor. Но целочисленное деление в PyTorch не приведет к числам с плавающей запятой.

# 就像下面的代码会得到0一样
In [2]: torch.LongTensor([1]) / torch.LongTensor([5])
Out[2]: tensor([0])

Переменная acc накапливает правильное количество выборок в каждой партии.Из-за автоматического преобразования типов acc теперь указывает наtorch.LongTensorтип, поэтому его необходимо использовать при расчете показателя точности в конце..item()Извлеките целочисленное значение. Если эту деталь игнорировать, итоговая степень точности равна 0.

def training(model, data_iter, loss_fn, optimizer):
    """训练部分"""
    model.train()
    data_iter.init_epoch()
    acc, cnt, avg_loss = 0, 0, 0.0

    for batch in data_iter:
        Y_pred = model(batch.premise, batch.hypothesis)
        loss = loss_fn(Y_pred, batch.label)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        avg_loss += loss.item() / len(data_iter)
        # unsqueeze是因为label是一维向量,下同
        acc += (Y_pred.topk(1).indices == batch.label.unsqueeze(1)).sum()
        cnt += len(batch.premise)

    return avg_loss, (acc.item() / cnt)  # 如果不提取item,会导致accuracy为0


def validating(model, data_iter, loss_fn):
    """验证部分"""
    model.eval()
    data_iter.init_epoch()
    acc, cnt, avg_loss = 0, 0, 0.0

    with torch.set_grad_enabled(False):
        for batch in data_iter:
            Y_pred = model(batch.premise, batch.hypothesis)

            avg_loss += loss_fn(Y_pred, batch.label).item() / len(data_iter)
            acc += (Y_pred.topk(1).indices == batch.label.unsqueeze(1)).sum()
            cnt += len(batch.premise)

    return avg_loss, (acc.item() / cnt)


def train(model, train_data, val_data):
    """训练过程"""
    optimizer = optim.Adam(model.parameters(), lr=args.lr)
    loss_fn = nn.CrossEntropyLoss()
    scheduler = optim.lr_scheduler.ReduceLROnPlateau(
        optimizer, mode='min', factor=0.5, patience=args.scheduler_step, verbose=True)

    train_losses, val_losses, train_accs, val_accs = [], [], [], []

    # Before train
    tic = time.time()
    train_loss, train_acc = validating(model, train_data, loss_fn)
    val_loss, val_acc = validating(model, val_data, loss_fn)
    train_losses.append(train_loss)
    val_losses.append(val_loss)
    train_accs.append(train_acc)
    val_accs.append(val_acc)
    min_val_loss = val_loss
    print(f"Epoch: 0/{args.epoch}\t"
          f"Train loss: {train_loss:.4f}\tacc: {train_acc:.4f}\t"
          f"Val loss: {val_loss:.4f}\tacc: {val_acc:.4f}\t"
          f"Cost time: {(time.time()-tic):.2f}s")

    try:
        for epoch in range(args.epoch):
            tic = time.time()
            train_loss, train_acc = training(
                model, train_data, loss_fn, optimizer)
            val_loss, val_acc = validating(model, val_data, loss_fn)
            train_losses.append(train_loss)
            val_losses.append(val_loss)
            train_accs.append(train_acc)
            val_accs.append(val_acc)
            scheduler.step(val_loss)

            print(f"Epoch: {epoch + 1}/{args.epoch}\t"
                  f"Train loss: {train_loss:.4f}\tacc: {train_acc:.4f}\t"
                  f"Val loss: {val_loss:.4f}\tacc: {val_acc:.4f}\t"
                  f"Cost time: {(time.time()-tic):.2f}s")

            if val_loss < min_val_loss:  # 即时保存
                min_val_loss = val_loss
                take_snapshot(model, args.snapshot)

            # Early-stop:
            # if len(val_losses) >= 3 and (val_loss - min_val_loss) / min_val_loss > args.early_stop_ratio:
            #   print(f"Early stop with best loss: {min_val_loss:.5f}")
            #   break
            # args.early_stop_ratio *= args.early_stop_ratio

    except KeyboardInterrupt:
        print("Interrupted by user")

    return train_losses, val_losses, train_accs, val_accs


train_losses, val_losses, train_accs, val_accs = train(
    model, train_iter, dev_iter)
Epoch: 0/64	Train loss: 1.3871	acc: 0.3335	Val loss: 1.3871	acc: 0.3331	Cost time: 364.32s
Epoch: 1/64	Train loss: 1.0124	acc: 0.7275	Val loss: 0.9643	acc: 0.7760	Cost time: 998.41s
Snapshot has been saved to /content/drive/My Drive/Colab Notebooks/ESIM.pt
Epoch: 2/64	Train loss: 0.9476	acc: 0.7925	Val loss: 0.9785	acc: 0.7605	Cost time: 1003.32s
Epoch: 3/64	Train loss: 0.9305	acc: 0.8100	Val loss: 0.9204	acc: 0.8217	Cost time: 999.49s
Snapshot has been saved to /content/drive/My Drive/Colab Notebooks/ESIM.pt
Epoch: 4/64	Train loss: 0.9183	acc: 0.8227	Val loss: 0.9154	acc: 0.8260	Cost time: 1000.97s
Snapshot has been saved to /content/drive/My Drive/Colab Notebooks/ESIM.pt
Epoch: 5/64	Train loss: 0.9084	acc: 0.8329	Val loss: 0.9251	acc: 0.8156	Cost time: 996.99s
....
Epoch: 21/64	Train loss: 0.8236	acc: 0.9198	Val loss: 0.8912	acc: 0.8514	Cost time: 992.48s
Epoch: 22/64	Train loss: 0.8210	acc: 0.9224	Val loss: 0.8913	acc: 0.8514	Cost time: 996.35s
Epoch    22: reducing learning rate of group 0 to 5.0000e-05.
Epoch: 23/64	Train loss: 0.8195	acc: 0.9239	Val loss: 0.8940	acc: 0.8485	Cost time: 1000.48s
Epoch: 24/64	Train loss: 0.8169	acc: 0.9266	Val loss: 0.8937	acc: 0.8490	Cost time: 1006.78s
Interrupted by user

Нарисуйте кривую потери-точности

iters = [i + 1 for i in range(len(train_losses))]

# 防止KeyboardInterrupt的打断导致两组loss不等长
min_len = min(len(train_losses), len(val_losses))

# 绘制双纵坐标图
fig, ax1 = plt.subplots()
ax1.plot(iters, train_losses[: min_len], '-', label='train loss')
ax1.plot(iters, val_losses[: min_len], '-.', label='val loss')
ax1.set_xlabel("Epoch")
ax1.set_ylabel("Loss")

# 创建子坐标轴
ax2 = ax1.twinx()
ax2.plot(iters, train_accs[: min_len], ':', label='train acc')
ax2.plot(iters, val_accs[: min_len], '--', label='val acc')
ax2.set_ylabel("Accuracy")

# 为双纵坐标图添加图例
handles1, labels1 = ax1.get_legend_handles_labels()
handles2, labels2 = ax2.get_legend_handles_labels()
plt.legend(handles1 + handles2, labels1 + labels2, loc='center right')
plt.show()

Loss曲线

предсказывать

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

nlp = spacy.load("en")

# 重新加载之前训练结果最棒的模型参数
load_snapshot(model, args.snapshot)
# 小规模数据还是cpu跑得快
model.to(torch.device("cpu"))

with open(r"/content/drive/My Drive/Colab Notebooks/vocab_label_stoi.pkl", "rb") as f:
    vocab_stoi, label_stoi = pickle.load(f)
Load snapshot from /content/drive/My Drive/Colab Notebooks/ESIM.pt done.
def sentence2tensor(stoi, sent1: str, sent2: str):
    """将两个句子转化为张量"""
    sent1 = [str(token) for token in nlp(sent1.lower())]
    sent2 = [str(token) for token in nlp(sent2.lower())]

    tokens1, tokens2 = [], []

    for token in sent1:
        tokens1.append(stoi[token])

    for token in sent2:
        tokens2.append(stoi[token])

    delt_len = len(tokens1) - len(tokens2)

    if delt_len > 0:
        tokens2.extend([1] * delt_len)
    else:
        tokens1.extend([1] * (-delt_len))

    tensor1 = torch.LongTensor(tokens1).unsqueeze(0)
    tensor2 = torch.LongTensor(tokens2).unsqueeze(0)

    return tensor1, tensor2


def use(model, premise: str, hypothsis: str):
    """使用模型测试"""
    label_itos = {0: '<unk>', 1: 'entailment',
                  2: 'contradiction', 3: 'neutral'}

    model.eval()
    with torch.set_grad_enabled(False):
        tensor1, tensor2 = sentence2tensor(vocab_stoi, premise, hypothsis)
        predict = model(tensor1, tensor2)
        top1 = predict.topk(1).indices.item()

    print(f"The answer is '{label_itos[top1]}'")

    prob = predict.cpu().squeeze().numpy()
    plt.bar(["<unk>", "entailment", "contradiction", "neutral"], prob)
    plt.ylabel("probability")
    plt.show()

После ввода двух предложений выведите наиболее вероятные предположения и покажите вероятность каждого предположения в виде гистограммы.

# 蕴含
use(model,
    "A statue at a museum that no seems to be looking at.",
    "There is a statue that not many people seem to be interested in.")

# 对立
use(model,
    "A land rover is being driven across a river.",
    "A sedan is stuck in the middle of a river.")

# 中立
use(model,
    "A woman with a green headscarf, blue shirt and a very big grin.",
    "The woman is young.")
The answer is 'entailment'

The answer is 'contradiction'

The answer is 'neutral'