赞
踩
(个人在学习过程中一些整理总结,有些的不清楚的错误的,请及时在评论区指出,谢谢! 算法实现的完整代码见 github链接完整代码)
2017 年,Google 在论文 “Attention is All you need” 中提出了 Transformer 模型,其使用 Self-Attention 结构取代了在 NLP 任务中常用的 RNN 和LSTM网络结构。相比 RNN 网络结构,其最大的优点是可以并行计算。transformer基本模型图如下图所示。模型的左半边Encoder部分可以看作是一个编码器,右半边Decoder部分可以看作是一个解码器,其中编码器是双向的,解码器是单向的需要循环迭代输出。(这里不懂,没关系后文还有讲解)而著名的两个模型,Bert(Bidirectional Encoder Representations from Transformers 双向)是基于其Encoder部分的,ChatGpt(Generative Pre-trained Transformer 单向)所使用的Gpt模型是基于其Decoder部分。
下面这个图是每篇Transformer讲解文章中必会出现的图片。来源于Attention is All you need
这篇论文里面的。
Transformer源自于AI自然语言处理任务NLP;在计算机视觉领域CV,近年来Transformer逐渐替代CNN成为一个热门的研究方向。此外,Transformer在文本、语音、视频等多模态领域也在崭露头角,可见Transformer模型的重要之处。
Transformer和LSTM的最大区别,就是LSTM的训练是迭代的、串行的,必须要等当前字处理完,才可以处理下一个字。而Transformer的训练时并行的,即所有字是同时训练的,这样就大大增加了计算效率。Transformer使用了位置嵌入(Positional Encoding)来理解语言的顺序,使用自注意力机制(Self Attention Mechanism)
和前馈全连接层进行计算,这是transformer的创新点。
有对自注意力机制和位置编码不熟悉的UU可以看下这边文章 Transformer前置知识
Encoder编码器主要由以下几个部分组成:
下面开始正式逐部分讲解Encoder的各部分。
大家时刻要记住,计算机在处理任何数据的基础,就是能够进行运算,为了能够把文本数据或音频数据输入进Transformer这个黑盒子里面处理,我们必须先通过Embedding将其转换成编码标识,使其成为计算机能够处理的数字。
在理解Embedding层之前,我们需要关注于文本数据的特点,举例:“我爱北京天安门”,我们在做文本数据处理得时候,传统的汉字是不能给计算机做运算的,为了解决汉字运算的问题,提出了将汉字进行编码(或叫文本张量表示)的思想。目前主流的编码方式有one-hot
编码及word Embedding
。下面就两种编码进行介绍,其中穿插关于word2vec
的理解。
该编码格式较为傻瓜式,就是将词库中所有的单词,从[0,max_len-1]
的进行编号,使用哪个词对应的编号位置部分置1,其余部分置0。举例说明:如果整个词库为“我爱北京天安门”这七个字,依次对其进行编号,便会得到如下的表示:
我:1000 000 爱:0100 000 北:0010 000 京:0001 000 天:0000 100
安:0000 010 门:0000 001
word2vec
从字面意思能够看出来,即word to vec
文本推导张量,它存在两个模式,一个是CBOW,一个是skipgram,其中CBOW 类似于我们在英文考试中的完形填空,即根据上下文推导中间的单词。而skipgram与之相反,通过某个单词,推导上下文,其中Skip-gram的计算机复杂度是比CBOW的复杂度要大的,如下左图是CBOW的工作模式,右图是Skipgram的工作方式。(二者的详细比较可参考这篇文章cbow 与 skip-gram的比较)
下面再举个简单实际的例子:
Transformer的输入部分其实就是词向量
和 位置向量
的叠加:比如下面一句话"我有一只猫",经过分词之后的得到的token
是我
有
一只
猫
,那么我
,是
,一只
,猫
的 词向量
和 位置向量
的叠加后的向量再放在一起组成的矩阵就是这句话的词嵌入
。
相信到这里大家已经明白怎么处理输入了,那我接下来我们继续看Encoder的其他部分
在拿到一个Embedding的向量后,我们下一步要做的事就是利用Self-Attention机制去计算不同单词之间词向量的相似度,我们先从一个简单的例子入手开始分析。
计算 Self-Attention 的步骤如下:
第 1 步:对编码器的每个输入向量(在本例中,即每个词的词向量)创建三个向量:Query 向量、Key 向量和 Value 向量。它们是通过词向量分别和 3 个权重矩阵相乘得到的,这 3 个矩阵通过训练获得。
请注意,这些向量的维数小于词向量的维数。新向量的维数为 64,而 embedding 和编码器输入/输出向量的维数为 512。新向量不一定非要更小,这是为了使多头注意力计算保持一致的结构性选择。
第 2 步: 计算注意力分数。假设我们正在计算这个例子中第一个词 “Thinking” 的自注意力。我们需要根据 “Thinking” 这个词,对句子中的每个词都计算一个分数。这些分数决定了我们在编码 “Thinking” 这个词时,需要对句子中其他位置的每个词放置多少的注意力,也就是最后的综合权重。
这些分数,是通过计算 “Thinking” 的 Query 向量和需要评分的词的 Key 向量的点积得到的。如果我们计算句子中第一个位置词的注意力分数,则第一个分数是q1和k1的点乘 ,第二个分数是q1和k2的点乘。
第 3 步:将每个分数除以 d k \sqrt { d _ { k }} dk , d k d _ { k } dk是 Key 向量的维度。目的是在反向传播时,求梯度更加稳定。类似于一个归一化处理,可以避免远距离传播导致的梯度爆炸和梯度消失问题,使梯度更加稳定。
第 4 步:将这些分数进行 Softmax 操作。Softmax 将分数进行归一化处理,使得它们都为正数并且和为 1。也就是计算各个词的权重大小。
这些Softmax
分数决定了在编码当前位置的词时,对所有位置的词分别有多少的注意力。很明显,当前位置的词汇有最高的分数,有时注意一下与当前位置的词相关的词是很有用的。比如一句话Luck has her favorite food,her
和Luck
很明显是相关性较大的,那么在Self-Attention
中体现的就是算出来的Score权重较大。
第 5 步:将每个 Softmax 分数分别与每个 Value 向量相乘。这种做法背后的直觉理解是:对于分数高的位置,相乘后的值就越大,我们把更多的注意力放在它们身上;对于分数低的位置,相乘后的值就越小,这些位置的词可能是相关性不大,我们就可以忽略这些位置的词。
第 6 步:将加权 Value 向量(即上一步求得的向量)对应求和(即几个向量相同位置上的数字对应求和)。这样就得到了Thinking,这一个单词的自注意力层在这个位置的输出。具体计算如下图所示,最终的 Sum=
V
1
×
0.88
+
V
2
×
0.12
V _ { 1 } \times 0.88 + V _ { 2 } \times 0.12
V1×0.88+V2×0.12 `
上述过程是计算两个单词中,一个单词的Self-Attention,但是显然一个我们输入到模型中进行计算的一般都是矩阵,是很多个句子中的单词同步进行计算的。在实际实现中,此计算是以矩阵形式进行,以便实现更快的处理速度。下面我们来看看如何使用矩阵计算。
主要分为三步:
1 : 将所有两个单词的词向量合成一个矩阵并且与随机初始化的Q1 K1 V1参数矩阵相乘
得到Q K V矩阵
(参数矩阵初始化是随机的,但是后续随着梯度下降会不断调整)
2:将Q矩阵和K矩阵的转置矩阵
相乘(就是上方单独计算时 q 1 × k 1 q _ { 1 } \times k _ { 1 } q1×k1, q 1 × k 2 q _ { 1 } \times k _ { 2 } q1×k2 的过程,换成了矩阵形式表示)
3:经过Softmax
后再与原始的词向量矩阵相乘,得到最终的自注意力层输出。
用通俗的话来讲就是用Key,Query产生一个权重矩阵进而与Value相乘得到最终的输出,这也就是缩放点积注意力的过程(Scale Dot-Product Attention),过程也可如图所示,与前面讲解的是相同的。
简单总结一下 :对于 Self Attention 来讲,Q(Query),K(Key)和 V(Value)三个矩阵均来自同一输入,并按照以下步骤计算:
1.首先计算Q和K之间的点积,为了防止其结果过大,会除以 d k \sqrt { d _ { k }} dk ,其中 d k d _ { k } dk为Key向量的维度。
⒉.然后利用Softmax操作将其结果归一化为概率分布,再乘以矩阵V就得到权重求和的表示。
Attention(Q,K,V) = Softmax( Q K T d k \frac { Q K ^ { T } } { \sqrt { d _ { k } } } dk QKT) V
Transformer里用到的是多头注意力。多头的意思是,`有多组不同的 w Q , w k , w ν w ^ { Q } , w ^ { k } , w ^ { \nu } wQ,wk,wν权重矩阵。把同一个句子利用多组不同的Q、K、V权重矩阵相乘,最后把得到的8个Z0~Z7对位相加(或者拼接起来再乘一个权重矩阵W^{0} 可以恢复到原来embedding_size的大小维度)得到最终的Z,这样可以捕捉到更多的特征信息。
具体计算过程如下图所示:
多头注意力,与CNN卷积神经网络中的多通道
十分类似,每个头都可能提取到不同的特征,每个头的关注点,侧重点也有所不同。
现在让我们看一个多头Multi-Head
的例子,看看在对示例句中的“it”进行编码时,不同的注意力头关注的位置分别关注什么内容,下图也是十分经典的一张图。
在上图中
当我们对“it”进行编码时,一个注意力头关注“The animal”,另一个注意力头关注“tired”。从某种意义上来说,模型对“it”的表示,融入了“animal”和“tired”的部分表达。
Multi-head Attention 的本质是,在参数总量保持不变的情况下,将同样的 Query,Key,Value 映射到原来的高维空间的不同子空间中进行 Attention 的计算,在最后一步再合并不同子空间中的 Attention 信息。这样降低了计算每个 head 的 Attention 时每个向量的维度,在某种意义上防止了过拟合;由于 Attention 在不同子空间中有不同的分布,Multi-head Attention 实际上是寻找了序列之间不同角度的关联关系,并在最后拼接这一步骤中,将不同子空间中捕获到的关联关系再综合起来。也可以类比CNN中同时使用多个卷积核的作用,直观上讲,多头的注意力有助于网络捕捉到更丰富的特征/信息。
计算完Attention后,我们继续前向传播,下一步是将Self-Attention得到的向量和没进行处理的向量相加,即残差连接,具体过程如下图。
把得到的两个词的Attention值摞在一起后,将“加入位置编码后的词向量
(原来的向量)”与“摞在一起的Attention值
(计算出来的向量)” 相加
。残差连接减小了梯度消失的影响。加入残差连接,就能保证层次很深的模型不会出现梯度消失的现象。残差块是深度学习必学的基础内容,这里写的较为简单,想深入了解得可以搜索ResNet相关知识点看看。
上面的操作也适用于解码器的子层。假设一个 Transformer 是由 2 层编码器和 2 层解码器组成,其如下图所示:
为了方便进行残差连接,编码器和解码器中的所有子层和嵌入层的输出维度需要保持一致,在 Transformer 论文中
d
model
d _ { \text { model } }
d model = 512,也就是在每层输出的第四个维度都是512.
Layer Normalization的作用是把神经网络中隐藏层归一为标准正态分布,也就是i.j.d独立同分布,以起到加快训练速度,加速收敛的作用,也有利于模型更好的训练。
LN即层标准化,也是一种归一化方法,它带来的优势有:
在此在特意强调一下BN和LN的不同之处:
BN是批标准化,LN是层标准化,BN求标准化的均值和方差与该批次所有的向量有关(
比如计算第一个位置的均值和标准差,那么要用到该批次所有向量第一位置的值),LN求标准化的均值和方差只与自己的向量有关(用一个向量所有位置的值来求均值方差)。从图上也可知,BN是横向的,LN是纵向的。二者都是有利于模型的收敛,但是不同场景用的方法也不同。
NLP的文本本质上可以看成一个时间序列,而时间序列是不定长的,长度不同的序列原则上属于不同的统计对象,所以很难得到稳定的统计量,而得不到稳定的统计量,BN就无法成立了(因为BN依靠滑动平均来获得一组预测用的统计量),也就是横向求解均值和方差的时候,有些向量上可能因为句长较短而导致后面的部分没有值可计算,因而效果不好。
位置前馈网络就是一个全连接前馈网络,每个位置的词都单独经过这个完全相同的前馈神经网络。其由两个线性变换组成,即两个全连接层组成,第一个全连接层的激活函数为 ReLU 激活函数。可以表示为:
在每个编码器和解码器中,虽然这个全连接前馈网络结构相同,但是不共享参数。整个前馈网络的输入和输出维度都是 d model d _ { \text { model } } d model = 512 第一个全连接层的输出和第二个全连接层的输入维度为 d f f d _ { f f } dff = 2048
在Decoder的解码器也有编码器中这两层结构,但是它们之间还有一个注意力层(即 Encoder-Decoder Attention),其用来帮忙解码器关注输入句子的相关部分(类似于 seq2seq 模型中的注意力)。通过上面的介绍,我们已经了解第一个编码器的输入是一个序列,最后一个编码器的输出是一组注意力向量 Key 和 Value。这些向量将在每个解码器的 Encoder-Decoder Attention 层被使用,这有助于解码器把注意力集中在输入序列的合适位置。
我们先从HighLevel的角度观察一下Decoder结构,从下到上依次是
和Encoder一样,上面三个部分的每一个部分,都有一个残差连接,后接一个 Layer Normalization。Decoder的中间部件并不复杂,大部分在前面Encoder里我们已经介绍过了,但是Decoder由于其特殊的功能,它必须有多的一层即Masked Multi-Head Self-Attention 具体原因请看下节介绍。
具体来说,传统Seq2Seq中Decoder使用的是RNN模型,因此在训练过程中输入t 时刻的词,模型无论如何也看不到未来时刻的词,因为循环神经网络是时间驱动的,只有当t时刻运算结束了,才能看到t + 1时刻的词。而Transformer Decoder抛弃了RNN,改为Self-Attention,由此就产生了一个问题,在训练过程中,整个ground truth都暴露在Decoder中,这显然是不对的,我们需要对Decoder的输入进行一些处理,该处理被称为Mask,这样以便于在输出的时候,输出序列是一个单向的序列,每个输出的单词依靠前面的输出而不是依靠后面的输出。
如何实现上述功能呢,我们要先了解一下什么是掩码MASK?
Mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 Padding Mask 和 Sequence Mask。其中,Padding Mask 在所有的 scaled dot-product attention 里面都需要用到,而 Sequence Mask 只有在 Decoder 的 Self-Attention 里面用到。
Padding Mask
什么是 Padding mask 呢?因为每个批次输入序列的长度是不一样的,所以我们要对输入序列进行对齐。具体来说,就是在较短的序列后面填充 0(但是如果输入的序列太长,则是截断,把多余的直接舍弃)。因为这些填充的位置,其实是没有什么意义的,所以我们的 Attention 机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。
具体的做法:把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过 Softmax 后,这些位置的概率就会接近 0。
Sequence Mask
Sequence Mask 是为了使得 Decoder 不能看见未来的信息。也就是对于一个序列,在 t时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因为我们需要想一个办法,把 t 之后的信息给隐藏起来。
具体的做法:产生一个上三角矩阵,上三角的值全为 -∞。把这个矩阵作用在每个序列上,就可以达到我们的目的。
总的来说:对于 Decoder 的 Self-Attention,里面使用到的 scaled dot-product attention,同时需要 Padding Mask 和 Sequence Mask,具体实现就是两个 Mask 相加。其他情况下(比如Encoder的情况),只需要用到Padding Mask即可。
我们再来看一下具体它是如何实现的呢?
举个栗子,Decoder的ground truth为" I am fine",我们将这个句子输入到Decoder中,经过WordEmbedding和Positional Encoding之后,将得到的矩阵做三次线性变换得到 W k W v W Q W _ { k } W _ { v } W _ { Q } WkWvWQ然后进行self-attention操作,首先通过 Q × K T d k \frac { Q \times K ^ { T } } { \sqrt { d _ { k } } } dk Q×KT得到Scaled Scores,接下来非常关键,我们要对Scaled Scores进行Mask,举个例子,当我们输入"I"时,模型目前仅知道包括"I"在内之前所有字的信息,即""和"I"的信息,不应该让其知道"I"之后词的信息。道理很简单,我们做预测的时候是按照顺序一个字一个字的预测,怎么能这个字都没预测完,就已经知道后面字的信息了呢?Mask非常简单,首先生成一个下三角全0,上三角全为负无穷的矩阵,然后将其与Scaled Scores相加即可。
具体过程如下图所示:
Transformer 中Cross attetion 交叉注意力:
其实这一部分的计算流程和前面Masked Self-Attention很相似,结构也一摸一样,唯一不同的是这里的K ,V为Encoder的输出,Q为Decoder中Masked Self-Attention的输出,Q K V的来源有所不同。因此也叫Cross attetion 交叉注意力
。
到上一步为止,解码器的输出仍然是每个字或者单词对应的向量,而这个向量的维度在Transformer里面是是512,但是如何把这个向量转化为对应的字或者单词呢?那就需要一个线性层Linear和Softmax层来决定。
线性层是一个简单的全连接神经网络,其将解码器栈的输出向量映射到一个更长的向量,这个向量被称为 logits 向量。现在假设我们的模型有 1080个英文单词(模型的输出词汇表)。因此 logits 向量有 1080 个数字,每个数表示一个单词的分数。
然后,Softmax 层会把这些分数转换为概率(把所有的分数转换为正数,并且加起来等于 1)。最后选择最高概率所对应的单词,作为这个时间步的输出。
过程如图:
基础概念差不多理解之后,我们可以利用Transformer做一个小项目来检测一下自己的掌握情况,毕竟实践出真知,实践是检验真理的唯一标准嘛,题目具体要求如下。
通俗地来讲就是做一个完形填空,给出一句话,随机MASK一个字或者单词,用上下文之间的关系推断出来这个位置应该是什么,和BERT模型的训练方法很相似.
import os
import tensorflow as tf
from transformers import BertTokenizerFast, TFBertForMaskedLM, BertConfig
import numpy as np
# 判断是否有GPU
from tensorflow.python.client import device_lib
gpus = tf.config.list_physical_devices('GPU')
if gpus:
try:
for gpu in gpus:
tf.config.experimental.set_memory_growth(gpu, True)
# 有GPU配置正常,输出合理
print("GPU is available.")
except RuntimeError as e:
# 没有GPU配置错误
print(e)
# 在有无GPU的不同情况下设置不同的batchsize,因为二者训练的速度差别很大
if 'GPU' in str(device_lib.list_local_devices()):
batch_size = 256
epochs = 2*2
else:
batch_size = 8*1
epochs = 2
print("Batch size:", batch_size)
print("Epochs:", epochs)
本次采用的数据是人民日报2022年一年所有的新闻如下图所示,但是数据对于训练这个模型不算富裕,想要训练得到更好的模型,可以自行网络爬虫寻找语料。
with open("D:\S\summary.txt", encoding="gb2312", errors='ignore') as f:
text_lines = f.read().splitlines()
此时的
len(text_lines) = 513586
意味着有这么多行的句子,但是每行的大小不一,还需要进行后续处理
这一part的作用就是将上部分读入的句子处理为能训练使用的训练集和对应的标签。
tokenizer = BertTokenizerFast.from_pretrained('my_tokenizer')
# 创建训练集inputs和训练集labels的标签
inputs, labels = [], []
new_tokens = ['[BEGIN]', '[END]', '8M']
num_new_tokens = tokenizer.add_tokens(new_tokens) # 加入的一些特殊的token
for line in text_lines:
line = "[BEGIN]" + line + "[END]"
tokens = tokenizer.tokenize(line, max_length=128, truncation=True, padding='max_length')
token_ids = tokenizer.convert_tokens_to_ids(tokens)
# 将每句话加上开头[BEGIN] 和 结尾[END]
# 采用随机掩蔽方法训练数据,masked的范围是原句子line的15%
if len(line) < len(token_ids):
# 随机采样函数见下文
masked_indices = np.random.choice(range(1, len(line)+1), size=int(len(line) * 0.15), replace=False)
else:
masked_indices = np.random.choice(range(1, len(token_ids)), size=int(len(token_ids) * 0.15), replace=False)
# print(f"masked_indices: {masked_indices}")
labels_line = [-100] * len(token_ids) # 先全部设置为一个默认值
for idx in masked_indices:
labels_line[idx] = token_ids[idx] # 将被抽到的位置的label换成这个位置的真实字对应的值
token_ids[idx] = tokenizer.mask_token_id #将原来被抽到的位置的原来的字的token换为mask来进行预测。
inputs.append(token_ids)
labels.append(labels_line)
# 最后转换成为
inputs = tf.constant(inputs)
labels = tf.constant(labels)
BertTokenizerFast 是一个基于Bert模型的预训练模型的分词器。可以下载完保存到本地,然后
tokenizer = BertTokenizerFast.from_pretrained(bert_path,
add_special_tokens=False,
do_lower_case=True)
用上述方法加载到tokenizer里面后续可以使用其进行分词操作。具体操作可看tokenizer的使用
numpy.random.choice()函数的使用方法如下:
choice(a, size=None, replace=True, p=None):
各参数结解释
a
:待抽样的样本(一维数组或整数)size
: 输出大小,默认返回单个元素replace
: 抽样后的元素是否可重复,默认是p
: 每个样本点被抽样的概率,默认均匀抽样
更为详细的请看此链接numpy的抽样函数
tf.constant()函数介绍和示例
tf.constant(value, shape, dtype=None, name=None)
value
:值shape
:数据形状dtype
:数据类型name
:名称
编码原理过程详情见 1.3.2 Positional Encoding
# 位置编码
def positional_encoding(position, d_model):
# 用于计算角度的函数
def get_angles(pos, i, d_model):
angle_rates = 1 / np.power(10000, (2 * (i//2)) / np.float32(d_model))
return pos * angle_rates
# 每个元素pos_encoding[i, j]代表着词i的位置j的编码
angle_rads = get_angles(np.arange(position)[:, np.newaxis],
np.arange(d_model)[np.newaxis, :],
d_model)
# sin给偶数索引(indices)上的角度编码
angle_rads[:, 0::2] = np.sin(angle_rads[:, 0::2])
angle_rads[:, 1::2] = np.cos(angle_rads[:, 1::2])
pos_encoding = angle_rads[np.newaxis, ...]
return tf.cast(pos_encoding, dtype=tf.float32)
注意这里
transpose_b=True
是因为Q是与K的转置矩阵相乘,所以最后两维要交换位置。 transpose_b=True的含义
def scaled_dot_product_attention(q, k, v, mask):
# query key 矩阵相乘获取匹配关系 有个小细节transpose_b=True是因为Q是与K的转置矩阵相乘
matmul_qk = tf.matmul(q, k, transpose_b=True)
dk = tf.cast(tf.shape(k)[-1], tf.float32)
scaled_attention_logits = matmul_qk / tf.math.sqrt(dk)
if mask is not None:
scaled_attention_logits += (mask * -1e9)
# 通过softmax获取attention权重
attention_weights = tf.nn.softmax(scaled_attention_logits, axis=-1)
output = tf.matmul(attention_weights, v)
return output, attention_weights
def point_wise_feed_forward_network(d_model, dff):
return tf.keras.Sequential([
tf.keras.layers.Dense(dff, activation='relu'),
tf.keras.layers.Dense(d_model)
# 保证传播后的维度与原来仍然一致
])
class MultiHeadAttention(tf.keras.layers.Layer):
# 定义类,初始化参数,和函数
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
self.num_heads = num_heads
self.d_model = d_model
assert d_model % self.num_heads == 0
self.depth = d_model // self.num_heads
self.wq = tf.keras.layers.Dense(d_model)
self.wk = tf.keras.layers.Dense(d_model)
self.wv = tf.keras.layers.Dense(d_model)
self.dense = tf.keras.layers.Dense(d_model)
def split_heads(self, x, batch_size):
x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))
return tf.transpose(x, perm=[0, 2, 1, 3])
def call(self, v, k, q, mask=None):
# 前向传播的过程
batch_size = tf.shape(q)[0]
# 设置q, k, v的维度
q = self.wq(q)
k = self.wk(k)
v = self.wv(v)
# 分拆q, k, v的维度
q = self.split_heads(q, batch_size)
k = self.split_heads(k, batch_size)
v = self.split_heads(v, batch_size)
scaled_attention, attention_weights = scaled_dot_product_attention(
q, k, v, mask)
# 将多头维度的输出合并
scaled_attention = tf.transpose(scaled_attention, perm=[0, 2, 1, 3])
concat_attention = tf.reshape(scaled_attention,
(batch_size, -1, self.d_model))
output = self.dense(concat_attention)
return output, attention_weights
class TransformerEncoderLayer(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads, dff, rate=0.1):
super(TransformerEncoderLayer, self).__init__()
'''
mha : MultiHeadAttention 多头注意力机制
ffn : point_wise_feed_forward_network 前馈神经网络
'''
self.mha = MultiHeadAttention(d_model, num_heads)
self.ffn = point_wise_feed_forward_network(d_model, dff)
self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
self.dropout1 = tf.keras.layers.Dropout(rate)
self.dropout2 = tf.keras.layers.Dropout(rate)
def call(self, x, training, mask):
# 用multi-head attention
attn_output, _ = self.mha(x, x, x, mask)
# size (batch_size, input_seq_len, d_model)
attn_output = self.dropout1(attn_output, training=training)
out1 = self.layernorm1(x + attn_output)
# 残差连接 size (batch_size, input_seq_len, d_model)
ffn_output = self.ffn(out1)
#前馈网络,用于处理上面的输出维度相同 size (batch_size, input_seq_len, d_model)
ffn_output = self.dropout2(ffn_output, training=training)
out2 = self.layernorm2(out1 + ffn_output)
# 同上残差连接 (batch_size, input_seq_len, d_model)
return out2
可以实现自由选的编码器的层数,来适用不同的需求。
class TransformerEncoder(tf.keras.layers.Layer):
def __init__(self, num_layers, d_model, num_heads, dff, input_vocab_size, maximum_position_encoding, rate=0.1):
super(TransformerEncoder, self).__init__()
self.d_model = d_model
self.num_layers = num_layers
# 词嵌入层
self.embedding = tf.keras.layers.Embedding(input_vocab_size, d_model)
self.pos_encoding = positional_encoding(maximum_position_encoding,
self.d_model)
self.enc_layers = [TransformerEncoderLayer(d_model, num_heads, dff, rate)
for _ in range(num_layers)]
self.dropout = tf.keras.layers.Dropout(rate)
def call(self, x, training, mask):
seq_len = tf.shape(x)[1]
# 添加词嵌入和位置编码
x = self.embedding(x) # (batch_size, input_seq_len, d_model)
x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))
x += self.pos_encoding[:, :seq_len, :]
x = self.dropout(x, training=training)
# 有几层 迭代几次 确定编码器的cengshu
for i in range(self.num_layers):
x = self.enc_layers[i](x, training, mask)
return x # (batch_size, input_seq_len, d_model)
class MyTransformerModel(tf.keras.Model):
def __init__(self, num_layers, d_model, num_heads, dff, vocab_size, maximum_position_encoding, rate=0.1):
super(MyTransformerModel, self).__init__()
# 用于编码输入序列
self.encoder = TransformerEncoder(num_layers, d_model, num_heads, dff, vocab_size, maximum_position_encoding, rate)
self.final_layer = tf.keras.layers.Dense(vocab_size)
def call(self, x, training=False, mask=None):
enc_output = self.encoder(x, training, mask) # (batch_size, inp_seq_len, d_model)
final_output = self.final_layer(enc_output) # (batch_size, inp_seq_len, vocab_size)
return final_output
模型的回调函数: 便于保存模型的权重参数,选择最好的参数
mlm_loss
: 自定义损失函数
lr_scheduler
: 自定义学习率衰减函数
# 路径设置
model_dir = './transformer_model'
# 定义损失函数
def mlm_loss(y_true, y_pred):
y_true_masked = tf.boolean_mask(y_true, tf.not_equal(y_true, -100))
y_pred_masked = tf.boolean_mask(y_pred, tf.not_equal(y_true, -100))
return tf.nn.sparse_softmax_cross_entropy_with_logits(y_true_masked, y_pred_masked)
# 定义学习率衰减函数
def lr_scheduler(epoch, lr):
if epoch < 3:
return lr # 前5个epochs保持原始学习率
else:
return lr * math.exp(-0.1) # 后续epochs按指数衰减
checkpoint_dir = f'./{model_dir}/checkpoints'
os.makedirs(checkpoint_dir, exist_ok=True)
# 设置保存模型的回调函数
checkpoint_callback = SaveLastThreeModelCheckpoint(
filepath=os.path.join(checkpoint_dir, 'weights.{epoch:02d}.hdf5'),
monitor='loss',
verbose=1,
save_best_only=True,
save_weights_only=True,
mode='auto',
save_freq='epoch',
period=1
)
# 定义学习率调度器回调函数
lr_scheduler_callback = LearningRateScheduler(lr_scheduler)
设置优化器 和 compile
# 路径设置
model_dir = './transformer_model3'
# 不存在,或存在但为空
if not os.path.exists(model_dir) or not os.listdir(model_dir):
# 如果路径不存在,则进行训练
# 加载模型
model = MyTransformerModel(num_layers=2, d_model=512, num_heads=8, dff=1024, vocab_size=len(tokenizer)
, maximum_position_encoding=1000)
# 定义优化器
optimizer = Adam(learning_rate=1e-3)
model.compile(optimizer=optimizer, loss=mlm_loss)
checkpoint_dir = f'/{model_dir}/checkpoints'
os.makedirs(checkpoint_dir, exist_ok=True)
# 训练模型
history = model.fit(dataset, epochs=5, callbacks=[checkpoint_callback, lr_scheduler_callback])
# 保存模型
model.save(os.path.join(model_dir, 'transformer_model'))
# 保存tokenizer
tokenizer.save_pretrained(model_dir)
else:
# 如果路径存在,则加载已有模型
model = tf.keras.models.load_model(os.path.join(model_dir, 'transformer_model'),
custom_objects={'mlm_loss': mlm_loss})
print("Load model from", model_dir)
训练过程大概如下:
mask_token_indices = tf.where(input_ids[0] == tokenizer.mask_token_id)
predicted_sentence = tokenizer.decode(input_ids[0])
predicted_sentence = predicted_sentence.replace("[CLS] ", "").replace(" [SEP]", "").replace(" [PAD]", "")
predicted_sentence = predicted_sentence.replace(" ", "")
print(predicted_sentence)
for mask_token_index in mask_token_indices:
print(f"mask_token_index={mask_token_index}")
outputs = model.predict(input_ids)
predicted_token_index = tf.argmax(outputs[0, mask_token_index[0]]).numpy()
mask_token_index_2d = tf.reshape(mask_token_index, [1, 1]) # 转换成2d tensor以符合tf.tensor_scatter_nd_update的输入要求
predicted_token_index_1d = tf.reshape(predicted_token_index, [1]) # 转换成1d tensor以符合tf.tensor_scatter_nd_update的输入要求
predicted_token_index_1d = tf.cast(predicted_token_index_1d, dtype=tf.int32) # 将类型从int64转换为int32
input_ids = tf.tensor_scatter_nd_update(input_ids[0], mask_token_index_2d, predicted_token_index_1d)
input_ids = tf.expand_dims(input_ids, 0) # 重新扩展维度以符合模型的输入要求
predicted_sentence = tokenizer.decode(input_ids[0])
predicted_sentence = predicted_sentence.replace("[CLS] ", "").replace(" [SEP]", "").replace(" [PAD]", "")
predicted_sentence = predicted_sentence.replace(" ", "")
print(predicted_sentence)
训练了20余次,最终loss在2左右,测试结果如下图所示
作者写到这里已经很累了hhhh,等我后续再慢慢完善。
1. Transformer详解
2. Transformer 模型详解
3. transformer 细节理解
4. 基于Tensorflow 2.x手动复现BERT
5. Transformer的Embedding解析
6. LN和BN的不同之处
7. numpy的抽样函数
8. bert的预训练模型
9. 张量类型转换
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。