news 2026/1/16 8:30:49

大模型基础补全计划(五)---seq2seq实例与测试(编码器、解码器架构)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型基础补全计划(五)---seq2seq实例与测试(编码器、解码器架构)

编码器-解码器(encoder-decoder)架构

前面的文章中我们的模型示例都是根据已有的文字序列,续写N个字。在自然语言处理中,还有有一类需求也是比较经典,那就是机器翻译。

对于机器翻译来说,其核心就是将一种语言翻译为另外一种语言,换句话说就是一种序列数据到另外一种序列数据。从这里来看,出现了两种序列数据,那么必然的很容易想到类似两个RNN的独立网络来处理这种任务,基于这种情况,有人提出了编码器-解码器架构,下图是这种架构的示意图。

rep_img

从示意图可知,这种架构的核心就是处理输入序列,得到中间状态,将中间状态传给解码器,解码器负责生成输出序列。对于翻译任务来说,输入序列就是原文,输出序列就是译文。

这里说起来还是概念性的,我们下面从一个经典的编码器、解码器结构的模型来实际 演示一下翻译需求的模型是什么样子的。

基于 seq2seq 的 英文翻译中文 的实例

英文中文翻译数据集

首先数据集下载地址是http://www.manythings.org/anki/ 中的cmn-eng.zip 文件,其内部的数据集格式大概如下:

I try. 我试试。 CC-BY 2.0 (France) Attribution: tatoeba.org #20776 (CK) & #8870261 (will66)

I won! 我赢了。 CC-BY 2.0 (France) Attribution: tatoeba.org #2005192 (CK) & #5102367 (mirrorvan)

Oh no! 不会吧。 CC-BY 2.0 (France) Attribution: tatoeba.org #1299275 (CK) & #5092475 (mirrorvan)

Cheers! 乾杯! CC-BY 2.0 (France) Attribution: tatoeba.org #487006 (human600) & #765577 (Martha)

Got it? 知道了没有? CC-BY 2.0 (France) Attribution: tatoeba.org #455353 (CM) & #455357 (GlossaMatik)

Got it? 懂了吗? CC-BY 2.0 (France) Attribution: tatoeba.org #455353 (CM) & #2032276 (ydcok)

Got it? 你懂了吗? CC-BY 2.0 (France) Attribution: tatoeba.org #455353 (CM) & #7768205 (jiangche)

He ran. 他跑了。 CC-BY 2.0 (France) Attribution: tatoeba.org #672229 (CK) & #5092389 (mirrorvan)

由于我的卡(3060 12G)有点拉库,为了效率,因此整个数据集只用前面2千条即可。

文本预处理

# dataset.py

import collections

import torch

from torch.utils import data

# 下面返回的数据是:

# [['Hi.', '嗨。'], ['Hi.', '你好。'], ['Run.', '你用跑的。'], ['Stop!', '住手!'], ['Wait!', '等等!'], ... ...]

def read_data():

with open('cmn-eng/cmn.txt', 'r',

encoding='utf-8') as f:

lines = f.readlines()

return [line.split(" ")[:2] for line in lines]

# 输出是:

# [['Hi.'], ['Hi.'], ['Run.'], ['Stop!'], ['Wait!']]

# [['嗨', '。'], ['你', '好', '。'], ['你', '用', '跑', '的', '。'], ['住', '手', '!'], ['等', '等', '!']]

# ['Hi.', 'Hi.', 'Run.', 'Stop!', 'Wait!']

# ['嗨。', '你好。', '你用跑的。', '住手!', '等等!']

def tokenize(lines, token='char'): #@save

"""将文本行拆分为单词或字符词元"""

source_tokenize, target_tokenize = [], []

source_line, target_line = [], []

print(f'dataset len = {len(lines)}')

for line in lines:

s = line[0]

t = line[1]

source_line.append(s)

target_line.append(t)

source_tokenize.append(s.split(' '))

target_tokenize.append([word for word in t])

return source_tokenize, target_tokenize, source_line, target_line

# 词元的类型是字符串,而模型需要的输入是数字,因此这种类型不方便模型使用。 现在,让我们构建一个字典,

# 通常也叫做词表(vocabulary), 用来将字符串类型的词元映射到从开始的数字索引中。

def count_corpus(tokens): #@save

"""统计词元的频率"""

# 这里的tokens是1D列表或2D列表

if len(tokens) == 0 or isinstance(tokens[0], list):

# 将词元列表展平成一个列表

tokens = [token for line in tokens for token in line]

return collections.Counter(tokens)

# 返回类似{'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1}的一个字典

class Vocab:

"""文本词表"""

def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):

if tokens is None:

tokens = []

if reserved_tokens is None:

reserved_tokens = []

# 按出现频率排序

# 对于Counter("hello world"),结果如下

# Counter({'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1})

counter = count_corpus(tokens)

self._token_freqs = sorted(counter.items(), key=lambda x: x[1],

reverse=True)

# 未知词元的索引为0

self.idx_to_token = ['<unk>'] + reserved_tokens

self.token_to_idx = {token: idx

for idx, token in enumerate(self.idx_to_token)}

for token, freq in self._token_freqs:

if freq < min_freq:

break

if token not in self.token_to_idx:

self.idx_to_token.append(token)

self.token_to_idx[token] = len(self.idx_to_token) - 1

def __len__(self):

return len(self.idx_to_token)

def __getitem__(self, tokens):

if not isinstance(tokens, (list, tuple)):

return self.token_to_idx.get(tokens, self.unk)

return [self.__getitem__(token) for token in tokens]

def to_tokens(self, indices):

if not isinstance(indices, (list, tuple)):

return self.idx_to_token[indices]

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

@property

def unk(self): # 未知词元的索引为0

return 0

@property

def token_freqs(self):

return self._token_freqs

def truncate_pad(line, num_steps, padding_token):

"""截断或填充文本序列"""

if len(line) > num_steps:

return line[:num_steps] # 截断

return line + [padding_token] * (num_steps - len(line)) # 填充

def build_array(lines, vocab, num_steps):

"""将机器翻译的文本序列转换成小批量"""

lines = [vocab[l] for l in lines] # 每行的token转换为其id

lines = [l + [vocab['<eos>']] for l in lines] # 每行的token后加上eos的id

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 load_array(data_arrays, batch_size, is_train=True):

"""构造一个PyTorch数据迭代器

Defined in :numref:`sec_linear_concise`"""

dataset = data.TensorDataset(*data_arrays)

return data.DataLoader(dataset, batch_size, shuffle=is_train)

def load_data(batch_size, num_steps, num_examples=600):

"""返回翻译数据集的迭代器和词表"""

text = read_data()

source, target, src_line, tgt_line = tokenize(text)

# 返回类似{'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1}的一个字典

src_vocab = Vocab(source, min_freq=0,

reserved_tokens=['<pad>', '<bos>', '<eos>'])

tgt_vocab = Vocab(target, min_freq=0,

reserved_tokens=['<pad>', '<bos>', '<eos>'])

# 首先把每行的词转换为了其对应的id,然后给每一行的末尾添加token <eos>, 然后根据num_steps,如果line长度不足,补<pad>,如果长度超出,截断

# 一种类型的输出是:

# [

# [line0-char0-id, line0-char1-id, line0-char2-id, ...., eos-id],

# [line1-char0-id, line1-char1-id, line1-char2-id, ...., eos-id], 注意,最后的末尾可能没有eos

# .....

# ]

src_array, src_valid_len = build_array(source, src_vocab, num_steps)

tgt_array, tgt_valid_len = build_array(target, tgt_vocab, num_steps)

data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len)

data_iter = load_array(data_arrays, batch_size)

return data_iter, src_vocab, tgt_vocab, src_line, tgt_line

上面代码做了如下事情:

根据数据集的格式,读取每一行,只提前每行前面2个字符串。

然后我们对每一行进行文字切割,得到了一个二维列表,列表中的每一行又被分割为一个个中文文字和一个个英文的词,也就得到了一个个token。(特别注意,站在当前的时刻,这里的token和现在主流的大语言模型的token概念是一样的,但是不是一样的实现。)

由于模型不能直接处理文字,我们需要将文字转换为数字,那么直接的做法就是将一个个token编号即可,这个时候我们得到了词表(vocabulary)。

然后我们根据我们得到的词表,对原始数据集进行数字化,得到一个列表,列表中每个元素就是一个个token对应的索引。

最后得到:基于pytorch的DataLoader、原文词表、译文词表、原文文字列表、译文文字列表

此外,在这里出现了几个在后面的大语言模型中也会出现的词:BOS/EOS。这两个分别代表一次对话的起始、结尾,这里直接记住就行。

搭建seq2seq训练框架

首先引用一些包和一些辅助class

import os

import random

import torch

import math

from torch import nn

from torch.nn import functional as F

import numpy as np

import time

import visdom

import collections

import dataset

class Accumulator:

"""在n个变量上累加"""

def __init__(self, n):

"""Defined in :numref:`sec_softmax_scratch`"""

self.data = [0.0] * n

def add(self, *args):

self.data = [a + float(b) for a, b in zip(self.data, args)]

def reset(self):

self.data = [0.0] * len(self.data)

def __getitem__(self, idx):

return self.data[idx]

class Timer:

"""记录多次运行时间"""

def __init__(self):

"""Defined in :numref:`subsec_linear_model`"""

self.times = []

self.start()

def start(self):

"""启动计时器"""

self.tik = time.time()

def stop(self):

"""停止计时器并将时间记录在列表中"""

self.times.append(time.time() - self.tik)

return self.times[-1]

def avg(self):

"""返回平均时间"""

return sum(self.times) / len(self.times)

def sum(self):

"""返回时间总和"""

return sum(self.times)

def cumsum(self):

"""返回累计时间"""

return np.array(self.times).cumsum().tolist()

然后我们根据编码器、解码器架构,设计seq2seq的网络主干

class Encoder(nn.Module):

"""编码器-解码器架构的基本编码器接口"""

def __init__(self, **kwargs):

# 调用父类nn.Module的构造函数,确保正确初始化

super(Encoder, self).__init__(**kwargs)

def forward(self, X, *args):

# 抛出未实现错误,意味着该方法需要在子类中具体实现

raise NotImplementedError

class Decoder(nn.Module):

"""编码器-解码器架构的基本解码器接口

Defined in :numref:`sec_encoder-decoder`"""

def __init__(self, **kwargs):

# 调用父类nn.Module的构造函数,确保正确初始化

super(Decoder, self).__init__(**kwargs)

def init_state(self, enc_outputs, *args):

# 抛出未实现错误,意味着该方法需要在子类中具体实现

raise NotImplementedError

def forward(self, X, state):

# 抛出未实现错误,意味着该方法需要在子类中具体实现

raise NotImplementedError

class EncoderDecoder(nn.Module):

"""编码器-解码器架构的基类

Defined in :numref:`sec_encoder-decoder`"""

def __init__(self, encoder, decoder, **kwargs):

# 调用父类nn.Module的构造函数,确保正确初始化

super(EncoderDecoder, self).__init__(**kwargs)

# 将传入的编码器实例赋值给类的属性

self.encoder = encoder

# 将传入的解码器实例赋值给类的属性

self.decoder = decoder

def forward(self, enc_X, dec_X, *args):

# 调用编码器的前向传播方法,处理输入的编码器输入数据enc_X

enc_outputs = self.encoder(enc_X, *args)

# 调用解码器的init_state方法,根据编码器的输出初始化解码器的状态

dec_state = self.decoder.init_state(enc_outputs, *args)

# 调用解码器的前向传播方法,处理输入的解码器输入数据dec_X和初始化后的状态

return self.decoder(dec_X, dec_state)

#@save

class Seq2SeqEncoder(Encoder):

"""用于序列到序列学习的循环神经网络编码器"""

def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,

dropout=0, **kwargs):

super(Seq2SeqEncoder, self).__init__(**kwargs)

# 嵌入层

self.embedding = nn.Embedding(vocab_size, embed_size)

self.rnn = nn.GRU(embed_size, num_hiddens, num_layers,

dropout=dropout)

# self.lstm = nn.LSTM(embed_size, num_hiddens, num_layers)

def forward(self, X, *args):

# 输入X.shape = (batch_size,num_steps)

# 输出'X'的形状:(batch_size,num_steps,embed_size)

X = self.embedding(X)

# 在循环神经网络模型中,第一个轴对应于时间步

X = X.permute(1, 0, 2)

# 如果未提及状态,则默认为0

output, state = self.rnn(X)

# output : 这个返回值是所有时间步的隐藏状态序列

# output的形状:(num_steps,batch_size,num_hiddens)

# hn (hidden) : 这是每一层rnn的最后一个时间步的隐藏状态

# state的形状:(num_layers,batch_size,num_hiddens)

return output, state

class Seq2SeqDecoder(Decoder):

"""用于序列到序列学习的循环神经网络解码器"""

def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,

dropout=0, **kwargs):

super(Seq2SeqDecoder, self).__init__(**kwargs)

self.embedding = nn.Embedding(vocab_size, embed_size)

self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers,

dropout=dropout)

self.dense = nn.Linear(num_hiddens, vocab_size)

def init_state(self, enc_outputs, *args):

return enc_outputs[1]

def forward(self, X, state):

# 输出'X'的形状:(batch_size,num_steps,embed_size)

X = self.embedding(X).permute(1, 0, 2)

# 广播context,使其具有与X相同的num_steps

context = state[-1].repeat(X.shape[0], 1, 1)

X_and_context = torch.cat((X, context), 2)

output, state = self.rnn(X_and_context, state)

output = self.dense(output).permute(1, 0, 2)

# output的形状:(batch_size,num_steps,vocab_size)

# state的形状:(num_layers,batch_size,num_hiddens)

return output, state

我们结合上面的架构图对比着看,首先声明一下decoder/encoder的接口类:

声明了Encoder(nn.Module),Encoder(nn.Module)其输入是原文,输出是中间状态。

声明了Decoder(nn.Module),Decoder(nn.Module)的输入是BOS和中间状态,输出是译文。

声明了EncoderDecoder(nn.Module)类,串联Encoder(nn.Module)/Decoder(nn.Module)进行运行。

然后声明实际的Seq2SeqEncoder部分:

声明了Seq2SeqEncoder(Encoder),其是seq2seq编码器部分的实际定义,其输入是一串原文,然后经过了nn.Embedding,将输入的token序列转换为token-embedding,然后送入nn.GRU,得到了两个值:最后一层rnn的所有时间步的隐藏状态output(shape=num_steps,batch_size,num_hiddens),所有层rnn的最后一个时间步的隐藏状态h_n(shape=num_layers,batch_size,num_hiddens)

从Seq2SeqEncoder(Encoder)上面的分析可知:rnn的输出output代表的是每一个时间步,当前序列的总结信息,h_n encoder的隐藏态参数。

最后声明实际的Seq2SeqDecoder部分:

声明了Seq2SeqDecoder(Decoder),输入是:一个是bos,一个是Seq2SeqEncoder(Encoder)输出的隐藏态state(output,h_n)。首先将bos转换为embedding向量,然后将h_n的最后一个数据(也就是原文的总结,rnn最后一层最后一个时间步的隐藏态)和embedding组合在一起(注意:这里已经将原文的语义已经和bos输入混合在一起了),和Seq2SeqEncoder(Encoder) state作为的隐藏状态初始值,一起传入rnn,然后经过nn.Linear的映射,得到了decoder的输出。

从Seq2SeqDecoder(Decoder)的分析可知,经过了nn.Linear映射之后,我们将decoder层的rnn的output转换为词表大小的一个向量,这个向量我们可以看做下一个字的分数Logits(注意:这个概念在后续大语言模型中,有比较大的作用)。

这里,nn.RNN等pytorch层的输出,可以结合下面这个图来理解(图来自于参考文献相关链接):

rep_img

下面给出的就是训练、预测部分的代码:

def try_gpu(i=0):

"""如果存在,则返回gpu(i),否则返回cpu()

Defined in :numref:`sec_use_gpu`"""

if torch.cuda.device_count() >= i + 1:

return torch.device(f'cuda:{i}')

return torch.device('cpu')

def sequence_mask(X, valid_len, value=0):

"""在序列中屏蔽不相关的项"""

maxlen = X.size(1)

mask = torch.arange((maxlen), dtype=torch.float32,

device=X.device)[None, :] < valid_len[:, None]

X[~mask] = value

return X

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

def grad_clipping(net, theta): #@save

"""裁剪梯度"""

if isinstance(net, nn.Module):

params = [p for p in net.parameters() if p.requires_grad]

else:

params = net.params

norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))

if norm > theta:

for param in params:

param.grad[:] *= theta / norm

def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):

"""训练序列到序列模型"""

def xavier_init_weights(m):

if type(m) == nn.Linear:

nn.init.xavier_uniform_(m.weight)

if type(m) == nn.GRU:

for param in m._flat_weights_names:

if "weight" in param:

nn.init.xavier_uniform_(m._parameters[param])

net.apply(xavier_init_weights)

net.to(device)

optimizer = torch.optim.Adam(net.parameters(), lr=lr)

loss = MaskedSoftmaxCELoss()

net.train()

vis = visdom.Visdom(env=u'test1', server="http://127.0.0.1", port=8097)

animator = vis

for epoch in range(num_epochs):

timer = Timer()

metric = Accumulator(2) # 训练损失总和,词元数量

for batch in data_iter:

#清零(reset)优化器中的梯度缓存

optimizer.zero_grad()

# x.shape = [batch_size, num_steps]

X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]

# bos.shape = batch_size 个 bos-id

bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],

device=device).reshape(-1, 1)

# dec_input.shape = (batch_size, num_steps)

# 解码器的输入通常由序列的起始标志 bos 和目标序列(去掉末尾的部分 Y[:, :-1])组成。

dec_input = torch.cat([bos, Y[:, :-1]], 1) # 强制教学

# Y_hat的形状:(batch_size,num_steps,vocab_size)

Y_hat, _ = net(X, dec_input, X_valid_len)

l = loss(Y_hat, Y, Y_valid_len)

l.sum().backward() # 损失函数的标量进行“反向传播”

grad_clipping(net, 1)

num_tokens = Y_valid_len.sum()

optimizer.step()

with torch.no_grad():

metric.add(l.sum(), num_tokens)

if (epoch + 1) % 10 == 0:

# print(predict('你是?'))

# print(epoch)

# animator.add(epoch + 1, )

if epoch == 9:

# 清空图表:使用空数组来替换现有内容

vis.line(X=np.array([0]), Y=np.array([0]), win='train_ch8', update='replace')

# _loss_val = l

# _loss_val = _loss_val.cpu().sum().detach().numpy()

vis.line(

X=np.array([epoch + 1]),

Y=[ metric[0] / metric[1]],

win='train_ch8',

update='append',

opts={

'title': 'train_ch8',

'xlabel': 'epoch',

'ylabel': 'loss',

'linecolor': np.array([[0, 0, 255]]), # 蓝色线条

}

)

print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} '

f'tokens/sec on {str(device)}')

torch.save(net.cpu().state_dict(), 'model_h.pt') # [[6]]

torch.save(net.cpu(), 'model.pt') # [[6]]

def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,

device, save_attention_weights=False):

"""序列到序列模型的预测"""

# 在预测时将net设置为评估模式

net.eval()

src_tokens = src_vocab[src_sentence.lower().split(' ')] + [

src_vocab['<eos>']]

enc_valid_len = torch.tensor([len(src_tokens)], device=device)

src_tokens = dataset.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])

# 添加批量轴

enc_X = torch.unsqueeze(

torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)

enc_outputs = net.encoder(enc_X, enc_valid_len)

dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)

# 添加批量轴

dec_X = torch.unsqueeze(torch.tensor(

[tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)

output_seq, attention_weight_seq = [], []

for _ in range(num_steps):

Y, dec_state = net.decoder(dec_X, dec_state)

# 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入

dec_X = Y.argmax(dim=2)

pred = dec_X.squeeze(dim=0).type(torch.int32).item()

# 保存注意力权重(稍后讨论)

if save_attention_weights:

attention_weight_seq.append(net.decoder.attention_weights)

# 一旦序列结束词元被预测,输出序列的生成就完成了

if pred == tgt_vocab['<eos>']:

break

output_seq.append(pred)

return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq

def bleu(pred_seq, label_seq, k): #@save

"""计算BLEU"""

pred_tokens, label_tokens = pred_seq.split(' '), [i for i in label_seq]

len_pred, len_label = len(pred_tokens), len(label_tokens)

score = math.exp(min(0, 1 - len_label / len_pred))

for n in range(1, k + 1):

num_matches, label_subs = 0, collections.defaultdict(int)

for i in range(len_label - n + 1):

label_subs[' '.join(label_tokens[i: i + n])] += 1

for i in range(len_pred - n + 1):

if label_subs[' '.join(pred_tokens[i: i + n])] > 0:

num_matches += 1

label_subs[' '.join(pred_tokens[i: i + n])] -= 1

score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))

return score

这里首先要介绍一下其损失函数,核心两个:

通过交叉熵计算真实分布、预测分布的差异性,差异性越小,意味着我们的模型越好

由于我们是序列模型,可能涉及pad项,这些pad项的位置是无意义的,但是对模型有影响,我们需要再loss中剔除掉这种无意义的位置,我们用mask来屏蔽。

然后训练过程的核心就是:从数据集中获取 训练数据、验证数据,通过训练数据得到预测数据,预测数据和验证数据进行loss计算,然后进行反向传播,找到loss最小化的方向,然后最小化loss,模型就会越来越好。

然后就是介绍预测部分的内容:先将原文输入到seq的encoder,然后将bos序列 + seq的encoder的隐藏态传给seq的decoder,就可以得到下一个字的输出,直到我们遇到eos,预测结束。

我们虽然预测完毕了,得到了原文对应的译文,但是我们需要一种方法来评估我们翻译的是不是正确,这里用的方法是bleu,它的作用就是评估输出序列与目标序列的精确度。

最后,我们开始训练过程,注意,下面的例子是先进行训练,然后保存pt模型,然后加载模型进行预测推理。

if __name__ == '__main__':

embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1

batch_size, num_steps = 64, 10

lr, num_epochs, device = 0.005, 2000, try_gpu()

# train_iter 每个迭代输出:(batch_size, num_steps)

train_iter, src_vocab, tgt_vocab, source, target = dataset.load_data(batch_size, num_steps)

encoder = Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers,

dropout)

decoder = Seq2SeqDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers,

dropout)

net = EncoderDecoder(encoder, decoder)

is_train = False

if is_train:

train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)

else:

state_dict = torch.load('model_h.pt')

net.load_state_dict(state_dict)

net.to(device)

C = 0

C1 = 0

for i in range(2000):

# print(source[i])

# print(target[i])

translation, attention_weight_seq = predict_seq2seq(

net, source[i], src_vocab, tgt_vocab, num_steps, device)

score = bleu(translation, target[i], k=2)

if score > 0.0:

C = C + 1

if score > 0.8:

C1 = C1 + 1

print(f'{source[i]} => {translation}, bleu {score:.3f}')

print(f'Counter(bleu > 0) = {C}')

print(f'Valid-Counter(bleu > 0.8) = {C1}')

rep_img

rep_img

从上面的图可以看到,这个模型有一定的翻译效果。

此外,我这里计算了非零的bleu以及大于0.8的bleu的个数,这个个数勉强可以评估,我们对现在这个seq2seq模型优化的效果,为后面的文章提前做一些准备工作。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/12 16:29:32

bv-study05 vue基础(添加用户练习,最后附完整源码)

一.要实现的功能展示 二.分步解决 1.vue2搭建 nodejs安装下载https://blog.csdn.net/weixin_55992854/article/details/121140754?spm1001.2014.3001.5506 nvm安装下载 nvm安装教程 vue脚手架搭建 https://blog.csdn.net/qq_48164590/article/details/129440134 2.代码…

作者头像 李华
网站建设 2025/12/24 12:03:57

如何快速掌握猫抓资源嗅探器:新手必备的完整使用指南

猫抓资源嗅探器是一款专为浏览器设计的智能媒体捕获工具&#xff0c;能够自动识别网页中的视频、音频和图片资源&#xff0c;为普通用户提供简单高效的下载管理体验。无论您是想保存社交媒体视频、在线课程内容还是网页图片&#xff0c;这款免费工具都能完美胜任。 【免费下载链…

作者头像 李华
网站建设 2025/12/30 8:41:55

session和cookie的区别

Session的工作原理Session是一种服务器端的机制&#xff0c;用于跟踪用户的状态和数据。当用户首次访问网站时&#xff0c;服务器会创建一个唯一的Session ID&#xff0c;并通过Cookie或URL重写的方式将该ID发送给客户端。客户端在后续请求中会携带这个Session ID&#xff0c;服…

作者头像 李华
网站建设 2026/1/13 0:33:28

海外网红推广中的品牌声誉保护与危机处理机制

随着品牌出海规模不断扩大&#xff0c;海外网红推广已成为企业触达全球消费者最直接、最高效的方式之一。然而&#xff0c;红人合作的开放性、舆论传播的不可控性、跨文化解读的复杂性&#xff0c;使得品牌声誉保护成为企业在全球营销中的首要挑战。如果品牌缺乏稳固的危机处理…

作者头像 李华
网站建设 2025/12/21 2:28:42

基于.Net 8创建 CAD勘测定界图(三)——界址点标注+边长标注

好的&#xff0c;之前的两篇文章大概介绍了一下关于做这个功能的背景和关于Aspose.CAD For .Net填充无效&#xff0c;转用ACadSharp创建红线和界址点符号的内容&#xff0c;具体看&#xff1a; 基于.Net 8创建 CAD勘测定界图&#xff08;一&#xff09; 基于.Net 8创建 CAD勘测…

作者头像 李华
网站建设 2026/1/11 16:53:06

Qwen3-VL-235B-A22B:2025多模态AI革命,从看懂到行动的跨越

Qwen3-VL-235B-A22B&#xff1a;2025多模态AI革命&#xff0c;从看懂到行动的跨越 【免费下载链接】Qwen3-VL-235B-A22B-Instruct 项目地址: https://ai.gitcode.com/hf_mirrors/Qwen/Qwen3-VL-235B-A22B-Instruct 导语 阿里通义千问团队推出的Qwen3-VL-235B-A22B-Ins…

作者头像 李华