• UPDATED:2023 年 1 月 27 日,本文登上 ATA 头条。(注:ATA 全称 Alibaba Technology Associate,是阿里集团最大的技能社区)
  • UPDATED:2023 年 2 月 2 日,本文在 ATA 取得鲁肃点赞。(注:鲁肃,本名程立,是阿里合伙人、阿里集团上一任 CTO)

咱们好!我是麦克船长,现在上任于阿里巴巴集团,任总监/资深综合运营专家,先后负责过淘宝职业产品团队、天天特卖、大聚合算运营中心。网名一向用「麦克船长」,中科大核算机本科毕业后先是做的音视频流媒体技能、分布式系统等等,干过 Full Stack,后来创业在技能、产品、运营、营销、供应链等等方面多年后来到阿里,在淘系带过不同事务的产品、运营团队。文本来自我的个人博客:MikeCaptain – 麦克船长的技能、产品与商业博客,整理了自己在新年期间对 NLP 根底模型的技能演化学习笔记记录,写就于大年初一在香港过新年时。本文包括 3 个章节:

  • 第一章,首要介绍 Transformer 呈现之前的几个干流言语模型,包括 N 元文法(n-gram)、多层感知器(MLP)、卷积神经网络(CNN)、循环神经网络(RNN)。其间 CNN 首要运用范畴在核算机视觉,因而没有更详细打开。其他模型也未面面俱到,首要考虑仍是一个范畴学习者的视点来了解和运用,而非研讨。
  • 第二章,是本文的中心,先介绍了留意力机制(Attention Mechanism),然后依据第一章对此前几大言语模型了解后,咱们能更好地了解 Transformer 为什么会带来革新性的影响。
  • 第三章,是一个 Transformer 的完成版别,依据 Tensorflow。

新年期间,除了本文,我还整理了一篇关于「大型言语模型(LLM)在 Transformer 之后的演化总述」和一篇关于「LLM 引领生产力革新,带来的未来几年科技脉搏把控」,但没有时刻整理排版,待日后有空再归拢后宣布,这些权当是在新年期间消磨时刻的技能喜好,由所以偏向学习的技能笔记,所以十分欢迎咱们批评、纠正、沟通

  • 作者:钟超(麦克船长)
  • 邮箱:zhongchao.ustc#gmail.com (#->@)
  • 微信:sinosuperman(请注明「公司/机构、职位」便于我补白,谢谢)

前语

本文试图从技能视点搞清楚一个问题:曩昔一年 AIGC 爆火、曩昔五年 NLP(自然言语处理)范畴突飞猛进的缘起是什么?

这个问题被解答后,将还有两个问题,但暂时本文没有作答:1)假如以为通过图灵测验代表着 AGI(Artificial General Intelligence,通用人工智能)的话,当下 NLP,甚至 AGI 开展到什么程度了?2)未来一些年内,AGI 的开展道路或许会是怎样的?

运用新年时刻,写了这么一篇数万字的长文笔记,希望共同喜好的朋友能读完多多纠正。

1、我来阿里之后第一个新增喜好是「变形金刚模型」,第二个新增喜好是「变形金刚模型」

写了个这么冷的梗,其实想说的是,前者指的是闻名 IP「变形金刚」相关的手办玩具模型,后者指的是这个引领革新的人工智能言语模型 Transformer。这两个喜好,都与现在从事的电商工作本职没有表面上的直接联络,权当喜好了。

2022 年「生成式 AI」运用取得了突飞猛进的开展,作为一个「古典互联网」从业者,殷切地感到这一次 AI 技能或许会带来的颠覆式变革,这让我振奋又焦虑。2022 年上半年,我从天天特卖事务负责人到大聚合算运营中心负责人,在去年相当长一段时刻里在重视直播带货在营销渠道的形式出题,一向在考虑一个问题:直播电商的高效(更适合的产品演绎方法 + 私域权益 + 冲动购买等」vs. 直播电商的低效(直播分发无人货匹配 + 直播间内千人一面 + 货品状况不知道 + 主播不可控等),能否推进一个保存直播的高效,一同处理直播的低效的形式呢?

这儿面有许多的内容值得探讨,不过这不是麦克船长该系列文章的初衷,但这是我为什么开端十分重视 AI 的引子。直播电商的数字人技能根底,有动作捕捉、面部表情模仿、视觉烘托、直播话术生成、语音合成等等。依据第一性原理抽丝剥茧后,我发现尽管动作捕捉、视觉烘托等等许多技能仍有很大应战,可是从商业视角看真正最影响用户心智的,是直播话术生成和演绎,除了头部主播,绝大多数直播带货在这方面都做的很糟糕,那么这儿面就有巨大的「机器学习」生成内容逾越非头部的大多数从业者的商场空间,而这彻底依靠自然言语处理(NLP)。

这个问题就归于「生成式 AI」的范畴了,国外科技圈叫它「Gen-AI」,即 Generative AI,我国科技圈都叫它「AIGC」,即 AI Generated Content,与 UGC、PGC 相对应。Gen-AI 的叫法更重视主体,详细地说是「生成式 AI 模型」,它是个「内容引擎」。而我国的叫法更重视「内容运用」。

讲到 AIGC 这儿,咱们了解的 ChatGPT 就在 2022 年年末上台了。也是由于 ChatGPT 的破圈,带来了 AIGC 在国内科技圈的重视度暴涨。我从去年年中开端重视「文生图,text2image」范畴的明星 Stable Diffusion 开源,然后重视到了 text2image 运用的爆发,包括 Disco Diffusion、MidJourney、DALLE 2 等等,这些都源于 CV(核算机视觉)范畴由于 Diffusion 模型开展带来的技能突破。

AI 生成图片确实十分惊人。我酷爱变形金刚模玩,然后对机甲类都十分喜爱,所以顺手生成了几张图,这儿贴一下咱们看看,分钟级的创造速度。(留意:当下 AI 生成图片首要是依据 Diffusion 的运用开展,AI 生成文本的中心驱动才是 Transformer 模型,此处只是展现)

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型
人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

可是从第一性原理视点讲,生成图片的运用广度,远远小于生成文本。文本内容的本质是言语文字的了解与生成,人类前史有 600 万年,可是人类文明前史大约就 6000 年,文明的大开展呈现在近 2000 多年的原因,首要来自 3500 多年前人类发明晰文字。所以 AI 生成文本,意味着 AI 能够用人类了解的方法(言语文字)与人类高效协作,这必将引爆生产力革新。而这必将深化影响电商、内容、游戏、云核算、企业服务等许多范畴。

2、掌握技能根底,是当下读懂 AI 脉搏的基本功,而这个脉搏将带动各行各业

一旦深化重视 AI、重视 NLP 范畴,你就会发现当下依然处于一个技能开展突破的阶段,不重视技能的状况下来聊 AI、聊 NLP、聊 AIGC,那就只能是一个「喜好者」,而无法深化与这个职业界的弄潮儿对话,更不要提参与其间了。所以这个新年,麦克船长回归了当年做技能时的初心,翻了一些资料,学习了 NLP 言语模型的关键技能,在此作为技能学习笔记,与咱们共享。尽管忧虑布鼓雷门,可是本着费曼教师发起的输出学习法,我把自己学习整理的内容抛出来,除了会更帮助到我自己,也能结交一些对此同样在重视的同学们,欢迎感兴趣的同学加我的微信(微信号 sinosuperman)在业余时刻和我沟通。

阅览本文,先对你过往的根底知识做了一些假定,假如你暂未了解,或许在阅览时遇到以下内容做一些简略地查询即可:

  • Word Presentation:自然言语处理中的词表明法,首要触及 embedding。
  • 张量:需求一点根底,比方了解张量的形状、升降维度等。但不会触及到复杂问题,对一阶张量(向量)、二阶张量(矩阵)的简略运算有数学根底即可。对三阶张量,大约能想象出其空间含义即可。言语模型里了解词之间的间隔,是有其空间几何含义的。
  • 技能结构:PyTorch 或 TensorFlow 结构。由于时刻和篇幅联系,新年期间整理这些时,关于结构根底,我首要是 Google 现用现查,询问 ChatGPT 以及在微信读书里直接查找全文。

作为技能笔记难免有纰漏或了解过错,欢迎纠正。文中自绘图片用的是 Graphviz,公式生成用的是 KaTeX,贴到 ATA 后难免有一些没有兼容的部分(发现的已做了 fix),望见谅。

第一章 2017 年之前的几个关键 NLP 言语模型

NLP 的技能根底方面,我以为首要是这两部分:词表明法(Word Presentation)、言语模型(Language Model)。关于词表明法,这儿不做详细介绍,基本的思路便是把词表明为向量(一维张量),最基本的 One-Hot、Word2Vec、GloVe、fastText 等。这部分的技能演进也在不断前进,比方本文即将要点介绍的 Transformer 模型里,用到的词表明法是「引进上下文感知的词向量」。

言语模型从早期的 N 元文法(N-Gram,本文要介绍的),到神经网络被提出后最早期的感知器(Perceptron),再到后来席卷核算机视觉(CV)范畴的卷积神经网络(CNN),然后呈现考虑序列特征的循环神经网络(RNN,包括 Encoder-Decoder 模型),直到 2017 年横空出世的 Transformer,大约分这五个首要阶段。由于本文的要点是 Transformer,所以前面四个模型我会快速概览一下,然后介绍下最朴素的留意力(Attention)机制,依据此再详细介绍下 Transformer,并对一个完好的、精炼完成的代码实例进行精讲。

第 1 节 N 元文法言语模型

1.1、马尔科夫假定(Markov Assumption)与 N 元文法言语模型(N-gram Language Model)

下一个词呈现的概率只依靠于它前面 n-1 个词,这种假定被称为「马尔科夫假定(Markov Assumption」。N 元文法,也称为 N-1 阶马尔科夫链。

  • 一元文法(1-gram),unigram,零阶马尔科夫链,不依靠前面任何词;
  • 二元文法(2-gram),bigram,一阶马尔科夫链,只依靠于前 1 个词;
  • 三元文法(3-gram),trigram,二阶马尔科夫链,只依靠于前 2 个词;
  • ……

通过前 t-1 个词猜测时刻 t 呈现某词的概率,用最大似然估量:

P(wt∣w1,w2…wt−1)=C(w1,w2,…wt)C(w1,w2,…wt−1)P(w_t | w_1,w_2…w_{t-1}) = \frac{C(w_1,w_2,…w_t)}{C(w_1,w_2,…w_{t-1})}

进一步地,一组词(也便是一个句子)呈现的概率便是:

P(w1,w2,…wt)=P(wt∣w1,w2,…wt−1)⋅P(wt−1∣w1,w2,…wt−2)⋅…⋅P(w1)=∏i=1t−1P(wi∣w1:i−1)\begin{aligned} P(w_1,w_2,…w_t) &= P(w_t | w_1,w_2,…w_{t-1}) \cdot P(w_{t-1} | w_1,w_2,…w_{t-2}) \cdot … \cdot P(w_1) \\ &= \displaystyle\prod_{i=1}^{t-1}P(w_i | w_{1:i-1}) \end{aligned}

为了处理句头、尾逇概率核算问题,咱们再引进两个符号 <BOS> 和 <EOS> 别离表明 beginning of sentence 和 end of sentence,所以 w0=w_0 = <BOS>、wlength+1=w_{length + 1} =<EOS>,其间 length 是词的数量。

详细地,比方关于 bigram,该模型表明如下:

P(w1,w2,…wt)=∏i=1t−1P(wi∣wi−1)P(wt∣wt−1)=C(wt−1,wt)C(wt−1)\begin{aligned} P(w_1,w_2,…w_t) &= \displaystyle\prod_{i=1}^{t-1}P(w_i | w_{i-1}) \\ P(w_t | w_{t-1}) &= \frac{C(w_{t-1}, w_t)}{C(w_{t-1})} \end{aligned}
  • 假如有词呈现次数为了 0,这一串乘出来便是 0 了,咋办?
  • 由于依据马尔科夫假定,所以 N 固定窗口取值,对长间隔词依靠的状况会体现很差。
  • 假如把 N 值取很大来处理长间隔词依靠,则会导致严峻的数据稀少(零频太多了),参数规模也会急速爆破(高维张量核算)。

上面的第一个问题,咱们引进滑润 / 回退 / 差值等办法来处理,而后边两个问题则是在神经网络模型呈现后才更好处理的。

1.2、滑润(Smoothing)/ 折扣(Discounting)

尽管限定了窗口 n 巨细下降了词概率为 0 的或许性,但当 n-gram 的 n 比较大的时候会有的未登录词问题(Out Of Vocabulary,OOV)。另一方面,操练数据很或许也不是 100% 齐备掩盖实践中或许遇到的词的。所以为了避免 0 概率呈现,就有了让零滑润过渡为非零的补丁式技能呈现。

最简略的滑润技能,便是折扣法(Discounting)。这是一个十分简略想到的办法,便是把全体 100% 的概率腾出一小部分来,给这些零频词(也常把低频词一同考虑)。常见的滑润办法有:加 1 滑润、加 K 滑润、Good-Turing 滑润、Katz 滑润等。

1.2.1、加 1 滑润 / 拉普拉斯滑润(Add-One Discounting / Laplace Smoothing)

加 1 滑润,便是直接将一切词汇的呈现次数都 +1,不止针对零频词、低频词。假如持续拿 bigram 举例来说,模型就会变成:

P(wi∣wi−1)=C(wi−1,wi)+1∑j=1n(C(wi−1,wj)+1)=C(wi−1,wi)+1C(wi−1)+∣V∣P(w_i | w_{i-1}) = \frac{C_(w_{i-1},w_i) + 1}{\displaystyle\sum_{j=1}^n(C_(w_{i-1},w_j) + 1)} = \frac{C(w_{i-1}, w_i) + 1}{C(w_{i-1}) + |\mathbb{V}|}

其间 NN 表明一切词的词频之和,∣V∣|\mathbb{V}| 表明词汇表的巨细。

假如当词汇表中的词,许多呈现次数都很小,这样对每个词的词频都 +1,成果的误差影响其实挺大的。换句话说,+1 关于低频词许多的场景,加的太多了,应该加一个更小的数( 1 < < 1)。所以有了下面的「 滑润」技能。

1.2.2、加 K 滑润 / 滑润(Add-K Discounting / Delta Smoothing)

把 +1 换成 ,咱们看下上面 bigram 模型应该变成上面姿态:

P(wi∣wi−1)=C(wi−1,wi)+∑j=1n(C(wi−1,wj)+)=C(wi−1,wi)+C(wi−1)+∣V∣P(w_i | w{i-1}) = \frac{C_(w_{i-1},w_i) + \delta}{\displaystyle\sum_{j=1}^n(C_(w_{i-1},w_j) + \delta)} = \frac{C(w_{i-1}, w_i) + \delta}{C(w_{i-1}) + \delta|\mathbb{V}|}

是一个超参数,承认它的值需求用到困惑度(Perplexity,一般用缩写 PPL)。别的,有些文章里也会把这个办法叫做「加 K 滑润,Add-K Smoothing」。

1.2.3、困惑度(Perplexity)

关于指定的测验集,困惑度界说为测验集中每一个词概率的几何平均数的倒数,公式如下:

PPL⁡(Dtest)=1P(w1,w2…wn)n\operatorname{PPL}(\mathbb{D}_{test}) = \frac{1}{\sqrt[n]{P(w_1,w_2…w_n)}}

P(w1,w2,…wt)=∏i=1t−1P(wi∣wi−1)P(w_1,w_2,…w_t) = \displaystyle\prod_{i=1}^{t-1}P(w_i|w_{i-1}) 带入上述公式,就得到了 PPL 的核算公式:

PPL⁡(Dtest)=(∏i=1nP(wi∣w1:i−1))−1n\operatorname{PPL}(\mathbb{D}_{test}) = (\displaystyle\prod_{i=1}^nP(w_i|w_{1:i-1}))^{-\frac{1}{n}}

1.3、回退(Back-off)

在多元文法模型中,比方以 3-gram 为例,假如呈现某些三元语法概率为零,则不运用零来表明概率,而回退到 2-gram,如下。

P(wi∣wi−2wi−1)={P(wi∣wi−2wi−1)C(wi−2wi−1wi)>0P(wi∣wi−1)C(wi−2wi−1wi)=0andC(wi−1wi)>0P(w_i|w_{i-2}w_{i-1}) = \begin{cases} P(w_i|w_{i-2}w_{i-1}) & C(w_{i-2}w_{i-1}w_i) > 0 \\ P(w_i|w_{i-1}) & C(w_{i-2}w_{i-1}w_i) = 0 \enspace and \enspace C(w_{i-1}w_i) > 0 \end{cases}

1.4、差值(Interpolation)

N 元文法模型假如用回退法,则只考虑了 n-gram 概率为 0 时回退为 n-1 gram,那么自然要问:n-gram 不为零时,是不是也能够按一定权重来考虑 n-1 gram?所以有了插值法。以 3-gram 为例,把 2-gram、1-gram 都考虑进来:

P(wi∣wi−2wi−1)=1P(wi∣wi−2wi−1)+2P(wi∣wi−1)+3P(wi)P(w_i|w_{i-2}w_{i-1}) = \lambda_1 P(w_i|w_{i-2}w_{i-1}) + \lambda_2 P(w_i|w_{i-1}) + \lambda_3 P(w_i)

第 2 节 感知器(Perceptron)

N 元文法模型的明显问题,在「马尔科夫假定与 N 元文法言语模型」末节现已说到了。这些问题基本在神经网络模型中被处理,而要了解神经网络模型,就要从感知器(Perceptron)开端。1957 年感知机模型被提出,1959 年多层感知机(MLP)模型被提出。MLP 有时候也被称为 ANN,即 Artificial Neural Network,接下来咱们来深化浅出地了解一下,并有一些着手的操练。

2.1、感知器(Perceptron):处理二元分类使命的前馈神经网络

x 是一个输入向量,w 是一个权重向量(对输入向量里的而每个值分配一个权重值所组成的向量)。举一个详细使命比方,比方假如这两个向量的内积超越某个值,则判别为 1,否则为 0,这其实便是一个分类使命。那么这个终究输出值能够如下表明:

y={1(⋅x≥0)0(⋅x<0)y = \begin{cases} 1 & (\omega \cdot x \geq 0) \\ 0 & (\omega \cdot x \lt 0) \end{cases}

这便是一个典型的感知器(Perceptron),一般用来处理分类问题。还能够再增加一个误差项(bias),如下:

y={1(⋅x+b≥0)0(⋅x+b<0)y = \begin{cases} 1 & (\omega \cdot x + b \geq 0) \\ 0 & (\omega \cdot x + b \lt 0) \end{cases}

感知器其实便是一个前馈神经网络,由输入层、输出层组成,没有躲藏层。而且输出是一个二元函数,用于处理二元分类问题。

2.2、线性回归(Linear Regression):从离散值的感知器(处理类问题),到接连值的线性回归(处理回归问题)

一般来说,咱们以为感知器的输出成果,是离散值。一般来说,咱们以为离散值作为输出处理的问题,是分类问题;相应地,接连值处理的问题是回归(Regression)。比方关于上面的感知器,假如咱们直接将 ⋅x+b\omega \cdot x + b 作为输出值,则就变成了一个线性回归问题的模型了。

下面咱们用 PyTorch 来完成一个线性回归的代码示例,首要咱们要了解在 PyTorch 库里有一个十分常用的函数:

nn.Linear(in_features, out_features)

这个函数在创立时会主动初始化权值和偏置,而且能够通过调用它的 forward 函数来核算输入数据的线性改换。详细来说,当输入为 x 时,forward 函数会核算 y=⋅x+by = \omega \cdot x + b,其间 WWbb 别离是 nn.Linear 图层的权值和偏置。

咱们来一个完好的代码示例:

import torch
import torch.nn as nn
# 界说模型
class LinearRegression(nn.Module):
    def __init__(self, input_size, output_size):
        super(LinearRegression, self).__init__()
        self.linear = nn.Linear(input_size, output_size)
    def forward(self, x):
        return self.linear(x)
# 初始化模型
model = LinearRegression(input_size=1, output_size=1)
# 界说丢掉函数和优化器
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
# 创立输入特征 X 和标签 y
X = torch.Tensor([[1], [2], [3], [4]])
y = torch.Tensor([[2], [4], [6], [8]])
# 操练模型
for epoch in range(100):
    # 前向传达,在本文 2.7 节有详细介绍
    predictions = model(X)
    loss = criterion(predictions, y)
    # 反向传达,在本文 2.7 节有详细介绍
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
# 创立测验数据 X_test 和标签 y_test
X_test = torch.Tensor([[5], [6], [7], [8]])
y_test = torch.Tensor([[10], [12], [14], [16]])
# 测验模型
with torch.no_grad():
    predictions = model(X_test)
    loss = criterion(predictions, y_test)
    print(f'Test loss: {loss:.4f}')

上述代码,一开端先创立一个 LinearRegression 线性回归模型的类,其间有一个 forward 前向传达函数,调用时其实便是核算一下输出值 y

主程序,一开端创立一个线性回归模型实例,然后界说一个用于点评模型作用的丢掉函数点评器,和用随机梯度下降(Stochastic Gradient Descent)作为优化器。

然后创立一个输入特征张量,和标签张量。用这组特征和标签进行操练,操练的进程便是依据 X 核算与测验 predictions 向量,再把它和 y 一同给点评器算出丢掉 loss,然后进行反向传达(在本文 2.7 节有介绍)。留意反向传达的三行代码:

optimizer.zero_grad()
loss.backward()
optimizer.step()

如此操练 100 次(每一次都会黑盒化地更新模型的参数,一个 epoch 便是一次操练进程,有时也称为 iteration 或许 step,不断依据 loss 操练优化模型参数。

然后咱们创立了一组测验特征值张量 X_test,和测验标签张量 y_test,然后用它们测验模型功能,把测验特征得到的 predictionsy_test 共同传给点评器,得到 loss。在这个比方中咱们会得到如下成果:

Test loss: 0.0034

2.3、逻辑回归(Logistic Regression):没有值域束缚的线性回归,到限定在一个范围内的逻辑回归(常用于分类问题)

能够看到线性回归问题,输出值是没有范围限定的。假如限定(limit)在特定的 (0,L)(0, L) 范围内,则就叫做逻辑回归了。那么怎样将一个线性回归变成逻辑回归呢?一般通过如下公式改换:

y=L1+e−k(z−z0)y = \frac{L}{1 + e^{-k(z-z_0)}}

这样本来的 z∈(−∞,+∞)z \in (-\infty, +\infty) 就被改换成了 y∈(0,L)y \in (0, L) 了。

  • 激活函数:这种把输出值限定在一个方针范围内的函数,被叫做 激活函数(Activation Function)
  • 函数的峻峭程度kk 操控,越大越陡。
  • z=z0z = z_0 时,y=L2y = \frac{L}{2}

下面给出一个依据 Python 的 scikit-learn 库的示例代码:

from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
# 这是 scikit-learn 库里的一个简略的数据集
iris = load_iris()
# 把 iris 数据集拆分红操练集和测验集两部分
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, test_size=0.25, random_state=42)
# 用 scikit-learn 库创立一个逻辑回归模型的实例
lr = LogisticRegression()
# 用上边 split 出来的操练集数据,操练 lr 模型实例
lr.fit(X_train, y_train)
# 用操练过的模型,拿测验集的输入数据做测验
predictions = lr.predict(X_test)
# 用测验集的数据验证精确性
accuracy = lr.score(X_test, predictions)
print(accuracy)

2.4、Sigmoid 回归(Sigmoid Regression):归一化的逻辑回归,一般用于二元分类使命

L=1,k=1,z0=0L = 1, k = 1, z_0 = 0,此时的激活函数便是 SigmoidSigmoid 函数,也常表明为 \sigma 函数,如下:

y=11+e−zy = \frac{1}{1 + e^{-z}}

Sigmoid 回归的值域,恰好在 (0, 1) 之间,所以常备作为用来归一化的激活函数。而一个线性回归模型,再用 sigmoid 函数归一化,这种也常被称为「Sigmoid 回归」。Sigmoid 这个单词的意思也便是 S 形,咱们能够看下它的函数图像如下:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

由于归一化,所以也能够把输出值了解为一个概率。比方咱们面对一个二元分类问题,那么输出成果就对应归于这个类别的概率。

这样一个 sigmoid 模型能够表明为:

y=Sigmoid(W⋅x+b)y = Sigmoid(W \cdot x + b)

别的 sigmoidsigmoid 函数的导数(即梯度)是很好算的:y′=y⋅(1−y)y’ = y \cdot (1-y)。这十分便运用于「梯度下降算法」依据 loss 对模型参数进行优化。Sigmoid 回归,一般用于二元分类使命。那么关于超越二元的状况怎样办呢?这就引出了下面的 Softmax 回归。

2.5、Softmax 回归(Softmax Regression):从处理二元使命的 sigmoid,到处理多元分类使命的 Softmax

相对逻辑回归,Softmax 也称为多项逻辑回归。上面说 Sigmoid 一般用于处理二元分类问题,那么多元问题就要用 Softmax 回归了。咱们来拿一个详细问题来解说,比方问题是关于恣意输入的一个电商产品的图片,来判别这个图片所代表的的产品,归于哪个产品类目。假定咱们一共有 100 个类目。那么一个图片比方说其一切像素值作为输入特征值,输出便是一个 100 维的向量 zz,输出向量中的每个值 ziz_i 表明归于相对应类目的概率 yiy_i

yi=Softmax(z)i=eziez1+ez2+…+ez100y_i = Softmax(z)_i = \frac{e^{z_i}}{e^{z_1} + e^{z_2} + … + e^{z_100}}

那么终究得到的 yy 向量中的每一项就对应这个输入 zz 归于这 100 个类目的各自概率了。所以假如回归到一般问题,这个 Softmax 回归的模型就如下:

y=Softmax(W⋅x+b)y = Softmax(W \cdot x + b)

关于上面电商产品图片的比方,假定每个图片的尺寸是 512×512,这个模型打开式如下:

[y1y2…y100]=Softmax([w1,1,w1,2,…w1,512w2,1,w2,2,…w2,512…………w100,1,w100,2,…w100,512]⋅[x1x2…x512]+[b1b2…b512])\begin{bmatrix} y_1 \\ y_2 \\ … \\ y_{100} \end{bmatrix} = Softmax(\begin{bmatrix} w_{1,1}, & w_{1,2}, & … & w_{1, 512} \\ w_{2,1}, & w_{2,2}, & … & w_{2, 512} \\ … & … & … & … \\ w_{100,1}, & w_{100,2}, & … & w_{100, 512} \end{bmatrix} \cdot \begin{bmatrix} x_1 \\ x_2 \\ … \\ x_{512} \end{bmatrix} + \begin{bmatrix} b_1 \\ b_2 \\ … \\ b_{512} \end{bmatrix})

这个对输入向量 xx 履行 w⋅x+bw \cdot x + b 运算,一般也常称为「线性映射/线性改变」。

2.6、多层感知器(Multi-Layer Perceptron)

上面咱们遇到的一切使命,都是用线性模型(Linear Models)处理的。有时候问题复杂起来,咱们就要引进非线性模型了。

这儿咱们要介绍一个新的激活函数 —— ReLUReLU(Rectified Linear Unit)—— 一个非线性激活函数,其界说如下:

ReLU(z)=max(0,z)ReLU(z) = max(0, z)

比方关于 MNIST 数据集的手写数字分类问题,便是一个典型的非线性的分类使命,下面给出一个示例代码:

import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
# 界说多层感知器模型
class MLP(nn.Module):
    def __init__(self, input_size, hidden_size, num_classes):
        super(MLP, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, num_classes)
    def forward(self, x):
        out = self.fc1(x)
        out = self.relu(out)
        out = self.fc2(out)
        return out
# 超参数
input_size = 784
hidden_size = 500
num_classes = 10
num_epochs = 5
batch_size = 100
learning_rate = 0.001
# 加载 MNIST 数据集
train_dataset = datasets.MNIST(root='../../data',
                               train=True,
                               transform=transforms.ToTensor(),
                               download=True)
test_dataset = datasets.MNIST(root='../../data',
                              train=False,
                              transform=transforms.ToTensor())
# 数据加载器
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
                                           batch_size=batch_size,
                                           shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
                                          batch_size=batch_size,
                                          shuffle=False)
model = MLP(input_size, hidden_size, num_classes)
# 界说丢掉函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# 操练模型
for epoch in range(num_epochs):
    for images, labels in train_loader:
        # 前向传达
        outputs = model(images)
        loss = criterion(outputs, labels)
        # 反向传达
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    # 输出操练丢掉
    print(f'Epoch {epoch + 1}, Training Loss: {loss.item():.4f}')

这段代码里,咱们能看到 MLP 的模型界说是:

nn.Linear(input_size, hidden_size)
nn.ReLU()
nn.Linear(hidden_size, num_classes)

与前面的模型示例代码相似,也都用到了本文 2.7 节我会介绍的反向传达、丢掉函数点评器、优化器。假如用公式表明的话,便是如下的模型界说:

z=W1⋅x+b1h=ReLU(z)y=W2⋅h+b2\begin{aligned} &z = W_1 \cdot x + b_1 \\ &h = ReLU(z) \\ &y = W_2 \cdot h + b_2 \end{aligned}

咱们知道 MLP 一般是一个输入和输出长度相同的模型,但少数状况下也能够构建输入和输出长度不同的 MLP 模型,比方输入一组序列后,输出是一个离散的分类成果。

2.7、简述怎样操练一个模型:前向传达与反向传达

这是个很重要的议题。可是新年时刻有限,这部分只能简写了,咱们更多聚集在言语模型本身。这儿简述一下,后续或许会再补全。

  • 操练神经网络,首要包括前向传达、反向传达这两步。
  • 前向传达,便是将数据输入给模型,依据已承认的一组参数(比方 MLP 中的权重 W、偏置 b 等),得到输出成果。依据输出成果核算丢掉函数,衡量当时参数下的模型功能。
  • 反向传达最常用到的是梯度下降法(这儿不讨论其他办法),依托丢掉函数,将其间的参数作为变量来求偏导(核算梯度),沿着梯度下降的方向求解丢掉函数的极小值,此时的参数可替代此前的参数。这便是对模型优化操练的一个典型进程。

引申问题 —— 梯度消失、梯度爆破问题:由于对丢掉函数的求偏导,是从输出层向输入层反向依据「数学上的链式法则」核算的,数学上这是个连乘核算,层数越多越简略呈现这个问题。这个求导进程或许会呈现梯度为零的状况,即梯度消失。也有或许呈现梯度值特别大的状况。

处理梯度消失、梯度爆破问题,又是一个重要议题,这儿篇幅所限也难以打开做技能笔记。粗犷的方法比方梯度剪切,Hinton 提出的逐层预操练后再全体精调理论上也 work,本文后续说到的 LSTM、ResNet 等也能够处理问题,咱们也还能了解到业界各种处理手法,有时机再与朋友们沟通学习。

2.8、MLP 的一个明显问题,帮咱们引出 CNN 模型

咱们能够看到,在 MLP 中,不论有多少层,某一层的输出向量 hnh_n 中的每个值,都会在下一层核算输出向量 hn+1h_{n+1} 的每个值时用到。详细来说,假如关于某一层的输出值如下:

hn+1=Softmax(Wn+1⋅hn+bn+1)h_{n+1} = Softmax(W_{n+1} \cdot h_n + b_{n+1})

上一段话里所谓的「用到」,其实便是要针对 hnh_n 生成相应的特征值 Wn+1W_{n+1} 权重矩阵中的每个行列里的数值和 bn+1b_{n+1} 误差向量里的每个值。假如用图画出来,便是:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

能够看到,输入的一切元素都被衔接,即被分配权重 w 和误差项 b,所以这被称为一个「全衔接层(Fully Connected Layer)」或许「稠密层(Dense Layer)」。可是关于一些使命这样做是很蠢的,会支付许多无效的核算。

因而咱们需求 focus 在更少量核算成本的模型,所以有了卷积神经网络(CNN)。

第 3 节 卷积神经网络(CNN)

MLP 里每一层的每个元素,都要乘以一个独立参数的权重 W,再加上一个偏执 b,这样的神经网络层常被咱们叫做「全衔接层(Fully Connected Layer)或稠密层(Dence Layer)。可是这样有个明显问题:假如输入内容的部分重要信息只是发生细微移动并没有丢掉,在全衔接层处理后,整个输出成果都会发生很大改变 —— 这不合理。

所以咱们会想到,假如咱们用一个小一些的全衔接层,只对重要的部分输入进行处理呢?其实这个思路和 n-gram 是相似的,都是用一个窗口来扫描部分。卷积神经网络(Convolutional Neural Network,CNN)便是依据此诞生的。

  • 卷积核:卷积核是一个小的稠密层,用于提取部分特征,又称其为卷积核(kernel)/ 滤波器(filter)/ 感触野(receptive field / field of view)。
  • 池化层(Pooling,或称会聚层):通过卷积核处理的成果,进一步聚合的进程。关于输入巨细不相同的样本,池化后将有相同个数的特征输出。
  • 提取多个部分特征:一个卷积核只能提取单一类型的部分特征,需求提取多种部分特征则需求多个卷积核。有些文章里你看说到「多个形式」、「多个通道」,其实指的便是多个 kernel 辨认多个特征。
  • 全衔接分类层:多个卷积核得到的多个特征,需通过一个全衔接的分类层用于终究决议计划。

这样做有几个特性:

  • 本地性(Locality):输出成果只由一个特定窗口巨细区域内的数据决议。
  • 平移不变性(Translation Invariant):对同一个特征,扫描不同区域时只用一个 kernel 来核算。
  • 卷积层的参数规模,与输入输出数据巨细无关。

CNN 首要的适用范畴是核算机视觉。而在 NLP 中,文本数据的维度很高,而且言语的结构比图像更复杂。因而,CNN 一般不适用于处理 NLP 问题。

第 4 节 循环神经网络(RNN)

RNN(循环神经网络),这是一种强壮的神经网络模型,能够猜测序列数据,例如文本、语音和时刻序列。咱们将通过生动的代码示例和实践案例来演示怎样运用 RNN,并在日常日子中真实地体验它的功用。您将学习到怎样运用 RNN 处理各种机器学习问题,并着手测验运用 RNN 处理实践问题。这篇文章将为您提供一个完好的 RNN 入门攻略,并使您对 RNN 有更深化的了解。

RNN(Recurrent Neural Network)的 R 是 Recurrent 的意思,所以这是一个贷循环的神经网络。首要要了解一点,你并不需求搞懂 CNN 后才干学习 RNN 模型。你只需了解了 MLP 就能够学习 RNN 了。

4.1、经典结构的 RNN

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

上图这是一个经典结构的 RNN 暗示图,Unfold 箭头右侧是打开暗示。输入序列(这儿用 xx 表明)传递给躲藏层(hidden layer,这儿用 hh 表明),处理完生成输出序列(这儿用 oo 表明)。序列的下一个词输入时的、上一步躲藏层会一同影响这一步的输出。UUVVWW 都表明权重。在这个经典结构理,你能够看到十分重要的一点,便是输入序列长度与输出序列长度是相同的。

这种经典结构的运用场景,比方对一段普通话输入它的四川话版别,比方对视频的每一帧进行处理并输出,等等。

咱们知道 RNN 是一个一个序列处理的,每个序列中的数据项都是有序的,所以关于核算一个序列内的一切数据项是无法并行的。可是核算不同序列时,不同序列各自的核算则是能够并行的。假如咱们把上一个时刻 t 躲藏层输出的成果 ht−1h_{t-1} 传给一个激活函数(比方说用正切函数 tanh 函数),然后和当下时刻 t 的这个输入 xtx_{t} 一同,处理后发生一个时刻 tt 的输出 hth_t 。然后把躲藏层的输出通过多项逻辑回归(Softmax)生成终究的输出值 yy,咱们能够如下表明这个模型:

ht=tanh(Wxh⋅xt+bxh+Whh⋅ht−1+bhh)yt=Softmax(Why⋅ht+bhy)\begin{aligned} &h_t = tanh(W^{xh} \cdot x_t + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh}) \\ &y_t = Softmax(W^{hy} \cdot h_t + b^{hy}) \end{aligned}

对应的暗示图如下:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

这种输入和输出数据项数共同的 RNN,一般叫做 N vs. N 的 RNN。假如咱们用 PyTorch 来完成一个十分简略的经典 RNN 则如下:

import torch
import torch.nn as nn
# 创立一个 RNN 实例
# 第一个参数
rnn = nn.RNN(10, 20, 1, batch_first=True)  # 实例化一个单向单层RNN
# 输入是一个形状为 (5, 3, 10) 的张量
# 5 个输入数据项(也能够说是样本)
# 3 个数据项是一个序列,有 3 个 steps
# 每个 step 有 10 个特征
input = torch.randn(5, 3, 10)
# 躲藏层是一个 (1, 5, 20) 的张量
h0 = torch.randn(1, 5, 20)
# 调用 rnn 函数后,回来输出、终究的躲藏状况
output, hn = rnn(input, h0)
print(output)
print(hn)

咱们来解读一下这段代码:

  • 这段代码实例化了一个带有 1 个躲藏层的 RNN 网络。
  • 它的输入是一个形状为 (5, 3, 10) 的张量,表明有 5 个样本,每个样本有 3 个时刻步,每个时刻步的特征维度是 10。
  • 初始躲藏状况是一个形状为 (1, 5, 20) 的张量。
  • 调用 rnn 函数后,会回来输出和终究的躲藏状况。
  • 输出的形状是 (5, 3, 20),表明有 5 个样本,每个样本有 3 个时刻步,每个时刻步的输出维度是 20。
  • 终究的躲藏状况的形状是 (1, 5, 20),表明终究的躲藏状况是 5

可是上面的代码示例,并没有自己编写一个详细的 RNN,而是用了默许的 PyTorch 的 RNN,那么下面咱们就自己编写一个:

class MikeCaptainRNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        super().__init__()
        # 关于 RNN,输入维度便是序列数
        self.input_size = input_size
        # 躲藏层有多少个节点/神经元,经常将 hidden_size 设置为与序列长度相同
        self.hidden_size = hidden_size
        # 输入层到躲藏层的 W^{xh} 权重、bias^{xh} 偏置项
        self.weight_xh = torch.randn(self.hidden_size, self.input_size) * 0.01
        self.bias_xh = torch.randn(self.hidden_size)
        # 躲藏层到躲藏层的 W^{hh} 权重、bias^{hh} 偏置项
        self.weight_hh = torch.randn(self.hidden_size, self.hidden_size) * 0.01
        self.bias_hh = torch.randn(self.hidden_size)
    # 前向传达
    def forward(self, input, h0):
    	# 取出这个张量的形状
        N, L, input_size = input.shape
        # 初始化一个全零张量
        output = torch.zeros(N, L, self.hidden_size)
        # 处理每个时刻的输入特征
        for t in range(L):
        	# 取得当时时刻的输入特征,[N, input_size, 1]。unsqueeze(n),在第 n 维上增加一维
            x = input[:, t, :].unsqueeze(2)  
            w_xh_batch = self.weight_xh.unsqueeze(0).tile(N, 1, 1)  # [N, hidden_size, input_size]
            w_hh_batch = self.weight_hh.unsqueeze(0).tile(N, 1, 1)  # [N, hidden_size, hidden_size]
            # bmm 是矩阵乘法函数
            w_times_x = torch.bmm(w_xh_batch, x).squeeze(-1)  # [N, hidden_size]。squeeze(n),在第n维上减小一维
            w_times_h = torch.bmm(w_hh_batch, h0.unsqueeze(2)).squeeze(-1)  # [N, hidden_size]
            h0 = torch.tanh(w_times_x + self.bias_ih + w_times_h + self.bias_hh)
            output[:, t, :] = h0
        return output, h0.unsqueeze(0)

源码解读都在注释中。

4.2、N vs.1 的 RNN

上面那个图里,假如只保存终究一个输出,那便是一个 N vs. 1 的 RNN 了。这种的运用场景,比方说判别一个文本序列是英语仍是德语,比方依据一个输入序列来判别是一个正向情绪内容仍是负向或许中性,或许比方依据一段语音输入序列来判别是哪一首曲子(听歌识曲)。

ht=tanh(Wxh⋅xt+bxh+Whh⋅ht−1+bhh)y=Softmax(Why⋅hn+bhy)\begin{aligned} &h_t = tanh({W^{xh}} \cdot x_t + {b^{xh}} + {W^{hh}} \cdot h_{t-1} + {b^{hh}}) \\ &y = Softmax({W^{hy}} \cdot h_n + {b^{hy}}) \end{aligned}

即这个模型里,每个序列只要躲藏层对终究一个数据项进行处理时才发生输出 hnh_n 假如用暗示图表明,则是如下结构:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

4.3、1 vs. N 的 RNN

反过来,上面那个图里,假如只保存一个 x,那么便是一个 1 vs. N 的 RNN 了。这种场景的运用,比方 AI 创造音乐,还有通过一个 image 提炼或辨认某些文本内容输出。

ht={tanh(Wxh⋅x+bxh+0+bhh)(t=1)tanh(0+bxh+Whh⋅ht−1+bhh)(t>1)yt=Softmax(Why⋅ht+bhy)\begin{aligned} &h_t = \begin{cases} tanh(W^{xh} \cdot x + b^{xh} + 0 + b^{hh}) & (t=1) \\ tanh(0 + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh}) & (t>1) \end{cases} \\ &y_t = Softmax(W^{hy} \cdot h_t + b^{hy}) \end{aligned}

暗示图如下:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

到这儿咱们能够看到,在 RNN 的躲藏层是能够存储一些有关于输入数据的一些相关内容的,所以也常把 RNN 的躲藏层叫做回忆单元。

4.4、LSTM(Long Short-Term Memory)长短时回忆网络

4.4.1、怎样了解这个 Short-Term 呢?

1997 年论文《Long Short-Term Memory》中提出 LSTM 模型。咱们先从模型的界说,精确地来了解一下:

ht=ht−1+tanh(Wxh⋅xt+bxh+Whh⋅ht−1+bhh)yt=Softmax(Why⋅ht+bhy)\begin{aligned} &h_t = h_{t-1} + tanh(W^{xh} \cdot x_t + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh}) \\ &y_t = Softmax(W^{hy} \cdot h_t + b^{hy}) \end{aligned}

上式中与经典结构的 RNN(输入与输出是 N vs. N)相比,唯一的差异是第一个式子中多了一个「ht−1h_{t-1}」。假如咱们把第一个式子的 tanhtanh 部分记作 utu_t

ut=tanh(Wxh⋅xt+bxh+Whh⋅ht−1+bhh)u_t = tanh(W^{xh} \cdot x_t + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh})

所以:

ht=ht−1+uth_t = h_{t-1} + u_t

那么能够打开出如下一组式子:

hk+1=hk+uk+1hk+2=hk+1+uk+2……ht−1=ht−2+ut−1ht=ht−1+ut\begin{aligned} h_{k+1} &= h_k + u_{k+1} \\ h_{k+2} &= h_{k+1} + u_{k+2} \\ &…… \\ h_{t-1} &= h_{t-2} + u_{t-1} \\ h_t &= h_{t-1} + u_t \end{aligned}

假如咱们从 hk+1h_{k+1}hnh_n 的一切式子左边相加、右侧相加,咱们就得到如下式子:

hk+1+…+ht−1+ht=hk+hk+1+…+ht−2+ht−1+uk+1+uk+2+…+ut−1+ut\begin{aligned} &h_{k+1} + … + h_{t-1} + h_t \\ = &h_k + h_{k+1} + … + h_{t-2} + h_{t-1} \\+ &u_{k+1} + u_{k+2} + … + u_{t-1} + u_t \end{aligned}

然后推导出:

ht=hk+uk+1+uk+2+…+ut−1+uth_t = h_k + u_{k+1} + u_{k+2} + … + u_{t-1} + u_t

从这儿咱们就能够看到,第 t 时刻的躲藏层输出,直接相关到第 k 时刻的输出,t 到 k 时刻的相关性则用 uk+1u_{k+1}utu_t 相加表明。也便是有 t-k 的短期(Short Term)回忆。

4.4.2、引进忘记门 f、输入门 i、输出门 o、回忆细胞 c

假如咱们为式子 ht=ht−1+uth_t = h_{t-1} + u_t 右侧两项分配一个权重呢?便是躲藏层对上一个数据项本身被上一个数据项通过躲藏层核算的成果,这两者做一对权重考虑配比,如下:

ft=sigmoid(Wf,xh⋅xt+bf,xh+Wf,hh⋅xt−1+bf,hh)ht=ft⊙ht−1+(1−ft)⊙ut\begin{aligned} &f_t = sigmoid(W^{f,xh} \cdot x_t + b^{f,xh} + W^{f,hh} \cdot x_{t-1} + b^{f,hh}) \\ &h_t = f_t \odot h_{t-1} + (1 – f_t) \odot u_t \end{aligned}

其间:

  • ⊙\odot 是 Hardamard 乘积,即张量的对应元素相乘。
  • ftf_t 是「忘记门(Forget Gate)」,该值很小时 t-1 时刻的权重就很小,也便是「此刻忘记上一刻」。该值应依据 t 时刻的输入数据、t-1 时刻数据在躲藏层的输出核算,而且其每个元素必须是 (0, 1) 之间的值,所以能够用 sigmoid 函数来得到该值:

但这种方法,关于曩昔 ht−1h_{t-1} 和当下 utu_t 形成了互斥,只能此消彼长。但其实曩昔和当下或许都很重要,有或许都恨不重要,所以咱们对曩昔持续选用 ftf_t 忘记门,对当下选用 iti_t 输入门(Input Gate):

ft=sigmoid(Wf,xh⋅xt+bf,xh+Wf,hh⋅xt−1+bf,hh)it=sigmoid(Wi,xh⋅xt+bi,xh+Wi,hh⋅ht−1+bi,hh)ht=ft⊙ht−1+it⊙ut\begin{aligned} &f_t = sigmoid(W^{f,xh} \cdot x_t + b^{f,xh} + W^{f,hh} \cdot x_{t-1} + b^{f,hh}) \\ &i_t = sigmoid(W^{i,xh} \cdot x_t + b^{i,xh} + W^{i,hh} \cdot h_{t-1} + b^{i,hh}) \\ &h_t = f_t \odot h_{t-1} + i_t \odot u_t \end{aligned}

其间:

  • ftf_t 相似地,界说输入门 iti_t ,可是留意 ftf_tht−1h_{t-1} 而非 xt−1x_{t-1} 有关。

再引进一个输出门:

ot=sigmoid(Wo,xh⋅xt+bo,xh+Wo,hh⋅xt−1+bo,hh)o_t = sigmoid(W^{o,xh} \cdot x_t + b^{o,xh} + W^{o,hh} \cdot x_{t-1} + b^{o,hh})

再引进回忆细胞 ctc_t,它是本来 hth_t 的变体,与 t-1 时刻的回忆细胞有忘记联系(通过忘记门),与当下时刻有输入门的联系:

ct=ft⊙ct−1+it⊙utc_t = f_t \odot c_{t-1} + i_t \odot u_t

那么此时 hth_t,咱们能够把 hth_t 变成:

ht=ot⊙tanh(ct)h_t = o_t \odot tanh(c_t)

回忆细胞这个概念还有有一点点形象的,它存储了曩昔的一些信息。OK,到此咱们全体的 LSTM 模型就变成了这个姿态:

ft=sigmoid(Wf,xh⋅xt+bf,xh+Wf,hh⋅xt−1+bf,hh)it=sigmoid(Wi,xh⋅xt+bi,xh+Wi,hh⋅ht−1+bi,hh)ot=sigmoid(Wo,xh⋅xt+bo,xh+Wo,hh⋅xt−1+bo,hh)ut=tanh(Wxh⋅xt+bxh+Whh⋅ht−1+bhh)ct=ft⊙ct−1+it⊙utht=ot⊙tanh(ct)yt=Softmax(Why⋅ht+bhy)\begin{aligned} &f_t = sigmoid(W^{f,xh} \cdot x_t + b^{f,xh} + W^{f,hh} \cdot x_{t-1} + b^{f,hh}) \\ &i_t = sigmoid(W^{i,xh} \cdot x_t + b^{i,xh} + W^{i,hh} \cdot h_{t-1} + b^{i,hh}) \\ &o_t = sigmoid(W^{o,xh} \cdot x_t + b^{o,xh} + W^{o,hh} \cdot x_{t-1} + b^{o,hh}) \\ &u_t = tanh(W^{xh} \cdot x_t + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh}) \\ &c_t = f_t \odot c_{t-1} + i_t \odot u_t \\ &h_t = o_t \odot tanh(c_t) \\ &y_t = Softmax(W^{hy} \cdot h_t + b^{hy}) \end{aligned}

4.5、双向循环神经网络、双向 LSTM

双向循环神经网络很好了解,便是两个方向都有,例如下图:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

在 PyTorch 中运用 nn.RNN 就有参数表明双向:

bidirectional – If True, becomes a bidirectional RNN. Default: False

bidirectional:默许设置为 False。若为 True,即为双向 RNN。

4.6、堆叠循环神经网络(Stacked RNN)、堆叠长短时回忆网络(Stacked LSTM)

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

在 PyTorch 中运用 nn.RNN 就有参数表明双向:

num_layers – Number of recurrent layers. E.g., setting num_layers=2 would mean stacking two RNNs together to form a stacked RNN, with the second RNN taking in outputs of the first RNN and computing the final results. Default: 1

num_layers:躲藏层层数,默许设置为 1 层。当 num_layers >= 2 时,便是一个 stacked RNN 了。

4.7、N vs. M 的 RNN

关于输入序列长度(长度 N)和输出序列长度(长度 M)不相同的 RNN 模型结构,也能够叫做 Encoder-Decoder 模型,也能够叫 Seq2Seq 模型。首要接收输入序列的 Encoder 先将输入序列转成一个躲藏态的上下文表明 C。C 能够只与终究一个躲藏层有关,甚至能够是终究一个躲藏层生成的躲藏态直接设置为 C,C 还能够与一切躲藏层有关。

有了这个 C 之后,再用 Decoder 进行解码,也便是从把 C 作为输入状况开端,生成输出序列。

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

详细地,能够如下表明:

C=Encoder(X)Y=Decoder(C)\begin{aligned} &C = Encoder(X) \\ &Y = Decoder(C) \\ \end{aligned}

进一步打开:

et=EncoderLSTM/GRU(xt,et−1)C=f1(en)dt=f2(dt−1,C)yt=DecoderLSTM/GRU(yt−1,dt−1,C)\begin{aligned} e_t &= Encoder_{LSTM/GRU}(x_t, e_{t-1}) \\ C &= f_1(e_n) \\ d_t &= f_2(d_{t-1}, C) \\ y_t &= Decoder_{LSTM/GRU}(y_{t-1}, d_{t-1}, C) \end{aligned}

这种的运用就十分广了,由于大多数时候输入序列与输出序列的长度都是不同的,比方最常见的运用「翻译」,从一个言语翻译成另一个言语;再比方 AI 的一个范畴「语音辨认」,将语音序列输入后生成所辨认的文本内容;还有比方 ChatGPT 这种问答运用等等。

Seq2Seq 模型十分超卓,一向到 2018 年之前 NLP 范畴里该模型已成为干流。可是它有很明显的问题:

  • 当输入序列很长时,Encoder 生成的 Context 或许就会呈现所捕捉的信息不充分的状况,导致 Decoder 终究的输出是不尽如人意的。详细地,究竟仍是 RNN 模型,其词间距过长时仍是会有梯度消失问题,底子原因在于用到了「递归」。当递归作用在同一个 weight matrix 上时,使得假如这个矩阵满意条件的话,其最大的特征值要是小于 1 的话,就一定呈现梯度消失问题。后来的 LSTM 和 GRU 也仅仅能缓解问题,并不能底子处理。
  • 并行作用差:每个时刻的成果依靠前一时刻。

第 5 节 为什么说 RNN 模型没有体现「留意力」?

Encoder-Decoder 的一个十分严峻的问题,是依靠中心那个 context 向量,则无法处理特别长的输入序列 —— 回忆力缺乏,会忘事儿。而忘事儿的底子原因,是没有「留意力」。

关于一般的 RNN 模型,Encoder-Decoder 结构并没有体现「留意力」—— 这句话怎样了解?当输入序列通过 Encoder 生成的中心成果(上下文 C),被喂给 Decoder 时,这些中心成果对所生成序列里的哪个词,都没有差异(没有特别关照谁)。这相当于在说:输入序列里的每个词,关于生成任何一个输出的词的影响,是相同的,而不是输出某个词时是聚集特定的一些输入词。这便是模型没有留意力机制。

人脑的留意力模型,其实是资源分配模型。NLP 范畴的留意力模型,是在 2014 年被提出的,后来逐渐成为 NLP 范畴的一个广泛运用的机制。能够运用的场景,比方关于一个电商渠道中很常见的白底图,其边际的白色区域都是无用的,那么就不应该被重视(重视权重为 0)。比方机器翻译中,翻译词都是对部分输入要点重视的。

所以 Attention 机制,便是在 Decoder 时,不是一切输出都依靠相同的「上下文 CtC_t」,而是时刻 t 的输出,运用 CtC_t,而这个 CtC_t 来自对每个输入数据项依据「留意力」进行的加权。

第 6 节 依据 Attention 机制的 Encoder-Decoder 模型

2015 年 Dzmitry Bahdanau 等人在论文《Neural Machine Translation by Jointly Learning to Align and Translate》 中提出了「Attention」机制,下面请跟着船长,船长会深化浅出地为你解说清楚。

下图中 eie_i 表明编码器的躲藏层输出,did_i 表明解码器的躲藏层输出

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

更进一步细化关于 CtC_t 部分,船长在此引证《依据深度学习的道路短期交通状况时空序列猜测》一书中的图:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

这个图里的 h~i\widetilde{h}_i 与上一个图里的 did_i 对应,hih_i 与上一个图里的 eie_i 对应。

针对时刻 tt 要产出的输出,躲藏层每一个躲藏细胞都与 CtC_t 有一个权重联系 t,i\alpha_{t,i} 其间 1≤i≤n1\le i\le n,这个权重值与「输入项通过编码器后躲藏层后的输出ei(1≤i≤n)e_i(1\le i\le n)、解码器的前一时刻躲藏层输出 dt−1d_{t-1}」两者有关:

si,t=score(ei,dt−1)i,t=exp(si,t)∑j=1nexp(sj,t)\begin{aligned} &s_{i,t} = score(e_i,d_{t-1}) \\ &\alpha_{i,t} = \frac{exp(s_{i,t})}{\textstyle\sum_{j=1}^n exp(s_{j,t})} \end{aligned}

常用的 scorescore 函数有:

  • 点积(Dot Product)模型:si,t=dt−1T⋅eis_{i,t} = {d_{t-1}}^T \cdot e_i
  • 缩放点积(Scaled Dot-Product)模型:si,t=dt−1T⋅eidimensions of dt−1 or eis_{i,t} = \frac{{d_{t-1}}^T \cdot e_i}{\sqrt{\smash[b]{dimensions\:of\:d_{t-1}\:or\:e_i}}},可避免由于向量维度过大导致点积成果太大

然后上下文向量就表明成:

Ct=∑i=1ni,tei\begin{aligned} &C_t = \displaystyle\sum_{i=1}^n \alpha_{i,t} e_i \end{aligned}

还记得 RNN 那部分里船长讲到的 Encoder-Decoder 模型的公式表明吗?

et=EncoderLSTM/GRU(xt,et−1)C=f1(en)dt=f2(dt−1,C)yt=DecoderLSTM/GRU(yt−1,dt−1,C)\begin{aligned} e_t &= Encoder_{LSTM/GRU}(x_t, e_{t-1}) \\ C &= f_1(e_n) \\ d_t &= f_2(d_{t-1}, C) \\ y_t &= Decoder_{LSTM/GRU}(y_{t-1}, d_{t-1}, C) \end{aligned}

参加 Attention 机制的 Encoder-Decoder 模型如下。

et=EncoderLSTM/GRU(xt,et−1)Ct=f1(e1,e2…en,dt−1)dt=f2(dt−1,Ct)yt=DecoderLSTM/GRU(yt−1,dt−1,Ct)\begin{aligned} e_t &= Encoder_{LSTM/GRU}(x_t, e_{t-1}) \\ C_t &= f_1(e_1,e_2…e_n,d_{t-1}) \\ d_t &= f_2(d_{t-1}, C_t) \\ y_t &= Decoder_{LSTM/GRU}(y_{t-1}, d_{t-1}, C_t) \end{aligned}

这种一同考虑 Encoder、Decoder 的 Attention,就叫做「Encoder-Decoder Attention」,也常被叫做「Vanilla Attention」。能够看到上面最中心的差异是第二个公式 CtC_t。参加 Attention 后,对一切数据给予不同的留意力分布。详细地,比方咱们用如下的函数来界说这个模型:

e=tanh(Wxe⋅x+bxe)si,t=score(ei,dt−1)i,t=esi,t∑j=1nesj,tCt=∑i=1ni,teidt=tanh(Wdd⋅dt−1+bdd+Wyd⋅yt−1+byd+Wcd⋅Ct+bcd)y=Softmax(Wdy⋅d+bdy)\begin{aligned} e &= tanh(W^{xe} \cdot x + b^{xe}) \\ s_{i,t} &= score(e_i,d_{t-1}) \\ \alpha_{i,t} &= \frac{e^{s_{i,t}}}{\textstyle\sum_{j=1}^n e^{s_{j,t}}} \\ C_t &= \displaystyle\sum_{i=1}^n \alpha_{i,t} e_i \\ d_t &= tanh(W^{dd} \cdot d_{t-1} + b^{dd} + W^{yd} \cdot y_{t-1} + b^{yd} + W^{cd} \cdot C_t + b^{cd}) \\ y &= Softmax(W^{dy} \cdot d + b^{dy}) \end{aligned}

到这儿你能发现留意力机制的什么问题不?这个留意力机制疏忽了方位信息。比方 Tigers love rabbits 和 Rabbits love tigers 会发生相同的留意力分数。

第二章 Transformer 在 2017 年横空出世

船长先通过一个动画来看下 Transformer 是举例暗示,该图来自 Google 的博客文章 《Transformer: A Novel Neural Network Architecture for Language Understanding》:

中文网络里找到的解说得比较好的 blogs、answers,简直都指向了同一篇博客:Jay Alammar 的《The Illustrated Transformer》,所以建议读者调配该篇文章阅览。

Transformer 模型顶用到了自留意力(Self-Attention)、多头留意力(Multiple-Head Attention)、残差网络(ResNet)与捷径(Short-Cut)。下面咱们先通过第 1 到第 4 末节把几个基本概念讲清楚,然后在第 5 末节解说全体 Transformer 模型就会好了解许多了。终究第 6 末节咱们来一段着手实践。

第 7 节 自留意力机制(Self-Attention)

自留意力是了解 Transformer 的关键,原作者在论文中限于篇幅,没有给出过多的解说。以下是我自己的了解,能够比较通透、符合知识地去了解 Transformer 中的一些神来之笔的概念。

7.1、一段自然言语内容,其本身就「暗含」许多内部相关信息

在参加了 Attention 的 Encoder-Decoder 模型中,对输出序列 Y 中的一个词的留意力来自于输入序列 X,那么假如 X 和 Y 相等呢?什么场景会有这个需求?由于咱们以为一段文字里某些词便是由于别的某些词而决议的,能够粗犷地了解为「完形填空」的原理。那么这样一段文字,其实就存在其间每个词的自留意力,举个比方:

老王是我的主管,我很喜爱他的平易近人。

对这句话里的「他」,假如依据这句话核算自留意力的话,明显应该给予「老王」最多的留意力。受此启发,咱们以为:

一段自然言语中,其实暗含了:为了得到关于某方面信息 Q,能够通过重视某些信息 K,然后得到某些信息(V)作为成果。

Q 便是 query 检索/查询,K、V 别离是 key、value。所以相似于咱们在图书检索系统里查找「NLP书籍」(这是 Q),得到了一本叫《自然言语处理实战》的电子书,书名便是 key,这本电子书便是 value。只是关于自然言语的了解,咱们以为任何一段内容里,都本身暗含了许多潜在 Q-K-V 的相关。这是全体遭到信息检索范畴里 query-key-value 的启发的。

依据这个启发,咱们将自留意力的公式表明为:

Z=SelfAttention(X)=Attention(Q,K,V)\begin{aligned} Z = SelfAttention(X) = Attention(Q,K,V) \end{aligned}

X 通过自留意力核算后,得到的「暗含」了许多原数据内部信息的 Z。然后咱们拿着这个带有自留意力信息的 Z 进行后续的操作。这儿要着重的是,Z 向量中的每个元素 z_i 都与 X 的一切元素有某种相关,而不是只与 x_i 有相关。

7.2、怎样核算 Q、K、V

Q、K、V 悉数来自输入 X 的线性改换:

Q=WQ⋅XK=WK⋅XV=WV⋅X\begin{aligned} Q &= W^Q \cdot X \\ K &= W^K \cdot X \\ V &= W^V \cdot X \end{aligned}

WQ、WK、WVW^Q、W^K、W^V 以随机初始化开端,通过操练就会得到十分好的体现。关于 XX 中的每一个词向量 xix_i,通过这个改换后得到了:

qi=WQ⋅xiki=WK⋅xivi=WV⋅xi\begin{aligned} q_i &= W^Q \cdot x_i \\ k_i &= W^K \cdot x_i \\ v_i &= W^V \cdot x_i \end{aligned}

7.3、留意力函数:怎样通过 Q、V 得到 Z

依据上面的启发,咱们以为 X 通过自留意力的挖掘后,得到了:

  • 暗含信息 1:一组 query 与一组 key 之间的相关,记作 qk(想一下信息检索系统要用 query 先招到 key)
  • 暗含信息 2:一组 value
  • 暗含信息 3:qk 与 value 的某种相关

这三组信息,别离怎样表明呢?这儿又需求一些启发了,由于核算机科学其实是在「模仿复原」实践国际,在 AI 的范畴现在的研讨方向便是模仿复原人脑的考虑。所以这种「模仿复原」都是寻找某一种近似办法,因而不能依照数学、物理的逻辑推理来了解,而应该依照「工程」或许「核算科学」来了解,想想咱们大学时学的「核算办法」这门课,因而常需求一些启发来找到某种「表明」。

这儿 Transformer 的作者,以为 QQKK 两个向量之间的相关,是咱们在用 QQ 找其在 KK 上的投影,假如 QQKK 是单位长度的向量,那么这个投影其实能够了解为找「QQKK 向量之间的相似度」:

  • 假如 QQKK 笔直,那么两个向量正交,其点积(Dot Product)为 0;
  • 假如 QQKK 平行,那么两个向量点积为两者模积 ∥Q∥∥K∥\|Q\|\|K\|
  • 假如 QQKK 呈某个夹角,则点积便是 QQKK 上的投影的模。

因而「暗含信息 1」就能够用「Q⋅KQ\cdot K」再通过 Softmax 归一化来表明。这个表明,是一个一切元素都是 0~1 的矩阵,能够了解成对应留意力机制里的「留意力分数」,也便是一个「留意力分数矩阵(Attention Score Matrix)」。

而「暗含信息 2」则是输入 XX 通过的线性改换后的特征,看做 XX 的另一种表明。然后咱们用这个「留意力分数矩阵」来加持一下 VV,这个点积进程就表明了「暗含信息 3」了。所以咱们有了如下公式:

Z=Attention(Q,K,V)=Softmax(Q⋅KT)⋅V\begin{aligned} Z = Attention(Q,K,V) = Softmax(Q \cdot K^T) \cdot V \end{aligned}

其实到这儿,这个留意力函数现已能够用了。有时候,为了避免由于向量维度过大,导致 Q⋅KTQ \cdot K^T 点积成果过大,咱们再加一步处理:

Z=Attention(Q,K,V)=Softmax(Q⋅KTdk)⋅V\begin{aligned} Z = Attention(Q,K,V) = Softmax(\frac{Q \cdot K^T}{\sqrt{\smash[b]{d_k}}}) \cdot V \end{aligned}

这儿 dkd_k 是 K 矩阵中向量 kik_i 的维度。这一步修正还有进一步的解说,即假如通过 Softmax 归一化后模型稳定性存在问题。怎样了解?假如假定 Q 和 K 中的每个向量的每一维数据都具有零均值、单位方差,这样输入数据是具有稳定性的,那么怎样让「暗含信息 1」核算后依然具有稳定性呢?即运算成果依然坚持零均值、单位方差,便是除以「dk\sqrt{\smash[b]{d_k}}」。

7.4、其他留意力函数

为了提示咱们这种暗含信息的表明,都只是核算办法上的一种选择,好坏全赖成果鉴定,所以包括上面的在内,常见的留意力函数有(甚至你也能够自己界说):

Z=Attention(Q,K,V)={=Softmax(QTK)V=Softmax(QKTdk)V=Softmax(Ttanh(W[q;k]))V=Softmax(QTWK)V=cosine[QTK]VZ = Attention(Q,K,V) = \begin{cases} \begin{aligned} &= Softmax(Q^T K) V \\ &= Softmax(\frac{Q K^T}{\sqrt{\smash[b]{d_k}}}) V \\ &= Softmax(\omega^T tanh(W[q;k])) V \\ &= Softmax(Q^T W K) V \\ &= cosine[Q^T K] V \end{aligned} \end{cases}

到这儿,咱们就从原始的输入 XX 得到了一个包括自留意力信息的 ZZ 了,后续就能够用 ZZ 了。

第 8 节 多头留意力

到这儿咱们了解了「自留意力」,而 Transformer 这篇论文通过增加「多头」留意力的机制进一步提高了留意力层。咱们先看下它是什么,然后看下它的优点。从本末节开端,本文许多插图引证自《The Illustrated Transformer》,作者 Jay Alammar 写出一篇十分深化浅出的图解文章,被许多引证,十分超卓,再次建议咱们去阅览。

Transformer 顶用了 8 个头,也便是 8 组不同的 Q-K-V:

Q0=W0Q⋅X;K0=W0K⋅X;V0=W0V⋅XQ1=W1Q⋅X;K1=W0K⋅X;V1=W1V⋅X….Q7=W7Q⋅X;K7=W0K⋅X;V7=W7V⋅X\begin{aligned} Q_0 = W_0^Q \cdot X ;\enspace K_0 = &W_0^K \cdot X ;\enspace V_0 = W_0^V \cdot X \\ Q_1 = W_1^Q \cdot X ;\enspace K_1 = &W_0^K \cdot X ;\enspace V_1 = W_1^V \cdot X \\ &…. \\ Q_7 = W_7^Q \cdot X ;\enspace K_7 = &W_0^K \cdot X ;\enspace V_7 = W_7^V \cdot X \end{aligned}

这样咱们就能得到 8 个 Z:

Z0=Attention(Q0,K0,V0)=Softmax(Q0⋅K0Tdk)⋅V0Z1=Attention(Q1,K1,V1)=Softmax(Q1⋅K1Tdk)⋅V1…Z7=Attention(Q7,K7,V7)=Softmax(Q7⋅K7Tdk)⋅V7\begin{aligned} &Z_0 = Attention(Q_0,K_0,V_0) = Softmax(\frac{Q_0 \cdot K_0^T}{\sqrt{\smash[b]{d_k}}}) \cdot V_0 \\ &Z_1 = Attention(Q_1,K_1,V_1) = Softmax(\frac{Q_1 \cdot K_1^T}{\sqrt{\smash[b]{d_k}}}) \cdot V_1 \\ &… \\ &Z_7 = Attention(Q_7,K_7,V_7) = Softmax(\frac{Q_7 \cdot K_7^T}{\sqrt{\smash[b]{d_k}}}) \cdot V_7 \\ \end{aligned}

然后咱们把 Z0Z_0Z7Z_7 沿着行数不变的方向悉数衔接起来,如下图所示:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

咱们再操练一个权重矩阵 WOW^O,然后用上面拼接的 Z0−7Z_{0-7} 乘以这个权重矩阵:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

所以咱们会得到一个 Z 矩阵:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

到这儿便是多头留意力机制的悉数内容,与单头留意力相比,都是为了得到一个 Z 矩阵,可是多头用了多组 Q-K-V,然后通过拼接、乘以权重矩阵得到终究的 Z。咱们总览一下整个进程:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

通过多头留意力,每个头都会重视到不同的信息,能够如下相似表明:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

这通过两种方法提高了留意力层的功能:

  • 多头留意力机制,扩展了模型重视不同方位的能力。ZZ 矩阵中的每个向量 ziz_i 包括了与 XX 中一切向量 xix_i 有关的一点编码信息。反过来说,不要以为 ziz_i 只与 xix_i 有关。
  • 多头留意力机制,为留意力层提供了多个「表明子空间 Q-K-V」,以及 Z。这样一个输入矩阵 XX,就会被表明成 8 种不同的矩阵 Z,都包括了原始数据信息的某种解读暗含其间。

第 9 节 退化现象、残差网络与 Short-Cut

9.1、退化现象

关于一个 56 层的神经网路,咱们很自然地会觉得应该比 20 层的神经网络的作用要好,比方说从误差率(error)的量化视点看。可是华人学者何凯明等人的论文《Deep Residual Learning for Image Recognition》中给咱们呈现了相反的成果,而这个问题的原因并不是由于层数多带来的梯度爆破/梯度消失(究竟现已用了归一化处理了这个问题),而是由于一种反常的现象,这种现象咱们称之为「退化现象」。何凯明等人以为这是由于存在「难以优化好的网络层」。

9.2、恒等映射

假如这 36 层还帮了倒忙,那还不如没有,是不是?所以这多出来的 36 个网络层,假如关于提高功能(例如误差率)毫无影响,甚至更进一步,这 36 层前的输入数据,和通过这 36 层后的输出数据,彻底相同,那么假如将这 36 层笼统成一个函数 f36f_{36},这便是一个恒等映射的函数:

f36(x)=xf_{36}(x) = x

回到实践运用中。假如咱们关于一个神经网络中的接连 N 层是提高功能,仍是下降功能,是不知道的,那么则能够建立一个越过这些层的衔接,完成:

假如这 N 层能够提高功能,则选用这 N 层;否则就越过。

这就像给了这 N 层神经网络一个试错的空间,待咱们承认它们的功能后再决议是否选用它们。一同也能够了解成,这些层能够去独自优化,假如功能提高,则不被越过。

9.3、残差网络(Residual Network)与捷径(Short-Cut)

假如前面 20 层现已能够完成 99% 的准确率,那么引进了这 36 层能否再提高「残差剩余那 1%」的准确率然后达到 100% 呢?所以这 36 层的网络,就被称为「残差网络(Residual Network,常简称为 ResNet)」,这个叫法十分形象。

而那个能够越过 N 层残差网络的捷径,则常被称为 Short-Cut,也会被叫做跳动链接(Skip Conntection),这就处理了上述深度学习中的「退化现象」。

第 10 节 Transformer 的方位编码(Positional Embedding)

还记得我在第二部分终究说到的吗:

这个留意力机制疏忽了方位信息。比方 Tigers love rabbits 和 Rabbits love tigers 会发生相同的留意力分数。

10.1、Transformer 论文中的三角式方位编码(Sinusoidal Positional Encoding)

现在咱们来处理这个问题,为每一个输入向量 xix_i 生成一个方位编码向量 tit_i,这个方位编码向量的维度,与输入向量(词的嵌入式向量表明)的维度是相同的:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

Transformer 论文中给出了如下的公式,来核算方位编码向量的每一位的值:

Ppos,2i=sin(pos100002idmodel)Ppos,2i+1=cos(pos100002idmodel)\begin{aligned} P_{pos,2i} &= sin(\frac{pos}{10000^{\frac{2i}{d_{model}}}}) \\ P_{pos,2i+1} &= cos(\frac{pos}{10000^{\frac{2i}{d_{model}}}}) \end{aligned}

这样关于一个 embedding,假如它在输入内容中的方位是 pos,那么其编码向量就表明为:

[Ppos,0,Ppos,1,…,Ppos,dx−1]\begin{aligned} [P_{pos,0}, P_{pos,1}, … , P_{pos,d_x-1}] \end{aligned}

延打开的话,方位编码其实还分为肯定方位编码(Absolute Positional Encoding)、相对方位编码(Relative Positional Encoding)。前者是专门生成方位编码,并想办法融入到输入中,咱们上面看到的便是一种。后者是微调 Attention 结构,使得它能够分辨不同方位的数据。别的其实还有一些无法分类到这两种的方位编码办法。

10.2、肯定方位编码

肯定方位编码,如上面说到的,便是界说一个方位编码向量 tit_i,通过 xi+tix_i + t_i 就得到了一个含有方位信息的向量。

  • 习得式方位编码(Learned Positional Encoding):将方位编码作为操练参数,生成一个「最大长度 x 编码维度」的方位编码矩阵,跟着操练进行更新。现在 Google BERT、OpenAI GPT 模型都是用的这种方位编码。缺陷是「外推性」差,假如文本长度超越之前操练时用的「最大长度」则无法处理。现在有一些给出优化计划的论文,比方「层次分解方位编码」。
  • 三角式方位编码(Sinusoidal Positional Encodign):上面提过了。
  • 循环式方位编码(Recurrent Positional Encoding):通过一个 RNN 再接一个 Transformer,那么 RNN 暗含的「次序」就导致不再需求额外编码了。但这样牺牲了并行性,究竟 RNN 的两大缺陷之一就有这个。
  • 相乘式方位编码(Product Positional Encoding):用「xi⊙tix_i \odot t_i」代替「xi+tix_i + t_i」。

10.3、相对方位编码和其他方位编码

最早来自于 Google 的论文《Self-Attention with Relative Position Representations》相对方位编码,考虑的是当时 position 与被 attention 的 position 之前的相对方位。

  • 常见相对方位编码:经典式、XLNET 式、T5 式、DeBERTa 式等。
  • 其他方位编码:CNN 式、复数式、融合式等。

到此咱们都是在讲 Encoder,现在咱们知道一个 Encoder 能够用如下的暗示图表明:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

第 11 节 Transformer 的编码器 Encoder 和解码器 Decoder

11.1、Encoder 和 Decoder 的图示结构

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型
  • 第一层是多头留意力层(Multi-Head Attention Layer)。
  • 第二层是通过一个前馈神经网络(Feed Forward Neural Network,简称 FFNN)。
  • 这两层,每一层都有「Add & Normalization」和 ResNet。
人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型
  • 解码器有两个多头留意力层。第一个多头留意力层是 Masked Multi-Head Attention 层,即在自留意力核算的进程中只要前面方位上的内容。第二个多头留意力层买有被 Masked,是个正常多头留意力层。
  • 能够看出来,第一个留意力层是一个自留意力层(Self Attention Layer),第二个是 Encoder-Decoder Attention 层(它的 K、V 来自 Encoder,Q 来自自留意力层),有些文章里会用这个视点来指代。
  • FNN、Add & Norm、ResNet 都与 Encoder 相似。

11.2、Decoder 的第一个输出成果

产出第一个终究输出成果的进程:

  • 不需求通过 Masked Multi-Head Attention Layer(自留意力层)。
  • 只通过 Encoder-Decoder Attention Layer。
人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

这样咱们就像前面的 Encoder-Decoder Attention 模型相同,得到第一个输出。可是终究的输出成果,还会通过一层「Linear + Softmax」。

11.3、Decoder 后续的一切输出

从产出第二个输出成果开端:

  • Decoder 的自留意力层,会用到前面的输出成果。
  • 能够看到,这是一个串行进程。

11.4、Decoder 之后的 Linear 和 Softmax

通过一切 Decoder 之后,咱们得到了一大堆浮点数的成果。终究的 Linear & Softmax 便是来处理「怎样把它变成文本」的问题的。

  • Linear 是一个全衔接神经网络,把 Decoders 输出的成果投影到一个超大的向量上,咱们称之为 logits 向量。
  • 假如咱们的输出词汇表有 1 万个词,那么 logits 向量的每一个维度就有 1 万个单元,每个单元都对应输出词汇表的一个词的概率。
  • Softmax 将 logits 向量中的每一个维度都做归一化,这样每个维度都能从 1 万个单元对应的词概率中选出最大的,对应的词汇表里的词,便是输出词。终究得到一个输出字符串。

第 12 节 Transformer 模型全体

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

终究咱们再来全体看一下 Transformer:

  • 首要输入数据生成词的嵌入式向量表明(Embedding),生成方位编码(Positional Encoding,简称 PE)。
  • 进入 Encoders 部分。先进入多头留意力层(Multi-Head Attention),是自留意力处理,然后进入全衔接层(又叫前馈神经网络层),每层都有 ResNet、Add & Norm。
  • 每一个 Encoder 的输入,都来自前一个 Encoder 的输出,可是第一个 Encoder 的输入便是 Embedding + PE。
  • 进入 Decoders 部分。先进入第一个多头留意力层(是 Masked 自留意力层),再进入第二个多头留意力层(是 Encoder-Decoder 留意力层),每层都有 ResNet、Add & Norm。
  • 每一个 Decoder 都有两部分输入。
  • Decoder 的第一层(Maksed 多头自留意力层)的输入,都来自前一个 Decoder 的输出,可是第一个 Decoder 是不通过第一层的(由于通过算出来也是 0)。
  • Decoder 的第二层(Encoder-Decoder 留意力层)的输入,Q 都来自该 Decoder 的第一层,且每个 Decoder 的这一层的 K、V 都是相同的,均来自终究一个 Encoder。
  • 终究通过 Linear、Softmax 归一化。

第 13 节 Transformer 的功能

Google 在其博客于 2017.08.31 发布如下测验数据:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型
人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

第三章 一个依据 TensorFlow 架构的 Transformer 完成

咱们来看看一个简略的 Transformer 模型,便是比较早呈现的 Kyubyong 完成的 Transformer 模型:github.com/Kyubyong/tr…

第 14 节 先操练和测验一下 Kyubyong Transformer

下载一个「德语-英语翻译」的数据集:drive.google.com/uc?id=1l5y6…

de-en 下面的 tgz 解压后放在 corpora/ 目录下。假如需求先修改超参数,需求修改 hyperparams.py。然后运转如下命令,生成词汇文件(vocabulary files),默许到 preprocessed 目录下:

mikecaptain@local $ python prepro.py

然后开端操练:

mikecaptain@local $ python train.py

也能够越过操练,直接下载预操练过的文件,是一个 logdir/ 目录,把它放到项目根目录下。然后能够对操练出来的成果,运转点评程序啦:

mikecaptain@local $ python eval.py

会生成「德语-英语」测验成果文件在 results/ 目录下,内容如下:

- source: Sie war eine jhrige Frau namens Alex
- expected: She was a yearold woman named Alex
- got: She was a <UNK> of vote called <UNK>
- source: Und als ich das hrte war ich erleichtert
- expected: Now when I heard this I was so relieved
- got: And when I was I <UNK> 's
- source: Meine Kommilitonin bekam nmlich einen Brandstifter als ersten Patienten
- expected: My classmate got an arsonist for her first client
- got: Because my first eye was a first show
- source: Das kriege ich hin dachte ich mir
- expected: This I thought I could handle
- got: I would give it to me a day
- source: Aber ich habe es nicht hingekriegt
- expected: But I didn't handle it
- got: But I didn't <UNK> <UNK>
- source: Ich hielt dagegen
- expected: I pushed back
- got: I <UNK>
...
Bleu Score = 6.598452846670836

评价成果文件的终究一行是 Bleu Score:

  • 这是用来评价机器翻译质量的一种度量方法。它是由几个不同的 BLEU 分数组成的,每个 BLEU 分数都表明翻译成果中与参阅翻译的堆叠程度。
  • 一个常用的 BLEU 分数是 BLEU-4,它核算翻译成果中与参阅翻译的 N 元文法言语模型 n-gram(n 为 4)的堆叠程度。分数越高表明翻译成果越接近参阅翻译。

第 15 节 Kyubyong Transformer 源码剖析

  • hparams.py:超参数都在这儿,仅 30 行。将在下面 2.1 部分解读。
  • data_load.py:装载、批处理数据的相关函数,代码仅 92 行。首要在下面 2.2 部分解读。
  • prepro.py:为 source 和 target 创立词汇文件(vocabulary file),代码仅 39 行。下面 2.3 部分会为咱们解读。
  • train.py:代码仅 184 行。在下面 2.4 部分解读。
  • modules.py:Encoding / Decoding 网络的构建模块,代码仅 329 行。与 modules.py 一同会在 2.4 部分解读。
  • eval.py:评价作用,代码仅 82 行。将在 2.5 部分解读

总计 700 多行代码。

15.1、超参数

hyperparams.py 文件中界说了 Hyperparams 超参数类,其间包括的参数咱们逐个来解说一下:

  • source_train:操练数据集的源输入文件,默许是 'corpora/train.tags.de-en.de'
  • target_train:操练数据集的方针输出文件,默许是 'corpora/train.tags.de-en.en'
  • source_test:测验数据集的源输入文件,默许是 'corpora/IWSLT16.TED.tst2014.de-en.de.xml'
  • target_test:测验数据集的方针输出文件,默许是 'corpora/IWSLT16.TED.tst2014.de-en.en.xml'
  • batch_size:设置每批数据的巨细。
  • lr:设置学习率 learning rate。
  • logdir:设置日志文件保存的目录。
  • maxlen
  • min_cnt
  • hidden_units:设置编码器和解码器中躲藏层单元的数量。
  • num_blocks:编码器(encoder block)、解码器(decoder block)的数量
  • num_epochs:操练进程中迭代的次数。
  • num_heads:还记得上面文章里咱们说到的 Transformer 顶用到了多头留意力吧,这儿便是多头留意力的头数。
  • droupout_rate:设置 dropout 层的 dropout rate,详细 dropout 请看 2.4.1 部分。
  • sinusoid:设置为 True 时表明运用正弦函数核算方位编码,否则为 False 时表明直接用 position 做方位编码。

15.2、预处理

文件 prepro.py 完成了预处理的进程,依据 hp.source_trainhp.target_train 别离创立 "de.vocab.tsv""en.vocab.tsv" 两个词汇表。

def make_vocab(fpath, fname):
    # 运用 codecs.open 函数读取指定文件路径(fpath)的文本内容,并将其存储在 text 变量中
    text = codecs.open(fpath, 'r', 'utf-8').read()
    # 将 text 中的非字母和空格的字符去掉
    text = regex.sub("[^\s\p{Latin}']", "", text)
    # 将 text 中的文本依照空格切割,并将每个单词存储在 words 变量中
    words = text.split()
    # words 中每个单词的词频
    word2cnt = Counter(words)
    # 查看是否存在 preprocessed 文件夹,假如不存在就创立
    if not os.path.exists('preprocessed'): os.mkdir('preprocessed')
    with codecs.open('preprocessed/{}'.format(fname), 'w', 'utf-8') as fout:
    	# 按呈现次数从多到少的次序写入每个单词和它的呈现次数
    	# 在文件最前面写入四个特别字符 <PAD>, <UNK>, <S>, </S> 别离用于填充,不知道单词,句子开端和句子完毕
        fout.write("{}\t1000000000\n{}\t1000000000\n{}\t1000000000\n{}\t1000000000\n".format("<PAD>", "<UNK>", "<S>", "</S>"))
        for word, cnt in word2cnt.most_common(len(word2cnt)):
            fout.write(u"{}\t{}\n".format(word, cnt))
if __name__ == '__main__':
    make_vocab(hp.source_train, "de.vocab.tsv")
    make_vocab(hp.target_train, "en.vocab.tsv")
    print("Done")
  • 在主函数中调用 make_vocab 函数,在目录 preprocessed 生成 de.vocab.tsven.vocab.tsv 两个词汇表文件。
  • 在函数 make_vocab 中,先运用 codecs.open 函数读取指定文件路径 fpath 的文本内容,并将其存储在 text 变量中,再运用正则表达式 regextext 中的非字母和空格的字符去掉,接着将 text 中的文本依照空格切割,并将每个单词存储在 words 变量中。
  • 接下来,运用 Counter 函数统计 words 中每个单词的呈现次数,并将统计成果存储在 word2cnt 变量中。
  • 终究一切词与词频,存储在 de.vocab.tsven.vocab.tsv 两个文件中。

15.3、操练/测验数据集的加载

咱们先看下 train.pydata_load.pyeval.py 三个文件:

  • train.py:该文件包括了 Graph 类的界说,并在其结构函数中调用 load_data.py 文件中的 get_batch_data 函数加载操练数据。
  • data_load.py:界说了加载操练数据、加载测验数据的函数。
  • eval.py:测验成果的点评函数界说在这个文件里。

下面是函数调用的流程:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型
def load_de_vocab():
    vocab = [line.split()[0] for line in codecs.open('preprocessed/de.vocab.tsv', 'r', 'utf-8').read().splitlines() if int(line.split()[1])>=hp.min_cnt]
    word2idx = {word: idx for idx, word in enumerate(vocab)}
    idx2word = {idx: word for idx, word in enumerate(vocab)}
    return word2idx, idx2word
def load_en_vocab():
    vocab = [line.split()[0] for line in codecs.open('preprocessed/en.vocab.tsv', 'r', 'utf-8').read().splitlines() if int(line.split()[1])>=hp.min_cnt]
    word2idx = {word: idx for idx, word in enumerate(vocab)}
    idx2word = {idx: word for idx, word in enumerate(vocab)}
    return word2idx, idx2word

preprocessed/de.vocab.tsvpreprocessed/en.vocab.tsv 中贮存的德语、英语的词汇、词频,载入成 word2idxidx2word。前者是通过词查询词向量,后者通过词向量查询词。

load_de_vocabload_en_vocab 函数被 create_data 函数引证,该函数将输入的源言语和方针言句子子转化为索引表明,并对过长的句子进行切断或填充。详细的解说看下面代码里的注释。

# 输入参数是翻译模型的源言句子子、方针言句子子
def create_data(source_sents, target_sents):
    de2idx, idx2de = load_de_vocab()
    en2idx, idx2en = load_en_vocab()
    # 用 zip 函数将源言语和方针言句子子对应起来,并对句子进行切断或填充
    x_list, y_list, Sources, Targets = [], [], [], []
    for source_sent, target_sent in zip(source_sents, target_sents):
        x = [de2idx.get(word, 1) for word in (source_sent + u" </S>").split()] # 1: OOV, </S>: End of Text
        y = [en2idx.get(word, 1) for word in (target_sent + u" </S>").split()] 
        # 将句子的词的编号,原句以及编号后的句子存储下来,以供之后运用
        if max(len(x), len(y)) <=hp.maxlen:
        	# 将 x 和 y 转化成 numpy 数组并参加 x_list 和 y_list 中
            x_list.append(np.array(x))
            y_list.append(np.array(y))
            # 将原始的 source_sent 和 target_sent 参加 Sources 和 Targets 列表中
            Sources.append(source_sent)
            Targets.append(target_sent)
    # 关于每个 (x, y) 对,运用 np.lib.pad 函数将 x 和 y 别离用 0 进行填充,直到长度为 hp.maxlen
    # 这样做的目的是使得每个句子长度都相等,便利后续的操练
    X = np.zeros([len(x_list), hp.maxlen], np.int32)
    Y = np.zeros([len(y_list), hp.maxlen], np.int32)
    for i, (x, y) in enumerate(zip(x_list, y_list)):
        X[i] = np.lib.pad(x, [0, hp.maxlen-len(x)], 'constant', constant_values=(0, 0))
        Y[i] = np.lib.pad(y, [0, hp.maxlen-len(y)], 'constant', constant_values=(0, 0))
    # 回来转化后的索引表明,以及未经处理的源言语和方针言句子子
    # X 是原始句子中德语的索引
    # Y 是原始句子中英语的索引
    # Sources 是源原始句子列表,并与 X 一一对应
    # Targets 是方针原始句子列表,并与 Y 一一对应
    return X, Y, Sources, Targets
# 回来原始句子中德语、英语的索引
def load_train_data():
    de_sents = [regex.sub("[^\s\p{Latin}']", "", line) for line in codecs.open(hp.source_train, 'r', 'utf-8').read().split("\n") if line and line[0] != "<"]
    en_sents = [regex.sub("[^\s\p{Latin}']", "", line) for line in codecs.open(hp.target_train, 'r', 'utf-8').read().split("\n") if line and line[0] != "<"]
    X, Y, Sources, Targets = create_data(de_sents, en_sents)
    return X, Y

下面的 get_batch_data 则从文本数据中读取并生成 batch:

def get_batch_data():
    # 加载数据
    X, Y = load_train_data()
    # calc total batch count
    num_batch = len(X) // hp.batch_size
    # 将 X 和 Y 转化成张量
    X = tf.convert_to_tensor(X, tf.int32)
    Y = tf.convert_to_tensor(Y, tf.int32)
    # 创立输入队列
    input_queues = tf.train.slice_input_producer([X, Y])
    # 创立 batch 队列,运用 shuffle_batch 将一组 tensor 随机打乱,并将它们分为多个 batch
    # 运用 shuffle_batch 是为了避免模型过拟合
    x, y = tf.train.shuffle_batch(input_queues,
                                num_threads=8,
                                batch_size=hp.batch_size, 
                                capacity=hp.batch_size*64,   
                                min_after_dequeue=hp.batch_size*32, 
                                allow_smaller_final_batch=False)
    return x, y, num_batch # (N, T), (N, T), ()

15.4、构建模型并操练

Graph 的结构函数流程,便是模型的构建流程,下面船长来剖析这部分代码。

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

全体这个流程,首要触及 train.py 文件和 modules.py 文件。一切模型所需的首要函数界说,都是在 modules.py 中完成的。咱们先看下编码器(Encoder)的流程:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

下面是 train.py 中完成的 Transformer 流程,其间的每一段代码,船长都会做详细解说,先不用急。这个流程里,首要界说了编码器,先运用了 Embedding 层将输入数据转化为词向量,运用 Positional Encoding 层对词向量进行方位编码,运用 Dropout 层进行 dropout 操作,然后进行多层 Multihead Attention 和 Feed Forward 操作。

在构建模型前,先履行 train.py 的主程序段,首要 if __name__ == '__main__' 这句代码是在 Python 中常用的一种编写方法,它的意思是当一个文件被直接运转时,if 句子下面的代码会被履行。请看下面代码的注释。

if __name__ == '__main__':
    # 加载词汇表   
    de2idx, idx2de = load_de_vocab()
    en2idx, idx2en = load_en_vocab()
    # 构建模型并操练
    g = Graph("train"); print("Graph loaded")
    # 创立了一个 Supervisor 对象来办理操练进程
    sv = tf.train.Supervisor(graph=g.graph, 
                             logdir=hp.logdir,
                             save_model_secs=0)
    # 运用 with 句子打开一个会话
    with sv.managed_session() as sess:
    	# 操练迭代 hp.num_epochs 次
        for epoch in range(1, hp.num_epochs+1): 
            if sv.should_stop(): break
            # tqdm 是一个 Python 库,用来在循环履行操练操作时在命令行中显现进度条
            for step in tqdm(range(g.num_batch), total=g.num_batch, ncols=70, leave=False, unit='b'):
            	# 每次迭代都会运转操练操作 g.train_op
                sess.run(g.train_op)
            # 获取操练的步数,通过 sess.run() 函数获取 global_step 的当时值并赋值给 gs。这样可在后边运用 gs 保存模型时用这个值命名模型
            gs = sess.run(g.global_step)
            # 每个 epoch 完毕时,它运用 saver.save() 函数保存当时模型的状况
            sv.saver.save(sess, hp.logdir + '/model_epoch_%02d_gs_%d' % (epoch, gs))
    print("Done")
  • num_epochs 是操练进程中迭代的次数,它表明操练模型需求在操练数据上跑多少遍。每一次迭代都会在操练数据集上进行操练,一般来说,操练数据集会被重复多次迭代,直到达到 num_epochs 次。这样能够保证模型能够充分地学习数据的特征。设置 num_epochs 的值过大或过小都会导致模型功能下降。
15.4.1、编码进程
Embedding

embedding 用来把输入生成词嵌入向量:

# 词语转化为对应的词向量表明
self.enc = embedding(self.x, 
                      vocab_size=len(de2idx), 
                      num_units=hp.hidden_units, 
                      scale=True,
                      scope="enc_embed")
  • vocab_size 是词汇表的巨细。
  • num_units 是词向量的维度。
  • scale 是一个布尔值,用来承认是否对词向量进行规范化。
  • scope 是变量作用域的称号。
Key Masks

接着生成一个 key_masks 用于在之后的核算中屏蔽掉某些方位的信息,以便模型只重视有效的信息。

key_masks = tf.expand_dims(tf.sign(tf.reduce_sum(tf.abs(self.enc), axis=-1)), -1)
  • 先对 self.enc 张量进行对每个元素求肯定值的操作
  • 沿着终究一阶作为轴,进行 reduce_sum 操作,得到一个 (batch, sequence_length) 形状的张量。
  • 再进行 tf.sign 操作,对刚得到的每个元素进行符号函数的改换。
  • 终究再扩展阶数,变成形状 (batch, sequence_length, 1) 的张量。
Positional Encoding

下面生成 Transformer 的方位编码:

# 方位编码
if hp.sinusoid:
    self.enc += positional_encoding(self.x,
                      num_units=hp.hidden_units, 
                      zero_pad=False, 
                      scale=False,
                      scope="enc_pe")
else:
    self.enc += embedding(tf.tile(tf.expand_dims(tf.range(tf.shape(self.x)[1]), 0),
    							 [tf.shape(self.x)[0], 1]),
                      vocab_size=hp.maxlen, 
                      num_units=hp.hidden_units, 
                      zero_pad=False, 
                      scale=False,
                      scope="enc_pe")

假如超参数 hp.sinusoid=True,运用 positional_encoding 函数,通过运用正弦和余弦函数来生成方位编码,能够为输入序列增加方位信息。假如 hp.sinusoid=False,运用 embedding 函数,通过学习的词嵌入来生成方位编码。

方位编码生成后,用 key_masks 处理一下。留意 key_masks 的生成一定要用开始的 self.enc,所以在前面履行而不是这儿:

self.enc *= key_masks

这个不是矩阵乘法,而是对应元素相乘。这儿乘上 key_masks 的目的是将 key_masks 中值为 0 的方位对应的 self.enc 中的元素置为 0,这样就能够排除这些方位对核算的影响。

Drop out

下面调用了 TensorFlow 的 drop out 操作:

self.enc = tf.layers.dropout(self.enc,
                            rate=hp.dropout_rate, 
                            training=tf.convert_to_tensor(is_training))

drop out 是一种在深度学习中常用的正则化技巧。它通过在操练进程中随机地「封闭」一些神经元来削减 过拟合。这样做是为了避免模型过于依靠于某些特定的特征,而导致在新数据上的体现欠安。

在这个函数中,dropout 层通过在操练进程中随机地将一些神经元的输出值设置为 0,来削减模型的过拟合。这个函数中运用了一个参数 rate,表明每个神经元被「封闭」的概率。这样做是为了避免模型过于依靠于某些特定的特征,而导致在新数据上的体现欠安。

Encoder Blocks: Multi-Head Attention & Feed Forward

然后看下 encoder blocks 代码:

## Blocks
for i in range(hp.num_blocks):
    with tf.variable_scope("num_blocks_{}".format(i)):
        # 多头留意力
        self.enc = multihead_attention(queries=self.enc, 
                                        keys=self.enc, 
                                        num_units=hp.hidden_units, 
                                        num_heads=hp.num_heads, 
                                        dropout_rate=hp.dropout_rate,
                                        is_training=is_training,
                                        causality=False)
        # 前馈神经网络
        self.enc = feedforward(self.enc, num_units=[4*hp.hidden_units, hp.hidden_units])

上述代码是编码器(Encoder)的完成函数调用的流程,也是与船长上面的模型原理介绍共同的,在界说时同样运用了 Embedding 层、Positional Encoding 层、Dropout 层、Multihead Attention 和 Feed Forward 操作。其间 Multihead Attention 在编码、解码中是不相同的,待会儿咱们会在 Decoder 部分再说到,有自留意力层和 Encoder-Decoder 层。

  • 超参数 hp.num_blocks 表明 Encoder Blocks 的层数,每一层都有一个 Multi-Head Attention 和一个 Feed Forward。
  • 这个 Encoder 中的 Multi-Head Attention 是依据自留意力的(留意与后边的 Decoder 部分有差异)
  • causality 参数的意思是否运用 Causal Attention,它是 Self-Attention 的一种,可是只运用曩昔的信息,避免模型获取未来信息的干扰。一般关于猜测序列中的某个时刻步来说,只重视之前的信息,而不是整个序列的信息。这段代码中 causality 设置为了 False,即会重视整个序列的信息。
15.4.2、解码进程

再看一下解码的流程:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型
Embedding

下面咱们逐个看每段代码,首要重视与编码阶段的差异即可:

self.dec = embedding(self.decoder_inputs,
                      vocab_size=len(en2idx), 
                      num_units=hp.hidden_units,
                      scale=True, 
                      scope="dec_embed")
  • embedding 输入用的是 self.decoder_inputs
  • 词汇表尺寸用翻译后的输出言语英语词汇表长度 len(en2idx)
Key Masks
key_masks = tf.expand_dims(tf.sign(tf.reduce_sum(tf.abs(self.dec), axis=-1)), -1)
  • key_masks 输入变量用 self.dec
Positional Encoding & Drop out
# 方位编码
if hp.sinusoid:
    self.dec += positional_encoding(self.decoder_inputs,
                      vocab_size=hp.maxlen, 
                      num_units=hp.hidden_units, 
                      zero_pad=False, 
                      scale=False,
                      scope="dec_pe")
else:
    self.dec += embedding(tf.tile(tf.expand_dims(tf.range(tf.shape(self.decoder_inputs)[1]), 0),
    							 [tf.shape(self.decoder_inputs)[0], 1]),
                      vocab_size=hp.maxlen, 
                      num_units=hp.hidden_units, 
                      zero_pad=False, 
                      scale=False,
                      scope="dec_pe")
self.dec *= key_masks
self.dec = tf.layers.dropout(self.dec, 
                            rate=hp.dropout_rate, 
                            training=tf.convert_to_tensor(is_training))
  • 输入 self.decoder_inputs
  • 指定 vocab_size 参数 hp.maxlen
Decoder Blocks: Multi-Head Attention & Feed Forward
## 解码器模块
for i in range(hp.num_blocks):
    with tf.variable_scope("num_blocks_{}".format(i)):
        # 多头留意力(自留意力)
        self.dec = multihead_attention(queries=self.dec, 
                                        keys=self.dec, 
                                        num_units=hp.hidden_units, 
                                        num_heads=hp.num_heads, 
                                        dropout_rate=hp.dropout_rate,
                                        is_training=is_training,
                                        causality=True, 
                                        scope="self_attention")
        # 多头留意力(Encoder-Decoder 留意力)
        self.dec = multihead_attention(queries=self.dec, 
                                        keys=self.enc, 
                                        num_units=hp.hidden_units, 
                                        num_heads=hp.num_heads,
                                        dropout_rate=hp.dropout_rate,
                                        is_training=is_training, 
                                        causality=False,
                                        scope="vanilla_attention")
        # 前馈神经网络
        self.dec = feedforward(self.dec, num_units=[4*hp.hidden_units, hp.hidden_units])
  • 在用 multihead_attention 函数解码器模块时,留意传入的参数 scope 差异,先是自留意力层,用参数 self_attention,对应的 queriesself.deckeys 也是 self.dec。再是「Encoder-Decder Attention」用的是参数 vanilla_attention,对应的 queries 来自解码器是 self.dec,但 keys 来自编码器是是 self.enc
15.4.3、Embedding、Positional Encoding、Multi-Head Attention、Feed Forward
Embedding 函数完成
def embedding(inputs,
              vocab_size, 
              num_units, 
              zero_pad=True, 
              scale=True,
              scope="embedding", 
              reuse=None):
    with tf.variable_scope(scope, reuse=reuse):
    	# 创立一个名为 `lookup_table`、形状为 (vocab_size, num_units) 的矩阵
        lookup_table = tf.get_variable('lookup_table',
                                       dtype=tf.float32,
                                       shape=[vocab_size, num_units],
                                       initializer=tf.contrib.layers.xavier_initializer())
        # lookup_table 的第一行插入一个全零行,作为 PAD 的词向量
        if zero_pad:
            lookup_table = tf.concat((tf.zeros(shape=[1, num_units]),
                                      lookup_table[1:, :]), 0)
        # 在词向量矩阵 lookup_table 中查找 inputs
        outputs = tf.nn.embedding_lookup(lookup_table, inputs)
        # 对输出的词向量进行除以根号 num_units 的操作,能够操控词向量的统计稳定性。
        if scale:
            outputs = outputs * (num_units ** 0.5) 
    return outputs
Positional Encoding 函数完成
def positional_encoding(inputs,
                        num_units,
                        zero_pad=True,
                        scale=True,
                        scope="positional_encoding",
                        reuse=None):
    N, T = inputs.get_shape().as_list()
    with tf.variable_scope(scope, reuse=reuse):
    	# tf.range(T) 生成一个 0~T-1 的数组
    	# tf.tile() 将其扩展成 N*T 的矩阵,表明每个词的方位
        position_ind = tf.tile(tf.expand_dims(tf.range(T), 0), [N, 1])
        # First part of the PE function: sin and cos argument
        position_enc = np.array([
            [pos / np.power(10000, 2.*i/num_units) for i in range(num_units)]
            for pos in range(T)])
        # 用 numpy 的 sin 和 cos 函数对每个方位进行编码
        position_enc[:, 0::2] = np.sin(position_enc[:, 0::2])  # dim 2i
        position_enc[:, 1::2] = np.cos(position_enc[:, 1::2])  # dim 2i+1
        # 将编码成果转为张量
        lookup_table = tf.convert_to_tensor(position_enc)
        # 将编码的成果与方位索引相相关,得到终究的方位编码
        if zero_pad:
        	# 假如 zero_pad 参数为 True,则在编码成果的最初增加一个全 0 的向量
            lookup_table = tf.concat((tf.zeros(shape=[1, num_units]),
                                      lookup_table[1:, :]), 0)
        outputs = tf.nn.embedding_lookup(lookup_table, position_ind)
        # scale 参数为 True,则将编码成果乘上 num_units 的平方根
        if scale:
            outputs = outputs * num_units**0.5
        return outputs
Multi-Head Attention 函数完成
def multihead_attention(queries,
                        keys, 
                        num_units=None, 
                        num_heads=8, 
                        dropout_rate=0,
                        is_training=True,
                        causality=False,
                        scope="multihead_attention", 
                        reuse=None):
    with tf.variable_scope(scope, reuse=reuse):
        # Set the fall back option for num_units
        if num_units is None:
            num_units = queries.get_shape().as_list()[-1]
        # Linear Projections
        # 运用三个全衔接层对输入的 queries、keys 别离进行线性改换,将其转化为三个维度相同的张量 Q/K/V
        Q = tf.layers.dense(queries, num_units, activation=tf.nn.relu) # (N, T_q, C)
        K = tf.layers.dense(keys, num_units, activation=tf.nn.relu) # (N, T_k, C)
        V = tf.layers.dense(keys, num_units, activation=tf.nn.relu) # (N, T_k, C)
        # Split and concat
        # 按头数 split Q/K/V,再各自衔接起来
        Q_ = tf.concat(tf.split(Q, num_heads, axis=2), axis=0) # (h*N, T_q, C/h) 
        K_ = tf.concat(tf.split(K, num_heads, axis=2), axis=0) # (h*N, T_k, C/h) 
        V_ = tf.concat(tf.split(V, num_heads, axis=2), axis=0) # (h*N, T_k, C/h) 
        # Multiplication
        # 核算 Q_, K_, V_ 的点积来取得留意力权重
        # 其间 Q_ 的维度为 (hN, T_q, C/h)
        # K_ 的维度为 (hN, T_k, C/h)
        # 核算出来的成果 outputs 的维度为 (h*N, T_q, T_k)
        outputs = tf.matmul(Q_, tf.transpose(K_, [0, 2, 1])) # (h*N, T_q, T_k)
        # Scale
        # 对权重进行 scale,这儿除以了 K_ 的第三维的平方根,用于缩放权重
        outputs = outputs / (K_.get_shape().as_list()[-1] ** 0.5)
        # Key Masking
        # 这儿需求将 keys 的有效部分符号出来,将无效部分设置为极小值,以便在之后的 softmax 中被疏忽
        key_masks = tf.sign(tf.reduce_sum(tf.abs(keys), axis=-1)) # (N, T_k)
        key_masks = tf.tile(key_masks, [num_heads, 1]) # (h*N, T_k)
        key_masks = tf.tile(tf.expand_dims(key_masks, 1), [1, tf.shape(queries)[1], 1]) # (h*N, T_q, T_k)
        paddings = tf.ones_like(outputs)*(-2**32+1)
        outputs = tf.where(tf.equal(key_masks, 0), paddings, outputs) # (h*N, T_q, T_k)
        # Causality = Future blinding
        if causality:
        	# 创立一个与 outputs[0, :, :] 相同形状的全 1 矩阵
            diag_vals = tf.ones_like(outputs[0, :, :]) # (T_q, T_k)
            # 对 diag_vals 进行处理,回来一个下三角线矩阵
            tril = tf.linalg.LinearOperatorLowerTriangular(diag_vals).to_dense() # (T_q, T_k)
            masks = tf.tile(tf.expand_dims(tril, 0), [tf.shape(outputs)[0], 1, 1]) # (h*N, T_q, T_k)
   			# 将 masks 为 0 的方位的 outputs 值设置为一个十分小的数
   			# 这样会导致这些方位在之后的核算中对成果发生十分小的影响,然后完成了遮盖未来信息的功用
            paddings = tf.ones_like(masks)*(-2**32+1)
            outputs = tf.where(tf.equal(masks, 0), paddings, outputs) # (h*N, T_q, T_k)
        # 关于每个头的输出,运用 softmax 激活函数,这样能够得到一个概率分布
        outputs = tf.nn.softmax(outputs) # (h*N, T_q, T_k)
        # Query Masking
        # 关于查询(queries)进行 masking,这样能够避免输入序列后边的词对之前词的影响
        query_masks = tf.sign(tf.reduce_sum(tf.abs(queries), axis=-1)) # (N, T_q)
        query_masks = tf.tile(query_masks, [num_heads, 1]) # (h*N, T_q)
        query_masks = tf.tile(tf.expand_dims(query_masks, -1), [1, 1, tf.shape(keys)[1]]) # (h*N, T_q, T_k)
        outputs *= query_masks # broadcasting. (N, T_q, C)
        # Dropouts & Weighted Sum
        # 关于每个头的输出,运用 dropout 以及进行残差衔接
        outputs = tf.layers.dropout(outputs, rate=dropout_rate, training=tf.convert_to_tensor(is_training))
        outputs = tf.matmul(outputs, V_) # ( h*N, T_q, C/h)
        # Restore shape
        # 将每个头的输出拼接起来,运用 tf.concat 函数,将不同头的成果依照第二维拼接起来
        # 得到终究的输出成果,即通过多头留意力核算后的成果
        outputs = tf.concat(tf.split(outputs, num_heads, axis=0), axis=2 ) # (N, T_q, C)
        # Residual connection
        outputs += queries
        # Normalize
        outputs = normalize(outputs) # (N, T_q, C)
    return outputs
Feed Forward 函数完成

下面是 前馈神经网络层 的界说,这是一个非线性改换,这儿用到了一些卷积神经网络(CNN)的知识,咱们来看下代码再解说:

def feedforward(inputs,
                num_units=[2048, 512],
                scope="multihead_attention", 
                reuse=None):
    with tf.variable_scope(scope, reuse=reuse):
        # Inner layer
        params = {"inputs": inputs, "filters": num_units[0], "kernel_size": 1,
                  "activation": tf.nn.relu, "use_bias": True}
        outputs = tf.layers.conv1d(**params)
        # Readout layer
        params = {"inputs": outputs, "filters": num_units[1], "kernel_size": 1,
                  "activation": None, "use_bias": True}
        outputs = tf.layers.conv1d(**params)
        # 衔接一个残差网络 ResNet
        outputs += inputs
        # 归一化后输出
        outputs = normalize(outputs)
    return outputs
  • 先是运用了一个卷积层(conv1d)作为 inner layer、一个卷积层作为 readout layer,卷积核巨细都为 1。
  • filters 参数用来操控卷积层中输出通道数量,inner layer 的输出通道数设置为 num_units[0] ,readout layer 的设置为 num_units[1]。有时也会把这个解说为神经元数量。这两个的默许别离为 2048、512,调用时传入的是超参数的 [4 * hidden_units, hidden_units]
  • 其间 inner layer 用 ReLU 作为激活函数,然后衔接一个残差网络 RedNet,把 readout layer 的输出加上原始的输入。
  • 终究运用 normalize 归一化处理输出,再回来。下面来看下 normalize 函数。
def normalize(inputs,
              epsilon = 1e-8,
              scope="ln",
              reuse=None):
    with tf.variable_scope(scope, reuse=reuse):
    	# 输入数据的形状
        inputs_shape = inputs.get_shape()
        params_shape = inputs_shape[-1:]
    	# 平均数、方差
        mean, variance = tf.nn.moments(inputs, [-1], keep_dims=True)
        # 拉伸因子 beta
        beta= tf.Variable(tf.zeros(params_shape))
        # 缩放因子 gamma
        gamma = tf.Variable(tf.ones(params_shape))
        # 归一化:加上一个十分小的 epsilon,是为了避免除以 0
        normalized = (inputs - mean) / ( (variance + epsilon) ** (.5) )
        outputs = gamma * normalized + beta
    return outputs
  • 该函数完成了 Layer Normalization,用于在深度神经网络中处理数据的不稳定性问题。
15.4.4、编码和解码完成后的操作

解码器后的 Linear & Softmax

# 全衔接层得到的未通过归一化的概率值
self.logits = tf.layers.dense(self.dec, len(en2idx))
# 猜测的英文单词 idx
self.preds = tf.to_int32(tf.arg_max(self.logits, dimension=-1))
self.istarget = tf.to_float(tf.not_equal(self.y, 0))
# 正确猜测数量,除以一切样本数,得到准确率
self.acc = tf.reduce_sum(tf.to_float(tf.equal(self.preds, self.y))*self.istarget)/ (tf.reduce_sum(self.istarget))
#  记录了模型的准确率的值,用于 tensorboard 可视化
tf.summary.scalar('acc', self.acc)

操练集数据处理时,通过 Linear & Softmax 之后的终究处理如下。这儿用到了 tf.nn.softmax_cross_entropy_with_logits 穿插熵丢掉,来核算模型的过错率 mean_loss,并运用 Adam 优化器 AdamOptimizer 来优化模型参数。

# 运用 label_smoothing 函数对真实标签进行标签滑润,得到 self.y_smoothed
self.y_smoothed = label_smoothing(tf.one_hot(self.y, depth=len(en2idx)))

下面这段代码完成了一种叫做「label Smoothing」的技巧。

def label_smoothing(inputs, epsilon=0.1):
	# 获取输入的类别数,并将其赋值给变量 K
    K = inputs.get_shape().as_list()[-1] # number of channels
    return ((1-epsilon) * inputs) + (epsilon / K)

在操练进程中,样本的标签被表明为一个二维矩阵,其间第一维表明样本的编号,第二维表明样本的标签。这个矩阵的形状便是 (样本数, 类别数),所以类别数对应的便是终究一维。详细到这个模型用例里,第一个维度是德语样本句子数,终究一维便是英语词汇量的巨细。

用于处理在操练模型时呈现的过拟合问题。在标签滑润中,咱们给每个样本的标签加上一些噪声,使得模型不能彻底依靠于样本的标签来进行操练,然后削减过拟合的或许性。详细来说,这段代码将输入的标签 inputs 乘上 1-epsilon,再加上 epsilon / K,其间 epsilon 是滑润因子,K 是标签类别数(英语词汇量巨细)。这样就能够在操练进程中让模型对标签的猜测更加平稳,而且下降过拟合的风险。

然后咱们看后续的操作。

# 关于分类问题来说,常用的丢掉函数是穿插熵丢掉
self.loss = tf.nn.softmax_cross_entropy_with_logits(logits=self.logits, labels=self.y_smoothed)
self.mean_loss = tf.reduce_sum(self.loss * self.istarget) / (tf.reduce_sum(self.istarget))
# Training Scheme
self.global_step = tf.Variable(0, name='global_step', trainable=False)
# Adam 优化器 self.optimizer,用于优化丢掉函数
self.optimizer = tf.train.AdamOptimizer(learning_rate=hp.lr, beta1=0.9, beta2=0.98, epsilon=1e-8)
# 运用优化器的 minimize() 函数创立一个操练操作 self.train_op,用于更新模型参数。这个函数会主动核算梯度并运用更新
self.train_op = self.optimizer.minimize(self.mean_loss, global_step=self.global_step)
# 将平均丢掉写入 TensorFlow 的 Summary 中,用于 tensorboard 可视化
tf.summary.scalar('mean_loss', self.mean_loss)
# 将一切的 summary 合并到一同,便利在操练进程中写入事件文件
self.merged = tf.summary.merge_all()

15.5、作用点评

def eval():
    # 创立一个处理测验数据集的 Graph 实例
    g = Graph(is_training=False)
    print("Graph loaded")
    # 加载测验数据
    X, Sources, Targets = load_test_data()
    de2idx, idx2de = load_de_vocab()
    en2idx, idx2en = load_en_vocab()
    # Start session         
    with g.graph.as_default():
    	# TensorFlow 顶用于办理操练的一个类
    	# 它能够帮助你轻松地办理操练进程中的各种资源,如模型参数、查看点和日志
        sv = tf.train.Supervisor()
        # 创立一个会话
        with sv.managed_session(config=tf.ConfigProto(allow_soft_placement=True)) as sess:
            # 恢复模型参数
            sv.saver.restore(sess, tf.train.latest_checkpoint(hp.logdir))
            print("Restored!")
            # 获取模型称号
            mname = open(hp.logdir + '/checkpoint', 'r').read().split('"')[1] # model name
            ## Inference
            if not os.path.exists('results'): os.mkdir('results')
            # 初始化成果文件
            with codecs.open("results/" + mname, "w", "utf-8") as fout:
                list_of_refs, hypotheses = [], []
                # 循环处理数据
                for i in range(len(X) // hp.batch_size):
                    # 获取小批量数据
                    x = X[i*hp.batch_size: (i+1)*hp.batch_size]
                    sources = Sources[i*hp.batch_size: (i+1)*hp.batch_size]
                    targets = Targets[i*hp.batch_size: (i+1)*hp.batch_size]
                    # 运用自回归推理(Autoregressive inference)得到猜测成果
                    preds = np.zeros((hp.batch_size, hp.maxlen), np.int32)
                    for j in range(hp.maxlen):
                        _preds = sess.run(g.preds, {g.x: x, g.y: preds})
                        preds[:, j] = _preds[:, j]
                    # 将猜测成果写入文件
                    for source, target, pred in zip(sources, targets, preds): # sentence-wise
                        got = " ".join(idx2en[idx] for idx in pred).split("</S>")[0].strip()
                        fout.write("- source: " + source +"\n")
                        fout.write("- expected: " + target + "\n")
                        fout.write("- got: " + got + "\n\n")
                        fout.flush()
                        # bleu score
                        ref = target.split()
                        hypothesis = got.split()
                        if len(ref) > 3 and len(hypothesis) > 3:
                            list_of_refs.append([ref])
                            hypotheses.append(hypothesis)
                # 核算 BLEU 分数,并将其写入文件
                score = corpus_bleu(list_of_refs, hypotheses)
                fout.write("Bleu Score = " + str(100*score))
if __name__ == '__main__':
    eval()
    print("Done")

第 16 节 Kyubyong Transformer 的功能体现和一些问题

评价成果文件的终究一行有 Bleu Score = 6.598452846670836 表明这个翻译模型的翻译成果与参阅翻译堆叠程度比较高,翻译质量较好。不过需求留意的是,BLEU 分数不能彻底反映翻译质量,由于它不能评价语法,语义,语调等方面的问题。

别的前面咱们在代码中现已将进程数据保存在 logdir 下了,便是为了后续便利可视化,咱们能够用 TensorBoard 来可视化,详细运用办法如下:

mikecaptain@local $ tensorboard --logdir logdir

然后在浏览器里查看 http://localhost:6006,示例如下:

人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型

回顾整个模型的代码完成,咱们能够更直观地看到这个 Transformer 能够较好地捕捉长间隔依靠联系,提高翻译质量。然而,Kyubyong Transformer 的完成存在一些问题。该 Transformer 模型在操练进程中还需求调整许多超参数,如学习率(learning rate)、batch size 等,不同的使命或许需求不同的超参数调整。

结束 Transformer 面世后的这些年

Transformer 的优势显而易见:

  • 更快 —— 并行性好:在 Transformer 诞生之前,RNN 是 NLP 范畴的干流模型,可是 RNN 并行性差(序列串行处理)。
  • 不健忘 —— 词间隔缩短为 1:RNN 模型处理长文本内容已丢掉(在 RNN 模型中意味着词的空间间隔长)。
  • 处理不同长度序列:不需求输入数据的序列是固定长度的。
  • 易于搬运学习。

因而依据 Transformer 原理的模型,在许多 NLP 使命中都取得了卓越的体现。

说到底机器学习(Machine Learning)范畴仍是一个试验科学,而且是离工业界极近的试验科学。机器学习看待试验成果的视点,不是为了拿试验成果总结笼统后推进理论科学开展。机器学习的试验成果是要被点评的,其作用有客观量化评价规范。所以机器学习,一切以成果说话。依据 Transformer 架构 Decoder 部分诞生了 OpenAI 的 GPT 大模型,依据其架构的 Encoder 部分诞生了 Google 的 BERT 大模型,他们两个都诞生于 2018 年。这几年依据 Transformer 的各种优化思维不断呈现,其集大成者便是 2022 年年末依据 GPT-3.5 或许说依据 InstructGPT 的 ChatGPT。

感谢你有耐性看完本篇近 8 万字长文,由所以船长的技能笔记,所以关于关键点整理得细致了些。后续,我和解咱们一同聊聊 AIGC 的当下,假如说本篇内容更像一个教程(对缘起技能的深化),那么后续咱们的探讨则或许更像一篇报告了(对眼前学界与业界开展现状的总述),咱们将更重视文章「前语」部分的两个议题:1)假如以为通过图灵测验代表着 AGI(Artificial General Intelligence,通用人工智能)的话,当下 NLP,甚至 AGI 开展到什么程度了?2)未来一些年内,AGI 的开展道路或许会是怎样的?

AI 终将颠覆各行各业,阿里人有责任花些时刻重视前沿的开展脉搏,欢迎咱们在钉钉或**微信(id:sinosuperman)**上与我沟通。

终究,船长给咱们拜个晚年,祝咱们兔年里健康高兴。

参阅

  • web.stanford.edu/~jurafsky/s…
  • ai.googleblog.com/2017/08/tra…
  • 《自然言语处理:依据预操练模型的办法》车万翔 等著
  • cs.stanford.edu/people/karp…
  • arxiv.org/abs/1706.03…
  • arxiv.org/abs/1512.03…
  • github.com/Kyubyong/tr…
  • jalammar.github.io/illustrated…
  • towardsdatascience.com/this-is-how…
  • 《自然言语处理实战:预操练模型运用及其产品化》安库A帕特尔 等著
  • lilianweng.github.io/posts/2018-…
  • github.com/lilianweng/…
  • 《依据深度学习的道路短期交通状况时空序列猜测》崔建勋 著
  • www.zhihu.com/question/32…
  • luweikxy.gitbook.io/machine-lea…
  • 《Python 深度学习(第 2 版)》弗朗索瓦肖莱 著
  • en.wikipedia.org/wiki/Attent…
  • zhuanlan.zhihu.com/p/410776234
  • www.tensorflow.org/tensorboard…
  • paperswithcode.com/method/mult…
  • zhuanlan.zhihu.com/p/48508221
  • www.joshbelanich.com/self-attent…
  • learning.rasa.com/transformer…
  • deeplearning.stanford.edu/tutorial/su…
  • zhuanlan.zhihu.com/p/352898810
  • towardsdatascience.com/beautifully…
  • medium.com/analytics-v…