Transformer

Transformer

Attention Is All You Need

Transformer: A Novel Neural Network Architecture for Language Understanding

Tensor2Tensor announcement

A High-Level Look

img

img

Encoder-Decoder框架

目前大多数注意力模型附着在Encoder-Decoder框架下,其实注意力模型可以看作一种通用的思想,本身并不依赖于特定框架

img

抽象的文本处理领域常用Encoder-Decoder框架

可以把它看作适合处理由一个句子(或篇章)生成另外一个句子(或篇章)的通用处理模型。对于句子对<Source,Target>,我们的目标是给定输入句子Source,期待通过Encoder-Decoder框架来生成目标句子Target。Source和Target可以是同一种语言,也可以是两种不同的语言。而Source和Target分别由各自的单词序列构成

\[Source=\left \langle x_1,x_2,...,x_m \right \rangle \]

\[Target=\left \langle y_1,y_2,...,y_n \right \rangle \]

Encoder顾名思义就是对输入句子Source进行编码,将输入句子通过非线性变换转化为中间语义表示\(C\)

\[C=\mathcal{F} (x_1,x_2,...,x_m) \]

Decoder的任务是根据句子Source的中间语义表示\(C\)和之前已经生成的历史信息\(y_1,y_2,...,y_{i-1}\)来生成i时刻的输出单词\(y_i=\mathcal{g}(C,y_1,y_2,...,y_{i-1})\)

每个time step都依次产生\(y_i\),那么看起来就是整个系统根据输入句子Source生成了目标句子Target。

  • 如果Source是中文句子,Target是英文句子,那么这就是解决机器翻译问题的Encoder-Decoder框架;

  • 如果Source是一篇文章,Target是概括性的几句描述语句,那么这是文本摘要的Encoder-Decoder框架;

  • 如果Source是一句问句,Target是一句回答,那么这是问答系统或者对话机器人的Encoder-Decoder框架。

Encoder-Decoder框架不仅仅在文本领域广泛使用,在语音识别、图像处理等领域也经常使用。对语音识别来说,区别无非是Encoder部分的输入是语音流,输出是对应的文本信息;而对于“图像描述”任务来说,Encoder部分的输入是一副图片,Decoder的输出是能够描述图片语义内容的一句描述语。一般而言,文本处理和语音识别的Encoder部分通常采用RNN模型,图像处理的Encoder一般采用CNN模型

transformer内部结构

草稿纸上的Transformer

ENCODER结构

编码器之间没有共享参数,每个编码器可以分解为两个子层

img

从编码器输入的句子首先会经过一个自注意力(self-attention)层,这层帮助编码器在对每个单词编码时关注输入句子的其他单词。

自注意力层的输出会传递到前馈(feed-forward)神经网络中。每个位置的单词对应的前馈神经网络都完全一样(译注:另一种解读就是一层窗口为一个单词的一维卷积神经网络)。

解码器中也有编码器的自注意力(self-attention)层和前馈(feed-forward)层。除此之外,这两个层之间还有一个注意力层,用来关注输入句子的相关部分(和seq2seq模型的注意力作用相似)。

img

INPUT

graph LR subgraph 输入部分 Inputs-->input[input<br/>Embedding] pos1["Positional<br/>Encoding"] end input-->pos1

Word Embedding

Embedding 是一个将离散变量转为连续向量表示的一个方式,将一个词映射成为固定维度的稠密向量,例如有12个词组成的一句话要对其进行分析,则以每个词为单位分别映射为512维词向量

img

Word Embedding过程只发生在最底层的编码器中。所有的编码器都有一个相同的特点,即它们接收一个向量列表,列表中的每个向量大小为512维。在底层(最开始)编码器中它就是词向量,但是在其他编码器中,它就是下一层编码器的输出(也是一个向量列表)。向量列表大小是我们可以设置的超参数——一般是我们训练集中最长句子的长度

将输入序列进行Word Embedding之后,每个单词都会流经编码器中的两个子层

img

Transformer的一个核心特性,在这里输入序列中每个位置的单词都有自己独特的路径流入编码器。在自注意力层中,这些路径之间存在依赖关系。而前馈(feed-forward)层没有这些依赖关系。因此在前馈(feed-forward)层时可以并行执行各种路径

word2vec

what's word2vec?

在聊 Word2vec 之前,先聊聊 NLP (自然语言处理)。NLP 里面,最细粒度的是 词语,词语组成句子,句子再组成段落、篇章、文档。所以处理 NLP 的问题,首先就要拿词语开刀。

举个简单例子,判断一个词的词性,是动词还是名词。用机器学习的思路,我们有一系列样本(x,y),这里 x 是词语,y 是它们的词性,我们要构建 f(x)->y 的映射,但这里的数学模型 f(比如神经网络、SVM)只接受数值型输入,而 NLP 里的词语,是人类的抽象总结,是符号形式的(比如中文、英文、拉丁文等等),所以需要把他们转换成数值形式,或者说——嵌入到一个数学空间里,这种嵌入方式,就叫词嵌入(word embedding),而 Word2vec,就是词嵌入( word embedding) 的一种

我在前作『都是套路: 从上帝视角看透时间序列和数据挖掘』提到,大部分的有监督机器学习模型,都可以归结为:

f(x)->y

在 NLP 中,把 x 看做一个句子里的一个词语,y 是这个词语的上下文词语,那么这里的 f,便是 NLP 中经常出现的『语言模型』(language model),这个模型的目的,就是判断 (x,y) 这个样本,是否符合自然语言的法则,更通俗点说就是:词语x和词语y放在一起,是不是人话。

Word2vec 正是来源于这个思想,但它的最终目的,不是要把 f 训练得多么完美,而是只关心模型训练完后的副产物——模型参数(这里特指神经网络的权重),并将这些参数,作为输入 x 的某种向量化的表示,这个向量便叫做——词向量(这里看不懂没关系,下一节我们详细剖析)。

我们来看个例子,如何用 Word2vec 寻找相似词:

  • 对于一句话:『她们 夸 吴彦祖 帅 到 没朋友』,如果输入 x 是『吴彦祖』,那么 y 可以是『她们』、『夸』、『帅』、『没朋友』这些词
  • 现有另一句话:『她们 夸 我 帅 到 没朋友』,如果输入 x 是『我』,那么不难发现,这里的上下文 y 跟上面一句话一样
  • 从而 f(吴彦祖) = f(我) = y,所以大数据告诉我们:我 = 吴彦祖(完美的结论)
Skip-gram 和 CBOW 模型
  • 如果是用一个词语作为输入,来预测它周围的上下文,那这个模型叫做『Skip-gram 模型』
  • 而如果是拿一个词语的上下文作为输入,来预测这个词语本身,则是 『CBOW 模型』
Skip-gram 和 CBOW 的简单情形

们先来看个最简单的例子。上面说到, y 是 x 的上下文,所以 y 只取上下文里一个词语的时候,语言模型就变成:

用当前词 x 预测它的下一个词 y

但如上面所说,一般的数学模型只接受数值型输入,这里的 x 该怎么表示呢? 显然不能用 Word2vec,因为这是我们训练完模型的产物,现在我们想要的是 x 的一个原始输入形式。

答案是:one-hot encoder

所谓 one-hot encoder,其思想跟特征工程里处理类别变量的 one-hot 一样(参考我的前作『数据挖掘比赛通用框架』、『深挖One-hot和Dummy背后的玄机』)。本质上是用一个只含一个 1、其他都是 0 的向量来唯一表示词语。

我举个例子,假设全世界所有的词语总共有 V 个,这 V 个词语有自己的先后顺序,假设『吴彦祖』这个词是第1个词,『我』这个单词是第2个词,那么『吴彦祖』就可以表示为一个 V 维全零向量、把第1个位置的0变成1,而『我』同样表示为 V 维全零向量、把第2个位置的0变成1。这样,每个词语都可以找到属于自己的唯一表示。

OK,那我们接下来就可以看看 Skip-gram 的网络结构了,x 就是上面提到的 one-hot encoder 形式的输入,y 是在这 V 个词上输出的概率,我们希望跟真实的 y 的 one-hot encoder 一样。

img

首先说明一点:隐层的激活函数其实是线性的,相当于没做任何处理(这也是 Word2vec 简化之前语言模型的独到之处),我们要训练这个神经网络,用反向传播算法,本质上是链式求导,在此不展开说明了,

当模型训练完后,最后得到的其实是神经网络的权重,比如现在输入一个 x 的 one-hot encoder: [1,0,0,…,0],对应刚说的那个词语『吴彦祖』,则在输入层到隐含层的权重里,只有对应 1 这个位置的权重被激活,这些权重的个数,跟隐含层节点数是一致的,从而这些权重组成一个向量 vx 来表示x,而因为每个词语的 one-hot encoder 里面 1 的位置是不同的,所以,这个向量 vx 就可以用来唯一表示 x。

*注意:上面这段话说的就是 Word2vec 的精髓!!*

此外,我们刚说了,输出 y 也是用 V 个节点表示的,对应V个词语,所以其实,我们把输出节点置成 [1,0,0,…,0],它也能表示『吴彦祖』这个单词,但是激活的是隐含层到输出层的权重,这些权重的个数,跟隐含层一样,也可以组成一个向量 vy,跟上面提到的 vx 维度一样,并且可以看做是词语『吴彦祖』的另一种词向量。而这两种词向量 vx 和 vy,正是 Mikolov 在论文里所提到的,『输入向量』和『输出向量』,一般我们用『输入向量』。

需要提到一点的是,这个词向量的维度(与隐含层节点数一致)一般情况下要远远小于词语总数 V 的大小,所以 Word2vec 本质上是一种降维操作——把词语从 one-hot encoder 形式的表示降维到 Word2vec 形式的表示。

Skip-gram 更一般的情形

上面讨论的是最简单情形,即 y 只有一个词,当 y 有多个词时,网络结构如下:

img

可以看成是 单个x->单个y 模型的并联,cost function 是单个 cost function 的累加(取log之后)

如果你想深入探究这些模型是如何并联、 cost function 的形式怎样

CBOW 更一般的情形

跟 Skip-gram 相似,只不过:

Skip-gram 是预测一个词的上下文,而 CBOW 是用上下文预测这个词

网络结构如下

img

跟 Skip-gram 的模型并联不同,这里是输入变成了多个单词,所以要对输入处理下(一般是求和然后平均),输出的 cost function 不变

Word2vec 的训练trick

相信很多初次踩坑的同学,会跟我一样陷入 Mikolov 那篇论文(https://cs.stanford.edu/~quocle/paragraph_vector.pdf,https://arxiv.org/pdf/1301.3781)里提到的 hierarchical softmax 和 negative sampling 里不能自拔,但其实,它们并不是 Word2vec 的精髓,只是它的训练技巧,但也不是它独有的训练技巧。 Hierarchical softmax 只是 softmax 的一种近似形式(详见参考资料7.),而 negative sampling 也是从其他方法借鉴而来。

为什么要用训练技巧呢? 如我们刚提到的,Word2vec 本质上是一个语言模型,它的输出节点数是 V 个,对应了 V 个词语,本质上是一个多分类问题,但实际当中,词语的个数非常非常多,会给计算造成很大困难,所以需要用技巧来加速训练。

这里我总结了一下这两个 trick 的本质,有助于大家更好地理解,在此也不做过多展开

  • hierarchical softmax

    • 本质是把 N 分类问题变成 log(N)次二分类
  • negative sampling

    • 本质是预测总体类别的一个子集
扩展

很多时候,当我们面对林林总总的模型、方法时,我们总希望总结出一些本质的、共性的东西,以构建我们的知识体系,比如我在前作『分类和回归的本质』里,原创性地梳理了分类模型和回归模型的本质联系,比如在词嵌入领域,除了 Word2vec之外,还有基于共现矩阵分解的 GloVe 等等词嵌入方法。

深入进去我们会发现,神经网络形式表示的模型(如 Word2vec),跟共现矩阵分解模型(如 GloVe),有理论上的相通性,这里我推荐大家阅读参考资料5. ——来斯惟博士在它的博士论文附录部分,证明了 Skip-gram 模型和 GloVe 的 cost fucntion 本质上是一样的。是不是一个很有意思的结论? 所以在实际应用当中,这两者的差别并不算很大,尤其在很多 high-level 的 NLP 任务(如句子表示、命名体识别、文档表示)当中,经常把词向量作为原始输入,而到了 high-level 层面,差别就更小了。

鉴于词语是 NLP 里最细粒度的表达,所以词向量的应用很广泛,既可以执行词语层面的任务,也可以作为很多模型的输入,执行 high-level 如句子、文档层面的任务,包括但不限于:

  • 计算相似度

    • 寻找相似词
    • 信息检索
  • 作为 SVM/LSTM 等模型的输入

    • 中文分词
    • 命名体识别
  • 句子表示

    • 情感分析
  • 文档表示

    • 文档主题判别
Q&A

Q1. gensim 和 google的 word2vec 里面并没有用到onehot encoder,而是初始化的时候直接为每个词随机生成一个N维的向量,并且把这个N维向量作为模型参数学习;所以word2vec结构中不存在文章图中显示的将V维映射到N维的隐藏层。

A1. 其实,本质是一样的,加上 one-hot encoder 层,是为了方便理解,因为这里的 N 维随机向量,就可以理解为是 V 维 one-hot encoder 输入层到 N 维隐层的权重,或者说隐层的输出(因为隐层是线性的)。每个 one-hot encoder 里值是 1 的那个位置,对应的 V 个权重被激活,其实就是『从一个V*N的随机词向量矩阵里,抽取某一行』。学习 N 维向量的过程,也就是优化 one-hot encoder 层到隐含层权重的过程

Q2. hierarchical softmax 获取词向量的方式和原先的其实基本完全不一样,我初始化输入的也不是一个onehot,同时我是直接通过优化输入向量的形式来获取词向量?如果用了hierarchical 结构我应该就没有输出向量了吧?

A2. 初始化输入依然可以理解为是 one-hot,同上面的回答;确实是只能优化输入向量,没有输出向量了。具体原因,我们可以梳理一下不用 hierarchical (即原始的 softmax) 的情形:

隐含层输出一个 N 维向量 x, 每个x 被一个 N 维权重 w 连接到输出节点上,有 V 个这样的输出节点,就有 V 个权重 w,再套用 softmax 的公式,变成 V 分类问题。这里的类别就是词表里的 V 个词,所以一个词就对应了一个权重 w,从而可以用 w 作为该词的词向量,即文中的输出词向量。

PS. 这里的 softmax 其实多了一个『自由度』,因为 V 分类只需要 V-1 个权重即可

我们再看看 hierarchical softmax 的情形:

隐含层输出一个 N 维向量 x, 但这里要预测的目标输出词,不再是用 one-hot 形式表示,而是用 huffman tree 的编码,所以跟上面 V 个权重同时存在的原始 softmax 不一样, 这里 x 可以理解为先接一个输出节点,即只有一个权重 w1 ,输出节点输出 1/1+exp(-w*x),变成一个二分类的 LR,输出一个概率值 P1,然后根据目标词的 huffman tree 编码,将 x 再输出到下一个 LR,对应权重 w2,输出 P2,总共遇到的 LR 个数(或者说权重个数)跟 huffman tree 编码长度一致,大概有 log(V) 个,最后将这 log(V) 个 P 相乘,得到属于目标词的概率。但注意因为只有 log(V) 个权重 w 了,所以跟 V 个词并不是一一对应关系,就不能用 w 表征某个词,从而失去了词向量的意义

PS. 但我个人理解,这 log(V) 个权重的组合,可以表示某一个词。因为 huffman tree 寻找叶子节点的时候,可以理解成是一个不断『二分』的过程,不断二分到只剩一个词为止。而每一次二分,都有一个 LR 权重,这个权重可以表征该类词,所以这些权重拼接在一起,就表示了『二分』这个过程,以及最后分到的这个词的『输出词向量』。

我举个例子:

假设现在总共有 (A,B,C)三个词,huffman tree 这么构建:
第一次二分: (A,B), (C)
假如我们用的 LR 是二分类 softmax 的情形(比常见 LR 多了一个自由度),这样 LR 就有俩权重,权重 w1_1 是属于 (A,B) 这一类的,w1_2 是属于 (C) 的, 而 C 已经到最后一个了,所以 C 可以表示为 w1_2

第二次二分: (A), (B)
假设权重分别对应 w2_1 和 w2_2,那么 A 就可以表示为 [w1_1, w2_1], B 可以表示为 [w1_1, w2_2]

这样, A,B,C 每个词都有了一个唯一表示的词向量(此时他们长度不一样,不过可以用 padding 的思路,即在最后补0)

当然了,一般没人这么干。。。开个脑洞而已

Q3. 是否一定要用Huffman tree?

A3. 未必,比如用完全二叉树也能达到O(log(N))复杂度。但 Huffman tree 被证明是更高效、更节省内存的编码形式,所以相应的权重更新寻优也更快。 举个简单例子,高频词在Huffman tree中的节点深度比完全二叉树更浅,比如在Huffman tree中深度为3,完全二叉树中深度为5,则更新权重时,Huffmantree只需更新3个w,而完全二叉树要更新5个,当高频词频率很高时,算法效率高下立判

Encoding

上一小节提到,一个编码器接收向量列表作为输入,接着将向量列表中的向量传递到自注意力层进行处理,然后传递到前馈神经网络层中,将输出结果传递到下一个编码器中,本文采用一个简短的句子“Thinking Machines”来展示transformer的过程

img

RNN

单层网络

在学习RNN之前,首先要了解一下最基本的单层网络,它的结构如图:

img

输入是\(x\),经过变换\(Wx+b\)和激活函数\(f\)得到输出\(y\)

经典的RNN结构(N VS N)

在实际应用中,我们会遇到很多序列形的数据:

img

  • 自然语言处理问题。x1可以看做是第一个单词,x2可以看做是第二个单词,依次类推。
  • 语音处理。此时,x1、x2、x3……是每帧的声音信号。
  • 时间序列问题。例如每天的股票价格等等

序列形的数据就不太好用原始的神经网络处理了。为了建模序列问题,RNN引入了隐状态\(h\)(hidden state)的概念,\(h\)可以对序列形的数据提取特征,接着再转换为输出。先从\(h_1\)的计算开始看:

img

  • 圆圈或方块表示的是向量。
  • 一个箭头就表示对该向量做一次变换。如上图中\(h_0\)\(x_1\)分别有一个箭头连接,就表示对\(h_0\)\(x_1\)各做了一次变换。

在很多论文中也会出现类似的记号,初学的时候很容易搞乱,但只要把握住以上两点,就可以比较轻松地理解图示背后的含义。

依次计算剩下来的(使用相同的参数U、W、b):

img

我们这里为了方便起见,只画出序列长度为4的情况,实际上,这个计算过程可以无限地持续下去。

我们目前的RNN还没有输出,得到输出值的方法就是直接通过\(h\)进行计算:

img

正如之前所说,一个箭头就表示对对应的向量做一次类似于\(f(Wx+b)\)的变换,这里的这个箭头就表示对\(h_1\)进行一次变换,得到输出\(y_1\)

剩下的输出类似进行(使用和\(y_1\)同样的参数\(V\)\(c\)):

img

OK!大功告成!这就是最经典的RNN结构,我们像搭积木一样把它搭好了。它的输入是\(x_1, x_2,...,x_n\),输出为\(y_1,y_2,...,y_n\),也就是说,输入和输出序列必须要是等长的

由于这个限制的存在,经典RNN的适用范围比较小,但也有一些问题适合用经典的RNN结构建模,如:

  • 计算视频中每一帧的分类标签。因为要对每一帧进行计算,因此输入和输出序列等长。

  • 输入为字符,输出为下一个字符的概率。这就是著名的Char RNN(详细介绍请参考:The Unreasonable Effectiveness of Recurrent Neural Networks,Char RNN可以用来生成文章,诗歌,甚至是代码,非常有意思)

RNN变种(N VS 1)

有的时候,我们要处理的问题输入是一个序列,输出是一个单独的值而不是序列,应该怎样建模呢?实际上,我们只在最后一个h上进行输出变换就可以了

img

这种结构通常用来处理序列分类问题。如输入一段文字判别它所属的类别,输入一个句子判断其情感倾向,输入一段视频并判断它的类别等等

RNN变种(1 VS N)

输入不是序列而输出为序列的情况怎么处理?我们可以只在序列开始进行输入计算

img

还有一种结构是把输入信息X作为每个阶段的输入

img

也可以等价表示为

img

这种1 VS N的结构可以处理的问题有:

  • 从图像生成文字(image caption),此时输入的X就是图像的特征,而输出的y序列就是一段句子
  • 从类别生成语音或音乐等
RNN最重要变种(N VS M)又称Encoder-Decoder

原始的N vs N RNN要求序列等长,然而我们遇到的大部分问题序列都是不等长的,如机器翻译中,源语言和目标语言的句子往往并没有相同的长度

为此,Encoder-Decoder结构先将输入数据编码成一个上下文向量\(c\)

img

得到\(c\)有多种方式,最简单的方法就是把Encoder的最后一个隐状态赋值给\(c\),还可以对最后的隐状态做一个变换得到\(c\),也可以对所有的隐状态做变换。

拿到c之后,就用另一个RNN网络对其进行解码,这部分RNN网络被称为Decoder。具体做法就是将c当做之前的初始状态\(h_0\)输入到Decoder中

img

另一种方法是将\(c\)作为每一步的输入

img

由于这种Encoder-Decoder结构不限制输入和输出的序列长度,因此应用的范围非常广泛,比如:

  • 机器翻译。Encoder-Decoder的最经典应用,事实上这一结构就是在机器翻译领域最先提出的
  • 文本摘要。输入是一段文本序列,输出是这段文本序列的摘要序列。
  • 阅读理解。将输入的文章和问题分别编码,再对其进行解码得到问题的答案。
  • 语音识别。输入是语音信号序列,输出是文字序列
Seq2Seq——Encoder-Decoder结构的模型

Seq2Seq其实就是Encoder-Decoder结构的网络,它的输入是一个序列,输出也是一个序列。在Encoder中,将序列转换成一个固定长度的向量,然后通过Decoder将该向量转换成我们想要的序列输出出来

简说Seq2Seq原理及实现

注:当把语言当作一个sequence,<_BOS>可以看成是它的初始状态,<_EOS>则通常当作判断终止的标签

ncoder和Decoder一般都是RNN,通常为LSTM或者GRU,图中每一个方格都为一个RNN单元

在Encoder中,“欢迎/来/北京”这些词转换成词向量,也就是Embedding,我们用\(v_i\)来表示,与上一时刻的隐状态\(h_{i-1}\)按照时间顺序进行输入,每一个时刻输出一个隐状态\(h_i\),我们可以用函数\(f\)表达RNN隐藏层的变换:\(hi=f(v_i,h_{i-1})\)。假设有t个词,最终通过Encoder自定义函数\(q\)将各时刻的隐状态变换为向量\(c\)\(c=q(h_0,...,h_t)\),这个\(c\)就相当于从“欢迎/来/北京”这几个单词中提炼出来的大概意思一样,包含了这句话的含义

Decoder的每一时刻的输入为Eecoder输出的\(c\)和Decoder前一时刻解码的输出\(s_{i-1}\)还有前一时刻预测的词的向量\(E_{i-1}\) (如果是预测第一个词的话,此时输入的词向量为“_GO”的词向量,标志着解码的开始),我们可以用函数\(g\)表达解码器隐藏层变换\(s_i=g(c,s_{i-1},E_{i-1})\)。直到解码解出“_EOS”,标志着解码的结束

Seq2Seq's tricks
Teacher Forcing

在基础的模型中,Decoder的每一次解码又会作为下一次解码的输入,这样就会导致一个问题就是错误累计,如果其中一个RNN单元解码出现误差了,那么这个误差就会传递到下一个RNN单元,使训练结果误差越来越大。Teacher Forcing在一定程度上解决了这个问题,它的流程如图所示,在训练过程中,使用要解码的序列作为输入进行训练,但是在inference/test阶段是不能使用的,因为你不知道要预测的序列是个啥,当然只在训练过程中效果就很不错了,它帮助模型加速收敛。

简而言之:使用了 Teacher Forcing,不管模型上一个时刻的实际输出的是什么,哪怕输出错了,下一个时间片的输入总是上一个时间片的期望输出

基础模型

简说Seq2Seq原理及实现

teacher forcing

img

Attention

跟之前基础 seq2seq 模型的区别,就是给 decoder 多提供了一个输入“c”。因为 encoder把很长的句子压缩只成了一个小向量“u”,decoder在解码的过程中没准走到哪一步就把“u”中的信息忘了,所以在decoder 解码序列的每一步中,都再把 encoder 的 outputs 拉过来让它回忆回忆。但是输入序列中每个单词对 decoder 在不同时刻输出单词时的帮助作用不一样,所以就需要提前计算一个 attention score 作为权重分配给每个单词,再将这些单词对应的 encoder output 带权加在一起,就变成了此刻 decoder 的另一个输入“c”

Attention-Decoder

在inference/test阶段,不能使用Teacher Forcing,那么只能使用上一时刻解码的输出作为下一个解码的输入,刚才也说过了这样会导致误差传递,怎么解决这个问题呢?答案就是Beam Search!

之前基础的 seq2seq 版本在输出序列时,仅在每个时刻选择概率 top 1 的单词作为这个时刻的输出单词(相当于局部最优解),然后把这些词串起来得到最终输出序列。实际上就是贪心策略

在每个时刻解码器都会选择Top k个预测结果作为下一个解码器的输入,将这K个结果逐一输入到解码器进行解码,就会产生K*L(L为词表大小)个预测结果,从所有的解码结果中再选出Top K个预测结果作为下一个解码器的输入,在最后一个时刻再选出Top 1作为最终的输出,有点带剪枝的动态规划的意思。

img

Sequence Loss

按照通常的 loss 计算方法,如图假设 batch size=4,max_seq_len=4,需要分别计算这 4*4 个位置上的 loss。但是实际上_PAD上的 loss 计算是没有用的,因为_PAD本身没有意义,也不指望 decoder 去输出这个字符,只是占位用的,计算 loss 反而带来副作用,影响参数的优化

img

所以需要在 loss 上乘一个 mask 矩阵,这个矩阵可以把_PAD位置上的 loss 筛掉。其实有了这个 sequence_mask 矩阵之后,直接乘在 loss 矩阵上就完事了。

What's attention?

视觉注意力机制是人类视觉所特有的大脑信号处理机制。人类视觉通过快速扫描全局图像,获得需要重点关注的目标区域,也就是一般所说的注意力焦点,而后对这一区域投入更多注意力资源,以获取更多所需要关注目标的细节信息,而抑制其他无用信息。深度学习中的注意力机制从本质上讲和人类的选择性视觉注意力机制类似,核心目标也是从众多信息中选择出对当前任务目标更关键的信息。

img

Why we need Attention?

img

经典的Encoder-Decoder框架是没有体现出“注意力模型”的,所以可以把它看作是注意力不集中的分心模型。为什么说它注意力不集中呢?请观察下目标句子Target中每个单词的生成过程如下

\[y_1=f(C)\\y_2=f(C,y_1)\\y_3=f(C,y_1,y_2) \]

其中\(f\)是Decoder的非线性变换函数。从这里可以看出,在生成目标句子的单词时,不论生成哪个单词,它们使用的输入句子Source的语义编码C都是一样的,没有任何区别

而语义编码\(C\)是由句子Source的每个单词经过Encoder编码产生的,这意味着不论是生成哪个单词,\(y_1\),\(y_2\)还是\(y_3\),其实句子Source中任意单词对生成某个目标单词\(y_i\)来说影响力都是相同的,这是为何说这个模型没有体现出注意力的缘由。这类似于人类看到眼前的画面,但是眼中却没有注意焦点一样

如果拿机器翻译来解释这个分心模型的Encoder-Decoder框架更好理解,比如输入的是英文句子:Tom chase Jerry,Encoder-Decoder框架逐步生成中文单词:“汤姆”,“追逐”,“杰瑞”。

在翻译“杰瑞”这个中文单词的时候,分心模型里面的每个英文单词对于翻译目标单词“杰瑞”贡献是相同的,很明显这里不太合理,显然“Jerry”对于翻译成“杰瑞”更重要,但是分心模型无法体现这一点的,这就是为何说它没有引入注意力的原因。

没有引入注意力的模型在输入句子比较短的时候问题不大,但是如果输入句子比较长,此时所有语义完全通过一个中间语义向量来表示,单词自身的信息已经消失,可想而知会丢失很多细节信息,这也是为何要引入注意力模型的重要原因

soft Attention

如果引入Attention模型的话,应该在翻译“杰瑞”的时候,体现出英文单词对于翻译当前中文单词不同的影响程度,比如给出类似下面一个概率分布值:

\[(Tom,0.3),(Chaes,0.2),(Jerry,0.5) \]

每个英文单词的概率代表了翻译当前单词“杰瑞”时,注意力分配模型分配给不同英文单词的注意力大小。这对于正确翻译目标语单词肯定是有帮助的,因为引入了新的信息

同理,目标句子中的每个单词都应该学会其对应的源语句子中单词的注意力分配概率信息。这意味着在生成每个单词\(y_i\)的时候,原先都是相同的中间语义表示\(C\)会被替换成根据当前生成单词而不断变化的\(C_i\)。理解Attention模型的关键就是这里,即由固定的中间语义表示\(C\)换成了根据当前输出单词来调整成加入注意力模型的变化的\(C_i\)

img

引入注意力模型的Encoder-Decoder框架

生成目标句子单词的过程成了下面的形式:

\[y_1=f1(C_1)\\y_2=f1(C_2,y_1)\\y_3=f1(C_3,y_1,y_2) \]

每个\(C_i\)可能对应着不同的源语句子单词的注意力分配概率分布,比如对于上面的英汉翻译来说,其对应的信息可能如下:

\[C_{汤姆}=g(0.6*f2("Tom"),0.2*f2("Chase"),0.2*f2("Jerry"))\\C_{追逐}=g(0.2*f2("Tom"),0.7*f2("Chase"),0.1*f2("Jerry")) \\C_{杰瑞}=g(0.3*f2("Tom"),0.2*f2("Chase"),0.5*f2("Jerry")) \]

其中,\(f2\)函数代表Encoder对输入英文单词的某种变换函数,比如如果Encoder是用的RNN模型的话,这个\(f2\)函数的结果往往是某个时刻输入\(x_i\)后隐层节点的状态值;\(g\)代表Encoder根据单词的中间表示合成整个句子中间语义表示的变换函数,一般的做法中,\(g\)函数就是对构成元素加权求和,即下列公式

\[C_i=\sum^{L_x}_{i=1}a_{ij}h_j \]

其中,\(L_x\)代表输入句子Source的长度,\(a_{ij}\)代表在Target输出第\(i\)个单词时Source输入句子中第\(j\)个单词的注意力分配系数(即Encoder中第j阶段的hj和解码时第i阶段的相关性最终Decoder中第i阶段的输入的上下文信息\(c_i\)就来自于所有\(h_j\)\(a_{ij}\)的加权和),而\(h_j\)则是Source输入句子中第\(j\)个单词的语义编码。假设下标\(i\)就是上面例子所说的“汤姆”,那么\(L_x\)就是3,\(h_1=f(“Tom”),h_2=f(“Chase”),h_3=f(“Jerry”)\)分别是输入句子每个单词的语义编码,对应的注意力模型权值则分别是\(0.6,0.2,0.2\),所以\(g\)函数本质上就是个加权求和函数。如果形象表示的话,翻译中文单词“汤姆”的时候,数学公式对应的中间语义表示\(C_i\)的形成过程类似

img

Attention的形成过程

这里还有一个问题:生成目标句子某个单词,比如“汤姆”的时候,如何知道Attention模型所需要的输入句子单词注意力分配概率分布值呢?就是说“汤姆”对应的输入句子Source中各个单词的概率分布:\((Tom,0.6)(Chase,0.2) (Jerry,0.2)\)是如何得到的呢?

为了便于说明,我们假设对非Attention模型的Encoder-Decoder框架进行细化,Encoder采用RNN模型,Decoder也采用RNN模型,这是比较常见的一种模型配置。

img

基于此细化结构,可以较为便捷地说明注意力分配概率分布值的通用计算过程

img

对于采用RNN的Decoder来说,在时刻\(i\),如果要生成\(y_i\)单词,我们是可以知道Target在生成\(Y_i\)之前的时刻\(i-1\)时,隐层节点\(i-1\)时刻的输出值\(H_{i-1}\),而我们的目的是要计算生成\(Y_i\)时输入句子中的单词“Tom”、“Chase”、“Jerry”对\(Y_i\)来说的注意力分配概率分布,那么可以用Target输出句子\(i-1\)时刻的隐层节点状态\(H_{i-1}\)去分别和输入句子Source中每个单词对应的RNN隐层节点状态\(hj\)进行对比,即通过函数\(F(h_j,H_{i-1})\)来获得目标单词\(y_i\)和每个输入单词对应的对齐可能性,这个\(F\)函数在不同论文里可能会采取不同的方法,然后函数\(F\)的输出经过Softmax进行归一化就得到了符合概率分布取值区间的注意力分配概率分布数值

绝大多数Attention模型都是采取上述的计算框架来计算注意力分配概率分布信息,区别只是在F的定义上可能有所不同

下图可视化地展示了在英语-德语翻译系统中加入Attention机制后,Source和Target两个句子每个单词对应的注意力分配概率分布

img

上述内容就是经典的Soft Attention模型的基本思想

Attention的物理含义

一般在自然语言处理应用里会把Attention模型看作是输出Target句子中某个单词和输入Source句子每个单词的对齐模型,这是非常有道理的

目标句子生成的每个单词对应输入句子单词的概率分布可以理解为输入句子单词和这个目标生成单词的对齐概率,这在机器翻译语境下是非常直观的:传统的统计机器翻译一般在做的过程中会专门有一个短语对齐的步骤,而注意力模型其实起的是相同的作用

Self-Attention at a High Level

例如,下列句子是我们想要翻译的输入句子:

The animal didn't cross the street because it was too tired

这个“it”在这个句子是指什么呢?它指的是street还是这个animal呢?这对于人类来说是一个简单的问题,但是对于算法则不是。

当模型处理这个单词“it”的时候,自注意力机制会允许“it”与“animal”建立联系。

随着模型处理输入序列的每个单词,自注意力会关注整个输入序列的所有单词,帮助模型对本单词更好地进行编码。

如果你熟悉RNN(循环神经网络),回忆一下它是如何维持隐藏层的。RNN会将它已经处理过的前面的所有单词/向量的表示与它正在处理的当前单词/向量结合起来。而自注意力机制会将所有相关单词的理解融入到我们正在处理的单词中。

img

上图中$U,V,W$参数是一整套网络通用的,不根据时间变化

当我们在编码器#5(栈中最上层编码器)中编码“it”这个单词的时,注意力机制的部分会去关注“The Animal”,将它的表示的一部分编入“it”的编码中。

可视化transformer下载

img

Self-Attention in Detail

img
本质
  • 将Source中的构成元素想象成是由一系列的<Key,Value>数据对构成,此时给定Target中的某个元素Query,通过计算Query和各个Key的相似性或者相关性,得到每个Key对应Value的权重系数,然后对Value进行加权求和,即得到了最终的Attention数值

  • Attention机制本质是对Source中元素的Value值进行加权求和,而Query和Key用来计算对应Value的权重系数

  • \[Attention(Query,Source)=\sum^{L_x}_{i=1}Similarity(Query,Key_i)*Value_i \]

    其中$L_x=\left | Source \right | $代表Source的长度

    \[Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V \]

    img

    在NLP中Source中的Key和Value合二为一,指向的是同一个东西,也即输入句子中每个单词对应的语义编码,所以可能不容易看出这种能够体现本质思想的结构

  • 从概念上理解,把Attention仍然理解为从大量信息中有选择地筛选出少量重要信息并聚焦到这些重要信息上,忽略大多不重要的信息,这种思路仍然成立。聚焦的过程体现在权重系数的计算上,权重越大越聚焦于其对应的Value值上,即权重代表了信息的重要性,而Value是其对应的信息

为什么要\(\frac{QK^T}{\sqrt{d_k}}\)?即为什么要放缩,而且还是纬度的根号?
为什么比较大的输入会使得softmax的梯度变得很小?

对于一个输入向量\(x\in \mathbb{R}^d\),softmax函数将其映射/归一化到一个分布\(\hat{y}\in\mathbb{R}^d\)。在这个过程中,softmax先用一个自然底数\(e\)将输入中的元素间差距先“拉大”,然后归一化为一个分布。假设某个输入\(x\)中最大的的元素下标是\(k\)如果输入的数量级变大(每个元素都很大),\(\hat{y}_k\)会非常接近1。

我们可以用一个小例子来看看\(x\)的数量级对输入最大元素对应的预测概率\(\hat{y}_k\)的影响。假定输入\(x=[a,a,2a]^T\),我们来看不同量级\(a\)的产生的\(\hat{y}_3\)有什么区别。

  • \(a=1时,\hat{y}_3\)=0.5761168847658291
  • \(a=10时,\hat{y}_3=0.999909208384341\)
  • \(a=100时,\hat{y}_3\approx1.0\)(计算机精度限制)。

我们不妨把\(a\)在不同取值下,对应的\(\hat{y}_3\)全部绘制出来。代码如下:

from math import exp
from matplotlib import pyplot as plt
import numpy as np 
f = lambda x: exp(x * 2) / (exp(x) + exp(x) + exp(x * 2))
x = np.linspace(0, 100, 100)
y_3 = [f(x_i) for x_i in x]
plt.plot(x, y_3)
plt.show()

img

可以看到,数量级对softmax得到的分布影响非常大。在数量级较大时,softmax将几乎全部的概率分布都分配给了最大值对应的标签

然后我们来看softmax的梯度。不妨简记softmax函数为\(g(\cdot)\),softmax得到的分布向量\(\hat{y}=g(x)\)对输入的\(x\)的梯度为:

\[\frac{\partial g(x)}{\partial x}\approx diag(\hat{y})-\hat{y}\hat{y}^T \ \in\mathbb{R}^{d\times d} \]

将矩阵展开

\[\frac{\partial g(x)}{\partial x}=\begin{bmatrix} \hat{y}_1&0&...&0\\ 0&\hat{y}_2&...& 0\\ \vdots&\vdots&\ddots& \\ 0&0&...&\hat{y}_d \end{bmatrix}- \begin{bmatrix} \hat{y}_1^2&\hat{y}_1\hat{y}_2&...&\hat{y}_1\hat{y}_d \\ \hat{y}_2\hat{y}_1&\hat{y}_2^2&...&\hat{y}_2\hat{y}_d\\ \vdots&\vdots&\ddots&\vdots\\ \hat{y}_d\hat{y}_1&\hat{y}_d\hat{y}_2&...&\hat{y}_d^2 \end{bmatrix}\]

根据前面的讨论,当输入\(x\)的元素均较大时,softmax会把大部分概率分布分配给最大的元素,假设我们的输入数量级很大,最大的元素是\(x_1\),那么就将产生一个接近one-hot的向量\(\hat{y}\approx [1,0,...,0]^T\),此时上面的矩阵变为如下形式

\[\frac{\partial g(x)}{\partial x}\approx \begin{bmatrix} 1&0&...&0\\ 0&0&...& 0\\ \vdots&\vdots&\ddots& \\ 0&0&...&0 \end{bmatrix}-\begin{bmatrix} 1&0&...&0\\ 0&0&...& 0\\ \vdots&\vdots&\ddots& \\ 0&0&...&0 \end{bmatrix}=0\]

也就是说,在输入的数量级很大时,梯度消失为0,造成参数更新困难

维度与点积大小的关系是怎么样的,为什么使用维度的根号来放缩?

假设向量\(q,k\)的各个分量是相互独立的随机变量,均值是0,方差是1,那么点积\(q\cdot k\)的均值是0,方差是\(d_K\),下面为更详细的推导:

\(\forall i=1,...,d_k\),\(q_i和k_i\)都是随机变量,为了方便书写,不妨记\(X=q_i,Y=k_i\),则有\(D(X)=D(Y)=1\),\(E(X)=E(Y)=0\)

  1. \(E(XY)=E(X)E(Y)=0\times 0=0\)
  2. \(\begin{align} D(XY)&=E(X^2\cdot Y^2)-[E(XY)]^2\\&=E(X^2)E(Y^2)-[E(X)E(Y)]^2 \\&=E(X^2-0^2)E(Y^2-0^2)-[E(X)E(Y)]^2\\&=E(X^2-[E(X)]^2)E(Y^2-[E(Y)]^2)-[E(X)E(Y)]^2\\&=D(X)D(Y)-[E(X)E(Y)]^2\\&=1\times 1-(0\times 0)^2\\&=1\end{align}\)

这样\(\forall i=1,..,d_k\)\(q_i和k_i\)都是0,方差是1,又由于期望和方差的性质,对相互独立的分量\(Z_i\),有

\[E(\sum_iZ_i)=\sum_iE(Z_i) \]

\[D(\sum_iZ_i)=\sum_iD(Z_i) \]

所以有\(q,k\)的均值\(E(q\cdot k)=0\),方差\(D(q\cdot k)=d_k\)

方差越大也就说明,点积的数量级越大(以越大的概率取大值)。那么一个自然的做法就是把方差稳定到1,做法是将点积除以\(\sqrt{d_k}\),这样就有

\[D(\frac{q\cdot k}{\sqrt{d_k}})=\frac{d_k}{(\sqrt{d_k})^2}=1 \]

将方差控制为1,也就有效地控制了前面提到的梯度消失的问题

为什么需要将方差控制为1?

方差大表示各个分量的差距较大,然后softmax中的指数运算会进一步加大差距,导致最大值对应的概率很大,其他分量的概率很小。容易导致梯度消失,所以需要将其方差归一化到1

\(Q,K,V\)的含义各是什么呢?

对于待解码的词向量\(E(x_i)\),需要考虑它上一时刻的隐状态\(s_{i-1}\),与Encoder部分的每一个隐状态\(h_j\)行比较,通过某个打分函数\(a(\cdot)\)得到权重\(a_{ij}=a(s_{i-1},h_j)\)再将这\(j\)个权重赋给j个Encoder的隐状态\(h_j\)得到加权求和的向量\(c_i=\sum_j a_{ij}h_j\)

在上面这个过程中,不难发现实际上操作的有三类变量

第一类是需要与一个变量集合逐一比较的,我们称为query,简记为q。

而那个集合里每一个变量作为第二类变量,是要与q进行比较的,称为key,简记为k。

而最后一类变量是被权重\(a_{ij}\)对应j位置赋权的,称为value,简记为v

这样定义,很容易发现key和value是一一对应的,因为第j个需要和query比较的key,得到的权重要赋予给第j个value

对应上面的具体过程就是\(q=s_{i-1},k=v=j_j\)

有了这种抽象的表述,将每一个q,k,v以矩阵形式表述,可以表示为:

\[Q=(q_1,q_2,...,q_m)^T\\K=(k_1,k_2,...,k_n)^T\\V=(v_1,v_2,...,v_n)^T\\m,n为序列的长度 \]

\(Q,K,V\)的值是如何得出的呢?

计算自注意力的第一步就是从每个编码器的输入向量(每个单词的词向量)中生成三个向量。也就是说对于每个单词,我们创造一个Q向量、一个K向量和一个V向量。这三个向量是通过Word Embedding入与三个权重矩阵后相乘创建的。

可以发现这些新向量在维度上比词嵌入向量更低。他们的维度是64,而Word Embedding和编码器的输入/输出向量的维度是512. 但实际上不强求维度更小,这只是一种基于架构上的选择,它可以使多头注意力(multiheaded attention)的大部分计算保持不变。

img

\(X_1\)\(W_Q\)权重矩阵相乘得到\(q_1\), 就是与这个单词相关的查询向量

假设我们在为例子中的第一个词“Thinking”计算自注意力向量,我们需要拿输入句子中的每个单词对“Thinking”打分。这些分数决定了在编码单词“Thinking”的过程中有多重视句子的其它部分

这些分数是通过打分单词(所有输入句子的单词)的K向量与“Thinking”的Q向量相点积来计算的。所以如果我们是处理位置最靠前的词的自注意力的话,第一个分数是q1和k1的点积,第二个分数是q1和k2的点积

img

算出得分后,将分数除以\(\sqrt{d_k}\)(即K向量维数的平方根),然后通过softmax传递结果。

softmax的作用是使所有单词的分数归一化,即分数都为正值且和为1

img

这个softmax分数决定了每个单词对编码当下位置(“Thinking”)的贡献。显然,已经在这个位置上的单词将获得最高的softmax分数,但有时关注另一个与当前单词相关的单词也会有帮助。

之后将每个V向量乘以softmax分数(这是为了准备之后将它们求和)。这里的初衷是希望关注语义上相关的单词,并弱化不相关的单词(例如,让它们乘以0.001这样的小数)

下一步对加权值向量求和,然后即得到自注意力层在该位置的输出(在我们的例子中是对于第一个单词)

img

至此 self-attention calculation计算完成,但是实际算法组我们采用了矩阵来加速上述步骤

img

img

Attention计算
  • 如果对目前大多数方法进行抽象的话,可以将其具体计算过程归纳为两个过程

    1. 根据Query和Key计算权重系数

      1. 根据Query和Key计算两者的相似性或者相关性
      2. 对上一步的原始分值进行归一化处理
    2. 根据权重系数对Value进行加权求和

    • 根据上述过程可以将Attention计算总结为三阶段

      • 第一阶段

        • 可以引入不同函数和计算机制,根据\(Query,Key_i\),计算两者相似性或者相关性

        • 常见方法

          \[点积:Similarity(Query,Key_i)=Query\cdot Key_i \]

          \[Cosine相似性:Similarity(Query,Key_i)=\frac{Query\cdot Key_i}{\left \| Query \right \| \left \|Key_i \right \| } \]

          \[MLP网络:Similary(Query,Key_i)=MLP(Query,Key_i) \]

        • 本阶段产生的分值根据具体产生的方法不同其数值取值范围也不一样

      • 第二阶段

        • 引入类似SoftMax的计算方式对第一阶段的得分进行数值转换,一方面可以进行归一化,将原始计算分值整理成所有元素权重之和为1的概率分布;另一方面也可以通过SoftMax的内在机制更加突出重要元素的权重

        • 一般采用如下公式计算

          \[a_i=Softmax(Sim_i)=\frac{e^{Sim_i}}{\sum^{L_x}_{i=1}e^{Sim_j}} \]

      • 第三阶段

        • 上一阶段计算结果\(a_i\)即为\(Vealue_i\)对应的权重系数,之后进行加权求和即可得到Attention数值

          \[Attention(Query,Source)=\sum^{L_x}_{i=1}a_i\cdot Value_i \]

      img

  • 例子

    img

    以“我”“很”“帅”为例,对于“我”这个词来说,将其词向量作为query向量q,将"我很帅"及句子的起始和终止符号每个词的词向量都作为待对比的key,value向量取与key相同。设通过Attention操作后的向量为h,那么可得

    \[h(我)=softmax(\frac{E(我)(E(<s>),E(<我>),E(<很>),E(<帅>),E(</s>))}{\sqrt{d_k}})\begin{pmatrix}E(<s>)\\E(<我>)\\E(<很>)\\E(<帅>)\\E(</s>) \end{pmatrix}\\=softmax(\frac{E(我)E(<s>)}{\sqrt{d_k}})E(<s>)+softmax(\frac{E(我)E(<我>)}{\sqrt{d_k}})E(<我>)\\+softmax(\frac{E(我)E(<很>)}{\sqrt{d_k}})E(<很>)+softmax(\frac{E(我)E(<帅>)}{\sqrt{d_k}})E(<帅>)\\+softmax(\frac{E(我)E(</s>)}{\sqrt{d_k}})E(</s>) \]

    对“我”这个词整合成隐层信息的时候,是将“我”这个词与其所在句子中的每一个词进行比较,考查其在句子中的表达更注重哪些位置的信息,从而分配不同的权重给对应位置的词进行加权求和,这也符合Attention的基本思想。由于所有的词汇的Attention操作都是在其句子自身进行的,因此这种Attention机制被称为self-Attention(自注意力机制)

Positional Encoding

RNN处理单词/向量的表示时是按照单词的顺序进行的,而自注意力机制会将所有相关单词的理解融入到我们正在处理的单词中,因此我们需要找出一种方法表示单词的顺序

为了解决这个问题,Transformer为每个输入的word embedding添加了一个向量。这些向量遵循模型学习到的特定模式,这有助于确定每个单词的位置,或序列中不同单词之间的距离

这里的初衷是,将位置向量添加到word embedding中使得它们在接下来的运算中,能够更好地表达的词与词之间的距离

  • 位置编码公式:

    \[PE_{(pos,2i)}=sin(\frac{pos}{10000^{\frac{2i}{d_{model}}}})偶数位置使用sin \]

    \[PE_{(pos,2i+1)}=cos(\frac{pos}{10000^{\frac{2i}{d_{model}}}})奇数位置使用cos \]

    graph pos-->1((word))--embedding--> 2["词向量<br/>[0.1,0.1,0.2,...,0.02]<br/>位置<br/>[0,1,2,3,...,511]"] 3[位置编码<br/>sin,cos,sin,cos,...,cos] 4[+] 5[512维<br/>INPUT] 2-->3 2-->3 2-->3 2-->3 2-->3 2-->3 2--521维-->4 3--521维-->4 4-->5

img

  • 为什么位置嵌入会有用?

    • 根据位置编码的公式我们可以得到一个特定位置的\(d_{model}\)维的位置向量,借助三角函数的性质

      \[\begin{cases} sin(\alpha+\beta)=sin\alpha com\beta +cos\alpha sin\beta\\cos(\alpha+\beta)=cos\alpha cos\beta -sin\alpha sin\beta\end{cases} \]

      可得

      \[\begin{cases} PE(pos+k,2i)=PE(pos,2i)\times PE(k,2i+1)+PE(pos,2i+1)\times PE(k,2i)\\PE(pos+K,2i+1)=PE(pos,2i+1)\times PE(k,2i+1)-PE(pos,2i)\times PE(k,2i) \end{cases} \]

      可以看出,对于\(pos+k\)位置的位置向量某一维\(2i\)\(2i+1\)而言,可以表示为,\(pos\)位置和\(k\)位置的位置向量的\(2i\)\(2i+1\)维的线性组合,这样的线性组合意味着位置向量中蕴含了相对位置信息

      但是这种相对位置信息会在注意力机制处失效

      img

下图中,每一行对应一个词向量的位置编码,所以第一行对应着输入序列的第一个词。每行包含512个值,每个值介于1和-1之间

img

上图可以看到它从中间分裂成两半。这是因为左半部分的值由一个函数(使用正弦)生成,而右半部分由另一个函数(使用余弦)生成。然后将它们拼在一起而得到每一个位置编码向量

the code for generating positional encodings can be seenin get_timing_signal_1d()

Multi-head Attention

img

在引入了位置向量\(PE(pos)\)以后,我们将位置向量与词向量进行求和,输入到self-Attention中进行抽象信息的整合。多数情况下,单一的self-Attention难以捕获序列信息的多样性。因此考虑多个相同操作的self-Attention平行的提取每个词语的信息,然后再将多个Attention的结果拼接起来,用于后续层次的操作。这点类似于卷积神经网络中多个卷积核同时作用一个矩阵对象的想法,都是试图取实现信息不同角度的多样化采集

多头注意力机制扩展了模型专注于不同位置的能力,并给出了注意力层的多个“表示子空间”(representation subspaces)。接下来我们将看到,对于“多头”注意机制,我们有多个Q/K/V权重矩阵集(Transformer使用八个注意力头,因此对于每个编码器/解码器有八个矩阵集合)。这些集合中的每一个都是随机初始化的,在训练之后,每个集合都被用来将输入word embedding(或来自较低编码器/解码器的向量)投影到不同的表示子空间中。

如果做与上述相同的自注意力计算,只需八次不同的权重矩阵运算,我们就会得到八个不同的Z矩阵

img

但是feed-forward layer不需要8个矩阵,只需要一个矩阵(由每个单词的表示向量组成),因此我们需要找到一种方法来将这8个矩阵压缩成一个:

直接把这些矩阵拼接在一起,然后用一个附加的权重矩阵WO与它们相乘

img

我们把整个过程放在一起可以看到

img

既然我们已经摸到了注意力机制的这么多“头”,那么让我们重温之前的例子,看看我们在例句中编码“it”一词时,不同的注意力“头”集中在哪里

img

当我们编码“it”一词时,一个注意力头集中在“animal”上,而另一个则集中在“tired”上,从某种意义上说,模型对“it”一词的表示在某种程度上代表着“animal”和“tired”

如果我们将所有的attention都加到图示里,就比较难以理解这种机制了

img

残差和layer-Norm——对应Add&Norm模块

在每个编码器中的每个子层(自注意力、前馈网络)的周围都有一个residual connection,并且都跟随着一个“layer-normalization”步骤

残差边(short-cut)的出现是为了防止网络层数的加深导致网络内的参数退化。其想法是将低层的特征跃过一些网络层直接送进高层网络,这就保证了网络的最差情况也能学到那个低层特征本身。原因是过去的网络是计算\(l\)层输入\(x(l)\)的线性与非线性的变换\(z^(l+1)=\sigma(wx^{l}+b)\),而残差边的思想是将低层的(比如\(l-1\)\(x^{l-1}\))特征直接送入\(l\)层参与运算\(z^{l+1}=\sigma(wx^l+b+x^{l-1})\),这样一来,即使参数w,b都退化为0,也还能留下低层特征的信息送到高层,不至于让信息在深层网络传递的过程中丢失过多

注:前向神经网络的第\(l\)隐藏层等价于RNNs时刻\(l\)对应的隐藏层

graph 1[x]-->2[weight] 2--relu-->3[weight]--"f(x)"-->4 1--"x<br/>identity"-->4 4((+))--relu-->5["f(x)+x"]

img

Layer-Normalization(LN)则是起到与Batch-Normalization(BN)类似的作用,BN是指在批次这个维度来标准化数据,使得其落在梯度适中的区域。LN则是在某一层输出时对所有的神经元的值做标准化,好处是不用依赖Batch大小的设置,也能起到一定的加速收敛的作用

网络层的输出经过线性变换作为下层网络的输入,网络输出直接影响下层网络输入分布,这是一种协变量转移的现象。我们可以通过固定网络层的输入分布(固定输入的均值和方差)来降低协变量转移的影响。

Batch Normalization使用mini-batch的均值和标准差对深度神经网络的隐藏层输入进行标准化,对同一mini-batch中对不同特征进行标准化(纵向规范化:每一个特征都有自己的分布),受限于batch size,难以处理动态神经网络中的变长序列的mini-bach。

RNNs不同时间步共享权重参数,使得RNNs可以处理不同长度的序列,RNNs使用 Layer Normalization 对不同时间步进行标准化(横向标准化:每一个时间步都有自己的分布),从而可以处理单一样本、变长序列,而且 训练和测试处理方式一致,对于使用LN的RNNs,每个时刻加权后的输入通过标准化被重新调整在合适的范围,很大程度避免了梯度消失、梯度爆炸问题,隐藏状态的传递更加稳定

img

img

解码器的子层也具有同样的结构,假设有个2层编码-解码结构的transformer:

img

前馈神经网络模块——Feed Forward

前馈神经网络模块(即结构中的Feed Forward)由两个线性变换组成,中间有一个ReLU激活函数,对应到公式的形式为

\[FFN(x)=max(0,xW_1+b_1)W_2+b_2 \]

论文中前馈神经网络模块输入和输出的维度均为\(d_{model}=512\),其内层的维度\(d_{ff}=2048\)

Decoding

编码器从处理编码层的输入序列开始,第一个编码器的输出之后会变转化为一个包含向量K(键向量)和V(值向量)的注意力向量集 。这些向量将被每个解码器用于自身的“encoder-decoder attention” layer,而这些层可以帮助解码器关注输入序列的合适位置

img

解码阶段的每个步骤都会输出一个输出序列的元素,直到到达一个特殊的终止符号,它表示transformer的解码器已经完成了它的输出。每个步骤的输出在下一个time step被提供给底端解码器,并且就像编码器之前做的那样,这些解码器会输出它们的解码结果 。

另外,就像我们对编码器的输入所做的那样,我们会嵌入并添加位置编码给那些解码器,来表示每个单词的位置

img

self attention layers in decoders

那些解码器中的自注意力层表现的模式与编码器不同:

  • 在解码器中,自注意力层只被允许处理输出序列中更靠前的那些位置。在计算自注意值的softmax步骤前,它会把后面的位置给隐去(把它们设为-inf)。

这个“encoder-decoder attention” layer的工作方式基本就像Multi-head Attention一样,只不过它是通过在它前面的层来创造Q矩阵,并且从编码器的输出中取得K/V矩阵

Masking

在论文原文中介绍的相对简洁,作者认为序列在解码的过程中,对于\(i\)时刻的解码,只能依赖到\(i\)之前时刻的词语信息,以保证解码过程中的有序性(在预测时,是“看不到未来的序列的”,所以要将当前预测的单词(token)及其之后的单词(token)全部mask掉)。因此要对\(i\)时刻后的信息进行masking操作。

关于masking的具体做法,主要是设置一个与序列长度相同维度的mask向量,将其第\(i\)个分量以后的分量全部置为0,第\(i\)个分量以前的分量全部置为1。用于标记解码序列哪些在网络中的计算是有效的

final linear and softmax layer

解码器最后会输出一个实数向量,那么我们又如何将浮点数转换为最终需要的单词呢?这便是linear layer的工作。

线性变换层是一个简单的全连接神经网络,它可以把解码器产生的向量投射到一个比它大得多的、被称作对数几率(\(logits\)的向量里

不妨假设我们的模型从训练集中学习一万个不同的英语单词(模型已经处理好的“输出词表”)。因此\(logits\)向量为一万个单元格长度的向量——每个单元格对应某一个单词的分数。

接下来的Softmax 层便会把那些分数变成概率(都为正数、上限1.0)。概率最高的索引被选中,并且它咋单词表中对应的单词被作为这个time step的输出。

img

Training

在训练过程中,一个未经训练的模型会通过一个完全一样的前向传播。但因为我们用有标记的训练集来训练它,所以我们可以用它的输出去与真实的输出做比较

为了把这个流程可视化,不妨假设我们的输出词汇仅仅包含六个单词:a, am, i, thanks, student以及 <eos>(end of sentence的缩写形式)

img

模型的输出词表在我们训练之前的预处理流程中就被设定好了

一旦我们定义了我们的输出词表,我们可以使用一个相同宽度的向量来表示我们词汇表中的每一个单词。这也被认为是一个one-hot 编码。所以,我们可以用下面这个向量来表示单词am

img

one-hot encoding

  • 作用:分类特征的每个元素转化为一个可以用来计算的值

  • 直观来说就是有多少个状态就有多少比特,而且只有一个比特为1,其他全为0的一种码制

  • 例子

    • 假如有三种颜色特征:红、黄、蓝。 在利用机器学习的算法时一般需要进行向量化或者数字化。那么你可能想令 红=1,黄=2,蓝=3. 那么这样其实实现了标签编码,即给不同类别以标签。然而这意味着机器可能会学习到“红<黄<蓝”,但这并不是我们的让机器学习的本意,只是想让机器区分它们,并无大小比较之意。所以这时标签编码是不够的,需要进一步转换。因为有三种颜色状态,所以就有3个比特。即红色:1 0 0 ,黄色: 0 1 0,蓝色:0 0 1 。如此一来每两个向量之间的距离都是根号2,在向量空间距离都相等,所以这样不会出现偏序性,基本不会影响基于向量空间度量算法的效果。

      自然状态码为:

    ​ 000,001,010,011,100,101

     独热编码为:
    

    ​ 000001,000010,000100,001000,010000,100000

  • 为什么需要one-hot encode?

    • 独热编码(哑变量 dummy variable)是因为大部分算法是基于向量空间中的度量来进行计算的,为了使非偏序关系的变量取值不具有偏序性,并且到圆点是等距的。使用one-hot编码,将离散特征的取值扩展到了欧式空间,离散特征的某个取值就对应欧式空间的某个点。将离散型特征使用one-hot编码,会让特征之间的距离计算更加合理。离散特征进行one-hot编码后,编码后的特征,其实每一维度的特征都可以看做是连续的特征。就可以跟对连续型特征的归一化方法一样,对每一维特征进行归一化。比如归一化到[-1,1]或归一化到均值为0,方差为1
  • 为什么特征向量要映射到欧式空间?

    • 将离散特征通过one-hot编码映射到欧式空间,是因为,在回归,分类,聚类等机器学习算法中,特征之间距离的计算或相似度的计算是非常重要的,而我们常用的距离或相似度的计算都是在欧式空间的相似度计算,计算余弦相似性,基于的就是欧式空间
  • 优缺点

    • 优点:独热编码解决了分类器不好处理属性数据的问题,在一定程度上也起到了扩充特征的作用。它的值只有0和1,不同的类型存储在垂直的空间
    • 缺点:当类别的数量很多时,特征空间会变得非常大。在这种情况下,一般可以用PCA来减少维度。而且one hot encoding+PCA这种组合在实际中也非常有用
  • 什么时候(不)使用one-hot encode?

    • 用:独热编码用来解决类别型数据的离散值问题,
    • 不用:将离散型特征进行one-hot编码的作用,是为了让距离计算更合理,但如果特征是离散的,并且不用one-hot编码就可以很合理的计算出距离,那么就没必要进行one-hot编码。 有些基于树的算法在处理变量时,并不是基于向量空间度量,数值只是个类别符号,即没有偏序关系,所以不用进行独热编码。 Tree Model不太需要one-hot编码: 对于决策树来说,one-hot的本质是增加树的深度
  • 什么情况下(不)需要归一化?

    • 需要: 基于参数的模型或基于距离的模型,都是要进行特征的归一化
    • 不需要:基于树的方法是不需要进行特征的归一化,例如随机森林,bagging 和 boosting等

loss function

假设要将merci翻译成thanks,这意味着我们想要一个表示thanks概率分布的输出,但是因为模型还没训练好,所以不太可能出现我们的预期结果

img

因为模型的参数(权重)都是随机生成的,(未经训练的)模型产生每个单元格/单词的概率分布都被赋予了随机数值。

因此我们可以用真实的输出来比较它,然后用反向传播算法来略微调整所有模型的权重,生成更接近结果的输出。

那么如何比较两个概率分布呢?我们可以简单地用其中一个减去另一个。

但注意到这是一个过于简化的例子。更现实的情况是处理一个句子。例如,输入“je suis étudiant”并期望输出是“i am a student”。那我们就希望我们的模型能够成功地在这些情况下输出概率分布:

  • 每个概率分布被一个以词表大小(我们的例子里是6,但现实情况通常是3000或10000)为宽度的向量所代表。
  • 第一个概率分布在与“i”关联的单元格有最高的概率
  • 第二个概率分布在与“am”关联的单元格有最高的概率
  • 以此类推,第五个输出的分布表示<end of sentence>关联的单元格有最高的概率

img

依据例子训练模型得到的目标概率分布。

在一个足够大的数据集上充分训练后,我们希望模型输出的概率分布看起来像这个样子:

img

我们期望训练过后,模型会输出正确的翻译。当然如果这段话完全来自训练集,它并不是一个很好的评估指标(参考:交叉验证——将原始数据(dataset)进行分组,一部分做为训练集来训练模型,另一部分做为测试集来评价模型)。

注意到每个位置(词)都得到了一点概率,即使它不太可能成为那个time step的输出——这是softmax的一个很有用的性质,它可以帮助模型训练

因为这个模型一次只产生一个输出,不妨假设这个模型只选择概率最高的单词,并把剩下的词抛弃。这是其中一种方法(叫贪心解码)。

另一个完成这个任务的方法是留住概率最靠高的两个单词(例如I和a),那么在下一步里,跑模型两次:其中一次假设第一个位置输出是单词“I”,而另一次假设第一个位置输出是单词“me”,并且无论哪个版本产生更少的误差,都保留概率最高的两个翻译结果。然后我们为第二和第三个位置重复这一步骤。这个方法被称作集束搜索(beam search)。

在我们的例子中,集束宽度是2(因为保留了2个集束的结果,如第一和第二个位置),并且最终也返回两个集束的结果(top_beams也是2)。这些都是可以提前设定的参数


交叉熵&KL散度

希望一种分布(可以是概率向量)与另一种分布接近, 而交叉熵和KL散度为我们提供了一种自然的方法测量两个分布之间的差距,这个差距就可以被当作损失函数

最佳编码及编码成本\代价

假设有个人特别喜欢动物,会在线上和其他人讨论,假设他只说四个词dog,cat,fish,bird,为了降低与朋友间的通信成本,他将四个单词编制成二进制

注:每个字母需要用一个字节即8 bit来表示

img

每一个二进制代码称为码字(codeword),这样用两位二进制表示的码字字长为2比特

假设此人对这些动物的喜好程度不同,使用的频率不同,那么当他和别人通信时,使用频率越高的词语出现的概率越大

img

那么我们是否能够利用这个特质来进一步压缩平均每个码字所需要的比特呢?

比如我们采用以下编码方案:

img

平均字长可以用概率*字长的加权和来计算,则原方案的平均码字长为\(\frac{1}{4}\times 2+\frac{1}{4}\times 2+\frac{1}{4}\times 2+\frac{1}{4}\times 2=2\),而新方案的平均码字长为\(\frac{1}{2}\times 1+\frac{1}{4}\times 2+\frac{1}{8}\times 3+\frac{1}{8}\times 3=1.75\),可见新方案明显缩短了平均码字长

在制定新方案是需要特别注意的要保证前缀编码,即任何码字都不是另一个码字的前缀

考虑前缀属性的一种有用方法是,每个代码字都需要牺牲可能的码字空间。 如果我们使用码字01,则会失去使用其前缀的任何其它码字的能力。 比如我们无法再使用010或011010110,因为会有二义性。那么,当我们确定下来一个码字,我们会丢失多少码字空间呢?

以下图为例,01是我们选定的一个码字,以它为前缀的码字空间占整个码字空间的\(\frac{1}{4}\),这就是我们损失的空间,是我们选择2比特(01)作为码字的成本。其它码字只能用3比特或更多比特的编码了(如果需要编码的词多于4)。总的来说,如果我们选择长度为的\(L\)码字,那么我们需要付出的代价就是\(\frac{1}{2^L}\)

img

假设我们愿意为长度为\(L\)的代码付出的代价是\(cost\),那么,\(cost=\frac{1}{2^L}\)。换算一下,长度\(L\)与付出的代价的关系是\(L=log_2(\frac{1}{cost})\)

那么应该如何为不同长度的代码分配成本预算从而获得最短平均编码呢?

根据事件的普遍程度来分配我们的预算——如果一个事件发生了50%的时间,我们应该花费50%的预算为它买一个简短的代码。但是,如果一个事件只发生1%的时间,我们只花1%的预算,因为我们不太在乎代码是否长

因为一个代码\(x\)分配的成本预算\(cost\)与该代码出现的概率\(p(x)\)成正比,让\(cost\)等于这个概率\(p(x)\)。这样,只要知道每个码字出现的概率而无需知道具体的编码方案,我们就可以计算编码的平均长度是

\[L(x)=log_2\frac{1}{p(x)} \]

可以证明,这样的编码方法不仅是自然的,而且是最佳的

这样,当每个码字\(x\)出现的概率是\(p(x)\),那么最短的平均码字的长度是

\[\begin{align}H(p)&=\sum_x p(x)\times L(x)\\&=\sum_x p(x)\times log_2(\frac{1}{p(x)})\\&=-\sum_x p(x)\times log_2 p(x)\end{align} \]

\(H(p)\)就是熵!

我们可以直观地得到编码方案的熵,它就是下图中各矩形的面积总和。可以看到,如果不用最佳编码,那么它的熵就比较大

img

img

交叉熵

假设有另外一个人,很喜欢猫,他使用cat的频率要更高

img

当上述爱猫和爱狗人士交流采用的是爱狗人士制定的编码时,爱猫人士所要发送的信息就比另外一方要长

爱狗人士对应的平均码字长度为1.75位,而爱猫人士在这套编码方案下的平均码字长度为2.25位

我们将爱猫人士的平均码字长度称为他在这套方案下的交叉熵,即把来自一个分布\(q\)的消息使用另一个分布\(p\)的最佳代码传达的平均码字长度。形式上可以定义为

\[H_p(q)=\sum_x q(x)log_2(\frac{1}{p(x)})=-\sum_x q(x)log_2 p(x) \]

img

注意:交叉熵不对称

那么,为什么要关心交叉熵呢? 这是因为,交叉熵为我们提供了一种表达两种概率分布的差异的方法。\(p,q\)的分布越不相同,\(p\)相对于\(q\)的交叉熵将越大于\(p\)的熵。

KL散度

真正有趣的是熵和交叉熵之间的差。 这个差可以告诉我们,由于我们使用了针对另一个分布而优化的代码使得我们的消息变长了多少。 如果两个分布相同,则该差异将为零。 差增加,则消息的长度也增加

我们称这种差异为Kullback-Leibler散度,或简称为KL散度

\(p\)相对\(q\)的KL散度可定义为:

\[D_q(p)=H_q(p)-H(p) \]

KL散度的真正妙处在于它就像两个分布之间的距离,即KL散度可以衡量它们有多不同!

用交叉熵作为损失函数

交叉熵常用来作为分类器的损失函数。不过,其它类型的模型也可能用它做损失函数,比如生成式模型。

假设有数据集\(D=(x_1,y_1),(x_2,y_2),...,(x_N,y_N)\),其中,\(x={x_1}^N_{i=1}\in X\)是特征值或输入变量;\(y={y_i}^N_{i=1}\in Y\)是观察值,也是我们期待的模型的输出,最简单的情况是它只有两个离散值的取值,比如\(y_i\in \{"yes","no"\}\),或者\(y_i\in \{"positive","negative"\}\),或者\(y_i\in\{-1,+1\}\)。能根据新的\(x\)\(y\)做出预测的模型就是我们常用的二元分类器(Binary Classifier)

此处刻意没有选用\(y_i\in\{0,1\}\)作为例子,是为了避免有人认为观察到的数据\(y\)是概率向量,因而可以直接套用\(y\)和模型的输出\(\hat{y}\)之间的交叉熵作为损失函数。实际上,我们可以使用交叉熵作为分类器的损失函数的根本原因是我们使用了最大似然法,即我们通过在数据集上施用最大似然法则从而得到了与交叉熵一致的目标函数(或者损失函数)。我们观察原始数据时是看不到概率的,即使\(y_i\in\{0,1\}\),它的取值0或1只是客观上的观察值而已,其概率意义是我们后来人为地加给它的。

似然函数的定义

假设离散随机变量\(Y\)具有概率质量函数(PMF)\(p\),即\(Y\)的某个具体取值\(y_i\)的概率是\(P(Y=y_i)=p(y_i)\)

注:概率质量函数和概率密度函数不同之处在于——概率密度函数是对连续随机变量定义的,本身不是概率,只有对连续随机变量的取值进行积分后才是概率

如果\(p\)由参数向量\(\theta\)决定,在我们观测到\(Y\)的一些具体取值的集合\(y\in Y\)后,在这些观测值上的似然函数为$\mathcal{L}(\theta|y)=p_{\theta}(y)=P_{\theta}(Y=y) $

\(\mathcal{L}(\theta|y)\)\(y\)的似然函数,当\(y\)固定时,它是\(\theta\)的函数,即\(\theta\)为自变量,不同的\(\theta\)对应的\(p_\theta\)也不同,从而\(p_\theta(y)\)\(y\)不变时也不同

这里要注意似然和概率的区别——求概率时需要在固定分布参数\(\theta\)时计算\(p_\theta(y)\),这与求似然值完全不同

另外,从似然函数的定义也可以看出,\(\mathcal{L}(\theta|y)\)\(p(\theta|y)\)不同

最大似然法就是通过最大化\(\mathcal{L}(\theta|y)\)获得\(\theta\),或者说寻找使一组固定的观察值\(y\)最可能出现同时使\(p_\theta(Y=y)\)最大的参数\(\theta\)

不过从模型训练的角度来看,找到\(\theta\)并不是最终目的,这个寻找过程(模型训练过程)才是最重要的。当模型以找到\(\theta\)为目标,不断去拟合数据点,使得输出距离观察值越来越接近并最终达到预期时,模型就算训练好了。然后,这个模型针对任何一个新的输入值,就会产生符合训练数据集的分布的一个输出,此输出值即模型产生的预测值。

如果\(Y\)是连续随机变量,那么它的一组取值的集合(观测值)\(y\)的似然函数为\(\mathcal{L}(\theta|y)=f_\theta(y)=p(Y=y|\theta)\)

此处\(f_\theta\)\(Y\)的概率密度函数(PDF),其它含义与随机变量是离散时的相同

二元分类器的损失函数

先以二元分类器为例说明交叉熵是如何成为损失函数的

对于二元分类(binary classification),观察值\(y\)的取值是二选一。无论实际观察值是什么,我们都用\(\{0,1\}\)代替\(y\)。显然,\(y\)符合Bernoulli分布,而Bernoulli分布只有一个参数,即\(P_\theta(y=1)=\theta\\P_{\theta}(y=0)=1-\theta\)合并一下\(P_\theta(y)=\theta^y(1-\theta)^{1-y}\)

由于数据集时\(D=(x_1,y_1),(x_2,y_2),...,(x_N,y_N)\)假设这些观察到的数据点都是\(i.i.d.\)的,那么它们被观察到的对数似然等于\(l(\theta)=log\coprod_{i=1}^{N}p_\theta(y_i)=log\coprod_{i=1}^{N}\theta^{y_i}(1-\theta)^{1-y_i}\\=\sum^N_{i=1}[y_i log\theta+(1-y_i)log(1-\theta)]\)

似然函数\(l(\theta)\)就是目标函数,若加上负号,则\(-l(\theta)\)转变为损失函数,同时我们可以发现此处的损失函数就是\(y_i\)\(\theta\)的交叉熵\(H_y(\theta)\)

上述交叉熵公式也称为binary cross-entropy即二元交叉熵,从\(l(\theta)\)的公式可以看到,它是所有数据点的交叉熵之和,也就是说每个数据点的交叉熵时可以独立计算的

只有当观察值\(\{y_i\}^N_{i=1}\)的取值为1或0时,我们才能获得交叉熵形式的损失函数。当然我们可以为\(y_I\in\{0,1\}\)赋予概率意义,这样就可以理解交叉熵损失的含义

例如,我们对数据\(y\)进行one-hot编码,假设两个观察到的数据点是\(y_1="positive"\\y_2="negative"\),把它们转换成one-hot编码\(y_1=[1,0]\\y_2=[0,1]\)

我们可以把这种编码理解成为概率分布向量\([P("positive"),P("negative")]\),且满足\(P("positive")+P("negative")=1\),而被数据集训练的模型试图估计出整个数据集之上的\(P("positive")\)\(P("negative")\),它对应每个输入数据点的输出是概率分布\([\theta,(1-\theta)]\),比如\([0.7,0.3]\),这个估计值与数据之间的差距就用它与数据之间的交叉熵来衡量

我们来计算它分别与\(y_1\)\(y_2\)之差\(l(y_1,\theta)=H_{y_1}(\theta)=-(1\times log(0.7)+0\times log(1-0.7))=0.36\\l(y_2,\theta)=H_{y_2}(\theta)=-(0\times log(0.7)+1\times log(1-0.7))=1.20\)

显然模型的输出\([0.7,0.3]\)更接近\(y_1=[1,0]\),所以模型对这个输入的数据点的分类结果就是\("positive"\)

容易推导出\(\frac{dl(\theta)}{d\theta}=0\)时,\(\theta=\frac{1}{N}\sum^N_{i-1}y_i\),也就是说,使已有的观察值具有最大似然时的\(\theta\)等于这些观察值的均值

: 如果观察值符合高斯分布,那么由最大似然法则可以推出最小二乘或均方差作为损失函数,经常用在回归类的任务中。有趣的是,使最小二乘最小时的预测值也是当前所有观察值的均值,这与交叉熵是一样的,虽然它们的表达式是那么的不同。

有很多算法都是以数据的最大似然法则得到用交叉熵作为损失函数为出发点的,比如在Gradient Boost分类算法里,我们通过不断建新的决策树来拟合上一棵树的预测值与观测到的数据之间的残差(residuals)。 一开始看到这样的优化方法,我感觉它的思路很妙,因为它不是直接拟合数据而是拟合残差。但仔细研究它背后的理论推导,我们就能发现,它的优化残差的策略仍然来自数据的极大似然准则和交叉熵。下面简要解释一下

从优化数据的似然函数出发,我们得到交叉熵形式的损失函数(对单个样本):\(Loss=-[y_ilogp+(1-y_i)log(1-p)]\)\(y_i\)是第\(i\)个样本的观察值(比如观察到的是"yes"或"no",分别被1或0取代,以赋予概率意义)。由于Gradient Boost模型的输出不是直接用预测的概率\(p\)而是胜率(odds)的对数\(log(odds)\),我们利用\(p\)\(log(odds)\)之间的关系\(p=\frac{e^{log(odds)}}{1+e^{log(odds)}}\),也可以把损失函数改写成\(Loss=-y_i log(odds)+log(1+e^{log(odds)})\)。我们要找到新的\(log(odds)\)输出,使得\(Loss\)最小。但与一般的算法不同,Gradient Boost并不是一步到位就使\(Loss\)最小,而是逐步使它越来越小。具体来说,我们仍然像求最小值那样,计算损失函数对预测值的导数(\(r\)的下标\(m\)是指第\(m\)棵树,\(i\)是指第\(i\)个样本或新据点):$$\begin{align}r_{i,m}=&\frac{\partial Loss}{\partial log(odds)}\=&\frac{\partial}{\partial log(odds)}(-y_i log(odds)+log(1+e{log(odds)}))\=&y_i-\frac{e{log(odds)}}{1+e^{log(odds)}}\=&y_i-p\end{align}$$

Gradient Boost的策略不是像我们一般优化一个函数那样,直接使\(r_{i,mm}=0\),从而解出\(p\),而是让这里的\(p\)等于上一次的预测概率,则\(r_{i,m}\)相当于在\(Loss\) 在上一次预测值处的梯度。接着,继续建新的决策树来拟合\(r_{i,m}\),使\(r_{i,m}\)逐步逼近0,从而在最小化损失函数的同时避免模型过拟合。

而从上式我们看到,\(r_{i,m}\)是我们观察到的概率(1或0)减去上一次预测的概率,即两者之间的残差。所以,Gradient Boost的优化残差的策略是使用最大似然准则和交叉熵损失函数的自然结果。

另一个以交叉熵为损失函数的例子是在MNIST数据集上的变分自编码器VAE。如果手写体数字图像的每个像素取值只为0或1,那么每个像素的分布就是Bernoulli分布,整个模型的输出就符合多变量Bernoulli分布。因此,解码器的损失函数可以使用输入图像\(x\)(此时也我们期待的输出,相当于标签)与解码器的实际输出\(y\)之间的交叉熵:

\[\begin{align}f(z)&=-log p(x|z)\\&=-\sum^D_{i=1}[x_i logy_i+(1-x_i)log(1-y_i)]\end{align} \]

此处\(y_I\)是输出像素\(i=1\)的概率,是输出数据(即模型的预测)的分布参数,相当于上面的\(\theta\)\(D\) 是一个数据点的维度。MNIST的\(D\)等于\(28\times 28=794\)

有时,我们不一定非得把\(\theta\)定义成数据的分布参数。比如在logistic regression里,我们把输入向量\(x\)的权重(即系数)向量当作参数\(\theta\),模型的输出为\(\hat{y}=g(\theta^Tx)\),这里\(g\)是logistic函数:\(g(z)=\frac{1}{1+e^{-z}}\)

\(p(y=1|x;\theta)=\hat{y}\),则\(p(y=0|x;\theta)=1-\hat{y}\),合并两式得\(p(y|x;\theta)=\hat{y}^y(1-\hat y)^{1-y}\)\(y\)的对数似然(即目标函数,损失函数的负数)为\(l(\theta)=l(y,\hat{y})=\sum^N_{i=1}[y_ilog\hat{y}+(1-y_i)log(1-\hat y)]\),然后求\(l(\theta)\)\(\theta\)的梯度\(\bigtriangledown _\theta\)

由于\(\theta\)是一个含有多个参数的向量,我们对每个参数\(\theta_j\)逐一求偏导。下面是模型从输入到输出的计算步骤,用导数的链式法则自己推导一下只有一个数据点\((x,y)\)时的梯度:\(x\longrightarrow z=\theta^Tx\longrightarrow \hat y=\frac{1}{1+e^{-z}}\longrightarrow l(y,\hat y)\),结果为\(\bigtriangledown _j=\frac{\partial}{\partial \theta_j}l(\theta)=(\hat y-y)x_j\),这样就可以用梯度上升法不断更新\(\theta_j\),最终获得有足够精度的\(\theta_j\)的近似值\(\theta_j:=\theta_j+\alpha \bigtriangledown _j\),此处\(\alpha\)是学习率

注:如果每次更新参数时使用全体数据或者一个batch的数据,那\(\bigtriangledown _j\)也可以是这些数据产生的梯度的均值

多元分类器的损失函数

如果我们观察到的数据有\(K(K>2)\)个分类呢?设\(y\in\{1,2,...,K\}\),即相当于给每一类一个\(1\sim K\)的编号。我们观察到的\(y\)的分布是 multinomial distribution (多元分布),每个类对应的分布参数是:\(\phi _1,\phi _2,...,\phi _k\),满足\(P(y=i)=\phi _i\)\(\sum^K_{i-1}\phi _i=1\)

img

如同在Bernoulli分布那样,为了用一个公式表达任意观察值\(y\)\(p_{\phi}(y)\),我们先定义Indicator函数\(I\)(有人翻译成”指示函数“):

\[I\{True\}=1,I\{False\}=0,例如I\{1+1=2\}=1 \]

\(p(y_i)=(\phi_1)^{I(y_i=1)}(\phi_2)^{I(y_i=2)}...(\phi_K)^{I(y_i=K)}={\textstyle \prod_{k=1}^{K}}(\phi_k)^{I(y_i=k)}\),则\(y_i\)的对数似然等于\(l(\theta)=log\ p(y_i)=\sum^K_{k=1}I(y_i=k)log \phi_k\),其中,\(I(y_i=1)\)\(I(y_i=K)\)中只有一项等于1,其他所有项都等于0。如果将这些\(I\)项放到一个向量里\([I(y_i=1),I(y_i=2),...,I(y_i=K)]=[0,...,1,...,0,0]\),那么这个\(K\)维向量其实就是观察值\(y_i\)的one-hot编码。我们在讲二元交叉熵时提到给one-hot编码赋予概率意义,所以这个向量就是观察到的数据的概率向量。

如果我们把\(y_i\)的one-hot编码里的\(I(y_i=k)\)记作\(y_{i,k}\),那么\(\phi_k\)也应加一个下标\(i\),变成\(\phi_{i,k}\),表明它是\(y_i=k\)的概率。这样,\(y_i\)的对数似然就可以写成:\(l(\phi)=\sum^K_{k=1}y_{i,k}log\phi_{i,k}\)

那么所有\(N\)\(i.i.d.\)样本的平均对数似然值就是\(l(\phi)=\frac{1}{N}\sum^N_{i=1}\sum^K_{k=1}y_{i,k}log\phi_{i,k}\)

在整个式子前面加上负号后,目标函数 \(l(\phi)\)变身为损失函数,它与交叉熵的形式完全一样。在只看单个数据点时,这个多元交叉熵也叫categorical cross-entropy\(y\)是观察到的概率向量,而\(\phi\)就是模型输出的概率向量。比如\(\begin{align} y_1&=(0,0,0,1,0,0\\\phi_1&=(0.15,0.2,0.16,0.3,0.12,0.07)^T \\\end{align}\)它们之间的交叉熵大约是1.2

小结一下。如果我们要找的数据分布符合Bernoulli Distribution或者Multinomial Distribution,那么我们通过最大似然准则可以推导出以交叉熵作为损失函数。不过在实践中,每当我们的数据标签\(y\)是一个概率向量(一般是one-hot编码过的),模型的输出\(\hat{y}\)是同维度的另一个概率向量(一般把模型的输出向量经过Softmax归一化成概率向量),那么我们经常就直接使用两者的交叉熵或者KL散度作为损失函数,而不再理会数据的分布是什么了。这就如同最小二乘准则理论上只在高斯分布下与最大似然准则是等价的,但我们还是经常在非高斯分布下使用它作为最优化的准则,因为在非高斯分布下,最大似然法一般无法得到简洁的解析表达式。当然,这样做在性能上可能比最大似然差一些。

scikit-learn里的log_loss()

scikit-learn里的log_loss()可以用来计算交叉熵,它的计算公式与多元交叉熵完全一样:\(L_{log}(Y,P)=-logPr(Y|P)=-\frac{1}{N}\sum^{N-1}_{i=0}\sum^{K-1}_{k=0}y_{i,k}log\ p_{i,k}\)

但实际用起来却有些令人迷惑。这里举两个文档中没有的例子说明一下,首先看看它的语法

img

  • 例一、用log_loss计算二元交叉熵

    from sklearn.metrics import log_loss
    import numpy as np
    
    true_label = np.array([0,1,0])   
    predicted_label = np.array([0.1,0.9,0])  
    log_loss(true_label, predicted_label)
    
    0.07024034377188453
    

    这里,log_loss其实做的是binary cross-entropy。为什么这么说呢?我们一步一步地解释。

    因为参数labels=None,而predicted_label的shape是(3, ),所以log_loss认为样本数是3,即\(N=3\);并且此时只有2个分类,即\(K=2\)。具体是哪2类要看true_label里面的值,这里只有0和1,所以2个分类就是类0和类1。根据这些推想得到的配置参数,log_loss开始构建\(y\)矩阵和\(p\)矩阵,以便利用上面的公式进行计算。

    rue_label = [0, 1, 0],看上去是一个一维的向量,log_loss认为它代表3个数据点的实际分类(即标签)。log_loss先把这个向量里的每一个元素(即它们代表的分类)按照\(K=2\)编码成one-hot,接着按\(N=3\)把这个向量写成\(N\times K\)矩阵:\(y=\begin{bmatrix} 1 & 0\\ 0 & 1\\ 1&0 \end{bmatrix}_{3\times 2}=\begin{bmatrix} 1-0 & 0\\ 1-1 & 1\\ 1-0 &0 \end{bmatrix}_{3\times 2}\)

    predicted_label = [0.1, 0.9, 0]被当作3个数据点对应的正分类的概率预测值(不是单个数据点的属于不同类的概率),即对应分类1的概率;对应的负分类的概率则等于1减正分类的概率。同样,把它写成\(3\times 2\)的矩阵:\(p=\begin{bmatrix} 1-0.1 & 0.1\\ 1-0.9& 0.9\\ 1-0&0\end{bmatrix}\)这样就可以代入上面的公式计算\(y\)\(p\)之间的交叉熵了(\(L_{log}(Y,P)\)公式是按元素计算):\(\begin{align}CrossEntropy=&-\frac{1}{3}\times ReduceSum(\begin{bmatrix} 1-0 & 0\\ 1-1 & 1\\1-0 &0\end{bmatrix}*ln\begin{bmatrix} 1-0.1 & 0.1\\ 1-0.9& 0.9\\ 1-0&0\end{bmatrix})\\=&-\frac{1}{3}\times ReduceSum(\begin{bmatrix} (1-0)\times ln(1-0,1) & 0\times ln0.1\\ (1-1)\times ln(1-0.9) & 1\times ln0.9\\(1-0)\times ln(1-0) &0\times ln0\end{bmatrix})\\=&-\frac{1}{3}\times ReduceSum(\begin{bmatrix} ln(0.9) &0 \\ 0&ln(0.9) \\ ln(1)&0 \end{bmatrix})\\=&\frac{1}{3}\times RduceSum([0.105,0.105])\\=&\frac{1}{3}\times 0.21\\=&0.07\end{align}\)

    我们可以自己编个binary cross-entropy的小程序验证一下这个计算是否是二元交叉熵:

    def binary_cross_entropy(targets,predictions):
        
        if len(predictions.shape) ==1:
            N = len(predictions)
        else:
            N = predictions.shape[0]
            
        MINOR = 1e-15
        
        # to avoid log(0)
        predictions = predictions.astype(float)
        predictions[np.where(predictions == 0.)[0]] = MINOR
            
        predictions = predictions.astype(float)
        predictions[np.where(predictions == 1.)[0]] = 1 - MINOR
        
        # Binarizing target
        targets[np.where(targets == min(targets))] = 0
        targets[np.where(targets != 0)] = 1
        
        cross_entropy = -np.sum(targets * np.log(predictions)+(1-targets)*np.log(1-predictions)) / N
        
        return cross_entropy
    
    true_label = np.array([0,1,0])   
    predicted_label = np.array([0.1,0.9,0])  
    
    binary_cross_entropy(true_label, predicted_label)
    
    0.07024034377189749
    

    结果是一样的。

    注意,数据的标签targets里允许是其它数值,但只能有2个不同的数,比如8和1,这是因为log_loss对targets做了二值化的处理,把[1, 8, 1]变成[0, 1, 0]。我们的小程序也做了相应的处理。

    true_label = np.array([1,8,1])   
    predicted_label = np.array([0.1,0.9,0])  
    
    print(log_loss(true_label, predicted_label))
    print(binary_cross_entropy(true_label, predicted_label))
    
    0.07024034377188453
    0.07024034377188453
    

    但是,我们的小程序不支持以字符串表示的二分类,而log_loss是支持的。

    true_label=np.array(['no', 'yes', 'no'])
    print(log_loss(true_label, predicted_label))
    
    0.07024034377188453
    
  • 例二、 用log_loss计算categorical交叉熵

    如果我们想用log_loss计算categorical cross-entropy:

    true_label = np.array([1])   
    predicted_label = np.array([[0.1,0.9,0]])  # must carry two squre brackets for one data point
    log_loss(true_label, predicted_label, labels=[0,1,2])
    
    0.10536051565782739
    

    true_label里只有一个元素,代表只有一个数据点,其标签属于分类1。predicted_label里面也只能有一个数据点,但它输出的是3个分类的概率,所以这3个元素外面必须有两层方括号,最外层的表示内层方括号里无论有几个元素,它们都属于一个数据点。所以, \(N=1\)。因为此时true_label无法提供实际预测的分类信息,log_loss使用显式的预测分类参数labels=[0,1,2],表明predicted_label输出3个概率值[0.1,0.9,0]对应的是分类0,1,2。所以, \(K=3\)。和例一描述的过程类似,我们构建两个\(N\times K=1\times 3\) 的矩阵\(y\)\(p\)后就可计算了:\(CrossEntropy=-\frac{1}{1}\times ReduceSum([0\ 1\ 0]*ln[0,1\ 0.9\ 0])=-ln(0.9)=0.105\)

    我们编个小程序验证一下:

    def categorical_cross_entropy(targets, predictions):
        if len(predictions.shape) ==1:
            N = 1
        else:
            N = predictions.shape[0]   
        
        MINOR = 1e-15
           
        predictions = predictions.astype(float)
        predictions[np.where(predictions == 0.)[0]] = MINOR
        
        cross_entropy = -np.sum(targets * np.log(predictions)) / N
        
        return cross_entropy
    
    true_label = np.array([0,1,0])   
    predicted_label = np.array([0.1,0.9,0])  
    categorical_cross_entropy(true_label, predicted_label)
    
    0.10536051565782628
    

    结果是一样的。

    注意,与\(binary\_cross\_entropy()\)不同,对于\(categorical\_cross\_entropy()\)来说,[0.1,0.9,0]是单个数据点的多分类的概率。

posted @ 2021-10-10 17:14  甫生  阅读(463)  评论(0编辑  收藏  举报