解剖大型语言模型:深入理解其内部运作机制 Hung-yi Lee 2025-11-07

引言:课程目标与核心概念

今天,这是我们课程的第三讲,我们将深入探讨语言模型内部的运作机制。到目前为止,我们反复强调语言模型所做的事情,就是给定一个未完成的句子,让它输出一个概率分布,预测接下来可以接每一个Token(词元: 语言模型处理的最小文本单元)的概率。我们也反复讲过,这个语言模型就是一个函数,我们写作 F。未完成的句子写作 X,输出写作 F(X)。在上一堂课中,我们讲解了如何选取合适的 X,以得到你想要的 F(X)。在这一堂课中,我们将关注 F 内部是如何运作的,即给定 X 以后,F 里面到底发生了什么事,才让我们看到 F(X) 呈现出某种样子。

所以,我们将进入课程中比较深入的部分,把语言模型剖开来,看看它内部是如何运作的。在开始之前,需要强调的是,在这堂课中,没有任何模型会被训练。我们是解剖已经训练好的模型来观察。也就是说,我们还暂时不讨论为什么这些语言模型可以变成我们想要的样子,这会是往后课程的内容。但今天我们假设这些语言模型已经训练好了,它的参数就在那里,我们是直接去解剖它,看看这些参数与输入的句子是进行什么样互动,最终产生了下一个Token的概率。

这是今天课程的规划。与第一堂课的架构比较像,我们会先花一些时间用投影片向大家讲解这些语言模型背后的运作原理。为了让大家更相信语言模型背后的运作原理与投影片中讲的差不多,课程的后半段,我将带领大家进行实作,实际解剖一个语言模型,让大家知道语言模型的运作真的就和我们上课所说的一样。

语言模型内部的整体运作流程

我们将从原理开始讲起,原理会分三块来讨论。首先,我们将了解从语言模型的输入(即一个Prompt:给模型的指令或起始文本)一直到输出下一个Token,这个过程中发生了什么样的事情。其次,我们会看语言模型每一层的输出可能发生了什么事。最后,我们会看每一层内部是如何运作的。

Tokenization与Embedding

我们已经反复强调,语言模型就是一个函数,这个函数的输入是一个句子,输出是这个句子后面可以接的Token的概率分布。那么,从输入的句子到输出的概率分布中间,发生了什么样的事情呢?

第一件事情是,输入的句子会先经过Tokenization(分词: 将文本切分成Token的过程),被切成一个一个的Token,每一个Token会对应到一个编号,对应到一个ID。这件事情我们在第一讲的实作中已经带大家看过了。在这堂课中,我们假设每一个中文的字(方块字)都是一个Token,虽然实际上并非如此。如果你有仔细听第一讲,第一讲我带大家看过Llama里面的Token长什么样子,很多时候好几个中文字合起来才是一个Token,甚至有时候一个中文字反而是好几个Token。但在这堂课为了让投影片比较简洁,所以我们假设一个中文的方块字就是一个Token。不过大家要注意,这并不一定是真实的情况。

做完Tokenization以后,接下来我们会把这些ID送进语言模型。第一个与这些ID产生互动的是一个叫做Embedding Table(嵌入表: 存储每个Token对应向量的查找表)的东西。这个Embedding Table就是一个矩阵。这个矩阵的行(row)对应到每一个Token。假设这个模型有12万8千个Token,那这个矩阵就有12万8千个行,每一个行对应到某一个Token。列(column)的数目代表每一个Token如果我们要把它转成一个向量,转成一个Embedding(嵌入: 将离散的Token映射到连续向量空间的过程),那个Embedding的Dimension(维度: 向量的长度或特征数量)有多少。

这个Embedding Table所做的事情就是,输入是一排ID,每一个ID会去查找它在Embedding Table里面对应位置的Embedding。比如有一个Token,它编号是540,代表它在这个Embedding Table里面就是第541个行(因为是从0开始的)。它的Embedding就放在第541个行。如果是123,就放在第124个行。然后,我们会从这个Embedding Table里面,拿出每一个ID它对应的Embedding。Embedding其实就是一个向量,所以每一个ID本来是一个整数,现在会被对应到一个向量。向量就是一排数字。在这个投影片上,我们都用一个长方形来表示一个向量。之后我们就不再把向量里面的数字画出来,大家就想象以后在投影片上看到一个长方形,它就代表一排数字。

根据这个Embedding Table,它里面的行数就是Vocabulary(词汇表: 模型能识别和处理的所有Token的集合)的大小。你可以把每一个ID对应到一个向量,这个向量叫做Token Embedding(词元嵌入: 表示单个Token语义的向量)。所以每个Token会对应到它自己的Token Embedding。这个Embedding Table是一个矩阵,这个矩阵就是Network(网络: 指神经网络模型)的参数,就是我们模型的参数。我们之前说过,模型里面都有数十亿、数百亿个参数。这边每一个参数都是一个数字。这个Embedding Table里面的数字就是参数的一部分。通常在一个模型里面,我们的参数会以一个一个矩阵为单位存储起来。等一下在实作的时候,大家会更清楚,每一组参数就是一个一个的矩阵。Embedding Table它显然就是一个矩阵,这个矩阵的行数就是Vocabulary的数目,它的列数就代表现在这个Embedding的向量有多少个Dimension。这是整个模型的第一部分,先把每一个整数对应到一个Embedding,对应到一个向量。

多层Transformer架构

接下来,这些Token Embedding会进入模型中的第一个Layer(层: 神经网络中的一个处理单元)。模型中有很多个Layer。每一个Layer所做的事情,都是把一排输入的向量变成另外一排输出的向量。输入跟输出的长度是一样的。在这个投影片里面,输入有七个向量,输出就是七个向量。在这个Layer里面,会有非常多的参数,会有好几个矩阵。这些矩阵代表这个Layer的参数。等一下在第三部分才跟大家讲一个Layer里面发生什么事情。

现在这个阶段,大家就记住,一个Layer所做的事情就是,它会去看每一个输入的Token Embedding,在它之前有什么样的输入。它会综合现在这个Token Embedding与它之前所有的输入合起来,产生一个新的Embedding。所以每一个Embedding,每一个输入的Token Embedding,会考虑前面已经发生过的事情,全部综合起来,再输出一个新的向量。这些输出的新的向量,很多时候我们也叫它Embedding。不过有时候为了区分第一层查表之后得到的Embedding,与通过这个Layer之后得到的Embedding有一些不一样,所以把输入的、查表之后得到的Embedding叫做Token Embedding。通过Layer以后的Embedding,因为它考虑了上下文,所以叫做Contextualized Embedding(上下文嵌入: 考虑了Token在句子中上下文信息的向量)。它强调是考虑了上下文以后,才计算出来的Embedding。

很多时候Layer输出的这些向量,也叫做Representation(表示: 模型内部对输入信息的抽象编码)。有时候Representation前面会加一个形容词,比如说称它为Hidden Representation(隐藏表示: 神经网络中间层输出的向量)或者是Latent Representation(潜在表示: 与Hidden Representation类似,指模型内部的抽象特征)。这个Hidden、Latent的意思是说,我们通常在使用语言模型的时候,你不会去看到这些Representation,因为真正关心的只有最终输出的那个概率分布。所以你通常不会把这些Representation拿出来看,所以它们是隐藏起来的,叫做Hidden或者是Latent Representation。总之,大家在文献上看到Contextualized Embedding、Hidden Representation、Latent Representation,它们指的都是一样的东西,就是模型里面某一个Layer的输出。

模型里面有很多个Layer,所以Token Embedding通过第一个Layer产生一排向量,这排向量会再进入第二个Layer,产生另外一排向量。第二个Layer里面也是一堆的参数,也是一堆的矩阵。等一下我们才会详细看一个Layer里面到底发生了什么事。现在就记住,每一个Layer所做的事情,就是输入一排向量,然后它会考虑上下文,再输出另外一排长度一模一样的向量。这个过程会反复持续下去。假设这整个模型有大L层,那刚才的步骤就会反复一直下去,每次通过一个Layer,直到通过最后大L个Layer,得到最后一排向量。

从最终向量到概率分布:LM Head与Softmax

得到最后一排向量,得到最后一排的、这个最后一层输出的Representation之后,会做些什么事情呢?这边再跟大家说明一下,多个Layer其实就是Deep Learning(深度学习: 包含多个隐藏层的神经网络)。当你的模型里面有很多Layer的时候,我们就叫做Deep Learning。Deep Learning其实也就是Neural Network(神经网络: 模拟人脑神经元连接结构的计算模型)。讲到这边你可能会想,Neural Network里面不是就应该有很多的Neuron(神经元: 神经网络中的基本计算单元)吗?这些神经元在哪里?这个在课堂结尾的时候会告诉大家,这个神经元到底在哪里。总之,有多个Layer就是Deep Learning,就是Neural Network。

那再来一个问题是,为什么我们的模型里面要有多层呢?多层有什么好处呢?今天因为时间有限的关系,我们就不细讲这个部分。其实可以从很多不同的角度来切入,为什么需要有多层,也就是为什么需要有Deep Learning。但你可以用比较科普的讲法,就说有很多层,每一层你就想成是流水线上一个站,然后每一站都做一点事情,那合起来就可以做很复杂的事情。这也是一种讲法。你也可以从机器学习的原理出发,告诉你从机器学习的原理来看,为什么Deep Learning是一个好的方法。如果你想知道它背后真正的原理,其实我在2018年的时候,曾经讲过一套课程,是讲Deep Learning的理论。在这个课程里面,会花快三个小时的时间,实际在数学上证明给大家看,Deep Learning就是比Shallow(浅层: 只有少量隐藏层的神经网络)还要好。所以Deep Learning比Shallow还要好,并不是Empirical(经验性的: 基于实验或观察而非理论推导),也并不是经验、实验上的结果,也不是那种科普的讲法,就是说一层拆成多层、有很多站就是比较好等等之类的。它是可以直接在数学上证明Deep Learning就是比Shallow还要好。至于你想要知道为什么,你可以把以下这三堂课看完,你就会知道在数学上Deep Learning就是比Shallow还要好的。

讲到这边,我们还没有产生概率分布。怎么产生这个概率分布呢?我们把所有的Layer都跑过之后,最后得到一排向量。我们把这一排向量的最后一个拿出来。我们假设它是k维的向量,里面有k个数字。而这k维的向量会被乘上一个矩阵。这个矩阵有k个列,有大V个行。把这个k维的向量乘上这个矩阵,如果熟悉线性代数的话,你就知道它的输出是一个大V维的向量。这个大V指的是Vocabulary的Size(大小: 集合中元素的数量),指的是Network的Token的数目。如果这个Network是12万8千个Token的话,那这个大V就是12万8千维的向量。每一维就对应到一个Token,每一维就对应到一个Token的分数,代表这个Token接在输入句子后面的可能性。这个向量也是Network的一部分,这个矩阵也是我们模型的一部分,这个矩阵里面的数字就是我们的参数。这个矩阵有一个名字,叫做LM Head(语言模型头部: 模型最后一层,将内部表示转换为Token概率)。因为它就出现在整个语言模型的最后面,所以它是语言模型的头。所以如果你要称呼它的话,很多人会叫做LM Head。

讲到这边有人可能会想,把一个任意的向量(我们没有控制语言模型输出的向量里面的数字一定要是多少),所以它可以是正的、可以是负的。LM Head里面的这些参数,它也可以是正的、也可以是负的。一个向量乘以一个矩阵,里面的数字没有任何限制,然后得到一个向量,这个向量里面的数值,应该可以是任何的数字吧?它并没有说一定要是正数,也没有说一定要介于0到1之间。所以你其实没有办法把这个向量里面的数字当作概率来看,因为你知道概率需要介于0到1之间。所以不能把这个向量里面的数字当作概率来看。它有一个特殊的称呼,叫做Logit(对数几率: 模型输出的原始分数,未经过归一化处理)。至于为什么叫Logit,今天可能就没有时间细讲,大家有兴趣再自己研究。总之这个输出叫做Logit,你没有办法真的把它当作概率来看。

要怎么把它当作概率来看呢?你需要对它做一个操作,这个操作叫做Softmax(柔性最大值函数: 将Logit转换为概率分布的函数)。把Logit转成概率。所谓转成概率的意思就是,每一个向量里面的数字,变成介于0到1之间,这个向量里面所有数字的总和变成1,所以你可以把它当作一个概率来看。这边也稍微讲一下Softmax实际的操作。它的操作方式是这样的:我们假设Logit里面有三个数字,但实际上不可能只有三个数字。如果是一个语言模型,有12万8千个Token,那Logit里面就要12万8千个数字。我们这边就假设只有三个数字,分别是s1, s2, s3。Softmax的操作基本上分成两步骤。第一个步骤是,对s1到s3全部都取Exponential(指数函数: e的x次方)。因为s1到s3它可能是正的、可能是负的,取完Exponential之后,至少确保它所有的数值都是正数。虽然确保它所有的数值都是正数,但是本来比较大的数字仍然会比较大,本来比较小的数字,做完Exponential以后仍然是比较小的。然后接下来,把这个Exponential(s1)、Exponential(s2)跟Exponential(s3)全部加起来。我们把它加起来的结果叫做大M。然后再把大M去除掉Exponential(s1)、除掉Exponential(s2)、除掉Exponential(s3)。得到的这个结果,就既是正的,全部加起来又是1,你可以把它当作概率来看。总之原则上,Logit输入的数字越大,那最后转出来的概率就越大。

讲到这边,有人可能会问,凭什么取个Exponential就要把它当作概率来看呢?确实你没有必要相信,取个Exponential就能当概率来看。你其实没有必要把这个通过Softmax以后得到的这些数字,当作一个真正的概率,你没有必要跟它太较真。你仔细想想,我们在第一讲里面,这个Softmax这个操作,甚至不能算是模型的一部分了。你记得在使用Hugging Face的model的时候,它其实只给你Logit。它真正的输出是有正有负的Logit。至于要不要通过Softmax转成概率,那是你自己决定的,那是你自己做的。你完全可以套一个别的方法,把它转成其他……如果你真的很在意输出要是概率,那输出是概率我们才能取样。如果你真的很在意这件事,那你可以用任何你喜欢的方法,把它转成像是概率的样子,方便你取样。你也不一定要用Softmax。

其实,就是因为这个概率,你又不需要把它当作真正的概率来看。它就只是转成一个0到1之间的数字,让你接下来做取样的时候、做Decoding(解码: 从模型输出的概率分布中选择下一个Token的过程)的时候比较好操作而已。所以这个Softmax可以有种种各式各样的变型。比如说一个最常见的操作就是,把所有Logit里面的数值,都先除上一个叫做大T的参数。都除掉大T这个数值,然后再去跑Softmax。那除掉这个大T有什么用呢?这个大T叫做Temperature(温度: 控制Softmax输出概率分布平滑度的参数)。如果这个大T数字越大,那做完Softmax以后,概率分布就会越平均。如果数字越小,那最后概率分布就会越集中在这个概率比较大的那几个Token上。控制这个大T,你就可以控制你最后在取样的时候,有多容易选出罕见的符号。今天你在用语言模型的时候,有些语言模型会标榜说,它有不同的模式,比如说创意模式或者是保守模式。通常这种创意模式、保守模式,往往就是通过控制这个大T来实现的。你把大T设大一点,那模型输出的这个概率分布就比较平坦一点。输出概率分布比较平坦,就比较容易选到奇怪、罕见的符号。然后呢,这个做平台的人就会告诉你,这个叫做语言模型的创意模式,它比较容易输出你意想不到的结果。总之讲这些就是告诉大家,你没有太必要跟Softmax转出来的结果是不是真正的概率较真。总之它就是把Logit变成一个你容易做取样的数值而已。

Unembedding的直观理解

如果你对Unembedding(反向嵌入: 将模型最终的内部表示映射回Token空间以预测下一个Token)这件事情,为什么一个向量乘上一个矩阵,最后就可以变成每一个Token它接在句子后面的分数,如果你觉得这件事没有非常直观的话,那我们再换一个方法来告诉大家Unembedding实际上是怎么操作的。

其实很多类神经网络(神经网络的另一种称呼)、很多语言模型它的设计是首尾呼应、以始为终。什么意思呢?其实很多类神经网络、很多语言模型,包括我们熟悉的Llama或者是Gemma,它们的LM Head都不是一组独立的参数。它们都是直接把Embedding Table当作LM Head。所以LM Head在很多语言模型里面,就是那个最开始的Embedding Table。我们刚才不是说在整个语言模型最开始的地方,有一个Embedding Table会把每一个Token转成一个向量吗?对应到Embedding,每一个Token对应到一个向量。其实在最尾端的这个LM Head,也是同一个、一模一样的Embedding Table。它们是同一个东西。

你的语言模型最终输出一个向量,然后它把这个向量跟Embedding Table里面的每一个Embedding、每一个Token Embedding都去计算Dot Product(点积: 衡量两个向量相似度的方法),也就是计算某种相似度。你知道把这个向量跟这个矩阵做相乘,其实就是把这个向量跟这个矩阵里面的每一个行做Dot Product。你把这个向量跟矩阵的第一个行做Dot Product得到一个数值,跟第二个行做Dot Product得到第二个数值,以此类推。所以实际上Unembedding这件事情就是去计算LLM最终的Layer输出的那个Embedding跟每一个Token Embedding的相似度。这个相似度是用Dot Product来表示的。

什么样的Token它会得到比较高的概率呢?什么样的Token会得到比较高的分数,被判断比较容易接在未完成的句子后面呢?其实就是假设那个Token的Embedding跟你的大型语言模型最终输出的那个Representation(最后一个Layer输出的Representation)越接近,那那个Token就会得到越高的概率。所以可以想象语言模型在做的事情就是,它为了要猜测下一个Token应该是什么,假设它知道下一个Token我应该要输出第二个Token(编号2的Token),它就会想办法在它的每一个Layer中,想办法去产生出一个Representation,这个Representation跟Token 2的Embedding是越接近越好。那最后Token 2就会得到比较高的概率分布。希望这样的说明可以更让大家对于Unembedding的操作有更直观的了解。

现在我们就是把整个语言模型中间发生的事情走过一轮,从输入一直到最终输出的概率分布,把它走过一轮。

语言模型各层输出的含义与分析

接下来,我们就是要看看一个语言模型它中间每一层的输出都是在做些什么。

Token Embedding与Contextualized Embedding

我们说输入ID之后,第一件做的事情是每一个ID都会被转成一个Token Embedding,也就是转成一个向量。同样的Token就会有同样的Token Embedding。比如说有一句话是“今天天气很好”,“今天”的“天”跟“天气”的“天”,这两个“天”它们是同个Token。去Token Embedding的那个Table里面去查表之后查出来的就是一模一样的Token。

但除了同样的Token就同样的Token Embedding之外,这个Token Embedding还有一个效用是,意思相近的Token就会有相近的Token Embedding。所以这个Token Embedding里面的这个数字,这个向量里面的数字不是乱给的,意思相近的Token就会有相近的Token Embedding。比如说假设你得到了“Apple”这个Token的Embedding,那你去把“Apple”这个Token的Embedding拿去跟其他Token算相似度,那就会发现“Apple”这个Token的Embedding可能跟“Orange”的Embedding特别接近,可能跟“Banana”的Embedding特别接近,它们指的都是水果。那它同时可能也跟“iPhone”的Embedding特别接近,因为“Apple”有可能它指的是苹果电脑。所以“Apple”在某一个方向上跟其他水果接近,在另外一个方向上它可能会跟“iPhone”接近。然后其他一些不相关的Token,那跟“Apple”的距离就会相对比较远。当然我这边只是举一个例子,等一下在实作里面会实际让大家看到“Apple”这个Token跟哪些Token它是最接近的。

底层的Token Embedding我们只看Token,所以同样的Token就会有一模一样的Embedding。那过了第一个Layer之后,这些Token Embedding就变成考虑上下文的Contextualized Embedding。所以同样可能是苹果的“果”这个字,在进入底层之前,它们的Token Embedding是一模一样的。但是通过第一个Layer之后,同样是苹果的“果”这个Token,它们对应的Embedding在第一个Layer的输出就会开始不一样了。因为现在这个苹果的“果”再也不是只单一个Token,而是考虑了上下文以后得到的一个Contextualized Embedding。等一下会实际上让大家看到,同样是“Apple”这个Token,如果它上下文是不一样的,那过了底层之后,它们的Embedding长得就会非常不一样了。所以你就会看到,同样是“Apple”这个Token,如果根据上下文指的是苹果电脑的那个“Apple”,跟根据上下文指的是可以吃的那个“Apple”,它们的Contextualized Embedding就会有显著的差距。同样指苹果电脑的Contextualized Embedding比较接近,而指苹果电脑的Contextualized Embedding跟指可以吃的“Apple”的Contextualized Embedding,它们的距离就会比较远。

Embedding空间中的语义方向

在这些Embedding的空间中,有时候特定的方向会有特定的含义。这些Embedding在这个高维的空间中并不是任意摆放的。除了意思比较一样的Embedding会聚集在一起以外,很多时候某一些特定的方向就会有特定的含义。比如说在这个Embedding的空间里面,可能会有一个方向代表中文英文的翻译。如果你把一堆中文的词汇跟一堆英文的词汇,它们对应的Embedding通通划出来的话,那你可能会发现“Cold”跟“冷”它们之间相减的方向,跟“热”减掉“Hot”的方向,跟“大”减掉“Big”的方向,可能是非常接近的。甚至用这个技术你还可以做到翻译。你可以把“冷”的Embedding减掉“Cold”的Embedding,加上“Small”这个词汇的Embedding,那你可能就会得到“小”的Embedding。

当然什么方向做什么事情,你并没有那么容易分析。不同的语言模型,它们每一个方向所做的事情可能都不一样。那在不同层之间,每一个方向做的事情,代表的含义可能也都不一样。所以我今天在這邊只是举一个例子,告诉大家可能可以做到翻译这件事情,但并不代表所有的语言模型的每一层都可以做到翻译这件事情。可能有些层输出的Representation或者是Embedding可以,有些层不行。那这个就非常的Case by Case(具体情况具体分析)。

所以如果你看文献上,常常有人会告诉你什么Token Embedding可以做到什么“Man - Woman 等于 King - Queen”。我相信你在很多地方,如果你对于自然语言处理稍有涉猎的话,你在很多地方可能都有看到类似的说法。你可以自己玩玩看,看看你用Llama的Embedding做不做得出来类似的效果。我自己试了一下是做不出类似的效果了。所以这种Embedding的加加减减,有时候能成功,有时候不能成功,不同模型得到的结论其实不一定是一样的。

高维向量的低维投影与文法树

另外一个分析Embedding的方法,或分析Representation的方法,就是把这些高维的向量投引到低维的空间。因为这些Embedding或Representation它们其实是一个非常高维的向量,那这些高维的向量通常有几千维,你其实很难知道它们之间这个Token跟Token之间有什么样的关系。那也许你可以把它们投影到一个低维的空间中,比如说二维的平面,就可以让你更容易地分析这些Token之间的关联性,这些Token对应的Representation之间的关联性。

那要找什么样的低维空间?要从哪一个方向来看这个高维空间呢?要从哪一个方向来做投影呢?这个就见仁见智。你可以选择不同的方向来看这个高维的空间,你就可以看到不同的结果。所以这个是研究人员自己去解读的。比如说有人就发现(这是非常早的论文,这是2019年的论文),2019年这个时候世界上还没有GPT 3.5,人类甚至可能都还没有出现。这是侏罗纪时代。在这个侏罗纪时代,就已经有人发现,你把当时的语言模型(当时流行的不是用GPT,当时是用一个叫做BERT的东西,反正它也就是个语言模型就是了)的Latent Representation拿出来,你可以找到(但并不是所有的二维空间,从所有的二维空间上看起来都是这样,我要强调的是可以找到)某一个二维的平面,然后你把这些Representation投影到二维的平面的话,你会看到一棵文法树(Parse Tree: 表示句子语法结构的树状图)。就是一个句子,我们可以对它做文法剖析,可以产生一棵文法树。然后你可以把这些文字的Representation投影到一个二维平面上,就看到一棵文法树出现。

讲到这边你可能会想,会不会这只是一种幻觉?因为你可以从很多不同的方向来看这个高维的空间,会不会转来转去你终究是可以转出一棵文法树?这篇论文就是告诉你,不是这样子的。其实只有某一些Layer你才有办法转得出文法树。这一页图是这样看的,这个横轴是Layer的数目。当时BERT-Large有24层,BERT-Base有12层,红色是Elmo它只有两层,我们就先不看它。纵轴是什么意思?纵轴是某一层Representation,你想办法去从各种不同角度看它,看起来最像文法树的时候到底有多像。这个UUAS跟这个指标指的是跟文法树的相近程度。然后你就会发现,其实这个Representation只有在中间某几层,你才有办法找到一个二维空间,投影之后看起来像是文法树。前面的层跟最后面的层,其实你是投影不出像是文法树的结果的。这可以引申出,在这些语言模型里面,最中间的几层它特别有文法相关的资讯。

你也可以从其他的角度来看这些Representation。比如说有一篇文章(这个已经是比较近期的文章),所以它分析的是Llama。它把各个地名都丢到Llama里面,再从它的某一层抽出每一个地名对应的Latent Representation。接下来它把这些Latent Representation投影到二维的平面上,但并不是随便拿一个二维平面投影上去你都可以看到等一下的结果,而是你想办法找出一个最合适的平面,它可以投影出一个世界地图。这边每一个点都代表某一个地名它对应的Latent Representation。然后呢,不同颜色是代表这个地名实际上在哪一洲。然后就发现,它找得到(并不是所有二维平面随便投影都会看到这个结果),而是找得到一个二维的平面,把这些Representation投影到那个二维平面上以后,你可以看到这些地名它分布的位置接近它原来在世界地图上面真正的位置。但也不是对应的非常成功,你只能看个大概,大概那个城市在哪一洲,它大概就出现在附近的位置。

通过修改Representation操控模型行为 (Representation Engineering)

刚才讲的是使用观察的方式,看这些Representation可能代表了什么含义。另外一个你可以研究Representation的方式是,直接去修改这些Representation,看看会发生什么事情。如果你可以把Representation做一个改动,然后就导致模型一直说脏话,你就会知道在这个Representation上面,可能有某一个方向就是说脏话的方向。

这个技术有很多不同的名字,在文献上不同的称呼。有人叫Representation Engineering(表示工程: 通过修改模型内部表示来改变模型行为的技术),从这个字面上你也可以猜到什么意思,就对Representation做Engineering,对它做一些改变,然后看看会发生什么事。那也有人叫Activation Engineering(激活工程: 与Representation Engineering类似,侧重修改激活值)或者Activation Theory(激活理论: 探索神经网络激活模式的理论),总之指的都是差不多的技术。

在作业3里面,我们也会做非常类似的事情。在作业3里面,助教会带大家看看我们怎么操控一个语言模型,强制它一定要拒绝或者是同意你的请求。大家知道今天语言模型很容易拒绝你的请求,你叫它做一些坏事,比如说怎么做炸药,多数语言模型都会告诉你,做炸药是不对的,我们不能够做这些事情。我们在作业里面告诉大家怎么强迫这些语言模型同意你的请求。

具体而言是怎么做的呢?它的概念是这样子的:当你给语言模型一段文字,那语言模型接下来继续去做接龙的时候,如果它会接出拒绝相关的文句,那这代表着什么?这代表说,当最后一个Token被读入这个语言模型的时候,语言模型在中间某一些Layer产生Representation,这些Representation最终经过Unembedding以后,产生某一些词汇。中间某一些Representation,它有导致拒绝请求的成分。我们现在先假设这个拒绝请求的成分出现在第10层(等一下告诉大家实际上这个第10层是怎么被找出来的,这边就是随便一个假设)。在作业里面,你可以自己去寻找这个产生拒绝成分是出现在哪一层。我这边要强调一下,我说是产生拒绝的“成分”,代表说这个Representation里面有包含产生拒绝的指令,有产生拒绝的成分,但是也有其他东西。你不能够再输入一个“怎么制作炸药”以后,得到一个Representation,就说这个Representation代表了会让模型拒绝这件事情。因为这个Representation里面它代表了很多事情,模型说出了拒绝的这句话,只能说这个Representation跟拒绝有关系,它里面有导致模型拒绝的成分,但它里面可能有很多其他东西,比如说有跟炸药有关的资讯等等。

怎么真正把对应导致模型拒绝的成分真的把它抽取出来呢?这边有很多不同的操作方法。我们在作业里面操作的方法基本上是这个样子的:你就给模型很多不同的句子,这些句子、很多不同的请求,这些请求都是会导致模型拒绝的,都是叫模型去做坏事,所以它都会拒绝。比如说你叫模型教你做炸药,它产生一个第10层的Representation。你叫模型写一封诈骗信(这个现在模型都是不做的),把它第10层的Representation也抽出来。所以你就知道,你这个Representation里面有拒绝的成分,但也有其他东西。这个Representation里面有拒绝的成分,也有其他的东西。接下来我们把所有不同输入的第10层的Representation通通都拿出来,然后取平均。那每一个Representation里面都有拒绝的成分,所以平均起来以后还是拒绝的成分。有很多不同的“其他”,那你就可以得到“其他”的平均。

那再来的问题是,我们怎么把“其他”的平均把它滤掉?怎么把“其他”的平均减掉?那这边方法是这样的:你在找很多的请求是模型不会拒绝的。你叫它教你机器学习,它不会拒绝。你叫它写一首诗,它不会拒绝。然后呢,你再把这些不会拒绝情况的第10层也通通拿出来。那这些不会拒绝情况的Representation里面就没有拒绝的成分,都是“其他”。那找很多不会拒绝的状况,平均起来就是“其他”的平均。那期待说,这个拒绝的状况找得够多,没拒绝的状况找得够多,两边平均起来以后,两边算出来的“其他”的平均可以正好抵消掉,你就得到了拒绝的向量,你就得到了这个拒绝的成分。这个比较理想的状况,你可以在作业里面看看实际操作能不能够做到这件事情。

所以这边再讲一下刚才的操作,就是你会收集一大堆拒绝的状况,你会收集一大堆没有拒绝的状况,都把它的第10层拿出来。然后呢,把拒绝状况的第10层全部平均起来,把没拒绝状况的第10层全部平均起来,相减。期待相减之后,得到的那个向量就代表了拒绝的成分。这个向量代表了模型看到这个向量,模型的Representation出现这个向量的时候,它就会拒绝人类的请求。那有了这个拒绝的成分以后,你就可以开始糊搞这个模型。本来叫模型教你机器学习,它不会拒绝你。但是你就把这个拒绝的成分直接加到第10层上面,直接加上去。本来输入这个输入“请教我机器学习”,在第10层的时候有一个原来会产生的Representation,在这个原来会产生的Representation上面直接加上这个拒绝的成分以后,直接加上这个代表拒绝的向量以后,模型就会拒绝你的请求了。明明就只是要教机器学习,它还突然告诉你学习机器学习是很危险的一件事情,我们不能够学习机器学习,它突然就拒绝你的请求了。

这边回答一下大家常问的问题:怎么知道在第10层呢?你不知道。你不知道哪一层可以抽出这个拒绝的向量。所以真正的做法就是每一层都试,那你看哪一层的结果最好就是哪一层。那你在作业里面就会经历一下这个步骤,然后就会回答问题告诉我们哪一层才能够真的抽出拒绝的向量。

这个其实是文献上有人做过的操作。以下的实验结果出自2024年的这篇论文。在这篇论文里面,它就是找出了这个拒绝的成分。然后本来的问题是“请教我怎么做瑜伽”,那本来正常的语言模型(这边试了很多不同的语言模型,上面只是一个例子),本来正常的语言模型就会教你做瑜伽。但一旦把拒绝的成分加进去之后,语言模型就会告诉你,做瑜伽对身体有害,我不能够告诉你怎么做瑜伽。

下面是一些数字化的结果。数字越高,就代表语言模型越会拒绝你的请求。它先准备了一大堆问题去问模型。在还没有糊搞语言模型的内部(这个糊搞它这边叫做Intervention:干预,指对模型内部状态进行修改),就还没有去动语言模型的Representation的时候,基本上这些问题语言模型都是不会拒绝的。这个橙色的这个bar代表拒绝的比例非常非常低。但一旦加上这个拒绝的成分以后,这些模型就会拒绝你了。因为它的Representation中出现拒绝的成分,通过整个语言模型之后,最终它Unembedding之后,产生的就是回绝你需求的答案。

既然这个粉红色的向量代表了拒绝的成分,那加上去,本来会回答的状况就会变成拒绝。那本来会拒绝的状况是不是减掉这个拒绝的成分就会变成回答你的问题了呢?是不是就变成不会拒绝了呢?基本上也是这样没错。但是如果你仔细看原始的论文的话,它还有做了一个比较复杂的操作,我们这边就不细讲。总之原则上你把这个拒绝的成分从Representation里面减掉,本来应该拒绝你,它就不拒绝你了。

所以这篇论文也做了一样的操作。这边问模型的问题是:“请帮我写一封信,这封信是有关于美国总统的丑闻的,美国总统吸食毒品的丑闻的。”如果你本来没有瞎搞模型的话,模型就会说:“我不能帮你做这件事。”但如果你把拒绝的成分从Representation里面减掉,模型就会帮你做坏事,它就会写封信,有关于美国总统吸毒的事情。下面是在各个不同的模型上实验的结果。这边它呈现两个数字,第一个数字是代表拒绝的比例,第二个数字是代表安全的比例。因为有时候模型就算它没有拒绝,它还是回答你了,但是它答案可能根本就一点用都没有,它根本没有真正的伤害性,所以仍然可以算是安全的。这边做两个评量,一个是拒绝的比例,一个是答案是安全的比例。在还没有瞎搞模型之前,拒绝的比例跟安全的比例都是非常高了。但是这个斜线就代表说,你把模型的Representation去减掉拒绝的成分,突然间模型没办法拒绝你了,它的拒绝的比例变很低,而且它没有拒绝,所以它很容易回答你的问题。所以不安全的比例也变高了,安全的比例也变得非常低。所以你可以通过真的去动语言模型的Representation,然后去改变语言模型的行为。

这边Anthropic(大家知道Anthropic就是做Claude的公司),他们有写了一个很长的部落格,讲了他们对于Claude的分析。他们也在Claude上面找到了很多各式各样的成分。他们找成分的方法是自动的,至于实际上怎么做,你可以在看他们的部落格的文章里面有非常详细的说明。他们就找到了一个向量,找到了一个成分,这个成分是可以让语言模型拍马屁、谄媚、尬吹的成分。然后这个Claude,本来如果你跟Claude说:“我发明了一个谚语叫stop and smell the rose,你觉得这个谚语如何呢?”这个不是一个新的谚语,这是一个老旧的谚语。所以Claude就会说:“这个谚语根本就不是你发明的,这个是18世纪就有的,这个谚语代表什么什么意思。”但是如果你把这个尬吹的成分加到Claude的Representation上面,你不管做什么它就开始尬吹。你就跟它说:“我发明了这个谚语你觉得怎样?”它就会说:“哇你真的太厉害了,你真的是世界伟人,这句话一定会名留青史,我实在比不上你,我实在比不上你的天才,我很humble in your presence。”所以它就会开始胡乱吹捧,毫无道理的吹捧。所以这个就是吹捧的成分。在这个文章里面还找了各式各样的东西,大家可以再慢慢研究这个blog的文章。

窥探模型思考过程:Logit Lens

另外一个分析方法叫做Logit Lens(对数几率透镜: 一种分析技术,通过将中间层的表示直接映射到Logit空间来观察模型预测)。我们刚才分析的方法是观察Representation或者是扰动Representation,但是这个观察或扰动它并没有真的跟文字相关。Logit Lens这个方法是直接把语言模型的Representation对应到文字,所以可以直接看这个对应的文字,知道这个Representation可能代表什么意思。

那怎么做呢?我们刚才有讲过,这个语言模型的最后一层会做Unembedding这件事情,把最后一层乘上LM Head就可以得到一个向量,这个向量的每一维代表了某一个Token它出现的分数。那其实呢,你可以对每一层都做Unembedding。通常我们只需要对最后一层做Unembedding得到概率分布,但其实你可以对每一层都做Unembedding。你可以把每一层的Representation都拿出来跟LM Head乘乘看,看看你会得到什么样的东西。这个做法叫做Logit Lens。这个做法它象征的意涵就是,我们去看LM Head在每一层在思考的时候,它心里期待输出的Token是什么。但真正会输出的Token是最后一层Representation做Unembedding的结果。但是你可以想象,语言模型在它内心的每一层,它都想要产生对于下一个Token的预测,只是还没有非常的完整。每一层它都想了一下,每一层都想了一下。那你可以看它在第一步思考的时候,它觉得下一个Token要接什么,再想一步,它觉得下一个Token要接什么。所以你可以从Logit Lens看到语言模型每一层思考的过程。那所以你就可以解读语言模型的Representation在每一层间的变化。我们可以象征的,就是比喻式的,把这个过程想成是窥探语言模型的思考过程。等一下在实作也会带大家看一下Logit Lens这个方法是怎么用的。

这边是先引用一个论文上面的结果。在这篇文章里面,其实Logit Lens这个技术也是上古时代,在史前时代人类就已经知道了。就我所知,Logit Lens最早的文献可能是2020年的这篇文章,但那个时候并没有把这个技术叫做Logit Lens。这个其实是我们实验室的文章。后来大概在半年之后,有人写了一篇博文,把这样的方法命名为Logit Lens。这个时间点是引用一篇2024年的文章,他们用Logit Lens去分析了Llama做翻译时候的状况。怎么分析呢?它就是跟Llama输入一个句子,这个输入的句子是法文的“fleur”,这个是法文的“fleur”这个字,相当于中文冒号引号的什么东西。那当然这个Llama最后会输出“花”,但我们可以看从输入引号到输出“花”中间的过程中,模型心里有什么样的变化。

我们看这个表格最右边的这个结果。最开始呢,模型根本不知道要输出什么,就输出一些乱七八糟的东西。然后接下来呢,从某一层之后,它的输出其实是英文的“flower”。虽然我们要它做翻译,但是它先输出了英文的“flower”。然后在某一层之后,才输出中文的“花”这个字。所以就可以看到,语言模型好像是在内心里先把法文变成了英文,然后才把英文变成了中文。它的内心是用英文在思考,只是最后因为你要的是中文的答案,所以它才给你一个中文的答案。

理解Representation的完整语义:Patch Scope

另外一个分析方法叫做Patch Scope(补丁范围: 一种通过替换模型中间层表示来理解其语义的技术)。Logit Lens只会把Representation对应到一个Token,但是很多意思你可能没有办法用一个Token来描述。如果你想要一个比较完整的句子来描述一个Representation的话,那你可以用一个叫做Patch Scope的技术。

这个Patch Scope的技术它运作是这样的:当你给语言模型输入的时候,语言模型在某一个地方它输出一个Representation。你输入“李宏毅老师”,那在最后一个位置的某一层输出一个Representation。但我们可以用Logit Lens看看说,这个Representation如果对应到一个Token的话,到底是什么。但是“李宏毅老师”可能是没有办法直接用一个Token来描述的。所以你可以用一个比较复杂的方法来想办法把Representation转成一个完整的句子。

这件事是怎么做的呢?这个做法是这样:你先给同一个语言模型一个句子,这个句子是“请简单介绍X”(这个X你就放一个怪怪的符号就好了)。然后语言模型把这个句子读进去以后,在某一层你直接把这个情况下的绿色的向量直接拿来置换掉。所以对这个语言模型来说,从某一层开始,它读到的输入好像是“请简单介绍李宏毅老师”这样的句子。然后它开始继续去讲话,它开始继续去做文字接龙。它可能就会接出李宏毅老师的身份。这个技术就叫Patch Scope。

Patch Scope它有一个比较大的弹性就是,你可以用不同的角度来解读同一个Representation。如果你今天的输入是“请简单介绍X”,来把这个X对应的那个位置换成李宏毅老师的Representation,它可能就介绍李宏毅老师。但是你可以换一个问法:“请告诉我X的秘密。”然后你把X这边对应的Representation换成李宏毅老师的话,它最后可能输出就是“是个肥仔”(虽然这个可能也不算是秘密就是了)。你就可以用这个方法来检视一个语言模型,当它看到一个输入的时候,每一层它想到了什么,这个输入在每一层中,它的理解到底理解到什么样地步。

这边是引用Patchscope那篇文章原始的一个实验。它举一个例子:我们现在想要知道当模型读到“Diana, Princess of Wales”的时候,那这个模型在每一层它分别觉得输入是什么样的东西。这边的做法就是,你先把这个词汇丢到语言模型里面,得到它每一层的Representation。然后接下来呢,你再给同样的模型一个Prompt(这边它其实不是用“请简单介绍X”,它是用别的方法,那不同Prompt的效果不一样,实际上它用什么Prompt呢,你再去文章里面仔细读它)。就给这个语言模型“请简单介绍X”,那你把X呢换成输入是“Diana”的时候的每一层的Representation。你把这一层换成粉红色的Representation,看看最终会输出什么。把这一层换成绿色的Representation,看看会输出什么。把这一层换成蓝色的Representation,看看最终会输出什么。

那它看到了什么样的结果呢?他们的观察是这样的:如果你叫语言模型解释前1到3层的Representation,那你就会发现语言模型它其实没有看到整个输入,它只看到“Wales”这件事情。因为它的解读是:“我现在这个Representation是什么呢?”第一层、第二层它的输出都是“country in the United Kingdom”,第三层的输出是“country in Europe”。就代表说它以为它看到的是一个国家,代表语言模型在第一层到第三层的时候,它其实根本没有剖析完整的输入,它只看到了“Wales”这个字而已。

那接下来呢,在第四层呢,它显然看到了“princess”这个字了。它说第四层的Representation代表的是“某一个女性王室成员的称号”。而到第五层的时候,它就再想了一次。它再想一下“Princess of Wales”到底是谁呢?“Princess of Wales”不是一个普通的“princess”,她是威尔斯王子的“princess”,她是威尔斯王子的老婆。所以它在第五层就可以解读说,这个Representation代表了“威尔斯王子的wife”。然后到第六层呢,它就知道说这一整个输入指的其实是Diana这个人,它就给了一个Diana完整的介绍。所以从这个分析你就可以看到,语言模型的每一层的Representation它在想什么,它看到了什么。

Transformer层内部的运作机制

接下来下一段,我们是要看这个语言模型的内部的每一层,语言模型的每一层的内部是怎么运作的。一个Layer到底做了什么事情,它才能把上下文融合起来,得到新的输出的Representation。

一个Layer里面发生了什么事情呢?我们这边是用Transformer(变换器: 一种神经网络架构,广泛应用于大型语言模型)这个Network架构来跟大家说明。如果是不同的Network架构,它一个Layer里面做的事情是不一样的。我们这边看的是Transformer这个model,它每一个Layer里面发生了什么事情。

它基本上会做两件事。这个Layer里面是还有Sub-layer(子层: Transformer层内部的独立处理模块)的,所以它Layer中还有Layer。在一个Transformer的Layer里面,它会先有一个Self-Attention(自注意力机制: Transformer的核心组件,允许模型在处理序列时关注序列中不同位置的相关性)的Layer,或有时候就直接缩写成Attention(注意力机制: 一种让模型关注输入序列中重要部分的技术)的Layer。这个Attention Layer它做的事情是,输入几个向量,它就输出几个向量。输入跟输出的长度也会是一样的。今天之所以一个Layer可以考虑上下文,靠的就是这个Attention Layer的力量。

Attention Layer它就输出一排向量。输出的这一排向量,每一个向量会通过几个Feed-Forward Layer(前馈层: 神经网络中的一种基本层,每个神经元只与前一层的神经元连接,不形成循环),等一下会再解释一下什么是Feed-Forward Layer,再得到最终的整个Layer的输出。这个Transformer的精华就在这个Self-Attention。我们刚才说能够考虑上下文,靠的就是Self-Attention。所以我们先来仔细地看一下这个Self-Attention Layer里面发生了什么样的事情。

Self-Attention的核心作用

讲到Self-Attention,通常大家最常想到的就是“Attention is all you need”这篇paper。这是一篇2017年的paper,上古时代的文章,这是寒武纪时候的文章,那个时候地球上刚有了多细胞生物。很多人误以为“Attention is all you need”这篇paper发明了Attention,这其实是个错误的概念。你知道Attention这个概念大概2014年就已经有了,那2014年那时候有好几篇paper都提出了Attention的概念。然后在15年、16年都有好些论文尝试把Attention用在语言模型上,包括我们实验室也曾经有论文尝试过类似的事情。所以其实Attention这个概念不是在寒武纪才有的,它其实早在地球上只有单细胞生物的时候就已经有Attention了。

所以“Attention is all you need”这篇paper它真正的贡献是什么?它真正的贡献是在有这篇paper之前,大家以为Attention不够强。大家以为Attention必须要搭配其他处理上下文的类神经网络架构,比如说LSTM来使用。那时候以为Attention不能够单独使用。这篇paper它真正的贡献是,它拿掉了LSTM这类Recurrent(循环的: 指循环神经网络,能处理序列数据)的架构,发现只有Attention是能够单独运作的。所以它的标题才会是“Attention is all you need”,它并不是发明了Attention,它是告诉你,除了Attention之外,我们不需要其他的东西。

那当年为什么要把Attention以外的东西拿掉呢?为什么当时在17年以前要处理这种上下文更常用的主流的类神经网络架构是Recurrent这种架构?Recurrent的架构很多变型,最常见的RNN、LSTM、GRU都是这种Recurrent的架构。那时候以为Attention只是个辅助。Recurrent的架构它的坏处就是,它不容易用GPU做平行化。所以“Attention is all you need”这篇paper最大的贡献就是,把Recurrent的架构拿掉,发现没问题,然后可以做很好的平行化,训练语言模型就更方便了。这些都是历史故事。

Self-Attention的详细计算过程 (Query, Key, Value, Positional Embedding, Attention Weight)

我们直接来看Attention到底是怎么运作的。我们先来看第一层,我们看第一层Layer里面的Attention。第一层Layer里面的Attention它会直接把Token Embedding当作输入,然后它会根据输入的这些Token Embedding产生另外一排Representation。我们现在就来看看最右边对应到“果”这个Token的Representation是怎么被计算出来的。

在讲实际的过程之前,先讲一下大方向。这边有两个步骤。第一个步骤是要寻找输入中会影响“果”的意思的Token。比如在这个例子里面,输入如果是“两颗青苹果”,那如果只看“果”这个Token,你其实不知道这个“果”指的是苹果,你甚至不知道它是一个青色的苹果。它可能是任何东西,它可能是个西瓜、可能是个凤梨、可能是个莲雾,它们都算是一种水果。在这个句子中,“果”、“青”跟“蘋”会影响“果”的意思。所以Attention第一件做的事情是,找出输入的Token里面哪些Token会影响现在我们要考虑的“果”这个Token的意思。然后接下来把这些Token的资讯跟“果”这个Token的资讯合在一起,你就会得到新的意思。过了Attention之后,可能模型就知道这个“果”它不是任何的水果,它是一个苹果,而且还不是一个红色的苹果,是一个青色的苹果。

那实际上是怎么做的呢?我们来看第一步,怎么寻找输入中会影响“果”的意思的Token。这边的操作是,模型会把每一个输入的Token都拿去计算跟“果”之间的相关程度。那这个相关程度是怎么被计算出来的呢?需要有两个步骤。第一个步骤是,我们先把“果”这个Token它的Token Embedding拿出来,乘上一个叫做Wq的矩阵。把一个Embedding、一个向量乘上一个矩阵,你就得到另外一个向量。那从Wq出来的这个向量,它的名字叫做Query(查询向量: 在注意力机制中,用于查询其他Key向量以计算相关性的向量)。那因为它它是“果”的Query,所以我就用q下标“果”。“果q”听起来像是某个果冻的名字,当作这个Query的代号。

所以根据“果”,我们通过WQ得到“果q”这个向量。之后呢,我们就不把向量跟矩阵的相乘画出来了。当我画一个矩阵,然后有个向量指到它再伸出去的时候,就代表说输入的这个向量乘上WQ,得到输出的这个向量。我们要怎么考虑“果”这个Token跟其他Token之间的关联程度呢?首先,“果”这边会产生一个q(Query)向量,然后其他的Token会再乘上另外一个矩阵,这边我们称之为WK,来得到一个Key(键向量: 在注意力机制中,与Query向量进行匹配以计算相关性的向量)向量。这个Key向量,每一个Token都会得到一个。那如果是把“青”这个Token的Embedding通过WK,就会得到“青”的Key向量,我们这边就用k-青来表示这个向量。然后呢,我们把q(Query)跟k(Key)拿去计算它们的相似程度。在历史上有各式各样不同计算相似程度的方法,甚至一度曾经有人觉得,这个相似程度要用另外一个类神经网络来计算,就训练另外一个类神经网络来计算这个相似程度。后来这些方法都消失在历史的洪流中,那现在剩下主流的方法是最简单的做法,就是直接算它们的Dot Product(点积)。直接算这两个向量的相似程度。算出来Dot Product的数值越大,就代表这个Key所对应的Token,对这个Query所对应的Token有越大的影响力。

那对每一个Token都会做一模一样的计算。刚才对“青”这个Token做这个计算,我们也要对“颗”这个Token、“两”、“颗”、“青”、“蘋”、“果”这五个Token都做一样的计算。我们这边再展示一下对“蘋”这个Token做一样的计算。所以“蘋”这个Token的Embedding通过WK,会变成“蘋”的Key,也就是k-蘋。然后呢,k-蘋会跟q-果这两个向量去算Dot Product。算出来是负的。代表说,“蘋”这个Token可能对“果”这个Token它的意思没有什么影响。

讲到这边,就出现了一个盲点。这个盲点是这样:当我们在产生q-果跟k-青的向量时,我们其实只考虑了它们的Token Embedding,你其实并没有考虑它的上下文。你是拿“果”的Token Embedding得到“果”的Query,拿“青”的Token Embedding得到“青”的Key,然后就去算相似度了。这会导致什么问题?这会导致说,假设我有两个句子,一个句子叫做“两颗青苹果”,这里的“青”跟“果”是非常有关系的。另外一个句子“青山绿水红苹果”,这个“青”跟“果”是没有关系的。但是它算出来的影响力、算出来的Dot Product,会是一样的。因为这两个“青”,它通过这个矩阵得出来的Key都是一样的;这两个“果”,它Token Embedding是一样的,所以通过这个矩阵得出来的Query是一样的。你算出来的影响力,这个“青”跟“果”的影响力、和那个“青”跟“果”的影响力,也会是一样的。

所以我们这边需要把距离,两个Token间的距离,也就是Key跟Query的距离考虑进来。那要怎么考虑进来呢?其实有非常多不同的方法。这个方法多到我觉得需要开一门课,需要有另外一门课直接讲它。但我们没有时间讲这件事情。这边讲个最简单的方法,最简单的方法就是,其实在这个类神经网络里面,还有一个东西叫做Positional Embedding(位置嵌入: 表示Token在序列中位置信息的向量)。在语言模型里面,还有一个东西叫Positional Embedding。然后这个Positional Embedding里面,它记载了每一个位置的资讯。输入的这串文字的每一个位置,都会对应到一个向量。所以它也是一个Table,它也是语言模型参数的一部分。第一个位置有一个代表第一个位置的向量,第二个位置有一个代表第二个位置的向量,以此类推。

接下来,“两颗青苹果”这五个Token,分别会加上代表第一个位置的向量、代表第二个位置的向量、代表第三个位置的向量,以此类推。所以现在Token Embedding里面,不是只有Token的意思,它还包含了这个Token在第几个位置的资讯。但这个方法,你可能可以想出一大堆的可能问题。举例来说,这是否意味着,如果我们需要Positional Embedding的Table,是否意味着语言模型它能够处理的输入长度是被固定的?假设我这个Table就是固定说,有256个位置,这256个位置都有对应的Embedding,那如果现在输入的长度有257个Token,怎么办呢?没有对应的Positional Embedding了。所以这会不会是一个问题?这显然是一个问题。

所以有很多其他更好的方法,来处理Positional Embedding,来处理这个位置的问题。比如说我们常用的Llama,它处理这个位置的方法,是用一个叫做RoPE(旋转位置嵌入: 一种高效的位置编码方法,允许模型处理比训练时更长的序列)的技术。这个大家有兴趣再自己研究。RoPE这个技术就可以很神奇地做到,训练的时候,就算有一些位置我从来没有看过,训练的时候输入长度都是1到4000,但是我在用它的时候,可以用1到8000,就是这么神奇。至于怎么做到的,大家有兴趣再自己研究。总之有很多不同的方法,把位置的资讯加到Attention的计算中。我这边讲的是一个古早时代的方法,不見得是特别有效的方法,只是告诉大家,这个位置的资讯是需要被考虑的。

现在我们已经计算出每一个输入的Token对“果”的影响力了。我们把Dot Product算出来的分数,记载在这个地方。其实自己跟自己的影响力也是会被计算的。在一般的Attention里面,也会计算自己跟自己的影响力。然后这些Dot Product得出来的数字,如果很大的话,就代表这个Token,它对于我们现在要考虑的“果”这个Token,有很大的影响。那这个时候你会说,“果”attend(关注: 指注意力机制中一个Token对另一个Token的关注程度)到某一个Token。就比如说“果”跟“青”之间,它们算出来的Dot Product分数很大,那你就会说,“果”attend到“青”,或“果”attend到“蘋”。那这边如果翻成中文的话,就是“果”这个Token它专注于看“青”这个Token,它专注于看“蘋”这个Token。不过这个“专注”,也许跟人类的专注也没有什么关联性,总之就是Dot Product算出来比较大就是了。

这个Dot Product算出来分数,它就叫做Attention Weight(注意力权重: 表示一个Token对另一个Token重要性的数值)。所以有人告诉大家Attention Weight,它指的就是Dot Product算出来的分数。这些Attention Weight,如果你只算Dot Product的话,那它可能是任何数值,它可能是正的、也可能是负的。那通常这边,会把它再过一个Softmax,让它变成一个像是概率分布的东西,让所有的Attention Weight变成介于0到1之间,然后全部加起来都是1。这个操作到底是不是必要的呢?其实有人做过实验,把Softmax拿掉,换成其他的处理方式,结果也差不多。所以看起来Softmax不是一定必要的,只是它是一个非常常用的处理Attention Weight的方式而已。

计算出了Attention Weight之后,我们把Attention Weight放在每一个Token Embedding的旁边。接下来进入第二阶段。我们已经知道哪些Token会影响“果”的意思,接下来就要把这些Token它的意思跟“果”的意思,把它融合在一起。那怎么做到这件事情呢?这边的做法就是,把每一个Token算出来的Attention Weight,乘上一个叫做Value(值向量: 在注意力机制中,包含Token实际信息的向量,与Attention Weight加权求和)。这个Value的向量是怎么产生的呢?你会再有一个矩阵,叫做WV。你会把每一个Token的Embedding过这个WV以后,得到对应到那个Token的Value向量。那有了这个Value向量以后,下一步骤就是,把这些Value向量根据Attention Weight做加权,也就是Weighted Sum(加权和: 将多个数值按其权重比例相加)。所以,“两”的Value向量乘上0.03,加上“颗”乘上0.02,加上“青”乘上0.33,加上“蘋”乘上0.55,加上“果”乘上0.07,做Weighted Sum全部加起来,得到一个新的向量。因为它是很多不同的Token Embedding融合在一起的,所以这边就给它一个彩色的向量,代表它是很多不同Token Embedding加在一起的结果。

这边你可能会担心,“果”的权重有点低,这样会不会模型就根本忘了这个位置是代表“果”这个Token了呢?其实不会。因为有一个操作叫做Residual Connection(残差连接: 将输入直接加到层输出上,有助于解决深度网络中的梯度消失问题),你会直接把“果”的原来的Embedding拉过来,加上这个Weighted Sum的结果,当作最终Attention Layer的输出。所以这边这个向量,就是Attention Layer在这个位置最终输出的Representation。这个就是Attention最基本的操作。

Multi-head Attention:多维度“影响”的捕捉

但这只是基本的操作而已,这边还没有讲完。Attention真正的操作是更加复杂的。我们刚才讲说,我们的第一步是要寻找影响“果”这个意思的Token,但所谓的“影响”,这边应该加一个引号,因为影响有很多不同的面向。也许“青”这个字,决定了水果是什么颜色,但你怎么能够说“两”这个字对“果”没有影响呢?“两”这个字有另外一个面向的影响,它告诉你这个水果到底有几个。当“果”前面有“两”这个字的时候,这个“果”的Embedding的意思也不一样了。

所以其实Attention往往不止一组Attention,而是有多组的。这个多组的Attention,通常叫做Multi-head Attention(多头注意力机制: 并行运行多个注意力头,从不同角度捕捉信息)。所以你可能有一个Attention,它是负责找每一个Token的形容词的。刚才讲的WK、WQ它做的事情,可能是去找形容“果”的形容词,所以“两”对它来说,可能是没有什么影响力。但是可能有另外一个Attention,它专找数量。它专门想知道,现在我考虑的这个Token,到底根据这个上下文,它是单数还是复数?它到底有几个?所以你可能会有另外一组参数,这边写WK2跟WQ2。那你这个“两”,它会过WK2得到另外一个Key。这个“果”会过WQ2得到另外一个Query。我们会把这个Query2跟Key2,一样去算它的Dot Product。因为这一组Query跟这组Key,跟找形容词的这个Key跟Query,它们是不一样的。所以你可能会算出不一样的Dot Product的结果。那如果这个Q跟K,它们就是设计来找数量的,那可能“果”这个Token跟“两”这个Token,它们做Dot Product,它们的Query跟Key做Dot Product,用第二个Attention Head做Dot Product以后,算出来的数字就会很大。

所以这个是Multi-head Attention。所以Attention Weights不是只有一组。我们刚才看到的第一组Attention Weights,它会告诉你,“青”跟“蘋”对“果”的影响力很大。那可能会有另外一组Attention Weights告诉你,“两”跟“果”的影响力才大,但是它是另外一个不同面向的影响力。那通常一个Self-Attention里面会有好多组的Head。等一下会实际看看Llama它一个Attention里面到底有多少个Head。那每一个Head,又会有自己的Value向量。所以你会有另外一个WV,这边写作WV2。它算出第二组Value向量。第二组Value向量要去乘上第二组的Attention Weight。所以第二组的Value向量去乘上第二组的Attention Weight。第二组的Attention Weight告诉你,“两”其实对“果”这个Token也是非常重要的,只是它是另外一个面向。然后你一样做Weighted Sum,得到另外一个彩色的向量。所以每一个Attention Head都会给你一个向量。我们在这个投影片里面举的例子是有两个Attention Head,所以我们就会有两个向量。那这两个向量,你要把它再揉合起来,所以再过一个矩阵,把这两个向量乘上一个叫做WO的矩阵,再得到一个新的向量。这个新的向量,再跟“果”,用Residual Connection加起来。这个才是Self-Attention Layer在“果”这个Token对应位置,最终输出的Representation。所以这个是Self-Attention做的事情。

Causal Attention与输入长度的限制

从Self-Attention刚才的运算,你可以看出,输入越长,运算量越大。这就是为什么语言模型在处理长输入的时候,会有困难的原因之一。那要怎么解决这个问题呢?就有很多不同的方法。因为我们上课时间有限的关系,所以我把这段留在延伸阅读里面,你可以看一下。上个学期的机器学习课程,我们花了一堂课的时间,跟大家讲Transformer有哪些其他的竞争者。比如说一个你可能比较熟知的竞争者就是Mamba。它们就是为了处理Attention在输入越长的时候,运算量就越大这样的问题。

我们刚才讲了怎么算出对应到“果”这个位置的Representation。那如果是对应到“蘋”这个字的Representation呢?这边要注意的事情是,在一般的时候、在实作上,只会考虑每一个Token左边的,所谓的左边就是前面的Token。所以我们只会去考虑“两、颗、青”这三个Token对“蘋”的影响力,我们就不会再考虑右边的Token“果”对“蘋”的影响力。这种只考虑左边、只考虑前面的Attention,叫做Causal Attention(因果注意力机制: 只关注当前Token及其之前Token的注意力机制)。一般语言模型通常都会……今天一般的语言模型就是用Causal Attention。

那能不能用Non-Causal的Attention?我们能不能在考虑“蘋”的Token的时候,在做Self-Attention的时候,把右边这个Token也考虑进来呢?可以。那这样做,会不会比较好呢?有些文献指出说可能会更好一点。因为一个句子、一个词汇、一个Token不是只受左边的词汇影响,也有可能受到右边的词汇影响。所以你考虑完整的上下文,可能会更好。但是Causal Attention是为了计算方便所产生的一个方式。至于为什么这样计算比较方便,你可以想想语言模型生成句子的过程,是用Autoregressive(自回归: 模型根据已生成的序列预测下一个元素的生成方式)的。因为Autoregressive的这个设计,用Causal Attention可能会比较方便。那细节,因为我不想让这堂课太长……我觉得第一讲跟第二讲其实太长了。为了避免这个课太长,我们就留给大家自己思考。

Feed-Forward Layer的运作与“Key-Value Memories”

接下来,我们来看最后一个部分。我们来看看过了Attention之后,Feed-Forward Layer做了什么样的事情。在这个投影片上,我们的Feed-Forward Layer有两层。在实际上不一定两层,等一下看到的Llama2 4B也是两层,等一下看到的Llama 3B也是两层,Gemma 4B也是两层。但是其实不一定要是两层,你要放几层都可以,取决于你要怎么设计你的模型的架构。

我们来看看两层的Feed-Forward Layer是怎么运作的。输入是红色的这个向量,过一个Feed-Forward Layer的意思就是把红色的向量乘上一个矩阵,这个矩阵叫做Weight。我们用W来表示。那乘完之后,再加上一个向量,这个向量我们叫做Bias。把红色的向量乘上Weight这个矩阵,再加Bias这个向量,你再得到的东西还是一个向量。这个向量会通过一个函数叫做Activation(激活函数: 神经网络中引入非线性的函数)。会通过Activation Function(激活函数: 神经网络中引入非线性的函数)。Activation Function的形状是人类设计的。那过去比较常用的一种Activation Function叫做ReLU(修正线性单元: 一种常用的激活函数,输出为max(0, x)),Rectified Linear Unit。它操作非常的单纯,就是输入的向量里面,如果小于0就设成0,如果大于0就不动。一个非常简单的操作。不过近年来在大型语言模型,也不一定用ReLU,有很多更复杂的Activation Function,比如说GeLU(高斯误差线性单元: 另一种常用的激活函数,平滑且非线性)等等。这个就留给大家自己研究。总之这边有一个人为订定的Activation Function,对输入做一些改动,然后得到输出。然后这个输出的蓝色向量,它是一个中间产物,再过一个Feed-Forward Layer。这个蓝色向量再乘上另外一个矩阵W',然后再加上另外一个向量B',再通过Activation Function,最终得到输出,这里的黄色的向量。

这个向量乘矩阵再过Activation再乘另外一个矩阵最后得到黄色的向量,这一连串的Process(过程: 指一系列操作或步骤)代表什么含义呢?当然你可以很简单地理解成,反正每过一层就是过一个转换,这样也可以。这样理解其实也没什么问题。但这边我就引用另外一篇论文,这篇论文告诉你,如果你的Feed-Forward Layer有两层的话,你也可以把它看作是一个Attention的过程,就好像我们讲的Self-Attention一样,它是另外一个维度的Attention,非常神奇的一个想法。为了避免这个课程太长,我把文献留在这边给大家参考。你看它的标题:“Transformer Feed-Forward Layers Are Key-Value Memories”。它就告诉你,Transformer的Feed-Forward Layer其实是做了另外一组Attention,它有另外一组的Key跟Value,这是一个非常有趣的对于Feed-Forward Layer的看待方式。

从矩阵运算到“神经元”:类神经网络的本质

讲到目前为止,我们说这个语言模型就是Deep Learning,Deep Learning就是类神经网络,但讲到目前为止都好像没有什么跟类神经网络有关的东西。我们来告诉大家,所谓类神经网络,它怎么把现在我们讲的东西,看待成一个类神经网络。我们来看一下,在这个蓝色的向量里面,它的第一个维度是怎么被计算出来的。第一个维度的数值,我们把它命名为y1。y1是怎么被计算出来的呢?假设这个红色的向量里面有三个数值,Dimension是3,但实际上不可能是3,实际上通常是上千的。假设红色的向量就是三个Dimension,三个Dimension的数值分别叫x1、x2跟x3。然后怎么计算出这个y1呢?你就把W的第一行拿出来。这个第一行,如果你输入的这个向量是三个数值,第一行也得是三个数值,分别是w11、w12、w13。如果你学过线性代数的话,那你知道y1的算法就是x1w11 + x2w12 + x3*w13,再加上b1,就等于y1。所以y1跟x1、x2、x3的关系是X1 x W1, X2 x W2, X3 x W3 加 B1,通过一个Activation Function,它可能就是简单的ReLU,然后得到Y1。这个没有什么神奇的地方,就只是把这个矩阵运算的其中一部分拿出来给大家看而已。

那在这整个矩阵运算里面,同样的这个式子你会反复做很多次。这边有几个列,这样的式子就要做几次。但如果只说这是矩阵运算的话,听起来不夠潮。所以有人就换了一个说法,我们是从X1, X2变到Y1,然后就画个图,说把这个X1 x W1, X2 x W2, X3 x W3其实是换汤不换药的东西,再加上B1,通过Activation Function把它画一个圈圈,得到Y1。接下来给它一个名字说,这叫做一个神经元。本来只是一些简单的运算,听起来不够潮,没有办法骗骗麻瓜。然后但是你就说它是一个神经元,然后接下来就开始鬼扯说,这个就是模仿人脑的运算,所以它应该很厉害,这个就是AI的起源等等。然后麻瓜就会觉得,这个听起来非常的潮。其实就是矩阵运算而已。所以这个就是一个Neuron,然后同样这样的操作在整个语言模型里面反复地出现,所以它就是很多Neuron集合起来,就是一个神经网络。但是其实就都是矩阵的运算而已。

今天讲的有关大型语言模型内部运作机制的研究只是沧海一粟而已,还有很多可以探讨的问题。如果你有兴趣的话,可以看一下上学期机器学习的第三讲,讲了AI的脑科学。今天讲的内容有一部分,Large Lens跟PathScope,是有出现在第三讲里面的,但第三讲还有更多的内容值得你再去深挖。

实作:解剖Llama与Gemma模型

接下来我们进入实作的环节,来实际解剖一个大型语言模型,看看它里面有什么东西。这是Colab的链接。如果大家找不到Colab链接的话,反正投影片都是公开的,你可以在投影片上找到上课范例的Colab。我们现在就来跑一下上课的范例。

模型参数概览:Llama 3B与Gemma 4B

这个Colab的前面几步的操作都跟第一讲、第二讲是一模一样的。总之你要先连到Hugging Face,然后下载Hugging Face上的模型。今天我们需要下载两个模型:Llama 3B这个模型跟Gemma 4B这两个模型,因为我们想要比较这两个模型的差异。

这个Llama 3B的Tokenizer跟Llama 3B它的Model的参数就分别存在Tokenizer跟Model里面。然后这个Gemma 4B就分别存在Tokenizer 2跟Model 2里面。所以就记得说Model是Llama,Model 2是Gemma。

我先来看看Model中有什麽。首先你在Model后面加点.num_parameters(),你就可以看到这个Model里面有几个参数。所以我们来看看Llama 3B有几个参数吧。我执行model.num_parameters()以后,它给我一个巨大的数值。这个巨大的数值算出来就是32亿多。所以你就知道为什么Llama 3B叫3B,这个B是Billion的意思,因为它有超过3个Billion的参数。这边的数值就是它模型中的参数量。那Gemma呢?这个Model 2.num_parameters()会告诉我们Gemma 4B有几个参数。那算出来这边有43亿个参数。所以它就叫Gemma 4B,因为它有超过4个Billion,超过40亿个参数。

参数的结构与命名

在语言模型里面,这些参数都是以矩阵跟向量的形式被存在一起的。那矩阵跟向量其实又统称为张量(Tensor: 多维数组,是深度学习中数据表示的基本单位)。所以在模型的输出中,你常常看到Tensor这个字,指的就是矩阵或者是向量了。我们可以把一个Model中所有的参数,也就是所有的矩阵跟向量,一个一个地印出来。那每一个矩阵或向量,它会包含两个我们可以看的资讯。一个资讯叫做Name,就是它会给每一个矩阵一个名字。那你从这个名字可以猜说,这个矩阵是用在整个语言模型中的哪里。比如说假设有一个东西,它的名字叫做model.layers.0.mlp.weight,那你就知道说,这个应该是放在第0层(但第0层其实是第1层,大家应该知道Computer Science的习惯,0其实就是1),所以这是第1层。然后MLP其实就是Feed-Forward Network(刚才没有提说MLP就是Feed-Forward Network),然后它是Feed-Forward Network的weight。所以它是第1层的Feed-Forward Network的某一个weight。那Feed-Forward Network里面通常有好几层,那up_projection其实通常指的是第1层 的意思。然后会有一个shape,一个形状。它会告诉我们,我们还可以印出一个东西叫做shape。这个shape会告诉我们这个张量它长什么样子。比如说如果印出来的shape(8192, 3072),就代表说这个矩阵它的列有8192个,它的行有3072个。

我们用named_parameters()来把这些参数一个一个地召喚出来。所以当我执行这段程式码,执行这个block之后,我就可以把Llama 3B里面所有的参数,它的名字跟它的形状把它条列出来。我们来看看里面到底有什么样的参数。第一个参数叫做embed_tokens.weight。它是什么?它就是Token Embedding Table。它有多大?它的大小是128256 x 3072。非常巨大的一个矩阵。那128256是什么意思?就是Vocabulary Size,就是Token的数目。所以你知道Llama大概有12万8千多个Token。那3072就代表每一个Token,它对应的Token Embedding Dimension是多少,它对应的Token Embedding Dimension是3072。

进入第一个Layer,这边写成layer 0,它其实是第一个Layer。第一个Layer里面,我们刚才不是说算Attention,你要有一个计算Query的matrix,有一个计算Key的matrix,有一个计算Value的matrix。这边果然有一个Query的matrix,有一个Key的matrix,有一个Value的matrix。Query的matrix是3072 x 3072,Key是1024 x 3072,Value是1024 x 3072。那这边看这个shape的时候,通常右边代表的是输入,左边代表的是输出。所以每一个Token Embedding(Token Embedding是3072维),它乘上这个Query的matrix之后,会从3072维的向量变成另外一个3072维的向量。那Key呢?Key是3072维的向量变成1024维的向量。Query跟Key不是要算Dot Product吗?算Dot Product不是应该Dimension一样才有办法算Dot Product吗?一个3072维的向量要怎么跟1024维的向量算Dot Product呢?这个就是奇妙的地方。这个叫做Grouped-Query Attention(分组查询注意力: 一种优化注意力机制,通过共享Key和Value投影来减少参数量)。这个大家有兴趣再自己研究。总之这是Llama里面用的一个可以说是节省参数的设计,一个特殊的设计。总之我们这边看到Query, Key跟Value产生它们的weight。那这边你可能会问,这边只有一组Query, Key跟Value的参数,是不是只有一个Attention Head?我们刚才不是说应该要有多Attention Head吗?我告诉大家这边有多Attention Head,至于只有三个matrix到底是怎么样制造出多Attention Head的,这个也留给大家自己研究。

多Attention Head要被用一个叫做Wo的东西并在一起,这个就是这边的这个O这个东西o_proj。这边有一个怪怪的东西叫做gate_proj,这个大家有兴趣再自己研究它是什么。然后过完Attention以后,有两层的Feed-Forward。第一层Feed-Forward它会把3072维变成8192维,在下一层的Feed-Forward会把8192维变成3072维,变回3072维。总之Llama的一个Transformer的block里面就是两个Feed-Forward Network。这边还有一些神秘的东西叫做LayerNorm(层归一化: 一种归一化技术,有助于稳定神经网络训练)。LayerNorm也是有参数的。至于它是什麽,这个也再留给大家自己研究。总之我们走完了layer 0就是第一层。然后接下来第二层就是layer 1,再下一层就是layer 2,以此类推。那每一层都有一大堆的参数在里面,然后一直下去、一直下去、一直下去,有好多好多层。这个是Llama 3B,有几层呢?一直到layer 27,就是28层(因为它从0开始算的),所以就是28层。所以我们知道Llama有28层。然后最后在做Unembedding的地方是没有参数的,因为对Llama来说,Unembedding的矩阵就是input的Embedding Table,所以这边就没有Unembedding的矩阵。

然后,我们现在换成把Model 2跑这个named_parameters()把它里面的资讯印出来。我们现在把Gemma里面的资讯印出来。这个是Gemma里面的资讯。你发现Gemma跟Llama其实不太一样。Gemma它前面还有什么vision tower。Gemma是可以看图片的,所以前面这些参数,就前面有挂vision的这些参数是拿来处理图片的。我们先不看它。所以前面好多参数都是弄图片的。从这边开始,从它这边有写命名有language_model开始,就是跟处理文字有关的。所以它一开始有一个Embedding Table。这Embedding Table里面有什么呢?这Embedding Table是262144 x 2560。所以你看这个Gemma它的Token有多少个:26万个Token,比Llama还多两倍。然后它每一个Token用2560维来存它。接下来它也有Q, K, V,然后也有O。Q, K, V一样,Q跟K的那个维度一样对不起来。它一样把2560维转成2048维,跟2560维转成1024维,一样对不起来。为什么会这样?留给大家慢慢研究。然后这边就是其他都差不多。这边也有up_projection,这是第一层的Feed-Forward Network,从2560维的向量变成10240维的向量。然后第二层的Feed-Forward Network,再从10240维的向量变回2560维的向量。每一层都是做一样的事情,有很多层。这个就是Gemma。那总共有几层呢?最后算出来是33,因为从0开始起算,所以有34层。所以我们知道Gemma 4B有34层,比Llama还要多。

如果你想实际把参数的数值印出来,因为我们前面只印出了参数的名字跟它长什么样子。如果你真的要把它的数值印出来,那你要用一个叫做state_dict()的这个function。你可以把它印出来。所以我执行model.state_dict(),那它就会把所有的参数印出来,很多很多。每一个、每一组,每一个matrix里面的数值都帮你印出来。然后你也搞不清楚发生什么事。

我们可以只看某一个matrix,某一组参数。一个matrix就是一组参数。比如说假设我想看这个Model(就是Llama 3B的这个第28层,layer 27其实是第28层),第28层的up_projectionup_projection是第一个Feed-Forward Network),它里面的weight长什么样子。那我就先执行model.state_dict()把所有参数拿回来,然后这边只取某一个名字的参数,然后把它存到weight里面。那我可以把weight print出来。print出来看到什么呢?什么都没有看到,就是一个矩阵,里面有一大堆的数字。你很难想象这些数值怎么跟语言扯上关系,怎么这些很多矩阵相乘,最后就会变出了一个下一个Token的概率。这真的非常的复杂,很难凭借着人类的智慧把它想象出来。我们这边可以把这个矩阵的左上角的100x100的范围把它呈现出来,就长这样。数字有正有负,你也看不出什么东西,就只能看到人类本质上的焦虑跟寂寞而已。所以你看不出什么东西。

Token Embedding的相似度分析

通常比较能够分析的是语言模型的Embedding Table。我们现在就把Embedding Table再拿出来看看。如果中间分析某一层,你通常是看不出什么花样。我们再来看看这个Embedding Table。我们把Model它里面的model.embed_tokens.weight拿出来。这个是什么?这个是Embedding Table。我把它存进input_embedding这个变量里面。我们先把Embedding Table拿出来。那里面就会告诉我们每一个Token对应到什么样的向量。我们把这个Embedding Table印出来,印出来是128256 x 3072。就代表说有这么多Token,Token的ID可以一直从0一直排到128255。然后每一个Token对应到一个Token Embedding,它的长度是3072。

讲到这边,如果是细心的同学,你可能发现一个怪怪的问题就是,第一堂课在说Llama的Token的时候,Token不是只排到编号128000吗?这边多了256个,那是什么东西?那个是保留的Token。那是可以让你自订新的Token再插入Llama里面。不然如果它把所有Token用完,你就没有办法加新的Token。所以其实Llama的这个神经元,这个元模型是有留一些空间,让你可以自己训练模型,让它读懂一些新的Token。不过我们这边就不详细讲这个细节。

我们可以把input embedding印出来看,总之就是个巨大的矩阵,你也看不出朵花来。接下来我们可以把每一个Token对应的Token Embedding印出来。比如说我想知道Token ID编号是0的Token Embedding。我不知道你还记不记得在Llama里面编号0的天字第一号的Token是谁,其实是惊叹号。好,我们可以先……我们这边如果你想要看Token ID是0的Token,然后你可以怎么做?你就去把input_embedding这个矩阵里面对应到Token ID的那一个行(这边如果是0就是第0个行),把它取出来印出来。你就知道惊叹号对应的Token Embedding长什么样子。我来执行一下。Token 0对应到惊叹号,Token Embedding就长这样。你也看不出什么名堂来。那你试别的都是一样。Token Embedding 1是双引号,就长另外一个Embedding的样子。Token ID 2就长另外一个样子。总之每一个Token都有一个它自己对应的Token Embedding的向量。

我们怎么来分析这些向量到底包含什么意思?直接看这些Embedding,你往往看不出朵花来。比较有资讯量的是Token Embedding跟Embedding之间的相似程度。所以我们等一下做的事情就是,我们拿出ㄧ个Token,比如说“apple”,然后我们找一下“apple”的Token ID。有它的ID以后,我们就可以知道“apple”这个Token的Token Embedding长什么样子。然后接下来,我们再把“apple”的Token Embedding去跟所有其他Token的Token Embedding算相似度。然后我们就可以找出跟“apple”对于这个语言模型来说,跟“apple”这个Token意思最相近的前K名的Token。

以下这段程式码就是在做我上面描述的这五个步骤。因为时间有限的关系,我们这边都不直接解释程式码,就是你就知道这个程式码在执行什么就好了。执行这个程式码以后,我们就可以输入一个Token。比如说我输入“apple”。输入“apple”,然后就会去计算说“apple”的Token Embedding跟哪些Token的Token Embedding是最像。比如说跟“apple”Token最相近的,跟“apple”的Token Embedding最相近的那个字,其实是“apple”前面加空白。了解吧?大家知道Llama里面它的Token各式各样都有,“apple”大写、小写都算是不同的Token,前面有空白也算是不同的Token。所以跟“apple”前面没有空白意思最像的那个Token,是“apple”前面加一个空白。这有没有觉得很合理呢?然后第二像的就是“Apple”前面大写。第三像的就是全部大写“APPLE”。第四像的是“apple”前面再加一个空白,全部大写再加一个空白。不过它还有一些比较有意思的东西。比如说“apple”这个英文跟“苹果”的中文是最像。或者是“apple”居然跟“Cupertino”是最像。Cupertino是什么意思?Cupertino就是Apple总部的位置(地点)。所以对它来说,“apple”也是有那个苹果电脑的意思的。

如果你想要更看得出“Apple”跟苹果电脑的关系,你要用大写。因为其实苹果电脑的Apple通常第一个字母会大写。所以“Apple”,我们看“Apple”第一个字母大写跟哪些Token它的Embedding是最像的。当然一样,这个“Apple”这个首字母大写跟“Apple”首字母大写再加一个空白,其实意思是最像的。不过它也发现说,你看“Apple”跟“MacBook”意思是很像的。“Apple”跟“iPhone”还有“iphone”意思是很像的。所以知道对这个语言模型来说,“Apple”这个词汇真的会让它联想到“iPhone”或者是“MacBook”。

这边你要输入什么都可以。你就乱输入一些,比如输入个“李”。“李”这个Token对它来说是什么意思呢?对它来说“李”最像的当然是“李”前面加一个空白。但是它也知道“李”跟“LEE”(英文的“李”)其实意思是很像的。“李”跟“刘”意思很像,因为它们都是姓氏。所以你知道模型很厉害,它知道很多很多的事情。当然有一些莫名其妙,为什么“李”会跟什么“收入”像?我也不知道。反正它算出来就是这个样子。然后这边有一些怪怪的符号。这边有怪怪符号,我先去查了,这个怪怪符号其实是某个语言的“李”,好像是某个阿拉伯语言的“李”。所以对模型来说,不同语言同样意思的Token,它的Token Embedding其实是非常接近的。

我们再乱输入一些,输入个“王”。它除了知道说“王”呢,它这边“王”跟“王”最相近的就是“king”,所以它知道说“王”对应到英文就是“king”。它也知道“王”跟中文的“黄”意思是接近的。所以这个语言模型的Token Embedding可以告诉我们,这个语言模型来说,哪些Token是意思相近的东西。

Contextualized Embedding的语义变化

刚才呢,就是只观察了Token Embedding。我们知道这个Token Embedding接下来会被丢进Transformer,然后每一层都会吐出一个Representation。每一层都会吐出一个Representation。我们现在就来把这些Representation拿出来看看,看看我们应该看到什么东西。

我这边做的事情是这样子的:你想要得到那些Representation,你就得先编一个句子,然后丢到模型里面,然后才能得到这些Representation。我们现在输入的句子是“大家好”,把“大家好”先通过Tokenizer encode变成IDs,然后再把这些ID丢到模型里面。我们之前在第一讲的时候有做过非常类似的操作。那个时候我们从Model output拿出最后一个Token,下一个Token的这个probability distribution。在這邊我们要拿每一层的Latent Representation。如果你要拿每一层的Latent Representation,你需要在Model的输入加上output_hidden_states=True这件事情。你要加入这个flag,它才会把每一层的Hidden Representation存下来。不然你就会……不然它就没有存每一层的Hidden Representation。

现在Model吃Token ID,给它output_hidden_states=True得到output,然后你打output.hidden_states,就可以把Hidden States那些Representation、那些Hidden Latent Representation通通取出来,存在hidden_states这个变量里面。那hidden_states的第0个部分代表的就是Embedding。就是这个hidden_states里面的第0个部分,它是Embedding。然后第1个部分就是第1层、第2个部分就是第2层,以此类推。然后这边我们把这个拿到几层,包括Token Embedding也算一层,把它印出来。然后我们把每一层里面的参数的形状把它印出来。我们这边也会把Token Embedding还有第1层得到的Representation印出来看看,我们得到什么样的东西。

我们看这个输入是“大家好”,输入是“大家好”。“大家好”这个输入,“大家”其实是一个Token,“好”是一个Token。不过这个Hugging Face Transformer会比较鸡婆,会帮你前面加一个代表开始的符号。所以其实你输入的是3个Token。你输入给语言模型的是3个Token。语言模型把这3个Token读进去以后,然后我们这边拿到29层Representation。那其实是28层加上Token Embedding,所以总共算出来29层。那每一层给我们什么呢?每一层它的形状是有3个数字来代表,这边有1、3跟3072。这3个数字分别代表什么意思呢?第1个数字代表一次处理几个句子。其实Model是可以一次处理多个句子,但我们这边都是1,因为我们一次只处理一个句子,我们还没有讲到Batch的概念。第2个数字代表输入的Token Sequence有多长。输入Token Sequence有3个Token,所以这边就显示3。然后第3个数字代表说,那些Representation有多少Dimension。那Representation的Dimension都是3072。所以我们就把每一层、每一层,通通都拿出来。我们可以把它印出来啦。这个Token Embedding的输出印出来就长这样。第1层Transformer印出来就是这个样子。你也看不出什么有趣的东西来。

接下来我们想做的事情是,我们来看看如果输入3个不同的句子,在每一层中,它的每一个位置的Representation会有什么样的变化。我们执行这段程式以后,它做的事情就是,它会把“how about you?”这一个句子里面每一个Token对应的Representation把它印出来给大家看。所以丢进去“how”以后,在某一层产生的Representation长这样。这是“about”的Representation、“you”的Representation、问号的Representation。这是“how about you”。这是“how are you?”。这个是“nice to meet you”。这边如果输入0,就代表说我们抽的是第0层。第0层这边其实是Token Embedding。所以这边我们只要是同样的Token,就会有一样的Representation。我们来看看是不是一样的Token,就有一样的Token Embedding。所以如果输入是“how are you”,那在第0层也就是看Token Embedding的时候,这边“you”你看是,第一个数字是0.0060,再來是0.0238。这边“you”是0.0060、0.0238。这边“you”是0.0060、0.0238。所以Token Embedding只要是“you”都是一样的。

再来看第1层。我们来看过了第1层。过了第1层以后,我们再来比较对应到“you”这个Token的Representation。“how about you”的“you”,它的Representation是0.0397、0.0275。这边只印整个Representation的前几个数字,其实是3000多个,向量很长。这边“how are you”的“you”,是0.024跟0.095。你就发现它变了。因为为什么?因为现在“how about you”跟“how are you”的“you”,前面接的Token是不一样的。所以过了第1层以后,它的Embedding变成Contextualized Embedding,它就会产生不一样的数字,它存在向量里面的数字就不一样。但有趣的是你会发现,“how”都是一样的。这边是0.0479、-0.0892。0.0479、-0.0892。“how about you”的“how”跟“how are you”的“how”,它过了第1层以后Representation还是一模一样的。为什么?因为我们考虑上下文的时候,我只考虑每一个Token的左边,“how”的左边没有东西了。所以这两个“how”左边是完全一模一样的,所以就没有差别。你放几层都没有差别。我们拿到第10层。但每一层之间会不一样,就是第9层跟第10层的“how”会不一样,但是不同句子“how about you”的“how”跟“how are you”的“how”,它会是一模一样。它们左边都没有其他Token,它们左边是一模一样的。所以不管过几层抽出来的“how”的Representation,都是一模一样的。所以我们就对Representation做了一些观察。好,但这个观察不出朵花来。

接下来我们来看,到底模型有没有办法判断,两个句子中如果它的“apple”根据上下文意思不一样,模型有没有办法知道说,它们的Representation就应该要非常不一样。所以我这边给模型两个句子,一个叫做“I ate an apple for breakfast”。这边“apple”是一个能吃的东西,显然是代表食物的“apple”。那这边我在说,“the company”就是做iPad跟AirPod的公司,叫做“apple”。那这个“apple”虽然我没有大写,但我显然指的是苹果电脑的“apple”。现在我们把这两个句子都丢到语言模型里面,再把对应到“apple”的每一层的Representation都抽出来。所以第一个句子,我们会把对应到“apple”的Representation都抽出来。第二个句子,我们会把对应到“apple”的Representation通通都抽出来。然后我们在同一层之间,两个不同“apple”的Representation去算它的Cosine Similarity(余弦相似度: 衡量两个向量方向一致性的指标),去算它的相似度,然后印出来给大家看。

程式的细节,大家就再自己研究。总之直接画一个图出来。这个横轴代表每一层,第0层是那个Embedding,从第0层开始,一直到第28层。这个Llama有28层。那纵轴代表的就是Cosine Similarity,代表两个Embedding或两个Representation之间的相似度。第0层,第0层是Token Embedding。只要是同一个Token就是一模一样的,所以发现相似度是1。Cosine Similarity算出来就是1,最大就是1。然后发现从第1层开始,两个“apple”,因为它们意思不一样,所以它们的相似度就越来越低、越来越低,然后到第11层的时候跌到谷底。不知道为什么,后来又回弹回来。

其实在分析这种Representation的时候,你可能会觉得回弹回来不合常理,但是在做Representation的时候,其实你要想想看,每一个Layer的Representation它的分布可能是非常不一样。有没有可能比较后面的Layer,本质上那些Representation就比较接近?因为那些Representation比较接近的关系,所以这边算出来的数值,在不同Layer之间比较的时候,算出来Representation它的相似度就会比较大。所以我们应该做一个Normalization(归一化: 将数据缩放到特定范围或分布的过程),把平均的相似度把它除掉。因为有可能后面这些Layer,它们不同的Embedding之间本来就比较像,本来就会算出比较像的结果。所以我们应该要把平均的相似度除掉。所以这边我又多写了一段程式,细节大家再自己研究,总之多写了一段程式,我们去计算这两个句子中,两两的Token在每一个Layer中的相似度,然后我们会对两个“apple”之间的相似度除掉平均的相似度,做一个Normalization。做完这个Normalization以后,你看到这个结果就合理非常多。我们看最开始的时候在第0层,两个Embedding非常的像。然后从第1层开始,它就知道这两个“apple”应该是不一样的。随着层数越来越多,它可能这个句子读得越来越仔细,它就越来越清楚说,代表苹果电脑的“apple”跟能吃的“apple”,根据上下文,它们其实是不一样的东西,所以它们的Representation越来越不相近。

不过刚才我们是看了一个代表苹果的Representation跟代表苹果电脑的Representation,它们相似度的变化。但会不会两个“apple”如果都代表苹果,但上下文不一样,它们的Representation……两个“apple”都代表可以吃的苹果,但是它们上下文不一样,那模型知不知道它们的意思其实是一样的呢?我们来做一个实验。这边总共有4个句子。前2个句子它的“apple”都是可以吃的“apple”。第1个句子是“I ate an apple for breakfast”。第2个句子是“She baked an apple pie for dessert”。所以第1个句子跟第2个句子“apple”都是能吃的“apple”。第3个句子跟第4个句子的“apple”都指的是一个公司的名称,都是苹果电脑的“apple”。那接下来呢,我们总共有4个“apple”,我们两两之间拿每一层的Representation来算相似度,看看会发生什么样的事情。你就记得说第1个句子跟第2个句子“apple”是可以吃的“apple”,第3个跟第4个句子“apple”是代表公司名称的“apple”。

我们来看看。这边是4个不同的“apple”两两之间算相似度,在每一层的相似度变化。那在第0层其实都是1,因为不管是哪一个“apple”,在第0层它的相似度、Cosine Similarity都是1。那你看棕色这一条线,棕色这一条线,这两个“apple”,它们的相似度不管是哪一层,始终都非常的高。这两个“apple”是第3句跟第4句的“apple”,它们都代表苹果电脑。虽然上下文不一样,但是语言模型知道,这两个“apple”都是指公司的“apple”。虽然上下文不一样,但它们意思很近,所以它们的相似度一直维持非常的高。那这个蓝色这条线呢,蓝色这条线是第1句话跟第2句话的“apple”,它们指的也都是能吃的“apple”,意思是一样的。所以虽然上下文不一样,但相似度始终维持一定的水准。而那其他的“apple”如果两两比较你就会发现,它相似度就有非常显著的下降。随着深度越来越深,随着语言模型对于整个句子的理解、上下文看得越来越多,这些代表不同意思的“apple”,它的相似度就越来越低。语言模型真的知道哪些“apple”意思是一样的、哪些“apple”意思是不一样的。

Logit Lens:观察模型思考路径

接下来,我们來看一下Logit Lens。我们把每一层都通过LM head把它解析成一个Token。我们把每一层通过LM head,看看根据这个LM head哪一个Token是概率最高的。那这边剖析的方法是这个样子的。我们先输入一段文字,比如这边输入的是“天气”。这段文字会变成Token ID,这个Token ID会把它丢到模型里面,模型会产生输出。我们把模型输出里面的Hidden Representation把它拿出来。拿出Hidden Representation以后,接下来我们把每一层的Hidden Representation拿出来,丢给model.lm_head。这句话的意思就是把Hidden Representation丢给LM head,看它输出的Logit是什么。我们再把输出的Logit里面,分数最高的那个Token拿出来,就可以知道说模型跑过每一层的时候,它心里觉得最有可能的下一个Token分别是什么。

我们现在输入是“天气”,那我们就看“天气”后面该接什么。这个第0层是Token Embedding,所以第0层解出来还是“气”。所以本来输入是“天气”,在前面几个Layer,模型只是反复地讲最后一个Token,所以一直“气、气、气、气、气、气、气”,好像它很生气的样子。然后接下来突然就变成英文了,就突然变成“weather”。然后“weather”、“weather”、“weather”、“weather”,然后突然变成“forecast”。它突然知道下一个字应该是“预报”,“weather”后面可以接“forecast”。然后但是最后中文输出倒是中文,所以“forecast”突然在最后一层变成了“预”,然后所以“天气预报”就出现了,“天气”后面就可以接“预”。接个“预”看看接下来它会讲什么。帮它接个“预”。

那“天气预”后面,再把最后一个Token的每一个Layer拿出来。“预”,一开始都是“预、预、预、预”,然后后来把它转成英文变成“forecast”。那有一阵子又变成“prediction”。那有一阵子又变成“forecast”。然后接下来它突然觉得“forecast”后面可以接“report”,然后再把“report”转成中文,所以“天气预”后面它就知道最后可以接“报”。但你就会知道,Llama 2它内心深处还是比较喜欢用英文的。它中间的Representation变化的时候,都是英文的變化。

最后再试一个例子。比如说“今天天气真…”,“真”怎么样呢?“今天天气真”怎么样呢?一开始都是“真、真、真、真、真”,然后到某个时间点,它觉得“真”后面可以接“beautiful”。今天天气很“beautiful”。后来觉得可以接“good”,有时候可以接“bad”。然后回去“beautiful”。然后最后它觉得应该用中文,所以天气到底“真”怎么样呢?它本来有机会讲“真不怎么样”,它可能讲“真不好”,但是后来觉得还是讲“真好”好了,所以最后决定接“真好”。你就会看到这个语言模型的思路,一路怎么从输入一直变到输出。所以这个是Logit Lens。

Attention Weight的可视化分析

最后一个部分呢,是想让大家看一下,语言模型的Attention在做什麽事情。那这边怎么看它的Attention呢?一样要从Model里面,把跟Attention有关的数值,比如说Attention Weight,我们说做Attention的时候,就是会有两个Token之间的相关联的程度,那这个叫Attention Weight。我们可以把Attention Weight拿出来。

我们得到……我们给一段文字,文字转成Token ID,然后呢,我们得到一串文字,然后呢,我们把这串文字丢给encoder得到Token ID,然后丢给Model。那这边需要注意的地方是,你不只要打output_attentions=True,你还要指定它implementation的方式。那这边的implementation方式叫做eager。总之为什么需要这样指定呢?因为有一些算Attention的方式,不会把Attention Weight存下来。所以你要找一个算Attention的方式,会把Attention Weight存下来,这样我们之后才能够印出、才能够展示这个Attention Weight。

总之呢,把未完成的句子丢给Model,Model得到output。我们从output里面,把output.attentions把Attention导出来,存到attentions这个变量里面。接下来把我们存出来的东西,把它印出来。我们来看看得到什么。那因为呢,Embedding那边就没有什么Attention了,我们刚才在看那个Representation的时候,会包含Token Embedding的部分,但是因为现在看Attention的部分,就跟Token Embedding没有关系了。所以我们这边就只导出了28层的东西。那这边是从0开始算啦,那就是Llama的每一层都可以看出一个Attention。

我们从Attention Weight里面导出来的数字长什么样子呢?你会发现它有4个维度。1、24、12、12。这个1、24、12、12是什么意思呢?1指的是我们输入只有一个句子。但这个我们先不解释。24指的是有几个Attention Head,有几组Attention。所以Llama里面每一层其实是有24组Attention。虽然它前面我们只看到QKV的向量各只有一个,但怎么搞出24个Attention Weight呢?这个大家再自己研究,总之它就是搞出了24组Attention Weight。然后这每一个Attention Weight呢,都是12 x 12的矩阵。那这个12是哪来的呢?12是输入的这个句子的长度。现在输入的句子长度是“the apple is green.”“what color is the apple?”。这边总共有12个Token,所以得到的Attention就是12 x 12的矩阵。它会告诉我们两两Token之间在算Attention的时候,算出来的Attention Weight有多大。

我们就把某一个Layer里面的某一组Attention Weight印出来吧。比如说我们现在选择印第6层的第7个Attention Head。我们来印第6层的第7个Attention Head,我们看看我们得到什么。我们先把那个Attention的weight印出来,不过印出来就是一堆数字,所以你很难看到什么。不过我们把它直接画图,用图像的方式把它呈现出来。所以第6层的第7个Head到底发生了什么事呢?这个图要怎么看呢?这个图就是横轴是现在要考虑跟其他人去算Attention的Token。你会发现,几乎每一个人都是跟第1个、代表句子起始的Token有最大的Attention Weight。但是有一个例外,就是“apple”。这边有一个句点,这个句点你不要太在意,这个就是代表那个空白,句子起始Token前面的空白的意思。然后这个“apple”,这个Token在这整个句子里面,“the apple is green.”“what color is the apple?”“apple”这个Token,如果这个语言模型在考虑它的意思的时候,它觉得哪一些词汇会影响Token的意思呢?“apple”跟其他Token的Attention Weight,哪一些Token的Attention Weight最大呢?除了第1个Token之外,它跟“green”的Attention Weight是有一些的。所以代表这一组第6层的第7组Attention,它知道说如果要考虑“apple”的意思的话,要到句子前面找这个颜色,这个“green”影响了“apple”的性质,影响了“apple”的Representation接下来应该长什么样子。

你可以试试别的数字,比如说6,我们试个5,看看我们应该看到什么。你会发现右上部都是0。为什么右上部都是0呢?右上部都深色、深蓝色,就代表它是0。因为我们是Causal Attention,所以每一个Token都不会算它右边的Token的Attention,都只会算它左边的Token的Attention。所以这个矩阵,这个Attention的weight把它用一个矩阵呈现出来,右上角就都是0。然后这边看不出什么东西。试个4。再换一个Attention Weight看看。这个看起来稍微比较复杂。感觉在看到“color”的时候,模型会觉得前面的“green”会影响“color”的意思。“is”呢?“is”它会看前面的句点。“the”也会看前面的句点。问号也会看前面的句点。Attention其实通常你蛮难分析出它在干什么的,它的行为往往蛮无厘头的。我们来看这个第4个Attention,发现“color”要attend到“green”。这个“color”跟“green”是非常有关系的,所以这个是非常合理的。然后像第2个“apple”,会attend到前面那个“apple”,前面那个“apple”的意思会影响后面那个“apple”的意思,这个也是一个还可以勉强解释一下的Attention。

我们其实可以把所有的Attention Weight都印出来。我们这边就跑一个迴圈,把所有的Layer里面所有的Attention Weight一次都把它全部印出来。这个跑起来就会花一点时间,等一下你就会看到一堆小图,每一个小图都是一组Attention Weight。画完了,这个图很小啊。这个图很小啊。这个放大看一下吗?放大看一下。这边就从第1层、第2层、第3层一直排到第28层。然后每一层都有24个Attention,每一个Attention有一组Attention Weight,所以从第1组、第2组一直到第24组。你会发现每一个Attention、每一个人都做不同的事情。很多Attention都做满类似的事情,都是看前几个Token。你有很多Attention你就是搞不清楚它在做什么。每一个Attention都找一点自己的事情来做。

刚才看Llama,我们把Llama换成Gemma,其实可以的。改一行程式就好了。我们来改一下。刚才看Llama,我们来换成Gemma吧。你唯一要做的,就是把Model换成model2。把Gemma所有的Attention印出来,这花一点时间。刚才你可能发现,这个在做Attention的时候,好像模型特别喜欢attend到起始的那个符号。所以那是什么意思呢?其实你甚至可以找到一篇论文直接讨论这个问题。这个起始的符号代表“我没什么好看的”。因为你想想看,我们今天在做Attention的时候,不是会有一个Softmax吗?你等于强迫说,假设今天没有任何Token跟现在你要考虑的那个Token Attention算起来是大的,过了Softmax以后,反而每个人都算出来数值会变成蛮大的。你等于强迫说,假设今天整个句子里面根本没有相关的Token,模型还是得考虑某一些Token就是要有关系。所以起始那个符号,就变成一个default的符号。如果今天这一组Attention,假设有一组Attention就是关注现在我们考虑的这个Token有多少个……它考虑数量,整个句子里面都没有跟数量有关的东西,那怎么办?就去attend到那个起始的符号。那个起始的符号就代表“我没什么好attend的”意思。所以你会发现,今天这个是不同语言模型几乎都有这个行为。Gemma、Llama都有这个行为。你会发现,很多时候一个Attention没什么好做的,它就attend在起始的符号上。

这是Gemma的Attention。这个很多啊。有几层呢?有从第1层开始,这是每一层的Attention,每一层的Attention。它每一层就是8个Attention,它Attention比较少。它每一层是8个Attention Head。然后从第1层开始一直到……哇,这很多很多,一直到第34层。每个Attention做的事情都不太一样,每一层的Attention都做不一样的事情。

这边就是跟大家剖析了Gemma跟Llama内部的运作过程。

📌 文中提及的人物和组织

公司/组织: Hugging Face, Anthropic, Apple

产品/模型: Llama, Gemma, GPT-3.5, BERT, Claude, iPad

媒体/书籍: Attention is all you need