本文将解释和演示“结构化生成式AI”的概念:即被限制在特定格式内的生成式AI。此外, 还将介绍一个与结构化语言特别相关的分词重要提示。
生成式AI的众多用途之一是作为翻译工具。这通常涉及两种人类语言之间的翻译,但也可以包括计算机语言或格式。例如,你的应用程序可能需要将自然语言翻译成SQL:
Natural language: “Get customer names and emails of customers from the US”
SQL: "SELECT name, email FROM customers WHERE country = 'USA'"
或者将文本数据转换为JSON格式:
Natural language: “I am John Doe, phone number is 555–123–4567,
my friends are Anna and Sara”
JSON: {name: "John Doe",
phone_number: "555–123–5678",
friends: {
name: [["Anna", "Sara"]]}
}
当然,对于其他结构化语言,还有更多的应用可能。这种任务的训练过程涉及将自然语言的示例与结构化格式一起输入到编码器-解码器模型中。或者,利用预训练的语言模型(LLM)也可以满足需求。
虽然实现100%的准确性是不可能的,但我们可以消除一类错误:语法错误。这些错误违反了语言的格式,比如用句号替换逗号、使用SQL架构中不存在的表名,或者省略括号闭合符,这些都会使SQL或JSON无法执行。
由于我们正在翻译成结构化语言,这意味着在每个生成步骤中合法的标记列表是有限的且预先确定的。如果我们能将这种知识融入到生成式AI过程中,就可以避免大量的错误结果。这就是结构化生成式AI的核心理念:将其限制在合法标记的列表中。
关于标记如何生成的快速提醒
无论是使用编码器-解码器还是GPT架构,标记的生成都是按顺序进行的。每个标记的选择都依赖于输入和先前生成的标记,这个过程会一直持续到生成一个<end>标记,标志着序列的完成。在每个步骤中,一个分类器会给词汇表中的所有标记分配逻辑值,代表每个标记作为下一个选择的概率。下一个标记就是基于这些逻辑值进行采样的。
限制标记生成
为了限制标记生成,我们整合了输出语言结构的知识。非法标记的逻辑值被设置为-inf,确保它们不会被选中。例如,如果“Select name,”后面只有效的是逗号或“FROM”,那么所有其他标记的逻辑值都将被设置为-inf。
如果你使用的是Hugging Face,这可以通过“逻辑值处理器”来实现。使用它,你需要实现一个具有__call__方法的类,这个方法将在逻辑值计算之后、采样之前被调用。这个方法接收所有标记的逻辑值和生成的输入ID,返回所有标记的修改后的逻辑值。
用一个简化的例子来演示代码。首先,我们初始化模型,这里我们将使用Bart,但这个方法可以适用于任何模型。
from transformers import BartForConditionalGeneration, BartTokenizerFast, PreTrainedTokenizer
from transformers.generation.logits_process import LogitsProcessorList, LogitsProcessor
import torch
name = 'facebook/bart-large'
tokenizer = BartTokenizerFast.from_pretrained(name, add_prefix_space=True)
pretrained_model = BartForConditionalGeneration.from_pretrained(name)
如果我们想要从自然语言生成SQL的翻译,我们可以运行:
to_translate = 'customers emails from the us'
words = to_translate.split()
tokenized_text = tokenizer([words], is_split_into_words=True)
out = pretrained_model.generate(
torch.tensor(tokenized_text["input_ids"]),
max_new_tokens=20,
)
print(tokenizer.convert_tokens_to_string(
tokenizer.convert_ids_to_tokens(
out[0], skip_special_tokens=True)))
返回
'More emails from the us'
由于我们没有针对文本到SQL任务对模型进行微调,因此输出并不像是SQL。在本教程中,我们不会训练模型,但我们会引导它生成SQL查询。我们将通过使用一个函数来实现这一点,该函数将每个生成的标记映射到一个允许的下一个标记的列表。为了简单起见,我们将只关注前一个标记,但更复杂的机制很容易实现。我们将使用一个字典,为每个标记定义哪些标记可以跟随其后。例如,查询必须以“SELECT”或“DELETE”开头,而在“SELECT”之后只允许“name”、“email”或“id”,因为这些是我们模式中的列。
rules = {'<s>': ['SELECT', 'DELETE'], # beginning of the generation
'SELECT': ['name', 'email', 'id'], # names of columns in our schema
'DELETE': ['name', 'email', 'id'],
'name': [',', 'FROM'],
'email': [',', 'FROM'],
'id': [',', 'FROM'],
',': ['name', 'email', 'id'],
'FROM': ['customers', 'vendors'], # names of tables in our schema
'customers': ['</s>'],
'vendors': ['</s>'], # end of the generation
}
现在我们需要将这些标记转换为模型使用的ID。这将在从LogitsProcessor继承的类中发生。
def convert_token_to_id(token):
return tokenizer(token, add_special_tokens=False)['input_ids'][0]
class SQLLogitsProcessor(LogitsProcessor):
def __init__(self, tokenizer: PreTrainedTokenizer):
self.tokenizer = tokenizer
self.rules = {convert_token_to_id(k): [convert_token_to_id(v0) for v0 in v] for k,v in rules.items()}
最后,我们将实现__call__函数,该函数在逻辑值计算之后被调用。该函数创建一个新的-inf张量,根据规则(字典)检查哪些ID是合法的,并将它们的分数放在新张量中。结果是一个张量,其中只有有效标记的有效值。
class SQLLogitsProcessor(LogitsProcessor):
def __init__(self, tokenizer: PreTrainedTokenizer):
self.tokenizer = tokenizer
self.rules = {convert_token_to_id(k): [convert_token_to_id(v0) for v0 in v] for k,v in rules.items()}
def __call__(self, input_ids: torch.LongTensor, scores: torch.LongTensor):
if not (input_ids == self.tokenizer.bos_token_id).any():
# we must allow the start token to appear before we start processing
return scores
# create a new tensor of -inf
new_scores = torch.full((1, self.tokenizer.vocab_size), float('-inf'))
# ids of legitimate tokens
legit_ids = self.rules[int(input_ids[0, -1])]
# place their values in the new tensor
new_scores[:, legit_ids] = scores[0, legit_ids]
return new_scores
就这样!我们现在可以使用逻辑值处理器进行生成:
to_translate = 'customers emails from the us'
words = to_translate.split()
tokenized_text = tokenizer([words], is_split_into_words=True, return_offsets_mapping=True)
logits_processor = LogitsProcessorList([SQLLogitsProcessor(tokenizer)])
out = pretrained_model.generate(
torch.tensor(tokenized_text["input_ids"]),
max_new_tokens=20,
logits_processor=logits_processor
)
print(tokenizer.convert_tokens_to_string(
tokenizer.convert_ids_to_tokens(
out[0], skip_special_tokens=True)))
返回
SELECT email , email , id , email FROM customers
结果有点奇怪,但请记住:我们甚至还没有训练模型!我们仅仅根据特定的规则强制生成标记。值得注意的是,限制生成并不会干扰训练;限制只在训练后的生成过程中应用。因此,当这些限制得到适当实施时,它们只会提高生成的准确性。
我们简化的实现未能涵盖所有SQL语法。真正的实现必须支持更多语法,可能需要考虑不只最后一个标记,而是几个,并且需要支持批量生成。一旦这些增强功能得到实施,我们训练的模型就可以可靠地生成可执行的SQL查询,限制在架构中的有效表和列名上。类似的方法可以在生成JSON时强制实施约束,确保键的存在和括号的闭合。
注意分词
分词通常被忽视,但在使用生成式AI进行结构化输出时,正确的分词至关重要。然而,在幕后,分词会对模型的训练产生影响。例如,您可能微调了一个模型来将文本翻译成JSON。作为微调过程的一部分,您向模型提供了文本-JSON对示例,它将这些示例进行分词。那么这种分词会是什么样的呢?
当您阅读“[[“作为两个方括号时,分词器将它们转换为一个ID,该ID将被标记分类器视为与单个括号完全不同的类别。这使得模型必须学习的整个逻辑更加复杂(例如,记住需要关闭多少个括号)。同样,在单词前添加空格可能会改变它们的分词和类别ID。例如:
再次强调,这会使模型需要学习的逻辑变得更加复杂,因为与这些ID相关的权重将需要针对略有不同的情况单独学习。
为了简化学习,通过在单词和字符前添加空格,确保每个概念和标点符号都一致地转换为相同的标记。
在微调期间输入带有空格的示例可以简化模型需要学习的模式,从而提高模型准确性。在预测期间,模型将输出带有空格的JSON,然后您可以在解析之前删除这些空格。
总结
生成式AI为翻译成格式化语言提供了一种有价值的方法。通过利用输出结构的知识,我们可以限制生成过程,消除一类错误,并确保查询的可执行性和数据结构的可解析性。
此外,这些格式可能会使用标点和关键字来表示某些含义。确保这些关键字的分词是一致的,可以大大减少模型需要学习的模式的复杂性,从而减小模型的大小和训练时间,同时提高其准确性。
结构化生成式AI可以有效地将自然语言翻译成任何结构化格式。这些翻译能够从文本中提取信息或生成查询,这是众多应用的有力工具。