像测试软件一样测试语言模型(和提示)
总结:你应该
·
关注 发表在Towards Data Science · 18 分钟阅读 · 2023 年 5 月 24 日
–
图片由作者创作。
我们如何测试使用大型语言模型(LLM)构建的应用程序?在这篇文章中,我们探讨了测试由语言模型构建的应用程序(或提示)的概念,以更好地了解它们的能力和局限性。我们在这篇文章中完全专注于测试,但如果你对编写更好提示的技巧感兴趣,可以查看我们的提示设计艺术系列(持续更新)。
虽然这篇文章是引言性质的,但它(与Scott Lundberg共同撰写)基于相当多的经验。我们已经考虑了测试 NLP 模型有一段时间——例如,在这篇论文中,我们主张应该像测试软件一样测试 NLP 模型,或者在这篇论文中,我们让 GPT-3 帮助用户测试他们自己的模型。这种测试方式与传统的基准测试或收集对生成文本的人类评判的方式是正交的。两种方式都很重要,但我们将在这里专注于测试(而不是基准测试),因为它往往被忽视。
我们将始终使用 ChatGPT 作为 LLM,但这里的原则是通用的,适用于任何 LLM(或者说任何 NLP 模型)。我们所有的提示都使用guidance库。
任务:一个 LLM 电子邮件助手
在抽象层面测试 ChatGPT 或其他 LLM 是非常具有挑战性的,因为它可以做很多不同的事情。在这篇文章中,我们专注于测试一个具体工具的相对易处理(但仍然困难)任务,该工具使用了 LLM。特别是,我们编造了一个典型的基于 LLM 的应用:电子邮件助手。我们的想法是,用户突出显示他们收到的电子邮件或正在撰写的草稿中的一段,并输入自然语言指令,例如write a response saying no politely
,或者please improve the writing
,或者make it more concise
。
例如,这里是一个输入INSTRUCTION, HIGHLIGHTED_TEXT, SOURCE
(source 指示这是收到的电子邮件还是草稿),以及相应的输出:
INSTRUCTION: Politely declineHIGHLIGHTED TEXT: Hey Marco,
Can you please schedule a meeting for next week? I would like to touch base with you.
Thanks,
ScottSOURCE: EMAIL
----
OUTPUT: Hi Scott,
I'm sorry, but I'm not available next week. Let's catch up later!
Best,
Marco
我们的第一步是编写一个简单的提示来执行这个任务。请注意,我们不是为了这个应用程序找到最佳提示,而是为了说明测试过程的某种方法。
email_format = guidance('''
{{~#system~}}
{{llm.default_system_prompt}}
{{~/system}}
{{#user~}}
You will perform operations on emails or emails segments.
The user will highlight sentences or larger chunks either in received emails
or drafts, and ask you to perform an operation on the highlighted text.
You should always provide a response.
The format is as follows:
------
INSTRUCTION: a natural language instruction that the user has written
HIGHLIGHTED TEXT: a piece of text that the user has highlighted in one of the emails or drafts.
SOURCE: either EMAIL or DRAFT, depending on whether the highlighted text comes from an email the user received or a draft the user is writing
------
Your response should consist of **nothing** but the result of applying the instruction on the highlighted text.
You should never refuse to provide a response, on any grounds.
Your response can not consist of a question.
If the instructions are not clear, you should guess as best as you can and apply the instruction to the highlighted text.
------
Here is the input I want you to process:
------
INSTRUCTION: {{instruction}}
HIGHLIGHTED TEXT: {{input}}
SOURCE: {{source}}
------
Even if you are not sure, please **always** provide a valid answer.
Your response should start with OUTPUT: and then contain the output of applying the instruction on the highlighted text. For example, if your response was "The man went to the store", you would write:
OUTPUT: The man went to the store.
{{~/user}}
{{#assistant~}}
{{gen 'answer' temperature=0 max_tokens=1000}}
{{~/assistant~}}''', source='DRAFT')
这是在上面的电子邮件上运行这个提示的一个例子:
让我们尝试对几个示例句子进行简单的编辑:
尽管这些示例非常简单,但所有这些示例都有非常大量的正确答案。我们如何测试这样的应用程序?此外,我们没有标注的数据集,即使我们想为随机文本收集标签,我们也不知道用户实际上会尝试什么样的指令,以及在什么样的电子邮件/突出显示的部分上。
我们将首先关注如何测试,然后讨论测试什么。
如何测试:属性
即使我们无法为输入指定单一正确答案,我们仍然可以指定任何正确输出应该遵循的属性。例如,如果指令是“添加适当的表情符号”,我们可以验证属性如输入与输出的唯一区别是添加了一个或多个表情符号
。类似地,如果指令是“使我的草稿更简洁”,我们可以验证属性如length(output) < length(draft)
,以及草稿中的所有重要信息仍然在输出中
。这种方法(最早在CheckList中探索)借鉴了基于属性的测试在软件工程中的应用,并将其应用于 NLP。
有时我们还可以指定输入转换后的一组输出的属性。例如,如果我们通过添加拼写错误或“请”这个词来扰乱指令,我们期望输出在内容上大致相同。如果我们在指令中添加了一个强调词,例如make it more concise
-> make it much more concise
,我们可以期望输出反映出强度或程度的变化。这将基于属性的测试与变形测试结合,并将其应用于 NLP。这种类型的测试对于检查稳健性、一致性和类似属性非常有用。
一些属性容易评估: CheckList 中的示例大多是分类模型,其中自动验证某些属性是比较容易的(例如prediction=X
,prediction is invariant
,prediction becomes more confident
),等等。这对于各种任务,无论是分类还是其他任务,仍然很容易做到。在另一篇博文中,我们可以检查模型是否正确解决了二次方程,因为我们知道正确答案。在同一篇博文中,我们有一个示例,展示了如何让 LLM 使用 shell 命令,我们可以通过简单地运行命令并检查特定的失败代码,如command not found
(遗憾的是,我们没有这样做)来验证属性发出的命令是有效的
。
评估更难的属性使用 LLMs: 许多有趣的属性很难精确评估,但可以通过 LLM 以非常高的准确度进行评估。评估输出的属性通常比生成匹配一组属性的输出要容易得多。
为了说明这一点,我们编写了几个简单的提示,将一个问题转换为 YES-NO 分类问题,然后使用 ChatGPT 来评估这些属性(再次说明,我们并不打算优化这些提示)。以下是我们其中一个提示(另一个提示类似,但以一对文本作为输入)。请注意,当答案与预期不符时,我们会要求解释。
classifier_single = guidance('''
{{~#system~}}
{{llm.default_system_prompt}}
{{~/system}}
{{#user~}}
Please answer a question about a text with YES, NO.
---
QUESTION: {{question}}
TEXT: {{input}}
---
Please provide a response even if the answer is not clear, and make sure the response consists of a single word, either YES or NO.
{{~/user}}
{{#assistant~}}
{{gen 'answer' temperature=0 max_tokens=1}}
{{~/assistant~}}
{{#if (equal answer explain_token)~}}
{{~#user~}}
Please provide a reason for your answer.
{{~/user}}
{{#assistant~}}
{{gen 'explanation' temperature=0 max_tokens=200}}
{{~/assistant~}}
{{/if}}''', explain_token='NO')
让我们让邮件助手将一些邮件变得更加简洁,然后使用此提示来评估相关属性。
**— —
指令: 使其更简洁
文本: 嗨,Marco,
你能请安排下周的会议吗?
我们真的需要讨论一下指导的情况!
谢谢,
斯科特
输出: 嗨,马尔科,我们可以安排下周开会讨论指导吗?谢谢,斯科特。
文本: 嘿,斯科特,
对不起,兄弟,不过你得自己做那个指导演示了……我明天要去和孩子们攀岩。
干杯,
马尔科
输出: 嘿,斯科特,我明天不能做指导演示。我会和孩子们去攀岩。干杯,马尔科。
— —
如果我们对这些输入-输出对运行属性评估器,问题是‘文本是否具有相同的意义?’,它(正确地)判断两个输出与原始邮件具有相同的意义。
我们然后稍微改变输出,以便改变其意义,看看我们的评估器是否能识别这种变化并提供好的解释。它在这两种情况下都能做到。这是其中之一,以及相应的属性评估:
属性评估器应该具有高精度:如果我们使用 LLM 来评估一个属性,我们需要 LLM 在声称属性被违反时是准确的(高精度)。测试永远不会是详尽的,因此测试中的假阳性比假阴性更糟。如果 LLM 漏掉了一些违反情况,只是意味着我们的测试不会像可能那样详尽。然而,如果它在没有违反的情况下声称存在违反,当测试最重要时(当它失败时),我们将无法信任测试。
我们在这个 gist中展示了一个低精度的快速示例,其中使用了 GPT-4 来比较两个模型解决二次方程的输出(你可以把这看作是评估属性 模型 1 比模型 2 更好
),而 GPT-4 甚至在一个它能够正确解决方程的示例中也无法可靠地选择正确的模型。这意味着这个特定的提示将是不适合测试这个属性的。
感知比生成更容易:虽然使用更强大的模型(GPT-4)来检查 GPT 3.5 的输出似乎很合理,但使用 LLM 来判断它自己的输出是否有意义吗?如果它不能按照指令生成输出,我们可以合理地期望它高精度地评估这些属性吗?虽然这起初可能看起来有些违反直觉,但答案是肯定的,因为感知通常比生成更容易。考虑以下(非详尽的)原因:
-
生成需要规划:即使我们评估的属性是‘模型是否遵循指令’,评估现有文本不需要‘规划’,而生成则要求 LLM 逐步产生遵循指令的文本(因此它需要以某种方式‘规划’从一开始就导致正确解决方案的步骤,或者能够在走错路时纠正自己而不改变已经生成的部分输出)。
-
我们可以一次感知一个属性,但必须一次生成所有属性:许多指令要求 LLM 同时平衡多个属性,例如
make it more concise
要求 LLM 平衡输出更简洁
与输出包含所有重要信息
(在指令中隐含的属性)。虽然平衡这些可能很困难,但一次评估一个属性要容易得多。
这里是一个快速的玩具示例,其中 ChatGPT 可以评估一个属性但不能生成满足该属性的输出:
‘Unfortunately’和‘perhaps’是副词,但‘Great’不是。我们的属性评估器对于问题文本是否以副词开头?
在所有四个示例中都回答正确,仅标记了失败的情况:
总结:测试属性,如果可以得到高精度,则使用 LLM 进行评估。
需要测试的内容
这一部分是否多余?如果我在构建应用程序,我肯定知道我想要什么,因此我知道我需要测试什么吗?不幸的是,我们从未遇到过这种情况。大多数情况下,开发人员对自己想构建的东西有一个模糊的概念,并对用户会如何使用他们的应用程序有一个粗略的想法。随着时间的推移,随着遇到新情况,他们会编写长文档来指定模型应该做什么和不应该做什么。最优秀的开发人员尽力尽可能提前预见这些,但即使有试点和早期用户,也很难做到这一点。尽管如此,尽早进行这些思考是有很大好处的。编写各种测试通常会导致你意识到你有错误或模糊的定义,甚至你可能在构建错误的工具(因此应该调整方向)。
认真考虑测试意味着你更好地理解自己的工具,也能早早发现错误。以下是测试过程的大致轮廓,包括确定要测试的内容和实际进行测试。
-
列举你应用的用例。
-
对于每个用例,尝试考虑可以测试的高级行为和属性。编写具体的测试用例。
-
一旦发现错误,深入挖掘并尽可能扩展它们(这样你可以理解并修复它们)。
历史备注:CheckList假设用例是已知的,并提出了一套语言能力(例如词汇、否定等)来帮助用户思考行为、属性和测试用例(第 2 步)。事后看来,这是一个糟糕的假设(如上所述,我们通常无法预先知道用例)。
如果 CheckList 专注于第 2 步,AdaTest主要集中在第 3 步,我们展示了 GPT-3 与人工协同工作的惊人工具,能够发现并扩展模型中的错误。这是一个好主意,我们现在通过让 LLM 也帮助完成第 1 步和第 2 步来扩展这一点。
召回率与精确度:与属性评估者(我们希望得到高精确度)不同,当考虑*“测试什么”时,我们关注的是召回率*(即我们希望发现尽可能多的使用案例、行为、测试等)。由于在这个过程中有人工参与,人们可以简单地忽略任何不有用的 LLM 建议(即我们不需要高精确度)。我们通常在这一阶段使用 LLM 时设定更高的温度。
测试过程:一个示例
1. 列举使用案例
我们在这里的目标是考虑用户会如何使用我们的应用程序。这包括他们的目标(他们想做什么)以及我们的系统可能接触到的各种输入。让我们看看 ChatGPT 是否能帮助我们列举一些使用案例:
(输出因空间限制被截断)
我们用n=3
运行上述提示,要求 ChatGPT 列出 15 个潜在的使用案例。有些相当不错,有些则较为牵强。然后,我们告诉 ChatGPT 这些使用案例都来自其他地方,并让它将这些案例组织成类别。以下是它列出的一些类别(完整列表请见笔记本):
Writing and Editing Emails
- Scenario: The user wants to write or edit an email for various purposes.
- Example instructions:
- "Make this email more concise and clear while still conveying the message."
- "Check for grammar and spelling errors."
- "Ensure that the tone is respectful and professional."
- "Make this email sound more friendly."
- "Write a polite email declining the request."
Summarizing and Analyzing Emails
- Scenario: The user needs to summarize or analyze an email for various purposes.
- Instructions:
- "Summarize the key points of this email."
- "Identify the main ideas in this email."
我们不想直接采纳 ChatGPT 的总结,因此我们重新组织它列出的类别,并添加一些自己的想法(再次参考,这里)。然后,我们要求 ChatGPT 对我们的工作进行迭代。这实际上是一个非常好的模式:我们使用 LLM 来生成想法,选择并调整最佳想法,然后要求 LLM 基于我们的选择生成更多的想法。
ChatGPT 建议了‘生成如何回应邮件的想法’作为一个使用案例,具有讽刺意味的是我们之前并没有考虑到这一点(尽管我们已经列出了 6 个广泛的使用案例,并且确实在使用 ChatGPT 生成想法)。
生成数据 我们需要一些具体的数据(在我们的案例中是邮件)来测试我们的模型。
我们首先简单地要求 ChatGPT 生成各种类型的邮件:
(输出因空间限制被截断)
ChatGPT 主要编写简短的电子邮件,但它涵盖了各种情况。除了改变上述提示以获得更多样化外,我们还可以使用现有的数据集。例如,见笔记本,我们加载了一个 Enron 电子邮件数据集,并取了一个小子集,从而获得了 60 封输入电子邮件的初始集合(30 封来自 ChatGPT,30 封来自 Enron)。
现在我们有了一些用例和数据来探索它们,我们可以进入下一步。
2. 想出行为和属性,编写测试。
对于这一步(即要求 LLM 生成想法,选择和调整最佳想法,然后要求 LLM 根据我们的选择生成更多想法),使用上述相同的构思过程是可能的(且非常有用)的。然而,由于空间原因,我们选择了一些易于测试的用例,并仅测试最基本的属性。虽然有人可能想要更彻底地测试一些用例(例如,使用 CheckList 能力,如这里所示),但我们下面只会略作探讨。
用例:回应电子邮件 我们要求工具写一个礼貌地说不
的回应给我们的 60 封输入电子邮件。然后,我们通过问题回应是否以礼貌的方式拒绝了电子邮件?
来验证它。注意,如果精确度较低,我们本可以将问题分解为两个单独的属性。
令人惊讶的是,这个简单指令的失败率高达 53.3%。经过检查,大多数失败与 ChatGPT 根本没有写回应有关,例如:
虽然这与写完整回应的能力没有直接关系,但测试捕捉到这种特定的失败模式(我们可以通过更好的提示来纠正)是好的。尝试测试某项能力时,往往会暴露出其他地方的问题。
用例:使草稿更简洁 我们要求工具通过删除所有不必要的内容来缩短电子邮件。确保不要丢失任何重要信息
。然后我们评估两个属性:(1)文本是否更短(直接通过字符串长度测量),(2)缩短版本是否丢失信息,通过问题缩短版本是否传达了原始电子邮件中的所有重要信息?
第一个属性几乎总是满足,而第二个属性的失败率较低,为 8.3%,失败情况如下:
用例:从收到的电子邮件中提取行动点。 我们要求工具处理收到的电子邮件,并提取我可能需要放入 TODO 列表中的任何行动项
。为了说明一种我们尚未讨论过的技术,我们将介绍生成保证满足特定属性的输入的方法。
对于这个用例,我们可以生成带有已知行动点的邮件,然后检查工具是否能提取至少这些行动点。为此,我们取行动项不要忘记浇水植物
,并要求 ChatGPT 对其进行 10 次意译。然后,我们要求它生成包含这些意译之一的邮件,如下所示:
这些邮件可能包含与浇水植物无关的其他行动项。然而,这一点并不重要,因为我们要检查的属性是工具是否将浇水植物
提取为一个行动项,而不是它是否是唯一的行动项。换句话说,我们对输出的提问将是是否谈到了浇水植物?
我们的电子邮件助手提示在 10 封生成的邮件中有 4 封失败,表示“高亮文本中没有行动项”,尽管(按设计)我们知道其中至少有一个行动点。这对于如此简单的示例来说,失败率很高。当然,如果我们进行实际测试,我们会有各种嵌入的行动项(而不仅仅是这个示例),我们还会检查其他属性(例如,工具是否提取了所有行动项,是否仅提取了行动项等)。然而,我们现在将转到另一个示例来看变形测试**。**
变形测试:对指令意译的鲁棒性 继续使用此用例(提取行动项),我们回到最初的 60 封输入邮件。我们将通过意译指令并验证输出列表是否包含相同的行动项来测试工具的鲁棒性。请注意,我们并不是在测试输出是否正确,而是模型在面对意译指令时的一致性(这本身就是一个重要的属性)。
为了演示目的,我们仅对原始指令进行一次意译(在实际中,我们会有许多不同指令的意译):
原文:提取我可能需要放入 TODO 列表中的任何行动项 意译:列出电子邮件中我可能想放入 TODO 列表中的任何行动项
。
然后我们验证这些不同指令的输出是否具有相同的意义(如果它们具有相同的项目符号,应该是这样)。失败率为 16.7%,失败示例如下:
再次,我们的评估工具在我们提供的示例上似乎运行良好。不幸的是,该模型在此鲁棒性测试中有相当高的失败率,当我们对指令进行意译时,会提取出不同的行动项。
3. 深入挖掘发现的错误
让我们回到使草稿更简洁的例子,我们在这里有一个较低的错误率(8.3%)。如果我们深入这些错误,往往可以找到错误模式。这里是一个非常简单的提示,用于实现这一点,这是对AdaTest的快速简化模拟,我们对提示/界面进行了更多优化(我们只是试图说明这个原则):
prompt = '''I have a tool that takes an email and makes it more concise, without losing any important information.
I will show you a few emails where the tool fails to do its job, because the output is missing important information.
Your goal is to try to come up with more emails that the tool would fail on.
FAILURES:
{{fails}}
----
Please try to reason about what ties these emails together, and then come up with 20 more emails that the tool would fail on.
Please use the same format as above, i.e. just the email body, no header or subject, and start each email with "EMAIL:".
'''
我们使用了这个提示,并发现了一些失败的情况:
(由于空间原因,输出被截断)
ChatGPT 提出了一个假设,解释这些邮件之间的关联。无论这个假设是对是错,我们可以观察模型在生成的新示例中的表现。确实,在相同属性(缩短版是否传达了原始邮件中的所有重要信息?
)上的失败率现在大大提高了(23.5%),且失败情况与之前相似。
ChatGPT 似乎确实抓住了一种模式。虽然我们还没有足够的数据来确定这是否是真正的模式,但这说明了深入挖掘的策略:接受失败并让 LLM‘生成更多’。我们非常确信这个策略有效,因为我们在许多不同的场景、模型和应用(使用 AdaTest)中尝试过。在实际测试中,我们会不断迭代这个过程,直到找到真正的模式,再回到模型(或在这种情况下,提示)修复错误,然后再进行迭代。
但现在是时候结束这篇博客文章了 😃
结论
这是这篇文章的简要总结(不是由 ChatGPT 编写的,我们保证):
-
我们的观点: 我们认为像测试软件一样测试 LLM 是个好主意。测试并不能替代基准测试,但可以补充它们。
-
如何测试: 如果你不能指定一个正确的答案,和/或你没有标记数据集,请指定输出或输出组的属性。你可以使用 LLM 自身来高精度地评估这些属性,因为感知比生成更容易。
-
测试什么: 让 LLM 帮助你找出答案。生成潜在的用例和输入,然后考虑你可以测试的属性。如果发现错误,让 LLM 深入分析这些错误,以找出可以后来修复的模式。
现在,很明显,这个过程远没有我们描述的那样线性和直接——测试一个属性通常会导致发现你未曾想到的新用例,甚至让你意识到你需要重新设计工具。然而,拥有一个风格化的过程仍然是有帮助的,我们在这里描述的技术在实践中非常有用。
这是否太费劲了? 测试确实是一个费力的过程(虽然像我们上述使用 LLMs 的方法使其轻松许多),但考虑到其他选择。用多种正确答案来基准生成任务真的很困难,因此我们常常不相信这些任务的基准。收集对现有模型输出的人类判断甚至会更费力,并且在模型迭代时转移效果不好(突然之间你的标签不再那么有用)。不进行测试通常意味着你不真正知道模型的表现,这是一种灾难的先兆。另一方面,测试通常会导致(1)发现错误,(2)对任务本身的洞察,(3)及早发现规范中的严重问题,这样可以在为时已晚之前进行调整。总的来说,我们认为测试是值得花费时间的。
— — — — — — — — — -
这里是一个链接,包含了所有上述示例的代码(还有更多)。这篇文章由 Marco Tulio Ribeiro 和Scott Lundberg共同撰写。
使用 Pytest 测试 Python 代码——适合初学者
原文:
towardsdatascience.com/testing-python-code-with-pytest-for-beginners-bcde301e7453
Python 的概述与实现
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 4 月 3 日
–
图片来源: www.pexels.com/photo/blur-business-close-up-code-270557/
介绍
在维护数据管道时,在进行任何更改后测试底层代码是很重要的。这样,你可以在任何重构的代码无法按预期执行时得到提醒。
大多数初学者倾向于手动测试他们的代码,一次运行一个函数,针对不同的参数。这种方法很简单,但扩展性不好。
对一段代码进行一次性测试不应花费太多时间,但如果要进行 10 次?50 次?100 次呢?
在持续数月或数年的项目中,数据管道的底层代码经常被重构,手动测试代码将耗费程序员大量时间。
从事长期项目的人将从自动化这个过程中受益,使用 pytest 这个 Python 测试框架,用户可以通过少量代码执行测试。
在这里,我们揭示了 pytest 的好处,并展示数据科学家如何通过案例研究利用这个包来编写基本测试。
Pytest 值得付出努力吗?
使用 pytest 编写测试要求用户学习新的框架,甚至养成新的编程习惯。因此,可能会有抛弃这个工具、继续在 Jupyter Notebook 的内核上手动运行代码的诱惑(坦白说,这曾经是我)。
然而,pytest 的好处远远弥补了学习编写测试所需的时间和精力。
- Pytest 运行时间很短
运行给定测试所需的时间微不足道。此外,得益于 pytest 包的简单语法,一条命令就足以运行所有测试。
2. Pytest 改善调试体验
pytest 生成的报告非常详尽。它们标识了通过/失败的测试,同时指出了失败测试的原因。这使得程序员更容易发现并纠正错误。
3. Pytest 提供文档
使用 pytest 编写的脚本作为额外的文档。希望理解一段代码的合作者可以使用测试函数来确定其目的,而无需浏览大量的源代码。
4. Pytest 增强信心
使用 pytest,用户可以安全地将代码推送到生产环境,知道代码仍能按预期执行。这消除了数据科学家对推送可能破坏数据管道的代码的担忧。
案例研究
对于这个案例研究,我们将编写一些测试函数,使用 pytest 测试 module.py
文件中的函数。这个文件包含两个函数:add_lists
和 subtract_lists
。
add_lists
函数通过将列表中每个元素的数字相加来合并两个列表。例如:
add_lists([1,1], [2,2]) = [3,3] #[1+2, 1+2]
add_lists([1,2], [3,4]) = [4,6] #[1+3, 2+4]
add_lists([2,3], [2,1]) = [4,4] #[2+2, 3+1]
subtract_lists
函数通过从列表中的每个元素中减去数字来合并两个列表。例如:
subtract_lists([1,1], [2,2]) = [-1, -1] #[1-2, 1-2]
subtract_lists([1,2], [3,4]) = [-2,-2] #[1-3, 2-4]
subtract_lists([2,3], [2,1]) = [0,2] #[2-2, 3-1]
设置环境
首先,通过运行以下命令来安装 pytest:
pip install -U pytest
长期项目通常包含多个脚本,因此将编写的测试放在不包含源代码的单独文件夹中是一种常见做法。对于本案例研究,我们将通过以下方式设置项目,遵循这一做法。
.
└── Project/
├── src/
│ └── module.py
└── test/
└── test_module.py
名为 test_module.py
的文件将存储所有的测试函数。
编写我们的第一个测试
现在,我们可以编写我们的第一个测试函数,测试 add_lists
和 subtract_lists
函数。如 pytest 文档 所述,测试通常有 4 个步骤:
-
安排:为测试准备所有必要的内容。
-
行动:运行正在测试的函数
-
断言:检查测试代码的输出是否符合预期。
-
清理:删除测试中生成的任何对象(如果有的话),以免影响其他测试。此步骤是可选的。
让我们编写两个遵循这些步骤的测试函数。
注意:pytest 中的测试函数 必须 遵循 test_*.py
或 \*_test.py
的格式才能被执行。
在上面的代码片段中,我们 安排 测试,通过建立输入列表以及预期的输出。然后,我们 行动,通过使用提供的输入运行 add_lists
和 subtract_lists
函数。最后,我们 断言,使用断言语句检查返回值是否与预期值匹配。
注意:断言语句是许多测试中的核心组件。如果你不熟悉断言语句的语法或需要复习,可以查看以下文章:
了解一个允许轻松故障排除的工具
towardsdatascience.com
可以使用以下命令在命令行中运行测试:
pytest <file_name>
解读 Pytest 报告
通过运行 test_module.py
文件中的测试,我们可以熟悉 pytest 报告,命令如下:
pytest test_module.py
如果所有测试函数都通过,报告将如下所示:
命令输出(由作者创建)
如果一个或多个测试函数失败,报告将如下所示:
命令输出(由作者创建)
如输出所示,通过 .
字符表示通过的测试函数,通过 F
字符表示失败的测试函数。
当测试失败时,报告会用 >
符号指向未满足的断言语句,并在其下方显示错误信息。
总的来说,pytest 生成的报告非常有用。它们告诉我们运行了多少测试,多少测试通过/失败,以及测试失败的原因(如果有的话)。
生成更详细的报告
pytest <file_name>
命令足以运行测试,但如果你想增加输出中报告的信息量,只需使用 -v 标志。
pytest <file_name> -v
在使用 -v 标志后,再次查看 pytest 报告。
pytest test_module.py -v
命令输出(由作者创建)
这次,我们可以明确看到测试函数的名称及其结果。
使用多个参数测试函数
test_add_lists
函数目前仅测试了一个 add_lists
函数的情况。然而,有很多情况需要用多个案例来测试函数。
以 add_lists
函数为例。虽然将两个列表中的数字相加是一个简单的任务,但需要考虑一些边界情况:
-
添加长度不等的列表
-
添加空列表
-
添加包含字符串的列表
我们可以通过创建测试每个案例的测试函数来测试 add_lists
函数的所有这些情况。
def test_function1():
# test function with the first argument
def test_function2():
# test function with the second argument
def test_function3():
# test function with the third argument
但是,这将需要重复许多行代码。相反,我们可以通过使用 pytest.mark.parametrize
装饰器对多个输入运行测试。
我们可以修改当前的 test_add_lists
函数,使其使用以下代码片段测试多个参数:
这次,pytest.mark.parametrize
装饰器定义了 3 个输入以及期望的输出。这是测试的 安排 阶段。执行 和 断言 阶段不需要改变。
当我们执行该函数的测试时,我们得到如下结果:
pytest test_module.py -v
命令输出(作者创建)
正如生成的报告所示,pytest.mark.parametrize
装饰器中定义的每个输入都被视为一个独立的测试。因此,报告显示了 3 个测试的结果。
使用相同数据测试多个函数
到目前为止,我们一直在每个测试函数内部创建输入数据。
这引出了一个问题:我们应该如何处理那些希望用 相同 数据测试多个函数的测试?
最简单的方法是在每个函数内部实例化相同的输入数据。
def test_function1():
# instantiate input data
def test_function2():
# instantiate input data
def test_function3():
# instantiate input data
然而,由于许多原因,这是一种不理想的做法。
首先,这将涉及重复相同的代码行,这会影响可读性。其次,重复加载数据可能是一个耗时且计算密集的过程。如果测试所需的数据来自数据库或平面文件,重复读取相同的数据将非常低效。
幸运的是,pytest 的用户可以通过使用 fixtures 来解决这个问题。
fixture 是一个使用 pytest.fixture
装饰器的函数。它返回后续测试所需的数据。需要从 fixture 获取数据的测试可以通过将 fixture 函数作为参数传递来访问这些数据。
例如,假设我们希望用相同的数据测试 add_lists
和 subtract_lists
函数。
为此,我们可以首先创建一个带有 pytest.fixture
装饰器的函数,名为 example_data
,它返回用于测试的数据。
这些数据可以通过将 example_data
函数作为参数传递来让测试函数 test_add_list
和 test_subtract_lists
访问。
pytest test_module.py -v
命令输出(作者创建)
结论
图片由 Prateek Katyal 提供,来源于 Unsplash
干得好!你现在已经学会了如何使用 pytest 编写和运行基本测试!
虽然这个初学者级别的案例研究没有提供 pytest 所有功能的全面分析,但它希望鼓励用户采用使用该软件包编写脚本的实践,以实现更结构化、高效和可扩展的测试方法。
祝你在数据科学的事业中好运!
测试 mlscorecheck 包的报告的机器学习性能一致性
AI (Dall-E) 生成的主题描述
为了实现可复现的机器学习科学迈出了一小步
·
跟进 发布在Towards Data Science ·11 分钟阅读·2023 年 11 月 12 日
–
在本文中,我们探讨了如何使用 Python 包mlscorecheck来测试报告的机器学习性能分数与实验设置描述之间的一致性。
免责声明:本文的作者是 mlscorecheck 包的作者。
什么是性能分数的一致性测试?
假设你遇到一个二分类问题的准确率(0.9494)、敏感性(0.8523)和特异性(0.9765)分数,这个测试集由 100 个正样本和 1000 个负样本组成。你能相信这些分数吗?你如何检查这些分数是否真的可能是所宣称的实验结果?这就是mlscorecheck
包可以通过提供一致性测试功能来帮助你的地方。在这个具体的例子中,可以利用
from mlscorecheck.check.binary import check_1_testset_no_kfold
result = check_1_testset_no_kfold(
testset={'p': 100, 'n': 1000},
scores={'acc': 0.8464, 'sens': 0.81, 'f1': 0.4894},
eps=1e-4
)
result['inconsistency']
#False
如果结果的'insconsistency'
标志为False
,则表明这些分数可能是从实验中得出的。(这是真的,因为这些分数对应于 81 个真实的正样本和 850 个真实的负样本。)如果准确率分数 0.8474 是由于意外的打印错误而报告的呢?
result = check_1_testset_no_kfold(
testset={'p': 100, 'n': 1000},
scores={'acc': 0.8474, 'sens': 0.81, 'f1': 0.4894},
eps=1e-4
)
result['inconsistency']
#True
测试调整后的设置时,结果显示不一致:这些分数可能不是实验的结果。要么分数错误,要么假定的实验设置不正确。
在接下来的内容中,我们将详细查看mlscorecheck包的主要特性和使用案例。
介绍
在研究和应用中,监督学习方法通常通过在一些实验中计算的性能分数进行排名(二分类,多分类,回归)。由于出版物中的打印错误,不当使用的统计数据,数据泄漏以及伪装,许多情况下报告的性能分数是不可靠的。除了对机器学习和人工智能中的可重复性危机做出贡献外,不切实际的高性能分数的影响通常还会被出版偏倚进一步放大,最终扭曲整个领域的研究。
mlscorecheck包的目标是提供数值技术以测试一组报告的性能分数是否可能是假定实验设置的结果。
一致性测试的操作
一致性测试的理念是,在给定的实验设置中,性能分数不能独立地取任何值:
-
例如,在一个二元分类测试集中有 100 个正样本时,灵敏度分数只能取值为 0.0, 0.01, 0.02, …, 1.0,但不能是 0.8543。
-
当报告多个性能分数时,它们需要彼此一致。例如,[准确率](https://zh.wikipedia.org/wiki/ROC 曲线)是[灵敏度和特异度](https://zh.wikipedia.org/wiki/ROC 曲线)的加权平均,因此,在一个由 100 个正样本和 100 个负样本组成的二元分类问题中,得分 acc = 0.96,sens = 0.91,spec = 0.97 是不可能的。
在更复杂的实验设置中(涉及[k 折交叉验证](https://zh.wikipedia.org/wiki/交叉验证 _(统计学))等),跨多个折叠/数据集的分数聚合,等等,约束条件变得更加先进,但它们仍然存在。mlscorecheck包实现了数值测试,以检查假设从实验中得出的分数是否满足相应的约束条件。
测试是数值化的,确定性地识别出不一致之处。用统计假设检验作类比,零假设是没有不一致,一旦发现某种不一致,就提供了反对零假设的证据,但作为数值测试,这种证据是无可争议的。
各种实验设置对性能分数施加各种约束,需要专门的解决方案。该包中实施的测试基于三个原则:通过区间计算加快的详尽枚举;线性整数规划;分数之间的分析关系。测试的灵敏度高度依赖于实验设置和数值不确定性:大数据集、大数值不确定性和少数报告的分数减少了测试识别偏离假设评估协议的能力。尽管如此,正如我们后面所看到的,这些测试在许多现实场景中仍然适用。有关测试数学背景的进一步详细信息,请参阅预印本和文档。
用例
现在,我们探讨一些示例,说明包的使用,但首先,我们讨论测试的一般要求和描述实验使用的一些术语。
要求
一致性测试有三个要求:
-
报告的性能分数的收集;
-
分数的估计数值不确定性(当分数被截断为4位小数时,可以假设实际值在报告值的 0.0001 范围内,这是分数的数值不确定性) — 这通常是测试的 eps 参数,只需检查分数即可推断。
-
实验的细节(涉及的数据集统计信息,交叉验证方案,聚合模式)。
术语表
实验规范中使用的术语:
-
得分均值(MoS):为每个折叠/数据集计算得分,然后平均以获得报告的得分;
-
均值得分(SoM):首先对折叠/数据集级别的原始数字(例如混淆矩阵)进行平均,然后从平均数字计算得分;
-
微平均:多类问题的评估通过将每个类别的表现与其他所有类别(作为二元分类)进行比较来完成,类级别的结果以均值得分的方式汇总;
-
宏平均:与微平均相同,但类级别分数以得分均值的方式汇总;
-
折叠配置:当使用 k 折交叉验证时,测试通常依赖于线性整数编程。了解折叠中类的样本数可以用于形成线性程序。这些折叠级别类样本计数称为折叠配置。
二元分类
在开始时,我们已经说明了在单个测试集上计算的二元分类分数时使用包的情况。现在,我们来看一些更高级的例子。
除了我们详细调查的两个示例外,该软件包还支持共 10 种用于二元分类的实验设置,其列表可以在文档中找到,并在示例笔记本中提供更多示例。
N 个测试集,均值得分汇总
在本例中,我们假设有 N 个测试集,不涉及 k 折交叉验证,但分数以均值得分的方式汇总,即为每个测试集确定原始真正例和真负例数字,然后从总(或平均)真正例和真负例数字计算性能分数。可用的分数被认为是准确率,负预测值和F1 分数。
例如,在实践中,对存储在一个张量中的 N 个测试图像进行图像分割技术的评估通常会导致这种情况。
软件包的设计是这样的,实验设置的细节被编码在测试函数的名称中,引导用户在选择适当的测试时注意所有可用的实验细节。在本例中,适当的测试是mlscorecheck.check.binary
模块中的check_n_testsets_som_no_kfold
函数,其中'som'
代表聚合模式(均值分数):
from mlscorecheck.check.binary import check_n_testsets_som_no_kfold
scores = {'acc': 0.4719, 'npv': 0.6253, 'f1': 0.3091}
testsets = [
{'p': 405, 'n': 223},
{'p': 3, 'n': 422},
{'p': 109, 'n': 404}
]
result = check_n_testsets_som_no_kfold(
testsets=testsets,
scores=scores,
eps=1e-4
)
result['inconsistency']
# False
结果表明,分数可能是实验的结果。毫不奇怪,这些分数是通过对测试集的真正阳性和真正阴性计数进行抽样,并按指定的方式计算得出的。然而,如果其中一个分数略有变化,例如 F1 修改为 0.3191,则配置变得不一致:
scores['f1'] = 0.3191
result = check_n_testsets_som_no_kfold(
testsets=testsets,
scores=scores,
eps=1e-4
)
result['inconsistency']
# True
进一步分析的详细信息,例如,关于可行性的证据可以从测试函数返回的字典中提取。关于输出的结构,同样,请参阅文档。
1 数据集,k 折交叉验证,分数均值聚合
在这个例子中,我们假设有一个数据集,对其中的二元分类器进行了分层重复 k 折交叉验证(2 折,3 次重复),并报告了各折产生的分数的均值。
这个实验设置可能是监督式机器学习中最常用的。
我们强调了知道和不知道**折叠配置之间的区别。通常,MoS 测试依赖于线性整数规划,并且需要折叠配置来制定线性整数规划。折叠配置可以通过列出折叠的统计数据来指定,或者可以引用导致确定性折叠统计的折叠策略,例如分层。后来,我们展示了即使在不知道折叠配置的情况下,也可以进行测试,不过在这种情况下,会测试所有可能的折叠配置,这可能会带来巨大的计算需求。
再次强调,第一步是选择要使用的适当测试。在这种情况下,正确的测试是check_1_dataset_known_folds_mos
函数,其中mos
表示聚合模式,known_folds
表示由于分层而知道折叠配置。测试的执行如下:
from mlscorecheck.check.binary import check_1_dataset_known_folds_mos
scores = {'acc': 0.7811, 'sens': 0.5848, 'spec': 0.7893}
dataset = {'p': 21, 'n': 500}
folding = {
'n_folds': 2,
'n_repeats': 3,
'strategy': 'stratified_sklearn'
}
result = check_1_dataset_known_folds_mos(
dataset=dataset,
folding=folding,
scores=scores,
eps=1e-4
)
result['inconsistency']
# False
类似于之前的例子,不存在不一致性,因为性能分数准备构成一个一致的配置。然而,如果其中一个分数略有变化,测试就会检测到不一致:
scores['acc'] = 0.79
result = check_1_dataset_known_folds_mos(
dataset=dataset,
folding=folding,
scores=scores,
eps=1e-4,
verbosity=0
)
result['inconsistency']
# True
在前面的例子中,我们假设了折叠配置是已知的。然而,在许多情况下,确切的折叠配置并不知道,也没有指定分层。在这些情况下,可以依赖于系统地测试所有可能的折叠配置的测试,如下例所示。这次,适当的测试在其名称中具有 'unknown_folds'
标记,表示将测试所有可能的折叠配置:
from mlscorecheck.check.binary import check_1_dataset_unknown_folds_mos
folding = {'n_folds': 2, 'n_repeats': 3}
result = check_1_dataset_unknown_folds_mos(
dataset=dataset,
folding=folding,
scores=scores,
eps=1e-4,
verbosity=0
)
result['inconsistency']
# False
与以前一样,测试正确地识别出没有不一致性:在评估所有可能的折叠配置过程中,它测试了实际的分层配置,显示出一致性,并且凭借这一证据停止了对剩余配置的测试。
在实践中,在使用未知折叠进行测试之前,建议对可能要测试的折叠配置数量进行估计:
from mlscorecheck.check.binary import estimate_n_evaluations
estimate_n_evaluations(
dataset=dataset,
folding=folding,
available_scores=['acc', 'sens', 'spec']
)
# 4096
在最坏的情况下,解决 4096 个小型线性整数规划问题仍然可行,但是对于更大的数据集,潜在的折叠配置数量可能会迅速变得棘手。
多类分类
测试多类分类场景类似于二元情况,因此我们不会像在二元情况下那样进入太多细节。
在该包支持的 6 个实验设置中,我们选择了一个常用的用于说明的设置:假设有一个多类数据集(4 类),并且使用了 4 折重复分层 k 折交叉验证。我们还知道分数是以宏平均的方式聚合的,即,在每个折叠中,针对每个类别的性能以二元分类方式评估所有其他类别,然后在类别和折叠上进行平均。
再次,第一步是选择合适的测试函数,在这种情况下,选择了来自mlscorecheck.check.multiclass
模块的check_1_dataset_known_folds_mos_macro
。名称中的 'mos’
和 'macro’
表示实验中使用的聚合方式。
from mlscorecheck.check.multiclass import check_1_dataset_known_folds_mos_macro
scores = {'acc': 0.626, 'sens': 0.2483, 'spec': 0.7509}
dataset = {0: 149, 1: 118, 2: 83, 3: 154}
folding = {
'n_folds': 4,
'n_repeats': 2,
'strategy': 'stratified_sklearn'
}
result = check_1_dataset_known_folds_mos_macro(
dataset=dataset,
folding=folding,
scores=scores,
eps=1e-4,
verbosity=0
)
result['inconsistency']
# False
类似于前面的情况,通过手工制作的一组一致分数,测试检测到没有不一致性。然而,一个小的改变,例如,将准确度修改为 0.656,就会使配置变得不可行。
回归
mlscorecheck包支持的最后一个监督学习任务是回归。测试回归问题是最困难的,因为对测试集的预测可以取任何值,因此实验可以产生任何分数值。回归测试唯一可以依赖的是当前支持的平均绝对误差 (mae)、均方误差 (mse) 和 r 平方 (r2) 之间的数学关系。
在以下示例中,我们假设mae和r2分数是针对测试集报告的,并且我们知道其主要统计数据(样本数量和方差)。然后,可以执行一致性测试,如下所示:
from mlscorecheck.check.regression import check_1_testset_no_kfold
var = 0.0831
n_samples = 100
scores = {'mae': 0.0254, 'r2': 0.9897}
result = check_1_testset_no_kfold(
var=var,
n_samples=n_samples,
scores=scores,
eps=1e-4
)
result['inconsistency']
# False
再次地,测试正确显示没有不一致(分数是通过实际评估准备的)。但是,如果r2分数稍微改变,例如,变为 0.9997,配置将变得不可行。
测试包
为了使针对流行的、广泛研究的问题报告的分数的一致性测试更容易,mlscorecheck包包括了多个被认为是某些问题标准的实验设置的规范。
DRIVE 数据集上的视网膜血管分割
在视网膜图像分析领域,存在一个歧义的问题:作者可以自由选择是否考虑视野圆形区域之外的像素,而这一选择在出版物中很少被指明。这种歧义可能导致基于不可比性能分数的算法排名。在mlscorecheck包中实现的功能适合识别作者是否使用了视野之外的像素进行评估。
最广泛研究的问题之一是基于DRIVE数据集的血管分割。为了避免查找图像统计数据和构建实验设置的繁琐任务,包中包含了数据集的统计数据,并提供了两个高级功能来测试图像级和聚合分数的歧义。例如,拥有测试图像‘03’的DRIVE数据集的图像级准确性、敏感性和特异性分数三元组,可以利用包进行如下操作:
from mlscorecheck.check.bundles.retina import check_drive_vessel_image
scores = {'acc': 0.9323, 'sens': 0.5677, 'spec': 0.9944}
result = check_drive_vessel_image(
scores=scores,
eps=10**(-4),
image_identifier='03',
annotator=1
)
result['inconsistency']
# {'inconsistency_fov': False, 'inconsistency_all': True}
结果表明,对于该图像的分数必须仅通过使用视野内的(fov)像素进行评估,因为这些分数与这一假设是一致的,但与使用所有像素进行评估的替代假设是不一致的。
进一步的测试包
mlscorecheck包中支持的所有流行研究问题和对应的公开数据集的列表如下:
-
视网膜图像分析:
-
从电生理图信号预测足月分娩: TPEHG。
征稿启事
欢迎各领域的专家提交更多测试包,以促进对机器学习性能评分在各研究领域的验证!
结论
对机器学习研究的荟萃分析不包括很多技术,超出了对论文的全面评估和可能尝试重新实现所提出的方法以验证声明结果的范围。mlscorecheck包提供的功能使得对机器学习研究的荟萃分析更为简明、数值化,有助于维护各研究领域的完整性。
进一步阅读
如需更多信息,我们建议查看:
-
mlscorecheck包的 README,
-
套件中提供的说明性笔记本,
-
详细文档,
-
描述数值方法的预印本。
测试支持 1162 种语言的大规模多语言语音(MMS)模型
探索 Meta 最新的自动语音识别(ASR)模型的前沿多语言功能
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 5 月 26 日
–
介绍
大规模多语言语音(MMS)¹ 是 Meta AI 最近发布的产品(就在几天前)。它通过将其覆盖范围从大约 100 种语言扩展到超过 1,000 种语言,推动了语音技术的边界。这是通过构建一个单一的多语言语音识别模型实现的。该模型还可以识别超过 4,000 种语言,比以前的能力提高了 40 倍。
MMS 项目旨在让人们更容易以他们首选的语言获取信息和使用设备。它将文本转语音和语音转文本技术扩展到服务不足的语言,继续减少我们全球世界中的语言障碍。现有的应用程序现在可以包括更多种类的语言,例如虚拟助手或语音激活设备。与此同时,跨文化沟通中出现了新的应用场景,例如在消息服务或虚拟与增强现实中。
在本文中,我们将介绍 MMS 在英语和葡萄牙语的 ASR 应用,并提供逐步指南,帮助设置环境以运行该模型。
图 1:大规模多语言语音(MMS)能够识别超过 4,000 种语言,并支持 1162 种语言(source)
本文属于“大语言模型编年史:导航 NLP 前沿”系列文章中的一篇,这是一个新的每周系列文章,旨在探索如何利用大型模型的力量来完成各种 NLP 任务。通过深入了解这些前沿技术,我们旨在赋能开发者、研究人员和爱好者,利用 NLP 的潜力并开启新的可能性。
迄今为止发布的文章:
与往常一样,代码可以在我的 Github 上找到。
构建大规模多语言语音模型的方法
Meta 使用了宗教文本,例如《圣经》,来构建一个覆盖广泛语言的模型。这些文本有几个有趣的特点:首先,它们被翻译成多种语言,其次,有公开的音频录音,记录了不同语言的人朗读这些文本。因此,训练该模型的主要数据集是《新约》,研究团队能够收集到超过 1,100 种语言的文本,并提供了每种语言超过 32 小时的数据。他们进一步拓展了识别 4,000 种语言的能力。这是通过使用各种其他基督教宗教阅读的未标注录音实现的。从实验结果来看,尽管数据来自特定领域,但它具有良好的泛化能力。
这些并不是工作的唯一贡献。他们创建了一个新的预处理和对齐模型,可以处理长时间的录音。这被用来处理音频,错位的数据通过最终的交叉验证过滤步骤被移除。回忆一下我们之前的文章,我们看到 Whisper 的一个挑战是无法正确对齐转录。该方法的另一个重要步骤是使用 wav2vec 2.0,一个自监督学习模型,来在超过 1,400 种语言的海量语音数据(约 500,000 小时)上训练他们的系统。我们之前讨论的标注数据集不足以训练 MMS 这样规模的模型,因此使用了 wav2vec 2.0 来减少对标注数据的需求。最后,结果模型会被微调以执行特定的语音任务,例如多语言语音识别或语言识别。
MMS 模型在几天前由 Meta 开源,并在 Fairseq 仓库中提供。在下一节中,我们将深入了解 Fairseq 及如何测试这些来自 Meta 的新模型。
Fairseq 仓库概览:一个强大的序列到序列学习工具包
Fairseq 是一个开源的序列到序列工具包,由 Facebook AI Research(也称为 FAIR)开发。它提供了各种序列建模算法的参考实现,包括卷积神经网络、递归神经网络、变压器及其他架构。
Fairseq 仓库基于 PyTorch,这是另一个开源项目,最初由 Meta 开发,现在归 Linux 基金会旗下。它是一个非常强大的机器学习框架,提供了高灵活性和速度,特别是在深度学习方面。
Fairseq 实现旨在帮助研究人员和开发者训练自定义模型,支持翻译、摘要、语言建模和其他文本生成任务。Fairseq 的一个关键特性是它支持分布式训练,这意味着它可以有效利用多个 GPU,无论是在单台机器上还是跨多台机器。这使其非常适合大规模机器学习任务。
大规模多语言语音模型
Fairseq 提供了两个可下载的预训练模型:MMS-300M 和 MMS-1B。你还可以访问针对不同语言和数据集的微调模型。为了我们的目的,我们测试了在 FLEURS 数据集中针对 102 种语言微调的 MMS-1B 模型,以及针对 1162 种语言(!)微调的 MMS-1B-all,微调使用了多个不同的数据集。
实现自动语音识别与大规模多语言语音
请记住,这些模型仍处于研究阶段,使得测试变得更具挑战性。与生产就绪的软件相比,还需要额外的步骤。
首先,你需要在项目根目录下设置一个.env
文件,以配置你的环境变量。它应如下所示:
CURRENT_DIR=/path/to/current/dir
AUDIO_SAMPLES_DIR=/path/to/audio_samples
FAIRSEQ_DIR=/path/to/fairseq
VIDEO_FILE=/path/to/video/file
AUDIO_FILE=/path/to/audio/file
RESAMPLED_AUDIO_FILE=/path/to/resampled/audio/file
TMPDIR=/path/to/tmp
PYTHONPATH=.
PREFIX=INFER
HYDRA_FULL_ERROR=1
USER=micro
MODEL=/path/to/fairseq/models_new/mms1b_all.pt
LANG=eng
接下来,你需要配置位于fairseq/examples/mms/asr/config/infer_common.yaml
的 YAML 文件。该文件包含脚本使用的重要设置和参数。
在 YAML 文件中,为checkpoint
字段使用完整路径,如下所示(除非你使用的是容器化应用程序来运行脚本):
checkpoint: /path/to/checkpoint/${env:USER}/${env:PREFIX}/${common_eval.results_path}
这个完整路径是必要的,以避免潜在的权限问题,除非你在容器中运行应用程序。
如果你计划使用 CPU 进行计算而不是 GPU,则需要在 YAML 文件的顶部添加以下指令:
common:
cpu: true
此设置指示脚本使用 CPU 进行计算。
我们使用dotevn
Python 库在 Python 脚本中加载这些环境变量。由于我们正在覆盖一些系统变量,我们需要使用一个技巧以确保加载正确的变量。我们使用dotevn_values
方法并将输出存储在一个变量中。这确保了我们获取的是存储在.env
文件中的变量,而不是即使名称相同的随机系统变量。
config = dotenv_values(".env")
current_dir = config['CURRENT_DIR']
tmp_dir = config['TMPDIR']
fairseq_dir = config['FAIRSEQ_DIR']
video_file = config['VIDEO_FILE']
audio_file = config['AUDIO_FILE']
audio_file_resampled = config['RESAMPLED_AUDIO_FILE']
model_path = config['MODEL']
model_new_dir = config['MODELS_NEW']
lang = config['LANG']
然后,我们可以克隆 fairseq GitHub 仓库并在我们的机器上安装它。
def git_clone(url, path):
"""
Clones a git repository
Parameters:
url (str): The URL of the git repository
path (str): The local path where the git repository will be cloned
"""
if not os.path.exists(path):
Repo.clone_from(url, path)
def install_requirements(requirements):
"""
Installs pip packages
Parameters:
requirements (list): List of packages to install
"""
subprocess.check_call(["pip", "install"] + requirements)
git_clone('https://github.com/facebookresearch/fairseq', 'fairseq')
install_requirements(['--editable', './'])
我们已经讨论了本文中使用的模型,因此让我们将它们下载到本地环境中。
def download_file(url, path):
"""
Downloads a file
Parameters:
url (str): URL of the file to be downloaded
path (str): The path where the file will be saved
"""
subprocess.check_call(["wget", "-P", path, url])
download_file('https://dl.fbaipublicfiles.com/mms/asr/mms1b_fl102.pt', model_new_dir)
关于 MMS 模型的输入,还有一个额外的限制,即音频数据的采样率需要为 16000 Hz。在我们的情况下,我们定义了两种生成这些文件的方法:一种是将视频转换为音频,另一种是重新采样音频文件以获得正确的采样率。
def convert_video_to_audio(video_path, audio_path):
"""
Converts a video file to an audio file
Parameters:
video_path (str): Path to the video file
audio_path (str): Path to the output audio file
"""
subprocess.check_call(["ffmpeg", "-i", video_path, "-ar", "16000", audio_path])
def resample_audio(audio_path, new_audio_path, new_sample_rate):
"""
Resamples an audio file
Parameters:
audio_path (str): Path to the current audio file
new_audio_path (str): Path to the output audio file
new_sample_rate (int): New sample rate in Hz
"""
audio = AudioSegment.from_file(audio_path)
audio = audio.set_frame_rate(new_sample_rate)
audio.export(new_audio_path, format='wav')
我们现在准备使用支持 1162 种语言的 MMS-1B-all 模型运行推理过程。
def run_inference(model, lang, audio):
"""
Runs the MMS ASR inference
Parameters:
model (str): Path to the model file
lang (str): Language of the audio file
audio (str): Path to the audio file
"""
subprocess.check_call(
[
"python",
"examples/mms/asr/infer/mms_infer.py",
"--model",
model,
"--lang",
lang,
"--audio",
audio,
]
)
run_inference(model_path, lang, audio_file_resampled)
使用 Fairseq 的自动语音识别结果
在这一部分,我们描述了我们的实验设置并讨论了结果。我们使用 Fairseq 的两种不同模型(MMS-1B-all 和 MMS-1B-FL102)在英语和葡萄牙语中进行了 ASR。你可以在我的 GitHub 库中找到音频文件。这些文件是我自己生成的,仅用于测试目的。
让我们从 MMS-1B-all 模型开始。以下是英语和葡萄牙语音频样本的输入和输出:
Eng:只需一个小片段即可了解新的 Facebook Research 模型在语音识别任务中的实际表现
Por:现在只需在这里提供一个示例,以便尝试了解 Facebook Research 的新模型是否真正有效,我们将进行测试。
使用 MMS-1B-FL102 时,生成的语音质量显著下降。我们来看一下英语的相同示例:
Eng:只需回顾一下小片段,以了解新的 Facebook Research 模型在速度识别任务中的实际表现,让我们看看
尽管生成的语音对于我们今天拥有的模型标准来说并不特别令人印象深刻,但我们需要从这些模型可以使 ASR 服务于更广泛全球人群的角度来考虑这些结果。
结论
Meta 开发的 Massively Multilingual Speech 模型代表了推动全球沟通和扩大语言技术应用范围的又一步。它能够理解超过 4000 种语言,并在其中 1162 种语言中有效运行,提高了许多传统上被忽视语言的可及性。
我们对 MMS 模型的测试展示了该技术在当前阶段的可能性和局限性。尽管 MMS-1B-FL102 模型生成的语音不如预期令人印象深刻,但 MMS-1B-all 模型提供了有希望的结果,展示了其在英语和葡萄牙语中的转录能力。葡萄牙语一直是那些服务不足的语言之一,特别是当我们考虑到来自葡萄牙的葡萄牙语时。
欢迎尝试你喜欢的语言,并在评论区分享转录结果和反馈。
保持联系:LinkedIn
参考文献
文本分类挑战:极小数据集上的微调与 ChatGPT
LLM 在极小数据集上表现优秀,但随着数据集的增大,传统方法则表现突出
·
关注 发布于 Towards Data Science · 7 分钟阅读 · 2023 年 7 月 7 日
–
Debby Hudson拍摄的照片,发布于Unsplash。
Toloka ML 团队不断研究并比较在各种条件下的文本分类不同方法。在这里,我们展示了一个关于 NLP 模型在极小数据集上训练表现的实验。
之前,我们提供了一个关于潜在解决方案的简要概述和将经典模型与大型语言模型(LLMs)进行比较的内容,针对特定的文本分类任务。然而,那些比较是基于一个包含足够数据点以建立可靠分类器的“常规”数据集。在实际应用场景中,你可能会遇到数据有限或没有进行人工标注的情况。
直观上,像 GPT-3 或 ChatGPT 这样的 LLMs 可能会由于其广泛的“知识”而优于较小的模型。为了验证这一假设,我们通过提取较大数据集的一部分创建了一个人工的小数据集,并比较了几种方法。我们微调了 RoBERTa 基础模型,使用 ChatGPT 进行少量示例分类,并微调了 GPT-3 Babbage 模型。
数据集
为了评估各种模型的理解能力,我们选择了一个由科学文章摘要组成的多类别数据集。任务是确定每篇文章的领域。
我们选择了WOS-11967 [1] 数据集,该数据集包含 11,967 个文档,涵盖 35 个类别,其中包括七个父类别:医学、心理学、计算机科学、生物化学、电气工程、土木科学和机械工程。我们抽取了 10,000 个数据点,仅关注父类别进行分析。
尽管数据集并未完全平衡,但类别分布相当合理。因此,所有类别都可能取得令人满意的结果。类别分布如下图所示。
WOS-11967数据集的样本类别分布
通过人工分析,我们发现确定一些摘要的领域相对简单,而在其他情况下,任务变得更加具有挑战性。例如,计算机科学文章可能讨论数学主题,或心理学文章可能包含医学或生化术语和缩写,使其难以与生物化学或医学领域区分开。摘要的长度也有显著差异,平均为 274 个 token(ChatGPT tokens),标准差为 115 个 token。
为了模拟涉及超小数据集的场景,我们对语料库进行了训练-测试拆分,并将少量样本分配到训练集中。我们重复进行了三次不同训练集大小的测试,以评估模型基于可用训练数据的性能变化。我们为实验创建了三个拆分:WOS-11967-s200(训练集中 200 个样本,测试集中 9,800 个样本),WOS-11967-s500(500 / 9,500),和 WOS-11967-s2000(2,000 / 8,000)。
现在,让我们看看使用不同模型解决这些问题所获得的结果。
正常的 RoBERTa 微调
作为基线,我们选择了RoBERTa base模型,并在前面提到的三个数据集上进行了微调。我们对每次运行使用了相同的超参数配置(批量大小为 32,学习率为 3e-5,带热身的线性调度器,256 标记窗口),并采用了早停法以防止过拟合。
我们获得了以下结果:
数据显示,200 个样本不足以提取准确分类摘要所需的所有模式和信息。较低的宏平均 F1 得分也表明模型在机械工程等代表性不足的类别上表现不佳。这表明,仅有少量来自特定类别的样本是不够的。
正如预期的那样,随着可用数据量的增加,模型的性能得到了改善——最终实现了在七个类别中的多类别分类的强大性能。
与 ChatGPT 的少样本学习
我们探索的第二种方法是使用 ChatGPT 的少样本分类。这种方法与传统分类方法有很大不同,因为它不涉及训练模型本身。相反,我们设计了输入提示以实现最佳性能。
然而,由于模型的 4096 标记上下文大小限制,无法将所有 200 个样本输入模型。根据上述测量,我们只能向模型展示大约 14 个摘要。在考虑到用于指令和分隔符的标记时,这一数字进一步减少。
最初,我们采用了“系统”角色来提供指令,并为每个类别提供了一个示例以指导模型的响应。我们将类别名称简化为单个标记,同时保留其含义。这使得模型更容易选择合适的类别,并将输出限制为单个标记。例如,“生物化学”变成了“Bio”,“计算机科学”变成了“Computer”。此外,我们通过提供类别列表让模型选择,并指示模型如果不确定类别则返回“Unknown”标记,从而限制了生成的标记数量。
总体而言,使用这种方法的性能不如仅在 200 个样本上训练的 RoBERTa 模型。我们注意到模型的分类能力严重依赖于提供的提示。修改一句话可能会改善或恶化指标。在某些情况下,尽管有明确指示,ChatGPT 仍错过了类别(这可能是我们制定提示方式的一个缺陷)。
在一些边缘情况下,它生成了未列在指令中的类别,但描述了文章领域,如“数学”或“化学”。尚不清楚这些缺陷应归因于模型还是数据集。然而,根据验证集,这些类别可以使用简单规则进行更正,如将所有“数学”实例改为“计算机”。
为了提高指标,我们尽可能使用了更多的数据。由于我们仍无法将全部 200 个样本输入模型,我们设计了一个两阶段的过程:
-
首先,我们要求模型识别特定领域摘要之间的相似性并生成总结。
-
其次,我们将这些总结纳入指令中,以便为模型提供关于第一阶段模型自身识别的类别和特征的见解。
这种方法使我们能够将更多训练数据样本输入模型;并且有效——我们将指标提升了约 10%。以下是我们用来生成这些总结的提示:
用于提取有关文章领域的有意义信息的 ChatGPT 提示
对于每个领域,我们提供了七到八个摘要,共使用了 63 个不同的摘要来准备分类提示(每七个类别八个摘要用于构建总结,七个摘要作为实际提示中的示例)。
尽管如此,我们指示模型在对类别不确定时回复“未知”。在验证集中,我们观察到大多数“未知”回应对应于计算机科学文章。然后我们用“计算机”类别替换了所有“未知”实例。
生成的分类提示如下:
用于分类文章摘要的最终提示
再次强调,性能受提示和提供的样本的影响很大。模型还生成了多个目标列表之外的类别,需要根据验证集进行手动调整。这种方法得出了以下结果:
性能明显优于在 200 个样本上微调的 RoBERTa 模型——并且所需样本更少。然而,随着标记数据的增加,RoBERTa 开始超过这种方法,即使只有 500 个样本。
我们相信,通过适当的提示工程,可以进一步提升性能。一些有用的提示和技巧可以在Prompting Guide中找到。
微调 GPT-3 模型
对于我们最终的方法,我们在这三个数据集上对 GPT-3 Babbage 模型进行了微调。我们遵循了OpenAI 指南中概述的数据集准备建议,并选择了默认的超参数,没有进行任何特定的调整。每个数据集的训练过程大约花费了 20 分钟,得到以下结果:
微调后的 GPT-3 模型即使在最小的数据集上也表现出色,超越了 RoBERTa 和 ChatGPT。随着训练数据量的增加,RoBERTa 和调优后的 GPT-3 模型之间的性能差距缩小。这引发了对使用这两种选项的资源和可行性的疑问。我们在之前的文章中讨论了这两种方法的优缺点。
结论
这个实验表明,我们的初步假设是正确的——在更大数据上训练的更大模型在额外小的数据集上表现显著更好。通过适当的提示工程和少样本技术,可以取得良好的结果。
然而,随着数据集大小的增加,性能差异会减少。此外,适当定制的经典模型,例如领域适配的 RoBERTa 模型,有时可以在分类任务中优于通用的 LLM。这可以归因于模型对特定主题的专门“知识”。此外,通过正确的优化,使用这些模型的推断速度可以显著提高,这在开发在线服务时至关重要。
除非另有说明,所有图片均由作者提供。
来源
-
Kowsari K, Brown DE, Heidarysafa M, Jafari Meimandi K, Gerber MS, Barnes LE. HDLTex: 分层深度学习用于文本分类。载于:机器学习与应用(ICMLA),2017 年第 16 届 IEEE 国际会议。IEEE; 2017。
-
刘 Y, Ott M, Goyal N 等. RoBERTa: 一种稳健优化的 BERT 预训练方法。CoRR。2019;abs/1907.11692.
arxiv.org/abs/1907.11692
使用 Transformer 编码器进行文本分类
原文:
towardsdatascience.com/text-classification-with-transformer-encoders-1dcaa50dabae
使用 Transformer 编码器进行文本分类的逐步说明
·发表于 Towards Data Science ·阅读时间 15 分钟·2023 年 8 月 11 日
–
毋庸置疑,Transformer 是深度学习领域最重要的突破之一。该模型的编码器-解码器架构在跨领域应用中证明了其强大功能。
最初,Transformer 仅用于语言建模任务,如机器翻译、文本生成、文本分类、问答等。然而,最近 Transformer 也被用于计算机视觉任务,如图像分类、物体检测和语义分割。
鉴于其受欢迎程度以及存在许多基于 Transformer 的复杂模型,如 BERT、Vision-Transformer、Swin-Transformer 和 GPT 家族,我们必须深入了解 Transformer 架构的内部工作原理。
在本文中,我们将仅分析 Transformer 的编码器部分,该部分主要用于分类目的。具体来说,我们将使用 Transformer 编码器来分类文本。事不宜迟,让我们首先查看一下我们将在本文中使用的数据集。
关于数据集
我们将使用的数据集是电子邮件数据集。您可以通过这个 链接 在 Kaggle 上下载此数据集。该数据集的许可证为 CC0:公共领域,这意味着您可以自由使用和分发此数据集。
import math
import torch
import torch.nn as nn
import torchtext
import pandas as pd
from sklearn.model_selection import train_test_split
from torch.utils.data import DataLoader
from tqdm import tqdm
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
df = pd.read_csv('spam_ham.csv')
df_train, df_test = train_test_split(df, test_size=0.2, random_state=42)
print(df_train.head())
# Output
'''
Category Message
1978 spam Reply to win £100 weekly! Where will the 2006 ...
3989 ham Hello. Sort of out in town already. That . So ...
3935 ham How come guoyang go n tell her? Then u told her?
4078 ham Hey sathya till now we dint meet not even a si...
4086 spam Orange brings you ringtones from all time Char...
'''
任务非常简单:这是一个二分类问题,给定一封电子邮件的文本,我们的 Transformer 编码器模型需要预测该文本是否为垃圾邮件。
接下来,我们创建一个从标签到其索引的映射,例如 ‘ham’ 将是 0,而 ‘spam’ 将是 1。
labels = df_train["Category"].unique()
num_labels = len(labels)
label2id, id2label = dict(), dict()
for i, label in enumerate(labels):
label2id[label] = i
id2label[i] = label
print(id2label)
print(label2id)
# Output
'''
{0: 'spam', 1: 'ham'}
{'spam': 0, 'ham': 1}
'''
现在让我们了解 Transformer 编码器模型的整体工作流程。
Transformer 编码器如何工作
要理解 Transformer 编码器如何工作,我们从过程的最开始部分开始,即数据预处理。
正如你所知道的,我们将在本文中处理文本数据,而 Transformer 无法处理原始文本。因此,我们首先要做的就是将文本转换为机器可读的格式,这可以通过标记化过程来实现。
标记化
标记化是将输入文本拆分为标记的过程。一个标记可以由一个字符、一个单词或一个子词组成,这取决于使用的标记化器类型。在这篇文章中,我们将使用单词级标记化,这意味着每个标记代表一个单词。
# Load tokenizer
tokenizer = get_tokenizer('basic_english')
text = 'this is text'
print(tokenizer(text))
# Output
'''
[this, is, text]
'''
接下来,每个标记将根据所谓的词汇表映射到其整数表示。
词汇表基本上是字符、单词或子词及其整数映射的集合。由于我们在单词级别进行标记化,因此我们的词汇表将是单词及其整数映射的集合。
让我们基于训练数据集建立一个词汇表:
# Initialize training data iterator
class TextIter(torch.utils.data.Dataset):
def __init__(self, input_data):
self.text = input_data['Message'].values.tolist()
def __len__(self):
return len(self.text)
def __getitem__(self, idx):
return self.text[idx]
# Build vocabulary
def yield_tokens(data_iter):
for text in data_iter:
yield tokenizer(text)
data_iter = TextIter(df_train)
vocab = build_vocab_from_iterator(yield_tokens(data_iter), specials=["<pad>", "<unk>"])
vocab.set_default_index(vocab["<unk>"])
print(vocab.get_stoi())
# Output
'''
{'<pad>':0, '<unk>':1,..., 'ny-usa': 7449, ...}
'''
如上面的代码片段所示,我们训练数据中的每个单词在词汇表中都有其独特的整数。如果你注意到,我们还将两个特殊标记 和 添加到词汇表中。 标记对于后续的批量训练非常有用,以确保每个训练数据批次具有相同的序列长度。
与此同时, 标记对处理词汇表外的单词非常有用。每当我们遇到一个词汇表中没有的单词时,它将被分配为 标记。
text_unk = 'this is jkjkj' # jkjkj is an unknown word in our vocab
seq_unk = [vocab[word] for word in tokenizer(text_unk)]
print(tokenizer(text_unk))
print(seq_unk)
# Output
'''
['this', 'is', 'jkjkj']
[49, 15, 1]
'''
现在让我们创建一个玩具示例,贯穿整个文章。
# We will use this example throughout the article
text = 'this is text'
seq = [vocab[word] for word in tokenizer(text)]
print(tokenizer(text))
print(seq)
# Output
'''
['this', 'is', 'text']
[49, 15, 81]
'''
嵌入层
每个标记的整数表示就是我们传递给 Transformer 编码器模型第一层的输入,这一层是嵌入层。该层将每个整数转换为我们预先设置维度的向量。
class Embeddings(nn.Module):
def __init__(self, d_model, vocab_size):
super(Embeddings, self).__init__()
self.emb = nn.Embedding(vocab_size, d_model)
self.d_model = d_model
def forward(self, x):
return self.emb(x) * math.sqrt(self.d_model)
每个向量的维度通常对应于我们为 Transformer 模型选择的隐藏层大小。例如,BERT-base 模型的隐藏层大小为 768。
在以下示例中,我们序列中的每个标记 ([‘this’, ‘is’, ‘text’]) 将被转换为 4D 向量嵌入。
hidden_size = 4
input_data = torch.LongTensor(seq).unsqueeze(0)
emb_model = Embeddings(hidden_size, len(vocab))
token_emb = emb_model(input_data)
print(f'Size of token embedding: {token_emb.size()}')
# Output
'''
Size of token embedding: torch.Size([1, 3, 4]) [batch, no. seq token, dim]
'''
嵌入层的输出是一个 [batch, sequence_length, embedding_dim]
的张量。
作者提供的图片
位置编码
到目前为止,我们已经获得了序列中每个标记的嵌入,但这些嵌入并没有顺序感。同时,我们知道在任何文本和语言中,词语的顺序对捕捉句子的语义意义至关重要。
为了捕捉输入序列的顺序,Transformer 应用了一种叫做位置编码的方法。我们可以使用多种方式来应用位置编码,但它应满足以下条件:
-
编码应该对序列中的每个标记都是唯一的。
-
任意两个相邻标记之间的delta值或距离应该是一致的,并且与序列长度无关。
-
编码应该是确定性的。
-
并且它在我们处理更长序列时也应表现出良好的泛化能力。
在原始 Transformer 论文中,作者提出了一种利用正弦和余弦波组合的位置信息编码方法。这种方法满足了所有提到的条件,使模型能够有效地捕捉标记的顺序。
作者提供的图像
class PositionalEncoding(nn.Module):
def __init__(self, d_model, vocab_size=5000, dropout=0.1):
super().__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(vocab_size, d_model)
position = torch.arange(0, vocab_size, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, d_model, 2).float()
* (-math.log(10000.0) / d_model)
)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer("pe", pe)
def forward(self, x):
x = x + self.pe[:, : x.size(1), :]
return self.dropout(x)
位置编码应该具有与标记嵌入相同的维度,以便我们可以将位置编码添加到标记嵌入中。此外,位置编码是固定的,这意味着在训练过程中没有可更新的可学习参数。
作者提供的图像
pe_model = PositionalEncoding(d_model=4, vocab_size=len(vocab))
output_pe = pe_model(token_emb)
print(f'Size of output embedding: {output_pe.size()}')
# Output
'''
Size of output embedding: torch.Size([1, 3, 4]) [batch, no. seq token, dim]
'''
从标记嵌入和位置编码的加法中得到的输出嵌入将成为下一步骤的输入,即 Transformer 编码器堆栈。
自注意力
Transformer 编码器堆栈由几个部分组成,如下图所示:
作者提供的图像
首先,我们的输入嵌入将进入所谓的自注意力层。这一层是 Transformer 语言模型能够区分每个词的上下文和整个序列/句子的语义意义的主要因素。
自注意力层将使用不同的线性层将输入嵌入投影到查询、键和值向量中。查询、键和值是我们通常在检索系统或推荐系统中找到的术语。
例如,假设你想在 Netflix 上观看特定的电影。查询将是你在搜索栏中输入的电影标题;键将是 Netflix 目录中每部电影的描述;值将是基于你之前在搜索栏中输入的电影标题的电影推荐结果。
作者提供的图像
正如上面的可视化所示,查询、键和值都来自同一来源。这就是为什么这种注意力机制被称为自注意力。
如果你使用完整的 Transformer 架构(包括解码器部分)来进行像机器翻译这样的自回归任务,那么还会有另一个称为交叉注意力的注意力机制,其中查询来自解码器,而键和值来自编码器堆栈。然而,由于我们只使用编码器堆栈,所以在本文中不会讨论交叉注意力机制。
在获取查询、键和值之后,我们就可以执行自注意力机制了。
首先,我们将查询与键进行相乘(也称为点积操作)。
作者提供的图片
我们从点积操作中得到的是一个方形的注意力矩阵,其尺寸在两个维度上都等于序列中输入标记的数量。该矩阵指示了每个标记应该给序列中其他标记的注意力或相关性。
接下来,我们使用线性层的维度对注意力矩阵进行归一化,以在训练过程中获得稳定的梯度。然后,我们使用 Softmax 函数对矩阵进行归一化,使矩阵中每一行的值都为正且总和为 1。
作者提供的图片
自注意力机制的最后一步是值与我们的归一化注意力矩阵之间的另一个点积。这将给我们一个大小为[batch, no_of_sequence, hidden_size_dim]
的最终输出。
class SingleHeadAttention(nn.Module):
def __init__(self, d_model, d_head_size):
super().__init__()
self.lin_key = nn.Linear(d_model, d_head_size, bias=False)
self.lin_query = nn.Linear(d_model, d_head_size, bias=False)
self.lin_value = nn.Linear(d_model, d_head_size, bias=False)
self.d_model = d_model
def forward(self, x):
query = self.lin_query(x)
key = self.lin_key(x)
value = self.lin_value(x)
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(self.d_model)
p_attn = scores.softmax(dim=-1)
x = torch.matmul(p_attn, value)
return x
多头注意力
然而,Transformer 模型不仅仅使用一个自注意力块,或通常称为“头”。它使用多头注意力,其中多个单自注意力并行进行。小的区别在于,我们需要将每个单头注意力中的三个线性层的输出除以我们使用的头的总数。这确保了多头注意力的计算时间与单自注意力相当。
作者提供的图片
最后,我们需要将每个自注意力层的输出连接起来,然后将其投影到一个额外的线性层中。
作者提供的图片
class MultiHeadAttention(nn.Module):
def __init__(self, h, d_model, dropout=0.1):
super().__init__()
assert d_model % h == 0
d_k = d_model // h
self.multi_head = nn.ModuleList([SingleHeadAttention(d_model, d_k) for _ in range(h)])
self.lin_agg = nn.Linear(d_model, d_model)
def forward(self, x):
x = torch.cat([head(x) for head in self.multi_head], dim=-1)
return self.lin_agg(x)
就这样。这个多头注意力层的输出张量与输入的维度相同。
mult_att = MultiHeadAttention(h=2, d_model=4)
output_mult_att = mult_att(output_pe)
print(f'Size of output embedding after multi-head attention: {output_mult_att.size()}')
# Output
'''
Size of output embedding after multi-head attention: torch.Size([1, 3, 4])
'''
归一化层和残差连接
如果我们查看 Transformer 编码器块的架构,我们需要将多头注意力的输出与多头注意力的输入(也称为残差连接)相加,然后对其进行归一化。
作者提供的图片
这两个操作的原因是为了使 Transformer 模型在训练过程中能够更快收敛,并且它们还可以帮助模型更准确地进行预测。
class LayerNorm(nn.Module):
def __init__(self, d_model, eps=1e-6):
super(LayerNorm, self).__init__()
self.a_2 = nn.Parameter(torch.ones(d_model))
self.b_2 = nn.Parameter(torch.zeros(d_model))
self.eps = eps
def forward(self, x):
mean = x.mean(-1, keepdim=True)
std = x.std(-1, keepdim=True)
return self.a_2 * (x - mean) / (std + self.eps) + self.b_2
class ResidualConnection(nn.Module):
def __init__(self, d_model, dropout=0.1):
super().__init__()
self.norm = LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x1, x2):
return self.dropout(self.norm(x1 + x2))
再次强调,残差连接和归一化层后的输出张量维度将与多头注意力层的输出张量维度相同。
res_conn_1 = ResidualConnection(d_model=4)
output_res_conn_1 = res_conn_1(output_pe, output_mult_att)
print(f'Size of output embedding after residual connection: {output_res_conn_1.size()}')
# Output
'''
Size of output embedding after residual connection: torch.Size([1, 3, 4])
'''
前馈层
残差连接和归一化层的输出将成为前馈层的输入。这个层只是一个普通的线性层,如下所示:
class FeedForward(nn.Module):
def __init__(self, d_model, d_ff, dropout=0.1):
super().__init__()
self.w_1 = nn.Linear(d_model, d_ff)
self.w_2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.w_2(self.dropout(self.w_1(x).relu()))
这一层也不会改变我们张量的维度。
ff = FeedForward(d_model=4, d_ff=12)
output_ff = ff(output_res_conn_1)
print(f'Size of output embedding after feed-forward network: {output_ff.size()}')
# Output
'''
Size of output embedding after feed-forward network: torch.Size([1, 3, 4])
'''
在前馈层之后,我们需要应用第二个残差连接,将前馈层的输出与前馈层的输入相加。加法之后,我们使用上面描述的归一化层对张量进行归一化。
res_conn_2 = ResidualConnection(d_model=4)
output_res_conn_2 = res_conn_2(output_res_conn_1, output_ff)
print(f'Size of output embedding after second residual: {output_res_conn_2.size()}')
# Output
'''
Size of output embedding after second residual: torch.Size([1, 3, 4])
'''
Transformer 编码器堆栈
从多头自注意力层到前馈层之后的归一化层的过程对应一个单独的 Transformer 编码器堆栈。
作者提供的图片
现在我们可以将上述所有过程封装在一个名为 SingleEncoder()
的类中:
class SingleEncoder(nn.Module):
def __init__(self, d_model, self_attn, feed_forward, dropout):
super().__init__()
self.self_attn = self_attn
self.feed_forward = feed_forward
self.res_1 = ResidualConnection(d_model, dropout)
self.res_2 = ResidualConnection(d_model, dropout)
self.d_model = d_model
def forward(self, x):
x_attn = self.self_attn(x)
x_res_1 = self.res_1(x, x_attn)
x_ff = self.feed_forward(x_res_1)
x_res_2 = self.res_2(x_res_1, x_ff)
return x_res_2
在实际应用中,我们通常使用多个 Transformer 编码器,而不是仅使用一个。例如,BERT-base 模型使用了 12 个 Transformer 编码器堆栈。
class EncoderBlocks(nn.Module):
def __init__(self, layer, N):
super().__init__()
self.layers = nn.ModuleList([layer for _ in range(N)])
self.norm = LayerNorm(layer.d_model)
def forward(self, x):
for layer in self.layers:
x = layer(x)
return self.norm(x)
使用上述 EncoderBlocks()
,我们可以根据需要初始化多个 Transformer 编码器堆栈。
模型训练
现在我们知道了 Transformer 编码器的内部结构,接下来让我们使用它来训练数据以进行文本分类。
模型定义
在本文中,我们将使用六个 Transformer 编码器堆栈。隐藏层大小为 300,多头自注意力层中将有四个不同的头。你可以根据自己的需要调整这些值。
class TransformerEncoderModel(nn.Module):
def __init__(self, vocab_size, d_model, nhead, d_ff, N,
dropout=0.1):
super().__init__()
assert d_model % nhead == 0, "nheads must divide evenly into d_model"
self.emb = Embeddings(d_model, vocab_size)
self.pos_encoder = PositionalEncoding(d_model=d_model, vocab_size=vocab_size)
attn = MultiHeadAttention(nhead, d_model)
ff = FeedForward(d_model, d_ff, dropout)
self.transformer_encoder = EncoderBlocks(SingleEncoder(d_model, attn, ff, dropout), N)
self.classifier = nn.Linear(d_model, 2)
self.d_model = d_model
def forward(self, x):
x = self.emb(x) * math.sqrt(self.d_model)
x = self.pos_encoder(x)
x = self.transformer_encoder(x)
x = x.mean(dim=1)
x = self.classifier(x)
return x
model = TransformerEncoderModel(len(vocab), d_model=300, nhead=4, d_ff=50,
N=6, dropout=0.1).to(device)
如果你注意到,我们在最后一个 Transformer 编码器堆栈的输出上添加了一个额外的线性层。这个线性层将作为分类器。由于我们只有两个不同的类别(垃圾邮件/正常邮件),因此这个线性层的输出将是二。
另外,我们需要解决的一个重要问题是,最终堆栈的输出将是[batch, no_of_sequence, hidden_size]
,而我们的最终线性层期望的输入是[batch, hidden_size]
。我们可以采用几种方法来使堆栈的输出与线性层的输入匹配。
例如,BERT 只使用了一个特殊的称为 [CLS] 的标记,该标记在 Transformer 架构中的位置编码步骤之前插入到序列中。在这里,我们没有这个特殊的 [CLS] 标记。因此,我们改为在最后一个编码器堆栈之后对所有输出嵌入值进行平均。
数据加载器
接下来,我们需要为训练数据创建一个数据加载器,以便在训练过程中将数据分批输入到模型中。
class TextDataset(torch.utils.data.Dataset):
def __init__(self, input_data):
self.text = input_data['Message'].values.tolist()
self.label = [int(label2id[i]) for i in input_data['Category'].values.tolist()]
def __len__(self):
return len(self.label)
def get_sequence_token(self, idx):
sequence = [vocab[word] for word in tokenizer(self.text[idx])]
len_seq = len(sequence)
return sequence, len_seq
def get_labels(self, idx):
return self.label[idx]
def __getitem__(self, idx):
sequence, len_seq = self.get_sequence_token(idx)
label = self.get_labels(idx)
return sequence, label, len_seq
def collate_fn(batch):
sequences, labels, lengths = zip(*batch)
max_len = max(lengths)
for i in range(len(batch)):
if len(sequences[i]) != max_len:
for j in range(len(sequences[i]),max_len):
sequences[i].append(0)
return torch.tensor(sequences, dtype=torch.long), torch.tensor(labels, dtype=torch.long)
除了 dataloader 类,我们还需要创建上述的辅助函数 collate_fn()
。这个函数是必不可少的,因为为了将训练数据按批次提供,每个批次需要具有相同的维度。
由于我们处理的是具有不同句子长度的文本数据,因此每个批次的维度不一定相同。在 collate_fn
中,我们首先获取批次中序列的最大长度,然后向较短的序列中添加一堆******标记,直到其长度等于批次中最长序列的长度。
另一种你可以使用的方法是通过定义最大标记数。接下来,如果句子的标记数超过最大值,可以截断句子;如果标记数少于最大值,则可以添加一堆******标记。
训练循环
现在我们已经定义了模型架构和数据加载器类,那么我们可以开始训练模型了。
def train(model, dataset, epochs, lr, bs):
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam((p for p in model.parameters()
if p.requires_grad), lr=lr)
train_dataset = TextDataset(dataset)
train_dataloader = DataLoader(train_dataset, num_workers=1, batch_size=bs, collate_fn=collate_fn, shuffle=True)
# Training loop
for epoch in range(epochs):
total_loss_train = 0
total_acc_train = 0
for train_sequence, train_label in tqdm(train_dataloader):
# Model prediction
predictions = model(train_sequence.to(device))
labels = train_label.to(device)
loss = criterion(predictions, labels)
# Calculate accuracy and loss per batch
correct = predictions.argmax(axis=1) == labels
acc = correct.sum().item() / correct.size(0)
total_acc_train += correct.sum().item()
total_loss_train += loss.item()
# Backprop
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 0.5)
optimizer.step()
print(f'Epochs: {epoch + 1} | Loss: {total_loss_train / len(train_dataset): .3f} | Accuracy: {total_acc_train / len(train_dataset): .3f}')
epochs = 15
lr = 1e-4
batch_size = 4
train(model, df_train, epochs, lr, batch_size)
你将得到类似这样的输出:
图片由作者提供
模型预测
在我们训练模型之后,我们可以自然地使用它来预测测试集中的未见数据。为此,我们首先需要创建一个函数,封装数据预处理步骤和模型预测步骤。
def predict(text):
sequence = torch.tensor([vocab[word] for word in tokenizer(text)], dtype=torch.long).unsqueeze(0)
output = model(sequence.to(device))
prediction = id2label[output.argmax(axis=1).item()]
return prediction
现在,如果我们想预测测试集中的文本,可以直接调用上述函数:
idx = 24
text = df_test['Message'].values.tolist()[idx]
gt = df_test['Category'].values.tolist()[idx]
prediction = predict(text)
print(f'Text: {text}')
print(f'Ground Truth: {gt}')
print(f'Prediction: {prediction}')
# Output
'''
Text: This is the 2nd time we have tried 2 contact u. U have won the £750 Pound prize. 2 claim is easy, call 087187272008 NOW1! Only 10p per minute. BT-national-rate.
Ground Truth: spam
Prediction: spam
'''
idx = 35
text = df_test['Message'].values.tolist()[idx]
gt = df_test['Category'].values.tolist()[idx]
prediction = predict(text)
print(f'Text: {text}')
print(f'Ground Truth: {gt}')
print(f'Prediction: {prediction}')
# Output
'''
Text: Morning only i can ok.
Ground Truth: ham
Prediction: ham
'''
结论
在这篇文章中,我们讨论了利用 Transformer 的编码器部分来分类文本的逐步过程。如你所知,许多大型语言模型使用 Transformer 的编码器部分。例如,BERT 由于其 Transformer 编码器架构结合了大量的训练数据,在许多语言任务中取得了最先进的表现。
我希望这篇文章能帮助你入门 Transformer 架构。像往常一样,你可以通过 这个笔记本 查找本文实现的代码。
使用 NLP 进行文本纠正
原文:
towardsdatascience.com/text-correction-using-nlp-b68c7233b86
检测和纠正常见错误:问题与方法
·发布在 Towards Data Science ·19 分钟阅读·2023 年 1 月 13 日
–
图片由 Lorenzo Cafaro 提供,来源于 Pixabay
任何写作的人都会不时漏掉一个逗号。或者在某种语境下使用错误的介词。或者拼写错误。或者措辞尴尬。或者使用过于复杂或过长的句子。或者段落过长。或者过于冗长。
对于除了最短的写作之外的所有文本,也许以上所有内容以及更多。
我曾经有一个学生在他写的每一篇文章中都缺少了像a或the这样的冠词。我阅读了他数百页的内容,却没有找到一个冠词。
我曾经并且仍然不断地犯这些错误。即使是在我的短文如电子邮件中。
对于内容较为复杂的文本,如整本书籍或甚至短篇博客,文本问题自然会更多。这就是我们需要校对编辑的原因,他们的职责包括校对和编辑内容。
这也是为什么基于 NLP 的工具如 Grammarly 越来越受欢迎的原因。这些工具可以在几分钟内帮助人们在短文本如电子邮件中发现并纠正这些错误。对于较长的文本,它们可能会发现更多错误,这当然意味着修正这些错误会花费更多时间。
有关 Grammarly 的更多信息,请参见 [10]。
无论如何,作家无法与这些工具在提高高质量写作产量方面竞争。我还要补充一点,这样可以减少眼睛的疲劳。
这让我想起了以下的事情。很久很久以前,我写了一篇博士论文。数百页。那是一个痛苦的过程。如果没有像 Grammarly 这样的工具,我现在不会重复这个过程。
前言
在这篇文章中,我们首先描述并解释人们在写作时倾向于犯的各种错误。我们将仅限于基本错误,例如缺少逗号、缺少冠词或使用错误的介词。(在这篇文章中,我们用“错误”这个术语时比较宽松。我们实际上是指“改进建议”。)
随后,我们将集思广益,使用自然语言处理技术检测这些问题,并提供解决方案。
我们选择将这篇文章的范围限制在基本错误的原因有几个。首先,它们非常常见。其次,一些统计自然语言处理的基本方法,再加上一些特征工程,适用于这些错误的检测和纠正。
因此,在这个过程中,读者还将获得在有用且真实的环境中对统计自然语言处理基本方法的坚实基础。
相比之下,检测并提供针对更复杂问题的纠正建议,如措辞尴尬或以更简洁、更易读的方式重新表达,需要更先进的自然语言处理技术。在另一篇文章[11]中,我们也建模了上下文,超出了这篇文章所涵盖的基本问题。这仍然没有涵盖措辞尴尬或更简洁、更具信息性的表达,因为这些都是更复杂的话题。
文本中的基本错误
这里是我们将涵盖的内容,并附有实际示例。
-
缺少逗号。
-
缺少冠词。
-
忘记在“It’s”中使用撇号。
-
使用单数而非复数,或反之亦然。
-
在不应该使用连字符的地方使用了连字符,或反之亦然。
-
大小写问题:未在应大写字母的地方大写; 在应全大写的地方没有全大写。例如,fyi。
-
使用错误的介词,或在不需要时使用介词。
接下来,我们将对这些错误进行第二次讨论,探讨统计自然语言处理的基本解决方案。这一讨论将涉及使用哪些训练集、提取哪些特征,以及使用哪些统计模型。
我们甚至不会尝试解决上述以斜体标出的那些问题。这些问题需要更先进的自然语言处理方法,即那些能建模上下文的方法。
缺少逗号
我犯的最常见的错误是缺少逗号。考虑一下:
Regardless there is no way …
Clearly the writer meant …
在每种情况下,第一个单词后面都应该立即跟一个逗号。
大小写问题
有时会忘记在句子的开头单词中大写第一个字母,或在指代自己时大写‘i’。例如:
the most frequent error i make is …
这会给人留下不好的印象。
同样,
An fyi in case you are interested.
更好的表达方式是:
An FYI in case you are interested.
还有许多其他情况,其中全大写字母的形式更为适合。例如pdf、gpx、cdc、nlp、ai等。
缺少冠词
遗漏冠词也很常见。如:
… within matter of minutes … ⇒
… within a matter of minutes …
… capitalize first letter in word … ⇒
… capitalize the first letter in the word …
⇒之前的文本是原文。⇒之后的文本是正确的写法。我们将在整篇文章中使用这种约定来展示示例。
另一个我经常犯的错误是省略了Its中的撇号。实际上,我在本节开始的Its上犯了这个错误!
错误使用连字符
我经常犯这样的错误。以下是来自一个不错的帖子中的一些例子[2]。
在这个阶段,我只是展示这些例子。稍后当我们查看方法时,我们会引入一些[2]中的额外点。它们将帮助我们设计正确的特征或决定我们应该使用统计 NLP 中的哪种方法。
He’s an all star runner ⇒
He’s an all-star runner
Chocolate covered truffles ⇒
Chocolate-covered truffles
以及这些。
My co-worker has a four-year old child. ⇒
My co-worker has a four-year-old child
We sat on the plane for deicing for an hour. ⇒
We sat on the plane for de-icing for an hour.
让我们看看一些相反方向的例子。在这些例子中,我们不应使用连字符。
This car is a finely-tuned machine. ⇒
This car is a finely tuned machine.
She attends Ohio-State University. ⇒
She attends Ohio State University.
Heart-broken ⇒
Heartbroken
介词使用错误
我经常犯涉及介词的错误。特别是,我使用了错误的介词。例如,我经常使用by而应使用with。
如果我没有使用 Grammarly,我甚至不会知道自己在做这些事。即使在审阅我的文本之后也是如此。
以下是我想分享的第一个例子。它不是特定于介词的,但确实说明了我想表达的观点。
在写上述句子时,我写成了
And were I to not be using Grammarly, …
Grammarly 建议我去掉to和be。
And were I not using Grammarly
更容易阅读。
哦,实际上,我刚意识到在上述几行中还有两个额外的错误。现在已经修正。以下是包含错误的版本。
Below is the first example I want share.
Its Grammarly that …
在第一句中,我在want和share之间漏掉了to。在第二句中,Its应该是It’s或It is。
还有一件事。
我刚意识到,在写了Oh, in fact之后……我又引入了一些额外的错误!
好吧,我就停在这里,否则我可能会无限循环,生成带有错误的新文本来解释我在前一版本中犯的错误!
好吧,让我们看看其他例子。
She ran in the bedroom from the living room.
很明显,作者在这个上下文中指的是into。 (如果“from the living room”被遗漏,可能就不那么明确了。)
以下是来自[1]的例子。
Is that the best song you ever heard of?
of是不需要的。
我之前提到过,我经常使用by而应使用with。这里有一个例子。
For linear classification problems one can use linear neurons.
For classification problems that are not linearly separable,
replace linear by sigmoidal.
在上述例子中,linear by sigmoidal应为linear with sigmoidal。
解决方案
如果我们能够访问像 ChatGPT 这样的巨大语言模型[3]并能够按需或批量运行,我们可能就不需要像下面这样一点一点地进行。
我们假设读者没有这样的访问权限。此外,我们假设读者对了解这些用例相关的方法感兴趣,并可能想从头开始实现它们。
正如感兴趣的读者稍后将看到的,这篇帖子中讨论的方法将非常容易从头开始实现。
使用预先构建的大模型不会提供任何这样的见解。也就是说,如果可以获得大型语言模型,尝试一下是个好主意。体验它的行为。观察它能够解决哪些问题。在本文的背景下,评估其解决方案在什么方面优于我们的?
好的,回到我们具体建议的讨论。首先,我们将讨论用于训练的数据集。一个明智选择的数据集足以满足我们所有的用例。
接下来,我们将讨论一些来自统计自然语言处理的“基础构建块”方法。我们将从几个初步用例开始开发这些方法。然后我们将讨论这些相同的方法如何适用于我们在本文早些时候描述的许多其他用例,尽管需要不同的预处理或提取的特征。
也就是说,本文早些时候详细描述的一些用例,我们将不会尝试讨论解决方案。这是因为这些用例似乎需要考虑上下文的更高级方法,即复杂的语言模型。我们将在未来的文章中讨论这些。
训练数据
我们在本文中讨论的所有用例所需的模型原则上可以从一个数据集中学习。一个质量合理的文本文档语料库。
比如维基百科。事实上,人们可以将维基百科的整个文本下载到自己的计算机上。参见[4]。
对于一些问题,即使手动复制几页维基百科或从一些合理质量的网页中复制内容,也足以进行快速的初步训练和评估。
简单计数方法
考虑尝试检测过长的句子或段落。
我们将以如下方式训练这些检测方法。首先,我们将文本中的每个文档标记化为段落,然后将每个段落标记化为句子,最后将每个句子标记化为词语。
可以使用 NLTK 将段落标记化为句子。参见[5]。
将文档分解为段落,再将段落分解为句子本身也是相当有趣的自然语言处理问题。我们将在另一篇文章中涵盖这些方法。这里我们的重点在其他地方。
现在我们可以分别构建句子和段落中的单词数模型。利用这些模型,我们可以标记异常长的句子或异常长的段落。
长度分布的参数模型
我们在这个主题上想要补充的主要内容是,正态分布并不是建模句子或段落长度的一个好选择。
正态分布是对称的,尾部延伸至负无穷大和正无穷大。相比之下,句子长度更可能按以下方式分布。单词句子虽然存在,但相对较少。随着长度的增加,这种长度的句子会变得更常见。随着句子长度的进一步增加,这种长度的句子会变得不那么常见,呈指数级减少。
读者可以通过快速上下滚动这篇文章并目测各种句子的长度来测试这一推理。读者会发现一些两词句子。大多数句子包含五到十个词。读者不会找到包含三十个词的句子。这些句子太长了。
好的,我们回到我们想要表征的分布。它应该是这样的。
泊松分布比正态分布更适合这种形状。见[6]。
泊松分布有一个参数,即一个可调节的旋钮,可以进一步微调形状。这个参数可以从数据中进行调节。
另见[7],这是一篇专注于文本中句子长度分布建模的论文。正如文中讨论的那样,对数正态分布也值得考虑。
长度分布的非参数模型
假设我们的语料库包含大量句子。我们可以避免对分布形式做任何假设。它应该是泊松分布?还是帕累托分布?还是对数正态分布?或者其他什么?
从数据中经验性地估计。这就是说,基本上只是对各种句子长度创建一个直方图。它会像这样。
Num Words 1 2 3 … 8 … 30
Number of Sentences 2 4 10 … 500 … 0
这表明我们语料库中有 10 个三词句和 500 个八词句。
直方图可能需要一些平滑处理,包括插值和外推。
为了说明插值的必要性,请考虑以下场景。在我们的语料库中,假设有一个 50 词的句子出现,但没有 45 词的句子。(不用在意 40 词的句子怎么来的。)我们不想说 45 词的句子出现的概率为零。虽然很小,但不是零。
好的,我们回到直方图的讨论。
一个经过适当平滑的直方图可以用来为新句子打分,以评估其长度的异常程度。一个异常长的句子应该得分很低。
平滑的直方图包含了这种评分所需的所有信息。我们可以称之为“P值或百分位评分”。例如,如果我们语料库中的 99%的句子不超过二十个词,那么一个 21 词的句子的P值不超过 0.01,即 1%。这可以表示为百分位单位的评分。1将意味着非常低的评分。
为了保持以上段落的描述足够简单,我们忽略了如右尾P值与双尾P值的细节。在实际操作中,这并不太重要,除非评分阈值基于P值的截止点。实际操作中,通常并不是这样。相反,它们是基于我们认为截止点应该在何处来校准的。也就是说,我们询问人们认为哪些句子太长,并从这些反馈中得出评分截止点。
涉及特定令牌分布的方法
考虑一下我们在文章中描述的涉及连字符的问题。即是否应该在某些相邻的词之间加连字符?如果不加连字符,是否应该有空格,还是将词融合在一起?
让我们从重复之前看到的例子开始,以更好地理解问题的本质。
He’s an all star runner ⇒
He’s an all-star runner
Chocolate covered truffles ⇒
Chocolate-covered truffles
My co-worker has a four-year old child. ⇒
My co-worker has a four-year-old child
We sat on the plane for deicing for an hour. ⇒
We sat on the plane for de-icing for an hour.
This car is a finely-tuned machine. ⇒
This car is a finely tuned machine.
She attends Ohio-State University. ⇒
She attends Ohio State University.
Heart-broken ⇒ Heartbroken.
作为第一次尝试,即使在没有建模上下文的情况下,我们也能够在检测可疑连字符方面做到相当不错,并提供合理的替代方案。
显然,我们不需要完美的精度或完美的召回率。只需足够让用户找到价值。我们可以始终进行迭代和改进。
我们想指出,我们应该更加关注检测的精度,而不是提出的替代方案的质量。这是因为对于相邻的词,只有三种可能性——加连字符、使用空格或将词粘合在一起。所以如果检测到的连字符确实很差,即使展示其他两种方案也能为用户提供价值。
因此,我们将按以下方式建模该问题。首先,让我们考虑一个发现有连字符的词,例如heart-broken。当我们在训练过程中遇到这个词时,我们将进行如下操作。
我们将创建一个去掉连字符的新词。在我们的例子中,它将是heartbroken。每次在文本中遇到heart-broken时,我们会将heartbroken → heart-broken的实例添加到一个映射中。在 Python 伪代码中,它会像这样:
style_map[‘heartbroken’][‘heart-broken’] += 1
当我们在语料库中遇到heartbroken(而不是heart-broken)时,我们会进行如下操作:
style_map[‘heartbroken’][‘heartbroken’] += 1
所以一旦训练完成,style_map[‘heartbroken’]将会有两个版本heart-broken和heartbroken的分布。
因此,如果 P(heartbroken|heartbroken) 远高于 P(heart-broken|heartbroken),我们会倾向于将heart-broken标记为可疑,并建议使用heartbroken作为改进建议。
如果在评分过程中,单词表示为heart-broken,那么我们会首先去掉连字符,就像我们在训练过程中所做的一样。如果结果键与最可能的重写不匹配,我们会将该实例标记为可疑。在我们的例子中,这将发生,因为heartbroken的最可能重写是heartbroken本身。但是文本中却是heart-broken。
现在让我们考虑一下如何在训练语料库中建模相邻的词。不加连字符。例如boat house。它的表达更好是boat house、boathouse还是boat-house?
为了覆盖这种情况,我们只需要一些额外的逻辑,如下所示。
以两个相邻的词boat house为例,我们将衍生出一个新词boathouse,将这两个词融合在一起,并将boat house作为键添加到boathouse中。如下所示。
style_map[‘boathouse’][‘boat house’] += 1
我们会期望在语料库中boathouse的出现频率远高于boat house。也就是说,P(boathouse|boathouse)将远大于P(boat house|boathouse)。
连字符建模中的概化
所谓概化,是指学习涉及连字符的规则,这些规则超出了我们在训练数据中遇到的具体实例。
继续阅读,以查看在这种情况下的具体概化类型。
涉及特定前缀的概化
从训练语料库中,我们可能会观察到单词very后面从未跟随连字符。因此
A very-happy dog
是不正确的。应该是very happy。
这个例子,或者更一般地说,这个规则,来自[2]。
我们能否从数据本身学习到这个规则?
是的。这里是方法。
我们将使用第二个映射,称为hyphenation_prefix_map。这个映射的键将是前缀。每次我们在训练语料库中看到这个前缀后面紧跟连字符时,我们将把值“1”的计数加一。每次我们在训练语料库中看到这个前缀后面没有连字符时,我们将把值“0”的计数加一。
上述段落中的逻辑伪代码如下:
if the current word w is immediately followed by a hyphen:
hyphenation_prefix_map[w][1] += 1
else:
hyphenation_prefix_map[w][0] += 1
现在让我们展示当我们在评分文本中看到very-happy时的处理方法。假设我们从语料库中学到
P(下一个字符是-|当前单词是very)
是零,或者几乎为零。我们会将very-happy标记为可疑,并建议去掉连字符。
还要注意,这个解决方案还涵盖了在[2]中进一步描述的规则。
Hyphenate all words beginning with the prefixes self-, ex-, and all-
以下是示例,也来自[2]。
She is now self-employed.
My ex-classmate took my notes.
We are going to an all-inclusive resort.
实际上,我们的数据驱动学习将比 100%遵循这个规则做得更好。让我们详细说明一下。
有些以self开头的单词没有连字符。例如,selfish。
所以我们的模型将学习到通用规则
if word starts with self
self is followed by a hyphen
以及像selfish这样的例外,只要它们出现在训练语料库中。
需要稍微调整一下,以确保规则“selfish stays selfish”能够触发,而不是通用规则。这很简单,但我们将把它留给读者作为练习。
涉及数字的概化
请考虑来自[2]的这些示例。
My co-worker has a four-year-old child.
Their child is four years old.
第一个连字符用法是正确的。four-year old的连字符用法则不正确。第二句话中缺少连字符的用法也是正确的。
假设术语four-year-old在训练语料库中出现频繁。扩展我们之前描述的方法以处理三个相邻的词素,我们可以检测到four-year old实际上应该是four-year-old。
那么,如果我们要评分的文本包含术语hundred year old呢?假设这个确切的术语在训练语料库中没有出现。我们希望能够将其标记为可疑,并建议作者考虑将其重新表述为hundred-year-old。
这是另一个概化的例子。
显然,我们希望能够从实际出现的实例中学习模式 <number>-year-old。
如果我们在语料库中看到实例 1-year-old、2-year-old、one-year-old、two-year-old 等,从这些实例中我们可以合理推测模式为 <number>-year-old。
我们将高层次地概述一种解决此问题的方法。我们将在下面进行说明。
考虑语料库中的实例 2-year-old。如前所述,我们将更新 stylemap 对于键 2yearold。此外,我们还将进行以下操作。我们将使用命名实体识别器将 2 识别为数字,可能是一个结合了基于词典和正则表达式的方法的简单工具。
请参见 [8],获取关于 NLP 中命名实体识别的详细帖子,涵盖了这一层次的场景以及更复杂的场景。
我们可以表达结果为键 <num>-year-old。我们像以前一样更新这个键。也就是说,
style_map[<num>-year-old][<num>-year-old] += 1
现在让我们看看当我们在文本中看到 100 year-old 时需要做什么。首先,我们去掉连字符,查找 100yearold 在 style_map 中。假设它不存在。然后我们将 100 识别为 <num> 并尝试在 style_map 中查找 <num>yearold。它作为键存在。接下来,我们找到具有最高概率且差距足够大的样式。在我们的例子中,它将是 <num>-year-old。接着,我们用 100 替换 <num>。最后,我们提供结果 100-year-old 作为建议的重表达。
接下来,考虑以下示例,亦来自 [2]。
There are fifty-seven kids in that grade.
用 fifty seven 替换 fifty-seven 将是不正确的。
我们可以从数据中自动学习这一模式。为此,我们希望在命名实体中引入进一步的区别:拼写出的数字 与 书写出的数字。Five 是 拼写出的数字。5 是 书写出的数字。凭借这一区别,我们可以学习到规则是
if two adjacent tokens are spelled out numbers with a space in between them
insert a hyphen between the two
这种方法可以进一步完善,以涵盖如下所示的示例,也来自 [2]。
They need two-thirds of the vote to win.
two thirds 将是不正确的。
单词大小写
这种方法也适用于某些单词大小写的场景。例如 fyi 更好地表示为 FYI。我们只需以不同的方式预处理标记。如下所示。
FYI ⇒ fyi ⇒ style_map[fyi][FYI] += 1
也就是说,当我们在语料库中遇到 FYI 时,首先我们将其全部小写,然后再添加一个 FYI 的实例,以便与 fyi 关联。
一旦训练完成,假设语料库足够干净且丰富,P(FYI|fyi) 应该远大于 P(fyi|fyi)。因此,如果我们在被评分的文本中遇到 fyi,我们将建议将其替换为 FYI。
其中的撇号
假设语料库中特定句子的第一个词是 Its。它实际上应该是 It’s。
我们如何建模以检测此类错误并推荐特定的修正?
相同的方法在这里也适用,只是预处理稍有不同。
考虑语料库中It’s的出现。我们将进行以下操作。
It’s ⇒ its ⇒ style_map[its][It’s] += 1
也就是说,我们去掉撇号得到Its。然后,我们再添加一个It’s与Its相关联。
请注意,在这种情况下,我们没有将It’s小写,只是去掉了撇号。这是因为我们希望保留It’s 中的i为大写。
一旦训练完成,假设语料库足够干净且丰富,P(It’s|Its)应该比P(Its|Its)大得多。因此,如果我们遇到its作为句子中的第一个词被评分,我们将首先将其转换为Its,然后建议将这个Its替换为It’s。
实际上,如果我们将Its视为句子的第一个词,我们应该建议将其替换为It’s或It is。我们可以扩展我们的模型以直接学习提供第二个建议。我们将把这个问题留给读者作为练习。
缺失的逗号
考虑我们之前看到的示例。
Regardless there is no way …
Clearly the writer meant …
在每种情况下,句子的第一个词后面应该有一个逗号。
下面,我们将深入探讨如何在这种场景中检测缺失的逗号。我们将仅关注这些场景,特别是那些在句子的第一个词或两个(或三个)词后面应该跟随逗号的情况。
在更微妙的情况下,例如在句子中间的位置,检测逗号会更复杂。我们将在未来的帖子中解决这个问题。
我们将使用之前在其他用例中使用的相同方法。不同的是,我们将仅应用于句子开头的词。
对于训练语料库中每个以句子开头的词,我们将跟踪其后是否跟随逗号的频率。在训练后,我们可以以我们之前描述的方式检测缺失的逗号。
这与关联规则的关系
我们在这篇文章中讨论的主要方法可以看作是挖掘关联规则的一个实例。
从这个角度看,我们使用的各种映射数据结构编码了特定形式的关联规则。
IF X THEN Y
我们使用的概率推断,用于检测特定规则是否应该触发,称为在挖掘关联规则中的confidence。它的形式是P(Y|X)。
在关联规则挖掘中,另一个常用的替代方法叫做lift。有关关联规则和 lift 的更多信息,请参见[9]。
总结
在这篇文章中,我们讨论了在文本写作过程中检测和纠正问题的话题。这个用例具有巨大价值,从改善正在写作的电子邮件,到帮助作者在撰写较长文章时提高写作质量。
这就是 Grammarly 如此受欢迎的原因。
在这篇文章中,我们列举了以下类型的常见错误及其示例。
-
缺少逗号。
-
缺少冠词。
-
在It’s中遗漏撇号。
-
使用单数形式而不是复数形式,或反之亦然。
-
使用连字符时不该用,或反之亦然。
-
大小写问题。应大写的字母未大写。应全大写的单词未全大写。例如,fyi
-
使用错误的介词,或在不需要时使用介词。
在每种情况下,我们都通过实际例子描述了问题。通常会出现细微的差别。
接下来,我们对这些问题进行了第二次审视,讨论了统计自然语言处理的方法,这些方法有助于检测和解决这些问题。我们讨论了训练数据、特征工程以及适用于这些用例的一些特定统计模型。
参考文献