文章目录
前言
一直在使用chatgpt,但是它到底是什么?人工智能怎么理解?神经网络怎么理解?
本文尽量用通俗易懂的语言解释这些,不涉及具体公式,主要是为了方便大家理解。
请先阅读前两篇文章,或者对神经网络有一定了解。
需要注意的是,本篇文章为 从chatgpt理解transformers(一),内容讲完了transformer架构中的编码器。
剩余部分会在后续的文章中继续梳理。
这篇文章很长,你如果想要搞懂,至少留出一下午的时间一口气读完,尽量不要分几次阅读。
一、chatgpt是什么。
让我们先从名字开始讲起
是一种大语言模型(大规模的预训练语言模型)。
全称是 生成式 预训练 模型(Generative Pre-trained Transformer,GPT)
名字中有三部分:
1.生成式:像CNN等判别式神经网络,根据输入判断是什么然后进行输出。而生成式神经网络,是根据输入,生成新的数据(文字或者图像)。就像AI绘图,可以根据你的描述生成完全没有存在过的新的图片或者视频。
2.预训练:神经网络中都有预训练这个过程,分为无监督预训练和有监督预训练。预训练的目的就是得到更加合理的神经网络中的各种参数。之后我应该会补充一期神经网络的基础知识汇总,再细讲,
3.transformer模型:是GPT的核心,第三部分会细讲。
需要提前补充一点:由于命名的原因,像是注意力机制等,导致很多人从CNN和RNN等其他神经网络到transformer的理解上有疑惑.
实际上,既然是神经网络,就都是分层结构,只是这里的命名不带“层”这个字了,在本篇文章中的第三部分会详细解释。
二、chatgpt到底是什么?
当我问你,你叫什么?
你脑子里会出现”我叫xxx“,然后再讲出来。
但如果我要问你,你从小到大的经历能给我讲一下吗?
你脑子里不可能会出现10000个字,然后再念出来。而是出现第一句话或者前几个字,讲出来,后面的话再边想边讲,
同时,在听到这个问题的时候,你脑子里大概有个模糊的"大纲",你要讲的总体的内容,大概是小学到大学之类的,并且你继续往下讲的时候,你逐渐会根据说话的内容,去不断产生后续的内容的想法。
而正常来说,后续的话都和前面的话有关联。就像你从小学开始讲,然后初中,然后大学。
而对于chatgpt来说,当你问他一个问题,你叫什么?
chatgpt的”脑子“里会先根据你的问题,跳出第一个字”我“,再根据你的问题和第一个字”我“,写出第二个字”叫“,然后再以此往后。后面的每个字都是根据你的问题和之前的字关联生出。
当你问一个比较复杂的问题,你从小到大的经历能给我讲一下吗?
chatgpt的”脑子“也是一个字一个字的蹦,和简单的问题的回答并无不同。
这个叫”自回归生成模型,意味着它通过逐词生成来构建文本。当输入一个句子或问题时,模型根据已知的上下文信息一步步生成下一个词,直到输出完整的答案或对话内容。“
从这里就体现出chatgpt这种大语言模型和人的区别:
首先是chatgpt是被动回答,你的小爱同学也罢,还是别的什么同学,不会突然唱起”啦啦啦啦“之类的,只有你让他唱他才会唱,因为它需要根据你前面的问题去蹦出一个个字,而人不一样,有各种主动的随机性的事件。
其次是chatgpt顾头不顾尾,也就是他回答问题时只根据前面的话去生成后续的字。
这里就显示出了chatgpt在写小说上有短板,你让他写八万字的小说,它没法像是人一样,写后面的章节时,不断修复前面的内容。
那chatgpt为什么能根据你问的问题进行一个字一个字的回答呢?
这里就需要其实现原理,也就是神秘的——Transformer神经网络。
三、Transformer神经网络
Transformer神经网络没有合适的中文名,所以很难像CNN“卷积神经网络”这样根据名字就了解内容和组成。
但无论如何,它始终是一种神经网络,我们之前两篇所讲的神经网络的知识就依然有效。
接下来我们对transformer的实现逻辑进行讲解,以帮助大家理解它。
一、transformer层次内容。
类比CNN,transformer的网络结构按照顺序整理如下。
Transformer
├── 输入层
│ └── 嵌入表示 (Embedding) + 位置编码 (Positional Encoding) [将输入转化为向量并加入位置信息]
├── 编码器 (Encoder) [由N个编码器层堆叠组成]
│ ├── 编码器层 (Encoder Layer)
│ │ ├── 子层1
│ │ │ ├── 多头注意力机制 (Multi-Head Attention) [全局关系建模]
│ │ │ ├── 残差连接 (Residual Connection) [稳定训练]
│ │ │ └── 层归一化 (Layer Normalization) [加速收敛]
│ │ ├── 子层2
│ │ │ ├── 前馈神经网络 (Feedforward Neural Network, FFN) [位置特征增强]
│ │ │ ├── 残差连接 (Residual Connection) [稳定训练]
│ │ │ └── 层归一化 (Layer Normalization) [加速收敛]
├── 解码器 (Decoder) [由N个解码器层堆叠组成]
│ ├── 解码器层 (Decoder Layer)
│ │ ├── 子层1
│ │ │ ├── 掩码多头注意力机制 (Masked Multi-Head Attention) [自回归解码]
│ │ │ ├── 残差连接 (Residual Connection) [稳定训练]
│ │ │ └── 层归一化 (Layer Normalization) [加速收敛]
│ │ ├── 子层2
│ │ │ ├── 编码器-解码器注意力机制 (Encoder-Decoder Attention) [结合编码器上下文]
│ │ │ ├── 残差连接 (Residual Connection) [稳定训练]
│ │ │ └── 层归一化 (Layer Normalization) [加速收敛]
│ │ ├── 子层3
│ │ │ ├── 前馈神经网络 (Feedforward Neural Network, FFN) [非线性变换]
│ │ │ ├── 残差连接 (Residual Connection) [稳定训练]
│ │ │ └── 层归一化 (Layer Normalization) [加速收敛]
├── 输出层
│ ├── 全连接层 (Fully Connected Layer) [映射到词表维度]
│ └── Softmax [生成概率分布]
├── 丢弃层 (Dropout) [嵌入层及每个子层后应用,防止过拟合]
二、输入层
要知道,所有的神经网络和人工智能本质上都是对数字和矩阵的操作。
对于上一章所说的CNN而言,输入的图像不需要做过多的处理,因为图像本身就是一个矩阵。
但是文字信息不同,文字信息并不是一个矩阵,无法直接进行后续的数学操作。
所以需要首先对其进行数字化,也就是需要将输入的文字转化为一个数字矩阵,这就是transfoemer输入层所需要完成的工作。
将一串文字转化为矩阵的过程,可以分为两大步:
1.嵌入层
2.位置编码
其中嵌入层的作用就是将文字(或者单词或者字符),对应的“词表”中,转化为数字。
比如说,“你叫什么?”,这一共五个字符,分别对应的“词表”中的“52”,“45”,‘’5“,”89“,”26“五个数字。
这个“词表”是预先就有的,将每个词语或者字与数字一一对应上。
每个字都转化为数字了,但是“52”,“45”,‘’5“,”89“,”26“,这五个数字总不能随便输入吧,要知道字的顺序如果不同,那句子的意思可就完全不同了。
于是,还要将每个数字的位置标注。
这就是输入层的第二个工作:位置编码。
ps:位置编码的计算其实并没有这么简单,再次为了说明意思简化了。
说着好像有些复杂,其实就是查表,然后将对应的数字依次放入数组中,得到了[52,45,5,89,26].
这个数组,就是后续处理的输入序列。
通过输入层的作用:将一句话转化为一个数组。
说明一下,这部分对输入层进行了简化,但是原理是一样的,为的是让初学者便于理解,否则一开始就讲一大堆编码计算之类的,大家就乱了。我之后会有文章详细解释。
三、编码器层
编码器的作用便是理解句子的内容。
类比CNN中的【卷积层+激活函数层+池化层】的组合层,一层层的不断提取图像特征并简化数据量。
transformer中的编码器层的目的,也是为了不断提取“语句特征”并简化数据量。
而编码器层,也是由两个子层给组成的:
├── 编码器 (Encoder) [由N个编码器层堆叠组成]
│ ├── 编码器层 (Encoder Layer)
│ │ ├── 子层1
│ │ │ ├── 多头注意力机制 (Multi-Head Attention) [全局关系建模]
│ │ │ ├── 残差连接 (Residual Connection) [稳定训练]
│ │ │ └── 层归一化 (Layer Normalization) [加速收敛]
│ │ ├── 子层2
│ │ │ ├── 前馈神经网络 (Feedforward Neural Network, FFN) [位置特征增强]
│ │ │ ├── 残差连接 (Residual Connection) [稳定训练]
│ │ │ └── 层归一化 (Layer Normalization) [加速收敛]
我们接下来依次进行讲解。
ps:对了,在很多讲解中提到了“堆叠”一词,实际就是多个编码器层串并联起来。
一、多头注意力机制 (Multi-Head Attention)
只要提到transformer,就会提到这个多头注意力机制,光这个名字就很难懂。
那我们就先不管这个名字,先明确输入输出,然后整清楚它是干啥的就行。
一、输入输出
输入
大家还记得上面的输入层吗?
输入层将我们输入的文字给转换成向量表达。
这个向量表达便是编码器层的输入,也就是多头注意力层的输入。
也就是形状是(batch_size, seq_len, d_model)的一个向量矩阵。
以“今天天气真好”为例。
模型的隐藏维度 d_model = 512(一个常见的设置)。
批量大小 batch_size = 1(单句输入)。
序列长度 seq_len = 4(“今天 天气 真 好”有4个词)。
那输入就是一个形状为(1, 4, 512)的向量:
[
[[1.1, -0.2, 1.29, ..., 0.51], // “今天”
[1.24, 0.64, 0.87, ..., 0.22], // “天气”
[0.61, 0.08, 1.14, ..., -0.07], // “真”
[0.86, -0.85, 1.30, ..., 0.34]] // “好”
]
这其中的每一个字,也就是对应在向量中的每一行,都是词嵌入 + 位置编码,这部分有不太懂的可以返回去看一下前一节的输入层。
输出
依然以“今天天气真好”为例。
输出也还是一个(1, 4, 512)的向量:
[
[[1.1, -0.2, 1.29, ..., 0.51], // “今天”
[1.24, 0.64, 0.87, ..., 0.22], // “天气”
[0.61, 0.08, 1.14, ..., -0.07], // “真”
[0.86, -0.85, 1.30, ..., 0.34]] // “好”
]
大家仔细看,虽然形式一样,向量大小形状一样,但是其中的每个数字却不一样。
这是因为输入时,每个词的每一行,只包含了词(token)的语义和位置信息,但是输出时,还包含了这个词(token)在上下文中的角色信息,也就是说这个词和别的词的关系。
上面这个说法依然很抽象,什么是词(token)与词(token)之间的“关系”呢?
其实就是当前这个token对这个句子每个token的关注度。
依然很抽象难以理解对吧,没关系,让我们继续举例。
依然以“今天天气真好”为例。
输入的词嵌入和位置编码:
“今天”: [1.1, -0.2, ..., 0.51]
“天气”: [1.24, 0.64, ..., 0.22]
“真”: [0.61, 0.08, ..., -0.07]
“好”: [0.86, -0.85, ..., 0.34]
计算每个词对其他词的关注权重。例如,“今天”可能分配的权重是:
关注“今天”本身:0.4
关注“天气”:0.3
关注“真”:0.2
关注“好”:0.1
然后用这些权重加权求和所有词的表示,生成新的“今天”表示:
新“今天” = 0.4 * [1.1, -0.2, ...] + 0.3 * [1.24, 0.64, ...] + 0.2 * [0.61, 0.08, ...] + 0.1 * [0.86, -0.85, ...]
最终得到了输出:
新“今天”: [0.9, 0.1, ..., 0.6]
新“天气”: [1.3, 0.7, ..., 0.3]
新“真”: [0.5, 0.2, ..., -0.1]
新“好”: [0.8, -0.6, ..., 0.4]
二、计算流程
清楚了输入输出了,那接下来我们便需要搞清楚如何从输入到输出。
首先让我们先看一下多头注意力机制中的简单的流程图:
原始输入 (X) → 线性变换 → 分割为多头 → Scaled Dot-Product Attention (每个头) → 拼接多头 → 线性变换 → 输出
现在肯定还看不懂这个流程图,没关系,只要时刻记得输入输出,我们开始一步步进行讲解。
一、原始输入
这个就是上面讲的输入,也就是那个(1, 4, 512)的向量。
二、线性变换
目的:从原始输入生成 Q、K、V。
首先讲一下这个 Q、K、V是什么。
查询(Q):决定模型关注什么。
键(K):提供可以被关注的候选。
值(V):提供实际的信息内容。
到了这部分,已经非常抽象难懂了,因为这三个量几乎没什么物理意义,定义和操作都完全基于线性代数和概率计算,但是我依然希望能给大家尽量讲的理解。
没有物理意义就很难理解这些是做什么的,这个是正常现象。
意思是什么呢,你看输入向量,里面包含了每个token的语义和位置编码,但都是混在一块的,所以需要先把他们拆开,便于后续的计算。
具体计算过程为:
Q = X * W_q
K = X * W_k
V = X * W_v
其中:
X 是原始输入张量。
W_q、W_k、W_v 是权重矩阵,形状为 (d_model, d_model),是可学习参数,也就是说会随着训练数据的变化而变化。
结果的Q、K、V这三个张量每个的形状和大小和原始张量相同,相当于变成了三个张量,将原始数据密集的内容给分开了。
例子:
假设原始输入“今天”的向量是 [1.1, -0.2, 1.29, …, 0.51](512维)。
经过 W_q 变换:
Q(“今天”) = [1.1, -0.2, …, 0.51] * W_q → [1.2, -0.1, 1.3, …, 0.55](仍为512维)。
同样,K(“今天”) 和 V(“今天”) 也会生成,但用不同的 W_k 和 W_v。
那这样子拆开后,得到的Q、K、V的意义是什么呢?
Q(查询,Query):
代表:模型当前想关注的内容或问题。
作用:Q 用于决定输入序列中哪个部分最相关。它是“提问者”,帮助模型找到与当前词相关的其他位置。
例子:对于“今天”,Q 可能表示“今天”想知道序列中哪些部分与它相关(如“天气”)。
K(键,Key):
代表:序列中每个位置可以提供的信息。
作用:K 是“候选答案”的索引,Q 会与 K 进行匹配,计算相似度,确定哪些位置值得关注。
例子:K 包含“今天”“天气”“真”“好”的所有表示,Q 会与这些 K 比较,找到最相关的部分。
V(值,Value):
代表:实际的信息内容。
作用:V 是当 K 被选中后,模型实际提取的信息。Q 和 K 确定了注意力权重后,V 提供具体的值来更新表示。
例子:如果“今天”关注“天气”,V 提供“天气”的详细表示,供“今天”融合。
关系:
Q 和 K 的点积(Q * K^T)计算相似度,决定关注权重。
权重与 V 相乘,生成新的表示。
三、分割为多头
到了这一步终于出现了 多头注意力机制 这个名字中的多头。
在上一步我们得到了Q、K、V 张量,而这一步,我们需要将其分解为多个并行的“头”(heads),每个头负责关注输入序列的不同部分或特征。
听着云里雾里的对吧,其实就是将输入的向量,给分成了几个小向量,分成几个小向量就被称为分成了几头。
还是以今天天气真好举例:
Q = [
[[1.2, -0.1, 1.3, ..., 0.55], // “今天”
[1.25, 0.65, 0.88, ..., 0.23], // “天气”
[0.62, 0.09, 1.15, ..., -0.06], // “真”
[0.87, -0.84, 1.31, ..., 0.35]] // “好”
]
设定头数(h) = 8(常见值)。
每个头的维度(d_k):通过公式计算:
d_k = d_model / h
例如,若 d_model = 512 且 h = 8,则 d_k = 512 / 8 = 64。
之前的Q、K、V 的形状为(1, 4, 512),分解后就成了(1, 4, 8, 64)。
也就是输出形状为(batch_size, seq_len, h, d_k)。
至于这么做的意义,是为了后续能够并行计算。
四、 Scaled Dot-Product Attention (缩放点注意力机制)
上面讲了多头是什么,这一节就讲多头注意力是什么。
这部分是多头注意力的核心部分。
先说一下这个名字的汉语翻译,我查了一下别的文章中也大多用的是这个缩放点注意力机制的说法,这个叫法晦涩难懂,所以大家先不要被这个名字所纠结,跟着我往下看它的工作。
大家是否还记得编码器层的输出,如果不记得话翻上去看一看。
编码器层最后的输出就是要获得每个token的上下文联系,也就是它和句子中其他各个token的关系权重。
本着这个目的,我们看一下Scaled Dot-Product Attention的工作流程图:
Q, K, V (每个头: (1, 4, 64)) →
计算点积 (Dot Product) →
缩放 (Scaling) →
Softmax 归一化 (Softmax Normalization) →
加权求和 (Weighted Sum) →
输出 (Output: (1, 4, 64))
一、输入
首先看我们的输入,就是上一步得到的其中一个head的数据,形状为(1, 4, 64)。
二、计算点积 (Dot Product)
含义:计算 Q 和 K 之间的相似度,生成一个得分矩阵,表示每个查询位置与每个键位置的相关性。
细节:
公式:scores = Q * K^T,其中 K^T 是 K 的转置。
输入:Q 形状 (1, 4, 64),K 形状 (1, 4, 64),转置后为 (1, 64, 4)。
输出:形状 (1, 4, 4),每个元素是 Q 中某位置与 K 中某位置的内积(点积)。
这一步是计算权重的核心操作。(再提醒一下权重就是每个词之间的关系)
让我们举个例子看一下结果大家就理解了:
上面那个:今天天气真好。在这一步的计算结果有可能是:
[
[[0.8, 0.6, 0.4, 0.3], // “今天”对各词的得分
[0.5, 0.9, 0.4, 0.2], // “天气”对各词的得分
...]
]
这个矩阵就是scores矩阵,也就是得分矩阵。
三、缩放 (Scaling)
含义:对点积结果进行缩放,防止数值过大导致 Softmax 输出过于集中,影响训练稳定性。
细节:
公式:scaled_scores = scores / sqrt(d_k)。
d_k 是每个头的维度(这里是64),缩放因子为 sqrt(64) = 8。
输出形状仍为 (1, 4, 4),但值被缩小。
四、Softmax 归一化 (Softmax Normalization)
含义:将缩放后的得分转换为概率分布,确保每个查询位置的注意力权重和为1。
细节:
公式:attention_weights = softmax(scaled_scores)。
对 scaled_scores 的最后一维(4)应用 Softmax,输出形状仍为 (1, 4, 4)。
例如,“今天”对“今天”“天气”“真”“好”的权重可能变为 [0.4, 0.3, 0.2, 0.1]。
五、加权求和 (Weighted Sum)
含义:用归一化后的注意力权重加权 V,生成最终的输出,表示每个查询位置的上下文增强表示。
细节:
公式:output = attention_weights * V。
attention_weights 形状 (1, 4, 4),V 形状 (1, 4, 64)。
通过矩阵乘法或广播,输出形状为 (1, 4, 64)。
例如,“今天”的新表示是其权重与所有 V 向量的加权和。
六、输出 (Output: (1, 4, 64))
含义:Scaled Dot-Product Attention 的最终输出,表示每个位置的上下文增强表示,准备好传递给下一阶段(拼接多头)。
细节:
形状:(batch_size, seq_len, d_k) = (1, 4, 64)。
内容:每个位置的64维向量,融合了序列中所有位置的信息。
七、总结
上面的六步是其中一个head矩阵的计算流程,而在实际运行过程中,8个头是同时并行运行的,这便是多头注意力机制的含义,以算力代替时间。
这步结束,就已经计算出了多头注意力机制中的【权重】,不过是每个头的权重,最后给整合在一起进行输出。
现在提问大家,这个【权重】是如何计算出来的呢?
答不上来的就需要反思一下自己是不是光看文字了。
五、 拼接多头
上面的那一步,是分为几个head进行了权重计算,这一步就是将几个头合并,恢复到之前的维度,按照我们的例子来说,也就是恢复到(1, 4, 512)的维度。
这一步没什么需要讲的,就是合并信息,恢复维度。
六、 线性变换
线性变换的目的是将上面拼接多头后的输出调整为一个更优化的的表示。
公式:
output = concatenated * W_o + b_o
其中:
concatenated 是拼接多头的输出,形状为 (batch_size, seq_len, h * d_k)(比如 (1, 4, 512))。
W_o 是线性变换的权重矩阵,是可学习的,形状为 (h * d_k, d_model)(比如 (512, 512))。
b_o 是偏置向量(可选),是可学习的,形状为 (d_model,)(比如 (512,))。
输出形状为 (batch_size, seq_len, d_model)(比如 (1, 4, 512))。
例子:
拼接多头输出:
“今天”: [0.9, 0.2, ..., 0.7, 0.8, -0.1, ..., 0.4] (512维)
线性变换后:
“今天”: [1.2, -0.3, 1.5, ..., 0.8] (512维)
也就是数值发生了变化,这部分类比卷积神经网络中的相关内容,总之目的就是为了让数值区分明显,突出特征。
七、输出
输出就是上面线性变换的输出
三、总结
到这里为止,transformer中特有的,且至关重要的多头注意力机制就算讲完了,主要内容就是分为多头,并行计算。
—分割线,下面的过两天有空了再写,要不这部分太他妈多了—
二、前馈神经网络 (Feedforward Neural Network, FFN)
三、残差连接 (Residual Connection)
四、层归一化 (Layer Normalization)
四、解码器层
编码器的作用便是编写输出的句子内容。
五、输出层
总结
为了便于理解,将其中一些内容简单化了,之后我会逐步在接下来的文章中详细讲解。

被折叠的 条评论
为什么被折叠?



