NLP基础
自注意力机制
在NLP方向上,自注意力机制要解决的问题。
在CV方向上,一般我们输入的都是图片,无论这个图片多大,都会resize到一个统一的尺寸。最终经过CNN的提取,变成一个特征向量,那么这个特征向量的维度是一样的。再经过softmax变成一个分类(Class)的概率
那么在NLP方向上就不是这样了,它可能输入的是很多个不同长度,不同大小的向量。例如我们输入的是一个句子,这个句子,我们也不知道它有多长。
我们会将句子中的每一个词汇都描述成一个向量——词向量。那对于一个句子来说,就是一个词向量的集合(a set of vectors)。一般词变词向量的方式使用的是word embedding。
对于输出来说,可以分为三种情况
1、输入多少个词向量,输出多少个label。
2、无论输入多少个词向量,只输出一个label
3、输入输出的长度上没有确定关系,输出不由输入的词向量的长度决定。它其实就是Seq2Seq。
因为有一大段的词向量,如果我们只是把它们的每个词向量简单的丢入到神经网络中,可能我们什么都得不到。
因为对于一个句子来说,我们可能更多关心的是每个词之间的位置关系以及词与词之间的语义关系。故而我们会在自注意力机制中将一个句子中所有的词向量变成一个向量,再由各个全连接层来进行每一个词向量的输出
但一般我们会使用两次自注意力机制再进行输出
在自注意力机制中,每一个输出都是考虑了上一层中所有的词向量的
现在我们来看看对于单个输出\(b^1\)是怎么产生的,其他的\(b^2、b^3、b^4\)是类似的。
对于\(b^1\)来说,我们需要看\(a^1\)与其他词向量之间的关系,看看哪些词跟\(a^1\)的关系更密切,哪些词跟\(a^1\)没太大关系。每一个词跟\(a^1\)的重要程度,我们会用一个α来表示。一般计算这个α会有几种方法,最常用的就是Dot-product。
首先我们会用a( \(a^1,a^2,a^3,...,a^n\))分别和两个矩阵\(W^q\)和\(W^k\)相乘得到q和k,然后再将它们点乘得到α。
\(a^1\)跟所有的词向量都做了计算之后,就得到了不同的α(这里需要注意的是\(a^1\)跟自己本身也要做一个相关性计算),再经过一个Softmax就得到了一组新的α(\(α_{1.1}',α_{1.2}',α_{1.3}',α_{1.4}'....\))。这个α可以理解成权重,因为Softmax本身就是占比。
之后我们会把每一个a(\(a^1,a^2,a^3,a^4...\))乘以一个矩阵\(w^v\),得到一组新的向量v(\(v^1,v^2,v^3,v^4...\)),每一个v与α相乘再相加就得到了\(b^1\),所以\(b^1\)相比与\(a^1\)来说是考虑了所有词向量与\(a^1\)的关系得到的新的词向量。公式为
- 矩阵运算
我们知道对于单个的a来说,对于q的计算就是q=\(W^qa\),对于所有的a来说
就有
\(q^i=W^qa^i\)
那么对于上图中的4个a来说,就有
我们将\(a^1\)到\(a^4\)四个向量合并成一个矩阵I,那么得到的四个向量\(q^1\)到\(q^4\)合并成一个矩阵Q。这样就有了
\(Q=W^qI\)
这里的\(W^q\)就是词向量集合I的参数。k,v跟q是一样的,以此类推,
\(K=W^kI\)
\(V=W^vI\)
所以就是句子矩阵I,分别跟三个不同的矩阵\(W^q、W^k、W^v\)相乘得到Q、K、V。这样就从单个的向量运算变成了矩阵运算。
然后是每一个q都会跟每一个k去计算α
这个过程同样也可以看成是一个矩阵乘以一个向量得到一个新的向量(这里需要注意矩阵乘向量为向量)
这里不仅仅是\(q^1\)需要计算,而是所有的q都要计算。
这样就有了
\(A=K^TQ\)
这个A就是注意力机制的分数。再把这个分数经过Softmax就得到了A',这个A'就是权重矩阵
之前我们计算新的词向量\(b^1\),就是
但我们仔细看的话,它其实就是一个矩阵与向量相乘
\(b^1=Vα_1'\)
对于其他的\(b^2、b^3、b^4\),就有了
这里的O就是由所有的新的词向量构成的新的矩阵
\(O=VA'\)
所以对于整个自注意力机制来说就是一连串的矩阵运算,这里的输入就是句子矩阵I,分别乘以三个矩阵\(W^q、W^k、W^v\)得到Q、K、V。再由\(K^TQ\)得到A,将A经过Softmax得到A',这个A'又叫Attenntion Matrix(注意力矩阵,权重)。再经过\(VA'\)得到输出O。这一系列的操作中唯一需要学习的参数就是\(W^q、W^k、W^v\),只有这三个矩阵是未知的,需要通过我们的训练来得到的。而其他的步骤都不需要学习,都是已知的。
多头自注意力机制(Multi-head Self-attention)
上图是一个二头自注意力的机制,之前我们已经知道\(q^i=W^qa^i\),那么二头自注意力机制中会将\(q^i\)再分别乘以两个矩阵(如果是多头的话就乘以多个矩阵),分别再得到两个\(q^{i,1}\)和\(q^{i,2}\);同样,k和v也做同样的操作,分别得到\(k^{i,1}、k^{i,2}\)以及\(v^{i,1}、v^{i,2}\),剩下的步骤就跟之前是一样的,只不过是1跟1的玩,2跟2的玩。即\(α^{i,1}=q^{i,1}k^{i,1}\),\(α^{i,2}=q^{i,2}k^{i,2}\)。最后算出每个头各自的b。
这里我们得到的\(b^{i,1}\)和\(b^{i,2}\)会再经过一个变换矩阵\(W^o\),得到下一层的输出\(b^i\)(这里如果有多个头的话就会得到多个\(b^{i,n}\))
位置编码(Positionnal Encoding)
在以上的自注意力机制中都没有位置的信息。所有的\(a^1\)到\(a^4\),它们都是一样的,一样的操作,一样的运算,没有前后之分。
如果我们觉得在一个句子中,位置是很重要的信息(比如说动词不会出现在首位),那么我们就会对位置进行编码,变成一个唯一的位置向量\(e^i\)。
而具体,我们只需要将位置编码向量加到词向量中就好了。
最早的位置编码用的就是最简单粗暴的方式,你在哪个位置,就编码为几,称为hand-crafted。
位置编码现在还没有一个公认一致的方法,仍然存在着很多的创新。
上图中有这么几种方式,有人为设定好的方式也有使用数据学习出来的。
Transformer
Transformer是一种Sequence-to-sequence的结构,属于输入的向量数与输出的向量数无关的类型。
- 编码器(Encoder)
上图中的左边就是编码器。
这是编码器的大致结构,它输入的就是一个一个的词向量,经过几层网络结构之后,最后输出跟输入相同数量的新的词向量。第一个Block就是之前说的自注意力机制加上全连接层组成的。但上图并不是确切的编码器,在第一个Block之间其实是有一个残差连接的(residual),并且会做一个Layer Norm。
Layer Norm是不同于BatchNorm的,它不用考虑batch的问题。它输入的是一个向量,输出的是另外一个向量。不同于BatchNorm(BatchNorm是针对不同的向量,去计算同一个维度的值的均值和方差),Layer Norm是计算同一个向量的不同维度的值的均值和方差。然后用每一个维度的值去减去均值再除以方差,得到新的向量。而这个新的向量就是全连接层的输入,而这个全连接层也有一个残差连接。残差连接之后再做一次Layer Norm的输出才是一个编码器Block的最终输出。
- 解码器(Decoder)
解码器会将编码器的输出给读取进去,但并不是作为解码器的原始输入。解码器有两种,比较常见的叫做自回归(Autoregressive(AT))。
在解码器中,它的原始输入并不是一次就输入一个完整的句子,最开始会辨别一个特殊的字符,如上图中的BEGIN,该字符是一个one-hot编码。输入了该字符后,解码器会输出一个向量。
这个输出的向量的长度很长,它是一个字典的字符的总数量(如英文可能是26个字母,中文3000多个字)。再输出这个向量之前会先运算一个Softmax,得到该字典内所有的字符的概率(总概率为1),这个向量并不是最终输出结果,我们会根据Softmax的计算结果,取概率最大的字符作为最终的输出。如上图中"机"的概率最高为0.8,则最终输出的就是"机"这个字符。
然后我们会将这个最终的输出"机"作为解码器的新的输入,再重复第一轮的过程,从字典中得到一个概率最高的字符进行输出,例如上图中输出的为"器"。
经过了一系列的过程之后,就有了这样一个结果。
掩码自注意力机制(Masked Self-attention)
解码器中的自注意力机制跟编码器中有所不同,称为掩码自注意力机制
简单说,在只有\(a^1\)作为输入的时候,就只考虑\(a^1\)自己的信息,而不能考虑后面的\(a^2、a^3、a^4\);当有\(a^2\)输入的时候,会同时考虑\(a^1\)和\(a^2\)的信息;以此类推当有一个解码器自回归的输入时,可以考虑前面所有的输入,而后面还未输入的信息是不考虑的。
具体一点说,当我们去计算\(b^2\)的时候,我们只会用\(q^2\)去分别乘以\(k^1\)和\(k^2\)得到\(α_{2,1}'\)和\(α_{2,2}'\),而不必理会后面的\(k^3、k^4\)。再用\(α_{2,1}'、α_{2,2}'\)分别乘以\(v^1、v^2\),结果再相加就得到了\(b^2\)。
解码器有一个最大的问题,就是它的输入的数量跟输出的数量是不一致的,虽然我们上面的例子中是输入4个字符,输出4个字符,但事实并非如此。
在解码器的输出向量中的字典里其实会有一个特殊字符END,表示输出需要终结。
例如在上面的例子中,当解码器输出"习"以后,再将"习"作为输入,那么它产生的新的输出就是END,不再作为输入而结束了。
还有一种解码器是非自回归(Non-autoregressive(NAT))
上图中左边的是自回归解码器,右边的就是非自回归解码器。它会同时输入多个BEGIN,然后输出多个字符,只需要一个步骤就可以完成句子的生成。
那我们该如何决定非自回归解码器的输出长度呢?
- 使用另外一个分类器(classifier),它的输入是整个Transformer的输入句子,输出的是一个数字,这个数字就是非自回归解码器的输出长度。
- 使用一个固定比较长的数字,例如300,那么就会输入300个BEGIN,输出300个字符。
如果在解码器的输出中发现了END,那么END后面的输出,我们都当成没有输出就好。
NAT的好处是它是并行的,不像AT需要一个一个的输出再输入。并且它可以控制输出的长度。
- 编码器-解码器(Encoder-Decoder)
在上图的红色区域内就是编解码器的连接处,该处称为交叉注意力机制(Cross attention)。在解码器中,如果屏蔽掉这部分,那么编解码器的结构就大致相同了。在交叉注意力机制中,编码器提供了两个输入,解码器提供了一个输入。
在上图中,编码器这一端会先输入一个句子,经过编码器产生了几个新的词向量,这些词向量再分别乘以两个转换矩阵(假设是\(W^k\)和\(W^v\))得到各自的k和v;在解码器这一端,先输入BEGIN,通过掩码自注意力机制,产生一个向量,该向量再乘以一个转换矩阵(假设说是\(W^q\))得到q,那么q就会和编码器端的各个k(\(k^1、k^2、k^3\))相乘得到各个α'(\(α_1'、α_2'、α_3'\)),再分别乘以编码器端的v(\(v^1、v^2、v^3\))再相加,就得到了交叉注意力机制层的输出——新的向量v。新的向量再通过全连接层进行处理。
然后就是解码器自回归得到的第二个输入的字符,上图中为"机"。再经过一遍上面的过程就会得到一个新的输出向量v',再将v'输入到全连接层中。
- 模型训练
现在假设编码器这边输入的是一段语音,这段语音的内容就是"机器学习"。所以我们会给这段语音打上标签(Label)——"机器学习"。但是我们标签的打法是以one-hot来标注的,如"机器学习"的"机"字,我们会标注为"0100",这个1其实也代表了Ground truth,即概率为1,其他的概率都为0。在解码器这一端输入BEGIN,输出的向量字典中的概率分布,我们希望它越接近Ground truth越好。所以我们会去计算Ground truth跟这个向量字典概率分布的交叉熵损失最小化。
所以我们对于每一个自回归的输入,都会做一次交叉熵的最小化,这其中也包括了结束字符END的交叉熵最小(END也是一个one-hot编码)。则我们会将所有的交叉熵的总和最小化。
BERT
原理解析
BERT是基于Transformer的一种模型结构。
Trannsformer的原始结构是这样子的
它是由6个编码器和6个解码器共同构成了整体结构,但对于BERT来说
它是由12个编码器构成的。
BERT最大的特点就是它输入的句子可以是任意的,并且不需要标签(Label)。
BERT做的事情就是输入一个句子,然后输出一个个的词向量(word embedding)。我们来看一下什么是词向量
词向量就是把有相近语义的词尽可能的具有一定的相似度。而不是简单one-hot编码,完全看不出词与词之间的关系。
当然这个词向量是需要去训练才能得到的,而不是天然就可以编码的。
虽然我们这里的例子用的是中文的词——"潮水"、"退了"、"就"、"知道"。但是一般我们在训练中文的时候会使用字,因为词的数量太大,而常用字也就3000~4000左右,作为字典是比较合适的。
- 模型训练
BERT的训练方式有两种,第一种叫Masked LM
Masked LM会将所有输入的句子有随机15%的词汇会被置换成一个特殊的token——[MASK]。BERT在训练的时候需要去猜测这些被置换的词汇到底是什么词汇。
BERT会先将输入的句子的每一个词汇变成embedding,包括这个被替换掉的词,然后将被替换掉到词生成的embedding再放入到一个线性多分类器(Linear Multi-class Classifier)中,预测出被替换掉的词汇是哪一个词汇。由于是线性分类器,它的分类能力其实是比较弱的,这就对BERT的层数要求比较高,可能会达到24层,32层,而不再是之前说的12层。
如果两个词汇填在同一个地方没有违和感,那它们就有类似的embedding。比如上图中被替换掉的词其实是"退了",但如果BERT预测出来的词是"落了",同样是可以的,那就说明"退了"跟"落了"具有相似性。
第二种训练方式叫Next Sentence Prediction。
这种方式是判断两个句子是否是衔接的,还是不能衔接。
这里需要引入两个特殊的token——[SEP],[CLS]。[SEP]是两个句子之间的边界(boundary),BERT要预测两个句子是不是相衔接的,还需要输入一个特殊的token就是[CLS],它通常位于句子的开头,表示要做分类。[CLS]通过BERT输出的embedding会进入一个线性二分类器(Linear Binary Classifier)以对后面的两个句子是应该接在一起还是不应该接在一起做一个二分类。
[CLS]之所以能够放在开头,而不需要放在两个句子的结尾,是由BERT的网络架构决定的,因为BERT使用的是Transformer的编码器架构,编码器会同时处理输入句子中的所有的词,而不是像RNN一个一个去处理的。
由于我们会输入大量的句子,这些句子天然就有连接的,所以BERT会根据我们输入来训练哪些句子是可以连接在一起,哪些是不能连接在一起。
上面的这两种训练方式同时使用,会训练的最好。
- 使用方式
1、输入一个句子,输出一个分类标签。比如句子的情感分析,预测一个句子是正面的还是负面的。句子的新闻分类,比如说是体育新闻还是财经新闻,或者政治新闻等。
这种方式其实也是在句子的开头加入一个特殊的token——[CLS]表示分类,[CLS]通过BERT产生的embedding同样送入一个线性分类器中做分类,不过这里我们是需要对这些句子做标注的,比如情感分析中,我们需要标注这些句子哪一个句子是正面的,哪一个句子是负面的,这样BERT通过不断的学习就可以对其他的句子作出一个情感分类的判断。而BERT的预训练模型是支持中文的,我们只需要做好数据集(带标注),并且使用BERT的预训练模型参数进行微调(find-tune)就可以了。
2、输入一个句子,对句子中的每一个词都做一个分类。
比方说这个位置填充,我们需要知道上面输入的句子的每一个词属于哪一种Slot,上面的分类类别就有other、dest、time等,这些都是每一个词的标签。
我们会将输入的句子的每一个词都通过BERT生成的embedding都送入到线性分类器中去做分类,当然类别是我们自己人为去设定的。
3、输入两个句子,输出一个分类。
这里同第二种训练方式。
4、QA(回答基本问题,Extraction-based Question Answering)
一般这种类型的答案都是在文章中有出现过的。
在上图中,D表示文章中所有的token的集合,Q表示问题中所有的token的集合。而我们的模型(QA Model)会将这两种集合同时输入,输出两个整数s和e。那么答案就是文章中第s个token到第e个token。
比方说gravity是文章中的第17个token,那么我们的第一个问题输出的s,e两个整数就都是17。其他问题以此类推,
。
在上图中,[SEP]左边的是问题的tokens,右边是文章的tokens,文章的tokens通过BERT会生成各自的embedding。此时,我们会训练出两个不同的向量,如上图中的橙色和蓝色的向量,它们的维度跟BERT输出的黄色的embedding向量的维度是相同的。再使用其中橙色的向量与文章的所有token输出的embedding进行点乘得到一组标量(向量与向量相乘,结果为标量),再将这组标量通过Softmax得到一个组概率值(总和为1)。这组概率值中最大的那个所代表的token的序号就是整数s的值。如上图中s=2。
同理,我们会将蓝色的向量与文章的所有token输出的embedding进行点乘得到一组标量,再将这组标量通过Softmax得到一个组概率值(总和为1)。这组概率值中最大的那个所代表的token的序号就是整数e的值。如上图中e=3。
如果我们得到的s>e,那么就代表问题没有答案,我们需要输出此题无解。
实战篇
模型源码下载地址:https://github.com/google-research/bert
另外我们还需要下载预训练模型来做finetune,我这里下载的是wwm_uncased_L-24_H-1024_A-16.zip
解压后,有5个文件,其中bert_config.json为训练时使用到的训练参数,bert_model开头的都是训练过的模型参数,vocab.txt是字典。将该预训练模型参数放入到bert-master/GLUE/BERT_BASE_DIR下面。
如果你的cuda是11.x的,cudnn一定要使用8.9.4的,其他版本的无效。
下载数据集GLUE data,运行如下脚本进行下载
import os
import io
import sys
import shutil
import argparse
import tempfile
import urllib
import urllib.request as URLLIB
import zipfile
TASKS = ["CoLA", "SST", "MRPC", "QQP", "STS", "MNLI", "QNLI", "RTE", "WNLI", "diagnostic"]
TASK2PATH = {"CoLA":'https://dl.fbaipublicfiles.com/glue/data/CoLA.zip',
"SST":'https://dl.fbaipublicfiles.com/glue/data/SST-2.zip',
"QQP":'https://dl.fbaipublicfiles.com/glue/data/QQP-clean.zip',
"STS":'https://dl.fbaipublicfiles.com/glue/data/STS-B.zip',
"MNLI":'https://dl.fbaipublicfiles.com/glue/data/MNLI.zip',
"QNLI":'https://dl.fbaipublicfiles.com/glue/data/QNLIv2.zip',
"RTE":'https://dl.fbaipublicfiles.com/glue/data/RTE.zip',
"WNLI":'https://dl.fbaipublicfiles.com/glue/data/WNLI.zip',
"diagnostic":'https://dl.fbaipublicfiles.com/glue/data/AX.tsv'}
MRPC_TRAIN = 'https://dl.fbaipublicfiles.com/senteval/senteval_data/msr_paraphrase_train.txt'
MRPC_TEST = 'https://dl.fbaipublicfiles.com/senteval/senteval_data/msr_paraphrase_test.txt'
def download_and_extract(task, data_dir):
print("Downloading and extracting %s..." % task)
if task == "MNLI":
print("\tNote (12/10/20): This script no longer downloads SNLI. You will need to manually download and format the data to use SNLI.")
data_file = "%s.zip" % task
urllib.request.urlretrieve(TASK2PATH[task], data_file)
with zipfile.ZipFile(data_file) as zip_ref:
zip_ref.extractall(data_dir)
os.remove(data_file)
print("\tCompleted!")
def format_mrpc(data_dir, path_to_data):
print("Processing MRPC...")
mrpc_dir = os.path.join(data_dir, "MRPC")
if not os.path.isdir(mrpc_dir):
os.mkdir(mrpc_dir)
if path_to_data:
mrpc_train_file = os.path.join(path_to_data, "msr_paraphrase_train.txt")
mrpc_test_file = os.path.join(path_to_data, "msr_paraphrase_test.txt")
else:
try:
mrpc_train_file = os.path.join(mrpc_dir, "msr_paraphrase_train.txt")
mrpc_test_file = os.path.join(mrpc_dir, "msr_paraphrase_test.txt")
URLLIB.urlretrieve(MRPC_TRAIN, mrpc_train_file)
URLLIB.urlretrieve(MRPC_TEST, mrpc_test_file)
except urllib.error.HTTPError:
print("Error downloading MRPC")
return
assert os.path.isfile(mrpc_train_file), "Train data not found at %s" % mrpc_train_file
assert os.path.isfile(mrpc_test_file), "Test data not found at %s" % mrpc_test_file
with io.open(mrpc_test_file, encoding='utf-8') as data_fh, \
io.open(os.path.join(mrpc_dir, "test.tsv"), 'w', encoding='utf-8') as test_fh:
header = data_fh.readline()
test_fh.write("index\t#1 ID\t#2 ID\t#1 String\t#2 String\n")
for idx, row in enumerate(data_fh):
label, id1, id2, s1, s2 = row.strip().split('\t')
test_fh.write("%d\t%s\t%s\t%s\t%s\n" % (idx, id1, id2, s1, s2))
try:
URLLIB.urlretrieve(TASK2PATH["MRPC"], os.path.join(mrpc_dir, "dev_ids.tsv"))
except KeyError or urllib.error.HTTPError:
print("\tError downloading standard development IDs for MRPC. You will need to manually split your data.")
return
dev_ids = []
with io.open(os.path.join(mrpc_dir, "dev_ids.tsv"), encoding='utf-8') as ids_fh:
for row in ids_fh:
dev_ids.append(row.strip().split('\t'))
with io.open(mrpc_train_file, encoding='utf-8') as data_fh, \
io.open(os.path.join(mrpc_dir, "train.tsv"), 'w', encoding='utf-8') as train_fh, \
io.open(os.path.join(mrpc_dir, "dev.tsv"), 'w', encoding='utf-8') as dev_fh:
header = data_fh.readline()
train_fh.write(header)
dev_fh.write(header)
for row in data_fh:
label, id1, id2, s1, s2 = row.strip().split('\t')
if [id1, id2] in dev_ids:
dev_fh.write("%s\t%s\t%s\t%s\t%s\n" % (label, id1, id2, s1, s2))
else:
train_fh.write("%s\t%s\t%s\t%s\t%s\n" % (label, id1, id2, s1, s2))
print("\tCompleted!")
def download_diagnostic(data_dir):
print("Downloading and extracting diagnostic...")
if not os.path.isdir(os.path.join(data_dir, "diagnostic")):
os.mkdir(os.path.join(data_dir, "diagnostic"))
data_file = os.path.join(data_dir, "diagnostic", "diagnostic.tsv")
urllib.request.urlretrieve(TASK2PATH["diagnostic"], data_file)
print("\tCompleted!")
return
def get_tasks(task_names):
task_names = task_names.split(',')
if "all" in task_names:
tasks = TASKS
else:
tasks = []
for task_name in task_names:
assert task_name in TASKS, "Task %s not found!" % task_name
tasks.append(task_name)
return tasks
def main(arguments):
parser = argparse.ArgumentParser()
parser.add_argument('--data_dir', help='directory to save data to', type=str, default='glue_data')
parser.add_argument('--tasks', help='tasks to download data for as a comma separated string',
type=str, default='all')
parser.add_argument('--path_to_mrpc', help='path to directory containing extracted MRPC data, msr_paraphrase_train.txt and msr_paraphrase_text.txt',
type=str, default='')
args = parser.parse_args(arguments)
if not os.path.isdir(args.data_dir):
os.mkdir(args.data_dir)
tasks = get_tasks(args.tasks)
for task in tasks:
if task == 'MRPC':
format_mrpc(args.data_dir, args.path_to_mrpc)
elif task == 'diagnostic':
download_diagnostic(args.data_dir)
else:
download_and_extract(task, args.data_dir)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))
下载完成后会在当前文件夹出现一个glue_data文件夹,里面包含了我们下载的数据集。将该文件夹放入到bert-master/GLUE下面。
我们的第一个训练只以其中的MRPC数据集为例进行训练,这是一个分辨两个句子是否是同一个意思的数据集,数据格式大致如下
Quality #1 ID #2 ID #1 String #2 String
1 702876 702977 Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence . Referring to him as only " the witness " , Amrozi accused his brother of deliberately distorting his evidence .
0 2108705 2108831 Yucaipa owned Dominick 's before selling the chain to Safeway in 1998 for $ 2.5 billion . Yucaipa bought Dominick 's in 1995 for $ 693 million and sold it to Safeway for $ 1.8 billion in 1998 .
1 1330381 1330521 They had published an advertisement on the Internet on June 10 , offering the cargo for sale , he added . On June 10 , the ship 's owners had published an advertisement on the Internet , offering the explosives for sale .
其中Quality指的是标签,1代表后面的两个句子具有相同的意思,0代表不同,#1 ID代表第一个句子的ID编号,#2 ID代表第二个句子的ID编号,#1 String是第一个句子,#2 String是第二个句子。在MRPC文件夹下面执行
cp msr_paraphrase_train.txt train.tsv
cp train.tsv dev.tsv
添加环境变量
export BERT_BASE_DIR=/home/dell/下载/bert-master/GLUE/BERT_BASE_DIR/wwm_uncased_L-24_H-1024_A-16
export GLUE_DIR=/home/dell/下载/bert-master/GLUE/glue_data
修改/home/dell/anaconda3/lib/python3.9/site-packages/keras/src/optimizers/optimizer.py
def learning_rate(self):
#if not hasattr(self, "_learning_rate") or self._learning_rate is None:
# raise ValueError(
# "Missing learning rate, please set self.learning_rate at"
# " optimizer creation time."
# )
self._learning_rate = self._build_learning_rate(2e-5)
lr = self._learning_rate
if isinstance(lr, learning_rate_schedule.LearningRateSchedule):
# If the optimizer takes in LearningRateSchedule, then each call to
# learning_rate would return `self._current_learning_rate`, which is
# updated at each call to `apply_gradients`.
return self._current_learning_rate
return lr
修改文件optimization.py,修改内容如下
class AdamWeightDecayOptimizer(tf.optimizers.Optimizer):
修改文件run_classifier.py,修改内容如下
flags = tf.compat.v1.flags
flags.DEFINE_string(
"tpu_name", None,
"The Cloud TPU to use for training. This should be either the name "
"used when creating the Cloud TPU, or a grpc://ip.address.of.tpu:8470 "
"url.")
flags.DEFINE_string(
"tpu_zone", None,
"[Optional] GCE zone where the Cloud TPU is located in. If not "
"specified, we will attempt to automatically detect the GCE project from "
"metadata.")
flags.DEFINE_string(
"gcp_project", None,
"[Optional] Project name for the Cloud TPU-enabled project. If not "
"specified, we will attempt to automatically detect the GCE project from "
"metadata.")
flags.DEFINE_string("master", None, "[Optional] TensorFlow master URL.")
----------------------------------------------------------------------
tf.compat.v1.app.run()
tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.INFO)
tf.compat.v1.logging.info("Writing example %d of %d" % (ex_index, len(examples)))
tf.compat.v1.logging.info("*** Example ***")
tf.compat.v1.logging.info("guid: %s" % (example.guid))
tf.compat.v1.logging.info("tokens: %s" % " ".join(
[tokenization.printable_text(x) for x in tokens]))
tf.compat.v1.logging.info("input_ids: %s" % " ".join([str(x) for x in input_ids]))
tf.compat.v1.logging.info("input_mask: %s" % " ".join([str(x) for x in input_mask]))
tf.compat.v1.logging.info("segment_ids: %s" % " ".join([str(x) for x in segment_ids]))
tf.compat.v1.logging.info("label: %s (id = %d)" % (example.label, label_id))
tf.compat.v1.logging.info("***** Running training *****")
tf.compat.v1.logging.info(" Num examples = %d", len(train_examples))
tf.compat.v1.logging.info(" Batch size = %d", FLAGS.train_batch_size)
tf.compat.v1.logging.info(" Num steps = %d", num_train_steps)
tf.compat.v1.logging.info("*** Features ***")
for name in sorted(features.keys()):
tf.compat.v1.logging.info(" name = %s, shape = %s" % (name, features[name].shape))
-------------------------------------------------------------------
tf.io.gfile.MakeDirs(FLAGS.output_dir)
#tf.io.gfile.MakeDirs(FLAGS.output_dir)
if not os.path.exists(FLAGS.output_dir):
os.makedirs(FLAGS.output_dir)
with open(input_file, "r") as f:
-----------------------------------------------------------------------
is_per_host = tf.compat.v1.estimator.tpu.InputPipelineConfig.PER_HOST_V2
run_config = tf.compat.v1.estimator.tpu.RunConfig(
tpu_config=tf.compat.v1.estimator.tpu.TPUConfig(
estimator = tf.compat.v1.estimator.tpu.TPUEstimator(
-------------------------------------------------------------------------
writer = tf.compat.v1.python_io.TFRecordWriter(output_file)
-------------------------------------------------------------------------
name_to_features = {
"input_ids": tf.io.FixedLenFeature([seq_length], tf.int64),
"input_mask": tf.io.FixedLenFeature([seq_length], tf.int64),
"segment_ids": tf.io.FixedLenFeature([seq_length], tf.int64),
"label_ids": tf.io.FixedLenFeature([], tf.int64),
"is_real_example": tf.io.FixedLenFeature([], tf.int64),
}
-------------------------------------------------------------------------
# d = d.apply(
# tf.data.map_and_batch(
# lambda record: _decode_record(record, name_to_features),
# batch_size=batch_size,
# drop_remainder=drop_remainder))
d = d.map(lambda record: _decode_record(record, name_to_features))
d = d.batch(batch_size, drop_remainder)
-------------------------------------------------------------------------
example = tf.io.parse_single_example(record, name_to_features)
-------------------------------------------------------------------------
t = tf.cast(t, tf.int32)
hidden_size = tf.compat.v1.dimension_value(output_layer.shape[-1])
修改modeling.py,修改内容如下
with tf.io.gfile.GFile(json_file, "r") as reader:
with tf.compat.v1.variable_scope(scope, default_name="bert"):
with tf.compat.v1.variable_scope("embeddings"):
embedding_table = tf.compat.v1.get_variable(
return tf.compat.v1.truncated_normal_initializer(stddev=initializer_range)
token_type_table = tf.compat.v1.get_variable(
assert_op = tf.compat.v1.assert_less_equal(seq_length, max_position_embeddings)
full_position_embeddings = tf.compat.v1.get_variable(
layer_norma = tf.keras.layers.LayerNormalization(axis=-1, epsilon=1e-12, dtype=tf.float32)
return layer_norma(input_tensor)
with tf.compat.v1.variable_scope("encoder"):
with tf.compat.v1.variable_scope("layer_%d" % layer_idx):
with tf.compat.v1.variable_scope("attention"):
attention_heads = []
with tf.compat.v1.variable_scope("self"):
query_layer = tf.compat.v1.layers.dense(
from_tensor_2d,
num_attention_heads * size_per_head,
activation=query_act,
name="query",
kernel_initializer=create_initializer(initializer_range))
# `key_layer` = [B*T, N*H]
key_layer = tf.compat.v1.layers.dense(
to_tensor_2d,
num_attention_heads * size_per_head,
activation=key_act,
name="key",
kernel_initializer=create_initializer(initializer_range))
# `value_layer` = [B*T, N*H]
value_layer = tf.compat.v1.layers.dense(
to_tensor_2d,
num_attention_heads * size_per_head,
activation=value_act,
name="value",
kernel_initializer=create_initializer(initializer_range))
with tf.compat.v1.variable_scope("output"):
attention_output = tf.compat.v1.layers.dense(
attention_output,
hidden_size,
kernel_initializer=create_initializer(initializer_range))
attention_output = dropout(attention_output, hidden_dropout_prob)
attention_output = layer_norm(attention_output + layer_input)
# The activation is only applied to the "intermediate" hidden layer.
with tf.compat.v1.variable_scope("intermediate"):
intermediate_output = tf.compat.v1.layers.dense(
attention_output,
intermediate_size,
activation=intermediate_act_fn,
kernel_initializer=create_initializer(initializer_range))
# Down-project back to `hidden_size` then add the residual.
with tf.compat.v1.variable_scope("output"):
layer_output = tf.compat.v1.layers.dense(
intermediate_output,
hidden_size,
kernel_initializer=create_initializer(initializer_range))
layer_output = dropout(layer_output, hidden_dropout_prob)
layer_output = layer_norm(layer_output + attention_output)
prev_output = layer_output
all_layer_outputs.append(layer_output)
修改tokenization.py,修改内容如下
with tf.io.gfile.GFile(vocab_file, "r") as reader:
还有很多要做修改的地方就不一一列出了,具体可以参考https://zhuanlan.zhihu.com/p/495830763
- 模型训练
在bert主文件夹下执行
python run_classifier.py \
--task_name=MRPC \
--do_train=true \
--do_eval=true \
--data_dir=$GLUE_DIR/MRPC \
--vocab_file=$BERT_BASE_DIR/vocab.txt \
--bert_config_file=$BERT_BASE_DIR/bert_config.json \
--init_checkpoint=$BERT_BASE_DIR/bert_model.ckpt \
--max_seq_length=128 \
--train_batch_size=16 \
--learning_rate=2e-5 \
--num_train_epochs=3.0 \
--output_dir=/tmp/mrpc_output/
- 执行推理
设置环境变量
export TRAINED_CLASSIFIER=/tmp/mrpc_output/model.ckpt-764
修改代码modeling.py
# assignment_map[name] = name
assignment_map[name] = name_to_variable[name]
执行
python run_classifier.py \
--task_name=MRPC \
--do_predict=true \
--data_dir=$GLUE_DIR/MRPC \
--vocab_file=$BERT_BASE_DIR/vocab.txt \
--bert_config_file=$BERT_BASE_DIR/bert_config.json \
--init_checkpoint=$TRAINED_CLASSIFIER \
--max_seq_length=128 \
--output_dir=/tmp/mrpc_output/
模型预测结果会保存在/tmp/mrpc_output文件夹下的test_results.tsv中。
- 代码解析
run_classifier.py
程序开始会进行一系列的检测,然后是开始读取数据集
if FLAGS.do_train: # 如果是训练模式
# 读取训练样本
train_examples = processor.get_train_examples(FLAGS.data_dir)
# 获取全训练步骤(样本数量/batchsize*epochs)
num_train_steps = int(
len(train_examples) / FLAGS.train_batch_size * FLAGS.num_train_epochs)
# 先将学习率设置的比较小,经过warmup步之后进行还原,warmup就是全训练样本的一个比值数
num_warmup_steps = int(num_train_steps * FLAGS.warmup_proportion)
# bert模型创建,见后续代码
model_fn = model_fn_builder(
bert_config=bert_config,
num_labels=len(label_list),
init_checkpoint=FLAGS.init_checkpoint,
learning_rate=FLAGS.learning_rate,
num_train_steps=num_train_steps,
num_warmup_steps=num_warmup_steps,
use_tpu=FLAGS.use_tpu,
use_one_hot_embeddings=FLAGS.use_tpu)
if FLAGS.do_train:
# 转化为tensorflow数据格式的路径
train_file = os.path.join(FLAGS.output_dir, "train.tf_record")
# 将训练样本转化为tensorflow(tf_record)的数据格式,见后续代码
file_based_convert_examples_to_features(
train_examples, label_list, FLAGS.max_seq_length, tokenizer, train_file)
tf.compat.v1.logging.info("***** Running training *****")
tf.compat.v1.logging.info(" Num examples = %d", len(train_examples))
tf.compat.v1.logging.info(" Batch size = %d", FLAGS.train_batch_size)
tf.compat.v1.logging.info(" Num steps = %d", num_train_steps)
train_input_fn = file_based_input_fn_builder(
input_file=train_file,
seq_length=FLAGS.max_seq_length,
is_training=True,
drop_remainder=True)
estimator.train(input_fn=train_input_fn, max_steps=num_train_steps)
def model_fn_builder(bert_config, num_labels, init_checkpoint, learning_rate,
num_train_steps, num_warmup_steps, use_tpu,
use_one_hot_embeddings):
"""Returns `model_fn` closure for TPUEstimator."""
def model_fn(features, labels, mode, params): # pylint: disable=unused-argument
"""The `model_fn` for TPUEstimator."""
tf.compat.v1.logging.info("*** Features ***")
for name in sorted(features.keys()):
tf.compat.v1.logging.info(" name = %s, shape = %s" % (name, features[name].shape))
input_ids = features["input_ids"]
input_mask = features["input_mask"]
segment_ids = features["segment_ids"]
label_ids = features["label_ids"]
is_real_example = None
if "is_real_example" in features:
is_real_example = tf.cast(features["is_real_example"], dtype=tf.float32)
else:
is_real_example = tf.ones(tf.shape(label_ids), dtype=tf.float32)
is_training = (mode == tf.estimator.ModeKeys.TRAIN)
# 整个的Transformer模型的创建,见后续代码
(total_loss, per_example_loss, logits, probabilities) = create_model(
bert_config, is_training, input_ids, input_mask, segment_ids, label_ids,
num_labels, use_one_hot_embeddings)
tvars = tf.compat.v1.trainable_variables()
initialized_variable_names = {}
scaffold_fn = None
if init_checkpoint:
(assignment_map, initialized_variable_names
) = modeling.get_assignment_map_from_checkpoint(tvars, init_checkpoint)
if use_tpu:
def tpu_scaffold():
tf.train.init_from_checkpoint(init_checkpoint, assignment_map)
return tf.train.Scaffold()
scaffold_fn = tpu_scaffold
else:
tf.compat.v1.train.init_from_checkpoint(init_checkpoint, assignment_map)
tf.compat.v1.logging.info("**** Trainable Variables ****")
for var in tvars:
init_string = ""
if var.name in initialized_variable_names:
init_string = ", *INIT_FROM_CKPT*"
tf.compat.v1.logging.info(" name = %s, shape = %s%s", var.name, var.shape,
init_string)
output_spec = None
if mode == tf.estimator.ModeKeys.TRAIN:
train_op = optimization.create_optimizer(
total_loss, learning_rate, num_train_steps, num_warmup_steps, use_tpu)
output_spec = tf.compat.v1.estimator.tpu.TPUEstimatorSpec(
mode=mode,
loss=total_loss,
train_op=train_op,
scaffold_fn=scaffold_fn)
elif mode == tf.estimator.ModeKeys.EVAL:
def metric_fn(per_example_loss, label_ids, logits, is_real_example):
predictions = tf.argmax(logits, axis=-1, output_type=tf.int32)
accuracy = tf.compat.v1.metrics.accuracy(
labels=label_ids, predictions=predictions, weights=is_real_example)
loss = tf.compat.v1.metrics.mean(values=per_example_loss, weights=is_real_example)
return {
"eval_accuracy": accuracy,
"eval_loss": loss,
}
eval_metrics = (metric_fn,
[per_example_loss, label_ids, logits, is_real_example])
output_spec = tf.compat.v1.estimator.tpu.TPUEstimatorSpec(
mode=mode,
loss=total_loss,
eval_metrics=eval_metrics,
scaffold_fn=scaffold_fn)
else:
output_spec = tf.compat.v1.estimator.tpu.TPUEstimatorSpec(
mode=mode,
predictions={"probabilities": probabilities},
scaffold_fn=scaffold_fn)
return output_spec
return model_fn
def create_model(bert_config, is_training, input_ids, input_mask, segment_ids,
labels, num_labels, use_one_hot_embeddings):
"""创建一个分类模型.见后续代码"""
model = modeling.BertModel(
config=bert_config, # 配置文件
is_training=is_training, # 是否训练
input_ids=input_ids, # 构建数据时的字典id,维度为(batchsize,句子的最大长度,一般为128)
input_mask=input_mask, # 构建数据时的掩码,维度同上
token_type_ids=segment_ids, # 构建数据时的区分标识,维度同上
use_one_hot_embeddings=use_one_hot_embeddings) # 对数据集是否使用独热编码embeddings,为TPU加速专用,我们不用考虑
# In the demo, we are doing a simple classification task on the entire
# segment.
#
# If you want to use the token-level output, use model.get_sequence_output()
# instead.
output_layer = model.get_pooled_output()
# 768
hidden_size = tf.compat.v1.dimension_value(output_layer.shape[-1])
# 生成初始参数的权重
output_weights = tf.compat.v1.get_variable(
"output_weights", [num_labels, hidden_size],
initializer=tf.compat.v1.truncated_normal_initializer(stddev=0.02))
# 生成初始参数的偏置
output_bias = tf.compat.v1.get_variable(
"output_bias", [num_labels], initializer=tf.zeros_initializer())
# 损失函数
with tf.compat.v1.variable_scope("loss"):
if is_training:
# I.e., 0.1 dropout
output_layer = tf.nn.dropout(output_layer, rate=0.1)
# 输出数据乘以权重
logits = tf.matmul(output_layer, output_weights, transpose_b=True)
# 再加上偏置
logits = tf.nn.bias_add(logits, output_bias)
probabilities = tf.nn.softmax(logits, axis=-1)
log_probs = tf.nn.log_softmax(logits, axis=-1)
one_hot_labels = tf.one_hot(labels, depth=num_labels, dtype=tf.float32)
# 交叉熵损失函数
per_example_loss = -tf.reduce_sum(one_hot_labels * log_probs, axis=-1)
loss = tf.reduce_mean(per_example_loss)
return (loss, per_example_loss, logits, probabilities)
def file_based_convert_examples_to_features(
examples, label_list, max_seq_length, tokenizer, output_file):
"""Convert a set of `InputExample`s to a TFRecord file."""
# tf-record写入器
writer = tf.compat.v1.python_io.TFRecordWriter(output_file)
# 遍历所有的样本数据
for (ex_index, example) in enumerate(examples):
# 每遍历10000行打印日志
if ex_index % 10000 == 0:
tf.compat.v1.logging.info("Writing example %d of %d" % (ex_index, len(examples)))
# 单行特征提取,见后续代码
feature = convert_single_example(ex_index, example, label_list,
max_seq_length, tokenizer)
def create_int_feature(values):
f = tf.train.Feature(int64_list=tf.train.Int64List(value=list(values)))
return f
# 对单行特征做整型转换
features = collections.OrderedDict()
features["input_ids"] = create_int_feature(feature.input_ids)
features["input_mask"] = create_int_feature(feature.input_mask)
features["segment_ids"] = create_int_feature(feature.segment_ids)
features["label_ids"] = create_int_feature([feature.label_id])
features["is_real_example"] = create_int_feature(
[int(feature.is_real_example)])
# 对单行特征做tf_record转换
tf_example = tf.train.Example(features=tf.train.Features(feature=features))
# 对转换后的内容进行序列化并写入到文件tf_record文件中
writer.write(tf_example.SerializeToString())
writer.close()
def convert_single_example(ex_index, example, label_list, max_seq_length,
tokenizer):
"""Converts a single `InputExample` into a single `InputFeatures`."""
if isinstance(example, PaddingInputExample):
return InputFeatures(
input_ids=[0] * max_seq_length,
input_mask=[0] * max_seq_length,
segment_ids=[0] * max_seq_length,
label_id=0,
is_real_example=False)
# 构建数据标签字典,在MRPC中只有0和1两种标签dict{'0':0, '1':1}
label_map = {}
for (i, label) in enumerate(label_list):
label_map[label] = i
# 第一句话分词,见后续代码
tokens_a = tokenizer.tokenize(example.text_a)
tokens_b = None
if example.text_b:
# 第二句话分词,因为我们在训练的过程中是对两句话做比较,但是在推理的时候可能是只有一句话的
tokens_b = tokenizer.tokenize(example.text_b)
if tokens_b:
# Modifies `tokens_a` and `tokens_b` in place so that the total
# length is less than the specified length.
# Account for [CLS], [SEP], [SEP] with "- 3"
# 如果两句话加起来太长,会进行一个截断操作,并且有第二句话的时候保留三个特殊字符
_truncate_seq_pair(tokens_a, tokens_b, max_seq_length - 3)
else:
# Account for [CLS] and [SEP] with "- 2"
# 如果没有第二句话,只保留两个特殊字符
if len(tokens_a) > max_seq_length - 2:
tokens_a = tokens_a[0:(max_seq_length - 2)]
tokens = [] # 两句话的所有词都会保存在该列表中
segment_ids = [] # 对第一句话和第二句话中的词进行区分的标识,第一句中为0,第二局中为1
tokens.append("[CLS]") # 第一句话以特殊字符[CLS]开头
segment_ids.append(0)
for token in tokens_a:
tokens.append(token) # 添加第一句话中的所有词
segment_ids.append(0)
tokens.append("[SEP]") # 第一句话以特殊字符[SEP]结尾
segment_ids.append(0)
if tokens_b:
for token in tokens_b:
tokens.append(token) # 添加第二句话中的所有词
segment_ids.append(1)
tokens.append("[SEP]") # 第二句话以特殊字符[SEP]结尾
segment_ids.append(1)
# 将字符(词)转换成字典中的id
input_ids = tokenizer.convert_tokens_to_ids(tokens)
# The mask has 1 for real tokens and 0 for padding tokens. Only real
# tokens are attended to.
# 指定真实的词的掩码为1,做自注意力机制时要用,忽略因为长度不够而补充的0
input_mask = [1] * len(input_ids)
# Zero-pad up to the sequence length.
# 当句子的长度不足最大长度时,用0补充
while len(input_ids) < max_seq_length:
input_ids.append(0)
input_mask.append(0)
segment_ids.append(0)
# 断言,检测字典id、掩码、标识列表是否达到最大长度
assert len(input_ids) == max_seq_length
assert len(input_mask) == max_seq_length
assert len(segment_ids) == max_seq_length
# 获取训练样本的标签
label_id = label_map[example.label]
# 对以上信息进行打印
if ex_index < 5:
tf.compat.v1.logging.info("*** Example ***")
tf.compat.v1.logging.info("guid: %s" % (example.guid))
tf.compat.v1.logging.info("tokens: %s" % " ".join(
[tokenization.printable_text(x) for x in tokens]))
tf.compat.v1.logging.info("input_ids: %s" % " ".join([str(x) for x in input_ids]))
tf.compat.v1.logging.info("input_mask: %s" % " ".join([str(x) for x in input_mask]))
tf.compat.v1.logging.info("segment_ids: %s" % " ".join([str(x) for x in segment_ids]))
tf.compat.v1.logging.info("label: %s (id = %d)" % (example.label, label_id))
# 对以上所有信息保存到一个特征对象中
feature = InputFeatures(
input_ids=input_ids,
input_mask=input_mask,
segment_ids=segment_ids,
label_id=label_id,
is_real_example=True)
return feature
modeling.py
class BertModel(object):
def __init__(self,
config,
is_training,
input_ids,
input_mask=None,
token_type_ids=None,
use_one_hot_embeddings=False,
scope=None):
# 读取配置文件
config = copy.deepcopy(config)
if not is_training:
config.hidden_dropout_prob = 0.0
config.attention_probs_dropout_prob = 0.0
# 获取字典id(batchsize,最大长度128)
input_shape = get_shape_list(input_ids, expected_rank=2)
batch_size = input_shape[0]
seq_length = input_shape[1]
# 检查是否做了掩码操作,如果没有做会自动添加一个全部为1的掩码(batchsize,最大长度128)
if input_mask is None:
input_mask = tf.ones(shape=[batch_size, seq_length], dtype=tf.int32)
# 检查是否做了标识操作,如果没有做自动添加一个全部为0的标识(batchsize,最大长度128)
if token_type_ids is None:
token_type_ids = tf.zeros(shape=[batch_size, seq_length], dtype=tf.int32)
# 构建bert模型
with tf.compat.v1.variable_scope(scope, default_name="bert"):
# 第一个是embedding(词嵌入)层,包括词本身,句子标识以及位置信息
with tf.compat.v1.variable_scope("embeddings"):
# 对词id进行嵌入查找,见后续代码.
(self.embedding_output, self.embedding_table) = embedding_lookup(
input_ids=input_ids, # 输入的词(字典中的id)
vocab_size=config.vocab_size, # 语料库(字典)的数量
embedding_size=config.hidden_size, # 嵌入的词向量维度,这里为768
initializer_range=config.initializer_range, # 初始化取值范围
word_embedding_name="word_embeddings", # 词嵌入名称
use_one_hot_embeddings=use_one_hot_embeddings) # 是否使用独热编码嵌入,这里为不使用
# 添加位置嵌入和标记类型嵌入,然后分层
# 规范化并执行丢弃
self.embedding_output = embedding_postprocessor(
input_tensor=self.embedding_output,
use_token_type=True,
token_type_ids=token_type_ids,
token_type_vocab_size=config.type_vocab_size,
token_type_embedding_name="token_type_embeddings",
use_position_embeddings=True,
position_embedding_name="position_embeddings",
initializer_range=config.initializer_range,
max_position_embeddings=config.max_position_embeddings,
dropout_prob=config.hidden_dropout_prob)
# 第二个是Transformer编码器层
with tf.compat.v1.variable_scope("encoder"):
# This converts a 2D mask of shape [batch_size, seq_length] to a 3D
# mask of shape [batch_size, seq_length, seq_length] which is used
# for the attention scores.
attention_mask = create_attention_mask_from_input_mask(
input_ids, input_mask)
# Run the stacked transformer.
# `sequence_output` shape = [batch_size, seq_length, hidden_size].
# Tansformer编码器模型,见后续代码
self.all_encoder_layers = transformer_model(
input_tensor=self.embedding_output,
attention_mask=attention_mask,
hidden_size=config.hidden_size,
num_hidden_layers=config.num_hidden_layers,
num_attention_heads=config.num_attention_heads,
intermediate_size=config.intermediate_size,
intermediate_act_fn=get_activation(config.hidden_act),
hidden_dropout_prob=config.hidden_dropout_prob,
attention_probs_dropout_prob=config.attention_probs_dropout_prob,
initializer_range=config.initializer_range,
do_return_all_layers=True)
# 获取编码器层返回的结果
self.sequence_output = self.all_encoder_layers[-1]
# The "pooler" converts the encoded sequence tensor of shape
# [batch_size, seq_length, hidden_size] to a tensor of shape
# [batch_size, hidden_size]. This is necessary for segment-level
# (or segment-pair-level) classification tasks where we need a fixed
# dimensional representation of the segment.
with tf.compat.v1.variable_scope("pooler"):
# We "pool" the model by simply taking the hidden state corresponding
# to the first token. We assume that this has been pre-trained
# 获取起始字符[CLS]
first_token_tensor = tf.squeeze(self.sequence_output[:, 0:1, :], axis=1)
self.pooled_output = tf.compat.v1.layers.dense(
first_token_tensor,
config.hidden_size,
activation=tf.tanh,
kernel_initializer=create_initializer(config.initializer_range))
def embedding_lookup(input_ids,
vocab_size,
embedding_size=128,
initializer_range=0.02,
word_embedding_name="word_embeddings",
use_one_hot_embeddings=False):
"""Looks up words embeddings for id tensor.
Args:
input_ids: int32 Tensor of shape [batch_size, seq_length] containing word
ids.
vocab_size: int. Size of the embedding vocabulary.
embedding_size: int. Width of the word embeddings.
initializer_range: float. Embedding initialization range.
word_embedding_name: string. Name of the embedding table.
use_one_hot_embeddings: bool. If True, use one-hot method for word
embeddings. If False, use `tf.gather()`.
Returns:
float Tensor of shape [batch_size, seq_length, embedding_size].
"""
# This function assumes that the input is of shape [batch_size, seq_length,
# num_inputs].
#
# If the input is a 2D tensor of shape [batch_size, seq_length], we
# reshape to [batch_size, seq_length, 1].
# 将所有batch_size个句子(2维(batch_size,最大句子长度))扩展一个维度,变成3维的输入
if input_ids.shape.ndims == 2:
input_ids = tf.expand_dims(input_ids, axis=[-1])
# 创建一个嵌入表格
embedding_table = tf.compat.v1.get_variable(
name=word_embedding_name,
shape=[vocab_size, embedding_size],
initializer=create_initializer(initializer_range))
# 将该3维的输入变成1维的输入
flat_input_ids = tf.reshape(input_ids, [-1])
if use_one_hot_embeddings:
one_hot_input_ids = tf.one_hot(flat_input_ids, depth=vocab_size)
output = tf.matmul(one_hot_input_ids, embedding_table)
else:
# 网格化操作嵌入表格和输入作为输出
output = tf.gather(embedding_table, flat_input_ids)
input_shape = get_shape_list(input_ids)
# 转换输出格式
output = tf.reshape(output,
input_shape[0:-1] + [input_shape[-1] * embedding_size])
return (output, embedding_table)
def transformer_model(input_tensor,
attention_mask=None,
hidden_size=768,
num_hidden_layers=12,
num_attention_heads=12,
intermediate_size=3072,
intermediate_act_fn=gelu,
hidden_dropout_prob=0.1,
attention_probs_dropout_prob=0.1,
initializer_range=0.02,
do_return_all_layers=False):
"""Multi-headed, multi-layer Transformer from "Attention is All You Need".
This is almost an exact implementation of the original Transformer encoder.
See the original paper:
https://arxiv.org/abs/1706.03762
Also see:
https://github.com/tensorflow/tensor2tensor/blob/master/tensor2tensor/models/transformer.py
Args:
input_tensor: float Tensor of shape [batch_size, seq_length, hidden_size].
attention_mask: (optional) int32 Tensor of shape [batch_size, seq_length,
seq_length], with 1 for positions that can be attended to and 0 in
positions that should not be.
hidden_size: int. Hidden size of the Transformer.
num_hidden_layers: int. Number of layers (blocks) in the Transformer.
num_attention_heads: int. Number of attention heads in the Transformer.
intermediate_size: int. The size of the "intermediate" (a.k.a., feed
forward) layer.
intermediate_act_fn: function. The non-linear activation function to apply
to the output of the intermediate/feed-forward layer.
hidden_dropout_prob: float. Dropout probability for the hidden layers.
attention_probs_dropout_prob: float. Dropout probability of the attention
probabilities.
initializer_range: float. Range of the initializer (stddev of truncated
normal).
do_return_all_layers: Whether to also return all layers or just the final
layer.
Returns:
float Tensor of shape [batch_size, seq_length, hidden_size], the final
hidden layer of the Transformer.
Raises:
ValueError: A Tensor shape or parameter is invalid.
"""
# 如果编码器的层数不是头数的整数倍,报错
if hidden_size % num_attention_heads != 0:
raise ValueError(
"The hidden size (%d) is not a multiple of the number of attention "
"heads (%d)" % (hidden_size, num_attention_heads))
attention_head_size = int(hidden_size / num_attention_heads)
input_shape = get_shape_list(input_tensor, expected_rank=3)
batch_size = input_shape[0]
seq_length = input_shape[1]
input_width = input_shape[2]
# The Transformer performs sum residuals on all layers so the input needs
# to be the same as the hidden size.
if input_width != hidden_size:
raise ValueError("The width of the input tensor (%d) != hidden size (%d)" %
(input_width, hidden_size))
# We keep the representation as a 2D tensor to avoid re-shaping it back and
# forth from a 3D tensor to a 2D tensor. Re-shapes are normally free on
# the GPU/CPU but may not be free on the TPU, so we want to minimize them to
# help the optimizer.
# 对输入的张量进行转换,由(batch_size,句子最大长度128,输出词向量维度768)转成(batch_size*128,768)
prev_output = reshape_to_matrix(input_tensor)
all_layer_outputs = []
# 遍历每一个自注意力编码器层(这里是12层)
for layer_idx in range(num_hidden_layers):
with tf.compat.v1.variable_scope("layer_%d" % layer_idx):
layer_input = prev_output
with tf.compat.v1.variable_scope("attention"):
attention_heads = []
# 自注意力机制,同一个句子中的词做attention
with tf.compat.v1.variable_scope("self"):
# 注意力层,得到每一个头的概率值,见后续代码
attention_head = attention_layer(
# 交互的输入都相同
from_tensor=layer_input,
to_tensor=layer_input,
attention_mask=attention_mask, # 注意力掩码
num_attention_heads=num_attention_heads, # 注意力头数
size_per_head=attention_head_size,
attention_probs_dropout_prob=attention_probs_dropout_prob,
initializer_range=initializer_range,
do_return_2d_tensor=True,
batch_size=batch_size,
from_seq_length=seq_length,
to_seq_length=seq_length)
attention_heads.append(attention_head)
attention_output = None
# 如果只有一个头
if len(attention_heads) == 1:
attention_output = attention_heads[0]
else: # 多头
# In the case where we have other sequences, we just concatenate
# them to the self-attention head before the projection.
# 将所有头的概率值α_i'相加,得到总的概率值α'
attention_output = tf.concat(attention_heads, axis=-1)
# Run a linear projection of `hidden_size` then add a residual
# with `layer_input`.
# 输出b
with tf.compat.v1.variable_scope("output"):
attention_output = tf.compat.v1.layers.dense(
attention_output,
hidden_size, # 3072
kernel_initializer=create_initializer(initializer_range)) # W^o矩阵
attention_output = dropout(attention_output, hidden_dropout_prob)
# 残差连接加层归一化
attention_output = layer_norm(attention_output + layer_input)
# The activation is only applied to the "intermediate" hidden layer.
# 前馈神经网络,为下一层输入做准备,重新转换为768
with tf.compat.v1.variable_scope("intermediate"):
intermediate_output = tf.compat.v1.layers.dense(
attention_output,
intermediate_size, # 768
activation=intermediate_act_fn,
kernel_initializer=create_initializer(initializer_range))
# Down-project back to `hidden_size` then add the residual.
with tf.compat.v1.variable_scope("output"):
layer_output = tf.compat.v1.layers.dense(
intermediate_output,
hidden_size,
kernel_initializer=create_initializer(initializer_range))
layer_output = dropout(layer_output, hidden_dropout_prob)
# 残差连接加层归一化
layer_output = layer_norm(layer_output + attention_output)
# 当前层的输出作为下一层的输入
prev_output = layer_output
all_layer_outputs.append(layer_output)
# 是否返回所有层
if do_return_all_layers:
final_outputs = []
for layer_output in all_layer_outputs:
final_output = reshape_from_matrix(layer_output, input_shape)
final_outputs.append(final_output)
return final_outputs
else: # 只返回最后一层的结果
final_output = reshape_from_matrix(prev_output, input_shape)
return final_output
def attention_layer(from_tensor,
to_tensor,
attention_mask=None,
num_attention_heads=1,
size_per_head=512,
query_act=None,
key_act=None,
value_act=None,
attention_probs_dropout_prob=0.0,
initializer_range=0.02,
do_return_2d_tensor=False,
batch_size=None,
from_seq_length=None,
to_seq_length=None):
"""Performs multi-headed attention from `from_tensor` to `to_tensor`.
This is an implementation of multi-headed attention based on "Attention
is all you Need". If `from_tensor` and `to_tensor` are the same, then
this is self-attention. Each timestep in `from_tensor` attends to the
corresponding sequence in `to_tensor`, and returns a fixed-with vector.
This function first projects `from_tensor` into a "query" tensor and
`to_tensor` into "key" and "value" tensors. These are (effectively) a list
of tensors of length `num_attention_heads`, where each tensor is of shape
[batch_size, seq_length, size_per_head].
Then, the query and key tensors are dot-producted and scaled. These are
softmaxed to obtain attention probabilities. The value tensors are then
interpolated by these probabilities, then concatenated back to a single
tensor and returned.
In practice, the multi-headed attention are done with transposes and
reshapes rather than actual separate tensors.
Args:
from_tensor: float Tensor of shape [batch_size, from_seq_length,
from_width].
to_tensor: float Tensor of shape [batch_size, to_seq_length, to_width].
attention_mask: (optional) int32 Tensor of shape [batch_size,
from_seq_length, to_seq_length]. The values should be 1 or 0. The
attention scores will effectively be set to -infinity for any positions in
the mask that are 0, and will be unchanged for positions that are 1.
num_attention_heads: int. Number of attention heads.
size_per_head: int. Size of each attention head.
query_act: (optional) Activation function for the query transform.
key_act: (optional) Activation function for the key transform.
value_act: (optional) Activation function for the value transform.
attention_probs_dropout_prob: (optional) float. Dropout probability of the
attention probabilities.
initializer_range: float. Range of the weight initializer.
do_return_2d_tensor: bool. If True, the output will be of shape [batch_size
* from_seq_length, num_attention_heads * size_per_head]. If False, the
output will be of shape [batch_size, from_seq_length, num_attention_heads
* size_per_head].
batch_size: (Optional) int. If the input is 2D, this might be the batch size
of the 3D version of the `from_tensor` and `to_tensor`.
from_seq_length: (Optional) If the input is 2D, this might be the seq length
of the 3D version of the `from_tensor`.
to_seq_length: (Optional) If the input is 2D, this might be the seq length
of the 3D version of the `to_tensor`.
Returns:
float Tensor of shape [batch_size, from_seq_length,
num_attention_heads * size_per_head]. (If `do_return_2d_tensor` is
true, this will be of shape [batch_size * from_seq_length,
num_attention_heads * size_per_head]).
Raises:
ValueError: Any of the arguments or tensor shapes are invalid.
"""
def transpose_for_scores(input_tensor, batch_size, num_attention_heads,
seq_length, width):
output_tensor = tf.reshape(
input_tensor, [batch_size, seq_length, num_attention_heads, width])
output_tensor = tf.transpose(output_tensor, [0, 2, 1, 3])
return output_tensor
# 保证自注意力机制中比较张量具有相同的形状
from_shape = get_shape_list(from_tensor, expected_rank=[2, 3])
to_shape = get_shape_list(to_tensor, expected_rank=[2, 3])
if len(from_shape) != len(to_shape):
raise ValueError(
"The rank of `from_tensor` must match the rank of `to_tensor`.")
if len(from_shape) == 3:
batch_size = from_shape[0]
from_seq_length = from_shape[1]
to_seq_length = to_shape[1]
elif len(from_shape) == 2:
if (batch_size is None or from_seq_length is None or to_seq_length is None):
raise ValueError(
"When passing in rank 2 tensors to attention_layer, the values "
"for `batch_size`, `from_seq_length`, and `to_seq_length` "
"must all be specified.")
# Scalar dimensions referenced here:
# B = batch size (number of sequences) 我这里是16
# F = `from_tensor` sequence length 128
# T = `to_tensor` sequence length 128
# N = `num_attention_heads` 12头数
# H = `size_per_head` 64每个头数的特征数
from_tensor_2d = reshape_to_matrix(from_tensor) # 2048*768
to_tensor_2d = reshape_to_matrix(to_tensor) # 2048*768
# `query_layer` = [B*F, N*H]
# q
query_layer = tf.compat.v1.layers.dense(
from_tensor_2d, # a^1
num_attention_heads * size_per_head, # q的输出维度,这里是768
activation=query_act,
name="query",
kernel_initializer=create_initializer(initializer_range)) # q矩阵,即W^q
# `key_layer` = [B*T, N*H]
# k
key_layer = tf.compat.v1.layers.dense(
to_tensor_2d, # a^x
num_attention_heads * size_per_head, # k的输出维度,这里是768
activation=key_act,
name="key",
kernel_initializer=create_initializer(initializer_range)) # k矩阵,即W^k
# `value_layer` = [B*T, N*H]
# v
value_layer = tf.compat.v1.layers.dense(
to_tensor_2d, # a^x
num_attention_heads * size_per_head, # v的输出维度,这里是768
activation=value_act,
name="value",
kernel_initializer=create_initializer(initializer_range)) # v矩阵,即W^v
# `query_layer` = [B, N, F, H]
# 为了加速点乘计算做的位置变化(16,12,128,64)
query_layer = transpose_for_scores(query_layer, batch_size,
num_attention_heads, from_seq_length,
size_per_head)
# `key_layer` = [B, N, T, H]
# 同上
key_layer = transpose_for_scores(key_layer, batch_size, num_attention_heads,
to_seq_length, size_per_head)
# Take the dot product between "query" and "key" to get the raw
# attention scores.
# `attention_scores` = [B, N, F, T]
# 点乘计算得到α
attention_scores = tf.matmul(query_layer, key_layer, transpose_b=True)
# 消除维度的影响
attention_scores = tf.multiply(attention_scores,
1.0 / math.sqrt(float(size_per_head)))
# 考虑掩码
if attention_mask is not None:
# `attention_mask` = [B, 1, F, T]
attention_mask = tf.expand_dims(attention_mask, axis=[1])
# Since attention_mask is 1.0 for positions we want to attend and 0.0 for
# masked positions, this operation will create a tensor which is 0.0 for
# positions we want to attend and -10000.0 for masked positions.
# 当掩码为1时正常计算,当掩码为0时转成一个很大的负数,经过softmax后,该负数的概率值为0
adder = (1.0 - tf.cast(attention_mask, tf.float32)) * -10000.0
# Since we are adding it to the raw scores before the softmax, this is
# effectively the same as removing these entirely.
attention_scores += adder
# Normalize the attention scores to probabilities.
# `attention_probs` = [B, N, F, T]
# α经过softmax变成α'(概率值)
attention_probs = tf.nn.softmax(attention_scores)
# This is actually dropping out entire tokens to attend to, which might
# seem a bit unusual, but is taken from the original Transformer paper.
attention_probs = dropout(attention_probs, attention_probs_dropout_prob)
# `value_layer` = [B, T, N, H]
# 为加速计算做转换
value_layer = tf.reshape(
value_layer,
[batch_size, to_seq_length, num_attention_heads, size_per_head])
# `value_layer` = [B, N, T, H]
value_layer = tf.transpose(value_layer, [0, 2, 1, 3])
# `context_layer` = [B, N, F, H]
# α点乘v得到b,这些都是以加速为目的的矩阵运算
context_layer = tf.matmul(attention_probs, value_layer)
# `context_layer` = [B, F, N, H]
context_layer = tf.transpose(context_layer, [0, 2, 1, 3])
# 转换成2048*768为下一层的输入做准备
if do_return_2d_tensor:
# `context_layer` = [B*F, N*H]
context_layer = tf.reshape(
context_layer,
[batch_size * from_seq_length, num_attention_heads * size_per_head])
else:
# `context_layer` = [B, F, N*H]
context_layer = tf.reshape(
context_layer,
[batch_size, from_seq_length, num_attention_heads * size_per_head])
return context_layer
tokenization.py
class FullTokenizer(object):
"""Runs end-to-end tokenziation."""
def __init__(self, vocab_file, do_lower_case=True):
self.vocab = load_vocab(vocab_file)
self.inv_vocab = {v: k for k, v in self.vocab.items()}
self.basic_tokenizer = BasicTokenizer(do_lower_case=do_lower_case)
self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab)
def tokenize(self, text):
split_tokens = []
# 对一句话进行切分
for token in self.basic_tokenizer.tokenize(text):
# 对一句话中的词再次进行wordpiece切分,如amrozi切分成am,##ro,##zi
for sub_token in self.wordpiece_tokenizer.tokenize(token):
split_tokens.append(sub_token)
return split_tokens
def convert_tokens_to_ids(self, tokens):
return convert_by_vocab(self.vocab, tokens)
def convert_ids_to_tokens(self, ids):
return convert_by_vocab(self.inv_vocab, ids)
LLaMA
GPT一代
模型堆叠了12个transformer的解码器层。由于这种设置中没有编码器,这些解码器层将不会有普通transformer解码器层所具有的编码器-解码器交叉注意力层。但是它扔具有自注意力层。
上图中的GPT包含了12个右边的Decode结构。它没有层级间的交叉注意力机制,只有掩码自注意力机制。
输入:
U = {\(u_1,...,u_n\)} 这里的\(u_i\)是一个一个的词向量,比如"我爱北京天安门",那么\(u_1\)就是"我",\(u_2\)就是"爱",\(u_3\)就是"北京",\(u_4\)就是"天安门"。
\(h_0 = UW_e + W_p\) 神经网络
\(h_l = transformer\_block(h_{l-1}) ∀i∈[1,n]\) 上一层的神经网络需要经过一个trannsformer_block抵达下一层神经网络
输出:
\(P(u) = softmax(h_nW_e^T)\)
- 训练过程
我们的训练目标就是让某一个词在某个句子中出现的概率最大化,这其实就是一个完形填空。
北京是中国的_____。 (首都)
那我们的目标函数就为
\(L_1(U) = \sum_ilogP(u_i|u_{i-k},...,u_{i-1};θ)\) θ是神经网络参数
这里的\(u_i\)就是"首都",条件概率中的条件\(u_{i-k},...,u_{i-1}\)就是"北京"、"是"、"中国"、"的"。我们的目的就是要使得概率P最大化。
如此,我们就可以通过语料对该模型去进行一个训练。训练完了之后所得到的参数就可以去做推理测试。而LLaMA用到的语料包含了数万亿个词。
LLaMA背景介绍
LLaMA是一个基础语言模型的集合,参数范围从7B到65B。
- 这些模型是在来自公开数据集的数万亿个tokens上训练的。
- 它由Meta(前Facebook)于2023年2月发布,作为致力于开放科学和人工智能实践的一部分。
- 它与其他大型语言模型的关联
- LLaMA与GPT、GPT-3、Chinchilla和PaLM等其他大型语言模型类似,因为它使用Transformer architecture来预测给定单词或token序列作为输入的下一个单词或token。
- 然而,LLaMA与其他模型的不同之处在于,它使用在更多token上训练,得到较小模型,这使它更高效,资源密集度更低。
- LLaMA发展史
InstructGPT(基于提示学习的一系列模型) -> GPT3.5时代(大规模预训练语言模型,参数量超过1750亿) -> ChatGPT模型(高质量数据标注以及反馈学习(强化学习) -> LLaMA
- LLaMA的特点
- 参数量和训练语料:LLaMA有四种尺寸,7B、13B、33B和65B参数。最小的模型LLaMA 7B在一万亿个tokens上进行训练,而最大的模型LLaMA 65B在1.4万亿个tokens上训练。
- 语种:LLaMA涵盖了20种使用者最多的语言,重点是那些使用拉丁字母和西里尔字母的语言。这些语言包含英语、西班牙语、法语、俄语、阿拉伯语、印地语、汉语等。
- 生成方式:和GPT一样。
- 所需资源更小:LLaMA比其他模型更高效,资源密集度更低,因为它使用在更多tokens上训练的较小模型。这意味着它需要更少的计算能力和资源来训练和运行这些模型,也需要更少的内存和带宽来存储和传输它们。例如LLaMA 13B在大多数基准测试中都优于GPT-3(175B),而只使用了约7%的参数。
- 它对研究界很重要
- 它能够在人工智能领域实现更多的可访问性和个性化(垂直领域)。
- 通过共享LLaMA的代码和模型,Meta允许其他无法访问大量基础设施的研究人员研究,验证和改进这些模型,并探索新的用例和应用程序。
- 开源!
训练方式与训练数据
LLaMA模型训练方法和GPT-3差不多,都是自回归的方式(依据前/后出现的子词来预测当前时刻的子词)。在大量的语料中,使用标准的transformer优化器进行模型的训练。
- 数据集
LLaMA是用Common Crawl这个大规模的网络文本数据集和其他开源数据集来训练的。Common Crawl是一个公开的网络文本数据集,它包含了从2008年开始收集的数千亿个网页的原始数据、元数据和文本提取。另外进行了一些预处理,来确保数据的质量要求:
使用了fastText线性分类器执行语言识别以删除非英语页面,使用n-gram语言模型过滤低质量内容。
下载地址(42B tokens,300d vectors,1.75G):https://huggingface.co/stanfordnlp/glove/resolve/main/glove.42B.300d.zip
下载地址(840B tokens,300d vectors,2.03G):https://huggingface.co/stanfordnlp/glove/resolve/main/glove.840B.300d.zip
训练数据集是多个来源混合,如下表所示,涵盖了不同的领域
数据集 | 采样比例 | 训练轮数 | 数据集大小 |
CommonCrawl | 67.0% | 1.10 | 3.3T |
C4 | 15.0% | 1.06 | 783G |
Github | 4.5% | 0.64 | 328G |
Wikipedia | 4.5% | 2.45 | 83G |
Books | 4.5% | 2.23 | 85G |
ArXiv | 2.5% | 1.06 | 92G |
StackExchange | 2.0% | 1.03 | 78G |
C4数据集是一个巨大的、清洗过的Common Crawl网络爬取语料库的版本。另外进行了一些不同的预处理,包含去重和语言识别步骤,与CommmonCrawl的主要区别在于质量过滤,它主要依赖于启发式方法,例如对网页中的标点符号的过滤,或者限制单词和句子的数量。
GitHub是使用Google BigQuery上可用的公共GitHub数据集。只保留在Apache、BSD和MIT许可证下分发的项目。根据行长或字母数字字符的比例使用启发式方法过滤了低质量文件。在文件级别对生成的数据集进行重复数据删除。
Wikipedia添加了2022年6月至8月期间的维基百科数据,涵盖20种语言,使用拉丁文或西里尔文脚本。
以上这些数据集的下载地址可以参考https://zhuanlan.zhihu.com/p/612243919?utm_id=0
模型结构
LLaMA的网络也是基于Transformer架构。并且对Trannsformer架构进行了部分改进。
- LLaMA方法——Pre-normalization
为了提高训练稳定性,对每个Transformer子层的输入进行归一化,而不是对输出进行归一化。这个叫RMSNorm归一化函数。
上图是传统的Transformer的编解码器,无论是编码器还是解码器,它们都有一个Add & Norm层,Add代表残差连接,Norm则是Layer Norm。LLaMA有8组编解码器。
而LLaMA则是把Layer Norm放到多头注意力机制之前,而通过Multi-Head Attention之后不再进行归一化处理。因为大模型的参数量很大,要进行稳定的训练是比较困难的。
\(RMSNorm(x) = {x \over \sqrt {{1\over n}\sum_{i=1}^nx_i^2+ξ}}\)
其中x是输入的向量,n是向量的长度,ξ是一个很小的常数,用于避免分母为0。
- LLaMA方法——SwiGLU激活函数
SwiGLU激活函数代替ReLU非线性,以提高性能。
好处是:
- SwiGLU激活函数的收敛速度更快,效果更好。
- SwiGLU激活函数和ReLU都拥有线性的通道,可以使梯度很容易通过激活的units,更快收敛。
- SwiGLU激活函数相比ReLU更具有表达能力。
SwiGLU激活函数的收敛速度更快,这是因为它在计算过程中使用了门控机制,可以更好地控制信息的流动。
对于一些不重要的信息,我们就会让门完全关闭,而对于很重要的信息,则可以让门完全打开。SwiGLU是以门线性单元GLU为基础的,它是一个双线性函数,表达式为
这里的 ⊗ 表示矩阵的逐元素相乘,关于门控机制可以参考Tensorflow深度学习算法整理(二) 中的长短期记忆网络以及GRU。
- LLaMA方法——Rotary Embedding
删除了绝对位置嵌入,取而代之的是在网络的每一层中添加了旋转位置嵌入。
旋转位置嵌入的主要思想是将位置信息编码为一个旋转矩阵,然后将该矩阵与输入向量相乘,从而得到一个新的向量表示。这种方法可以更好地捕捉序列中不同位置之间关系,从而提升模型的性能。(有关旋转矩阵的内容可以参考线性代数整理 中的图形变换矩阵 (向量的函数))
虽然Transformer一般是处理文字数据的,但是它跟接近于CNN而不是RNN,对于句子中所有的输入tokens是同时送入网络并行处理的,而RNN是将句子中的tokens一个一个送入循环神经网络的,为了表征句子中词的位置关系,于是就有了位置编码Position Embedding。之前的位置编码又分为绝对位置和相对位置,绝对位置就是按照原句的顺序进行编码,如"我爱北京天安门",那么编码后就是"我"——1、"爱"——2、"北京"——3、“天安门”——4。一般来说Position Embedding会有一个长度限制——512。相对位置是以某一个词为基准来定义位置的,如"我爱北京天安门"中以"北京"为基准,那么"北京"的位置为0,"爱"——-1,"我"——-2,"天安门"——1。
LLaMA使用的是旋转位置嵌入,它可以更好的处理序列中的旋转对称性。在传统的位置编码方法中,位置信息只是简单的编码为一个向量,而没有考虑到序列中的旋转对称性。而旋转位置嵌入则将位置信息编码为一个旋转矩阵,从而更好的处理序列中的旋转对称性。
旋转对称性是指物体在旋转后仍然具有相同的性质。例如,一个正方形在旋转90度后仍然是一个正方形,因此具有旋转对称性。在句子序列中,旋转对称性指的是序列中的某些部分可以通过旋转变换得到其他部分。例如,在机器翻译任务中,源语言句子和目标语言句子之间存在一定的对称性。这意味着我们可以通过将源语言句子旋转一定角度来得到目标语言句子。
源码分析与解读
最核心的当然是Transformer
class Transformer(nn.Module): def __init__(self, params: ModelArgs): super().__init__() self.params = params # 类实例参数 self.vocab_size = params.vocab_size # 词向量维度 self.n_layers = params.n_layers # 层数 # 获取词向量 self.tok_embeddings = ParallelEmbedding( params.vocab_size, params.dim, init_method=lambda x: x ) # 添加所有的TransformerBlock,共8层 self.layers = torch.nn.ModuleList() for layer_id in range(params.n_layers): self.layers.append(TransformerBlock(layer_id, params)) # 置前的批归一化 self.norm = RMSNorm(params.dim, eps=params.norm_eps) # 线性特征提取 self.output = ColumnParallelLinear( params.dim, params.vocab_size, bias=False, init_method=lambda x: x ) # 计算旋转位置嵌入Rotary Embedding self.freqs_cis = precompute_freqs_cis( self.params.dim // self.params.n_heads, self.params.max_seq_len * 2 ) @torch.inference_mode() def forward(self, tokens: torch.Tensor, start_pos: int): # 获取句子输入的batchsize和句子的长度 _bsz, seqlen = tokens.shape # 将句子变成词向量 h = self.tok_embeddings(tokens) # 旋转位置嵌入 self.freqs_cis = self.freqs_cis.to(h.device) freqs_cis = self.freqs_cis[start_pos : start_pos + seqlen] mask = None if seqlen > 1: mask = torch.full((1, 1, seqlen, seqlen), float("-inf"), device=tokens.device) mask = torch.triu(mask, diagonal=start_pos + 1).type_as(h) # 将词向量(或隐层表示)送入到每一层的Transformer block for layer in self.layers: h = layer(h, start_pos, freqs_cis, mask) # 批归一化 h = self.norm(h) # 线性特征提取 output = self.output(h[:, -1, :]) # only compute last logits return output.float()
然后是Transformer block,它就是layers中的每一层
class TransformerBlock(nn.Module): def __init__(self, layer_id: int, args: ModelArgs): super().__init__() self.n_heads = args.n_heads # 多头 self.dim = args.dim # 句子中词向量的最大数量 self.head_dim = args.dim // args.n_heads # 多头头数 self.attention = Attention(args) # 自注意力机制 # 前馈神经网络 self.feed_forward = FeedForward( dim=args.dim, hidden_dim=4 * args.dim, multiple_of=args.multiple_of ) self.layer_id = layer_id # 批归一化 self.attention_norm = RMSNorm(args.dim, eps=args.norm_eps) self.ffn_norm = RMSNorm(args.dim, eps=args.norm_eps) def forward(self, x: torch.Tensor, start_pos: int, freqs_cis: torch.Tensor, mask: Optional[torch.Tensor]): # 这里的第一步就是批归一化,然后经过自注意力机制运算再接一个残差连接 h = x + self.attention.forward(self.attention_norm(x), start_pos, freqs_cis, mask) # 上一步的输出再经过前馈神经网络再接一个残差连接 out = h + self.feed_forward.forward(self.ffn_norm(h)) return out
然后是Attention
class Attention(nn.Module): def __init__(self, args: ModelArgs): super().__init__() # 注意力机制中的头 self.n_local_heads = args.n_heads // fs_init.get_model_parallel_world_size() # 注意力机制中头的长度 self.head_dim = args.dim // args.n_heads # 查询向量矩阵 self.wq = ColumnParallelLinear( args.dim, args.n_heads * self.head_dim, bias=False, gather_output=False, init_method=lambda x: x, ) # 键向量矩阵 self.wk = ColumnParallelLinear( args.dim, args.n_heads * self.head_dim, bias=False, gather_output=False, init_method=lambda x: x, ) # 值向量矩阵 self.wv = ColumnParallelLinear( args.dim, args.n_heads * self.head_dim, bias=False, gather_output=False, init_method=lambda x: x, ) # 合并后的矩阵 self.wo = RowParallelLinear( args.n_heads * self.head_dim, args.dim, bias=False, input_is_parallel=True, init_method=lambda x: x, ) # 将键词对进行缓存,以便于推理的时候更加方便 self.cache_k = torch.zeros( (args.max_batch_size, args.max_seq_len, self.n_local_heads, self.head_dim) ).cuda() self.cache_v = torch.zeros( (args.max_batch_size, args.max_seq_len, self.n_local_heads, self.head_dim) ).cuda() def forward(self, x: torch.Tensor, start_pos: int, freqs_cis: torch.Tensor, mask: Optional[torch.Tensor]): # 获取词向量的batchsize和长度 bsz, seqlen, _ = x.shape # 通过使用q、k、v的线性变换矩阵对词向量进行线性变换获得q、k、v xq, xk, xv = self.wq(x), self.wk(x), self.wv(x) # resize到各个头的大小 xq = xq.view(bsz, seqlen, self.n_local_heads, self.head_dim) xk = xk.view(bsz, seqlen, self.n_local_heads, self.head_dim) xv = xv.view(bsz, seqlen, self.n_local_heads, self.head_dim) # 进行旋转位置嵌入 xq, xk = apply_rotary_emb(xq, xk, freqs_cis=freqs_cis) # 缓存这些k、v self.cache_k = self.cache_k.to(xq) self.cache_v = self.cache_v.to(xq) self.cache_k[:bsz, start_pos : start_pos + seqlen] = xk self.cache_v[:bsz, start_pos : start_pos + seqlen] = xv # 从缓存中取出所有的k、v keys = self.cache_k[:bsz, : start_pos + seqlen] values = self.cache_v[:bsz, : start_pos + seqlen] # 计算Attention的值,计算公式为SoftMax(QK^T/√d + B)V xq = xq.transpose(1, 2) keys = keys.transpose(1, 2) values = values.transpose(1, 2) scores = torch.matmul(xq, keys.transpose(2, 3)) / math.sqrt(self.head_dim) if mask is not None: scores = scores + mask # (bs, n_local_heads, slen, cache_len + slen) scores = F.softmax(scores.float(), dim=-1).type_as(xq) output = torch.matmul(scores, values) # (bs, n_local_heads, slen, head_dim) # 合并 output = output.transpose( 1, 2 ).contiguous().view(bsz, seqlen, -1) return self.wo(output)
然后是前馈神经网络FeedForward
class FeedForward(nn.Module): def __init__( self, dim: int, hidden_dim: int, multiple_of: int, ): super().__init__() hidden_dim = int(2 * hidden_dim / 3) hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of) self.w1 = ColumnParallelLinear( dim, hidden_dim, bias=False, gather_output=False, init_method=lambda x: x ) self.w2 = RowParallelLinear( hidden_dim, dim, bias=False, input_is_parallel=True, init_method=lambda x: x ) self.w3 = ColumnParallelLinear( dim, hidden_dim, bias=False, gather_output=False, init_method=lambda x: x ) def forward(self, x): return self.w2(F.silu(self.w1(x)) * self.w3(x))
前馈神经网络其实就是一个全连接层,这个就不解释了。
然后是批归一化RMSNorm
class RMSNorm(torch.nn.Module): def __init__(self, dim: int, eps: float = 1e-6): super().__init__() # 计算公式中的ξ self.eps = eps self.weight = nn.Parameter(torch.ones(dim)) def _norm(self, x): # RMSNorm公式,torch.rsqrt是张量倒数的平方根 return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) def forward(self, x): output = self._norm(x.float()).type_as(x) return output * self.weight
实验分析
- 模型参数
模型的参数量有4种。
- 优化训练
- 使用因果多头注意力算子的高效实现。
- 为了进一步提高训练效率,减少了在带有检查点的反响传播过程中重新计算的激活量。通过手动实现变换器层的反向功能来实现,而不是依赖于PyTorch autograd。
- 训练时间
- 在训练65B参数模型时,代码在具有80G显存的2048 A100 GPU上处理大约380个Token/秒/GPU。
- 包含1.4T Token的数据集进行训练大约需要21天。
- 实验结果
由上图我们可以看到,模型的损失和Tokens之间的关系为当Tokens的数量不断增大的时候,模型的损失在不断的降低。该实验体现了在训练大模型时,数据量的重要性。
在20个数据集上对比了开源和闭源模型,主要是zero-shot和few-shot性能,也对比了instruct-tuning之后的效果。
在LLaMA 13B的模型与GPT-3 175B对比,我们会发现LLaMA 13B在各个数据集中都能跟GPT-3持平甚至超过。
模型推理
我们这里使用的是LLaMA 7B的模型去进行推理,在batch-size=2的时候,16G的显卡就够了,当然我这里使用的是24G的3090显卡。如果我们要去做模型微调(Fine tuning),需要四张A100。这里我们推理代码的下载地址为:https://github.com/pengwei-iie/llama_bugs
安装环境(我这里假设你的GPU,Cuda,Pytorch环境都装好了)
conda activate pytorch
pip install fairscale
pip install fire
pip install sentencepiece
pytorch是我自己的conda环境,需要换成你自己的环境。这里的senntencepiece是我们要用到的分词器组件。
- 预训练参数下载
下载地址:https://huggingface.co/nyanko7/LLaMA-7B/tree/main
进入推理代码主目录
cd llama_bugs-main
拉取预训练模型(如果git拉取有问题的话可以在网页上单独下载)
git clone https://huggingface.co/nyanko7/LLaMA-7B
- 开始执行推理
torchrun --nproc_per_node 1 example_small.py --ckpt_dir ./LLaMA-7B --tokenizer_path ./LLaMA-7B/tokenizer.model
运行后可以开始问答模式,不过这里只支持英文。
> initializing model parallel with size 1
> initializing ddp with size 1
> initializing pipeline with size 1
Loading..
Loaded in 4.94 seconds
Once upon a time, there were three bears. They were called Mama Bear, Papa Bear, and Little Bear. The three bears were very different from each other. Papa Bear was big and strong, and he liked to eat rocks. Mama Bear was not big but was very kind. Little Bear was a little bear, but he was very brave.
The three bears lived together in a house deep in the woods. They had a big garden and a stream for swimming. One day, Little Bear went to play in the garden.
“No!” shouted Little Bear. “Come back! You will get lost!” Then Little Bear began to cry.
Little Bear remembered what his Mama Bear had said, so he went back to the house.
Papa Bear did not answer. He was not there. Little Bear looked all over the house but could not find his Papa Bear.
“Papa Bear?” said Little Bear.
“Yes,” said Mama Bear, “he is right here.” Mama Bear was very happy that her Papa Bear was back home.
“Have I ever told you why I live in the woods?” asked Papa Bear.
“No, why?” asked Little Bear.
Mama Bear and Papa Bear smiled. “We live here to
==================================
what's your name?
what's your name? what's your d.o.b.?
where's your favorite place to go in the city?
how did you first get interested in fashion?
what was your favorite item to wear growing up?
what was your favorite item to wear growing up? how do you think this has influenced your style now?
what is your favorite item to wear now?
what is your favorite item to wear now? how do you think this has influenced your style now?
what is your favorite item to wear now? how do you think this has influenced your style now? in your eyes, what makes a "good" outfit?
what is your favorite item to wear now? how do you think this has influenced your style now?
in your eyes, what makes a "good" outfit?
what is your favorite item to wear now? how do you think this has influenced your style now? in your eyes, what makes a "good" outfit?
what is your favorite item to wear now? how do you think this has influenced your style now? in your eyes, what makes a "good" outfit? in your eyes, what makes a "good" outfit?
who are your favorite fashion designers
ChatGLM
背景介绍
- Chat-GLM 130B
- GLM-130B是一个双语(英语和汉语)预训练的语言模型,具有1300亿个参数,使用了General Language Model(GLM)的算法。
- ChatGLM参考了ChatGPT的设计思路,在千亿基座模型GLM-130B中注入了代码预训练,通过有监督微调(Supervised Fine-Tuning)等技术实现人类意图对齐。
- GLM-130B可以支持多种自然语言处理任务,如文本生成,文本理解,文本分类,文本摘要等。
- GLM-130B在多个英语和汉语的基准测试中优于其他模型,如GPT-3 175B、OPT-175B、BLOOM-176B、ERNIE TITAN 3.0 260B等。
- 开源应用
- 开源GLM-130B是为了促进双语自然语言处理的研究和应用,提供一个高质量的预训练模型给社区用。
- GLM-130B可以应用于多种场景,如机器翻译、对话系统、知识图谱、搜索引擎、内容生成等。
- GLM-130B可以帮助解决跨语言和跨领域的自然语言处理问题,提高人机交互的效率和体验。
- 贡献和创新
- GLM-130B是目前较大的开源双语预训练模型,而GLM-6B也是可以在单个服务器上单张GPU上支持推理的大模型。
- GLM-130B使用了GLM算法,实现双向密集连接的模型结构,提高了模型的表达能力和泛化能力。
- GLM-130B在训练过程中遇到了多种技术和工程挑战,如损失波动和不收敛等,提出了有效的解决方案,并开源了训练代码和日志。
- GLM-130B利用了一种独特的缩放性质,实现了INT4量化,几乎没有精度损失,并且支持多种硬件平台。
- 时间轨迹
- 2023.4.13,ChatGLM-6B开源30天内,全球下载量达到75万,Github星标数达到1.7万。
- 2023.3.31,ChatGLM-6B推出基于P-Tuning-v2的高效参数微调,最低只需7G显存即可进行微调,
- 2023.3.18,ChatGLM-6B登上Hugging Face Treding榜第一,持续12天.
- 2023.3.16,ChatGLM-6B登上Github Trending榜第一。
- 2023.3.14,千亿对话模型ChatGLM开始内测,60亿参数ChatGLM-6B模型开源。
- 应用
同时开源ChatGLM-6B模型,ChatGLM-6B是一个具有62亿参数的中英双语言模型。通过使用与ChatGLM(chatglm.cn)相同的技术,ChatGLM-6B初具中文问答和对话功能,并支持在单张2080Ti上进行推理使用。具体来说,ChatGLM-6B有如下特点:
- 充分的中英双语预训练:ChatGLM-6B在1:1比例的中英文语料上训练了1T的token量,兼具双语能力。
- 较低的部署门槛:FP16半精度下,ChatGLM-6B需要至少13G的显存进行推理,使得ChatGLM-6B可以部署在消费级显卡上。
- 更长的序列长度:相比GLM-10B(序列长度1024),ChatGLM-6B序列长度达204。
- 人类的意图对齐训练。
GLM模型方法
三种主流的预训练框架:
- autoregressive自回归模型代表是GPT,本质上是一个从左到右的语言模型,常用于无条件生成任务(unconditional generation)。
- autoencoding自编码模型是通过某个降噪目标(如掩码语言模型)训练的语言编码器,如BERT、ALBERT、DeBERTa。自编码模型擅长自然语言理解任务(natural language understanding tasks),常被用来生成句子的上下文提示。
- encoder-decoder则是一个完整的Transformer结构,包括一个编码器和一个解码器,以T5、BART为代表,常用于有条件的生成任务(conditional generation)。
Latent Diffusion AI绘画底层原理
研究背景
从前面的知识点,我们知道现在大型语言模型都是采用Transformer的网络结构,而生成式模型正是从这种结构慢慢演化而来。我们来看一下它的演化历史
- 自编码器(autoencoder)
自编码器的结构是Encoder(编码器)和Decoder(解码器),在上图中,我们先对图像x进行编码变成中间向量h(这里称为Latent Representation),再经过解码,变成解码图像r,则有
r=Decoder(Encoder(x))
如果隐层神经元数小于输入层神经元数,这样的自编码器称为欠完备或不完备自编码器,它的作用有
- 降维,信息瓶颈
- 数据压缩
- 特征提取
- 去除多余信息
还有一种去噪自编码器,我们可以在输入中加入噪声。这里更详细的内容可以参考Swin Transformer介绍
- 变分自编码器(Variational autoencoder (VAE))
变分自编码器是与GAN齐名的一种生成式网络结构,它与自编码器最大的不同就在于它不是生成一个中间向量,而是由编码器将图像生成一个正态分布。首先它会同时生成一个均值向量和标准差向量,这两个向量就组合成了一个正态分布,再从这个正态分布中进行采样得到潜在向量(latent vector),然后再由该潜在向量经过解码器进行解码成图像。通过这样一个步骤就将自编码器改造成了变分自编码器。
我们在实际使用的时候直接将编码过程就给去掉了,留下了从正态分布采样得到潜在向量,再经过解码器生成图像,这就是一个生成式的过程。而经常用到的正态分布就是标准正态分布,均值为0,标准差为1。其中用到的损失函数如下
generation_loss = mean(square(generated_image - real_image))
latent_loss = KL-Divergence(latent_variable, unit_gaussion)
loss = generation_loss + latent_loss
这里的generation_loss为生成损失,为生成图像与真实图像的均方根。latent_loss为分布损失,它代表生成的分布要尽量去接近标准正态分布,原式为生成的分布与标准正态分布的KL散度(有关KL散度的内容可以参考AI的数学理论基础 中的相对熵 (KL 散度))。总的损失即为这两者之和。
- VQGAN
在扩散模型出现之前,人们一直都使用GAN来进行图像生成,有关GAN的内容可以参考Tensorflow深度学习算法整理(三) 中的对抗神经网络
VQGAN也是一种GAN模型,这里VQ的意思指的是向量量化。在上图中,中部的整体是生成器的编解码结构,右上角是判别器的结构。为了能够学习到图像像素中块(patch)之间的联系,把图像块切割成一个一个的序列,并使用了Transformer来学习图像块序列的关联,但问题在于这个训练的过程的量是非常大的,为了降低训练的开销,就做了图像的量化。它会将这些图像块编码成一个一个的码本,即上图中的Codebook Z,并赋予了编号(从0到N-1),每一个编号代表了图像中的一小块区域,然后再将码本的编号送入到Transformer去学习。
GAN的问题
- 对抗训练的不稳定性
- 伪迹(Artifacts)比例高,难以消除
- 难以学习到过于复杂的分布(通用物体生成)
扩散模型(Diffusion model)
- 扩散模型也是一种生成式模型,其灵感来源于非平衡热力学中的扩散运动,如一滴墨水滴入了一杯清水中的过程。
- 扩散模型的工作方式是通过迭代添加高斯噪声来"破坏"训练数据,然后学习消除噪声来恢复数据。
- 扩散模型有两个主要过程
- 正向扩散——通过逐渐引入噪声来破坏图像,直到图像变成完全随机的噪声。
- 反向扩散——使用一系列马尔科夫链逐步去除预测噪声,从高斯噪声中恢复数据。
上图中从右到左的过程即为正向扩散,从左到右的过程即为反向扩散。
扩散模型与其他模型的比较
- 正向过程
- 给定真实图片\(x_0\)~q(x)(这里读作\(x_0\)符合q分布),扩散正向过程对其循环添加T次高斯噪声,得到\(x_1,x_2,...,x_T\).
- 正向过程由于每个时刻T只与t-1时刻有关,所以是一个马尔科夫过程。(有关马尔科夫过程可以参考概率论整理(三) 中的两类重要的随机过程)
- 这个过程中,随着t的增大,\(x_t\)越来越接近纯噪声。
- 当\(t->\infty\)时,\(x_t\)时完全的高斯噪声
以上整个的过程用数学公式来表达如下
\(q(X_t|X_{t-1})=N(X_t;\sqrt{1-β_t}X_{t-1},β_tI)\) \(q(X_{1:T}|X_0)=\prod_{t=1}^Tq(X_t|X_{t-1})\)
\(\{β_t∈(0,1)\}_{t-1}^T\)
以上公式中的\(X_t,X_{t-1}\)代表不同时刻的图像;I为高斯噪声;\(β_t\)为调节噪声大小的系数,一般该系数会取一个比较小的数,位于0到1之间;q代表条件概率模型;第一个公式为单步操作公式(t-1时刻到t时刻),第二个公式为整个过程的公式,为单步操作公式的连乘(1时刻到T时刻)。
- 反向过程
- 如果能逆转上述过程并从中采样\(q(X_{t-1}|X_t)\),就能够从高斯噪声\(X_T\)~N(0,1)中重新得到真实样本。
- 如果\(β_t\)足够小,\(q(X_{t-1}|X_t)\)也将是高斯分布的
- 无法轻易估计\(q(X_{t-1}|X_t)\),因为它需要使用整个数据集,因此需要训练一个模型来学习这个条件概率,以便进行反向扩散过程。
以上整个的过程用数学公式来表达如下
\(p_θ(X_{0:T})=p(X_T)\prod_{t=1}^Tp_θ(X_{t-1}|X_t)\) \(p_θ(X_{t-1}|X_t)=N(X_{t-1};μ_θ(X_t,t),\sum_θ(X_t,t))\)
\(L_{DM}=E_{x,ɛ~N(0,1),t}[||ɛ-ɛ_θ(x_t,t)||_2^2]\)
这里的θ表示生成式模型的权重,它其实就是马尔科夫链的状态转移概率,是通过学习得到的;第一个公式表示整个的反向过程,从正向过程最终生成的图像概率分布\(p(X_T)\)来连乘之前所有的过程的条件概率分布就反向得到我们需要的图像概率分布\(p_θ(X_{0:T})\);第二个公式表示单步反向的一个步骤,它是由模型推理来得到t-1时刻的均值\(μ_θ(X_t,t)\)和方差\(\sum_θ(X_t,t)\);第三个公式为最终的目标损失,表示最终反向生成的图像与原始图像尽量的接近。
- 加速采样
- 原始的扩散模型DDPM(Denoising Diffusion Probabilistic Models)生成图片的速度非常慢
- DDIM(Denoising Diffusion Implicit Models)中提出了一种牺牲多样性来换取更快推理的手段,与DDPM相比,DDIM:
- 能够使用更少的步骤(如100步)生成更高质量的样本
- 具有"一致性"属性,生成过程是确定性的,也就是说一个\(X_T\)对应一个固定的\(X_0\)
- 由于一致性,可以把\(X_T\)作为隐变量,DDIM可以对\(X_T\)执行语义上有意义的插值
上图中,原始的DDPM是反向过程应该是\(x_3->x_2->x_1->x_0\),但是在DDIM中,它直接从\(x_3\)到\(x_1\),跳过了\(x_2\),完成了加速。
Latent Diffusion模型结构
- 感知压缩
Latent Diffusion的图像压缩是一个独立的模型,它不仅仅可以支持Latent Diffusion,还可以支持任意的模型的压缩。
- 一般的扩散模型会通过下采样来忽略感知上不重要的细节,但仍然需要在像素空间中进行损失函数计算,而这里的感知压缩将压缩与生成进行显示分离来规避这一缺点。
- 使用自编码器来进行输入图像的压缩,训练自编码器主要使用感知损失和基于Patch的对抗损失。
- \(L_{Autoencoder}=\min_{ɛ,D}\max_ψ(L_{rec}(x,D(ɛ(x)))-L_{adv}(D(ɛ(x)))+logD_ψ(x)+L_{reg;}(x;ɛ,D))\)
- 在上述公式中,ɛ是一个编码器,它将图像x编码到一个潜在空间进行降维,并在此过程中进行下采样f倍。D是一个解码器,它将ɛ(x)进行解码,从潜在空间将图像复原,\(L_{rec}(x,D(ɛ(x)))\)是一个BCE损失(二值交叉熵损失),\(L_{adv}(D(ɛ(x))\)是一个类似于GAN的对抗损失,后面的是一些正则损失。
- 采用U-Net的主干网络架构,对图像数据特别有效,减轻了之前类似VQGAN要求的激进的压缩需要(采用了Transformer,训练量大)。
- 获得了通用的压缩模型,其潜空间可用于训练多个生成模型,也可用于其他下游应用。
- LDMs
- 通过已经训练好的由编码器E和解码器D组成的感知压缩模型,可以得到一个高效的低维潜在空间,其中高频的,难以察觉的细节被抽象出来。
- 与高维像素空间相比,这个空间更适合基于似然估计的生成模型,因为可以专注于数据的重要语义信息,并且在低维、计算效率高的多的空间中进行训练。
- 模型的骨干生成网络\(ɛ_0(z,t)\)被实现为以时间为条件的Unet.
- 由于前向过程是固定的,\(z_t\)可以在训练期间从编码器E中获得,而p(z)的样本可以通过解码器D得到。
- 条件扩散
- 之前的研究还只是探索过将类别标签或者低清图像作为扩散模型的条件输入
- 通过使用交叉注意力机制加入到Unet中,可以将扩散模型转变为更灵活的条件图像生成,从而可以输入各种模态的条件数据
- 这里引入了一个特定领域的编码器\(τ_θ\),将条件y投影到一个中间表示\(τ_θ(y)\),然后通过交叉注意力层将其映射到UNet不同尺度的层级中
- 这种条件输入机制是非常灵活的,因为\(τ_θ\)可以使用各个特定领域的专家模型。
\(Q=W_Q^{(i)}φ_i(z_i),K=W_K^{(i)}τ_θ(y),V=W_V^{(i)}τ_θ(y)\)
- 整体架构
在上图中,最右边的部分是多模态的条件输入(包括文本、图像...),通过特殊专家编码器\(τ_θ\)编码后,通过交叉注意力层(上图中的QKV黄色块)输入到UNet的反向过程(去噪)中来。上图中最左边的部分代表高维的像素空间,x是原图像,经过训练好的自编码器ɛ编码到潜在空间进行降维得到z,再经过正向扩散(添加高斯噪声)到\(z_T\),\(z_T\)再进行反向扩散,经过UNet网络来进行去噪,中间结合了条件输入到了\(z_{T-1}\),再将这一过程重复T次(每一次都包含了该UNet网络)得到最终的输出z,这里上下两个z会算一个loss,再通过解码器D将z还原成高维像素空间的生成图像。
- 图像生成的评价指标
- Inception Score(IS):
- \(exp({1\over N}\sum_{i=1}^ND_{KL}(p(y|x^{(i)}||p^*(y))))\)
- Frechet Inception Distance(FID):
- \(||μ_{data}-μ_g||+tr(\sum_{data}+\sum_g-2(\sum_{data}\sum_g)^{1\over 2})\)
- Maximum Mean Discrepancy(MMD)
- MS-SSIM
- \(SSIM(X,Y)=[L_M(X,Y)]^{αM}\prod_{J=1}^M[C_J(X,Y)]^{β_J}[S_J(X,Y)]^{γ_J}\)
- 关于感知压缩的权衡取舍(Tradeoffs)
- 具有不同下采样因子f∈{1,2,4,8,16,32}的模型缩写为LDM-f,其中LDM-1对应于基于像素的扩散模型
- LDM-{1,2}的小降采样因子导致训练进度缓慢,因为将大多数感知压缩留给了扩散模型处理
- f过大导致在相对较少后的训练step后,生成质量就已无法上升,因为太强的压缩导致了显著的信息损失
- LDM-{4-16}在效率和生产质量之间取得了很好的平衡
- 在ImageNet这样的复杂数据集上训练时,需要降低压缩率f,以避免降低生成质量
- 综上所述,LDM-{4,8}是在各个数据集上都能获得高质量合成结果的最佳选择
ImageNet类别条件图像合成
Celeba-HQ(左),ImageNet(右),修改采样步数来改变吞吐量(throughput)
- 基于Latent Diffusion的图像生成
- 在CelebA-HQ、FFHQ、LSUN-Churches和Bedrooms上训练256*256分辨率的无条件生成模型,并评估样本质量和对真实数据模型的覆盖率
- 在CelebA-HQ上,LDMs得到了SOTA的FID 5.11,超过了之前基于似然的模型和GAN
- LDMs的性能还超过了LSGM——基于扩散模型与压缩模型一起训练。LDMs在固定空间中训练扩散模型,避免了权衡重建质量与学习潜空间先验的困难
- 在除了LSUN-Bedrooms之外的所有数据集上,LDMs都优于之前的方法
- 总的来说,进一步证实了扩散模型对于GAN的优势
- 条件潜在扩散(Conditional Latent Diffusion)
- 在LAION-400M上训练了一个以文本信息为条件、1.45B参数、使用KL正则化的LDM,采用BERT-tokenizer处理输入文本,使用Transformer作为条件编码器\(τ_θ\)
- 在MS-COCO验证集上进行评估
- 应用Classifier-free diffusion guidance大大提高了生成质量,所得到的LDM-KL-8-G与最近最先进的自回归模型和扩散模型相当,同时大大减少了参数数量
- 训练模型在OpenImages上基于语义布局合成图像,在COCO上进行微调
- 语义图像合成
- 通过在空间中对齐的条件信息与图像数据进行拼接,LDMs也可以作为高效的通用图形翻译模型,来完成语义合成、超分辨率和图形修复任务
- 对于风景图像的语义合成,将语义分割进行下采样后,再与f=4模型的latent图像特征(编码后)进行拼接
- 在256分辨率上进行训练(crop自384分辨率),得到模型可推广到于更大的分辨率,推断时可以生成高达百万像素的图像
在256*256分辨率下训练的LDM可以生成更大分辨率(512*1024)的图像
- 超分辨率
- 通过降输入与低分辨率图像进行拼接,LDM可以进行超分辨率任务训练
- 使用在OpenImages上预训练的f=4自编码模型
- LDM-SR在FID上优于SR3,而SR3有更好的IS,简单图像回归模型取得了最高的PSNR和SSIM分数
- 在主观测评结果上,LDM-SR的优势非常明显
- 使用更多样化的退化训练了通用超分模型LDM-BSR
- 图像补全
- 比较了LDM-1和LDM-4的效率,以及在第一阶段没有任何attention的VQ-LDM-4;VQ-LDM-4在高分辨率下减少了用于解码的GPU内存消耗
- 相比于LDM-1,LDM-4的速度至少提高了2.7倍,同时将FID分数提高了至少1.6倍
- 带有注意力的LDM比LaMa在FID和LPIPS上更优
- 主观测评中,测试员同样认为LDM的结果优于LaMa
- 结论与展望
- 可以显著提高去噪扩散模型的训练和采样效率,而不降低其质量
- 可以应用于广泛的条件图像合成任务中,与各领域最先进的方法相比也毫不逊色
- 采样过程仍然比GAN慢
- 当需要非常高的生成精度时,LDMs可能无法满足要求——尽管在f=4的自编码模型中,图像质量的损失非常小;在超分辨率任务时,这方面的问题可能已经出现
- AI伦理:
- 创建和传播受操纵的数据或传播错误信息和垃圾邮件变得更加容易
- 生成模型还可能透露它们的训练数据,特别是数据包含敏感或个人信息且未经明确同意而收集时
- 虽然扩散模型比基于GAN的方法实现了更好的数据分布拟合,但LDM结合对抗性训练和似然估计的两阶段方法在多大程度上错误地代表了数据,仍然是一个重要的研究问题
- 总结
关键点
- 用自编码器进行降维
- 两阶段训练
- 在unet中加入了cross-attention
创新点
- 在低维空间中进行图像生成的训练
启发点
- 大规模图文数据集LAION对新一代图像生成模型非常重要
- cross-attention和unet是深度学习的基石
- 有效的降维对图像任务来说永远至关重要
LoRa和ControlNet的嵌入
LoRa是在NLP中进行微调使用的技术,现在也应用到了图像生成中来。
- DreamBooth
该技术的目的是可以快速的使用少量的数据,比如30~40张某个狗的图像(不同角度,不同光照),投喂模型进行微调,模型就可以很少的去生成狗的新的各个样子的图像。
其原理是扩展目标模型的text-image词典(其实就是文本编码器部分),将新的文本标识符与特定主题或图片进行联系(其实就是训练了对应词的词向量),然后引导图片生成小数据集会导致过拟合,而且茫然地扩展text-image还会导致语义漂移(language drift)。
DreamBooth的解决方式:
- 为新的主题使用一个罕见的词(文本反转),避免语义漂移(可以理解成promt撞车)。
- 保留同类的先验知识,通过设计一个损失函数,鼓励扩散模型不断生成与我们的主题相同大类别的不同实例。
- LoRa(Low Rank Adaption)
中文名为低秩适配。
原理:LoRa并不是扩散模型专有的技术,而是从LLM迁移过来,旨在解决避免将整个模型参数拷贝下来才能对模型进行调校的问题。因为大型语言模型的参数量过于恐怖,比如最新的GPT4参数量约为100万亿。
LoRa采用的方式是向原有的模型中插入新的数据处理层,这样就避免了去修改原有的模型参数,从而避免将整个模型进行拷贝的情况,同时其也优化了插入层的参数量,最终实现了一种很轻量化的模型调校方法。
LoRa在稳定扩散模型里将注意打在了crossattention(注意力交叉)所在模块,LoRa将会将自己的权重添加到注意力交叉层的权重中,以此来实现微调。添加权重是以矩阵的形式,如果这样做,LoRa势必需要存储同样大小的参数,那么LoRa又有了个好点子,直接以矩阵相乘的形式存储,最终文件大小就会小很多。训练时需要的显存也少了。
严格来说LoRa的名字指的是这种数据的存储方式。实现方式与我们的目标有关。上文我们所说LoRa作用注意力交叉模块的说法来自最早的LoRa训练脚本的早期时候。比如我们以DreamBooth的方式进行微调的时候额外使用LoRa这种存储方式,得到的结果也是LoRa。这也是目前大部分人口中的LoRa。如果训练方法中包含了正则化图像训练集,就可以理解为是DreamBooth的一种实现方式。
- ControlNet
ControlNet是一种神经网络结构,通过添加额外条件来控制扩散模型。
上图展示了将ControlNet应用于任意神经网络模块的方法,x、y是神经网络的深层特征,"+"表示功能添加,c是我们要添加到神经网络中的一个额外条件,zero convolution是一个1*1的卷积层,权重和偏差都初始化为0。
ControlNet将神经网络权重复制到一个锁定(locked)副本和一个可训练(trainable)副本。可训练副本将会学习新加入的条件,而锁定副本将会保留原有的模型,得益于此在进行小数据集训练时不会破坏原有的扩散模型。
可以看到使用了零卷积(zero convolution),这样做的好处是可以以0值初始化,卷积权重会以学习的方式从0开始增长到优化参数,所以在训练的初始阶段(训练前)不会影响原模型,所以不是从头开始训练,仍然属于微调(fine-tuning)。这种结构也方便我们对模型/权重/块/层进行合并/替换/偏移。
ControlNet在stable diffusion中产生作用的过程。stable diffusion的U-Net结构包含12个编码器块(Encoder Block),12个解码器块(Decoder Block),还有一个中间块(Middle),完整模型包括25个块,其中17个块是主块。文本使用clip进行编码,时间步长采用位置编码。
我们将上面的简单结构附加在stable diffusion原来的U-Net结构上14次(相当于复制了一次编码器块和中间块,然后改造成ControlNet结构),就完整的对原有的结构进行了控制(影响),原有的stable diffusion就化身为了stable diffusion+controlnet。
上图中,在条件部分Condition进入,经过各种编码之后,再送入U-Net的主网络中,但这里只涉及编码模块和中间模块,不会涉及解码去噪模块。
这样stable diffusion+controlnet就可以继续尝试用特定数据集来训练学习新东西来试图完成我们想要模型完成的新任务。比如边缘检测,比如人体姿态检测,整个过程流畅,顺滑。
stable diffusion+controlnet的损失函数,是在stable diffusion的基础上增加了新的控制条件
stable diffusion+controlnet既能做到边缘检测,又能做到涂鸦检测,而且是在一统的架构下进行的,多种任务只需要一种解决方式,这也许就是大模型+优秀结构的魅力吧。