代码之家  ›  专栏  ›  技术社区  ›  Bram Vanroy

word2vec的RNN模型(GRU)回归不学习

  •  -1
  • Bram Vanroy  · 技术社区  · 6 年前

    下面我提供了我几乎所有的PyTorch代码,包括初始化代码,以便您可以自己试用。您需要自己提供的唯一东西是单词embeddings(我相信您可以在网上找到许多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]
    ====================================================================================================
    

    作品 得到了预测和实际标签之间的+0.5皮尔逊相关性。不过,上面的Pythorch模型似乎根本不起作用。为了让您了解一下,以下是第一个纪元后的损失(均方误差)和皮尔逊(相关系数,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)
    

    loss plot

    最后一个可能出错的指标是,对于我的140K行输入,在我的gtx1080ti上,每个epoch只需要10秒。我觉得他没什么大不了的,我猜这优化是不起作用的。不过,我不知道为什么。问题可能在我的列车环路或模型本身,但我找不到它。

    -Keras模型 -训练速度太快了,不适合140K个句子 -训练后几乎没有改善。

    我错过了什么?这个问题很可能出现在训练回路或网络结构中。

    0 回复  |  直到 6 年前
        1
  •  6
  •   Szymon Maszke    6 年前

    太长,读不下去了 :使用 permute view 当交换坐标轴时,请看答案的末尾,以获得关于差异的直觉。

    1. 如果您正在使用,则无需冻结嵌入层 from_pretrained . 作为 documentation 国家,it 没有 使用渐变更新。

    2. 本部分:

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

      尤其是 dropout 没有提供 num_layers 是完全没有意义的(因为浅层网络不能指定丢失)。

    3. 缺陷和主要问题 forward 您正在使用的函数 看法 置换

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

      参见 this answer 以及这些函数的相应文档,并尝试使用以下行:

      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 非常不可读 而且似乎也很难维护,更不用说发现最终的bug了(我想它就在这里)。

    请不要那样做 torch.nn.pad_sequence 批量工作!

    本质上,首先将每个句子中的每个单词编码为指向嵌入的索引(就像您在 prepare_w2v torch.nn.pad_sequence torch.nn.pack_padded_sequence torch.nn.pack_sequence 如果行已经按长度排序。

    适当配料

    这部分是 非常重要 而且似乎您根本没有这样做(这可能是您的实现中的第二个错误)。

    Pythorch的RNN细胞接受输入 不像填充张量 ,但是 torch.nn.PackedSequence 物体。这是一个高效的对象,它存储 未添加 每个序列的长度。

    查看有关此主题的更多信息 here , here 在许多其他的博客文章中。

    第一批序列 一定是最长的 ,其他均须按降序提供。以下是:

    1. 你每次都要按序列长度排序 把你的目标分类 以类似的方式 或者
    2. 对你的批次进行分类,在网络上推送,然后 不排序 然后和你的目标匹配。

    两者都可以,这是你的决定,什么似乎对你更直观。

    1. 为每个单词创建唯一的索引,并适当地映射每个句子(您已经完成了)。
    2. torch.utils.data.Dataset 返回每个句子的对象 盖特姆 torch.Tensor )而标签(单值),似乎你也在这么做。
    3. 创建自定义 collate_fn 用于 torch.utils.data.DataLoader ,它负责在这个场景中对每个批进行排序和填充(+它返回要传递到神经网络中的每个句子的长度)。
    4. 分类和填充特征 他们的长度 我在用 torch.nn.pack U序列 神经网络内部 向前地 方法( 嵌入后再做! )把它推过RNN层。
    5. 根据用例,我使用 torch.nn.pad_packed_sequence . 在您的例子中,您只关心最后一个隐藏状态,因此 你不必那么做 . 如果你使用了所有的隐藏输出(比如说,注意力网络),你应该添加这个部分。

    说到第三点,下面是 ,你应该知道:

    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
    

    把它们当作 整理 DataLoaders 你应该很好(也许会有一些小的调整,所以你必须理解背后的想法)。

    其他可能的问题和提示

    • 训练回路 :对于很多小错误,您可以使用 PyTorch Ignite . 我有难以置信的困难时间通过你的像张量流估计器一样的API训练循环(例如。 self.model = self.w2v_vocab = self.criterion = self.optimizer = self.scheduler = None 这个)。请不要这样做,将每个任务(数据创建、数据加载、数据准备、模型设置、培训循环、日志记录)分别划分到各自的模块中。总而言之,PyTorch/Keras比Tensorflow更具可读性和更安全性是有原因的。

    • 使嵌入的第一行等于包含零的向量 torch.nn.functional.embedding 期望第一行用于填充。因此,您应该从1开始为每个单词建立唯一的索引 指定参数 padding_idx 不同的价值(尽管我强烈反对这种方法,最多让人困惑)。

    我希望这个答案至少能帮你一点忙,如果有什么不清楚的地方,请在下面发表评论,我会从不同的角度/更详细地解释。

    一些最后的评论

    这个代码 不可复制

    最后一件事:检查你的表现 非常小的子集 对于你的数据(比如96个例子),如果它不收敛,很可能你的代码中确实有一个bug。

    关于时间:它们可能是关闭的(由于没有排序和填充,我想),通常Keras和PyTorch的时间是非常相似的(如果我理解您的问题的这一部分是有意的),以正确和高效的实现。

    置换vs视图vs重塑解释

    这个简单的例子展示了 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 就像 看法 ,为来自 numpy

    • 看法 从不复制数据 并且只在连续内存上工作(所以在像上面这样的排列之后,你的数据可能不是连续的,因此对它的访问可能会比较慢)
    • 重塑 如果需要,可以复制数据
    推荐文章