序列到序列学习(seq2seq)


ValueError Traceback (most recent call last)
Cell In[14], line 11
8 decoder = Seq2SeqDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers,
9 dropout)
10 net = d2l.EncoderDecoder(encoder, decoder)
—> 11 train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)

Cell In[13], line 28, in train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device)
25 bos = torch.tensor([tgt_vocab[‘’]] * Y.shape[0],
26 device=device).reshape(-1, 1)
27 dec_input = torch.cat([bos, Y[:, :-1]], 1) # 强制教学
—> 28 Y_hat, _ = net(X, dec_input, X_valid_len)
29 l = loss(Y_hat, Y, Y_valid_len)
30 l.sum().backward() # 损失函数的标量进行“反向传播”

ValueError: too many values to unpack (expected 2)

2024.3.13
跑transformer时出现了这个错误,回来看9.7这个函数时也不能运行。有人遇到吗?

我觉得上面说的预测和训练的问题其实并没有,之所以训练时仅输入了一次X,而预测时要for num_steps,进行了时间步次输入,是因为其内部不同。
回到Decoder的代码中,有self.rnn(X,state),这里的X的形状是batch_size,num_steps,输入的state是Encoder获得的编码状态。同时,我们在之前学过GRU的内部代码,在GRU的内部其实对X的转置进行了for操作,也就是在GRU的内部进行了num_steps个时间步的推理,同时更新了隐状态state。也就是说在Decoder的GRU的内部,仅仅只有第一个时间步的状态输入来自Encoder的编码状态,后面是在更新的。
而预测时仅输入了一个单词,并不是一个时间序列,所以在Decoder的内部并没有时间步次的推理,只有一次,才需要在预测时进行时间步数次的推理和状态更新。
总结一下:就是在训练时Decoder的输入是包含时间步维度的X(会在内部进行时间步次的迭代),而预测时Decoder的输入只有一个单词,并不是num_steps个一起输入,这因为只有训练时我们才知道翻译的目标法语序列,而预测时我们只知道最开始的状态是bos,所以需要进行num_steps次推理,也就是把内部本来自动进行的时间步次的推理放到了外面一次一次地推理。
所以应该是不存在上面所说的训练和预测不一致的情况。
但还是存在与原文不一的情况,因为原文中的图是把Encoder的state作为编码信息输入到Decoder的每一时间步,但代码实现时由于GRU内部会自动进行状态更新,使得本应不变的state输入变成了随时间步变化。
如果有不同的理解请回复我,其中我不太确定的就是nn.GRU输入X和state后是否会固定state,因为之前学的GRU实现那一章内部是会变的,而pytorch的GRU我不是很确定。

是的,我也修改了,效果也从原来的:
go . => va !, bleu 1.000
i lost . => j’ai compris ., bleu 0.000
he’s calm . => il court ., bleu 0.000
i’m home . => je suis chez gagné ?, bleu 0.651

变为了:
go . => va !, bleu 1.000
i lost . => j’ai perdu ., bleu 1.000
he’s calm . => il est riche ., bleu 0.658
i’m home . => je suis chez moi ., bleu 1.000

我实现的做法就是在init中加入一个enc_state来记住。
context直接通过这个enc_state来构造

1 Like

但是这样你的解码器的RNN状态就丢失了,肯定会变差了

你好!我也发现了,原因是d2l的EncoderDecoder(参考上一章框架的代码)改了(英文版里只return1个),我的解决方案是把上一个框架代码的EncoderDecoder导入到这一章节代码里,以下代码可用于导入别的ipynb(在CSDN上找的):

在这一章节开头导入
import ipynb_importer
import 编码器解码器框架代码名

新建一个ipynb_importer.py
import io, os,sys,types
from IPython import get_ipython
from nbformat import read
from IPython.core.interactiveshell import InteractiveShell

class NotebookFinder(object):
“”“Module finder that locates Jupyter Notebooks”“”
def init(self):
self.loaders = {}

def find_module(self, fullname, path=None):
    nb_path = find_notebook(fullname, path)
    if not nb_path:
        return

    key = path
    if path:
        # lists aren't hashable
        key = os.path.sep.join(path)

    if key not in self.loaders:
        self.loaders[key] = NotebookLoader(path)
    return self.loaders[key]

def find_notebook(fullname, path=None):
“”"find a notebook, given its fully qualified name and an optional path

This turns "foo.bar" into "foo/bar.ipynb"
and tries turning "Foo_Bar" into "Foo Bar" if Foo_Bar
does not exist.
"""
name = fullname.rsplit('.', 1)[-1]
if not path:
    path = ['']
for d in path:
    nb_path = os.path.join(d, name + ".ipynb")
    if os.path.isfile(nb_path):
        return nb_path
    # let import Notebook_Name find "Notebook Name.ipynb"
    nb_path = nb_path.replace("_", " ")
    if os.path.isfile(nb_path):
        return nb_path

class NotebookLoader(object):
“”“Module Loader for Jupyter Notebooks”“”
def init(self, path=None):
self.shell = InteractiveShell.instance()
self.path = path

def load_module(self, fullname):
    """import a notebook as a module"""
    path = find_notebook(fullname, self.path)

    print ("importing Jupyter notebook from %s" % path)

    # load the notebook object
    with io.open(path, 'r', encoding='utf-8') as f:
        nb = read(f, 4)


    # create the module and add it to sys.modules
    # if name in sys.modules:
    #    return sys.modules[name]
    mod = types.ModuleType(fullname)
    mod.__file__ = path
    mod.__loader__ = self
    mod.__dict__['get_ipython'] = get_ipython
    sys.modules[fullname] = mod

    # extra work to ensure that magics that would affect the user_ns
    # actually affect the notebook module's ns
    save_user_ns = self.shell.user_ns
    self.shell.user_ns = mod.__dict__

    try:
      for cell in nb.cells:
        if cell.cell_type == 'code':
            # transform the input to executable Python
            code = self.shell.input_transformer_manager.transform_cell(cell.source)
            # run the code in themodule
            exec(code, mod.__dict__)
    finally:
        self.shell.user_ns = save_user_ns
    return mod

sys.meta_path.append(NotebookFinder())

哥们太牛了,这个逻辑!预测中,state会随时间步更新,encode不会随时间步变化,所以一直是encoder的信息。而又因为encode放在[-1]的位置不影响训练。太强了 :+1: :+1: :+1:

train_seq2seq 函数中,在把 “<bos>” 与 Y[:,:-1] cat 到一起的时候,解释说是为了原始的输出序列不包括序列结束词元 “<eos>”,但是在 9.5.4 节中,是对序列先加了 “<eos>”,再 padding 到 num_steps,这样 Y[:,:-1] 就不是在剔除 “<eos>” 了

def build_array_nmt(lines, vocab, num_steps):
    """将机器翻译的文本序列转换成小批量"""
    lines = [vocab[l] for l in lines]
    lines = [l + [vocab['<eos>']] for l in lines]
    array = torch.tensor([truncate_pad(
        l, num_steps, vocab['<pad>']) for l in lines])
    valid_len = (array != vocab['<pad>']).type(torch.int32).sum(1)
    return array, valid_len

def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):
    """训练序列到序列模型"""
    ... ...
    for epoch in range(num_epochs):
        ... ...
        for batch in data_iter:
            ... ...
            dec_input = torch.cat([bos, Y[:, :-1]], 1)  # 强制教学

Snipaste_2024-04-22_17-35-13
请问为什么对第一个句子进行翻译的时侯,会多出一个空格呢,换成别的词也总有这个问题,求大佬解答~

关于大伙说的那个“预测时context在不断变化”的问题,我也试着简单修改了下解码器和预测的函数,然后发现bleu提升了。

说实话,没什么卵用,完全用不上这个参数,只能说当假设step是10时,后面的默认也是一种单词了,然后以后预测都地是单词+填充=10,以满足符合训练情况,你可以理解为他将一句话强行扩充到十个单词,然后去训练他的翻译,训练的结果也将是对十个单词并用填充的输入才能得到结果。

除不除其实都可以,lr调整一下而已,想象一下曾经如何修正每个参数的公式,lr与1/n他们是毗邻相乘的,因此可以lr1/n也可以 lr(其中蕴含了1/n)。 Adam优化器可能是内部自动根据批量里样本数目调整了,我换成SGD,然后分别用sum与mean,更改lr以令得 lr批量里样本数目=0.005* 64,图片绘制的曲线将非常非常一致

关于Q1,我也觉得应当除以mark位的长度,对课本上代码做了调整

class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
    """带遮蔽的softmax交叉熵损失函数"""

    # pred的形状:(batch_size,num_steps,vocab_size)
    # label的形状:(batch_size,num_steps)
    # valid_len的形状:(batch_size,)
    def forward(self, pred, label, valid_len):
        weights = torch.ones_like(label)
        weights = sequence_mask(weights, valid_len)
        self.reduction = "none"
        unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(
            pred.permute(0, 2, 1), label
        )
        # 原书代码
        # weighted_loss = (unweighted_loss * weights).mean(dim=1)
        # 这里对原始代码进行修改, 改为计算除以有效位
        weighted_weights = (unweighted_loss * weights).sum(dim=1)
        weighted_loss = torch.where(
            valid_len != 0, weighted_weights / valid_len, torch.tensor(0.0, device=pred.device)
        )
        return weighted_loss

9.7.3节里定义的MaskedSoftmaxCELoss函数内部,没有看到对pred进行softmax操作。。是因为nn.CrossEntropyLoss函数内部自己做了softmax

#@save
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
    """带遮蔽的softmax交叉熵损失函数"""
    # pred的形状:(batch_size,num_steps,vocab_size)
    # label的形状:(batch_size,num_steps)
    # valid_len的形状:(batch_size,)
    def forward(self, pred, label, valid_len):
        weights = torch.ones_like(label)
        weights = sequence_mask(weights, valid_len)
        self.reduction='none'
        unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(
            pred.permute(0, 2, 1), label)
        weighted_loss = (unweighted_loss * weights).mean(dim=1)
        return weighted_loss

数据处理环节,针对的是法文和英文,他们都是单词之间通过空格来split的,而中文之间是没有空格的,需要处理下。比如改成tokenize_nmt函数的target部分改成:target.append(" ".join(parts[1]).split(’ '))

请问这一节代码李沐老师视频里讲的是用GRU来实现的,请问换成nn.LSTM为什么不可以呢,还需要改别的什么地方吗,求大佬源码或指导

May I ask if the code in this section is implemented by Mr. Li Mu in the video, please change it to nn. Why can’t LSTM?,Do you need to change something else?,Ask for the big guy source code or guidance.

net.decoder.attention_weights
我为什么没在decoder里看到attention_weights参数的定义,但是却在后面用到了


这里所说的超参数不变是和我上一次测试(没有处理编码器输入的PAD词元)相比,具体地:

BATCH_SIZE = 128
SEQ_LENGTH = 20
EMBED_DIM = 256
HIDDEN_NUM = 256
NUM_LAYERS = 2
DROPOUT = 0.2
LEARNING_RATE = 0.0005
EPOCHS_NUM = 100

具体细节大家感兴趣,可以参见我的笔记:

欢迎批评指正