这篇博客文章最初是作为一个笑话开始的。Claude这个名字是来自Anthropic(一个专注于人工智能的公司)的大型语言模型的名字。这个名字听起来就像“clawed”,听起来像是用Scratch编程语言编写的语言模型的完美名字。
Scratch是一种可视化的编程语言。用Scratch编写深度神经网络(虽然并非不可能)会显得很荒诞。而编写大型语言模型更是难以想象,但小型语言模型又如何呢?
术语‘大型语言模型’用于指那些实现为深度神经网络模型且拥有超过十亿参数的模型。如果你想了解更多关于深度神经网络和大型语言模型的信息,可以参考这篇《非常温和的大型语言模型介绍,去除了炒作》。
但是,让我们稍微后退一步。一个“语言模型”是一个非常简单的概念:它是在给定一个或多个前文单词的情况下,某个单词出现的可能性很大。考虑以下单词序列:“航行在深蓝色的海洋”。下一个单词会是什么?比如“深蓝色的海洋”在英语写作中出现得很多,“sea”(海洋)这样的单词出现的概率就非常高。而像“牛油果”这样的单词出现的概率就低得多,我们几乎看不到“航行在深蓝的牛油果中”这样的表达。
从数学的角度来看,一个语言模型是:
可以读作“给定前n-1个词,第n个词出现的概率。”
有许多方法来学习这种概率的方法。一种方法是开始计算词共现次数。另一种方法是开发一个高级的神经网络来近似该概率函数。无论哪种方式,你最终都会得到一个语言模型,
如果一个语言模型只是给定其他词语时对词语的概率分布,那么我们为什么不能在Scratch中统计一些词语并学习一个语言模型呢?没有什么是阻止不了的。
但首先,根据你希望跟踪的单词数量,语言模型有不同的类型。一个非常非常简单的模型叫做一元模型(unigram模型),其中“一元”意味着“一个词的”。一元模型是在不考虑任何先前单词的情况下,计算一个词的概率,它只是数据集中每个词的出现频率。一元模型能做的事情很少——它太简单了。
稍微复杂一点的语言模型是二元语法模型,即给定前一个词时,某个词出现的概率。二元意味着两个词:你要猜测的词及其前一个词。在二元语法模型中,你有一个词作为线索来预测下一个词。这就是早期自动补全功能的工作原理。
我会展示如何在Scratch中实现一元文法模型和二元文法模型。
首先,我们需要一些训练数据。训练数据其实就是些文字。它可以是一份文档,多个文档连接在一起,或者多个独立的文档。为了简单点,我们将使用刘易斯·卡罗的《贾伯沃克》的第一段,并将其进一步简化为全小写,并去掉所有标点。
那是个brillig的时候,滑稽的托维们在草地上旋转打洞,博罗古夫们显得忧郁又拘束,而那些疯狂的莫莫拉思们正大声吼叫着
让我们创建一个变量,并给它赋值为上面的字符串。
训练数据是一个字符串,取自路易斯·卡罗尔的《Jabberwocky》的第一段。
这是一个有用的实例,因为它包含了许多独特的词汇,但也包含一些常见的词汇,例如'和' 和 '的'。这将使概率计算变得足够简单,可以进行视觉检查和调试(如有需要)。
我们需要一些数据结构。Scratch 并没有提供很多选择的数据结构。除了变量外,Scratch 只提供了列表。我们将花费大量时间来应对这一限制。现阶段,我们会先创建五个列表:
在完成这些列表的制作后,我们应该确保在程序刚开始时,这些列表都为空。
我们的五个列表都空了。
解析数据(数据解析过程)
我们的训练数据是一串字符,但我们需要一个单词列表(词元)。我们需要做一些工作来将这串字符拆分成词元。我们将采用一个简单的假设,即单词之间通过空格来分隔。
我们可以通过一个辅助函数 找到一个单词 来受益。当我们找到一个单词时,需要将其添加到词列表中。如果我们之前从未见过这个单词,就需要把它加到词汇表里。如果我们之前见过这个单词,就需要增加它的词频。
我们找到一个词儿,记得留心它。
现在我们需要找一些单词。让我们创建一个名为 word 的新变量。当我们逐个处理文档中的字母时,我们将逐个字母地添加到这个单词中,直到遇到空格。当我们遇到空格时,我们将使用上面的辅助函数,清空单词,然后重新开始。我们会这样一直做,直到处理完训练文档中的所有字母为止。
将训练数据拆分成词(“token”)。
(注:保持“token”为斜体或加引号,以符合技术术语的一致性)
但是会有一个多出来的词,因为最后一个词后面没加空格。
最后,我们将在词汇表中添加两个特殊标记。
这些特征在一元模型中不会特别有用,但在生成二元模型时会很有用。
记录序列的起始和结束。
我们最后一步是训练一元模型,然后从该模型中采样生成文本。为了完成这些任务,我们将创建一些独立的区块。采样过程会生成一个新的变量,名为generated。
为了训练这个一元模型,我们将简单计算每个词在训练数据中出现的概率。一个词在训练数据中出现的概率是它的出现次数除以总的词数。我们已经收集了每个词的出现次数,所以我们只需要计算总的词数出现次数,然后用每个词的出现次数除以总的词数。
正在训练一元模型。
现在我们知道了每个词的概率,大致如下:
单词4出现的频率为20%。查阅我们的词汇表后,单词4就是英文中的“the”。
从一元语言模型生成时,我们只需要根据单词的概率来选择单词。我们会生成一个0到1之间的随机数。这就是我们要找的随机数。然后,我们将遍历我们的一元语言模型,累积概率“总量”,直到达到我们的目标。由于高概率的词在这个0到1的范围内占据更多的概率空间,因此我们更可能选中这些高概率的词。
根据一元模型生成的
我们将把所有的词收集起来并放入名为generated的变量里,该变量即为返回值。
运行这个模型并不会带来特别满意的结果。
单字模型缺乏连贯性。
我们应该能够用一个二元模型做得更好。二元模型(即基于前一个单词预测下一个单词的模型)使前一个单词能够提示接下来可能出现的单词。同样,从语言模型中抽样将要填充一个新的变量,generated。
在看到前一个词后,一个词出现的概率称为二元模型。例如,“the”很可能跟着“and”。 “gimble”也有可能跟着“and”。但是根据我们的数据,“brillig”不可能跟着“and”出现。
bigram 模型之所以复杂是因为需要两个词的概率,而 Scratch 中列表无法像其他编程语言那样嵌套来创建矩阵。
双词模型将包含N乘N个元素,其中N是单词的数量。每个索引代表两个连续单词同时出现的概率。比如,索引1可以是“twas twas”,索引2可以是“twas brillig”,以此类推。
让我们在二元模型列表中加入N乘N零来设置它。
开始初始化二元模型。
这将是一件麻烦事,在一维数组中保持清晰,而它实际上是一个扁平化的二维数组。我们可以做几个辅助函数,帮助我们在单词对和索引之间转换。
这个块取两个字并找出哪个索引代表它们。双字组索引是返回值。
这个块会根据二元词索引找出对应的两个词,Word1 和 word2 就是返回的值。
在 计算二元模型 块中的下一步是计算二元词对频数。这表示每一对可能的词语在训练数据中出现的次数。我们需要一次跟踪两个词,一个是 前一词,另一个是 当前词。我们将从前一词设为“SOS”开始,因为我们需要一个起始标志。然后我们遍历标记列表,对于每一对连续的词,找到相应的索引并增加其计数。
这将非常有帮助,如果我们能看到情况怎么样。让我们将每个索引处的双词对以一个特别的列表形式展示出来,称为 调试用双词对。这虽然不会起到实际作用,但可以帮助我们更直观地理解双词模型。
到目前为止我们有如下内容:
我们可以看出,“and the”出现了两次,而“and gimble”出现了一次。像“and slithy”这样的词组则根本没有出现。
不幸的是,其实我们不想要二元组的计数,我们想要bigram的概率。就像之前那样,我们可以把所有的二元组加起来,然后用总的频数除以总和。
现在我们有一个合适的二元模型,其中包含了每个二元组的概率。
双字符 ‘and the’ 的概率是 66.6%,双字符 ‘and gimble’ 的概率是 33.3%。
当我们从二元模型生成时,我们需要从所有可能跟在另一个词后面的词中进行采样。换句话说,我们需要从某个词开始。那么,我们从哪个词开始呢?我们使用“SOS”作为开始标记——所有的序列都应该从这个标记开始。然后我们将前一个词设为“SOS”。现在我们就可以找出最有可能跟在“SOS”后面的词。找到这个词后,我们再以此为基准,继续猜下个词。我们就这样继续,直到达到目标词数,或者直到生成“EOS”,这就表示序列完整了。
虽然我们没有生成“EOS”,但我们将会像在单字符情况中那样进行采样。我们将使用我们的辅助词来双字符索引,跳到 bigram 列表的正确部分,并遍历所有词语直到遇到我们的随机目标词。和之前一样,某些 bigram 在0到1之间的数轴上占用更多空间,因此我们更可能在高概率的 bigram 上停止。我们将遇到的词添加到生成的文本中,并将其设为前词。
现在如果我们现在运行二元语法模型,我们会看到更合理的结果。
我们可能得到一个长或短的生成序列。从“SOS”开始,我们唯一见过的后续词是“twas”,接着是“brillig”,再接着是“and”。数据表明该序列的概率为100%。一旦生成“and”,我们有66.6%的几率继续生成“the”,33.3%的几率生成“gimble”。如果接着生成“the”,那么分别生成“slithy”、“borogoves”、“mome”和“wabe”的概率各为25%。一旦生成“mome”,接下来必定生成“raths”、“outgrabe”,最后以“EOS”结束,生成过程至此完成。而其他词仍有可能生成“and”,这可能导致我们在生成过程中重复几次。
关于三元模型呢?三元模型是基于前两个词来计算一个词的概率。我还没实现三元模型。处理这样的三维概率矩阵并将其展平为一维列表会麻烦得多。但是它看起来会与二元文法模型非常相似,除了三元模型需要 N x N x N 个条目,其中 N 是不同单词的数量。随着我们增加 gram 的长度,我们需要存储的数据量也会增加。
我们可以思考一下三元模型如何处理《雅伯沃奇》的第一节。每个三词组合在数据中只出现一次。因此,从“SOS”开始,我们能够精确地复现《雅伯沃奇》,因为我们的数据非常简单。
像克劳德或ChatGPT这样的大型语言模型可以处理成千上万个n元组。让我们思考一下《Jabberwocky》中的三元组所传达的教训。如果一个语言模型可以根据前1000个词来预测下一个词的概率,那么理论上大型语言模型应该能够再现其训练数据的大量部分。实际上,我们确实可以看到,最大的语言模型能够在一定程度上输出训练数据中的整段内容。
另一种理解大规模语言模型规模的方式是,我们可能提出的所有问题很可能以某种形式出现在训练数据中,大规模语言模型可以给出记忆中的答案。因为我们通常会用几种不同的方式来提问同一个问题,所以存在一些变化,类似于我们在《胡言乱语》这个例子中,从一个简单的双词模式到假设的三词模式的过渡。