admin

  • 线性代数回顾(一)

    ·

    如果要理解大模型内部是如何工作的,良好的数学基础是必须的,而线性代数又是所有这些的基础。例如我们来看 \(\text{Attention} \) 机制中的如下问题。

    1. 为什么要重温线性代数

    考虑第 \(j \) 个 \( \text{Layer} \) 中的第 \( i \) 个 \( \text{Head} \) ,则有如下的计算(为了简化,下述的角标省略了 \( \text{Layer} \) 部分):

    $$
    \begin{aligned}
    Q_i &= XW_i^Q \\[0.3em]
    K_i &= XW_i^K \\[0.3em]
    \text{Therefore:} \\[0.3em]
    \text{Attention Score}_i &= Q_iK_i^T \\[0.3em]
    &= XW_i^Q(XW_i^K)^T \\[0.3em]
    &= XW_i^Q\big((W_i^K)^T X^T\big) \\[0.3em]
    &= XW_i^Q(W_i^K)^T X^T \\[0.3em]
    &= X\big(W_i^Q (W_i^K)^T\big) X^T \\[0.3em]
    \text{Let:} \quad \\[0.3em]
    W_i^{QK} &= W_i^Q (W_i^K)^T \\[0.3em]
    \text{Therefore:} \\[0.3em]
    \text{Attention Score}_i &= XW_i^{QK} X^T \\[0.3em]
    \end{aligned}
    $$

    注:上述的推导使用矩阵的一些简单特性,包括转置计算、结合律等。其中,在开源的GPT2模型中,\(W_i^Q \,, W_i^K \) 都是 \(64 \times 768 \)的矩阵。

    这里一个简单、又不太简单的问题是:那为什么每一个 \( \text{Head} \) 中不使用一个权重 \( W_i^{QK} \) 就可以了?这个问题从我第一次看明白 \( \text{Attention} \) 的计算后,困扰了我一会儿,直到重温了线性代数,才算是理解了上述的计算。

    大学时学习线性代数学得非常痛苦,而现在带着问题再去看这本书,竟然花了两个晚上就看完了。这里对里面的基本概念和结论做个梳理,以更好的理解什么是“线性变换”、与矩阵的关系是什么、如何研究一个线性变换或矩阵的性质等。

    本系列主要以介绍线性代数的“直觉”为主,不会做任何证明,为了更好的阐述“直觉”,甚至牺牲了很多的数学严谨,看客也需要从构建“直觉”与“联系”的角度阅读。本文的阅读前提是已经有一定的线性代数的基础、也对神经网络/LLM技术有一定了解,那么本文则尝试通过较小的篇幅去构建两者的联系,看看如何使用线性代数的技术去研究神经网络的中的问题。

    2. 线性代数讨论的主要问题

    通常“线性代数”课程会从 \(n\) 元一次线性方程组引入,并使用行列式理论,去较为彻底的回答如何解决 \(n\) 元一次线性方程组。更进一步的,为了更好/更完整的研究 \(n\) 元一次线性方程组的“解空间”,则需要引入一个新的研究对象:“向量空间”。而向量空间本身所具备的普遍性,已经远超出 \(n\) 元一次线性方程组本身。而后,“向量空间”、“线性变换”就变成了新的研究对象,因为现实问题中,我们经常会尝试通过“线性变换”来洞察向量空间中向量的关系。

    是不是感觉上面描述漏了什么?是的,漏了“矩阵”。无论是讨论 \(n\) 元一次线性方程组还是“向量空间”或“线性变换”,矩阵都是“核心”工具。这个工具“核心”或者说重要到什么程度呢?甚至很多问题,只需要研究清楚对应“矩阵”的特性,原来的问题就研究清楚了。所以,你会注意到,线性代数的书中,几乎全都在介绍“矩阵”的各种特性。

    而各种 \( \text{Embedding} \) 就可以理解为是在最为典型的欧氏空间中的向量。

    3. \(n \) 元一次方程组的解

    线性代数通常都会以解“\( n \)元一次方程组”为切入点,这个问题看似很简单,但是最终要完全讨论清楚,则需要很多篇幅。这也是“线性代数”前半部分比较枯燥的原因。整体上来看,关于“\( n \)元一次方程组”的解需要讨论清楚几个问题:

    • (a) \( n \)元一次方程组的解是否存在?
    • (b) 如果存在,如何求解
    • (c) 如果解不存在,充要条件是什么
    • (d) 如果有解,那么所有的解如何表达

    在探讨上述问题的时候,先是引入了“行列式”、“矩阵”的概念与理论,并通过矩阵的“初等变换”实现对上述问题的求解。这里涉及到的概念延伸出了:

    • (a) 矩阵的初等变换
    • (b) 矩阵的秩等

    这里我们列举一些主要的结论(并不做推倒,推倒过程还是非常复杂的,这也正是线性代数书比较枯燥的原因之一)。这里考虑如下的线性方程组:

    $$
    a_{11}x_1 + a_{12}x_2 + \cdots + a_{1n}x_n = b_1, \\
    a_{21}x_1 + a_{22}x_2 + \cdots + a_{2n}x_n = b_2, \\
    \quad\vdots \\
    a_{m1}x_1 + a_{m2}x_2 + \cdots + a_{mn}x_n = b_m.
    $$

    结论: 线性方程组有解的充分必要条件是:它的系数矩阵与增广矩阵有相同的秩。

    结论: 线性方程组系数矩阵和增广矩阵的秩都是\( r \),方程组的未知数的个数是\( n \),如果:

    • \( r = n \) 则线性方程组有唯一解
    • \( r < n \) 则线性方程组有无穷组解

    上述两个定理较为彻底的回答了解存在性的问题。那么,解的公式化表达是怎样的呢?为了略微简化问题,这里考虑仅考虑\( n \)个方程、\( n \)个未知数,且解唯一的情况:

    $$
    \begin{cases}
    a_{11}x_1 + a_{12}x_2 + \cdots + a_{1n}x_n = b_1, \\
    a_{21}x_1 + a_{22}x_2 + \cdots + a_{2n}x_n = b_2, \\
    \quad\vdots \\
    a_{n1}x_1 + a_{n2}x_2 + \cdots + a_{nn}x_n = b_n
    \end{cases}
    \quad\Longleftrightarrow\quad
    \begin{bmatrix}
    a_{11} & a_{12} & \cdots & a_{1n} \\
    a_{21} & a_{22} & \cdots & a_{2n} \\
    \vdots & \vdots & \ddots & \vdots \\
    a_{n1} & a_{n2} & \cdots & a_{nn}
    \end{bmatrix}
    \begin{bmatrix}
    x_1 \\ x_2 \\ \vdots \\ x_n
    \end{bmatrix}
    =
    \begin{bmatrix}
    b_1 \\ b_2 \\ \vdots \\ b_n
    \end{bmatrix}
    $$

    形式化的解,则可以由两种方式给出:

    使用矩阵的表达:

    $$
    \vec{x} = A^{-1}\vec{b}
    $$

    克莱默法则”(Cramer’s rule / formula):

    $$
    x_1 = \frac{D_1}{D},x_2 = \frac{D_2}{D},\dots , x_n = \frac{D_n}{D},
    $$

    注意:上述的两种表达,无论哪种,限制条件都非常苛刻,即要求:矩阵\( A \)可逆或者行列列式\(D \neq 0\),当然,这两个条件式等价的。并且,这里的\(D\)也经常写作:\( det(A) \)。

    4. 矩阵基础与部分结论

    虽然是为了求解线性方程组才引入矩阵的,但很快就会意识到,对矩阵本身特性的研究有着更为广泛的应用。

    首先,在定义了矩阵的运算之后,很快就会有一些矩阵的运算律。例如,加法的结合律、交换律、分配律等。这里,矩阵的乘法是重点,且略微复杂一些:

    • 首先,首先矩阵的乘法是不满足交换律的
    • 很幸运,结合律和分配律都是满足的
    • 再次,对于转置运算,是满足如下形式的:\( (AB)^T = B^TA^T \)

    结论: 线性方程组的初等变换,对应着三个初等变换矩阵:\( P_{ij} \)、\( D_i(k) \)、\( T_{ij}(k) \),且这三个初等变换矩阵都是可逆的。

    结论: 一个\( m \times n\)的矩阵\( A \)总是可以通过初等变换化为以下形式的矩阵:

    $$ \bar{A} = \begin{bmatrix}
    I_r & O_{r,\,n-r} \\
    O_{m-r,\,r} & O_{m-r,\,n-r}
    \end{bmatrix} $$

    这里,\(I_r\)是\( r \)阶单位矩阵,\(O_{st}\)表示\( s\times t\)的零矩阵,\( r \)等于矩阵\(A\)的秩。

    结论:\(n \) 阶矩阵\( A \)可逆,当且仅当\( A \)的秩等于\( n \)。

    如何求解矩阵的逆:有了这些结论,那么对于一个可逆矩阵要求其逆矩阵,则可以有些办法:即对一个矩阵实施一系列的初等变换,将其变为单位矩阵。而同时,在开始的时候,就将所有的这些初等变换作用在一个单位矩阵上。最后,当原矩阵变为单位矩阵的时候,后面的单位矩阵就变成原矩阵的逆了。

    结论: 两个矩阵乘积的秩,不大于任何一个矩阵的秩。特别的,如果有一个矩阵是可逆的,则乘积的秩则等于另一个矩阵的秩。

    结论: 一个矩阵的行空间的维数等于列空间的维数,等于这个矩阵的秩。

    5. 再看看前面的问题

    $$
    \begin{aligned}
    \text{Attention Score}_i = XW_i^{QK} X^T \quad \text{where} \,,W_i^{QK} = W_i^Q (W_i^K)^T
    \end{aligned}\tag{1}
    $$

    $$
    \text{Attention Score}_i = Q_iK_i^T = XW_i^Q(XW_i^K)^T \tag{2}
    $$

    在上面的计算(1) 和 计算(2),那么在模型中训练 \(W_i^{QK} \) 和 在模型中训练单独训练 \(W_i^Q \,,W_i^K \) 是否是等价的?

    答案是否定的。

    这里以 GTP2 模型为例,原因在于如果单独训练 \(W_i^{QK} \) ,那么这个矩阵的秩,则很可能是 768 ;而单独训练 \(W_i^Q \,,W_i^K \),这两个矩阵的秩则一定小于 64,这两个矩阵的乘积的秩也一定是 64 (严格来说是小于等于)。所以最终训练获得的效果一定是不同的。当然,哪个更好,这倒不一定,但他们并不是等价的。

    一般意义来说,使用 \(W_i^{QK} \) 可能有着更强的表达能力,只是意义没有那么明确,并且训练的参数要更多。

  • 通史阅读:魏晋南北朝

    ·

    很长时间以来,历史都是我的盲区,而“魏晋南北朝”大概属于盲区中的盲区。由于各种原因,大概大家并不太愿意过多的去讲述这段历史,相关的影视作品、游戏等内容也比较少,在大众的认识中,存在感是相对更低的。

    从哪里开始呢?就从那场改变曹氏与司马氏权力格局的政变开始吧。

    自249年“高平陵之变”,司马家族从曹魏手中夺权,最终在265年,魏元帝曹奂“禅”位司马炎,建立“晋”,是为魏武帝。316年,晋愍帝出降前赵刘曜。318年,琅琊王司马睿在建康继帝位,是为晋元帝。最后,在420年,晋恭帝退位,刘裕登基建“宋”(多称为“刘宋”)。南朝分别历经宋、齐、梁、陈,陈最后为隋所灭。

    而北方也非常混乱,各种政权势力繁多,包括刘渊、刘曜、石勒、石闵、慕容儁(音“骏”)、苻坚、拓跋珪,最终由拓跋珪建立的北魏逐步统一北方。约在398年,拓跋珪迁都平城,即皇帝位。北魏在传续百年后,分裂为宇文泰集团、高欢集团,分别继承部分北魏“遗产”,建立北周、北齐。北齐早期虽然更强,但北周历经七十年的“图治”,最终灭北齐(约577年)。但,并没有过太久,在581年,杨坚废周静帝自立(参考:隋文帝),改国号为隋,北周灭亡。

    最后由北方的“隋”继续征战南北,统一全国。

    1. 混乱、短暂的西晋

    自司马炎称帝,到愍帝出降,西晋总计存续约五十年。西晋除了比较短暂外,另一个存在感很低的原因大概是:“混乱”,后世用“八王之乱”概括之。

    八王之乱,是一场司马家族内部“混乱”的权力之争。晋武帝司马炎去世后,传位给愚弱的惠帝司马衷,而后皇后贾南风(西晋开国之臣贾充之女)当权。先后利用多方力量分别杀杨骏(惠帝母杨氏族),而后汝南王司马亮被杀、楚王司马玮亦被杀,最后,甚至杀了太子司马遹。这激怒了其他诸王,也给其他诸王以理由。之后,赵王司马伦联合齐王司马囧,尽灭贾后及其党羽。

    接着赵王伦称帝,诸王自然不认可,于是开启了“八王之乱”的下半场。不久,赵王伦被齐王等所诛杀,惠帝虽“愚”,再次复位。之后,由于诸王专权的问题,齐王、成都王、河间王、长沙王、东海王均卷入这场“混乱”之中。

    东海王司马越站到了最后,但他并没有“得意”多久。北方匈奴部逐渐强大,而内部怀帝(惠帝死后,他的弟弟豫章王即位是为怀帝)因惧其专权,永嘉五年(311年),“发布司马越的罪状,要各方镇讨伐”司马越。司马越率领着西晋最后的十万大军,自己在忧愤中死去,而其大军也被匈奴部的石勒所灭。自此,西晋便更加无力抵抗北方匈奴部。五年后,晋愍帝窘迫出降于刘曜。西晋灭亡。

    2. 北方诸族(五胡十六国)

    “五胡十六国” 通常可以以刘渊称帝(308年),建立“汉”为初始。刘渊为匈奴五部大单于,在八王之乱期间成都王司马颖,曾极力拉拢,在司马颖败落后,便联合匈奴各部,建立自己的政权。帝位传至其孙刘粲(音“灿”)后,粲被靳准所杀,靳准曾向西晋称臣;而后,靳准被石勒、刘曜所败,石勒、刘曜均是刘聪(刘渊子)的临终时的顾命大臣。“靳准之乱”后,刘曜、石勒也各自为政,分别建立了“前赵”、“后赵”。

    2.1 后赵

    这一时期的“国家”、“帝王”都很短暂与混乱。这里看看出生卑微的石勒建立后赵。

    329年石勒灭前赵(刘曜),次年称帝;333年石勒卒,次年其从子石虎篡位;349年石虎卒,其子为争帝位互相残杀。次年(350年),石虎养孙冉闵自立为帝,改国号魏,史称冉魏。后来,石氏子孙投降东晋,也被杀及诛灭。

    2.2 群雄并立

    这段时间,在北方建立政权的先后还有:慕容氏建立的前燕、后燕、南燕、西燕、后凉、后秦、西秦、北魏,这些势力多数时候是并立存在。曾经一度“北方的八个势力,并立了九年之久”(傅 “中国通史”)。

    2.3 拓跋氏统一北方

    直到,北魏拓跋氏一统北方,北方大地才再次有了一段相对和平的时代。北魏开国皇帝拓跋珪,于398年称帝,建都平城(即大同)。

    3. 摇摇欲坠的东晋

    自318年,晋元帝司马睿在建康即帝位,到420年刘宋建立。虽由一百年时间,但是,期间司马氏从不曾真正较长时间掌握大权,一直都是在“门阀”、“权臣”的交替与平衡中生存。

    先是“王与马共天下”,而后王敦起兵,晋元帝“忧愤而死”,翻译成今天的话,大概就是被气死了。那个说出“举目见日不见长安”的晋明帝虽平定了王敦之乱,但奈何命短,在位三年后去世。再到晋成帝则是苏峻之乱,一度破建康城,劫成帝。

    而后,桓温崛起。桓温先是北伐,后谋求自立为帝未果。桓温死后,其家族依旧强大,其子桓玄袭其位,其弟、侄等任荆州刺史、扬州刺史、江州刺史等要职。

    桓玄一度曾称帝,最终被刘裕平定,还帝位于晋室。哦,短暂的归还。

    4. 东晋北伐的困局

    很多人大概都听过“闻鸡起舞”这个成语,也还有不少人知道祖逖艰难北伐的故事。但似乎很少人谈论,为什么西晋北伐几乎是不可能成功的。祖逖、桓温、刘裕都是北伐名将,他们的经历大概某种程度给出了回答。

    4.1 闻鸡起舞的祖逖

    当祖逖在“进”表要北伐时(当时北方石勒、刘曜混战),晋室元帝司马睿当然口头上是非常支持的,但并给出不太大的实质上的支持:“以逖为奋威将军、豫州刺史,给千人禀,布三千匹,不给铠仗,使自招募”。

    时帝方拓定江南,未遑北伐,逖进说曰:“晋室之乱,非上无道而下怨叛也。由籓王争权,自相诛灭,遂使戎狄乘隙,毒流中原。今遗黎既被残酷,人有奋击之志。大王诚能发威命将,使若逖等为之统主,则郡国豪杰必因风向赴,沈弱之士欣于来苏,庶几国耻可雪,愿大王图之。”帝乃以逖为奋威将军、豫州刺史,给千人禀,布三千匹,不给铠仗,使自招募。

    晋书/卷062

    而祖逖也算不负众望,一路北伐恢复了大量黄河以南的土地,并与北方的石勒隔“河”相持对峙。

    而后,321年,晋元帝司马睿派遣戴渊(戴若思)为征西将军。祖逖有自己的战略考量,对戴若思并不太认可,同时也看到朝廷内政不稳,最终在“忧愤”与不得志中去世。关于这一段,各位看客可以读读唐朝房玄龄所著《晋书》中祖逖的列传内容:

    石勒不敢窥兵河南,使成皋县修逖母墓,因与逖书,求通使交市,逖不报书,而听互市,收利十倍,于是公私丰赡,士马日滋。方当推锋越河,扫清冀朔,会朝廷将遣戴若思为都督,逖以若思是吴人,虽有才望,无弘致远识,且已翦荆棘,收河南地,而若思雍容,一旦来统之,意甚怏怏。且闻王敦与刘隗等构隙,虑有内难,大功不遂。感激发病,乃致妻孥汝南大木山下。时中原士庶咸谓逖当进据武牢,而反置家险厄,或谏之,不纳。逖虽内怀忧愤,而图进取不辍,营缮武牢城,城北临黄河,西接成皋,四望甚远。逖恐南无坚垒,必为贼所袭,乃使从子汝南太守济率汝阳太守张敞、新蔡内史周闳率众筑垒。未成,而逖病甚。先是,华谭、庾阐问术人戴洋,洋曰:“祖豫州九月当死。”初有妖星见于豫州之分,历阳陈训又谓人曰:“今年西北大将当死。”逖亦见星,曰:“为我矣!方平河北,而天欲杀我,此乃不祐国也。”俄卒于雍丘,时年五十六。豫州士女若丧考妣,谯梁百姓为之立祠。册赠车骑将军。王敦久怀逆乱,畏逖不敢发,至是始得肆意焉。寻以逖弟约代领其众。约别有传。逖兄纳。

    晋书/卷062

    为什么建议要读一下这一段内容呢?我们现在看到的“通俗”内容中关于祖逖的说明,大多都有一些差异,而这些内容多半是出自上述原始材料,读者不同,翻译不同,理解也可能有不同。所以,大可读读原始材料。

    这段内容其实是可以有两种解读的。一方面祖逖北伐是比较成功的,收复和稳定了大量黄河以南的失地。此时,恰逢重新积蓄力量,修整军队的一段略微平静些的时间。石勒、祖逖也暂时的“默契休战”。石勒“修逖母墓”、“求通市”,祖逖虽未回复,但听任之,并且在互市过程中,获利颇丰(收利十倍),从而兵马日渐强壮(“士马日滋”)。这时,祖逖继续加强城池修缮,并且内心并不认可朝廷派遣来的“都督戴若思”。

    也许祖逖的战略是百分百正确,但祖逖的行为在政治上是“危险”的。只是恰逢东晋皇室羸弱,“门阀掌权”、权臣王敦亦有异心,并且祖逖不久就死去。否则,局势虽不同,但也可能会面临类似于“岳飞”类似的境地。

    4.2 北伐的困局

    可以看到,东晋朝廷虽然表面支持祖逖北伐,但并未给予实质性支持,某种程度上甚至是略有掣肘,尤其是在祖逖北伐顺利的时候。在傅乐成中国通史中有如下描述,则道出了北伐真正的困境:

    当时除少数苟安的士大夫外,晋人莫不希望早复中原,他们普遍抱有一种思想,认为谁能驱逐胡虏,谁便有称帝的资格,这种思想在东晋中末期尤为显著。因此若干野心家,都思北伐立功,以求名正言顺的称帝。正因如此,晋室中央及其执政者渐至视外战为畏途,不但不加支持,反处处掣肘。

    傅乐成 《中国通史》

    结合着祖逖、桓温、刘裕的北伐和后面的历史,可以看到,上面这段话是揭露了北伐困局的真相的。对于将相北伐风险、挑战自然是非常高的,并且一定会受到朝廷猜忌与防范,所以,如果一个人只是想北伐、恢复晋室,那么他可能北伐能够成功,但他不可能成功。最后,就“催生”了桓温、刘裕的“模式”,北伐成功,军队强大,只能自己做自己的皇帝。

    在中国古代,正如谚语所云“太平本是将军定,不许将军见太平”。

    4.3 桓温

    桓温家族在东晋虽“门地不高”,但也是“士族”阶层(参考)。桓温在多方势力“平衡”之中,镇守荆州,而在当时荆州是南方的一个重镇。而后,凭借勇武与敏锐的军事能力,“平灭成汉”,很大程度上扩大了东晋在南方的势力范围,桓温也因此“声名大盛”。而后,桓温多次北伐,其中第二次北伐曾成功收复洛阳。到桓温“晚年”,则主要谋求在朝廷的最高权利,但因为各种原因与阻碍,最终未能代晋称帝。

    桓温的两次北伐,都因粮尽退兵,为敌所乘,而致大败,可知桓温是以孤军击敌,并无后援。晋室中央绝不与他合作,必使他失败而后快,因为假使桓温一旦成功,晋室也将不保。事实上桓温确有做皇帝的念头,他准备平燕归来后便篡位的。所以就晋室的立场来说,它之不协助桓温北伐,也自有其苦衷,但复兴大业,却因而断送。

    傅乐成 《中国通史》

    4.4 刘裕

    所以,东晋北伐为什么总难成功呢?究其根本原因,大概是,力量难以统一。东晋王室是有这个威望的,但此时的司马家族已经势弱,虽能够聚集起一些力量,但是无法掌控这些力量。

    而其他力量,虽然能够在一段时间、或者某些地方称霸,甚至能够给北方诸族以致命打击,但是这些力量也因为孤立无援、甚至“黄雀在后”而无法完成“北伐”。

    但最后刘裕做到了,或者某种程度做到了。他曾经击败篡位的桓玄(桓温嗣子)恢复晋室、北伐克复洛阳与长安(虽然很短暂)、平定多次叛乱、打败成都王等。北方最终由于,后方不稳定,而很快再次丢失。

    最后,不出意外,意外就发生了。420年,刘裕迫使晋帝禅让,即位建立“宋”。

    5. 前秦与淝水之战

    在南北朝时期,“淝水之战”是关键的一战。北方原本强大的“前秦”(苻坚),则因为这一战而快速陨落。之前,苻坚所打败的一众力量则再次纷纷独立;南方晋室因为这一战的胜利,暂且保住了地盘。而由谢氏建立的北府兵也成长起来。在后来,北府兵的力量非常大程度的限制了桓氏(桓温家族)的力量。

    但,最终,北府兵旧将刘裕结束了东晋王朝。建立“刘宋”。

    6. 北朝

    “北朝”大概分成了两个阶段,一个是“群雄”争霸的五胡十六国时期,另一个是北魏(包括北周和北齐)时期。

    北魏由拓跋珪建立,最初定都“平城”(山西大同),而后拓跋焘统一北方。之后孝文帝迁都洛阳,并大力推行汉化,包括使用汉人姓氏、通婚、文字语言等都使用汉文。

    而后,北魏延续约100年,后分裂为东、西魏,并很快由宇文泰、高欢分别建立北周、北齐,两国之间多有摩擦与交战。起初,北齐虽占上风,但最终更加锐意变革的北周最终打败北齐。

    但,之后的北周并没有延续很久,最终,皇权由建立隋朝的杨坚所夺。

    不过,北周、隋、唐之间都有非常近的亲戚关系。例如杨坚的一个女儿,就是北周宣帝的皇后;更不用说,李渊和杨广就是姨表亲。

    7. 时代诗文

    7.1 举目见日 不见长安

    高中时就读过《举目见日 不见长安》这段内容,印象是非常深刻的,但是那种“莫名其妙”感记得也非常清楚。高中时,完全不了解东晋的情况,不知道晋元帝的困局,不知道东晋面临北方外族的南侵,不知道内部政局的混乱,自然难以理解这篇内容。

    明皇帝讳绍,字道畿,元皇帝长子也。幼而聪哲,为元帝所宠异。年数岁,尝坐置膝前,属长安使来,因问帝曰:「汝谓日与长安孰远?」对曰:「长安近。不闻人从日边来,居然可知也。」元帝异之。明日,宴群僚,又问之。对曰:「日近。」元帝失色,曰:「何乃异间者之言乎?」对曰:「举目则见日,不见长安。」由是益奇之。

    晋书 明帝

    再读这段文字,就有了不一样的体会。

    元帝在次日的“聚会”上,故意再次提问,可以看到一个普通的父亲的“虚荣心”,想在群僚面前让自己的孩子表现一下,就像现在的家长让自己的孩子在大人面前背诵一首唐诗、唱一首歌曲、表演一小段舞蹈、弹奏一首曲子是一样的。

    另外,晋元帝虽在南方逐步站稳,但是可以说是内外交困。外部北方政权(如石勒)一旦稳固则经常南侵;内部一方面内政士族强大,外部藩镇(王敦等)也不受指挥。此时,如果下一代如有明主,那么长远来看“东晋”政权则可能逐步强大。晋元帝对于下一代的期望自然相比于普通家长更加殷切。

    7.2 北朝名歌 敕勒歌

    敕勒川,阴山下。
    天似穹庐,笼盖四野。
    天苍苍,野茫茫,风吹草低见牛羊。

    这首诗歌的具体是作者和时间并没有特别明确的记载,有一种说法是该歌为“斛律金”所作,但更有可能是他曾经在关键时刻带领将士吟唱过。据记载,在高欢在玉璧之战大败后,斛律金为高欢部高唱此歌,最终稳住军心。

    所以,这是应该是一首鲜卑语的歌曲,怀念鲜卑人所生活的“刺勒川”美景。

    这首词经常被选入小学一二年级的课文。一方面,这首词语言精炼易懂,描述的景色开阔壮美、自然恬静;另一方面,这首歌为少数来自少数名族的词,也有表达中华民族是一个多民族融合的大家族。

    7.3 花木兰

      唧唧复唧唧,木兰当户织。不闻机杼声,唯闻女叹息。

      问女何所思,问女何所忆。女亦无所思,女亦无所忆。昨夜见军帖,可汗大点兵,军书十二卷,卷卷有爷名。阿爷无大儿,木兰无长兄,愿为市鞍马,从此替爷征。

      东市买骏马,西市买鞍鞯,南市买辔头,北市买长鞭。旦辞爷娘去,暮宿黄河边,不闻爷娘唤女声,但闻黄河流水鸣溅溅。旦辞黄河去,暮至黑山头,不闻爷娘唤女声,但闻燕山胡骑鸣啾啾。

      万里赴戎机,关山度若飞。朔气传金柝,寒光照铁衣。将军百战死,壮士十年归。

      归来见天子,天子坐明堂。策勋十二转,赏赐百千强。可汗问所欲,木兰不用尚书郎,愿驰千里足,送儿还故乡。

      爷娘闻女来,出郭相扶将;阿姊闻妹来,当户理红妆;小弟闻姊来,磨刀霍霍向猪羊。开我东阁门,坐我西阁床。脱我战时袍,著我旧时裳。当窗理云鬓,对镜帖花黄。出门看火伴,火伴皆惊忙:同行十二年,不知木兰是女郎。

      雄兔脚扑朔,雌兔眼迷离;双兔傍地走,安能辨我是雄雌?

    “木兰辞”被认为是一首创作于南北朝时期,但经过后世多次修改后最终形成我们现在看到的样子。在北朝(北魏/东西魏/北周/北齐),多使用“世兵制”或“府兵制”,“军户”在战时总是需要承担兵役,即便出现“阿爷无大儿,木兰无长兄”的情况也不能豁免。于是就有了木兰,以女儿身代父从军的故事。

    7.4 桃花源记

    … 土地平旷,屋舍俨然。有良田、美池、桑竹之属。阡陌交通,鸡犬相闻 … 自云先世避秦时乱,率妻子邑人来此绝境,不复出焉,遂与外人间隔。问今是何世,乃不知有汉,无论魏晋…

    陶渊明 《桃花源记》

    这首诗歌作于东晋、刘宋时期,在那前后的百年间,中国一直处于战乱之中。作者虚构此故事,表达了对和平安宁的向往。

    另外,我们加两个娃的名字,即来自于这首诗的“阡陌交通,鸡犬相闻”。一方面,希望他两兄弟能够拥有诗词中的生活。再者,一阡一陌,各有自己的方向与人生,但也有相交之处,这个相交的地方大概就是我们这个家吧。

    7.5 乌衣巷

    朱雀桥边野草花,乌衣巷口夕阳斜
    旧时王谢堂前燕,飞入寻常百姓家

    刘禹锡 《乌衣巷》

    这也是一首非常有名的怀古诗,作者站在乌衣巷前,看着“寻常”景色,遥想当年权倾一世的“王”、“谢”,早已随着时间而消失于历史长河。

  • 大语言模型的一个重要方向是“推理”优化,即如何在有限的硬件环境中提升推理的效率。对于所有的 MaaS 服务提供方,这都是至关重要的。一方面关乎用户的使用体验(诸如TTFT,time to first token)、另一方面关于服务提供的成本(有限的GPU如何提供更高的吞吐量)。

    1. 概述

    从 Transformer 架构的 Decoder 阶段原理来看,一个常见的、自然的优化就是使用“KV Cache”大大减少推理(自回归阶段)过程需要计算量,实现以显存换效率,从而加速推理过程。

    2. Decoder 模型的自回归计算

    在了解了“Attention”、“mask attention”、“autoregression”计算之后,比较自然可以注意到在 Q、K、V 矩阵在“autoregression”的过程中,有很多的部分是无需额外计算的。

    这里依旧继续使用《理解大语言模型的核心:Attention》中的示例,这里考虑在文章中的提示词“It’s very hot in summer. Swimming is”,生成新的Token为 “ a”,那么我们看看这个自回归过程某个Head中的计算。完成的代码可以参考:autoregression-of-attention.ipynb

    相比与在 prefill 阶段,需要额外计算的,在后续使用黄色标识出来。

    2. 1 Token Embedding 和 Positional Embedding

    Token Embedding

    +

    Positional Embedding

    这里,只需要计算最新的Token(即这里的“ a”)的Embedding即可。事实上,上面矩阵白色部分再自回归阶段完全不再需要使用了。所以,上述内容计算完成后,内存即可释放,无需缓存。

    2. 2 Normalize

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

    与前面类似,这里计算完成并推进到下一步后,内存即可释放,无需缓存。

    2. 3 Attention 层的参数矩阵

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

    2. 4 矩阵 Q K V的计算

    \(Q = XW^Q \)

    \(K = XW^K \)

    \(V = XW^V \)

    2. 5 计算 Attention Score

    \(\text{Attention Score} \)

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

    特别需要注意的,这一步中,“Attention Score Matrix”最后一行的计算,需要前面的Q的最后一行,此外还需要整个 K 矩阵。这就是为什么 K 矩阵是需要缓存的。

    2. 6 计算 Masked Attention Score

    \(\text{Masked Attention Score} \)

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

    2.7 计算 Softmax Masked Attention Score

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

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

    2. 8 计算 Contextual Embeddings

    \(\text{Contextual Embeddings} \)

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

    所以,这一步中,“Contextual Embeddings”最后一行的计算,需要前面 Softmax Masked Attention Score Matrix 的最后一行,此外还需要整个 V 矩阵。这就是为什么 V 矩阵是需要缓存的。

    此外可以看到,在这个自回归的计算中,Q 矩阵前面的所有行(即上一轮计算的Q矩阵)都用不上,这也是为什么 Q 矩阵不需要缓存,即我们需要的“KV Cache”,而不是“QKV Cache”的原因。

    3. 计算图示

    这里依据使用了图示的方式展示了在“自回归”过程中的数学计算。在下图中,第一个生成的 Token 为“ a”,该 Token 在进入 Decoder 模型再次进行计算时(即“自回归”),下图中:

    • 粉红色背景部分为新的、需要计算的部分;
    • 灰色背景部分为虽然不需要计算,但在计算新的内容时,需要使用的部分。

    灰色部分即为“KV Cache”需要缓存的部分。即,每一个 Token 对应的 “K”、“V” 矩阵都需要在后续的计算中使用。亦即,每一个 Token 的 Key 向量都需要保存,用于与新的 Token 的 Query 向量进行点击计算“关注度”值;每一个 Token 的 V 向量也需要保持

    在上述的计算中,注意到,在一次的新的“自回归”中,最终需要额外计算的就是新Token(这里是“ a”)对应的 Centextual Embedding,该内容计算,需要使用前述所有 Token 对应的 K、V 值,即这里的 K 和 V 矩阵。

    所以,在一次自回归推理中,最好上一次计算的所有 Token 的 K、V 向量都缓存起来,避免重复计算。本次自回归中计算新Token的对应的 K、V 向量也需要缓存,以供后续使用。

    4. KV Cache 的内存消耗

    在推理优化中,一个重要硬限制便是GPU卡的显存(memory)大小。当前,主流的企业级显卡H100显存为80GB,高端显卡 H200 显存为141 GB。现在的 LLM 参数量通常巨大,参数加载就需要耗费巨大的显存,以最新的 llama 4 17B为例,考虑 FP16 (半精度)考虑,则需要消耗约 30+ GB 。卡片上剩余的内存,才是用于实际的推理使用。而每次推理,例如提示词是1000个Token,输出也是1000个Token,那么,在生成最后一个Token的时候,需要的内存(按5%的经验值计算)约为1.5GB。这时候,单个H100的显卡也只能支持约33个并发,实际的情况则要考虑系统内存等,会比这个预估多很多。

    在这篇文章:Mastering LLM Techniques: Inference Optimization@developer.nvidia.com 中也类似的估算:

    • 7B 的模型(如Llama 2 7B),参数是16位(FP16 or BF16)则参数需要消耗约 14 GB 显存
    • Token 数为4096的推理(decoder),则需要约 2 GB KV Cache

    从上述粗略的预估可以看得出来,高效使用显存资源对于 LLM 推理来说至关重要。所以,各推理框架则会通过各种方法尝试去优化“KV Cache”以降低显存使用。这些方法包括“量化”(Quantization)、MQA/MGA 等。

    5. Multi-Query Attention/Group-Query Attention

    可以看到,无论是在模型参数加载的时候,还是推理 KV Cache 阶段,都需要大量的显存。关于 MQA 和 GQA 的经典论文是:GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints

    5.1 关于MQA与GQA

    Multi-Query Attention 则尝试通过减少 \(W^K \, W^V \) 参数的数量来减少上述显存,从而增加推理速度与并发能力。参考下图,可以看到在每一个 Layer 中,所有的 Head 共享一组 \(W^K \, W^V \) 参数,那么这两个相关参数就减少到了原来的 \(\frac{1}{h} \)。

    更进一步的,为了减少上述方法(MQA)对于模型效果的影响,另一个优化是 Group-Query Attention。即如下图,一组 Heads 共享一组 \(W^K \, W^V \) 。可以依照分组的大小,以平衡模型效果与资源使用。如果一个 Head 一组 \(W^K \, W^V \) 则退化到普通的 Multi-Head Attention;如果所有 Heads 分到一组,则退化到普通的 Multi-Query Attention。

    5.2 模型训练 Uptraining

    此外,比较关键的,论文提出了一些关于 GQA 架构的训练优化。

    例如,从一个 MHA 架构开始训练,然后从某个 checkpoint 开始,将MHA模型改成GQA模型,在初始化分组参数时,则使用原 MHA 模型中参数去求一个均值的方式初始化GQA中对应的 \(W^K \, W^V \) 。然后继续使用语料库对于该新模型训练。

    论文指出,这时候只需要使用非常少的计算资源就可以训练处效果还不错的GQA新模型。新的GQA模型,则可以使用更少的显存资源,有更好的并发吞吐能力,同时也达到还比较好的效果。

    参考

  • 在整个大语言模型学习之路中,对 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 云数据库魔力象限保持关注,历史相关文章包括: