语言模型(马尔可夫模型,n元语法)

news2024/11/26 20:23:21

参考:8.3. 语言模型和数据集 — 动手学深度学习 2.0.0 documentation

 假设长度为T的文本序列中的词元依次为x1,x2,…,xT。 于是,xt(1≤t≤T) 可以被认为是文本序列在时间步t处的观测或标签。 在给定这样的文本序列时,语言模型(language model)的目标是估计序列的联合概率

例如,只需要一次抽取一个词元xt∼P(xt∣xt−1,…,x1), 一个理想的语言模型就能够基于模型本身生成自然文本。 与猴子使用打字机完全不同的是,从这样的模型中提取的文本 都将作为自然语言(例如,英语文本)来传递。 只需要基于前面的对话片断中的文本, 就足以生成一个有意义的对话。 显然,我们离设计出这样的系统还很遥远, 因为它需要“理解”文本,而不仅仅是生成语法合理的内容。 

尽管如此,语言模型依然是非常有用的。 例如,短语“to recognize speech”和“to wreck a nice beach”读音上听起来非常相似。 这种相似性会导致语音识别中的歧义,但是这很容易通过语言模型来解决, 因为第二句的语义很奇怪。 同样,在文档摘要生成算法中, “狗咬人”比“人咬狗”出现的频率要高得多, 或者“我想吃奶奶”是一个相当匪夷所思的语句, 而“我想吃,奶奶”则要正常得多。

目录

1.学习语言模型

2.马尔可夫模型与n元语法

3.自然语言统计

4.读取长序列数据

5.随机采样

6.顺序分区

总结


1.学习语言模型

显而易见,我们面对的问题是如何对一个文档, 甚至是一个词元序列进行建模。 假设在单词级别对文本数据进行词元化, 我们可以依靠在 8.1节中对序列模型的分析。 让我们从基本概率规则开始:

例如,包含了四个单词的一个文本序列的概率是:

 为了训练语言模型,我们需要计算单词的概率, 以及给定前面几个单词后出现某个单词的条件概率。 这些概率本质上就是语言模型的参数。

这里,我们假设训练数据集是一个大型的文本语料库。 比如,维基百科的所有条目、 古登堡计划, 或者所有发布在网络上的文本。 训练数据集中词的概率可以根据给定词的相对词频来计算。 例如,可以将估计值P^(deep) 计算为任何以单词“deep”开头的句子的概率。 一种(稍稍不太精确的)方法是统计单词“deep”在数据集中的出现次数, 然后将其除以整个语料库中的单词总数。 这种方法效果不错,特别是对于频繁出现的单词。 接下来,我们可以尝试估计

 

 其中n(x)和n(x,x′)分别是单个单词和连续单词对的出现次数。 不幸的是,由于连续单词对“deep learning”的出现频率要低得多, 所以估计这类单词正确的概率要困难得多。 特别是对于一些不常见的单词组合,要想找到足够的出现次数来获得准确的估计可能都不容易。 而对于三个或者更多的单词组合,情况会变得更糟。 许多合理的三个单词组合可能是存在的,但是在数据集中却找不到。 除非我们提供某种解决方案,来将这些单词组合指定为非零计数, 否则将无法在语言模型中使用它们。 如果数据集很小,或者单词非常罕见,那么这类单词出现一次的机会可能都找不到。

一种常见的策略是执行某种形式的拉普拉斯平滑(Laplace smoothing), 具体方法是在所有计数中添加一个小常量。 用n表示训练集中的单词总数,用m表示唯一单词的数量。 此解决方案有助于处理单元素问题,例如通过:

其中,ϵ1,ϵ2和ϵ3是超参数。 以ϵ1为例:当ϵ1=0时,不应用平滑; 当ϵ1接近正无穷大时,P^(x)接近均匀概率分布1/m。 上面的公式是 (Wood et al., 2011) 的一个相当原始的变形。

然而,这样的模型很容易变得无效,原因如下: 首先,我们需要存储所有的计数; 其次,这完全忽略了单词的意思。 例如,“猫”(cat)和“猫科动物”(feline)可能出现在相关的上下文中, 但是想根据上下文调整这类模型其实是相当困难的。 最后,长单词序列大部分是没出现过的, 因此一个模型如果只是简单地统计先前“看到”的单词序列频率, 那么模型面对这种问题肯定是表现不佳的。

2.马尔可夫模型与n元语法

如果P(xt+1∣xt,…,x1)=P(xt+1∣xt), 则序列上的分布满足一阶马尔可夫性质。 阶数越高,对应的依赖关系就越长。 这种性质推导出了许多可以应用于序列建模的近似公式:

通常,涉及一个、两个和三个变量的概率公式分别被称为 一元语法(unigram)、二元语法(bigram)和三元语法(trigram)模型。 下面,我们将学习如何去设计更好的模型。 

3.自然语言统计

我们看看在真实数据上如果进行自然语言统计。 根据 8.2节中介绍的时光机器数据集构建词表, 并打印前10个最常用的(频率最高的)单词。

import random
from mxnet import np, npx
from d2l import mxnet as d2l

npx.set_np()

tokens = d2l.tokenize(d2l.read_time_machine())
# 因为每个文本行不一定是一个句子或一个段落,因此我们把所有文本行拼接到一起
corpus = [token for line in tokens for token in line]
vocab = d2l.Vocab(corpus)
vocab.token_freqs[:10]
Downloading ../data/timemachine.txt from http://d2l-data.s3-accelerate.amazonaws.com/timemachine.txt...

[3]:

[('the', 2261),
 ('i', 1267),
 ('and', 1245),
 ('of', 1155),
 ('a', 816),
 ('to', 695),
 ('was', 552),
 ('in', 541),
 ('that', 443),
 ('my', 440)]

正如我们所看到的,最流行的词看起来很无聊, 这些词通常被称为停用词(stop words),因此可以被过滤掉。 尽管如此,它们本身仍然是有意义的,我们仍然会在模型中使用它们。 此外,还有个明显的问题是词频衰减的速度相当地快。 例如,最常用单词的词频对比,第10个还不到第1个的1/5。 为了更好地理解,我们可以画出的词频图:

freqs = [freq for token, freq in vocab.token_freqs]
d2l.plot(freqs, xlabel='token: x', ylabel='frequency: n(x)',
         xscale='log', yscale='log')

通过此图我们可以发现:词频以一种明确的方式迅速衰减。 将前几个单词作为例外消除后,剩余的所有单词大致遵循双对数坐标图上的一条直线。 这意味着单词的频率满足齐普夫定律(Zipf’s law), 即第i个最常用单词的频率ni为: 

其中α是刻画分布的指数,c是常数。 这告诉我们想要通过计数统计和平滑来建模单词是不可行的, 因为这样建模的结果会大大高估尾部单词的频率,也就是所谓的不常用单词。 那么其他的词元组合,比如二元语法、三元语法等等,又会如何呢? 我们来看看二元语法的频率是否与一元语法的频率表现出相同的行为方式。

bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])]
bigram_vocab = d2l.Vocab(bigram_tokens)
bigram_vocab.token_freqs[:10]
[(('of', 'the'), 309),
 (('in', 'the'), 169),
 (('i', 'had'), 130),
 (('i', 'was'), 112),
 (('and', 'the'), 109),
 (('the', 'time'), 102),
 (('it', 'was'), 99),
 (('to', 'the'), 85),
 (('as', 'i'), 78),
 (('of', 'a'), 73)]

 这里值得注意:在十个最频繁的词对中,有九个是由两个停用词组成的, 只有一个与“the time”有关。 我们再进一步看看三元语法的频率是否表现出相同的行为方式。

trigram_tokens = [triple for triple in zip(
    corpus[:-2], corpus[1:-1], corpus[2:])]
trigram_vocab = d2l.Vocab(trigram_tokens)
trigram_vocab.token_freqs[:10]
[(('the', 'time', 'traveller'), 59),
 (('the', 'time', 'machine'), 30),
 (('the', 'medical', 'man'), 24),
 (('it', 'seemed', 'to'), 16),
 (('it', 'was', 'a'), 15),
 (('here', 'and', 'there'), 15),
 (('seemed', 'to', 'me'), 14),
 (('i', 'did', 'not'), 14),
 (('i', 'saw', 'the'), 13),
 (('i', 'began', 'to'), 13)]

最后,我们直观地对比三种模型中的词元频率:一元语法、二元语法和三元语法。

bigram_freqs = [freq for token, freq in bigram_vocab.token_freqs]
trigram_freqs = [freq for token, freq in trigram_vocab.token_freqs]
d2l.plot([freqs, bigram_freqs, trigram_freqs], xlabel='token: x',
         ylabel='frequency: n(x)', xscale='log', yscale='log',
         legend=['unigram', 'bigram', 'trigram'])

 

这张图非常令人振奋!原因有很多:

  1. 除了一元语法词,单词序列似乎也遵循齐普夫定律, 尽管公式 中的指数α更小 (指数的大小受序列长度的影响);

  2. 词表中n元组的数量并没有那么大,这说明语言中存在相当多的结构, 这些结构给了我们应用模型的希望;

  3. 很多n元组很少出现,这使得拉普拉斯平滑非常不适合语言建模。 作为代替,我们将使用基于深度学习的模型。

4.读取长序列数据

当序列变得太长而不能被模型一次性全部处理时, 我们可能希望拆分这样的序列方便模型读取。

在介绍该模型之前,我们看一下总体策略。 假设我们将使用神经网络来训练语言模型, 模型中的网络一次处理具有预定义长度 (例如n个时间步)的一个小批量序列。 现在的问题是如何随机生成一个小批量数据的特征和标签以供读取。

首先,由于文本序列可以是任意长的, 例如整本《时光机器》(The Time Machine), 于是任意长的序列可以被我们划分为具有相同时间步数的子序列。 当训练我们的神经网络时,这样的小批量子序列将被输入到模型中。 假设网络一次只处理具有n个时间步的子序列。 图8.3.1画出了 从原始文本序列获得子序列的所有不同的方式, 其中n=5,并且每个时间步的词元对应于一个字符。 请注意,因为我们可以选择任意偏移量来指示初始位置,所以我们有相当大的自由度。

因此,我们应该从 图8.3.1中选择哪一个呢? 事实上,他们都一样的好。 然而,如果我们只选择一个偏移量, 那么用于训练网络的、所有可能的子序列的覆盖范围将是有限的。 因此,我们可以从随机偏移量开始划分序列, 以同时获得覆盖性(coverage)和随机性(randomness)。 下面,我们将描述如何实现随机采样(random sampling)和 顺序分区(sequential partitioning)策略。

5.随机采样

在随机采样中,每个样本都是在原始的长序列上任意捕获的子序列。 在迭代过程中,来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻。 对于语言建模,目标是基于到目前为止我们看到的词元来预测下一个词元, 因此标签是移位了一个词元的原始序列。

下面的代码每次可以从数据中随机生成一个小批量。 在这里,参数batch_size指定了每个小批量中子序列样本的数目, 参数num_steps是每个子序列中预定义的时间步数。

def seq_data_iter_random(corpus, batch_size, num_steps):  #@save
    """使用随机抽样生成一个小批量子序列"""
    # 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1
    corpus = corpus[random.randint(0, num_steps - 1):]
    # 减去1,是因为我们需要考虑标签
    num_subseqs = (len(corpus) - 1) // num_steps
    # 长度为num_steps的子序列的起始索引
    initial_indices = list(range(0, num_subseqs * num_steps, num_steps))
    # 在随机抽样的迭代过程中,
    # 来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻
    random.shuffle(initial_indices)

    def data(pos):
        # 返回从pos位置开始的长度为num_steps的序列
        return corpus[pos: pos + num_steps]

    num_batches = num_subseqs // batch_size
    for i in range(0, batch_size * num_batches, batch_size):
        # 在这里,initial_indices包含子序列的随机起始索引
        initial_indices_per_batch = initial_indices[i: i + batch_size]
        X = [data(j) for j in initial_indices_per_batch]
        Y = [data(j + 1) for j in initial_indices_per_batch]
        yield np.array(X), np.array(Y)

下面我们生成一个从0到34的序列。 假设批量大小为2,时间步数为5,这意味着可以生成 ⌊(35−1)/5⌋=6个“特征-标签”子序列对。 如果设置小批量大小为2,我们只能得到3个小批量。

my_seq = list(range(35))
for X, Y in seq_data_iter_random(my_seq, batch_size=2, num_steps=5):
    print('X: ', X, '\nY:', Y)
X:  [[ 2.  3.  4.  5.  6.]
 [17. 18. 19. 20. 21.]] 
Y: [[ 3.  4.  5.  6.  7.]
 [18. 19. 20. 21. 22.]]
X:  [[22. 23. 24. 25. 26.]
 [12. 13. 14. 15. 16.]] 
Y: [[23. 24. 25. 26. 27.]
 [13. 14. 15. 16. 17.]]
X:  [[27. 28. 29. 30. 31.]
 [ 7.  8.  9. 10. 11.]] 
Y: [[28. 29. 30. 31. 32.]
 [ 8.  9. 10. 11. 12.]]

6.顺序分区

在迭代过程中,除了对原始序列可以随机抽样外, 我们还可以保证两个相邻的小批量中的子序列在原始序列上也是相邻的。 这种策略在基于小批量的迭代过程中保留了拆分的子序列的顺序,因此称为顺序分区。

def seq_data_iter_sequential(corpus, batch_size, num_steps):  #@save
    """使用顺序分区生成一个小批量子序列"""
    # 从随机偏移量开始划分序列
    offset = random.randint(0, num_steps)
    num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size
    Xs = np.array(corpus[offset: offset + num_tokens])
    Ys = np.array(corpus[offset + 1: offset + 1 + num_tokens])
    Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)
    num_batches = Xs.shape[1] // num_steps
    for i in range(0, num_steps * num_batches, num_steps):
        X = Xs[:, i: i + num_steps]
        Y = Ys[:, i: i + num_steps]
        yield X, Y

基于相同的设置,通过顺序分区读取每个小批量的子序列的特征X和标签Y。 通过将它们打印出来可以发现: 迭代期间来自两个相邻的小批量中的子序列在原始序列中确实是相邻的。

for X, Y in seq_data_iter_sequential(my_seq, batch_size=2, num_steps=5):
    print('X: ', X, '\nY:', Y)
X:  [[ 2.  3.  4.  5.  6.]
 [18. 19. 20. 21. 22.]] 
Y: [[ 3.  4.  5.  6.  7.]
 [19. 20. 21. 22. 23.]]
X:  [[ 7.  8.  9. 10. 11.]
 [23. 24. 25. 26. 27.]] 
Y: [[ 8.  9. 10. 11. 12.]
 [24. 25. 26. 27. 28.]]
X:  [[12. 13. 14. 15. 16.]
 [28. 29. 30. 31. 32.]] 
Y: [[13. 14. 15. 16. 17.]
 [29. 30. 31. 32. 33.]]

现在,我们将上面的两个采样函数包装到一个类中, 以便稍后可以将其用作数据迭代器

class SeqDataLoader:  #@save
    """加载序列数据的迭代器"""
    def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
        if use_random_iter:
            self.data_iter_fn = d2l.seq_data_iter_random
        else:
            self.data_iter_fn = d2l.seq_data_iter_sequential
        self.corpus, self.vocab = d2l.load_corpus_time_machine(max_tokens)
        self.batch_size, self.num_steps = batch_size, num_steps

    def __iter__(self):
        return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)

最后,我们定义了一个函数load_data_time_machine, 它同时返回数据迭代器和词表, 因此可以与其他带有load_data前缀的函数类似地使用。

def load_data_time_machine(batch_size, num_steps,  #@save
                           use_random_iter=False, max_tokens=10000):
    """返回时光机器数据集的迭代器和词表"""
    data_iter = SeqDataLoader(
        batch_size, num_steps, use_random_iter, max_tokens)
    return data_iter, data_iter.vocab

总结

  • 语言模型是自然语言处理的关键。

  • n元语法通过截断相关性,为处理长序列提供了一种实用的模型。

  • 长序列存在一个问题:它们很少出现或者从不出现。

  • 齐普夫定律支配着单词的分布,这个分布不仅适用于一元语法,还适用于其他n元语法。

  • 通过拉普拉斯平滑法可以有效地处理结构丰富而频率不足的低频词词组。

  • 读取长序列的主要方式是随机采样和顺序分区。在迭代过程中,后者可以保证来自两个相邻的小批量中的子序列在原始序列上也是相邻的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/88799.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ubuntu中使用gcc/g++编译C/C++

对于习惯了使用windows进行开发的朋友们,如果想要编译C语言,只要要在windows中安装一个可以编译的IDE即可。或者直接自己加载编译的工具链,使用Cmd命令行也可以完成C语言的编译。 但是如果想要在linux环境中编译C语言,安装编译工具…

【华为上机真题 2022】消消乐游戏

🎈 作者:Linux猿 🎈 简介:CSDN博客专家🏆,华为云享专家🏆,Linux、C/C、云计算、物联网、面试、刷题、算法尽管咨询我,关注我,有问题私聊! &…

MapReduce案例-TopN(倒序排序)

文章目录MapReduce案例-TopN(倒序排序)一、案例分析1、TopN分析法介绍2、案例需求及分析二、MapReduce 倒序排序代码实现1、准备数据文件(1) 在虚拟机上创建文本文件(2) 上传文件到HDFS指定路径2、map阶段实现(1) 创建前N成绩映射器类3、Reduce阶段实现4、Driver程序主类实现5、…

深入理解React中的虚拟DOM(源码+分析)

文章目录引文一、前端技术的发展历史1.1 前后端不分离1.1.2 静态网站1.1.3 动态网站1.2 前后端分离1.2.1 jQuery时代1.2.2 angularjs时代1.2.3 react与vue1.3 总结二、什么是虚拟dom2.1 概念2.2 react中的虚拟dom三、react虚拟DOM的优势是什么3.1 局部更新DOM的机制3.2 浏览器兼…

shell编程基础

重定向输入和输出 输出重定向 bash shell用大于号(>)来完成这项功能:command > outputfile 如果不想覆盖文件原有内容,而是想要将命令的输出追加到已有文件中,比如你正 在创建一个记录系统上某个操作的日志文件。在这种情况下&#xf…

postgresql备份恢复

一、逻辑备份与恢复 pg_dump -d test >/tmp/test.sql -p 5433 --备份test这个数据库 psql -p 5433 </tmp/test.sql --恢复 pg_dump > /tmp/full.sql -p 5433 --备份整库 psql </tmp/full.sql -p 5433 --恢复 二、物理备份与恢复 1.开启归档 &#xf…

新人小白想做跨境电商,怎么上手比较稳妥一点呢?

近年来&#xff0c;随着互联网的发展&#xff0c;国内外商业贸易越来越顺畅&#xff0c;直播电商的普及也带动了大量相关产业链的发展&#xff0c;其中跨境电商是尤为突出的一个。虽然国内做跨境电商的企业很多&#xff0c;但还是有很多新人争相进入市场。所以今天米贸搜就和大…

Flume

目录1、Flume简要描述2、Flume网址3、Flume的组成架构3.1、Agent3.2、Source3.3、Channel3.4、Sink3.5、Event4、拓扑结构5、运行原理6、简要安装7、开发案例7.1、监控端口数据官方案例1&#xff09;安装telnet工具2&#xff09;判断44444端口是否被占用3&#xff09;创建Flume…

线索二叉树构建和遍历

线索二叉树 文章目录线索二叉树引出线索解决方案(以中序线索树为例)引论:策略&#xff1a;问题1&#xff1a;♥ 对策线索二叉树的节点类型定义线索化二叉树♥ 线索化算法♥ 建立中序线索二叉树的算法♥ 算法实现遍历线索化二叉树引言:♥ 遍历思路(中序遍历举例)&#xff1a;♥ …

产品设计:Material Design 学习笔记一

​自从Material Design发布以来&#xff0c;我就在一直收集相关素材与资源&#xff0c;研究别人的作品。这套设计风格非常鲜明&#xff0c;带有浓郁的Google式严谨和理性哲学&#xff0c;深得我心。实际上&#xff0c;光是研究素材和别人作品&#xff0c;就能发现一些明显的规律…

【Docker】虚悬镜像(Dangling Image)介绍和处理方法

本期目录1. 虚悬镜像介绍2. 查看本地所有虚悬镜像3. 删除全部虚悬镜像4. 人为构建虚悬镜像专栏精选文章1. 虚悬镜像介绍 虚悬镜像 (Dangling Image) 指的是仓库名 (镜像名) 和标签 TAG 都是 <none> 的镜像。如下图所示。 在实际开发中&#xff0c;在构建或者删除镜像时出…

【案例实践】气象数据相关分析及使用系列:如何使用格点数据分析中国积温变化技术应用

【视频教程】气象数据相关分析及使用系列课程&#xff1a;如何使用格点数据分析中国积温变化技术应用https://mp.weixin.qq.com/s?__bizMzAxNzcxMzc5MQ&mid2247519871&idx8&sn5ccca4436825438ce253ab7455437259&chksm9be3916fac94187970353477a4c191cdb6d499d…

[LINUX]基本权限

&#x1f941;作者&#xff1a; 华丞臧. &#x1f4d5;​​​​专栏&#xff1a;【LINUX】 各位读者老爷如果觉得博主写的不错&#xff0c;请诸位多多支持(点赞收藏关注)。如果有错误的地方&#xff0c;欢迎在评论区指出。 推荐一款刷题网站 &#x1f449; LeetCode刷题网站 文…

web前端网页设计与制作——华夏第一县HTML+CSS+JavaScript

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…

【C++】C++入门

屏幕前的你&#xff0c;一起加油啊&#xff01;&#xff01;&#xff01; 文章目录一、命名空间&#xff08;namespace&#xff09;1.命名空间的定义&#xff08;::域作用限定符&#xff09;2.命名空间的使用&#xff08;三种使用方式&#xff09;二、C输入&输出&#xff…

Maven pom.xm javafx-maven-plugin打包javafx 应用及调试

1、添加 javafx-maven-plugin到 plugin内 <plugin><groupId>com.zenjava</groupId><artifactId>javafx-maven-plugin</artifactId><version>8.8.3</version><configuration><!-- 启动类 --><mainClass>com.test.d…

【春招必备】Java面试题,面试加分项,从jvm层面了解线程的启动和停止

前言 Spring 作为一个轻量级的 Java 开发框架&#xff0c;将面向接口的编程思想贯穿整个 Java 系统应用&#xff0c;因此在 Java 面试中常被提。本次介绍的主要是解析面试过程中如果从源码角度分析常见的问题&#xff0c;为了方便大家阅读&#xff0c;小编这里还整理了一份微服…

零拷贝技术面试题

文章目录1 零拷贝的介绍2 传统的IO的执行流程3 零拷贝涉及的技术点3.1 内核空间和用户空间3.2 用户态和内核态3.3 DMA技术4 零拷贝实现的几种方式4.1 mmapwrite4.2 sendfile4.3 sendfileDMA scatter/gather实现的零拷贝5 java提供的零拷贝方式5.1 Java NIO对mmap的支持5.2 Java…

抖音快手如何轻松接入虚拟人直播

在上一篇文章零基础开启元宇宙——创建虚拟形象中&#xff0c;我们实现了创建虚拟形象&#xff0c;接下来我们可以利用虚拟形象“为所欲为”。今天我们利用虚拟形象在短视频平台如快手、抖音中直播&#xff0c;对于不希望露脸的主播们这是可是一大利器呀&#xff01;话不多说&a…

【Unity项目实战】手把手教学:飞翔的小鸟(5)背景滚动

承接上一篇&#xff1a;【Unity项目实战】手把手教学&#xff1a;飞翔的小鸟&#xff08;4&#xff09;文本添加&#xff0c;我们已经使得主角小鸟接触到地面后跳转到Game Over状态&#xff0c;接下来我们将继续往下&#xff0c;讲解得分机制。 一、重新进入游戏 根据上篇最后…