非常个人的仅用于记录和备份的学习笔记,所以在学习资料的过程中没有整理 ref【磕头】
理解的侧重点在于工程实现,一些结果和理论的推导过程不会深究。
经典的内容常看常新,需要经常回顾复习以获得新的思考
构建逻辑框架后应该及时的将新学内容从基础进行联想和补充
从 Transformer 的组成出发,将内容分为几个子模块进行学习:
- Embedding
- Positional Encoding 位置编码
- Self-attention 自注意力机制
- Feed Forward 前馈神经网络
- Add & Norm 残差连接 & 归一化
- Transformer 整体描述
Embedding
无论输入的是 文字、图像还是音频,它们实际上都是非结构化的数据信息,是不能直接被计算的,因此需要通过一个映射方式进行信息的转换。
Subword Tokenization 子词分词法
Tokenization 构成
- Trainer 分词训练器:
- 封装分词算法
- 给定训练数据生成词表
- Vocabulary 词表
- 作为分词的标准
- 一般每个任务都会维护一个词表
- 可以根据当前训练数据生成,也可以直接加载训练好的词表
- Encoder 编码器
- 把语料按照词表拆分 token,并记录 token 在词表中的索引
- 使用索引,将 token 表示成 one-hot 向量
- one-hot 编码:对于每一个分类变量,都会为其分配一个唯一的二进制位,并使用该位来表示该变量的取值
- Deocoder 解码器
- 给定索引,将索引还原成最初的语料形式
Tokenization 类别
- Word-level 基于空格的分词器
- 将一个单词作为 token 纳入词表
- OOV(Out of vocabulary):语料中出现不在词表中的 token
- 大词表问题
- 罕见词难表示
- 词汇冗余
- Character-level 基于字符的分词器
- 单个字符本身缺少语义
- 输入序列变长
- Subword-level 基于子词的分词器
- 类似于借助词根词源来学习一系列单词,但不是基于语言学的,而是基于统计意义的。只是经过大量数据训练后,学习到了语言学上的含义
- 常见的子词分词器
- BPE(Byte Pair Encoding)
- WordPiece
- ULM
由原文可知,input_embedding 的形状应该是 (N 序列长度, D 序列中每个向量的维度)
无论是什么格式的输入,他们最终都要转化到一个二维矩阵进行表示,这个过程中会涉及不同方式的特征获取和转化方式,这不是 transformer 学习的重点,以下只是笔记了一个简单流程,并不是标准的 workflow:
文本
-
文本序列 -> token
- 文本序列会首先通过如 BPE(Byte Pair Encoding) 或 WordPiece 等方法进行 token 化
- 每个英文单词可能被编码为 1-2 个 tokens,汉字通常为 1-3 个 tokens
- 每个 token 有一个唯一的数字 id
-
token -> input_embedding
- token id 通过查表映射方式获得 embedding 矩阵
- 例如:一个 input_embedding 为 12288x4096,那么 12288 是输入的 token 个数,4096 是 embedding 的 dimensions
图像
- 图像 -> 固定大小的小图像块
- 原始输入图像会被切割成一系列固定大小的图像块,通常是 16x16 像素的小块,每个小块可以看作是一个图像 token
- 数量:image_H x image_W / (Patch x Patch)
- 维度:Patch x Patch x channels
- 图像块 -> 向量
- 每个图像小块进行到一维向量的展平
- 压缩维度从 Patch x Patch x channels 到 dimensions,这个过程中可以进行简单的维度压缩、也可以进行一些线性变化、也可以进行卷积等特征提取
音频
音频拥有天然的二维处理逻辑,音频信号经过 STFT 或者 MFCC 提取特征后的结果通常就已经是 时间-频率 二维矩阵 的表示了,每一行表示的是对应时间帧的特征。
对于音频的 token 获取,对原始音频进行切段即可。
Positional Encoding 位置编码
概念
如架构图所示的: attention_input = input_embedding + positional_encoding
- input_embedding: 通过 embedding 层,将每一个 token 的向量维度从 vocab_size 映射到 d_model
- positional_encoding: 由于进行的是向量加法,所以自然的, positional_encoding 也应该是一个 d_model 维度的向量
为什么需要一个参数表示位置信息呢? 因为在后续的 self-attention 的运算是无向的:即模型对 tokens 的位置信息是无法分辨的(绝对位置、相对位置、位置距离等)。
方法演变
整型值标记
类数组 index 索引
- 推理时可能会出现比训练时更长的序列,不利于模型的泛化
- 位置的表示会随着序列长度的增加而无界的增加
[0,1] 范围标记
相较于整型标记,将位置信息进行在 [0,1] 的范围的限制
- 映射到固定范围意味着,对于不同的序列长度会产生不同的相对距离
回到前面对分辨位置信息的需求,明确模型需要感知的有:绝对位置、相对位置、位置距离,那么对应的对位置表示方法的需求可以具象为:
- 能表示 token 在序列中的绝对位置
- 在序列长度不同的情况下,不同序列中的 token 的相对距离应该保持一致
- 对训练中没有出现的序列长度同样能进行表示
二进制向量标记:
使用和 input_embedding 维度一样的向量表示位置
(图示假设 d_model = 3)
由于 demensions 维度通常会比较大,所以基本上可以满足位置编码的需求
- 但由于是二进制的表示方式,所以这种表示方式天然的是离散的,不同位置之间的变化是不连续的(在二维坐标系中是间隔为 1 的点阵表示)
sin 标记:
从二进制表示具象需求的,我们需要一个更加连续的表示方式,可以考虑正弦函数 sin:
- 每个 token 都是向量唯一的(因为 sin 函数的频率能设置的足够小)
- 向量是有届且连续的
这样还差一个相对位置的运算需求:让相对位置的计算能通过线性转换实现
sin+cos 标记:
类似 FFT 的推理过程,使用由 sin 和 cos 让 embedding 两两一组进行表示,那么相对位置的计算就能变成和一个由 sin cos 构造出来的旋转矩阵进行线性变化获得。
Sinusoidal functions
其实就是 sin + cos
Self-attention 自注意力机制
Attention 基本流程
对于每个 token,会有 3 个向量:
- query:询问
- key:索引
- value:回答
图中以 a2 为例:
- a2 会产生一个 query
- query 会和所有 token 的 key 进行一次计算,这个计算结果称为 attention score(也会称为 attention weight)
- attention score 进一步与对应的 value 进行乘法,从而获得新的向量
- 向量相加,即作为 a2 通过 attention 后的结果 b2
Query、Key 与 Value
从 shape 的角度进行理解:
- I(input): (seq_len, d_model)
- Wq: (d_model, k_dim)
- Wk: (d_model, k_dim)
- Wv: (d_model, k_dim)
- Q = I * Wq: (seq_len, k_dim)
- K = I * Wk: (seq_len, k_dim)
- V = I * Wv: (seq_len, v_dim)
- A = Q * KT = (seq_len, k_dim) * (k_dim, seq_len) = (seq_len, seq_len)
- O(output) = A' * V: (seq_len, seq_len) * (seq_len, v_dim) = (seq_len, v_dim)
Attention Score
Attention 的大致流程已经理解了,可以分为两步:
- 利用 Q 和 K 计算 attention score 矩阵
- 利用 attention score 和 V 计算出最终结果
这里开始具体描述一下 attention score 的计算方式,方法有很多种,其中 transformer 提供的原始方法是 dot product
Dot product
- 左:Dot-product
- 右:Additive-product
- Scaled dot product:在 dot-product 的基础上乘上因子 1/sqrt(d_k)
- scaling 的意义在于能使得 softmax 过程中的梯度下降更加稳定,同时避免因为梯度过小造成模型参数更新停滞
Masked Attention
attetion score 的计算中,q 是对所有的 k 进行查询的,这意味着 token 看到的是整个序列,那么明显的,在一些只根据前序内容进行的生成任务中,我们需要将右边的序列进行 mask。
mask 的实现其实不改变 attention score 根本的计算,而只是在 attention score 计算完成后补充一个 mask 矩阵:( 0 和 1 的表示并不固定,具体实现中可能会出现差异)
- 元素为 1 表示 mask,那么可以将其替换为一个极小值 -> 后续会经过 softmax
- 元素为 0 表示保留原始数值
mask 矩阵的 shape 为 (seq_len, seq_len),所以很好理解的,这是一个非常裸的遍历处理方式。
Multihead Attention
Multihead 的意义在于能将 input 变化到更多的子空间进行表示,能够捕获更丰富的特征信息(但似乎可解释性工作还没完成 hhh)
- Multihead 本质上是训练 num_heads 个 Wq, Wk, Wv 矩阵,以产生 num_heads 个 Q、K、V 结果
- attention 的整体计算逻辑并没有发生改变,只是需要对最终生成的结果 b 进行一次连接
- 添加 Wo 矩阵,用于对 multihead 进行结果的拼接
- shape视角:
- k_dim = v_dim = d_model // num_heads
- Wq: (d_model, d_model // num_heads)
- Wk: (d_model, d_model // num_heads)
- Wv: (d_model, d_model // num_heads)
- Wo: (num_heads * v_dim, v_dim)
- Z = Z' * Wo = (seq_len, num_heads * v_dim) * (num_heads * v_dim, v_dim) = (seq_len, v_dim)
Feed Forward 前馈神经网络
FFN 之前是 MHA 模块
- MHA 是没有激活函数的,这意味着 MHA 会输出一些极为相似的结果
- FFN 可以提供非线性的变化,以提供更好的特征表达
Add & Norm 残差连接 & 归一化
Add 残差连接
- Add: residule block 残差模块
- 残差连接:将网络的输入与输出相加
这是因为当网络结构比较深的时候,反向传播容易出现梯度消失的问题,但如果每层的输出都加上输入(输入的求导结果为1),相当于每一层的求导会加上一个常数1,从而能有效解决梯度消失的问题。
Norm 归一化
Normalization 的方法有很多:
- Batch Normalization
- Layer Normalization
- Group Normalization
- Instance Normalization
但方法的差异在于处理维度的不同,而不是 norm 运算本身。
norm 的运算目的还是不变的:把输入转化为均值为 0,方差为 1 的数据
Normalization 的意义在于能损失一部分不重要的信息,从而降低拟合难度和过拟合风险,进而加速模型收敛速度。
Transformer 整体描述
上面已经将 transformer 中每个模块进行的操作进行了一定的描述,现在还差对模块之间的连接和运行机制进行理解。
如图所示的,Transformer 由 N 个 Encoder 层和 N 个 Decoder 层组成
- Encoder
- inputs 经过 embedding 获取 input_embedding
- 经过 positional encoding 获得 input_positional
- input = input_embedding + input_positional
- Multi-Head self-attention
- Add & Norm
- postion-wise feed-forward network
- Add & Norm
- Decoder
- 输入是 outputs (通常会通过添加一个表示开始的字符实现 shifted right)
- input = input_embedding + input_positional
- Masked Multi-Head Attention
- Add & Norm
- Cross Multi-Head Attention
- 传统 transformer 结构中的实现为:第 N 个 Encoder 层最后的输出与每一层 Decoder 进行交互(也有其他的 cross 方式)
- Q: Decoder 中上一个 Layer 的输出
- K、V:Encoder
- Add & Norm
- Feed Forward
- Add & Norm