gpt4 book ai didi

python - word2vec的RNN模型(GRU)回归未学习

转载 作者:行者123 更新时间:2023-11-30 08:33:27 26 4
gpt4 key购买 nike

我将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的Pearson相关性。但是,上面的PyTorch模型似乎根本不起作用。为了让您有所了解,这是第一个时期之后的损耗(均方误差)和Pearson(相关系数,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

最后一个指示可能是错误的指标是,对于我的140K行输入,每个纪元在GTX 1080TI上仅花费10秒。我觉得他的用处不大,我想优化工作或正在运行。不过,我不知道为什么。要发布的内容可能在我的火车循环中或模型本身中,但是我找不到它。

再次,一定要出问题了,因为:
  -Keras模型表现良好;
  -140K句子的训练速度“太快”
  -训练后几乎没有改善。

我想念什么?该问题很可能出现在培训循环或网络结构中。

最佳答案

TL; DR:交换轴时,使用permute而不是view,请参见答案的结尾以直观了解差异。

关于RegressorNet(神经网络模型)


如果使用from_pretrained,则无需冻结嵌入层。如documentation所述,它不使用渐变更新。
这部分:

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


尤其是没有可提供的 dropoutnum_layers完全没有意义(因为浅层网络无法指定任何丢失)。
错误和主要问题:在 forward函数中,您使用的是 view而不是 permute,在这里:

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参数,而不必以这种方式置换索引。
检查 torch.nn.GRU的文档,您在序列的最后一步之后,而不是在所有序列之后,因此应该在以下位置:

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


但是我认为这部分还可以。


资料准备

没有冒犯,但是 prepare_lines很难理解,而且似乎也很难维护,更不用说发现最终的错误了(我想它就在这里)。

首先,似乎您正在手动填充。请不要那样做,使用 torch.nn.pad_sequence进行批处理!

本质上,首先将每个句子中的每个单词编码为指向嵌入的索引(就像您在 prepare_w2v中所做的那样),然后使用 torch.nn.pad_sequencetorch.nn.pack_padded_sequencetorch.nn.pack_sequence(如果行已按长度。

正确分批

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

PyTorch的RNN单元输入的输入不是填充张量,而是 torch.nn.PackedSequence对象。这是存储指定每个序列的未填充长度的索引的有效对象。

在网络上的其他博客文章中,请参见有关 herehere主题的更多信息。

批处理中的第一个序列必须最长,而所有其他序列必须以降序长度提供。以下是:


您每次必须按序列长度对批次进行排序,并以类似方式对目标进行排序,或者
对您的批次进行分类,通过网络推送它,然后再对它进行分类以匹配您的目标。


两者都很好,这对您来说似乎更直观,这是您的要求。
我想做的或多或少是以下几点,希望对您有所帮助:


为每个单词创建唯一索引并适当映射每个句子(您已经完成了)。
创建常规的 torch.utils.data.Dataset对象,为每个geitem返回单个句子,并以元组的形式返回该句子,该元组由要素( torch.Tensor)和标签(单个值)组成,好像您也正在这样做。
创建自定义 collate_fn以与 torch.utils.data.DataLoader一起使用, torch.nn.pad_packed_sequence负责在这种情况下对每个批次进行排序和填充(+它将返回要传递到神经网络中的每个句子的长度)。
使用排序和填充的要素及其长度,我在神经网络的 torch.nn.pack_sequence方法中使用了 forward(嵌入后执行!)将其推入RNN层。
根据用例,我使用 PyTorch Ignite将它们解包。在您的情况下,您只关心最后一个隐藏状态,因此不必这样做。如果您正在使用所有隐藏的输出(例如注意力网络的情况),则应添加此部分。


关于第三点,这是 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,您应该就可以了(也许需要进行一些小的调整,因此了解它背后的想法非常重要)。

其他可能的问题和提示


培训循环:很多小错误的好地方,您可能希望使用 torch.nn.functional.embedding最小化这些错误。我很难经历像Tensorflow一样的Estimator一样的API一样的训练循环(例如 self.model = self.w2v_vocab = self.criterion = self.optimizer = self.scheduler = None this)。请不要这样做,请将每个任务(数据创建,数据加载,数据准备,模型设置,训练循环,记录)分开到各自的模块中。总而言之,PyTorch / Keras比Tensorflow更具可读性和完整性,这是有原因的。
使嵌入的第一行等于vector containsg零:默认情况下, 期望第一行用于填充。因此,您应该为每个单词从1开始唯一索引,或者将参数 padding_idx指定为不同的值(尽管我极力反对这种方法,充其量是令人困惑的)。


我希望这个答案至少对您有所帮助,如果有不清楚的地方,请在下面发表评论,我将尝试从其他角度/更详细地解释它。

最后的评论

该代码不可复制,也不是问题的具体内容。我们没有您正在使用的数据,也没有您的单词向量,随机种子不固定等。

PS。最后一件事:在很小的数据子集(例如96个示例)上检查性能,如果不收敛,很可能您的代码中确实存在一个错误。

关于时间:它们可能不合时宜(由于我想是由于不进行排序和填充),通常,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几乎像 view,是为来自 numpy的人添加的,因此对他们来说更容易,更自然,但是它有一个重要的区别:


view永远不会复制数据,而只能在连续的内存上工作(因此,在排列之后,上面的数据可能不是连续的,因此访问它可能会比较慢)
reshape可以根据需要复制数据,因此它也适用于非连续数组。

关于python - word2vec的RNN模型(GRU)回归未学习,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54824768/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com