目录
使用华为云modelart作为实验环境
在北京4选一个tensorflow的就可以
进入环境,本篇重点解析一下机器翻译(NLP)中解码器、自注意机制和解码器
参考来源:华为人才在线AI系列认证学习资料
home/user-name/work/ 的工作目录
data/目录
导入必要的库
import re
import os
import io
import time
import jieba # 使用命令安装 !pip install jieba
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from sklearn.model_selection import train_test_split
一,数据预处理
path_to_file = "data/cmn.txt" ## 数据集文件
# 步骤 7 定义预处理函数
def preprocess_eng(w):
w = w.lower().strip() # 单词都小写,用空格切分
# 单词和标点之间加空格
# eg: "he is a boy." => "he is a boy ." white-spaces-keeping-punctuation # Reference:- https://stackoverflow.com/questions/3645931/python-padding-punctuation-with-
w = re.sub(r"([?.!,])", r" \1 ", w)
# 多个空格合并为一个
w = re.sub(r'[" "]+', " ", w)
# 除了(a-z, A-Z, ".", "?", "!", ",")这些字符外,全替换成空格
w = re.sub(r"[^a-zA-Z?.!,]+", " ", w)
w = w.rstrip().strip()
# 增加开始结束标志,让模型知道何时停止预测
w = '<start> ' + w + ' <end>'
return w
def preprocess_chinese(w):
w = w.lower().strip()
w = jieba.cut(w, cut_all=False, HMM=True) # HMM=True 马尔科夫链 计算sentences里每个词的发射概率、转移概率
w = " ".join(list(w)) # 词之间增加空格
w = '<start> ' + w + ' <end>'
return w
解释:
用于预处理英文和中文文本数据。预处理是机器翻译中非常重要的步骤,它可以帮助模型更好地理解和处理输入文本。
1. preprocess_eng 函数
这个函数的作用是对英文文本进行预处理,主要包括以下几个步骤:
(1) 小写化和去首尾空格
w = w.lower().strip()
lower():将所有字符转换为小写,确保文本的统一性,避免大小写对模型的影响。
strip():去除字符串首尾的多余空格。
(2) 标点符号与单词之间加空格
w = re.sub(r"([?.!,])", r" \1 ", w)
使用正则表达式 re.sub,将标点符号(.、?、!、,)与单词之间添加空格。
([?.!,]):匹配标点符号。
r" \1 ":将匹配到的标点符号替换为前后各加一个空格的形式。
例如:"he is a boy." 转换为 "he is a boy ."
(3) 合并多个空格
w = re.sub(r'[" "]+', " ", w)
使用正则表达式将多个连续的空格替换为一个空格。
[" "]+:匹配一个或多个连续的空格。
替换为单个空格,确保文本中没有多余的空格。
(4) 替换非目标字符
w = re.sub(r"[^a-zA-Z?.!,]+", " ", w)
使用正则表达式将非目标字符替换为空格。
[^a-zA-Z?.!,]+:匹配不在 a-z、A-Z、.、?、!、, 范围内的字符。
替换为单个空格,确保文本中只包含目标字符。
(5) 去首尾空格
w = w.rstrip().strip()
rstrip():去除字符串尾部的多余空格。
strip():再次去除字符串首尾的多余空格。
(6) 添加起始和结束标志
w = '<start> ' + w + ' <end>'
在文本的开头添加 <start>,在结尾添加 <end>。
这是为了让模型知道何时开始预测和何时结束预测,通常用于序列生成任务(如机器翻译)。
2. preprocess_chinese 函数
这个函数的作用是对中文文本进行预处理,主要包括以下几个步骤:
(1) 小写化和去首尾空格
w = w.lower().strip()
与英文预处理相同,将文本转换为小写并去除首尾空格。
(2) 中文分词
w = jieba.cut(w, cut_all=False, HMM=True)
使用 jieba 库进行中文分词。
cut_all=False:使用精确模式分词,分词结果更准确。
HMM=True:使用隐马尔可夫模型(HMM)进行分词,可以更好地处理未登录词。
(3) 词之间添加空格
w = " ".join(list(w))
将分词结果转换为一个字符串,词之间用空格分隔。
这样可以将中文文本转换为类似英文的格式,方便模型处理。
(4) 添加起始和结束标志
w = '<start> ' + w + ' <end>'
与英文预处理相同,添加起始和结束标志。
3. 小结
这段代码实现了对英文和中文文本的预处理
- 统一文本格式:将文本转换为小写,去除多余空格,确保格式一致。
- 处理标点符号:通过在标点符号周围添加空格,让模型更好地理解句子结构。
- 分词处理:对中文文本进行分词,将中文句子转换为词序列。
- 添加起始和结束标志:让模型明确知道何时开始和结束预测。
4. 注意事项
- 正则表达式的使用:正则表达式是文本处理的强大工具,但需要根据具体需求调整。
- 分词工具的选择:对于中文分词,jieba 是常用的工具,但也可以选择其他分词工具(如 HanLP 或 SnowNLP)。
- 起始和结束标志:这些标志是序列生成任务中常用的技巧,但需要确保在训练和预测时一致使用。
二,检查数据处理效果
定义好了我们需要给模型传入的数据格式,查看效果:
en_sentence = "May I borrow this book?"
chn_sentence = "我可以借这本书吗?"
print(preprocess_eng(en_sentence))
print(preprocess_chinese(chn_sentence))
每条数据的格式已经转换成<start>word word 符号<end>的格式
# 步骤 8 加载数据集,并进行预处理操作
'''
加载原始数据集
预处理
文本转 id
padding,统一成相同的长度定义数据加载函数
'''
# 读取数据,每个元素的样式是 [英文, 中文]
def create_dataset(path, num_examples=None):
lines = open(path, encoding='UTF-8').read().strip().split('\n')
word_pairs = [[w for w in l.split('\t')] for l in lines[:num_examples]]
# [英文, 中文] 的词典
word_pairs = [[preprocess_eng(w[0]), preprocess_chinese(w[1])] for w in word_pairs]
return word_pairs
word_pairs = create_dataset(path_to_file)
# 展示前20 个数据
word_pairs[:20]
用en,chn两个变量接一下我们刚才构造的词典,并查看最后两条
# 把中文、英文分开:
en, chn = zip(*create_dataset(path_to_file))
print(en[-1])
print(chn[-1])
# 显示数据大小
print(len(en), len(chn))
统一输入input的范式,同时对输入的数据进行类似‘标准化’的处理,再定义好特征数据(en)和目标数据(chn)
def max_length(tensor):
# 取数据中的最大文本长度,用来将所有文本统一成一致的长度,模型才能够正常训练
return max(len(t) for t in tensor)
def tokenize(lang):
# 1. 分词
# 2. 转换成id
# 3. padding, 将每个句子统一成相同的长度,长度不足 的 后面 补0
lang_tokenizer = tf.keras.preprocessing.text.Tokenizer(filters='')
# 生成 词和id 的映射词典 {word:id}
lang_tokenizer.fit_on_texts(lang)
# 将词转换成对应的id
text_ids = lang_tokenizer.texts_to_sequences(lang)
# 统一成相同的长度
padded_text_ids = tf.keras.preprocessing.sequence.pad_sequences(text_ids, padding='post')
return padded_text_ids, lang_tokenizer
def load_dataset(path, num_examples=None):
# 加载数据,并做预处理
# 将中文设置为源语言,英文设置为目标语言
targ_lang, inp_lang = zip(*create_dataset(path, num_examples))
input_data, inp_lang_tokenizer = tokenize(inp_lang)
target_data, targ_lang_tokenizer = tokenize(targ_lang)
return input_data, target_data, inp_lang_tokenizer, targ_lang_tokenizer
三,编码器
Encoder
class Encoder(tf.keras.Model):
def __init__(self, vocab_size, embedding_dim, enc_units, batch_sz):
# vacab_size=vocab_inp_size=9394, embedding_dim=256 enc_units=1024 batch_sz=64
super(Encoder, self).__init__()
self.batch_sz = batch_sz
self.enc_units = enc_units
self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
self.gru = tf.keras.layers.GRU(self.enc_units,
return_sequences=True,
return_state=True,
#recurrent_activation='sigmoid',
recurrent_initializer='glorot_uniform')
def call(self, x, hidden):
# x 是训练数据,shape == (batch_size,max_length) -> (64, 46)
# embedding 后得到每个词的词向量, x shape == (batch_size, max_length, embedding_dim) -> (64, 46, 256)
x = self.embedding(x)
#在GRU 中,每一个时间步,输出层和隐藏层是相等的
# output 是所有时间步的输出层输出 shape == (batch_size, max_length, units) -> (64, 46, 1024)
# state 是最后一个时间步的隐藏层输出, shape == (batch_size, units) -> (64, 1024)
output, state = self.gru(x, initial_state=hidden)
return output, state
def initialize_hidden_state(self):
#初始化gru 的隐层参数, shape == (batch_size, units) -> (64,1024)
return tf.zeros((self.batch_sz, self.enc_units))
这个 Encoder 类是基于 TensorFlow 的 tf.keras.Model 构建的,用于机器翻译任务中的编码器部分。编码器的主要作用是将输入序列(如中文句子)转换为上下文向量(context vector),以便解码器(Decoder)能够基于这个上下文信息生成目标序列(如英文翻译)。
1. __init__ 方法
__init__ 方法是类的初始化函数,用于定义模型的结构和参数。
(1) 参数说明
- vocab_size:输入词汇表的大小。例如,如果输入是中文句子,vocab_size 表示中文词汇表的大小。
- embedding_dim:词嵌入的维度。词嵌入(Embedding)是将词汇表中的每个词映射到一个固定维度的向量空间中,以便模型能够处理。
- enc_units:GRU 单元的数量。GRU(Gated Recurrent Unit)是一种循环神经网络(RNN)的变体,用于处理序列数据。enc_units 表示 GRU 的隐藏层单元数。
- batch_sz:每批次的样本数量。在训练过程中,数据通常以批次的形式输入模型。
(2) 初始化模型组件
self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
self.gru = tf.keras.layers.GRU(self.enc_units,
return_sequences=True,
return_state=True,
recurrent_initializer='glorot_uniform')
- tf.keras.layers.Embedding:词嵌入层,将输入的词汇索引转换为固定维度的词向量。
- vocab_size:词汇表大小。
- embedding_dim:词向量的维度。
- tf.keras.layers.GRU:GRU 层,用于处理序列数据。
- self.enc_units:GRU 的隐藏层单元数。
- return_sequences=True:返回所有时间步的输出(用于后续的注意力机制)。
- return_state=True:返回最后一个时间步的隐藏状态(用于初始化解码器)。
- recurrent_initializer='glorot_uniform':GRU 的循环核初始化方式。
2. call 方法
call 方法是模型的前向传播函数,定义了输入数据如何通过模型进行计算。
(1) 输入参数
- x:输入序列,形状为 (batch_size, max_length),例如 (64, 46)。max_length 是序列的最大长度,64 是批次大小。
- hidden:GRU 的初始隐藏状态,形状为 (batch_size, enc_units),例如 (64, 1024)。
(2) 前向传播过程
x = self.embedding(x)
output, state = self.gru(x, initial_state=hidden)
- 词嵌入层:
- 输入 x 是词汇索引序列,形状为 (batch_size, max_length)。
- 通过词嵌入层后,每个词被转换为一个固定维度的向量,形状变为 (batch_size, max_length, embedding_dim)。
- GRU 层:
- 输入为嵌入后的序列 x 和初始隐藏状态 hidden。
- 输出:
- output:所有时间步的输出,形状为 (batch_size, max_length, enc_units)。
- state:最后一个时间步的隐藏状态,形状为 (batch_size, enc_units)。
(3) 返回值
- output:GRU 的所有时间步输出,用于后续的注意力机制。
- state:GRU 的最后一个时间步的隐藏状态,用于初始化解码器。
3. initialize_hidden_state 方法
这个方法用于初始化 GRU 的初始隐藏状态。
(1) 初始化方式
return tf.zeros((self.batch_sz, self.enc_units))
初始化为全零张量,形状为 (batch_size, enc_units)。
这种初始化方式简单且常见,但在某些情况下也可以尝试其他初始化方法(如随机初始化)。
4. 编码器的作用
在机器翻译任务中,编码器的作用是将输入序列(如中文句子)转换为上下文信息,以便解码器能够基于这些信息生成目标序列(如英文翻译)。
词嵌入:将输入的词汇索引转换为词向量。
序列处理:通过 GRU 处理序列数据,提取时间步的特征。
上下文向量:GRU 的最后一个时间步的隐藏状态(state)可以作为上下文向量,传递给解码器。
5. 注意事项
词嵌入层的作用:
词嵌入将离散的词汇索引转换为连续的向量空间,便于模型学习词汇之间的关系。
嵌入维度(embedding_dim)是一个超参数,需要根据任务调整。
GRU 的选择:
GRU 是一种高效的循环神经网络变体,适用于处理序列数据。
如果序列长度较长,可以考虑使用 LSTM(长短期记忆网络)以避免梯度消失问题。
初始隐藏状态:
在某些情况下,可以尝试使用非零初始化(如随机初始化)来观察模型性能的变化。
返回序列和状态:
return_sequences=True 和 return_state=True 是为了后续的注意力机制和解码器初始化。
6. 小结
这个 Encoder 类是一个典型的基于 GRU 的编码器,用于机器翻译任务。它通过词嵌入和 GRU 层将输入序列转换为上下文信息,为解码器提供必要的输入。编码器的设计简洁而高效,是序列到序列(Seq2Seq)模型中的关键部分。
四,Attention
# 步骤 11 定义Attention 层
class BahdanauAttention(tf.keras.Model):
def __init__(self, units):
super(BahdanauAttention, self).__init__()
self.W1 = tf.keras.layers.Dense(units)
self.W2 = tf.keras.layers.Dense(units)
self.V = tf.keras.layers.Dense(1)
def call(self, query, values):
# query shape == (batch_size, hidden size)
#扩展时间维度 shape == (batch_size, 1, hidden size)
#为了计算后面的 score
hidden_with_time_axis = tf.expand_dims(query, 1)
# score shape == (batch_size, max_length, 1)
# score 维度为1 是因为应用了self.V, V 的维度是1
# 应用self.V 前后的维度是 (batch_size, max_length, units) --> (batch_size, max_length, 1)
score = self.V(tf.nn.tanh(self.W1(values) + self.W2(hidden_with_time_axis)))
# 使用softmax 得到attention 的权重, attention_weights shape == (batch_size, max_length, 1)
attention_weights = tf.nn.softmax(score, axis=1)
# context_vector shape == (batch_size, max_length, hidden_size)
context_vector = attention_weights * values
# 相加后的attention 上下文向量的维度:shape context_vector == (batch_size, hidden_size)
context_vector = tf.reduce_sum(context_vector, axis=1)
return context_vector, attention_weights
这个 BahdanauAttention 类实现了 Bahdanau 注意力机制(Attention Mechanism),是机器翻译任务中解码器(Decoder)的关键部分。注意力机制的作用是让解码器在生成目标序列的每个词时,能够动态地关注输入序列(编码器输出)中最重要的部分,从而提高翻译的准确性和流畅性。
1. Bahdanau 注意力机制的原理
Bahdanau 注意力机制是一种加性注意力机制,其核心思想是通过学习一个权重分布(attention_weights),将编码器的输出(values)加权求和,得到一个上下文向量(context_vector)。这个上下文向量包含了当前解码步骤中最相关的输入信息,帮助解码器更好地生成目标词。
具体步骤如下:
计算分数(score):基于当前解码器的隐藏状态(query)和编码器的所有时间步输出(values),计算每个时间步的分数。
计算注意力权重(attention_weights):通过 softmax 函数将分数归一化为权重。
计算上下文向量(context_vector):将注意力权重与编码器的输出相乘并加权求和,得到上下文向量。
2. __init__ 方法
__init__ 方法用于初始化注意力机制的参数。
(1) 参数说明
units:注意力机制的隐藏单元数。这个参数决定了注意力层的内部维度。
(2) 初始化层
self.W1 = tf.keras.layers.Dense(units)
self.W2 = tf.keras.layers.Dense(units)
self.V = tf.keras.layers.Dense(1)
W1:用于对编码器的输出(values)进行变换。
W2:用于对解码器的当前隐藏状态(query)进行变换。
V:用于将加性注意力的输出压缩为一个分数(score)。
3. call 方法
call 方法定义了注意力机制的具体计算过程。
(1) 输入参数
query:解码器的当前隐藏状态,形状为 (batch_size, hidden_size)。
values:编码器的所有时间步输出,形状为 (batch_size, max_length, hidden_size)。
(2) 计算过程
扩展时间维度
hidden_with_time_axis = tf.expand_dims(query, 1)
将 query 的形状从 (batch_size, hidden_size) 扩展为 (batch_size, 1, hidden_size),以便与 values 的时间维度对齐。
计算分数(score)
score = self.V(tf.nn.tanh(self.W1(values) + self.W2(hidden_with_time_axis)))
self.W1(values):对编码器的输出进行变换,形状为 (batch_size, max_length, units)。
self.W2(hidden_with_time_axis):对解码器的当前隐藏状态进行变换,形状为 (batch_size, 1, units)。
tf.nn.tanh(...):将两个变换后的张量相加并通过 tanh 激活函数,形状为 (batch_size, max_length, units)。
self.V(...):将激活后的张量压缩为一个分数,形状为 (batch_size, max_length, 1)。
计算注意力权重(attention_weights)
attention_weights = tf.nn.softmax(score, axis=1)
-
- 使用 softmax 函数将分数归一化为权重,形状为 (batch_size, max_length, 1)。
- 权重表示每个时间步的重要性。
计算上下文向量(context_vector)
context_vector = attention_weights * values
context_vector = tf.reduce_sum(context_vector, axis=1)
将注意力权重与编码器的输出相乘,形状为 (batch_size, max_length, hidden_size)。
沿时间维度(axis=1)加权求和,得到上下文向量,形状为 (batch_size, hidden_size)。
(3) 返回值
- context_vector:上下文向量,包含了当前解码步骤中最相关的输入信息。
- attention_weights:注意力权重,表示每个时间步的重要性。
4. 注意力机制的作用
在机器翻译任务中,注意力机制的作用是让解码器在生成目标序列的每个词时,能够动态地关注输入序列中最相关的部分。具体来说:
动态关注:注意力权重是动态计算的,取决于当前解码器的状态和编码器的输出。
上下文信息:上下文向量将编码器的输出与解码器的当前状态相结合,为解码器提供更丰富的信息。
提高性能:注意力机制可以显著提高翻译的准确性和流畅性,因为它允许模型在生成每个词时只关注最相关的输入部分。
5. 注意事项
units 参数的选择:
units 是注意力机制的隐藏单元数,通常与编码器和解码器的隐藏单元数一致(如 enc_units 或 dec_units)。
如果 units 太小,可能会导致信息丢失;如果太大,可能会增加计算成本。
tf.expand_dims 的作用:
tf.expand_dims 用于扩展张量的维度,以便在计算中对齐形状。例如,将 query 的形状从 (batch_size, hidden_size) 扩展为 (batch_size, 1, hidden_size),使其能够与 values 的时间维度对齐。
softmax 的作用:
softmax 函数将分数归一化为权重,确保所有权重的和为 1。这使得注意力权重表示每个时间步的相对重要性。
tf.reduce_sum 的作用:
tf.reduce_sum 沿指定维度(时间维度)加权求和,得到上下文向量。上下文向量的形状为 (batch_size, hidden_size),表示当前解码步骤的上下文信息。
6. 小结
这个 BahdanauAttention 类实现了 Bahdanau 注意力机制,是机器翻译任务中解码器的关键部分。它通过动态计算注意力权重,将编码器的输出加权求和,得到上下文向量,从而为解码器提供最相关的输入信息。注意力机制不仅提高了翻译的准确性,还使得模型能够更好地处理长序列数据。这个实现简洁而高效,是 Seq2Seq 模型中常用的注意力机制之一。
五,解码器
Decoder
# 步骤 12 定义Decoder
class Decoder(tf.keras.Model):
def __init__(self, vocab_size, embedding_dim, dec_units, batch_sz):
# vocab_size=vocab_tar_size=6082, embedding_dim=256, dec_units=1024, batch_sz=64
super(Decoder, self).__init__()
self.batch_sz = batch_sz
self.dec_units = dec_units
self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
self.gru = tf.keras.layers.GRU(self.dec_units,
return_sequences=True,
return_state=True,
recurrent_initializer='glorot_uniform')
# 输出的维度是目标语言词汇表的大小,返回的是softmax 概率,词汇表中每一个词的概率
self.fc = tf.keras.layers.Dense(vocab_size)
# attention
self.attention = BahdanauAttention(self.dec_units)
def call(self, x, hidden, enc_output):
# This function outputs a result at each timestamp
# 计算decoder 的第一个隐状态和encoder 所有输入之间的attention 权重, 得到上下文向量, context_vector
context_vector, attention_weights = self.attention(hidden, enc_output)
# embedding 后的维度 == (batch_size, 1, embedding_dim)
x = self.embedding(x)
# 把上下文向量context_vector 和 输入embedding 拼接在一起
# context_vector shape == (batch_size, units) -> (64, 1024)
# 拼接后的数据维度 == (batch_size, 1, embedding_dim + hidden_size) -> (64, 1, 1024 + 256)
x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)
# 把拼接后的向量输入gru
# 得到当前时间步的输出和隐状态
(64,1024) # output shape == (batch_size, 1, units) -> (64, 1, 1024), state shape == (batch_size, units) ->
output, state = self.gru(x)
# output shape == (batch_size, hidden_size=1024)
output = tf.reshape(output, (-1, output.shape[2]))
# output shape == (batch_size, vocab) -> (64, 6082)
x = self.fc(output)
return x, state, attention_weights
这个 Decoder 类是机器翻译任务中的解码器部分,其主要作用是基于编码器的输出和当前解码器的状态,逐步生成目标序列(如英文翻译)。解码器结合了注意力机制(BahdanauAttention),能够动态地关注输入序列中最相关的部分,从而提高翻译的质量和流畅性。
1. __init__ 方法
__init__ 方法用于初始化解码器的结构和参数。
(1) 参数说明
- vocab_size:目标语言词汇表的大小,例如英文词汇表的大小。
- embedding_dim:词嵌入的维度,用于将目标语言的词汇索引转换为词向量。
- dec_units:解码器 GRU 的隐藏单元数。
- batch_sz:每批次的样本数量。
(2) 初始化模型组件
self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
self.gru = tf.keras.layers.GRU(dec_units,
return_sequences=True,
return_state=True,
recurrent_initializer='glorot_uniform')
self.fc = tf.keras.layers.Dense(vocab_size)
self.attention = BahdanauAttention(dec_units)
tf.keras.layers.Embedding:将目标语言的词汇索引转换为词向量。
tf.keras.layers.GRU:解码器的核心,用于处理序列数据。return_sequences=True 和 return_state=True 确保返回所有时间步的输出和最后一个时间步的隐藏状态。
tf.keras.layers.Dense:全连接层,将 GRU 的输出映射为目标语言词汇表大小的分布(softmax 概率)。
BahdanauAttention:注意力机制模块,用于计算上下文向量。
2. call 方法
call 方法定义了解码器的前向传播过程
(1) 输入参数
- x:当前时间步的输入,通常是目标语言的词汇索引,形状为 (batch_size, 1)。
- hidden:解码器的当前隐藏状态,形状为 (batch_size, dec_units)。
- enc_output:编码器的所有时间步输出,形状为 (batch_size, max_length, enc_units)。
(2) 计算注意力机制
context_vector, attention_weights = self.attention(hidden, enc_output)
- 使用 BahdanauAttention 模块计算上下文向量(context_vector)和注意力权重(attention_weights)。
- context_vector 的形状为 (batch_size, dec_units),表示当前时间步的上下文信息。
- attention_weights 的形状为 (batch_size, max_length, 1),表示每个时间步的注意力权重。
(3) 词嵌入
x = self.embedding(x)
将输入的词汇索引转换为词向量,形状为 (batch_size, 1, embedding_dim)。
(4) 拼接上下文向量和词嵌入
x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)
将上下文向量(context_vector)和词嵌入(x)拼接在一起。
context_vector 的形状为 (batch_size, dec_units),扩展为 (batch_size, 1, dec_units)。
拼接后的形状为 (batch_size, 1, embedding_dim + dec_units)。
(5) 输入 GRU
output, state = self.gru(x)
将拼接后的向量输入 GRU,得到当前时间步的输出和隐藏状态。
output 的形状为 (batch_size, 1, dec_units)。
state 的形状为 (batch_size, dec_units)。
(6) 重塑输出
output = tf.reshape(output, (-1, output.shape[2]))
将 output 的形状从 (batch_size, 1, dec_units) 重塑为 (batch_size, dec_units)。
(7) 全连接层
x = self.fc(output)
将 GRU 的输出通过全连接层,映射为目标语言词汇表大小的分布(softmax 概率)。
输出的形状为 (batch_size, vocab_size)。
(8) 返回值
- x:当前时间步的目标语言词汇表的分布(softmax 概率)。
- state:解码器的下一个隐藏状态。
- attention_weights:注意力权重,用于可视化注意力机制。
3. 解码器的作用
解码器的主要任务是基于编码器的输出和当前状态,逐步生成目标序列。具体来说:
注意力机制:通过 BahdanauAttention 动态计算上下文向量,关注输入序列中最相关的部分。
词嵌入:将目标语言的词汇索引转换为词向量。
GRU:处理序列数据,生成当前时间步的输出和隐藏状态。
全连接层:将 GRU 的输出映射为目标语言词汇表的分布,生成下一个词的概率。
4. 注意事项
输入形状:
解码器的输入 x 是目标语言的词汇索引,形状为 (batch_size, 1)。这是因为解码器是逐步生成目标序列的,每次只处理一个时间步。
hidden 是解码器的当前隐藏状态,形状为 (batch_size, dec_units)。
enc_output 是编码器的所有时间步输出,形状为 (batch_size, max_length, enc_units)。
拼接操作:
解码器将上下文向量和词嵌入拼接在一起,形状为 (batch_size, 1, embedding_dim + dec_units)。这种拼接方式使得解码器能够同时利用编码器的上下文信息和当前输入的信息。
GRU 的输出:
GRU 的输出 output 的形状为 (batch_size, 1, dec_units),需要通过 tf.reshape 重塑为 (batch_size, dec_units),以便输入全连接层。
全连接层的输出:
全连接层的输出是目标语言词汇表的分布,形状为 (batch_size, vocab_size)。这个分布可以用于选择下一个词。
注意力权重:
注意力权重的形状为 (batch_size, max_length, 1),表示每个时间步的注意力权重。这些权重可以用于可视化注意力机制,帮助理解模型的行为。
5. 小结
这个 Decoder 类是机器翻译任务中的关键部分,结合了注意力机制和 GRU,能够动态地生成目标序列。解码器通过以下步骤实现:
使用注意力机制计算上下文向量;
将上下文向量与当前输入的词嵌入拼接;
使用 GRU 处理拼接后的向量,生成当前时间步的输出和隐藏状态;
通过全连接层将输出映射为目标语言词汇表的分布。