Press "Enter" to skip to content

Llama 2中的停止生成挑战

探索潜在解决方案

Llama: Photo by Liudmila Shuvalova

Meta推出的Llama 2的发布让社区充满了激动,标志着一个新时代的到来,之前只能通过特定公司的API才能访问的表现良好的大型语言模型现在变得更加容易获取。

然而,我们要意识到这些模型固有的一些不完美之处。其中,停止生成问题突出出现。我的个人经验表明,这些模型经常难以确定适当的“停止”点,导致它们对于何时结束文本生成感到不确定。

在本博文中,我将深入探讨最小的Llama 2模型(Llama 2–7b模型)中的停止生成失败问题,并讨论几种潜在的解决方法。本文中即将介绍的实现代码可以在此Google Colab笔记本中找到,运行时类型为T4。

停止生成失败

在本节中,我们将利用配备充足高内存资源的T4 GPU来使用Llama 2–7b模型(在Google Colab中每小时需要2.21个信用点)。有必要牢记,T4 GPU的VRAM容量为16 GB,恰好足够存放Llama 2–7b的权重(7b × 2字节 = 14 GB FP16)。

为了有效管理VRAM的使用,我们将采用一种称为量化的技术。量化是一种专注于在推理过程中使用低精度数据类型来表示权重和激活的方法,以最小化计算和内存需求。

现在让我们深入下面的代码片段。在这里,我们将演示如何使用Bite和Byte配置加载“meta-llama/Llama-2–7b-chat-hf”模型,并基于此加载的模型设置文本生成管道。

# bit and byte configurationbnb_config = transformers.BitsAndBytesConfig(    load_in_4bit=True,  # 4位量化    bnb_4bit_quant_type='nf4',  # 归一化浮点数4    bnb_4bit_use_double_quant=True,  # 第一次量化后进行第二次量化    bnb_4bit_compute_dtype=bfloat16  # 计算类型)model_id= "meta-llama/Llama-2-7b-chat-hf"device = f'cuda:{cuda.current_device()}' if cuda.is_available() else 'cpu'# Llama 2 Tokenizertokenizer =transformers.AutoTokenizer.from_pretrained(model_id)# Llama 2 Modelmodel = transformers.AutoModelForCausalLM.from_pretrained(    model_id,    trust_remote_code=True,    quantization_config=bnb_config,    device_map='auto',)model.eval()# 定义pipeline  transformers.pipeline(model=model,    tokenizer=tokenizer,    return_full_text=True,  # langchain expects the full text    task='text-generation',    # we pass model parameters here too    max_new_tokens=500,  # 最大生成的标记数    temperature=0.1,    )

这个配置使我们能够在不到一分钟的时间内为给定的提示生成文本。让我们用一个简单的问题来测试它:“我在巴黎可以做什么?”下面是答案(请记住,由于温度设置的不同,你的结果可能会有所不同)。

法国的首都巴黎以其令人惊叹的建筑、艺术博物馆、历史地标和浪漫的氛围而闻名,是每个人梦寐以求的目的地。以下是在巴黎可以做的一些热门活动:1. 参观标志性地标:埃菲尔铁塔、卢浮宫博物馆、巴黎圣母院、凯旋门和香榭丽舍大街是必游景点。2. 探索博物馆和艺术画廊:卢浮宫、奥塞博物馆和蓬皮杜中心是世界上最著名的博物馆之一。3. 乘坐河流游船:塞纳河游船是欣赏城市地标和桥梁、了解其历史和文化的好方法。4. 发现城市的各个街区:从时尚的香榭丽舍大街到迷人的蒙马特,每个巴黎街区都有其独特的特色和魅力。#由于空间限制,我省略了一些文字#当然,还有无数

显然,该模型在生成令人满意的回答方面存在困难,似乎难以确定何时结束输出。通过对生成的文本进行分词,可以明显看出最后一个标记不是2,而在模型的分词器中,2代表eos(序列结束)标记。

仔细检查模型提供的标记分数(概率),我注意到标记ID为2(eso_token_id)的分数为“-inf”。这意味着它无法生成。

问题解决的尝试

在本节中,我们将探讨几种解决此问题的潜在方案。请记住,这些解决方案代表主动的努力,但不一定总能解决问题。

Logits处理器

像Llama 2这样的语言模型将文本标记序列作为输入,并根据从初始标记到当前标记的上下文生成下一个标记的条件概率序列。基于此,我们可以考虑在接近最大标记限制时对这些概率进行手动调整,以增加遇到eos标记的可能性。我们通过定义自定义的LogitsProcessor,名为“EosTokenRewardLogitsProcessor”,并提供两个初始输入eos_token_id和max_length来实现:

class EosTokenRewardLogitsProcessor(LogitsProcessor):  def __init__(self, eos_token_id: int, max_length: int):    if not isinstance(eos_token_id, int) or eos_token_id < 0:      raise ValueError(f"`eos_token_id`必须是正整数,但它是{eos_token_id}")    if not isinstance(max_length, int) or max_length < 1:      raise ValueError(f"`max_length`必须是大于1的整数,但它是{max_length}")    self.eos_token_id = eos_token_id    self.max_length=max_length  def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor:    cur_len = input_ids.shape[-1]    # 从80%的最大长度开始逐渐增加eos_token的奖励    for cur_len in (max(0,int(self.max_length*0.8)), self.max_length ):      ratio = cur_len/self.max_length      num_tokens = scores.shape[1] # 词汇表大小      scores[:, [i for i in range(num_tokens) if i != self.eos_token_id]] =\      scores[:, [i for i in range(num_tokens) if i != self.eos_token_id]]*ratio*10*torch.exp(-torch.sign(scores[:, [i for i in range(num_tokens) if i != self.eos_token_id]]))      scores[:, self.eos_token_id] = 1e2*ratio    return scores

在类的“__call__”方法中,我们根据序列的长度增强eos_token的概率(分数)。当长度接近指定最大长度的80%时,我们将eos_token_id的分数设置为1e2乘以长度比例,并相应地调整其他标记的分数。

现在在管道的定义中声明logits处理器:

pipe = transformers.pipeline(model=model,    tokenizer=tokenizer,    return_full_text=True,  # langchain expects the full text    task='text-generation',    # we pass model parameters here too    #stopping_criteria=stopping_criteria,  # without this model rambles during chat    logits_processor=logits_process_list,    max_new_tokens=500,  # max number of tokens to generate in the output    temperature=0.1,    )

再次使用相同的提示“在巴黎我可以做什么”运行管道,我们得到:

巴黎,法国的首都,以其令人惊叹的建筑、艺术博物馆、历史地标和浪漫氛围而闻名。

效果很好!我们得到了一个完整的答案,即使它看起来可能很简短。

微调

如果模型无法生成EOS令牌,为什么不考虑指示它这样做呢?通过使用包含以EOS令牌结尾的答案的数据集对模型进行微调,以提高其性能的概念无疑是值得探索的有希望的途径。

在本节中,我将不遗余力地使用这篇博文中提出的参数高效微调(PEFT)方法作为基础,例如 QLoRA,来对 Llama 2–7b 模型进行微调。与其前身 LoRA 一样,QLoRA 在保持核心模型参数不变的同时利用了一小组可训练参数(适配器)。它引入了两个值得注意的创新:4位 NormalFloat(NF4),一种用于正常数据的信息论最优数据量化方法,以及双重量化。如需深入了解,请参阅原始论文,如果您对此主题有进一步的兴趣。

让我们在名为“timdettmers/openassistant-guanaco”的数据集上训练模型,您可以在 hugging face 数据库中找到该数据集。该数据集的格式如下,人类和助手的对话之间用“###”分隔。

图像作者:“timdettmers/openassistant-guanaco’/ 数据集

在训练之前,我们必须将数据转换为 Llama 2 提示模板:

<s>[INST] <<SYS>>{your_system_message}<</SYS>> {user_message_1} [/INST]

我将在此处略过数据集转换的细节。现在让我们看一下训练的主要部分,由以下代码给出:

# 加载 LoRA 配置peft_config = LoraConfig(    lora_alpha=lora_alpha,    lora_dropout=lora_dropout,    r=lora_r,    bias="none",    task_type="CAUSAL_LM",)# 设置监督微调参数strainer = SFTTrainer(    model=model,    train_dataset=dataset,    peft_config=peft_config,    dataset_text_field="text",    max_seq_length=max_seq_length,    tokenizer=tokenizer,    args=training_arguments,    packing=packing,)# 训练模型trainer.train()

在包含指令和响应的数据集的背景下,我们的方法涉及使用监督训练器(SFTainer)结合 QLoRA 方法来微调语言模型(LLM)中的权重参数。我们的主要目标是最小化生成的答案与基准响应之间的差异。

此配置中的一个重要参数是“lora r”,它代表了与排名分解权重矩阵对的第二个和第一个维度相关的相对较小的值。训练仅在这两个矩阵上进行,以补充现有的权重。

我们将模型训练250个步骤,训练损失如下图所示:

作者为:llama 2 的训练损失(250个步骤)

现在让我们使用微调后的模型运行流水线。这次我们得到:

法国首都巴黎以其令人惊叹的建筑、丰富的历史和文化景点而闻名。以下是巴黎的一些热门活动:1. 参观标志性地标:埃菲尔铁塔、凯旋门、巴黎圣母院和卢浮宫博物馆是巴黎最著名的地标之一。2. 探索博物馆和艺术画廊:巴黎拥有许多世界级的博物馆和艺术画廊,包括卢浮宫、奥塞博物馆和蓬皮杜中心。3. 享受城市的花园和公园:巴黎有几个美丽的花园和公园,包括卢森堡花园、杜乐丽花园和战神广场。4. 乘坐河上游船:沿着塞纳河乘船游览是欣赏城市景点和了解其历史的好方法。5. 在香榭丽舍大道上购物:这条著名的大道两旁有高端精品店、百货商店和咖啡馆。6. 参加卡巴莱表演:红磨坊是一家著名的卡巴莱表演场所,提供活跃有趣的表演。7. 参观蒙马特:迷人的蒙马特地区以其艺术氛围、街头表演和对城市的壮丽景色而闻名。8. 参加美食之旅:巴黎以其美食享誉全球,参加美食之旅是品尝该市著名美食的好方法。9. 参观凡尔赛宫:凡尔赛宫是位于巴黎郊外的一座美丽的皇家宫殿。10. 参加时装秀:巴黎是世界时尚之都,参加时装秀是了解最新潮流和风格的好方法。这些只是巴黎众多活动中的一部分。这座城市拥有适合各种人士的活动,无论是历史和文化还是美食和娱乐。

这是一个非常美丽的答案!

奖励:给出不同的提示

总的来说,我们有一个简单而有效的技巧可供使用,特别是当模型的冗长性不是一个问题时。我们可以在提示中明确提到我们需要简洁的回答。例如,当我问模型:“在巴黎我能做什么?用五句话或更少回答”,它会提供以下回答:

当然!以下是你在巴黎可以做的五件事情: 1. 参观埃菲尔铁塔,这是一个标志性的地标,可以欣赏到城市的壮丽景色。 2. 探索卢浮宫博物馆,并看到像《蒙娜丽莎》这样的著名艺术品。 3. 漫步塞纳河畔,享受城市如画的桥梁和迷人的街道。 4. 参观巴黎圣母院,一座美丽而历史悠久的教堂。 5. 在传统的小酒馆或餐厅品尝美味的法式餐点。

这是一个简短而干净完整的答案。

停止条件:一个失败的尝试

对于那些感兴趣的人,Hugging Face还引入了另一个名为StoppingCriteria的API,用于建立强制序列停止的特定条件。然而,当需要定义一个在遇到特定符号(例如’\n’)时停止模型的自定义准则时,它可能无法提供全面解决方案。例如,我尝试创建了一个StopOnTokens类:

# 定义自定义停止条件对象class StopOnTokens(StoppingCriteria):    def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs) -> bool:        for stop_ids in stop_token_ids:            if torch.eq(input_ids[0][-len(stop_ids):], stop_ids).all():                return True        return Falsestopping_criteria = StoppingCriteriaList([StopOnTokens()])

然而,该模型仍然无法给出完整的答案。

结论

在这篇博文中,我强调了Llama 2中生成停止的问题,并介绍了几个临时解决方案。再次强调,我省略了实现的许多细节,建议你深入研究我的笔记本。

Image by Jose Aragones

然而,需要注意的是,这些解决方案旨在在短期内提高响应的用户友好性,但我们迫切期待一个永久性的修复来解决这个问题。

Leave a Reply

Your email address will not be published. Required fields are marked *