Модель RNN (GRU) word2vec для регрессии не учится - PullRequest
0 голосов
/ 22 февраля 2019

Я конвертирую код Keras в PyTorch, потому что я больше знаком с последним, чем с первым.Однако я обнаружил, что это не обучение (или только обучение).

Ниже я предоставил почти весь мой код PyTorch, включая код инициализации, чтобы вы могли попробовать его самостоятельно.Единственное, что вам нужно предоставить - это встраивание слов (я уверен, вы можете найти много моделей word2vec в Интернете).Первый входной файл должен быть файлом с токенизированным текстом, второй входной файл должен быть файлом с числами с плавающей запятой, по одному на строку.Поскольку я предоставил весь код, этот вопрос может показаться огромным и слишком широким.Тем не менее, мой вопрос достаточно конкретен, я думаю: что не так в моей модели или в цикле обучения, что приводит к тому, что моя модель не улучшается или едва улучшается.(См. Результаты ниже.)

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

Наиболее важными частями являются прямой метод RegressorNet и обучающий цикл RegressionNN (по общему признанию, эти имена были выбраны неправильно).Я думаю, что ошибка где-то есть.

from pathlib import Path
import time

import numpy as np
import torch
from torch import nn, optim
from torch.utils.data import DataLoader
import gensim

from scipy.stats import pearsonr

from LazyTextDataset import LazyTextDataset


class RegressorNet(nn.Module):
    def __init__(self, hidden_dim, embeddings=None, drop_prob=0.0):
        super(RegressorNet, self).__init__()
        self.hidden_dim = hidden_dim
        self.drop_prob = drop_prob

        # Load pretrained w2v model, but freeze it: don't retrain it.
        self.word_embeddings = nn.Embedding.from_pretrained(embeddings)
        self.word_embeddings.weight.requires_grad = False
        self.w2v_rnode = nn.GRU(embeddings.size(1), hidden_dim, bidirectional=True, dropout=drop_prob)

        self.dropout = nn.Dropout(drop_prob)
        self.linear = nn.Linear(hidden_dim * 2, 1)
        # LeakyReLU rather than ReLU so that we don't get stuck in a dead nodes
        self.lrelu = nn.LeakyReLU()

    def forward(self, batch_size, sentence_input):
        # shape sizes for:
        # * batch_size 128
        # * embeddings of dim 146
        # * hidden dim of 200
        # * sentence length of 20

        # sentence_input: torch.Size([128, 20])
        # Get word2vec vector representation
        embeds = self.word_embeddings(sentence_input)
        # embeds: torch.Size([128, 20, 146])

        # embeds.view(-1, batch_size, embeds.size(2)): torch.Size([20, 128, 146])
        # Input vectors into GRU, only keep track of output
        w2v_out, _ = self.w2v_rnode(embeds.view(-1, batch_size, embeds.size(2)))
        # w2v_out = torch.Size([20, 128, 400])

        # Leaky ReLU it
        w2v_out = self.lrelu(w2v_out)

        # Dropout some nodes
        if self.drop_prob > 0:
            w2v_out = self.dropout(w2v_out)
        # w2v_out: torch.Size([20, 128, 400

        # w2v_out[-1, :, :]: torch.Size([128, 400])
        # Only use the last output of a sequence! Supposedly that cell outputs the final information
        regression = self.linear(w2v_out[-1, :, :])
        regression: torch.Size([128, 1])

        return regression


class RegressionRNN:
    def __init__(self, train_files=None, test_files=None, dev_files=None):
        print('Using torch ' + torch.__version__)

        self.datasets, self.dataloaders = RegressionRNN._set_data_loaders(train_files, test_files, dev_files)
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

        self.model = self.w2v_vocab = self.criterion = self.optimizer = self.scheduler = None

    @staticmethod
    def _set_data_loaders(train_files, test_files, dev_files):
        # labels must be the last input file
        datasets = {
            'train': LazyTextDataset(train_files) if train_files is not None else None,
            'test': LazyTextDataset(test_files) if test_files is not None else None,
            'valid': LazyTextDataset(dev_files) if dev_files is not None else None
        }
        dataloaders = {
            'train': DataLoader(datasets['train'], batch_size=128, shuffle=True, num_workers=4) if train_files is not None else None,
            'test': DataLoader(datasets['test'], batch_size=128, num_workers=4) if test_files is not None else None,
            'valid': DataLoader(datasets['valid'], batch_size=128, num_workers=4) if dev_files is not None else None
        }

        return datasets, dataloaders

    @staticmethod
    def prepare_lines(data, split_on=None, cast_to=None, min_size=None, pad_str=None, max_size=None, to_numpy=False,
                      list_internal=False):
        """ Converts the string input (line) to an applicable format. """
        out = []
        for line in data:
            line = line.strip()
            if split_on:
                line = line.split(split_on)
                line = list(filter(None, line))
            else:
                line = [line]

            if cast_to is not None:
                line = [cast_to(l) for l in line]

            if min_size is not None and len(line) < min_size:
                # pad line up to a number of tokens
                line += (min_size - len(line)) * ['@pad@']
            elif max_size and len(line) > max_size:
                line = line[:max_size]

            if list_internal:
                line = [[item] for item in line]

            if to_numpy:
                line = np.array(line)

            out.append(line)

        if to_numpy:
            out = np.array(out)

        return out

    def prepare_w2v(self, data):
        idxs = []
        for seq in data:
            tok_idxs = []
            for word in seq:
                # For every word, get its index in the w2v model.
                # If it doesn't exist, use @unk@ (available in the model).
                try:
                    tok_idxs.append(self.w2v_vocab[word].index)
                except KeyError:
                    tok_idxs.append(self.w2v_vocab['@unk@'].index)
            idxs.append(tok_idxs)
        idxs = torch.tensor(idxs, dtype=torch.long)

        return idxs

    def train(self, epochs=10):
        valid_loss_min = np.Inf
        train_losses, valid_losses = [], []
        for epoch in range(1, epochs + 1):
            epoch_start = time.time()

            train_loss, train_results = self._train_valid('train')
            valid_loss, valid_results = self._train_valid('valid')

            # Calculate Pearson correlation between prediction and target
            try:
                train_pearson = pearsonr(train_results['predictions'], train_results['targets'])
            except FloatingPointError:
                train_pearson = "Could not calculate Pearsonr"

            try:
                valid_pearson = pearsonr(valid_results['predictions'], valid_results['targets'])
            except FloatingPointError:
                valid_pearson = "Could not calculate Pearsonr"

            # calculate average losses
            train_loss = np.mean(train_loss)
            valid_loss = np.mean(valid_loss)

            train_losses.append(train_loss)
            valid_losses.append(valid_loss)

            # print training/validation statistics
            print(f'----------\n'
                  f'Epoch {epoch} - completed in {(time.time() - epoch_start):.0f} seconds\n'
                  f'Training Loss: {train_loss:.6f}\t Pearson: {train_pearson}\n'
                  f'Validation loss: {valid_loss:.6f}\t Pearson: {valid_pearson}')

            # validation loss has decreased
            if valid_loss <= valid_loss_min and train_loss > valid_loss:
                print(f'!! Validation loss decreased ({valid_loss_min:.6f} --> {valid_loss:.6f}).  Saving model ...')
                valid_loss_min = valid_loss

            if train_loss <= valid_loss:
                print('!! Training loss is lte validation loss. Might be overfitting!')

            # Optimise with scheduler
            if self.scheduler is not None:
                self.scheduler.step(valid_loss)

        print('Done training...')

    def _train_valid(self, do):
        """ Do training or validating. """
        if do not in ('train', 'valid'):
            raise ValueError("Use 'train' or 'valid' for 'do'.")

        results = {'predictions': np.array([]), 'targets': np.array([])}
        losses = np.array([])

        self.model = self.model.to(self.device)
        if do == 'train':
            self.model.train()
            torch.set_grad_enabled(True)
        else:
            self.model.eval()
            torch.set_grad_enabled(False)

        for batch_idx, data in enumerate(self.dataloaders[do], 1):
            # 1. Data prep
            sentence = data[0]
            target = data[-1]
            curr_batch_size = target.size(0)

            # Returns list of tokens, possibly padded @pad@
            sentence = self.prepare_lines(sentence, split_on=' ', min_size=20, max_size=20)
            # Converts tokens into w2v IDs as a Tensor
            sent_w2v_idxs = self.prepare_w2v(sentence)
            # Converts output to Tensor of floats
            target = torch.Tensor(self.prepare_lines(target, cast_to=float))

            # Move input to device
            sent_w2v_idxs, target = sent_w2v_idxs.to(self.device), target.to(self.device)

            # 2. Predictions
            pred = self.model(curr_batch_size, sentence_input=sent_w2v_idxs)
            loss = self.criterion(pred, target)

            # 3. Optimise during training
            if do == 'train':
                self.optimizer.zero_grad()
                loss.backward()
                self.optimizer.step()

            # 4. Save results
            pred = pred.detach().cpu().numpy()
            target = target.cpu().numpy()

            results['predictions'] = np.append(results['predictions'], pred, axis=None)
            results['targets'] = np.append(results['targets'], target, axis=None)
            losses = np.append(losses, float(loss))

        torch.set_grad_enabled(True)

        return losses, results


if __name__ == '__main__':
    HIDDEN_DIM = 200

    # Load embeddings from pretrained gensim model
    embed_p = Path('path-to.w2v_model').resolve()
    w2v_model = gensim.models.KeyedVectors.load_word2vec_format(str(embed_p))
    # add a padding token with only zeros
    w2v_model.add(['@pad@'], [np.zeros(w2v_model.vectors.shape[1])])
    embed_weights = torch.FloatTensor(w2v_model.vectors)


    # Text files are used as input. Every line is one datapoint.
    # *.tok.low.*: tokenized (space-separated) sentences
    # *.cross: one floating point number per line, which we are trying to predict
    regr = RegressionRNN(train_files=(r'train.tok.low.en',
                                      r'train.cross'),
                         dev_files=(r'dev.tok.low.en',
                                    r'dev.cross'),
                         test_files=(r'test.tok.low.en',
                                     r'test.cross'))
    regr.w2v_vocab = w2v_model.vocab
    regr.model = RegressorNet(HIDDEN_DIM, embed_weights, drop_prob=0.2)
    regr.criterion = nn.MSELoss()
    regr.optimizer = optim.Adam(list(regr.model.parameters())[0:], lr=0.001)
    regr.scheduler = optim.lr_scheduler.ReduceLROnPlateau(regr.optimizer, 'min', factor=0.1, patience=5, verbose=True)

    regr.train(epochs=100)

Для LazyTextDataset вы можете обратиться к классу ниже.

from torch.utils.data import Dataset

import linecache


class LazyTextDataset(Dataset):
    def __init__(self, paths):
        # labels are in the last path
        self.paths, self.labels_path = paths[:-1], paths[-1]

        with open(self.labels_path, encoding='utf-8') as fhin:
            lines = 0
            for line in fhin:
                if line.strip() != '':
                    lines += 1

            self.num_entries = lines

    def __getitem__(self, idx):
        data = [linecache.getline(p, idx + 1) for p in self.paths]
        label = linecache.getline(self.labels_path, idx + 1)

        return (*data, label)

    def __len__(self):
        return self.num_entries

Как я уже писал ранее, я пытаюсь преобразовать Kerasмодель для PyTorch.Оригинальный код Keras не использует слой встраивания и использует предварительно созданные векторы word2vec на предложение в качестве входных данных.В приведенной ниже модели нет слоя для встраивания.Сводка Keras выглядит следующим образом (у меня нет доступа к настройке базовой модели).


Layer (type)                     Output Shape          Param #     Connected to
====================================================================================================
bidirectional_1 (Bidirectional)  (200, 400)            417600
____________________________________________________________________________________________________
dropout_1 (Dropout)              (200, 800)            0           merge_1[0][0]
____________________________________________________________________________________________________
dense_1 (Dense)                  (200, 1)              801         dropout_1[0][0]
====================================================================================================

Проблема в том, что при одинаковом вводе модель Keras работает и получает +0,5 корреляцию Пирсона между предсказанными и фактическими метками.Приведенная выше модель PyTorch, похоже, не работает вообще.Чтобы дать вам представление, вот потери (среднеквадратическая ошибка) и Пирсона (коэффициент корреляции, p-значение) после первой эпохи:

Epoch 1 - completed in 11 seconds
Training Loss: 1.684495  Pearson: (-0.0006077809280690612, 0.8173368901481127)
Validation loss: 1.708228    Pearson: (0.017794288315261794, 0.4264098054188664)

И после 100-й эпохи:

Epoch 100 - completed in 11 seconds
Training Loss: 1.660194  Pearson: (0.0020315421756790806, 0.4400929436716754)
Validation loss: 1.704910    Pearson: (-0.017288118524826892, 0.4396865964324158)

Потери показаны ниже (если вы посмотрите на ось Y, вы увидите, что улучшения минимальны).

loss plot

AПоследний индикатор того, что что-то может быть не так, заключается в том, что для моих 140 тыс. строк ввода каждая эпоха на моем GTX 1080TI занимает всего 10 секунд.Я чувствую, что это не так много, и я думаю, что оптимизация не работает / работает.Я не могу понять, почему, хотя.Возможно, выдача будет в моей поездной петле или в самой модели, но я не могу ее найти.

Опять же, что-то идет не так, потому что: - модель Keras работает хорошо;- скорость обучения «слишком высока» для 140 тыс. предложений - почти нет улучшений после тренировки.

Чего мне не хватает?Эта проблема, скорее всего, присутствует в цикле обучения или в структуре сети.

1 Ответ

0 голосов
/ 27 февраля 2019

TL; DR : используйте permute вместо view при смене осей, см. В конце ответа, чтобы получить представление о разнице.

О RegressorNet (нейронная сетьмодель)

  1. Нет необходимости замораживать слой встраивания, если вы используете from_pretrained.Как гласит документация 1013 *, не не использует обновления градиента.

  2. Эта часть:

    self.w2v_rnode = nn.GRU(embeddings.size(1), hidden_dim, bidirectional=True, dropout=drop_prob)
    

    и особенно dropout без оговорок num_layers совершенно бессмысленно (так как нельзя исключить выпадение в мелкой однослойной сети).

  3. ОШИБКА И ОСНОВНАЯ ПРОБЛЕМА : в вашем forward функция, которую вы используете view вместо permute, здесь:

    w2v_out, _ = self.w2v_rnode(embeds.view(-1, batch_size, embeds.size(2)))
    

    См. этот ответ и соответствующую документацию для каждой из этих функций и попробуйте использовать эту строку вместо:

    w2v_out, _ = self.w2v_rnode(embeds.permute(1, 0, 2))
    

    Вы можете использовать аргумент batch_first=True при создании w2v_rnode, вам не придется переставлять индексы таким образом.

  4. Проверьте документацию torch.nn.GRU , вы после последнего шага последовательности , а не после всех ваших последовательностей, поэтому вы должны быть после:

    _, last_hidden = self.w2v_rnode(embeds.permute(1, 0, 2))
    

    но я думаю, что эта часть в порядке в противном случае.

Подготовка данных

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

Прежде всего, кажется, что вы выполняете заполнение вручную. Пожалуйста, не делайте так , используйте torch.nn.pad_sequence для работы с пакетами!

По сути, сначала вы кодируете каждое слово в каждом предложении какиндекс, указывающий на встраивание (как вы, похоже, делаете в prepare_w2v), после этого вы используете torch.nn.pad_sequence и torch.nn.pack_padded_sequence или torch.nn.pack_sequence, если строки уже отсортированы по длине.

Правильная пакетная обработка

Эта часть очень важна , и, похоже, вы этого вообще не делаете (и, вероятно, это вторая ошибка в вашей реализации).

Ячейки PyTorch RNN принимают входные данные не как дополненные тензоры , а как torch.nn.PackedSequence объекты.Это эффективный объект для хранения индексов, которые определяют unpadded длину каждой последовательности.

См. Дополнительную информацию по теме здесь , здесь и вмножество других сообщений в блогах в Интернете.

Первая последовательность в пакете должна быть самой длинной , а все остальные должны быть представлены в убывающей длине.Далее следует:

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

Либо в порядке, но ваш звонок кажется вам более интуитивным,Мне нравится более или менее следующее, надеюсь, это поможет:

  1. Создание уникальных индексов для каждого слова и сопоставление каждого предложения соответствующим образом (вы уже сделали это).
  2. Создайте обычный torch.utils.data.Dataset объект, возвращающий одно предложение для каждого geitem , где он возвращается в виде кортежа, состоящего из объектов (torch.Tensor) и меток (одно значение), кажется, что вы делаете это какхорошо.
  3. Создайте пользовательский collate_fn для использования с torch.utils.data.DataLoader , который отвечает за сортировку и заполнение каждого пакета в этом сценарии (+ он возвращает длины каждого предложениядля передачи в нейронную сеть).
  4. Использование отсортированных и дополненных объектов и их длины Я использую torch.nn.pack_sequence внутри метода нейронной сети forward ( сделайте это после встраивания! ), чтобы протолкнуть его через слой RNN.
  5. В зависимости от варианта использования я распаковываю их, используя torch.nn.pad_packed_sequence .В вашем случае вы заботитесь только о последнем скрытом состоянии, поэтому вам не нужно делать это .Если бы вы использовали все скрытые выходы (как, например, в случае сетей внимания), вы бы добавили эту часть.

Когда дело доходит до третьего пункта, вот пример реализациииз collate_fn, вы должны понять:

import torch


def length_sort(features):
    # Get length of each sentence in batch
    sentences_lengths = torch.tensor(list(map(len, features)))
    # Get indices which sort the sentences based on descending length
    _, sorter = sentences_lengths.sort(descending=True)
    # Pad batch as you have the lengths and sorter saved already
    padded_features = torch.nn.utils.rnn.pad_sequence(features, batch_first=True)
    return padded_features, sentences_lengths, sorter


def pad_collate_fn(batch):
    # DataLoader return batch like that unluckily, check it on your own
    features, labels = (
        [element[0] for element in batch],
        [element[1] for element in batch],
    )
    padded_features, sentences_lengths, sorter = length_sort(features)
    # Sort by length features and labels accordingly
    sorted_padded_features, sorted_labels = (
        padded_features[sorter],
        torch.tensor(labels)[sorter],
    )
    return sorted_padded_features, sorted_labels, sentences_lengths

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

Другие возможные проблемы и советы

  • Учебный цикл : отличное место для множества мелких ошибок, вы можете захотетьминимизируйте их, используя PyTorch Ignite .Мне невероятно тяжело проходить цикл обучения типа Tensorflow-like-Estimator-like-API (например, self.model = self.w2v_vocab = self.criterion = self.optimizer = self.scheduler = None this).Пожалуйста, не делайте так, разделите каждую задачу (создание данных, загрузку данных, подготовку данных, настройку модели, цикл обучения, ведение журнала) в отдельный модуль.В общем, есть причина, по которой PyTorch / Keras более читабелен и сохраняет здравый смысл, чем Tensorflow.

  • Сделайте первый ряд встраивания равным вектору, связывающему нули : По умолчанию torch.nn.functional.embedding ожидает, что первая строка будет использоваться для заполнения.Следовательно, вы должны начать свое уникальное индексирование для каждого слова с 1 или , указав аргумент padding_idx для другого значения (хотя я очень не рекомендую этот подход, в лучшем случае сбивающий с толку).

Я надеюсь, что этот ответ поможет вам, по крайней мере, немного, если что-то неясно, оставьте комментарий ниже, и я постараюсь объяснить это с другой точки зрения / более подробно.

Некоторые заключительные комментарии

Этот код не воспроизводится , а также не является конкретным вопросом.У нас нет данных, которые вы используете, мы также не получили ваши векторы слов, случайное начальное число не зафиксировано и т. Д.

PS.И последнее: проверьте свою производительность на действительно небольшом подмножестве ваших данных (скажем, 96 примеров), если они не сходятся, весьма вероятно, что в вашем коде действительно есть ошибка.

По поводу времени: они, вероятно, отключены (из-за отсутствия сортировки и заполнения, я полагаю), обычно времена Keras и PyTorch довольно схожи (если я понял эту часть вашего вопроса как предполагалось) для правильных и эффективных реализаций.

Перестановка против представления против изменения формы объяснения

Этот простой пример показывает различия между permute() и view().Первая меняет оси, в то время как вторая не меняет структуру памяти, просто разбивает массив на нужную форму (если это возможно).

import torch

a = torch.tensor([[1, 2], [3, 4], [5, 6]])

print(a)
print(a.permute(1, 0))
print(a.view(2, 3))

И вывод будет:

tensor([[1, 2],
        [3, 4],
        [5, 6]])
tensor([[1, 3, 5],
        [2, 4, 6]])
tensor([[1, 2, 3],
        [4, 5, 6]])

reshape почти как view, был добавлен для тех, кто пришел с numpy, так что это проще и естественнее для них, но имеет одно важное отличие:

  • view никогда не копирует данные и работает только с непрерывной памятью (поэтому после перестановки, подобной приведенной выше, ваши данные могут не быть смежными, поэтому доступ к ним может быть медленнее)
  • reshape можетпри необходимости скопируйте данные , чтобы они работали и для несмежных массивов.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...