admin

  • 在整个大语言模型学习之路中,对 Attention 机制的理解大概是最为让我困惑的部分,最终经过层层解构、加上重新把“线性代数”温习了一遍之后,最终,总算某种程度的理解了 Attention 机制的设计。相信对于所有NLP专业的人,这部分都是不太容易理解的。

    1. 概述

    要想讲清楚,大概也是非常不容易的,这里就做一个尝试吧。这里的重点是讲清楚 Attention Score (简称Attention)的计算。介绍的顺序是“两个词语的相似度”、“Similarity Score Matrix”、“Attention Score Matrix”。

    1.1 要构建的是直觉,而不是“推理”

    为什么 Attention 理解起来很难呢?我想其中有一个原因大概是这个“机制”本身并不是某种“公式推导”出来的,而是通过一篇篇论文与实践,被证明非常有效的一个机制,所以,这个机制本身的所具备的“可解释性”其实也是有限的。这大概也是,无论你在互联网上如何搜索,也没有谁可以比较简单的把这个机制说清楚的原因。但,理解这个机制构建的直觉,对于理解整个 Transformer ,以及整个当代大语言模型技术基础都是至关重要的。

    2. 预处理

    在“大语言模型的输入:tokenize”中详细介绍了“提示词”进入大模型处理之前,如何将提示词换行成大模型可以处理的“词向量”或者说“token embedding”。

    大语言模型在开始“Attention”计算之前,还会对“token embedding”进行一些预处理,这些预处理包括了“融入”位置向量、对向量进行“归一化”处理(将各个向量都转化为均值为0、方差为1的向量,长度要统一变成1吗?)。

    例如,在这里的例子中,提示词 “It’s very hot in summer. Swimming is”,先转换为embedding,然后加上位置编码(positional encoder)、再进行正规化,最后变换为如下的向量 “ X ” :

    这里的 “X” 是一个由12个 “token embedding”组成的矩阵,“形状”是 12 x 768 。在数学符号上,有:

    3. Similarity Score Matrix

    在正式介绍 Attention 之前,为了能够比较好的理解“为什么”是这样,这里先引入了“Similarity”的概念,最终在该概念上,新增权重矩阵,就是最终的 Attention :

    $$ \text{Similarity} = \text{softmax}(\frac{XX^{T}}{\sqrt{d}})X $$

    这里删除了参数矩阵:\(W^Q \quad W^K \quad W^V \)

    $$ \text{Attention} = \text{softmax}(\frac{QK^{T}}{\sqrt{d}})V $$

    其中, \(Q = XW^Q \quad K = XW^K \quad V = XW^V \)

    3.1 两个“词语”的相似度

    在向量为单位长度的时候,通常可以直接使用“内积”作为两个向量的相似度度量。例如,考虑词语 “hot” 与 “summer” 的相似度,则可以“简化”的处理这两个词(Token)的向量的“内积”。

    在前面的文章“大语言模型的输入:tokenize”中,较为详细的介绍了大语言模型如何把一个句子转换成一个的 Token,然后再转换为一个个“向量”。那么,我们通常会通过两个向量的余弦相似度来描述其相似度,如果向量的“长度”(\(L_2 \) 范数)是单位长度,那么也通常会直接使用“内积”描述两个向量的相似度:

    $$
    \cos \theta = \frac{\alpha \cdot \beta}{\|\alpha\| \|\beta\| }
    $$

    \(f(x) = \cos(x) \) 的图像如右图,故:

    • 夹角为 0 时,最为相似,这时候 \(\cos(x) = 1 \)
    • 夹角 \(\pi \) 时,最“不”相似,这时候 \(\cos(x) = 0 \)

    例如,

    3.2 “Similarity Score Matrix”

    因为两个向量的“内积”某种程度可以表示为相似度。那么,对于句子中的某个 token 来说,与其他所有向量各自计算“内积”,就可以获得一个与其他所有向量“相似程度”的数组,再对这个数组进行 softmax 计算就可以获得一个该 token 与其他所有向量“相似程度”的归一化数组。这个归一化的数据,就可以理解为这里的“Similarity Score Matrix”。

    这里依旧以“大语言模型的输入:tokenize”示例中的句子为演示示例。

    更为具体的,可以参考右图。这里考虑 Token “It” 与其他所有词语的相似度。即计算 Token “It” 的 Embedding 向量,与其他所有向量的“内积”。

    更进一步,如果计算两两词语之间的相似度,并进行归一化(softmax ),则有如下的Similarity Matrix:

    在这个示例中,则会有上述 12×12 的矩阵。该矩阵反应了“词”与“词”之间的相似度。如果,我们把每一行再进行一个“归一化”(注右图已经经过了归一化),那么每一行,就反应了一个词语与其他所有词语相似程度的一个度量。

    例如,右图中 it 可能与 very 最为相似(除了自身)。

    4. Self-Attention

    4.1 对比

    注意到最终的 “Attention” 计算公式和上述的“Similarity Score Matrix”的差别就是参数矩阵W:

    $$ \text{Similarity Score} = \text{softmax}(\frac{XX^{T}}{\sqrt{d}})X $$

    这里没有参数矩阵:\(W^Q \quad W^K \quad W^V \)

    $$ \text{Attention} = \text{softmax}(\frac{QK^{T}}{\sqrt{d}})V $$

    其中, \(Q = XW^Q \quad K = XW^K \quad V = XW^V \)

    4.2 为什么需要参数矩阵 W

    那么,为什么需要 \(W^Q \,, W^K \,, W^V \) 呢?这三个参数矩阵乘法,意味着什么呢?要说清楚、要理解这个点并不容易,也没有什么简单的描述可以说清楚的,这也大概是为什么,对于非 NLP 专业的人,要想真正理解 Transformer 或 Attention 是比较困难的。

    你可能会看到过一种比较普遍的、简化版本,大概是说 \(W^Q \) 是一个 Query 矩阵,表示要查询什么;\(W^K \) 是一个 Key 矩阵,表示一个词有什么。这个说法似乎并不能增加对上述公式的理解。

    那么,一个向量乘以一个矩阵时,这个“矩阵”意味着什么?是的,就是“线性变换”。

    4.3 线性变换 Linear Transformations

    一般来说,\(W^Q \,, W^K \,, W^V \) 是一个 \(d \times d \) 的矩阵[1]。对于的 Token Embedding (上述的矩阵 X )所在的向量空间,那么 \(W^Q \,, W^K \,, W^V \) 就是该向量空间的三个“线性变换”。

    那么线性变换对于向量空间的作用是什么呢?这里我们以“奇异值分解”的角度来理解这个问题[2],即对向量进行拉伸/压缩、旋转、镜像变换。\(W^Q \,, W^K \,, W^V \) 则会分别对向量空间的向量(即Token Embedding)做类似的变换。变换的结果即为:

    $$ Q = XW^Q \quad \quad K = XW^K \quad \quad V = XW^V $$

    那么,如果参数矩阵“设计”合理,“Token”与“Token”之间就可以建立“期望”的 Attention 关系,例如:“代词”(it),总是更多的关注于“名词”;“名词”更多的关注与附近的“形容词”;再比如,“动词”更多关注前后的“名词”等。除了词性,线性变换关注的“维度”可能有很多,例如“位置”、“情感”、“动物”、“植物”、“积极/消极”等。关于如何理解 token embedding 的各个“维度”含义可以参考:Word Embedding 的可解释性探索

    当然,这三个参数矩阵都不是“设计”出来的,而是“训练”出来的。所以,要想寻找上述如此清晰的“可解释性”并不容易。2019年的论文《What Does BERT Look At? An Analysis of BERT’s Attention》较为系统的讨论了这个问题,感兴趣的可以去看看。

    关于线性变换如何作用在向量空间上,可以参考:线性代数、奇异值分解–深度学习的数学基础

    所以,\( \frac{QK^T}{\sqrt{d}} = \frac{XW^Q (XW^K)^T}{\sqrt{d}} \) 则可以系统的表示,每个“Token”对于其他“Token”的关注程度(即pay attention的程度)。可以注意到:

    • 增加了参数矩阵\(W^Q \,, W^K \,, W^V \)后,前面的“相似性”矩阵,就变为“注意力”矩阵
    • “Token” 之间的关注程度不是对称的。例如 Token A 可能很关注 B;但是 B 可能并不关注 A
    • 这里的 \(\sqrt{d} \) 根据论文描述,可以提升计算性能;

    如果,你恰好理解了上面所有的描述,大概会有点失望的。就只能到这儿吗?似乎就只能到这里了。如果,你有更深刻的理解,欢迎留言讨论。

    接下里,我们来看看 “Attention Score Matrix” 的计算。

    4.4 Attention Score Matrix

    使用上述的 “Similarity Score Matrix” 的计算方式,可以计算类似的 “Attention Score Matrix”,之后再对该矩阵进行 softmax 计算就可以获得每个词语对于其他所有词语的 Attention Score,或者叫“关注程度”。有了这个关注程度,再乘以 V 矩阵,原来的 Token Embedding 就变换为一个新的带有上下文的含义的 Token Eembedding 了,即 Context Embedding[3]

    类似的,我们有右图的 Attention Score Matrix 计算。

    该矩阵反应了两个 Token 之间的 Attention 关系。该关系,通过对经过线性变换的 Token Embedding ,再进行内积计算获得。

    4.5 Masked Attention Score Matrix

    上述计算,是一个典型的 Self Attention 计算过程,BERT 模型就使用类似的计算,但 GPT 模型(或者叫 Decoder 模型)还有一些不同。GPT 模型中为了训练出更好的从现有 Token 中生成新 Token 的模型,将上述的 Self Attention 更改成了 Masked Self Attention ,即将 Attention Score Matrix 的右上角部分全部置为 -inf (即负无穷),后续经过 softmax 之后这些值都会变成零,即,在该类模型下,一个词语对于其后面的词的关注度为 0 。

    在 Decoder 模型设计中,为了生成更准确的下一个 Token 所以在训练和推理中,仅会计算Token 对之前的 Token 的 Attention ,所以上述的矩阵的右上角部分就会被遮盖,即就是右侧的 “Masked Attention Score Matrix”。

    通常为了快速计算对于上述的计算值会除以 \(\sqrt{d} \) ,可以提升计算的效率。

    4.6 归一化(softmax) Attention Score

    对于上述矩阵的每一行都进行一个 softmax 计算,就可以获得一个归一化的按照百分比分配的Attention Score。

    经过归一化之后,每个词语对于其他词语的 Attention 程度都可以使用百分比表达处理。例如,“summer”对于“It”的关注程度最高,为26%;其次是关注“hot”,为15%。可以看到这一组线性变换(\(W^Q\,W^K \))对于第一个位置表达特别的关注。

    4.7 Contextual Embeddings

    最后,再按照上述的 Attention Matrix 的比例,将各个 Token Embedding 进行一个“加权平均计算”。

    例如,上述的加权计算时,“summer” 则会融入 26% 的“It”,15%的“hot”… ,最后生成新的 “summer” 的表达,这个表达也可以某种程度理解为 “Contextual Embeddings”。需要注意的是,这里在计算加权平均,也不是直接使用原始的 Token Embedding ,也是一个经过了线性变换的Embedding,该线性变换矩阵也是经过训练而来的,即矩阵 \(W^V \)。

    例如,上述的加权计算时,“summer” 则会融入 26% 的“It”,15%的“hot”… ,最后生成新的 “summer” 的表达,这个表达也可以某种程度理解为 “Contextual Embeddings”。

    需要注意的是,这里在计算加权平均,也不是直接使用原始的 Token Embedding ,也是一个经过了线性变换的Embedding,该线性变换矩阵也是经过训练而来的,即矩阵 \(W^V \)。

    5 计算示意图

    如下的示意图,一定的可视化的表达了,一个 Token 如何经过上述的矩阵运算,如何了其他 Token 的内容。

    6. 注意力矩阵的观察

    那么,我们给定于如下的提示词输入:“Martin’s one of my sons, and the other is Chanler.”。看看在 GPT 模型中,各个Token之间的 Attention 情况:

    • 这句话总计有21个token,所以这是一个21×21的矩阵
    • 这里是“masked self-attention”,所以矩阵的右上半区都是 “0” 。
    • 在GPT2中,一共12层,每层12个“头”,所以一共有“144”个类似的矩阵
    • \(W^Q \,, W^K \,, W^V \) 的维度都是768×64,所以粗略的估计这部分的参数量就超过2000万,具体的:

    768*64*3*144 = 21,233,664

    7. Multi-Head Attention

    7.1 Scaled Dot-Product Attention

    以前面小结“预处理”中的 X 为例,Attention Score Matrix 就有如下的计算公式:

    $$
    \begin{aligned}
    \text{Attention Score Matrix} & = \text{softmax}(\frac{QK^T}{\sqrt{d}}) \\
    & = \text{softmax}(\frac{XW^Q(XW^K)^T}{\sqrt{d}})
    \end{aligned}
    $$

    最终的 Attention 计算如下:

    $$
    \begin{aligned}
    \text{Attention}(Q,K,V) & = \text{softmax}(\frac{QK^T}{\sqrt{d}})V \\
    & = \text{softmax}(\frac{XW^Q(XW^K)^T}{\sqrt{d}})XW^V
    \end{aligned}
    $$

    7.2 Multi-Head Attention

    上述的“Attention”在论文“Attention Is All You Need”称为“Scaled Dot-Product Attention”。更进一步的在论文中提出了“Multi-Head Attention”(经常被缩写为“MHA”)。对应的公式如下(来自原始论文):

    $$
    \begin{aligned}
    \text{MultiHead}(Q, K, V) & = \text{Concat}(\text{head}_1, \dots, \text{head}_h)W^O \\
    \text{where} \quad \text{head}_i & = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)
    \end{aligned}
    $$

    更完整的解释,可以参考原始论文。这里依旧以前文的示例来说明什么是MHA。

    在本文第4章“Self-Attention”中,较为详细的介绍了相关的计算(即模型的推理过程)。在示例中,一共有12个“Token”,在进入 Attention 计算时经过位置编码、正则化后,12个“Token”向量组成矩阵“X”,这里的“X”的 shape 为 12 x 768,通常使用符号 \(l \times d \) 或者 \(l \times d_{model} \) 表示。最终输出的 Contextual Embedding 也是 \(l \times d_{model} \) 的一组表示12个 Token 向量,这是每个向量相比最初的输入向量,则融合上下文中其他词语的含义。在一个多层的模型中,这组向量则可以作为下一层的输入。

    在“Multi-Head Attention”其输入、输出与“Self-Attention”一样,都是 \(l \times d_{model} \) 。但是,对于最终输出的 \(l \times d_{model} \) 的向量/矩阵,在 MHA 中则分为多个 HEAD 各自计算其中的一部分,例如,一共有 \(d_{model} \) 列,那么则分别有 \(h \) 个HEAD,每个 HEAD 输出其中的 \(\frac{d_{model}}{h} \) 列。在上述示例中,供有12个HEAD,即 \(h=12 \),模型维度为768,即\(d_{model} = 768 \),所以每个HEAD,最终输出 \(64 = \frac{d_model}{h} = \frac{768}{12} \) 列。即:

    $$
    \begin{aligned}
    \text{where} \quad \text{head}_i & = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)
    \end{aligned}
    $$

    然后由12个 Head 共同组成(concat)要输出的 Contextual Embedding,并对此输出做了一个线性变换\(W^O \)。即:

    $$
    \begin{aligned}
    \text{MultiHead}(Q, K, V) & = \text{Concat}(\text{head}_1, \dots, \text{head}_h)W^O
    \end{aligned}
    $$

    7.3 Self Attention vs MHA

    Self Attention


    Multi Head Attention


    7.4 Multi-Head Attention 小结

    • “Multi-Head Attention” 与 经典“Attention” 有着类似的效果,但是有着更好的表现性能
    • “Multi-Head Attention” 与 经典“Attention” 有相同的输入,相同的输出

    8. Attention 数学计算示意图

    如下的图片,半可视化的展示了在GTP2中,某一个HEAD中Attention的计算。

    9. 全流程数学计算

    完整的计算,就是一个“forward propagation”或者叫“inference”的过程,这里依旧以上述的提示词“It’s very hot in summer. Swimming is”,并观察该提示词在 GPT2 模型中的第一个Layer、第一个Head中的计算。完成的代码可以参考:Attention-Please.ipynb

    9.1 Token Embedding 和 Positional Embedding

    Token Embedding

    +

    Positional Embedding

    9.2 Normalize

    即,将每一个token的embedding 进行正规化,将其均值变为0,方差变为1

    9.3 Attention 层的参数矩阵

    \(W^Q\,,W^K\,,W^V \)

    9.4 矩阵 Q K V的计算

    \(Q = XW^Q \)

    \(K = XW^K \)

    \(V = XW^V \)

    9.5 计算 Attention Score

    \(\text{Attention Score} \)

    \(= \frac{QK^T}{\sqrt{d}} \)

    9.6 计算 Masked Attention Score

    \(\text{Masked Attention Score} \)

    \(= \frac{QK^T}{\sqrt{d}} + \text{mask} \)

    9.7 计算 Softmax Masked Attention Score

    \(\text{Softmax Masked Attention Score} \)

    \(= \text{softmax}(\frac{QK^T}{\sqrt{d}} + \text{mask}) \)

    9.8 计算 Contextual Embeddings

    \(\text{Contextual Embeddings} \)

    \(= \text{softmax}(\frac{QK^T}{\sqrt{d}} + \text{mask})V \)

    10. 其他

    上述流程详述了 LLM 模型中 Attention 计算的核心部分。也有一些细节是省略了的,例如,

    • 在GPT2中,“线性变换”是有一个“截距”(Bias)的,所以也可以称为一个“仿射变换”,即在一个线性变换基础上,再进行一次平移;
    • 在GTP2中,Attention计算都是多层、多头的,本文主要以Layer 0 / Head 0 为例进行介绍;
    • 在生成最终的“Contextual Embeddings”之前,通常还需要一个MLP层(全连接的前馈神经网络)等,本文为了连贯性,忽略了该部分。

    总结一下,本文需要的前置知识包括:矩阵基本运算、矩阵与线性变换、SVD 分解/特征值特征向量、神经网络基础、深度学习基础等。

    • [1] 多头注意情况可能是 \(d \times \frac{d}{h} \)
    • [2] 对于满秩方阵也可以使用“特征值/特征向量”的方式去理解
    • [3] 在 GPT 2 中,一个 Attention 层的计算,会分为“多头”去计算;并在计算后,还会再经过一个 MLP 层
  • 对于非 NLP 专业的人来说,要向理解大语言模型的基础其实是非常不容易的。在有了一定的神经网络基础、数学基础之后,算是可以更进一步了,在了解LLM的系列中,大概可以分成几个部分:输入(即本文的tokenize)、计算(Attention)、输出(beam search/top-k)。在本篇中:(a) 通过代码实践观察大模型(GPT2)如何进行 tokenize;(b) 如何查看 token id 列表 (c) 观察模型中所有token的 Embedding Matrix。

    这是我的大语言(LLM)学习系列中的一篇,完整的学习路径参考:我的大模型学习路线

    1. 理解 tokenize

    1.1 Token ID

    这里我们使用如下的提示词,来看看大模型是如何处理的:It’s very hot in summer. Swimming is

    大模型会使用预先设计好的“tokenize”实现,将上述的句子分解成独立的“Token”,并转换为对应的“Token ID”,而每个Token,都有自己的编码,也就是 Embedding ,这些 Embedding 就最终作为大语言模型的输入。

    对于上述的句子,“openai-community/gpt2” 在进行 tokenize 之后对应的 Token 和 Token ID 如下:

    itsveryhotinsummer . Swimmingis
    Token ID1026447247 82 84530242873931 13 245127428318
    TokenItâĢĻsĠveryĠhotĠinĠsummer.ĠSwimmingĠis

    “openai-community/gpt2” 使用了较为常见的BPE(Byte Pair Encoding)对句子进行处理,把每个词语按照“subword”进行处理,例如:

    • “Swimming”拆分为“Sw”与“imming”
    • 这里两个 Token 447、247 组成特殊字符 ‘ (撇号)
    • Ġ(U+0120)的作用:表示这是一个新的词语(而不是一个拆分后子词),可以看到 “Swimming”在拆分后的“imming”前面并没有Ġ,表示这是一个拆分后的子词

    1.2 词表大小

    “openai-community/gpt2” 模型的词表大小为:50257。词表中的前三个 Token 为 ’emb’、 ‘ĠDraft’、 ‘Ġreinvent’,对应的 Token ID 为 24419、 13650、 36608。

    1.3 根据 Token ID 打印字符

    这里打印了 Token ID 为 0、 1、 2 和 50254、 50255、 50256 的几个字符如下:

    Token IDChar
    0!
    1
    2#
    50254Ġinformants
    50255Ġgazed
    50256<|endoftext|>
    --- Token ID 转换为 Token 字符 ---
    Token ID: 0 | 对应 Token 字符: '!'
    Token ID: 1 | 对应 Token 字符: '"'
    Token ID: 2 | 对应 Token 字符: '#'
    Token ID: 50254 | 对应 Token 字符: 'Ġinformants'
    Token ID: 50255 | 对应 Token 字符: 'Ġgazed'
    Token ID: 50256 | 对应 Token 字符: '<|endoftext|>'

    2. Token Embedding

    在大模型的通常是从 Embedding 开始的,即对于所有字符的处理,都是依赖字符对应的“向量”。所以,大致的处理逻辑是这样:一个句子,先切分为 Token,然后根据 Token ID 在“Embedding Matrix”中找到对应的“向量”,把该“向量”组作为输入。

    这里,我们观察上述示例句子,根据对应的 Token ID 可以查询到对应的向量。因为这里的向量是768维的,这里仅显示前三个维度的分量,如下:

    Token      | wte[:3]                         |
    ----------------------------------------------
    It | [0.039, -0.0869, 0.0662 ,...] |
    âĢ | [-0.075, 0.0948, -0.0034,...] |
    Ļ | [-0.0223, 0.0182, 0.2631 ,...] |
    s | [-0.064, -0.0469, 0.2061 ,...] |
    Ġvery | [-0.0553, -0.0348, 0.0606 ,...] |
    Ġhot | [0.0399, -0.0053, 0.0742 ,...] |
    Ġin | [-0.0337, 0.0108, 0.0293 ,...] |
    Ġsummer | [0.0422, 0.0138, -0.0213,...] |
    . | [0.0466, -0.0113, 0.0283 ,...] |
    ĠSw | [0.0617, 0.0373, 0.1018 ,...] |
    imming | [-0.1385, -0.1774, -0.0181,...] |
    Ġis | [-0.0097, 0.0101, 0.0556 ,...] |

    更为完整的,上述的每个词语对应的向量是一个 1×768 的向量;整个句子 12 个向量,可以理解为一个 12×768 的输入矩阵。对于各 LLM 来说,这通常就是其将要处理的输入。

    3. Positional encoding

    在“GPT-2”使用了“Learned Positional Embeddings”(注:与 Transformer 论文中固定的Sinusoidal 实现不同)。这是一个 \(L \times d \) 的矩阵,其中,\(L \) 是最大接收字符数,\(d \) 是 Token Embedding 的维度。该矩阵通常随机初始化,最终通过训练确定。在“GPT-2”中,最终训练后的矩阵有如下形式:

    pos_emb_layer = model.transformer.wpe
    
    # .weight.data : Embedding Matrix(PyTorch Tensor)
    pos_emb_matrix = pos_emb_layer.weight.data
    
    print(pos_emb_matrix[:12,:3])

    输入如下:

    tensor([[-0.0188, -0.1974,  0.0040],
            [ 0.0240, -0.0538, -0.0949],
            [ 0.0042, -0.0848,  0.0545],
            [-0.0003, -0.0738,  0.1055],
            [ 0.0076, -0.0251,  0.1270],
            [ 0.0096, -0.0339,  0.1312],
            [ 0.0027, -0.0205,  0.1196],
            [ 0.0025, -0.0032,  0.1174],
            [-0.0012, -0.0018,  0.1110],
            [ 0.0049,  0.0021,  0.1178],
            [ 0.0016,  0.0062,  0.1004],
            [-0.0036,  0.0175,  0.1068]])

    示例与代码

    环境准备

    !pip install transformers torch
    
    from transformers import AutoTokenizer, AutoModelForCausalLM
    
    MODEL_NAME = "openai-community/gpt2"
    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
    model = AutoModelForCausalLM.from_pretrained(MODEL_NAME)

    对句子进行 tokenize

    观察句子的tokenize:

    text = "It’s very hot in summer. Swimming is"
    
    # 1. 对句子进行 Tokenization
    # return_tensors='pt' 表示返回 PyTorch Tensor 格式 (虽然这里我们主要看 IDs)
    inputs = tokenizer(text, return_tensors='pt')
    
    # 2. 打印 Tokenization 结果
    print(f"--- 原始句子:{text} ---")
    
    # a. 打印 Token ID Tensor
    print("Token IDs (Tensor):")
    print(inputs['input_ids'])
    
    # b. 将 Token ID 转换回可读的 Token (Word Pieces)
    # .squeeze() 是为了去除 batch 维度
    tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'].squeeze().tolist())
    print("\nToken List (可读文本 Tokens):")
    print(tokens)
    
    # c. 打印 Attention Mask (1 表示是有效 Token,0 表示是 Padding Token)
    print("\nAttention Mask:")
    print(inputs['attention_mask'])
    
    --------------
    --- output ---
    --------------
    
    --- 原始句子:It’s very hot in summer. Swimming is ---
    Token IDs (Tensor):
    tensor([[ 1026,   447,   247,    82,   845,  3024,   287,  3931,    13,  2451,
             27428,   318]])
    
    Token List (可读文本 Tokens):
    ['It', 'âĢ', 'Ļ', 's', 'Ġvery', 'Ġhot', 'Ġin', 'Ġsummer', '.', 'ĠSw', 'imming', 'Ġis']
    
    Attention Mask:
    tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])

    词表大小查看

    查看词表大小,并打印词表中的前20个词:

    # 获取词汇表大小
    vocab_size = tokenizer.vocab_size
    print(f"--- GPT-2 模型支持的 Tokenize 总数 (词汇表大小): {vocab_size} ---")
    
    # 打印所有 Tokenize
    # tokenizer.get_vocab() 返回的是一个字典 {token: id}
    print("\n--- 打印前 20 个 Token (用于示例): ---")
    vocab = tokenizer.get_vocab()
    count = 0
    for token, id in vocab.items():
        if count < 20:
            # 使用 repr() 确保特殊字符(如空格 'Ġ')能被清晰展示
            print(f"ID: {id:<5} | Token: {repr(token)}")
            count += 1
        else:
            break
    
    --------------
    --- output ---
    --------------
    
    --- GPT-2 模型支持的 Tokenize 总数 (词汇表大小): 50257 ---
    
    --- 打印前 20 个 Token (用于示例): ---
    ID: 24419 | Token: 'emb'
    ID: 13650 | Token: 'ĠDraft'
    ID: 36608 | Token: 'Ġreinvent'
    ID: 36171 | Token: 'Recommended'
    ID: 20706 | Token: 'aunting'
    ID: 39558 | Token: 'Ġprotagonists'
    ID: 49309 | Token: 'raised'
    ID: 20589 | Token: 'Ġwicked'
    ID: 43074 | Token: 'ĠâĿ'
    ID: 22792 | Token: 'ĠTut'
    ID: 21620 | Token: 'erate'
    ...

    根据 Token ID 打印字符

    # 待查询的 Token ID 列表
    target_ids = [0, 1, 2, 50254 ,50255, 50256]
    
    tokens = tokenizer.convert_ids_to_tokens(target_ids)
    
    print(f"--- Token ID 转换为 Token 字符 ---")
    for id, token in zip(target_ids, tokens):
        # 使用 repr() 确保任何特殊的不可见字符(如空格或控制字符)能被清晰地展示
        print(f"Token ID: {id:<5} | 对应 Token 字符: {repr(token)}")
    
    # 额外打印这几个 Token 在词汇表中的 ID,以确认其对应关系
    # 这里的 token.id 并不是直接从 1, 2, 3 来的,而是从 tokenizer.get_vocab() 中查到的
    # 这是一个辅助验证步骤,确保 Tokenizer 的行为符合预期。
    print("\n--- 辅助验证 ---")
    for token in tokens:
        token_id_check = tokenizer.convert_tokens_to_ids(token)
        print(f"Token 字符: {repr(token):<10} | 查验 ID: {token_id_check}")
    
    --------------
    --- output ---
    --------------
    
    --- Token ID 转换为 Token 字符 ---
    Token ID: 0     | 对应 Token 字符: '!'
    Token ID: 1     | 对应 Token 字符: '"'
    Token ID: 2     | 对应 Token 字符: '#'
    Token ID: 50254 | 对应 Token 字符: 'Ġinformants'
    Token ID: 50255 | 对应 Token 字符: 'Ġgazed'
    Token ID: 50256 | 对应 Token 字符: '<|endoftext|>'

    根据 Token ID 查询对应向量

    在模型中,“词向量”(准确的应该是Token向量)存储在一个Embedding Matrix 中,可以使用如下的代码获取每个 token 对应 Embedding 后的向量:

    target_token = "ĠSw"  # 注意前面的特殊符号,确保它是模型词汇表中的 Token
    target_id = tokenizer.convert_tokens_to_ids(target_token)
    print(target_id)
    
    # embedding_matrix[target_id]
    target_embedding = embedding_matrix[target_id]
    print(target_embedding[:5].numpy())
    
    --------------
    --- output ---
    --------------
    
    2451
    [ 0.06167513  0.03733223  0.10182938  0.04881619 -0.09597597]
    Embedding 层的 dtype: torch.float32
    整个模型的默认 dtype: torch.float32

    句子到 Embedding 向量

    text = "It’s very hot in summer. Swimming is"
    inputs = tokenizer(text, return_tensors='pt')
    print("Token IDs (Tensor):")
    input_ids_tensor = inputs['input_ids']
    input_ids_list = input_ids_tensor.squeeze().tolist()
    
    for index, token_int in enumerate(input_ids_list):
        token_char = tokenizer.convert_ids_to_tokens(token_int)
        print(f"Token ID {token_int:<5} | Token: {repr(token_char):<9} | {embedding_matrix[token_int][:3]}")
    
    --------------
    --- output ---
    --------------
    
    Token ID 1026  | Token: 'It'      | tensor([ 0.0390, -0.0869,  0.0662])
    Token ID 447   | Token: 'âĢ'      | tensor([-0.0750,  0.0948, -0.0034])
    Token ID 247   | Token: 'Ļ'       | tensor([-0.0223,  0.0182,  0.2631])
    Token ID 82    | Token: 's'       | tensor([-0.0640, -0.0469,  0.2061])
    Token ID 845   | Token: 'Ġvery'   | tensor([-0.0553, -0.0348,  0.0606])
    Token ID 3024  | Token: 'Ġhot'    | tensor([ 0.0399, -0.0053,  0.0742])
    Token ID 287   | Token: 'Ġin'     | tensor([-0.0337,  0.0108,  0.0293])
    Token ID 3931  | Token: 'Ġsummer' | tensor([ 0.0422,  0.0138, -0.0213])
    Token ID 13    | Token: '.'       | tensor([ 0.0466, -0.0113,  0.0283])
    Token ID 2451  | Token: 'ĠSw'     | tensor([0.0617, 0.0373, 0.1018])
    Token ID 27428 | Token: 'imming'  | tensor([-0.1385, -0.1774, -0.0181])
    Token ID 318   | Token: 'Ġis'     | tensor([-0.0097,  0.0101,  0.0556])
    

    Embedding Matrix

    查看 Embedding Matrix

    # model.transformer.wte (Word Token Embeddings)
    embedding_layer = model.transformer.wte
    
    # .weight.data : Embedding Matrix(PyTorch Tensor)
    embedding_matrix = embedding_layer.weight.data
    
    print(embedding_matrix.shape)
    
    --------------
    --- output ---
    --------------
    
    torch.Size([50257, 768])

    最后

    大模型训练的第一步就是对于语料库(corpus)的处理,即将所有的语料转换为大模型训练能够接受的输入,即:tokenize。该过程会将语料库切分为独立的句子,多个句子可以作为一个批次(batch)作为输入进行训练。

  • 整体上,今年的魔力象限与去年的厂商完全相同,各厂商的相对位置变化也并不是很大。一些值得注意的点如下:

    • Redis 从 Visionaries 跌到 Niche Player 象限;这反应了 Redis 在社区所面临的困境,一方面是开源商业化的挑战;另一方,则是来自于 Valkey 社区–一个更加开放的 Key-Value 产品的竞争。
    • Neo4j 也从 Visionaries 跌到 Niche Player 象限。
    • 在第一军团(即Google AWS Microsoft Oracle)中,Oracle 位置略有下降。确实,Oracle 早就已经不再是一家数据库厂商了。
    • Databricks 和 Snowflake 凭借在数据处理上的领先,在横坐标(Visionaries)上前进了一大截
    • 此外,虽然没有在象限图中(仅前20的厂商),但依旧在Gartner关注对象中的厂商包括:Actian Broadcom ClickHouse InfluxData MotherDuck OceanBase PingCAP Tencent Cloud TigerGraph Yugabyte

    象限中的中国数据库厂商

    进入这次魔力象限的中国厂商与去年相同:阿里云数据库、华为云数据库。相比去年,两个厂商的位置变化也不太大,可以参考右图。

    阿里云数据库在 Vision 象限继续向前移动了一点。华为云则保持了相对位置几乎不变。

    此外,出现在“Honorable Mentions”部分的中国厂商有:

    • OceanBase
    • PingCAP
    • Tencent Cloud

    历史魔力象限列表

    2025-11

    其他

    作者最近几年持续对 Gartner 云数据库魔力象限保持关注,历史相关文章包括:

  • 标题:IBM $110亿收购Confluent; Gartner 云数据库魔力象限发布;AWS/阿里云支持PG18

    重要更新

    IBM 110亿美元收购数据流厂商 Confluent[1],该公司创建于2014年,最初致力于 Apache Kafka 的商业化,逐步扩展为完整的实时数据流平台。

    Gartner 2025云数据库魔力象限发布[2],阿里云依旧处于 Leaders 象限、华为云在 Challengers 象限。Redis、Neo4j从 Visionaries 象限落入 Niche Players。

    更新详情

    阿里云
    • RDS PostgreSQL新增支持PostgreSQL 18大版本。[16]
    • RDS SQL Server正式发布SQL Server 2025 企业集群版、SQL Server 2025 标准版。[21]
    火山云(字节)
    • SQL Server 按量计费实例支持暂停实例功能。实例暂停后,实例节点规格产生的费用将暂停计费,但使用存储空间的所产生的费用将持续计费[93]
    • 文档数据库 MongoDB 版新增支持查看事务平均响应时间、命令平均响应时间和写冲突次数等监控指标[95]
    • HBase 新增实例级别和 RegionServer 级别 Get、Scan、Put、BatchPut 请求的 QPS 和延迟监控指标[96]
    • 表格数据库 HBase 版支持为已开通容量型存储的实例申请冷存储桶路径,可直接将 HFile Bulkload 至对应冷存桶中[98]
    • 数据库工作台 DBW 支持为云数据库 MySQL 版、云数据库 veDB MySQL版、云数据库 PostgreSQL 版等类型的实例发起手动巡检、开启/关闭自动巡检,以及为开启安全管控的实例创建 SQL 审核工单。[101]
    • DBW 支持在 PostgreSQL 数据库的 SQL 数据交互台中导出 SQL 执行结果集,支持将当前页、所有页或选中的数据导出为 excel、csv、text 、JSON 和 insert SQL 格式的文件。[103]
    • DBW 对外开放 ExecuteSQL、ListDatabases、ListTables 和 GetTableInfo 接口,可以用来执行 SQL 语句,或查询数据库列表,所有表、或表元信息。[104]
    • DBW-在数据库工作台 DBW 中,SQL 助手与数据库智能助手(DBCopilot)合二为一,即在 SQL 窗口、观测诊断和导航栏 DB 智能助手中进入的会话中功能完全一样[105]
    Azure(微软云)
    • 将 Azure SRE 代理与 Azure Cosmos DB 结合使用 [26]
    GCP(谷歌云)
    • BigQuery 数据传输服务现在可以将数据从 Oracle 传输数据[29]
    • Cloud SQL for SQL Server 与 Microsoft Entra ID 集成[34]
    • AlloyDB 现已支持 C4 系列实例(Intel Granite Rapids 处理器),最高可达 288 个虚拟 CPU 和 2232 GiB 内存[35]
    • Oracle Database@Google Cloud 支持 us-central1(爱荷华州,北美)等区域 [38]
    • 您现在可以使用 BigQuery 远程 MCP 服务器[39]
    • 您可以使用 Gemini 来修复 AlloyDB Studio 查询编辑器中的查询错误[43]
    Oracle云
    • Autonomous AI Database API for DynamoDB 发布 [46]
    百度云
    • GaiaDB 支持设置SSL加密[48]
    • GaiaDB 支持暂停、启动GaiaDB集群[49]
    • GaiaDB支持暂停、启动、重启代理实例[50]
    华为云
    • GeminiDB 兼容DynamoDB接口支持按请求计费多租Serverless[62]
    • GeminiDB Redis接口性能版支持慢日志管理[63]
    • GeminiDB Cassandra接口云原生模式支持删除节点[64]
    • DDS实例鲲鹏CPU新增规格[65]
    • DDS支持容量预估[66]
    AWS(亚马逊云)
    • RDS 扩展支持版本 5.7.44-RDS.20251212 [67]
    • EC2 M8gn 和 M8gb 实例GA [68]
    • AWS 数据库现已在 Vercel Marketplace 上架[69]
    • RDS 数据库预览环境中支持 PostgreSQL 18.1[70]
    • Redshift Serverless 宣布正式推出支持 IPv6 的双栈模式[74]
    • Aurora DSQL 现在支持在几秒钟内创建集群[81]
    • Aurora PostgreSQL 现在支持与 Kiro powers(MCP)集成[82]
    • ElastiCache Serverless 现在支持 same-slot WATCH 命令[84]
    腾讯云
    • 云数据库 MySQL 发布数据库代理版本1.4.6。[87]
    • TDSQL-C MySQL 版发布数据库代理版本1.3.20和1.4.6。[88]
    • TDSQL-C MySQL 版于2025年12月16日起,优化了跨可用区部署功能:在集群出现故障的场景,当开启了跨可用区部署功能的集群本地切换失败后且集群90秒不可用时,集群将会自动进行跨可用区切换,请您及时关注集群运行状态。[89]
    • TDSQL-C MySQL 版发布了编译优化高性能版本的性能白皮书[90]
    • TDSQL-C MySQL 版发布了新版性能白皮书,其针对 MySQL 8.0版本刷新了在全缓存、大数据集场景下的不同规格的性能测试数据,为对性能有严苛要求的企业级场景提供了更可靠的数据库选择。[91]
    • 云数据库 SQL Server 云盘架构的单节点、双节点、RO 实例新增支持:北京金融一区、北京金融二区、利雅得一区、利雅得二区。[92]

    参考链接

  • DiskANN 是较为常见的向量数据库搜索算法,作者通过“贪婪搜索”和“健壮性剪枝”(“RobustPrune”)来构建一个“低直径”的图,从而实现高性能的向量搜索。这里的“低直径”很好的保障了点与点之间非常高效(只需要少数几条)完成搜索。“RobustPrune”则是构建低直径的“Vamana”图的关键步骤。本文通过计算推导、示例与试验的方式,来看看“RobustPrune”如何实现“低直径”图的构建,而这也正是 DiskANN 算法的核心之一。

    1. RobustPrune 概述

    论文中的原始描述如右图。

    可以这样理解:在找到 \(p \) 的若干“邻居”后(邻居集合记为 \(N_{out}(p) \) ),由于“边”(出度)数量有限(这里是 \(R \) ),故需要进行“剪枝”,那么这里的“剪枝”方法即为“RobustPrune”:

    • 在 \(N_{out}(p) \) 中找出距离 \(p \) 最近的点,记为 \(p^{*} \)
    • 需要“剪枝”时,将满足下列条件的点都“剪掉”:
      • \(\alpha \cdot d(p^{*},p’) \le d(p,p’) \)

    在初次看到,表达式 \(\alpha \cdot d(p^{*},p’) \le d(p,p’) \) 的时候,是比较难理解其背后的用意的,本文则较为详细以分析、推导、“试验”的方式介绍该公式在图构建过程中所取的效果。

    2. 二维空间的示例

    这里以二维空间为例,以一个具体的场景来观察 RobustPrune 的效果。具体的,将上述的“剪枝”操作描述为如下的数学形式:

    2.1 “剪枝”区域计算

    在二维坐标系中有 \(P = (0,0) \, A = (2,0) \) ,考虑该集合:

    $$
    \mathscr{L} = \{ X | \frac{d(X,P)}{d(X,A)} \le \alpha \quad \text{and} \quad d(X,A) < d(X,P) \} $$

    即,在 RobustPrune 中落入该区域的点,将被剪枝。

    这里 \(\alpha \) 是一个常数,且 \(\alpha \gt 1 \) 。 假设, \(\mathscr{L} \) 中的点 \(X\) 坐标是 \((x,y) \),那么推导 \(x \, y \) 需要满足怎样的限制条件。

    在上述的“数学”问题中,点 \(P = (0,0) \) 即为论文算法中的 \(p \),点 \(A \) 即为论文算法中距离 \(p \) 点最近的邻居 \(p^{*} \)。问题中的 \(x \, y \) 限制提交,即为论文中的“剪枝”条件。

    根据“简单”的数学推导(详细推导可以参考附录,为了连续性这里暂不详述),有如下结果,即坐标 \((x , y) \) 需满足如下条件:

    $$
    (x-\frac{2\alpha^2}{\alpha^2-1})^2 + y^2 \le \frac{4\alpha^2}{(\alpha^2-1)^2}
    $$

    可以看到,这是一个以 \(\frac{2\alpha}{\alpha^2 -1} \) 为半径,以 \((\frac{2\alpha^2}{\alpha^2-1},0) \) 为圆心的圆内部区域(包括边缘)。

    更为具体的,取 \(\alpha = 1.2 \),则在坐标系中,该区域为右图。

    即,当 \(P \) 点、 \(A \) 点确定时,落入右侧图形中阴影部分的点,均会被“剪枝”。

    2.2 实际“剪枝”区域

    更为一般的,如果在平面中任意去一些点,也有类似的结论。其对应的“剪枝圆”(有的地方会称为“遮蔽空间),则有如下的形式:

    考虑右图中, \(A \) 为距离 \(P \) 最近的邻居,那么看看点 \(B \)、\(C \) 是否会被遮蔽。

    与上述推导类似的,可以计算出对应的“剪枝圆”如右图阴影部分,因为 \(C \) 点恰好落在该区域(且又因为邻居数量超过最大出度),故 \(C \) 将被从邻居中删除。

    这就是 RobustPrune 在二维空间的效果。更进一步的,为什么这样进行剪枝就可以保障“低直径”呢?

    3. “剪枝”、不“剪枝”的对比

    为了进一步加深对 RobustPrune 效果的理解,我们考虑这样的问题:在一个 Vamana 图的构建过程中,使用 RobustPrune 进行“剪枝”和不使用该方法剪枝,构建的图会有什么不同?

    在进行正式的试验验证之前,我们有理由这样去考虑:因为“出度” \(R \) 是固定的值,将距离 \(A \) 很近的点从候选邻居中删除,可以避免某些区域邻居过于集中,并将该位置预留给距离 \(P \) 远一些的邻居,这可能会有利于降低图的直径。

    上述理解,即为 RobustPrune 的直觉建立的比较核心的理解。

    为了更好的理解上述内容,并验证上述“直觉”,这里通过程序随机生成30点,并尝试构建 Vamana 图,在构建过程中对比使用 RobustPrune 剪枝和不进行剪枝,看看两种方式构建的图有什么差异。

    3.1 剪枝、不剪枝对比图

    具体的实现程序参考:Vamana Graph.ipynb。程序中,先生成 30 个随机的点,如 Figure 1;然后分别使用带有“剪枝”、没有“剪枝”的算法,生成图,如 Figure 2、Figure3。

    这里可以较为直观的看到,因为这里的“出度” \(R = 3 \) 是固定的,点“3”和点“22”,在剪枝和不剪枝的情况下,两个点之间的距离相差非常大,这也最终导致图的“直径”相差很大。

    4. 拆解 RobustPrune “剪枝”过程

    这里以点“3”,记为 \(P_3 \), 为例,看看RobustPrune “剪枝”是如何进行的。

    首先,在该示例中使用“最近”初始化的方法(而不是随机初始化),即对于每一个点,例如这里的 \(P_3 \) ,初始其“出度”(out degree)候选邻居为距离其最近的10个点,即

    $$ N_{out}(p) = \{ P_{21}\,P_{16}\,P_5 \, P_{22}\, P_0 \, P_{20}\, P_{13}\, P_{12}\, P_8\, P_{23} \} $$

    这里考虑的欧式距离,故最近的点为 \(P_{21} \)。

    4.1 考虑RobustPrune剪枝

    在进行RobustPrune剪枝时,点 \(P_5 \, P_{16} \)处于点\(P_{21} \)的遮蔽区域,故进行剪枝。最终保留了点:\( N_{out}(p) = \{ P_{21} \, P_{22}\, P_0 \} \)

    4.2 不考虑剪枝

    不考虑“剪枝”,又因为这里最大出度\(R=3 \),故保留距离 \(P_3 \) 最近的三个点,则为:\( N_{out}(p) = \{ P_{5} \, P_{16}\, P_{21} \} \)

    4.3 剪枝区域

    在上述的示例中,我们可以绘制考虑 \(P_3 \) 时,取 \(\alpha = 1.2 \)时,剪枝所涉及的遮蔽区域如下,即落到该区域内的点(除了最近的 \(P_{21} \) ),都将被剪枝,如右图。

    并且,因为最大出度 \(R = 3 \),在剩余的点中,再额外选取两个最近的点保留,这里即为:\(\{ P_0 \, P_{22} \} \)。

    所以,经过RobustPrune剪枝,最终 \(\{ P_3 \} \) 保留的“出度”邻居点集为

    $$
    N_{out}(P_3) = \{ P_{21} \, P_{0} \, P_{22} \}
    $$

    可以看到,经过剪枝,在构建 \(P_3 \) 的出度邻居时,在最大出度限制为 \(R=3 \) 时,很好的避免了图邻居的“聚集”,从而最终减低了整个图的直径,就有了上述的最终构建效果:

    5. 最后

    这里给出的示例是二维的,在这个场景下,高维扩展并没有太大的不同。从构建“直觉”的角度,这里给出示例已经足够。对于更高维的场景,如果感兴趣的,可以自己进行推导。

    附录:数学推导

    这里,\(P = (0,0) \, A = (2,0) \),再根据剪枝公式有:

    $$
    \begin{aligned}
    \frac{d(X,P)}{d(X,A)} & \ge \alpha \\
    \frac{\sqrt{x^2+y^2}}{\sqrt{(x-2)^2+y^2}} & \ge \alpha \\
    \frac{x^2+y^2}{(x-2)^2+y^2} & \ge \alpha^2 \\
    x^2+y^2 & \ge \alpha^2((x-2)^2+y^2) \\
    x^2+y^2 & \ge \alpha^2x^2 -4\alpha^2 x + 4\alpha^2 + \alpha^2 y^2 \\
    0 & \ge x^2 – \frac{4\alpha^2 x}{(\alpha^2-1)} + \frac{4\alpha^2}{(\alpha^2-1)} + y^2 \\
    0 & \ge (x- \frac{2\alpha^2}{(\alpha^2-1)})^2 – (\frac{2\alpha^2}{(\alpha^2-1)})^2 + \frac{4\alpha^2}{(\alpha^2-1)} + y^2 \\
    (x- \frac{2\alpha^2}{(\alpha^2-1)})^2 + y^2 & \le (\frac{2\alpha^2}{(\alpha^2-1)})^2 – \frac{4\alpha^2}{(\alpha^2-1)}\\
    (x- \frac{2\alpha^2}{(\alpha^2-1)})^2 + y^2 & \le (\frac{2\alpha}{\alpha^2-1})^2 \quad \blacksquare
    \end{aligned}
    $$

  • This content is password-protected. To view it, please enter the password below.