从零实现深度学习框架——Seq2Seq机器翻译实战

news2024/9/20 16:27:45

引言

本着“凡我不能创造的,我就不能理解”的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导。
💡系列文章完整目录: 👉点此👈
要深入理解深度学习,从零开始创建的经验非常重要,从自己可以理解的角度出发,尽量不适用外部框架的前提下,实现我们想要的模型。本系列文章的宗旨就是通过这样的过程,让大家切实掌握深度学习底层实现,而不是仅做一个调包侠。

本文基于RNN作为编码器/解码器的seq2seq架构实现德语-英语的机器翻译。

上篇文章基于英语-汉语数据集实现了机器翻译,但发现它的验证集损失不下降,纠结了很久还是决定用德语-英语的数据集来做。

seq2seq简介

202307011809

上图是seq2seq翻译德语“早上好”的例子。源语句guten morgen首先经过嵌入层(黄色),然后输入到编码器(浅绿色)。为了表示句子的开头和结束,加入开始和结束标记 <bos><eos>

编码器这里是由GRU实现的,用最后一个token即<eos>输出的隐藏状态表示整个源语句的编码 z z z,这里 h 4 = z h_4=z h4=z

解码器也是一个GRU网络,它的初始隐藏状态为编码器生成的 z z z。解码器第一个输入的token为<bos>,表示开始生成目标语句。解码器基于输入的token和前一个隐藏状态生成当前隐藏状态 s 1 s_1 s1,这里假设 s 1 s_1 s1经过一个线性映射后投影到输出词表空间,经过softmax得到输出词表所有单词分布。这里可以选择概率最大的单词作为解码器当前时刻的预测。

数据集处理

从数据集处理开始,首先引入所需要的包:

import os
import random
from collections import defaultdict
from typing import Tuple

from tqdm import tqdm

import metagrad.module as nn
from metagrad import Tensor, cuda
from metagrad import functions as F
from metagrad import init
from metagrad.dataloader import DataLoader
from metagrad.dataset import TensorDataset
from metagrad.loss import CrossEntropyLoss
from metagrad.optim import Adam
from metagrad.tensor import no_grad
from metagrad.utils import grad_clipping

image-20230701182920717

这里我们用multi30k仓库中已经分好词的版本,但它的源语句和目标语句是分开的,因此,先写一个函数生成源语句和目标语句对。

"""
数据集来自已经分好词的版本: https://github.com/multi30k/dataset/tree/master/data/task1/tok
"""

base_path = "../data/de-en"


def build_nmt_pair(src_path, tgt_path, reverse=False):
    """
    构建机器翻译source-target对
    :param src_path: 源语言目录
    :param tgt_path: 目标语言目录
    :param reverse:  是否逆序源语言
    :return: 分好词的source和target
    """
    source, target = [], []
    with open(src_path, 'r', encoding='utf-8') as f:
        source_lines = f.readlines()
    with open(tgt_path, 'r', encoding='utf-8') as f:
        target_lines = f.readlines()

    for src, tgt in zip(source_lines, target_lines):
        src_tokens = src.split()
        if reverse:
            src_tokens.reverse()
        tgt_tokens = tgt.split()

        source.append(src_tokens)
        target.append(tgt_tokens)

    return source, target
source, target = build_nmt_pair(os.path.join(base_path, "val.de"), os.path.join(base_path, "val.en"))
print(source[:2])
print(target[:2])

打印前2条记录:

[['eine', 'gruppe', 'von', 'männern', 'lädt', 'baumwolle', 'auf', 'einen', 'lastwagen'], ['ein', 'mann', 'schläft', 'in', 'einem', 'grünen', 'raum', 'auf', 'einem', 'sofa', '.']]
[['a', 'group', 'of', 'men', 'are', 'loading', 'cotton', 'onto', 'a', 'truck'], ['a', 'man', 'sleeping', 'in', 'a', 'green', 'room', 'on', 'a', 'couch', '.']]

接下来是构建词典:

class Vocabulary:
    BOS_TOKEN = "<bos>"  # 句子开始标记
    EOS_TOKEN = "<eos>"  # 句子结束标记
    PAD_TOKEN = "<pad>"  # 填充标记
    UNK_TOKEN = "<unk>"  # 未知词标记

    def __init__(self, tokens=None):
        self._idx_to_token = list()
        self._token_to_idx = dict()

        # 如果传入了去重单词列表
        if tokens is not None:
            if self.UNK_TOKEN not in tokens:
                tokens = tokens + [self.UNK_TOKEN]
            # 构建id2word和word2id
            for token in tokens:
                self._idx_to_token.append(token)
                self._token_to_idx[token] = len(self._idx_to_token) - 1

            self.unk = self._token_to_idx[self.UNK_TOKEN]

    @classmethod
    def build(cls, text, min_freq=2, reserved_tokens=None):
        '''
        构建词表
        :param text: 处理好的(分词、去掉特殊符号等)text
        :param min_freq: 最小单词频率
        :param reserved_tokens: 预先保留的标记
        :return:
        '''
        token_freqs = defaultdict(int)
        for sentence in text:
            for token in sentence:
                token_freqs[token] += 1

        unique_tokens = (reserved_tokens if reserved_tokens else []) + [cls.UNK_TOKEN]
        unique_tokens += [token for token, freq in token_freqs.items() \
                          if freq >= min_freq and token != cls.UNK_TOKEN]
        return cls(unique_tokens)

    def __len__(self):
        return len(self._idx_to_token)

    def __getitem__(self, tokens):
        '''得到tokens对应的id'''
        if not isinstance(tokens, (list, tuple)):
            return self._token_to_idx.get(tokens, self.unk)
        return [self.__getitem__(token) for token in tokens]

    @property
    def id2token(self):
        '''返回idx_to_token列表'''
        return self._idx_to_token

    def token(self, indices):
        '''根据索引获取token'''
        if not isinstance(indices, (list, tuple)):
            return self._idx_to_token[indices]

        return [self._idx_to_token[index] for index in indices]

    def to_tokens(self, indices):
        return self.token(indices)

    def save(self, path):
        with open(path, 'w') as f:
            f.write("\n".join(self.id2token))

    @classmethod
    def load(cls, path):
        with open(path, 'r') as f:
            tokens = f.read().split('\n')
        return cls(tokens)

这个和之前见到的差不多,做了一些小的修改。

min_freq = 2
source, target = build_nmt_pair(os.path.join(base_path, "train.de"), os.path.join(base_path, "train.en"))

reserved_tokens = [Vocabulary.PAD_TOKEN, Vocabulary.BOS_TOKEN, Vocabulary.EOS_TOKEN]
src_vocab = Vocabulary.build(source, min_freq=min_freq, reserved_tokens=reserved_tokens)
tgt_vocab = Vocabulary.build(target, min_freq=min_freq, reserved_tokens=reserved_tokens)
7859 5921

打印出了源词表和目标词表大小。

接下来实现填充函数:

def truncate_pad(line, max_len, padding_token):
    """截断或填充文本序列"""
    if len(line) > max_len:
        return line[:max_len]  # 截断
    return line + [padding_token] * (max_len - len(line))  # 填充
print(truncate_pad(src_vocab[source[0]], 20, src_vocab[Vocabulary.PAD_TOKEN]))

注意这里隐式调用了Vocabulary__getitem__方法将token转换为对应的index,同时在该方法中还会处理未知词。

[4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 0, 0, 0, 0, 0, 0, 0]

下面把转换后的index列表打包成批量数据,以及在前后分别增加<bos><eos> token:

def build_array_nmt(lines, vocab, max_len=None):
    """将机器翻译的文本序列转换成小批量"""
    if not max_len:
        max_len = max(len(x) for x in lines)

    # 先转换成token对应的索引列表
    lines = [vocab[l] for l in lines]
    # 增加BOS和EOS token的索引
    lines = [[vocab[Vocabulary.BOS_TOKEN]] + l + [vocab[Vocabulary.EOS_TOKEN]] for l in lines]
    # max_len 应该加2了:额外的BOS和EOS ,并转换为seq_len, batch_size的形式
    array = Tensor([truncate_pad(l, max_len + 2, vocab[Vocabulary.PAD_TOKEN]) for l in lines])

    return array
src_array = build_array_nmt(source, src_vocab, 20)
print(src_array[:5])

并且会转换为Tensor变量:

Tensor([[ 1  4  5  6  7  8  9 10 11 12 13 14 15 16  2  0  0  0  0  0  0  0]
 [ 1 17  7 18 19 20 21  3 16  2  0  0  0  0  0  0  0  0  0  0  0  0]
 [ 1 21 22 23 24 11 21 25 26 27 16  2  0  0  0  0  0  0  0  0  0  0]
 [ 1 21 28 11 29 30 31 32 33 34 35 36 37 21 38 16  2  0  0  0  0  0]
 [ 1  4  7 39 40 41 36 42 43 44 16  2  0  0  0  0  0  0  0  0  0  0]], requires_grad=False)

最后通过一个函数结合上面所有方法构建数据集和数据加载器:

def load_dataset_nmt(data_path=base_path, data_type="train", batch_size=32, min_freq=2, src_vocab=None, tgt_vocab=None,
                     shuffle=False):
    """
    加载机器翻译数据集
    :param data_path: 保存数据集的目录
    :param data_type: 数据集类型 train|test|val
    :param batch_size: 批大小
    :param min_freq: 最小单词次数
    :param src_vocab: 源词典
    :param tgt_vocab: 目标词典
    :param shuffle: 是否打乱
    :return:
    """

    source, target = build_nmt_pair(os.path.join(data_path, f"{data_type}.de"),
                                    os.path.join(data_path, f"{data_type}.en"),
                                    reverse=True)
    # 构建源和目标词表
    reserved_tokens = [Vocabulary.PAD_TOKEN, Vocabulary.BOS_TOKEN, Vocabulary.EOS_TOKEN]

    if src_vocab is None:
        src_vocab = Vocabulary.build(source, min_freq=min_freq, reserved_tokens=reserved_tokens)
    if tgt_vocab is None:
        tgt_vocab = Vocabulary.build(target, min_freq=min_freq, reserved_tokens=reserved_tokens)

    print(f'Source vocabulary size: {len(src_vocab)}, Target vocabulary size: {len(tgt_vocab)}')
    # 转换成批数据
    max_src_len = max([len(line) for line in source])
    max_tgt_len = max([len(line) for line in target])

    print(f"max_src_len: {max_src_len}, max_tgt_len:{max_tgt_len}")

    src_array = build_array_nmt(source, src_vocab, max_src_len)
    tgt_array = build_array_nmt(target, tgt_vocab, max_tgt_len)

    # 构建数据集
    dataset = TensorDataset(src_array, tgt_array)
    # 数据加载器
    data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=shuffle)
    # 返回加载器和两个词表
    return data_loader, src_vocab, tgt_vocab

train_dataset, src_vocab, tgt_vocab = load_dataset_nmt()
for X, Y in train_dataset:
    print('X:', X.shape)
    print('Y:', Y.shape)
    break
X: (32, 46)
Y: (32, 42)

默认加载训练集数据,可以看到这里返回的是batch_first形式。

数据集处理好了之后,下面就可以定义模型了。

编码器

image-20230701192708371

编码器如上图所示,这里可以看到它是由两层的,不过实际上还可以是双向的,没有画出来。每一层都会产生一个编码向量。

class Encoder(nn.Module):
    def __init__(self, vocab_size: int, embed_size: int, num_hiddens: int, num_layers: int, dropout: float,
                 bidirectional: bool = True) -> None:
        """
        基于GRU实现的编码器
        :param vocab_size: 源词表大小
        :param embed_size: 词嵌入大小
        :param num_hiddens: 隐藏层大小
        :param num_layers: GRU层数
        :param dropout:  dropout比率
        :param bidirectional: 是否为双向
        """

        super().__init__()

        # 嵌入层 获取输入序列中每个单词的嵌入向量 padding_idx不需要更新嵌入
        self.embedding = nn.Embedding(vocab_size, embed_size, padding_idx=0)
        # 基于双向GRU实现 注意,这里默认batch_first为False
        self.rnn = nn.GRU(input_size=embed_size, hidden_size=num_hiddens, num_layers=num_layers, dropout=dropout,
                          bidirectional=bidirectional)
        self.dropout = nn.Dropout(dropout)

    def forward(self, input_seq: Tensor) -> Tuple[Tensor, Tensor]:
        """
        编码器的前向算法
        :param input_seq:  形状 (seq_len, batch_size)
        :return:
        """

        # (seq_len, batch_size, embed_size)
        embedded = self.dropout(self.embedding(input_seq))
        # embedded = self.embedding(input_seq)
        # outputs (seq_len, batch_size, num_direction * num_hiddens)
        # hidden  (num_direction * num_layers, batch_size, num_hiddens)
        outputs, hidden = self.rnn(embedded)
        # 融合双向的hidden, 因为解码器一定是单向的
        if self.rnn.bidirectional:
            hidden = hidden[:self.rnn.num_layers, :, :] + hidden[self.rnn.num_layers:, :, :]
        # hidden  (num_layers, batch_size, num_hiddens)
        return outputs, hidden

代码有很详细的注释,self.rnn(embedded)返回的是outputshidden,前者是每个时间步顶层的隐藏状态,这里我们用不到。后者是每层的最终隐藏状态。由于是双向的,实际上它的形状为 (num_direction * num_layers, batch_size, num_hiddens)。为了和解码器的初始隐藏状态形状保持一致,这里需要融合双向上的信息。

为了防止过拟合,除了在初始化GRU层时指定了dropout,还在词嵌入层上加了一层dropout。

解码器

image-20230701195521111

解码器如上图所示,由于我们这里的编码器和解码器层数一致,所有编码器每层最终的隐藏状态刚好可以作为解码器每层的初始状态。

class Decoder(nn.Module):
    def __init__(self, vocab_size: int, embed_size: int, num_hiddens: int, num_layers: int, dropout: float) -> None:
        """
        基于GRU实现的解码器
        :param vocab_size: 目标词表大小
        :param embed_size:  词嵌入大小
        :param num_hiddens: 隐藏层大小
        :param num_layers:  层数
        :param dropout:  dropout比率
        """
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_size, padding_idx=0)
        self.rnn = nn.GRU(input_size=embed_size, hidden_size=num_hiddens, num_layers=num_layers, dropout=dropout)
        # 将隐状态转换为词典大小维度
        self.fc_out = nn.Linear(num_hiddens, vocab_size)
        self.vocab_size = vocab_size
        self.dropout = nn.Dropout(dropout)

    def forward(self, input_seq: Tensor, hidden: Tensor) -> Tuple[Tensor, Tensor]:
        """
        解码器的前向算法
        :param input_seq: 初始输入,这里为<bos> 形状 (batch_size, )
        :param hidden: 编码器生成的上下文向量 形状 (num_layers, batch_size, num_hiddens)
        :return:
        """
        # input = (1, batch_size)
        input_seq = input_seq.unsqueeze(0)
        # embedded = (1, batch_size, embed_size)
        embedded = self.dropout(self.embedding(input_seq))
        # output (1, batch_size, num_hiddens)
        # hidden  (num_layers, batch_size, num_hiddens)
        output, hidden = self.rnn(embedded, hidden)
        # prediction (batch_size, vocab_size)
        prediction = self.fc_out(output.squeeze(0))
        return prediction, hidden

其实这里的Decoder每次仅处理一个时间步,从其forward方法中的input_seq大小也可以看出。

也有人直接通过RNNCell来实现。

Seq2Seq

最终组合编码器和解码器就成了序列到序列模型。

image-20230701202519274

在seq2seq的实现中有一些细节,首先有一个叫做teacher force的东西,仅用于训练阶段。当使用teacher force时,不管解码器的输出是什么,下一步的输入是真实的输入。不使用时,采用解码器自己的输出作为下一步的输入。

teacher force和dropout类似,也可以基于同样的方式实现,目的也是防止过拟合。

解码器的第一个输入token是 <bos>,最后一个token是序列中 <eos>前一个token。我们不会把<eos>输入到解码器中,从上图也可以看出来。

在推理阶段,解码器会一直输出,直到生成了<eos>或达到最大长度。

class Seq2seq(nn.Module):
    def __init__(self, encoder: Encoder, decoder: Decoder) -> None:
        """
        初始化seq2seq模型
        :param encoder: 编码器
        :param decoder: 解码器
        """
        super().__init__()
        self.encoder = encoder
        self.decoder = decoder

    def forward(self, input_seq: Tensor, target_seq: Tensor, teacher_forcing_ratio: float = 0.0) -> Tensor:
        """
        seq2seq的前向算法
        :param input_seq:  输入序列 (seq_len, batch_size)
        :param target_seq: 目标序列  (seq_len, batch_size)
        :param teacher_forcing_ratio:  强制教学比率
        :return:
        """

        tgt_len, batch_size = target_seq.shape
        # 保存了所有时间步的输出
        outputs = []
        # 这里我们只关心编码器输出的hidden
        # hidden  (num_layers, batch_size, num_hiddens)
        _, hidden = self.encoder(input_seq)
        # decoder_input (batch_size) 取BOS token
        decoder_input = target_seq[0, :]  # BOS_TOKEN
        # 这里从1开始,确保tgt[t]是下一个token
        for t in range(1, tgt_len):
            # output (batch_size, target_vocab_size)
            # hidden (num_layers, batch_size, num_hiddens)
            output, hidden = self.decoder(decoder_input, hidden)
            # 保存到outputs
            outputs.append(output)
            # 随机判断是否强制教学
            teacher_force = random.random() < teacher_forcing_ratio
            # 如果teacher_force==True, 则用真实输入当成下一步的输入,否则用模型生成的
            # output.argmax(1) 在目标词表中选择得分最大的一个 (batch_size, 1)
            decoder_input = target_seq[t] if teacher_force else output.argmax(1)

        # 把outputs转换成一个Tensor 形状为: (tgt_len - 1, batch_size, target_vocab_size)
        return F.stack(outputs)

其实把所有变量的形状标出来就不难理解了,把形状注释出来不仅可读性更好,还可以防止犯错。

注意这里forward函数最终返回的形状是 (tgt_len - 1, batch_size, target_vocab_size)

注意循环从1开始而不是0,我们的target_seqoutputs看起来是这样:
target = [ <bos> , y 1 , y 2 , y 3 , <eos> ] outputs = [ y ^ 1 , y ^ 2 , y ^ 3 , <eos> ] \text{target} = [\text{<bos>}, y_1,y_2,y_3, \text{<eos>}] \\ \text{outputs} = [\hat y_1,\hat y_2,\hat y_3, \text{<eos>}] target=[<bos>,y1,y2,y3,<eos>]outputs=[y^1,y^2,y^3,<eos>]
这里 y ^ \hat y y^表示模型的预测token。

后面当我们计算损失时,需要切掉target_seq的第一个token,变成:
target = [ y 1 , y 2 , y 3 , <eos> ] outputs = [ y ^ 1 , y ^ 2 , y ^ 3 , <eos> ] \text{target} = [y_1,y_2,y_3, \text{<eos>}] \\ \text{outputs} = [\hat y_1,\hat y_2,\hat y_3, \text{<eos>}] target=[y1,y2,y3,<eos>]outputs=[y^1,y^2,y^3,<eos>]

训练seq2seq

至此我们的模型已经实现好,可以开始训练了。

一开始我们定义必要的参数:

# 参数定义
embed_size = 256
num_hiddens = 512
num_layers = 2
dropout = 0.5

batch_size = 64
max_len = 40

lr = 0.001
num_epochs = 10
min_freq = 2
clip = 1.0 # 梯度裁剪,防止梯度爆炸

tf_ratio = 0.5  # teacher force ratio

print_every = 1

device = cuda.get_device("cuda" if cuda.is_available() else "cpu")

然后加载数据集:

# 加载训练集
train_iter, src_vocab, tgt_vocab = load_dataset_nmt(data_path=base_path, data_type="train", batch_size=batch_size,
                                                    min_freq=min_freq, shuffle=True)

# 加载验证集
valid_iter, src_vocab, tgt_vocab = load_dataset_nmt(data_path=base_path, data_type="val", batch_size=batch_size,
                                                    min_freq=min_freq, src_vocab=src_vocab, tgt_vocab=tgt_vocab)

接着构建模型:

# 构建编码器
encoder = Encoder(len(src_vocab), embed_size, num_hiddens, num_layers, dropout)
# 构建解码器
decoder = Decoder(len(tgt_vocab), embed_size, num_hiddens, num_layers, dropout)

model = Seq2seq(encoder, decoder)
model.apply(init_weights)
model.to(device)

print(model)
Seq2seq(
  (encoder): Encoder(
    (embedding): Embedding(7859, 256, padding_idx=0)
    (rnn): GRU(input_size=256, hidden_size=512, num_layers=2, dropout=0.5, bidirectional=True)
    (dropout): Dropout(p=0.5)
  )
  (decoder): Decoder(
    (embedding): Embedding(5921, 256, padding_idx=0)
    (rnn): GRU(input_size=256, hidden_size=512, num_layers=2, dropout=0.5)
    (fc_out): Linear(in_features=512, out_features=5921, bias=True)
    (dropout): Dropout(p=0.5)
  )
)

上面的init_weights是初始化权重:

def init_weights(model):
    for name, param in model.named_parameters():
        init.uniform_(param, -0.08, 0.08)

然后定义优化器和损失函数,这里使用交叉熵损失函数即可,指定了忽略填充索引。

TGT_PAD_IDX = tgt_vocab[Vocabulary.PAD_TOKEN]

optimizer = Adam(model.parameters())
criterion = CrossEntropyLoss(ignore_index=TGT_PAD_IDX)

Adam优化器的实现后面也会出文章介绍。

下面是单epoch的训练代码:

def train_epoch(model, data_iter, optimizer, criterion, clip, device, tf_ratio):
    model.train()
    epoch_loss = 0

    for batch in data_iter:
        optimizer.zero_grad()
        inputs, targets = [x.to(device).T for x in batch] # 变成batch_first=False 
        outputs = model(inputs, targets, tf_ratio)
		# outputs (tgt_len - 1, batch_size, target_vocab_size)
        # view  ( (tgt_len-1) * batch_size, target_vocab_size)
        outputs = outputs.view(-1, outputs.shape[2])
        # targets 去掉所有的bos token
        # view ((tgt_len-1) * batch_size, ) 
        targets = targets[1:].view(-1)
		# 计算损失
        loss = criterion(outputs, targets)
        loss.backward()
        with no_grad():
            # 梯度裁剪
            grad_clipping(model, clip)
        optimizer.step()

        epoch_loss += loss.item()
	# 每个样本的平均损失
    return epoch_loss / len(data_iter)

同时实现在验证集上评估的函数:

def evaluate(model, data_iter, criterion, device):
    model.eval()
    epoch_loss = 0
    with no_grad():
        for batch in data_iter:
            inputs, targets = [x.to(device).T for x in batch]
            outputs = model(inputs, targets, 0)  # 评估时不用teacher forcing

            outputs = outputs.view(-1, outputs.shape[2])
            targets = targets[1:].view(-1)

            loss = criterion(outputs, targets)
            epoch_loss += loss.item()

    return epoch_loss / len(data_iter)

最终定义训练函数:

def train(model, num_epochs, train_iter, valid_iter, optimizer, criterion, clip, device, tf_ratio):
    best_valid_loss = float('inf')
    for epoch in tqdm(range(1, num_epochs + 1), desc="Training", leave=False):
        train_loss = train_epoch(model, train_iter, optimizer, criterion, clip, device, tf_ratio)
        valid_loss = evaluate(model, valid_iter, criterion, device)
        if valid_loss < best_valid_loss:
            best_valid_loss = valid_loss
            model.save()
        tqdm.write(
            f"epoch {epoch:3d} , train loss: {train_loss:.4f} , validate loss: {valid_loss:.4f}, best validate loss: {best_valid_loss:.4f}")

训练过程如下:

Source vocabulary size: 7859, Target vocabulary size: 5921
max_src_len: 44, max_tgt_len:40
Source vocabulary size: 7859, Target vocabulary size: 5921
max_src_len: 33, max_tgt_len:30
Seq2seq(
  (encoder): Encoder(
    (embedding): Embedding(7859, 256, padding_idx=0)
    (rnn): GRU(input_size=256, hidden_size=512, num_layers=2, dropout=0.5, bidirectional=True)
    (dropout): Dropout(p=0.5)
  )
  (decoder): Decoder(
    (embedding): Embedding(5921, 256, padding_idx=0)
    (rnn): GRU(input_size=256, hidden_size=512, num_layers=2, dropout=0.5)
    (fc_out): Linear(in_features=512, out_features=5921, bias=True)
    (dropout): Dropout(p=0.5)
  )
)
TGT_PAD_IDX is 0
Training:  10%|█         | 1/10 [06:42<1:00:18, 402.01s/it]Save module to model.pkl
epoch   1 , train loss: 0.0696 , validate loss: 0.0673, best validate loss: 0.0673
Training:  20%|██        | 2/10 [13:21<53:23, 400.49s/it]  Save module to model.pkl
epoch   2 , train loss: 0.0554 , validate loss: 0.0598, best validate loss: 0.0598
Save module to model.pkl
epoch   3 , train loss: 0.0485 , validate loss: 0.0578, best validate loss: 0.0578
Training:  40%|████      | 4/10 [27:08<40:54, 409.05s/it]Save module to model.pkl
epoch   4 , train loss: 0.0441 , validate loss: 0.0554, best validate loss: 0.0554
Training:  50%|█████     | 5/10 [33:53<33:57, 407.60s/it]Save module to model.pkl
epoch   5 , train loss: 0.0405 , validate loss: 0.0553, best validate loss: 0.0553
Save module to model.pkl
epoch   6 , train loss: 0.0377 , validate loss: 0.0550, best validate loss: 0.0550
Training:  70%|███████   | 7/10 [47:19<20:14, 405.00s/it]epoch   7 , train loss: 0.0354 , validate loss: 0.0555, best validate loss: 0.0550
epoch   8 , train loss: 0.0333 , validate loss: 0.0552, best validate loss: 0.0550
Training:  90%|█████████ | 9/10 [1:00:35<06:41, 401.47s/it]epoch   9 , train loss: 0.0315 , validate loss: 0.0555, best validate loss: 0.0550
epoch  10 , train loss: 0.0299 , validate loss: 0.0552, best validate loss: 0.0550

可以看到,整个过程哪怕在GPU上跑也是挺久的,由于RNN无法并行化,这在很大程度上影响了速度,除非也像Pytorch一样,将核心代码迁移到C++实现,后面有机会进行这样的尝试。

我们重复造轮子的目的不是替代Pytorch等深度学习框架,而是为了了解它的实现原理,让我们更好地使用Pytorch。

这里的train loss和validate loss看起来很小,因为求的是平均。好在验证损失确实也下降了。

后面几篇文章还是关于seq2seq的,我们会尝试优化它的效果,具体如何优化,请看下文分解。

完整代码

点此 → Github

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/709108.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【你哥电力电子】 THE BUCK-BOOST 升降压斩波电路2

BUCK-BOOST电路2 2023年1月30日 nige in Tongji University #elecEngeneer 上链 文章目录 BUCK-BOOST电路26. CCM非理想能量守恒平均分析6.1 CCM非理想大信号平均模型6.2 CCM等效大信号平均模型6.3 CCM的DC电路模型6.4 CCM的小信号线性电路模型6.5 CCM非理想小信号传递函数6.…

【SaaS】多租户系统设计

文章目录 多租户系统设计一、SaaS 的系统分级二、应用程序必须支持多租户三、数据隔离方案3.1、独立应用独立库3.2、同一个应用程序&#xff0c;每个租户一个库3.3、同一个应用程序&#xff0c;同一个数据库3.4、分片多租户 四、我们的模型选择4.1、开发实践4.2、元数据/配置驱…

vue路由传参+案例(使用mock模拟后端数据)

路由传参 跳转路由时&#xff0c;可以给路由对应的组件内传参 声明式导航 /path?参数名值 /path/值 —需要路由对象提前配置 path: ‘/path/:参数名’ 对应的页面组件接收传递过来的值 $route.query.参数名 $route.params.参数名 router/index.js import Vue from vue // 1. …

解析matlab的audioread()输入输出参数

目录 一、API简介 二、实验 1. matlab 2. C语言 一、API简介 链接如下&#xff1a; 读取音频文件 - MATLAB audioread- MathWorks 中国 也可以浏览最新的英文版API说明&#xff1a; 简单说明如下&#xff1a; 1. 读取wav格式的文件&#xff0c;会自动跳过44个字节的文件…

初识React/JSX/组件/state/受控组件

JSX 推荐使用小括号包裹jsx 使用函数创建组件 使用类创建组件 抽离组件 事件绑定 事件对象 有状态和无状态组件/state 抽离事件处理程序 表单元素 受控组件 多表单优化 非受控组件(了解即可)

vhost-net-原理-初始化流程-数据传输流程-vhost-net后端

文章目录 1.vhost net2.vhost-net的初始化流程vhost net设置vhost dev设置vhost vring设置 3.数据收发流程分析3.1 数据发送3.2 数据接收 4ioventfd和irqfd的通知机制4.1ioeventfdqemu侧kvm侧总体效果 4.2irqfdqemu侧kvm侧总体效果 参考&#xff1a; 1.vhost net 传统的virtio…

ChatGPT Plugins内幕、源码及案例实战(一)

ChatGPT Plugins内幕、源码及案例实战 6.1 ChatGPT Plugins的工作原理 本节主要跟大家谈ChatGPT的插件(Plugins),这个内容非常重要。现在很多企业级的开发,一般都会基于ChatGPT 插件进行一些服务的封装,相当于开发了一个代理(Agent),把一些服务或者API封装在里面,然后…

eclipse编辑器汉化;eclipse安装中文插件

eclipse IDE默认是英文环境&#xff0c;使用起来略微不便&#xff0c;汉化还是很有必要的&#xff1b;下面记录一下安装中文插件的过程: 文章目录 一、 选择安装包地址二、 在eclipse安装中文插件2.1 在线安装2.2 手动下载安装包2.3 导入到eclipse 三、汉化插件介绍 一、 选择安…

实例005 可以拉伸的菜单界面

实例说明 如果管理程序功能菜单非常多&#xff0c;而用户只使用一些常用菜单&#xff0c;这时&#xff0c;可以将主菜单项下的不常用菜单隐藏起来。此种显示方式类似于对菜单进行拉伸。使用时&#xff0c;只需单击展开菜单&#xff0c;即可显示相应菜单功能。运行本例&#xf…

python matplotlib中colorbar的位置设置

colorbar单独设置一个轴对象&#xff0c;再对轴对象进行灵活设置 import numpy as np import matplotlib.pyplot as plt# 创建一个二维随机数组 data np.random.rand(10, 10)# 创建一个图形和一个子图 fig, ax plt.subplots()# 绘制热力图 heatmap ax.imshow(data, cmaphot…

在linux中快速安装Redis数据库

Redis中文网 点击该链接下载最5.0.4版本的Redis的压缩包 使用Xftp工具将Redis安装包上传到linux中 1.将压缩包解压到/opt目录下: tar -zxvf redis-5.0.4.tar.gz 2. 更新yun: sudo yum makecache fast 3.安装gcc: yum -y install gcc 4.安装完成通过输入 : gcc -v …

tiny tool - get_file_path_name_by_drop_file

文章目录 tiny tool - get_file_path_name_by_drop_file概述工程效果收获的知识点vs2022工程, 必须自己设置对话框可以接受文件的风格vs2022建立的工程, 默认是unicode编码, 设置剪贴板数据时, 必须要设置为unicode的格式, 否则剪切板中只有第一个字符工程主要实现END tiny too…

短信压力测试系统,支持自定义接口

短信压力测试系统,支持自定义接口 支持卡密充值&#xff0c;短信压力测试系统&#xff0c;解决一切骚扰电话&#xff0c;教程在压缩包里面 可多个服务器挂脚本分担压力&#xff0c;套了cdn导致无法正常执行脚本可以尝试添加白名单 这边建议使用MySQL方式 同服务器下直接配置…

MySQL生产环境高可用架构实战

分布式技术MongoDB 1. MySQL高可用集群介绍1.1 数据库主从架构与分库分表1.2 MySQL主从同步原理 2. 动手搭建MySQL主从集群2.1 基础环境搭建2.2 安装MySQL服务2.2.1 初始化MySQL2.2.2 启动mysql2.2.3 连接MySQL 2.3 搭建主从集群2.3.1 配置master主服务2.3.2 配置slave从服务主…

Radzen Blazor Studio 1.12 Crack

Radzen Blazor Studio 是一款桌面工具&#xff0c;使 开发人员 能够创建精美的商业 Blazor 应用程序。快速地。 开放技术栈 没有供应商锁定。生成的源代码是人类可读的&#xff0c;您可以使用免费工具构建它。 Radzen 由流行的开源技术 - ASP.NET Core、Blazor、Bootstrap 提供…

较少的分区也报错too many range table entries

问题现象 postgresql中update执行语句报错too many range table entries 源sql with t as (select id from LZLTAB where id8723 limit 100 ) update LZLTAB setSTATUS 00,FILE_ID null,DATE_UPDATED localtimestamp(0) where id in (select id from t)如果把update改写成…

碳排放预测模型 | Python实现基于机器学习的碳排放预测模型——数据清理和可视化

文章目录 效果一览文章概述研究内容源码设计参考资料效果一览 文章概述 碳排放预测模型 | Python实现基于机器学习的碳排放预测模型——数据清理和可视化 研究内容 碳排放被认为是全球变暖的最主要原因之一。 该项目旨在提供各国碳排放未来趋势的概述以及未来十年的全球趋势预测…

三维空间刚体运动之旋转矩阵与变换矩阵

1. 旋转矩阵 1.1 点、向量和坐标系 点&#xff1a;点是空间中的基本元素&#xff0c;没有长度&#xff0c;没有体积&#xff1b; 向量&#xff1a;把两个点连接起来&#xff0c;就构成了向量&#xff0c;向量可以看成从某点指向另一点的一个箭头&#xff1b;只有当我们指定这…

threejs精灵和粒子系统

个人博客地址: https://cxx001.gitee.io 前面我们了解到了场景中的网格对象由几何体和材质组成&#xff0c;并且分别系统学习了它们。这节我们将学习一个特殊的网格对象-----粒子(精灵)。 了解粒子 一个粒子(新版叫精灵)是 一个二维平面(小方块) &#xff0c;它总是面向摄像…

Linux--阅读文本指令:more、less

生成10000行数字并将其写入普通文件的指令&#xff1a; count0; while [ $count -le 10000 ]; do echo "hello bit ${count}"; let count; done > file.txt 直接cat < file.txt会刷屏的&#xff0c;故引入more 注&#xff1a;enter键控制下翻&#xff0c;q直…