自学留档,如有错误,恳请批评指正。
目录
- 理解LSTM(门控神经网络)
- 完成双语翻译器代码
- 评估NMT system
任务1 理解LSTM(门控神经单元)
前置学习任务:循环神经网络RNN
RNN(循环神经网络)是一种处理时序信息的网络结构,广泛应用于自然语言处理任务。然而,RNN面临梯度消失的问题,这导致模型难以训练和优化。因此,LSTM(长短期记忆网络)和GRU(门控循环单元)被提出作为改进。
在RNN中,每个神经元同时接收当前时刻的输入(x)和上一时刻的输出(ht-1)。其中,o是当前时刻的输出,ht是传递给下一神经元的信息。
为了更好地理解LSTM,我借助了B站视频BVqM4y1M7Nv和BV1Z34y1k7mc的类比方法。
我们可以将循环神经网络比作一个人的一生,每一天都重复着。这个人每天都需要完成各种任务,比如上学或考试。如果他今天要考数学分析,他的成绩取决于昨天学习的知识(ht-1)和今天考卷上的题目(xt)。他将这两部分信息进行整合(tanh),最终得到一个分数。由于RNN中的梯度消失问题,这个人的记忆和思考能力都受到限制,因此只能得到一般的成绩。
LSTM网络的主要改进在于,它除了接收输入x和h之外,还引入了一个新的信息单元C。可以将C比作一本笔记本,记录了LSTM所学的所有知识。如果这个人可以带着笔记本去考试,他的成绩自然会优于RNN。然而,笔记本上的所有信息并不一定都能用得上,因此LSTM将笔记本上的内容进行筛选,生成了一个精简版的小抄,即C×f,其中f是遗忘门,负责决定保留哪些信息。
此外,LSTM还引入了两个门控单元:输入门(i)和输出门(o)。输入门类似于遗忘门,用于处理当前输入数据,也就是考试试卷上的信息;输出门则相当于对已完成的答案进行检查和处理。通过这些机制,LSTM能够有效提高性能,得分自然也比RNN要高。
任务2 完成双语翻译器代码
双语翻译器设计图如上所示(中译英)
- 在 `utils.py` 中实现 `pad_sents` 函数,以生成这些填充后的句子。
- 在 `model_embeddings.py` 中实现 `__init__` 函数,初始化所需的源嵌入和目标嵌入。
- 在 `nmt_model.py` 中实现 `__init__` 函数,初始化 NMT 系统所需的模型层,包括 LSTM、CNN、投影层和 Dropout 层。
- 在 `nmt_model.py` 中实现 `encode` 函数。
- 在 `nmt_model.py` 中实现 `decode` 函数。
- 在 `nmt_model.py` 中实现 `step` 函数。
1. pad_sents函数实现
# pad_sents将输入的不同长度的矩阵统一到同意长度
# 方法是以最长的矩阵作为基准,其余长度矩阵用pad_token(如0)补全长度
# 如: 我喜欢吃草莓0 0 0 0 0 0 0 0 0 0 0 0
# 我明天要去学校上课0 0 0 0 0 0 0 0 0
# 你需要完成三项数学作业,两项英语作业
# 晚安0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
def pad_sents(sents, pad_token):
# 求出最长的句子长度。sents为句子合集,sent为其中的每个句子。
max_len = max(len(sent) for sent in sents)
# 将所有其他句子剩余部分补全为pad_token
sents_padded = [sent + [pad_token] * (max_len - len(sent)) for sent in sents]
return sents_padded
2. `model_embeddings.py` 中实现 `__init__` 函数,初始化所需的源嵌入和目标嵌入
class ModelEmbeddings(nn.Module):
def __init__(self, embed_size, vocab):
super(ModelEmbeddings, self).__init__()
self.embed_size = embed_size
# Embedding的目的是将词转化为一个数学化向量
# 如 我 -- ( 1.3 1.5 2.5 x …… x)
# 爱 -- ( x …… x)
# 吃 -- ( x …… x)
# 草 -- ( x …… x)
# 莓 -- ( x …… x)
# pad -- ( 0 0 0 0 …… 0 )
src_pad_token_idx = vocab.src['<pad>']
tgt_pad_token_idx = vocab.tgt['<pad>']
# 由于现在面对的是一个翻译任务,需要将源语言和目标语言(如英语翻译中文)
# 都进行embedding
# nn.Embedding可以自动转换,并且在学习过程中优化参数,参数优化后相似的词会距离相近
# 三个传递参数 第一个为句子的长度,第二个为设定的词向量的长度,第三个传递pad_token的位置
# 传递pad_token的位置是为了让这些位置在参数更新时候不优化
self.source = nn.Embedding(len(vocab.src), embed_size, src_pad_token_idx)
self.target = nn.Embedding(len(vocab.tgt), embed_size, tgt_pad_token_idx)
3. 初始化 NMT 系统所需的模型层,包括 LSTM、CNN、投影层和 Dropout 层。
def __init__(self, embed_size, hidden_size, vocab, dropout_rate=0.2):
super(NMT, self).__init__()
self.model_embeddings = ModelEmbeddings(embed_size, vocab)
self.hidden_size = hidden_size
self.dropout_rate = dropout_rate
self.vocab = vocab
# 这一部分定义了需要初始化的部分函数
# 一维卷积层,卷积核为2,可以提取相邻两个词的关系特征
self.post_embed_cnn = nn.Conv1d(in_channels=embed_size, out_channels=embed_size,
kernel_size=2, padding='same')
# 编码层,使用LSTM,隐藏状态维度决定了每个时间步的隐藏层可以捕捉和表示的信息量,可以设置
self.encoder = nn.LSTM(input_size=embed_size, hidden_size=hidden_size,
bidirectional=True)
# 解码层,由于要一步一步地解码,因此使用LSTMCell函数
self.decoder = nn.LSTMCell(input_size=embed_size + hidden_size,
hidden_size=hidden_size, bias=True)
# LSTM中包含隐藏状态h 和 细胞状态c,这两个状态通过以下的h_projection 和 c_projection线性映射,本质上是个全连接层。
self.h_projection = nn.Linear(in_features=hidden_size * 2,
out_features=hidden_size, bias=False)
self.c_projection = nn.Linear(in_features=hidden_size * 2,
out_features=hidden_size, bias=False)
# 注意力映射层,将注意力机制线性映射,本质上是个全连接层。
self.att_projection = nn.Linear(in_features=hidden_size * 2,
out_features=hidden_size, bias=False)
# 将隐藏状态,细胞状态和注意力映射到一起
self.combined_output_projection = nn.Linear(in_features=hidden_size * 3,
out_features=hidden_size, bias=False)
# 反向embedding
self.target_vocab_projection = nn.Linear(in_features=hidden_size,
out_features=len(vocab.tgt), bias=False)
self.dropout = nn.Dropout(dropout_rate)
4. 在 `nmt_model.py` 中实现 `encode` 函数。
# encode部分编码输入数据为 隐藏状态h 和 细胞状态c
def encode(self, source_padded: torch.Tensor, source_lengths: List[int]) -> Tuple[
torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
# 将输入数据(词)进行embedding
X = self.model_embeddings.source(source_padded)
# 将embedding后的数据卷积初步提取词语特征
# 由于卷积操作,因此需要将数据用permute变形,卷积后再变形回到原来形状
X = self.post_embed_cnn(torch.permute(X, (1, 2, 0)))
X = torch.permute(X, (2, 0, 1))
# 使用LSTM得到当前隐藏状态,上一个时刻的隐藏状态和细胞状态
enc_hiddens, (last_hidden, last_cell) = self.encoder(pack_padded_sequence(X, source_lengths))
# 使用 `pad_packed_sequence` 将编码器的输出还原为填充前的序列
(enc_hiddens, enc_hiddens_lengths) = pad_packed_sequence(enc_hiddens)
enc_hiddens = torch.permute(enc_hiddens, (1, 0, 2))
# 将双向 LSTM 的上一时刻隐藏状态和细胞状态拼接在一起
decoder_hidden = torch.cat((last_hidden[0, :, :], last_hidden[1, :, :]), 1)
# 通过线性层将拼接后的隐藏状态和细胞状态映射到解码器的目标维度
decoder_hidden = self.h_projection(init_decoder_hidden)
decoder_cell = torch.cat((last_cell[0, :, :], last_cell[1, :, :]), 1)
decoder_cell = self.c_projection(init_decoder_cell)
# 将初始隐藏状态和细胞状态组成元组
dec_init_state = (init_decoder_hidden, init_decoder_cell)
return enc_hiddens, dec_init_state
5. 在 `nmt_model.py` 中实现 `decode` 函数。
def decode(self, enc_hiddens: torch.Tensor, enc_masks: torch.Tensor,
dec_init_state: Tuple[torch.Tensor, torch.Tensor], target_padded: torch.Tensor) -> torch.Tensor:
target_padded = target_padded[:-1]
dec_state = dec_init_state
batch_size = enc_hiddens.size(0)
o_prev = torch.zeros(batch_size, self.hidden_size, device=self.device)
combined_outputs = []
# 对编码器隐藏状态进行线性变换,准备用于注意力计算
enc_hiddens_proj = self.att_projection(enc_hiddens)
# 对目标语言进行嵌入处理
Y = self.model_embeddings.target(target_padded)
# 遍历目标序列的每一个时间步
for Y_t in torch.split(Y, 1):
# 去掉维度1的大小为1的维度,(1, b, e)转化为(b, e),以为后期计算
Y_t_squeezed = torch.squeeze(Y_t, 0)
# 将当前时间步的嵌入 'Y_t_squeezed'(上文图中x) 和前一个时间步的输出 'o_prev'(上文途中ht-1) 拼接
Ybar_t = torch.cat((Y_t_squeezed, o_prev), 1)
# 调用 'step' 函数计算当前时间步的解码器状态和输出(下一部分)
(dec_state, o_t, e_t) = self.step(Ybar_t=Ybar_t, dec_state=dec_state, enc_hiddens=enc_hiddens,
enc_hiddens_proj=enc_hiddens_proj, enc_masks=enc_masks)
# 将当前时间步的输出添加到 `combined_outputs` 列表中,为最后输出使用
combined_outputs.append(o_t)
# 更新前一个时间步的输出为当前时间步的输出,为下一个step用
o_prev = o_t
# combined_outputs列表中的张量合并为一个单一张量
# 结果的形状为 (tgt_len, b, h),其中 'tgt_len' 是目标序列的长度
combined_outputs = torch.stack(combined_outputs, dim=0)
return combined_outputs
6. 在 `nmt_model.py` 中实现 `step` 函数。
def step(self, Ybar_t: torch.Tensor,
dec_state: Tuple[torch.Tensor, torch.Tensor],
enc_hiddens: torch.Tensor,
enc_hiddens_proj: torch.Tensor,
enc_masks: torch.Tensor) -> Tuple[Tuple, torch.Tensor, torch.Tensor]:
combined_output = None
# 使用decoder函数得到当前细胞状态和隐藏状态
dec_state = self.decoder(Ybar_t, dec_state)
(dec_hidden, dec_cell) = dec_state
# bmm进行批量矩阵乘法,计算注意力数值
e_t = torch.bmm(enc_hiddens_proj, torch.unsqueeze(dec_hidden, -1))
e_t = torch.squeeze(e_t, -1)
if enc_masks is not None:
e_t.data.masked_fill_(enc_masks.bool(), -float('inf'))
# 使用softmax函数对注意力分数 e_t 进行归一化处理,计算注意力权重 alpha_t
alpha_t = F.softmax(e_t, dim=1)
# 将注意力权重 与编码器隐藏状态 相乘,将注意力机制作用于隐藏状态上
a_t = torch.bmm(torch.unsqueeze(alpha_t, 1), enc_hiddens)
# 去掉大小为 1 的维度,使得 a_t 的形状为 (b, hidden_size)
a_t = torch.squeeze(a_t, 1)
# 按照图上要求将隐藏状态和注意力结果拼接
U_t = torch.cat((dec_hidden, a_t), 1)
V_t = self.combined_output_projection(U_t)
# V_t --- tanh --- dropout
O_t = self.dropout(torch.tanh(V_t))
# 返回解码器状态、当前时间步的输出 O_t 和注意力分数 e_t。
combined_output = O_t
return dec_state, combined_output, e_t
任务3 评估NMT system
一维卷积层作用
问:在将输入的中文序列编码为词汇表中的“片段”时,分词器会将序列映射为一系列词汇项,每个词汇项包含一个或多个字符。基于这些信息,在嵌入层之后并在将嵌入输入到双向编码器之前添加一维卷积层,如何帮助我们的神经机器翻译系统?
每个汉字要么是一个完整的单词,要么是词中的一个语素。查找“电”、“脑”和“电脑”各自的含义作为示例。字符“电”(电)和“脑”(脑)组合成短语“电脑”时,意思是“计算机”。卷积核为2,因此可以识别邻近两个字之间的关系,也就是将字结合成语词。
输出错误分析
1 Source Sentence: 贼人其后被警方拘捕及被判处盗窃罪名成立。
汉语中没有复数,因此culprit和culprits的翻译都可认为是正确的。但是后文判处盗窃罪名是本应该用法律术语,在NMT中并没有使用。通过增加法律术语训练数据,以提高对特定领域术语的识别能力。另外,这个网络可能更加在乎前后词语生成的连贯性,如果需要训练专门翻译学术专用文献的翻译器,可以对参数微调(我认为可以拉高输入数据传播路径的权重)。
2 Source Sentence: 几乎已经没有地方容纳这些人, 资源已经用尽。
3 Source Sentence: 当局已经宣布今天是国殇日。
4 Source Sentence : 俗语有云:“唔做唔错”。
未完待续……