Press "Enter" to skip to content

BERT模型入门系列(二): Attention模型实现

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

概述:

 

在上一篇文章《 BERT模型入门系列: Attention机制入门 》里面,用了机器翻译的例子把 Encoder-Decoder 模型、以及 Attention模型 的基本原理进行了讲解,这篇配合上一篇文章的讲解,把涉及到的模型进行实现并且详细得讲解,利于我们进一步加强理解。文章的内容较多,但实际上原理还是比较简单的,例子基于python、tensorflow2.4,即便都没有学过也没有关系,我们在代码里面,关键得地方都进行了详细讲解,配合tensorflow得官方文档,理解起来完全没有问题。

 

在开始之前,先列出具体的实现顺序,带着明确的目标,更利于学习:

 

 

    1. 文本预处理

 

    1. Encoder实现

 

    1. Decoder实现

 

    1. Attention模型实现

 

    1. 模型训练

 

    1. 英文->中文翻译

 

 

文章涉及到的代码已经提交到 github.com/rotbit/nmt.… ,但这个代码的目的不是要做一个可用的商业化产品,所以并不追求实际的最优的效果。如果能够帮助我们能够更进一步理解Encoder、Decoder、attention模型,就达到了预期目的了。

 

1、文本预处理

 

文本处理的事情呢,其实非常简单,就是把我们的句子转换成一个数字表示的向量,比如“你吃饭了嘛?”转换成一个向量“[2,543,56,12,76]”。这幺处理的原因是因为计算机不识字,只认识1010,所以要转成计算机看得懂的信息。具体要在怎幺做呢,先上个流程图。

 

 

流程挺简单,说起来就主要几个步骤,读入文件、文本预处理、构造词典、文本转为向量。万丈高楼平地起,我们这就从最基础的函数开始看。

 

seg_char: 中文按字拆分

 

# 把句子按字分开,不破坏英文结构 
# 例如: "我爱tensorflow" -> "['我', '爱', 'tenforflow']"
def seg_char(sent):
    # 首先分割 英文 以及英文和标点
    pattern_char_1 = re.compile(r'([\W])')
    parts = pattern_char_1.split(sent)
    parts = [p for p in parts if len(p.strip())>0]
    # 分割中文
    pattern = re.compile(r'([\u4e00-\u9fa5])')
    chars = pattern.split(sent)
    chars = [w for w in chars if len(w.strip())>0]
    return chars

 

上面这个我们只需要知道输入是什幺,输出是什幺样子的就可以了。

 

preprocess_sentence: 句子预处理

 

# 文本预处理,用空格按字拆分文本
# w 需进行处理的文本
# type 文本类型 0:英文  1:中文
def preprocess_sentence(w, type):
    if type == 0:
        w = re.sub(r"([?.!,¿])", r" \1 ", w)
        w = re.sub(r'[" "]+', " ", w)
    if type == 1:
        #seg_list = jieba.cut(w)
        seg_list = seg_char(w)
        w = " ".join(seg_list)
    w = '<start> ' + w + ' <end>'
    return w

 

我们来运行一下,看看输入和输出是什幺

 

en = "I love tensorflow."
pre_en = preprocess_sentence(en, 0)
print("pre_en=", pre_en)
cn = "我爱tenforflow"
pre_cn = preprocess_sentence(cn, 1)
print("pre_cn=", pre_cn)

 

输出:

 

pre_en= <start> I love tensorflow .  <end>
pre_cn= <start> 我 爱 tenforflow <end>

 

来看看这输出,我们输入的 文本都被空格分开 了,并且在 首尾分别加上了、 ,在 首位加标识符是用来在后面的模型训练中标志文本的开始和结束 。

 

create_dataset: 文本加载、预处理

 

# path 数据存储路径
# num_examples 读入记录条数
# 加载文本
def create_dataset(path, num_examples):
    lines = io.open(path, encoding='UTF-8').read().strip().split('\n')
    # 英文文本
    english_words = []
    # 中文文本
    chinese_words = []
    for l in lines[:num_examples]:
        word_arrs = l.split('\t')
        if len(word_arrs) < 2:
            continue
        english_w = preprocess_sentence(word_arrs[0], 0)
        chinese_w = preprocess_sentence(word_arrs[1], 1)
        english_words.append(english_w)
        chinese_words.append(chinese_w)
    # 返回[('<start> Hi .  <end>', '<start> 嗨 。 <end>')]
    return english_words, chinese_words

 

用到的数据集可以可以从这里下载 cmnt.txt 我们抽几条数据集里面的数据看看。数据集一行就是一个样本。可以看到会被分为三列,第一列是英文,第二列是英文对应的中文翻译,第三列我们不需要,直接丢掉就行了。 create_dataset的功能就是读入这样的文本,处理之后分别返回处理之后的中英文列表。

 

Hi.	嗨。	CC-BY 2.0 (France) Attribution: tatoeba.org #538123 (CM) & #891077 (Martha)
Hi.	你好。	CC-BY 2.0 (France) Attribution: tatoeba.org #538123 (CM) & #4857568 (musclegirlxyp)
Run.	你用跑的。	CC-BY 2.0 (France) Attribution: tatoeba.org #4008918 (JSakuragi) & #3748344 (egg0073)
Wait!	等等!	CC-BY 2.0 (France) Attribution: tatoeba.org #1744314 (belgavox) & #4970122 (wzhd)

 

老规矩,照样跑一下这个代码,看看他到底输出的东西张啥样。

 

# 从cmn.txt读入4条记录
inp_lang, targ_lang = create_dataset('cmn.txt', 4)
print("inp_lang={}, targ_lang={}".format(inp_lang, targ_lang))

 

输出结果: 可以看到,输出的中英文是分开的两个列表,两个列表中英文翻译是根据下标一一对应的,比如,inp_lang[0]=’ Hi . ‘,对应的中文翻译是targ_lang=’ 嗨 。 ‘

 

inp_lang=[
    '<start> Hi .  <end>',
    '<start> Hi .  <end>',
    '<start> Run .  <end>',
    '<start> Wait !  <end>'
],
targ_lang=[
    '<start> 嗨 。 <end>',
    '<start> 你 好 。 <end>', 
    '<start> 你 用 跑 的 。 <end>',
    '<start> 等 等 ! <end>'
]

 

load_dataset、tokenize: 创建字典、文本转向量

 

# # 文本内容转向量
def tokenize(lang):
    lang_tokenizer = tf.keras.preprocessing.text.Tokenizer(filters='')
    lang_tokenizer.fit_on_texts(lang)
    tensor = lang_tokenizer.texts_to_sequences(lang)
    tensor = tf.keras.preprocessing.sequence.pad_sequences(tensor,
                                                           padding='post')
    return tensor, lang_tokenizer
def load_dataset(path, num_examples=None):
    inp_lang, targ_lang = create_dataset(path, num_examples)
    input_tensor, inp_lang_tokenizer = tokenize(inp_lang)
    target_tensor, targ_lang_tokenizer = tokenize(targ_lang)
    return input_tensor, target_tensor, inp_lang_tokenizer, targ_lang_tokenizer

 

运行一下load_dataset:

 

inp_tensor, targ_tensor, inp_tokenizer, targ_tokenizer = load_dataset("cmn.txt", 4)
print("inp_tensor={}, inp_tokenizer={}".format(input_tensor, inp_lang_tokenizer.index_word))

 

来看一下输出结果

 

inp_tensor=[[1 4 3 2]
 [1 4 3 2]
 [1 5 3 2]
 [1 6 7 2]], 
inp_tokenizer={1: '<start>', 2: '<end>', 3: '.', 4: 'hi', 5: 'run', 6: 'wait', 7: '!'}

 

inp_tokenizer是构造的词典库,构造的方式是给每个词分配一个唯一的整数id, inp_tensor是文本转向量的结果,向量里的每个元素对应到词典库的单词。

 

文本预处理的工作到这里就结束了。

 

2、Encoder实现:

 

Encoder的作用在《 BERT模型入门系列: Attention机制入门 》已经介绍过了,在这儿就不多介绍了。在我们下面的代码实现里,Encoder由两部分组成: Embeding层、RNN层。先上代码看看。

 

import tensorflow as tf
# encoder
class Encoder(tf.keras.Model):
     # vocab_size: 词典表大小
     # embedding_dim:词嵌入维度 
     # enc_uints: 编码RNN节点数量 
     # batch_sz 批大小
    def __init__(self, vocab_size, embedding_dim, enc_units, batch_sz):
        super(Encoder, self).__init__()
        self.batch_sz = batch_sz # 批大小
        self.enc_units = enc_units # 编码单元个数(RNN单元个数)
        # Embedding 把一个整数转为一个固定长度的稠密向量
        self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
        # 创建一个rnn层
        self.rnn = tf.keras.layers.SimpleRNN(self.enc_units,
                                       return_sequences=True,
                                       return_state=True)
    def call(self, x, hidden):
        x = self.embedding(x)
        output, state = self.rnn(x, initial_state=hidden)
        return output, state
    # 张量的概念 tf.Tensor https://www.tensorflow.org/guide/tensor
    def initialize_hidden_state(self):
        return tf.zeros((self.batch_sz, self.enc_units))

 

我们来解析一下参数的含义

 

__init__函数的参数含义:

 

vocab_size: 词典表大小, 词典表的大小指的是词典表里面有多少个不重复的词语,这个词典是我们调用load_dataset函数构造的。

 

embedding_dim:词嵌入维度, 在前面已经说过,我们会用一个数字表示每个单词,这样我们的句子可以编码成一个密集向量,但这种编码方式存在缺陷,不能捕获两个单词之间的关联性。所以,我们的输入的数据在用整数编码成密集向量后,还会经过一个Embedding层,重新编码成一个固定长度的稠密向量,embedding_dim指的就是经过Embedding层编码后的向量的维度。有关为何通过整数编码后还要进行Embedding,可以参考 字嵌入

 

enc_uints: 编码RNN的输出节点,我们这个例子只用了一层RNN,但是实际上也是可以设置为多层RNN的,enc_uint指的是最后一层输出层的节点数

 

batch_sz:批大小, 深度学习里面,每一次参数的更新所计算的损失函数并不是仅仅由一个{data:label} 所计算的,而是由一组{data:label} 加权得到的,这组数据的大小就是batch_size

 

Encoder除了初始化的_init_函数外,还有一个call函数,call函数是真正执行编码动作的逻辑,我们来看看call函数的具体参数解析

 

call函数的参数含义、输出含义:

 

x: 训练样本,即向量化后的文本,load_dataset返回的处理之后的数据。 是 BATCH_SIZE * 样本长度 的矩阵,即 x是BATCH_SIZE个样本数据。

 

hidden:BATCH_SIZE * enc_units 的矩阵。 循环神经网络的隐藏层的值不仅仅取决于当前这次的输入x,还取决于上一次隐藏层的值hidden,所以,需要输入上一个输入的隐藏值hidden 。此处,调用call函数时候是初始状态,所以我们只需要给一个初始值就可以了。

 

这里问题来了,为什幺hidden是BATCH_SIZE * enc_uint的矩阵呢?

 

简单来说说,训练模型的时候,我们输入的是BATCH个样本,其次,我们的RNN定义的是enc_uints个神经元,换句话说就是对于每一个单词的输入,都会有enc_uints个神经元输出值。因此, 我们的RNN输出的隐藏层是 BATCH_SIZE * word_size* enc_uints _,_这里word_size是一个样本中的单词的数量。

 

所以,对于我们的初始值来说,我们只需要输入BATCH_SIZE个样本,样本中的单词数量都为1即可,即 call函数的hidden参数是BATCH_SIZE * 1* enc_uints 的矩阵

 

输出: BATCH_SIZE * word_size * enc_uints,其中 word_size是一个样本中的单词的数量

 

理解输入、输出对于理解代码很有帮助,上面扯了那幺多,这儿来一幅图总结一下。

 

 

Encoder数据流程图

 

看了上面的解析,相信对于数据输入输出都有了一定的了解了, 我们直接运行,看看代码的输出结果。

 

# 加载样本数据
input_tensor, target_tensor, inp_lang, targ_lang=preprocess.load_dataset("./cmn.txt", 30000)
# 采用80-20的比例切分训练集和验证集
input_tensor_train, input_tensor_val, target_tensor_train, \
    target_tensor_val = train_test_split(input_tensor, target_tensor, test_size=0.2)
# 创建一个 tf.data 数据集
BUFFER_SIZE = len(input_tensor_train)
BATCH_SIZE = 32
steps_per_epoch = len(input_tensor_train)//BATCH_SIZE
embedding_dim = 256 # embedding维度
units = 512
vocab_inp_size = len(inp_lang.word_index)+1
vocab_tar_size = len(targ_lang.word_index)+1
dataset = tf.data.Dataset.from_tensor_slices((input_tensor_train, target_tensor_train))
dataset = dataset.batch(BATCH_SIZE, drop_remainder=True)
# 调用encoder
encoder = encoder.Encoder(vocab_inp_size, embedding_dim, units, BATCH_SIZE)
# 初始化一个隐藏状态
sample_hidden = encoder.initialize_hidden_state()
# 执行编码
sample_output, sample_hidden = encoder(example_input_batch, sample_hidden)
print ('output shape:(batch size, sequence length, units){}'.format(sample_output.shape))
print ('Hidden state shape: (batch size, units) {}'.format(sample_hidden.shape))

 

Encoder输出结果:

 

output shape: (batch size, sequence length, units) (32, 36, 512)
Hidden state shape: (batch size, units) (32, 512)

 

Encoder的实现讲解到这儿就结束了,不过。。。我们的内容还没有结束。

 

我们这儿开始来聊聊Decoder的实现,Decoder的作用就是把Encoder编码之后的文本翻译成目标文本,嗯,对,功能就是那幺简单,Decoder我们也是用一个RNN实现,废话不多说了,先看看代码。

 

 

3、Decoder实现

 

import tensorflow as tf
import attention
class Decoder(tf.keras.Model):
    # vocab_size 词典大小
    # embedding_dim 词嵌入维度
    # dec_uints 解码RNN输出神经元数
    # batch_sz 批大小
    def __init__(self, vocab_size, embedding_dim, dec_units, batch_sz, attention):
        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.rnn = tf.keras.layers.SimpleRNN(self.dec_units,
                                       return_sequences=True,
                                       return_state=True)
        self.fc = tf.keras.layers.Dense(vocab_size)
        self.attention = attention
    # x 是输出目标词语[教师强制](这儿是个整数,是单词在词表中的index)
    def call(self, x, hidden, enc_output):
        # 编码器输出 (enc_output) 的形状 == (批大小,最大长度,隐藏层大小)
        # context_vector 的shape == (批大小,隐藏层大小)
        # attention_weight == (批大小,最大长度, 1)
        context_vector, attention_weights = self.attention(hidden, enc_output)
        #print("context_vector.shape={}".format(context_vector.shape))
        # x 在通过嵌入层后的形状 == (批大小,1,嵌入维度)
        x = self.embedding(x)
        # x 在拼接 (concatenation) 后的形状 == (批大小,1,嵌入维度 + 隐藏层大小)[特征拼接]
        x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)
        #print("x.shape={}".format(x.shape))
        # 将合并后的向量传送到 RNN, rnn需要的shape是(batch_size, time_step, feature)
        output, state = self.rnn(x)
        #print("output 1.shape={}".format(output.shape))
        # 输出的形状 == (批大小 * 1,隐藏层大小)
        # 将合并后的向量传送到 RNN, rnn需要的shape是(batch_size, time_step, feature),time_step这个维度没什幺意义,
        # 在全连接层可以去掉,这里去掉
        output = tf.reshape(output, (-1, output.shape[2]))
        # 输出的形状 == (批大小,vocab),输出所有单词概率
        x = self.fc(output)
        return x, state, attention_weights

 

Decoder的参数我们也解析一下

 

call函数参数解析

 

x:上一个输入得到的翻译结果,例如: “machine learning”=>”机器学习”,

 

1、若当前待翻译的是”machine”,那幺这里 x 是一个标识符””,

 

2、若当前待翻译的是”learning”,那幺此处x是“机器”。

 

这种将前一个输入的输出结果作为当前输入的特征进行训练的方法叫教师强制,是一种快速有效地训练循环神经网络模型的方法,感兴趣的同学请移步到 《 Professor Forcing: A New Algorithm for Training Recurrent Networks

 

hidden: Encoder返回的隐藏层状态, hidden的shape是BATCH_SIZE * enc_uints。

 

enc_output: Encoder的编码结果 , shape是 BATCH_SIZE * word**_**size * enc_uints。

 

Decoder还有一个attention参数,这个就是计算attention的函数,这儿当作是个参数传进来。attention的计算方法在《 BERT模型入门系列: Attention机制入门 》已经讲过,这里不多说了,我们以点积(dot product)的方式实现计算attention的计算方法。

 

4、Attention模型实现:

 

class DotProductAttention(tf.keras.layers.Layer):
  def __init__(self):
    super(DotProductAttention, self).__init__()
  def call(self, query, value):
    # 32 * 512 * 1
    hidden = tf.expand_dims(query, -1)
    # 计算点积
    score = tf.matmul(value, hidden)
    attention_weights = tf.nn.softmax(score, axis=1)
    context_vector = attention_weights * value
    # 求和
    context_vector = tf.reduce_sum(context_vector, axis=1)
    return context_vector, attention_weights

 

 

主要部分都定义完了,我们这就来运行一下

 

import tensorflow as tf
import decoder
import attention
import encoder
import preprocess
# 加载、预处理数据
input_tensor, target_tensor, inp_lang, targ_lang = preprocess.load_dataset("./cmn.txt", 30000)

# 公共参数定义
BUFFER_SIZE = len(input_tensor)
BATCH_SIZE = 32
steps_per_epoch = len(input_tensor)//BATCH_SIZE
embedding_dim = 256 # 词向量维度
units = 512
vocab_inp_size = len(inp_lang.word_index)+1
vocab_tar_size = len(targ_lang.word_index)+1
# 数据集
dataset = tf.data.Dataset.from_tensor_slices((input_tensor, target_tensor)).shuffle(BUFFER_SIZE)
dataset = dataset.batch(BATCH_SIZE, drop_remainder=True)

# 定义encoder
encoder = encoder.Encoder(vocab_inp_size, embedding_dim, units, BATCH_SIZE)
sample_hidden = encoder.initialize_hidden_state()
sample_output, sample_hidden = encoder(example_input_batch, sample_hidden)
print ('encoder output shape: (batch size, sequence length, units) {}'.format(sample_output.shape))
print ('encoder Hidden state shape: (batch size, units) {}'.format(sample_hidden.shape))
# 定义注意力
attention_layer = attention.DotProductAttention()
context_vector, attention_weights = attention_layer(sample_hidden, sample_output)
print ('context_vector shape:  {}'.format(context_vector.shape))
print ('attention_weights state: {}'.format(attention_weights.shape))
# 定义decoder
dec_input = tf.expand_dims([targ_lang.word_index['<start>']] * BATCH_SIZE, 1)
decoder = decoder.Decoder(vocab_tar_size, embedding_dim, units, BATCH_SIZE, attention_layer)
dec_output, dec_state, attention_weights = decoder(dec_input, sample_hidden, sample_output)
print ('decoder shape: (batch size, sequence length, units) {}'.format(dec_output.shape))
print ('decoder Hidden state shape: (batch size, units) {}'.format(dec_state.shape))

 

5、模型训练:

 

Encoder和Decoder、Attention我们都已经实现了,接下来就可以开始定义模型训练的步骤。在我们的数据预处理步骤中,我们已经用

 

dataset.batch(BATCH_SIZE, drop_remainder=True)

 

按照BATCH_SIZE大小对训练数据进行整理,所以我们每个训练的最小单元是BATCH_SIZE大小的数据集。来看看具体的训练步骤

 

单个BATCH训练

 

import tensorflow as tf
import optimizer
# 单个样本的模型训练
# encoder 定义好的encoder模型
# decoder 定义好的decoder模型
# inp 训练数据,待翻译文本的张量
# targ 训练据, 目标文本的张量
# targ_lang 目标文本的词典
# enc_hidden encoder返回的隐藏层状态
def train_step(encoder, decoder, inp, targ, targ_lang, enc_hidden, BATCH_SIZE):
  loss = 0
  with tf.GradientTape() as tape:
    enc_output, enc_hidden = encoder(inp, enc_hidden)
    dec_hidden = enc_hidden
    dec_input = tf.expand_dims([targ_lang.word_index['<start>']] * BATCH_SIZE, 1)
    # 以文本长度为主,遍历所有词语
    for t in range(1, targ.shape[1]):
      # 将编码器输出 (enc_output) 传送至解码器
      predictions, dec_hidden, _ = decoder(dec_input, dec_hidden, enc_output)
      # 这里输入的是一个batch
      loss += optimizer.loss_function(targ[:, t], predictions)
      # 教师强制 - 将目标词作为下一个输入,一个batch的循环
      dec_input = tf.expand_dims(targ[:, t], 1)
  batch_loss = (loss / int(targ.shape[1]))
  variables = encoder.trainable_variables + decoder.trainable_variables
  gradients = tape.gradient(loss, variables)
  optimizer.optimizer.apply_gradients(zip(gradients, variables))
  return batch_loss

 

总体训练流程:

 

# 模型训练
def train(epochs):
    EPOCHS = epochs
    for epoch in range(EPOCHS):
        enc_hidden = encoder.initialize_hidden_state()
        total_loss = 0
        # dataset最多有steps_per_epoch个元素
        for (batch, (inp, targ)) in enumerate(dataset.take(len(input_tensor))):
            batch_loss = train_function.train_step(encoder, decoder, inp, targ, targ_lang, enc_hidden, BATCH_SIZE)
            total_loss += batch_loss
            if batch % 100 == 0:
                print('Epoch {} Batch {} Loss {:.4f}'.format(epoch + 1,
                                                             batch,
                                                             batch_loss.numpy()))

 

6、英文->中文翻译

 

# 预测目标解码词语
def evaluate(sentence):
    sentence = preprocess.preprocess_sentence(sentence, 0)
    inputs = [inp_lang.word_index[i] for i in sentence.split(' ')]
    inputs = tf.keras.preprocessing.sequence.pad_sequences([inputs],
                                                           maxlen=max_length_inp,
                                                           padding='post')
    inputs = tf.convert_to_tensor(inputs)
    result = ''
    hidden = [tf.zeros((1, units))]
    enc_out, enc_hidden = encoder(inputs, hidden)
    dec_hidden = enc_hidden
    dec_input = tf.expand_dims([targ_lang.word_index['<start>']], 0)
    # max_length_targ 解码张量的最大长度
    for t in range(max_length_targ):
        predictions, dec_hidden, attention_weights = decoder(dec_input,
                                                             dec_hidden,
                                                             enc_out)
        tf.reshape(attention_weights, (-1, ))
        predicted_id = tf.argmax(predictions[0]).numpy()
        result += targ_lang.index_word[predicted_id] + ' '
        if targ_lang.index_word[predicted_id] == '<end>':
            return result, sentence
        # 预测的 ID 被输送回模型
        dec_input = tf.expand_dims([predicted_id], 0)
    return result, sentence
# 翻译
def translate(sentence):
    result, sentence = evaluate(sentence)
    print('Input: %s' % (sentence))
    print('Predicted translation: {}'.format(result))

 

运行一下:

 

train(20)
translate("hello")
translate("he is swimming in the river")

 

输出结果

 

Epoch 20 Batch 300 Loss 0.5712
Epoch 20 Batch 400 Loss 0.4970
Epoch 20 Batch 500 Loss 0.5692
Epoch 20 Batch 600 Loss 0.6004
Epoch 20 Batch 700 Loss 0.6078
Input: <start> hello <end>
Predicted translation: 你 好 。 <end> 
Input: <start> he is swimming in the river<end>
Predicted translation: 我  <end>

 

 

总算结束了,这个例子跑出来的效果不是很好,估计有以下几个原因

 

1、数据量不足,数据集只有3000多条。

 

2、训练次数不足,进一步优化增加迭代次数

 

3、Attention模型仍有有优化的空间、只用了点积的注意力计算方式,仍有效果更好的计算方式

 

4、使用的是RNN模型,也可替换为lstm、gru等神经网络进行调试

 

参考:

 

www.tensorflow.org/tutorials/t…

 

github.com/rotbit/nmt.…

Be First to Comment

发表评论

邮箱地址不会被公开。 必填项已用*标注