为了账号安全,请及时绑定邮箱和手机立即绑定

用Scratch编写语言模型:爪爪(Clawed)式简易教程

这篇博客文章最初是作为一个笑话开始的。Claude这个名字是来自Anthropic(一个专注于人工智能的公司)的大型语言模型的名字。这个名字听起来就像“clawed”,听起来像是用Scratch编程语言编写的语言模型的完美名字。

Scratch是一种可视化的编程语言。用Scratch编写深度神经网络(虽然并非不可能)会显得很荒诞。而编写大型语言模型更是难以想象,但小型语言模型又如何呢?

术语‘大型语言模型’用于指那些实现为深度神经网络模型且拥有超过十亿参数的模型。如果你想了解更多关于深度神经网络和大型语言模型的信息,可以参考这篇《非常温和的大型语言模型介绍,去除了炒作》。

但是,让我们稍微后退一步。一个“语言模型”是一个非常简单的概念:它是在给定一个或多个前文单词的情况下,某个单词出现的可能性很大。考虑以下单词序列:“航行在深蓝色的海洋”。下一个单词会是什么?比如“深蓝色的海洋”在英语写作中出现得很多,“sea”(海洋)这样的单词出现的概率就非常高。而像“牛油果”这样的单词出现的概率就低得多,我们几乎看不到“航行在深蓝的牛油果中”这样的表达。

从数学的角度来看,一个语言模型是:

可以读作“给定前n-1个词,第n个词出现的概率。”

有许多方法来学习这种概率的方法。一种方法是开始计算词共现次数。另一种方法是开发一个高级的神经网络来近似该概率函数。无论哪种方式,你最终都会得到一个语言模型,

如果一个语言模型只是给定其他词语时对词语的概率分布,那么我们为什么不能在Scratch中统计一些词语并学习一个语言模型呢?没有什么是阻止不了的。

但首先,根据你希望跟踪的单词数量,语言模型有不同的类型。一个非常非常简单的模型叫做一元模型(unigram模型),其中“一元”意味着“一个词的”。一元模型是在不考虑任何先前单词的情况下,计算一个词的概率,它只是数据集中每个词的出现频率。一元模型能做的事情很少——它太简单了。

稍微复杂一点的语言模型是二元语法模型,即给定前一个词时,某个词出现的概率。二元意味着两个词:你要猜测的词及其前一个词。在二元语法模型中,你有一个词作为线索来预测下一个词。这就是早期自动补全功能的工作原理。

我会展示如何在Scratch中实现一元文法模型和二元文法模型。

训练数据

首先,我们需要一些训练数据。训练数据其实就是些文字。它可以是一份文档,多个文档连接在一起,或者多个独立的文档。为了简单点,我们将使用刘易斯·卡罗的《贾伯沃克》的第一段,并将其进一步简化为全小写,并去掉所有标点。

那是个brillig的时候,滑稽的托维们在草地上旋转打洞,博罗古夫们显得忧郁又拘束,而那些疯狂的莫莫拉思们正大声吼叫着

让我们创建一个变量,并给它赋值为上面的字符串。

训练数据是一个字符串,取自路易斯·卡罗尔的《Jabberwocky》的第一段。

这是一个有用的实例,因为它包含了许多独特的词汇,但也包含一些常见的词汇,例如'和' 和 '的'。这将使概率计算变得足够简单,可以进行视觉检查和调试(如有需要)。

数据结构

我们需要一些数据结构。Scratch 并没有提供很多选择的数据结构。除了变量外,Scratch 只提供了列表。我们将花费大量时间来应对这一限制。现阶段,我们会先创建五个列表:

  • tokens: 这个列表将包含训练文档中的每个单词,按顺序排列。我们将称每个单词为一个 token。
  • 词汇表: 这个列表将包含文档中的每个唯一单词(文档中有些单词会重复)。虽然单词的顺序不重要,但每个单词的索引编号很重要,因为这使我们可以在单词字符串和数字之间来回转换。例如如果 “twas” 位于位置 1,那么数字 1 和字符串 “twas” 是等价的。
  • 单词计数: 训练数据中每个单词出现的次数。如果 “and” 出现了三次,将 “and” 放在词汇表的第 5 个位置,在单词计数的第 5 个位置放一个 “3” 就表示 “and” 出现了三次。
  • 一元模型: 一元模型是一组每个单词出现在训练数据中的概率。
  • 二元模型: 二元模型是一组每个可能的单词对(称为 bigram)出现在训练数据中的概率。例如,“and the” 出现了两次,“twas brillig” 出现了一次,而 “brillig twas” 没有出现过。

在完成这些列表的制作后,我们应该确保在程序刚开始时,这些列表都为空。

我们的五个列表都空了。

解析数据(数据解析过程)

我们的训练数据是一串字符,但我们需要一个单词列表(词元)。我们需要做一些工作来将这串字符拆分成词元。我们将采用一个简单的假设,即单词之间通过空格来分隔。

我们可以通过一个辅助函数 找到一个单词 来受益。当我们找到一个单词时,需要将其添加到词列表中。如果我们之前从未见过这个单词,就需要把它加到词汇表里。如果我们之前见过这个单词,就需要增加它的词频。

我们找到一个词儿,记得留心它。

现在我们需要找一些单词。让我们创建一个名为 word 的新变量。当我们逐个处理文档中的字母时,我们将逐个字母地添加到这个单词中,直到遇到空格。当我们遇到空格时,我们将使用上面的辅助函数,清空单词,然后重新开始。我们会这样一直做,直到处理完训练文档中的所有字母为止。

将训练数据拆分成词(“token”)。
(注:保持“token”为斜体或加引号,以符合技术术语的一致性)

但是会有一个多出来的词,因为最后一个词后面没加空格。

最后,我们将在词汇表中添加两个特殊标记。

  • 即“SOS”代表“开始序列”;“EOS”代表“结束序列”。我们还会在列尾添加“EOS”。

这些特征在一元模型中不会特别有用,但在生成二元模型时会很有用。

记录序列的起始和结束。

训练,运行单一模型

我们最后一步是训练一元模型,然后从该模型中采样生成文本。为了完成这些任务,我们将创建一些独立的区块。采样过程会生成一个新的变量,名为generated

训练单词模型

为了训练这个一元模型,我们将简单计算每个词在训练数据中出现的概率。一个词在训练数据中出现的概率是它的出现次数除以总的词数。我们已经收集了每个词的出现次数,所以我们只需要计算总的词数出现次数,然后用每个词的出现次数除以总的词数。

正在训练一元模型。

现在我们知道了每个词的概率,大致如下:

单词4出现的频率为20%。查阅我们的词汇表后,单词4就是英文中的“the”。

使用一元模型生成

从一元语言模型生成时,我们只需要根据单词的概率来选择单词。我们会生成一个0到1之间的随机数。这就是我们要找的随机数。然后,我们将遍历我们的一元语言模型,累积概率“总量”,直到达到我们的目标。由于高概率的词在这个0到1的范围内占据更多的概率空间,因此我们更可能选中这些高概率的词。

根据一元模型生成的

我们将把所有的词收集起来并放入名为generated的变量里,该变量即为返回值。

运行这个模型并不会带来特别满意的结果。

单字模型缺乏连贯性。

训练并运行我们的二元模型

我们应该能够用一个二元模型做得更好。二元模型(即基于前一个单词预测下一个单词的模型)使前一个单词能够提示接下来可能出现的单词。同样,从语言模型中抽样将要填充一个新的变量,generated

训练二元gram模型的过程

在看到前一个词后,一个词出现的概率称为二元模型。例如,“the”很可能跟着“and”。 “gimble”也有可能跟着“and”。但是根据我们的数据,“brillig”不可能跟着“and”出现。

bigram 模型之所以复杂是因为需要两个词的概率,而 Scratch 中列表无法像其他编程语言那样嵌套来创建矩阵。

双词模型将包含N乘N个元素,其中N是单词的数量。每个索引代表两个连续单词同时出现的概率。比如,索引1可以是“twas twas”,索引2可以是“twas brillig”,以此类推。

让我们在二元模型列表中加入N乘N零来设置它。

开始初始化二元模型。

这将是一件麻烦事,在一维数组中保持清晰,而它实际上是一个扁平化的二维数组。我们可以做几个辅助函数,帮助我们在单词对和索引之间转换。

这个块取两个字并找出哪个索引代表它们。双字组索引是返回值。

这个块会根据二元词索引找出对应的两个词,Word1word2 就是返回的值。

计算二元模型 块中的下一步是计算二元词对频数。这表示每一对可能的词语在训练数据中出现的次数。我们需要一次跟踪两个词,一个是 前一词,另一个是 当前词。我们将从前一词设为“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个词来预测下一个词的概率,那么理论上大型语言模型应该能够再现其训练数据的大量部分。实际上,我们确实可以看到,最大的语言模型能够在一定程度上输出训练数据中的整段内容。

另一种理解大规模语言模型规模的方式是,我们可能提出的所有问题很可能以某种形式出现在训练数据中,大规模语言模型可以给出记忆中的答案。因为我们通常会用几种不同的方式来提问同一个问题,所以存在一些变化,类似于我们在《胡言乱语》这个例子中,从一个简单的双词模式到假设的三词模式的过渡。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消