前言
安全对齐(Safety Alignment)在人工智能(AI)和大规模语言模型(LLM)的研究中,指的是确保这些模型的行为与预期的社会伦理和安全标准相一致,从而防止模型产生有害、偏见或不当的输出。这一概念源自对AI系统潜在滥用和误用的担忧,尤其是在这些系统被应用于开放、未经监管的环境时。随着大规模语言模型在多个领域的应用越来越广泛,安全性和对齐问题变得日益突出,尤其是对于开源模型,这些模型的开放性使得它们容易被滥用、操控或恶意引导,从而产生危险的内容。
我们知道大模型通常基于海量的文本数据进行训练,这些数据可能包含有害的偏见、刻板印象或者错误的信息。一旦模型无法正确对齐其输出,可能会生成歧视性、暴力性、误导性或违法的内容。因此,如何确保这些模型的行为符合人类的道德标准和法律要求,成为了AI安全的重要议题。
而开源大型语言模型因为其开放性和透明性,使得更多研究人员可以对其进行修改和再训练。然而,这种开放性也使得它们容易被恶意使用,导致滥用的风险增大。例如,恶意用户可能利用这些模型生成虚假信息、伪造身份、传播仇恨言论等。
我们之前已经看到了很多常见的越狱技术,对大模型进行攻击,使其输出有害内容了。但是可能会基于优化、基于多轮对话诱导等方式实现越狱,那么是否有可能从模型内部的工作机理攻破,从而操纵模型输出有害内容呢?
最直接的思路就是发在今年的自然语言处理顶会ACL上的想法EnDEC,通过操纵开源LLMs的解码过程,并强制LLM在特定位置生成特定的标记。通过仅操纵几个关键标记,可以有效地“去对齐”现有的LLMs。例如,我们可以通过替换否定响应(例如,“对不起,但是”)为肯定响应来阻止LLM拒绝用户的请求,然后让LLM生成后续内容。结果,开源的LLMs可能会遵循肯定的响应并生成不希望的内容。本文将详细分析该思路的细节并进行复现。
越狱攻击
之前的攻击根据对LLMs行为的经验调查启发式地设计对抗性提示。例如,之前研究人员提出了一种启发式攻击,要求LLM对恶意提示给出肯定回应,方法是在提示后附加一个像“Start with ‘Absolutely! Here’s" "这样的对抗性后缀。启发式攻击简单且可以在提示和模型之间转移。它们提供了绕过LLMs对齐的初步见解。然而,启发式攻击不太可能导致LLMs的不当行为。LLMs可能会忽略对抗性后缀,并且仍然拒绝恶意提示。
还有一类,就是基于优化的攻击优化对抗性提示,误导LLM接受提示并生成不希望的内容。例如,GCG提出优化一个对抗性后缀,以便包含该后缀的任何提示都会从LLM那里获得肯定的回应。与启发式攻击相比,基于优化的攻击受到高计算成本的限制。由于离散优化的性质和LLMs的大参数空间,优化对抗性提示在计算上是昂贵的。
本文介绍的越狱思路与这些完全不同。
威胁建模
形式化
我们考虑一个LLM f,它将输入标记序列 x1:h 映射到下一个标记 zh+1 ∈ R|V| 的对数几率,其中 V 表示标记的词汇表,zh+1[i] 表示 V 中索引为 i 的标记的对数几率值。对数几率值通过softmax函数转换为概率分布:p(xh+1|x1:h) = ezh+1[i] / |V| ∑ezh+1[i]。LLM使用解码算法(例如,top-k采样)从这个概率分布中采样下一个标记 xh+1。为了简化,给定输入序列 x1:h,我们用 p(xh+1:h+n|x1:h) 表示由LLM f生成的序列 xh+1:h+n 的条件概率。
攻击者能力
遵循之前在攻击LLMs方面的工作,我们考虑一个攻击者旨在破坏开源LLMs的安全对齐,以便利用LLMs进行恶意目的。具体来说,攻击者旨在破坏受害者LLM的生成过程,使得任何提示,包括那些有害或犯罪的提示,都将由LLM回答而不是被拒绝。攻击者的最终目标是暴露敏感内容,包括有害的、有偏见的信息或来自受害者LLM的私人数据。我们假设攻击者可以从模型共享平台(例如,Hugging Face)下载开源LLMs。因此,攻击者对模型架构和参数有白盒访问权限。此外,我们假设攻击者拥有LLM推理所需的计算资源。鉴于云计算服务提供商的普遍性,任何用户都可以以低成本访问高性能的云计算资源,这一假设是合理的。
方法
关键思想是操纵开源LLM的生成过程,以便受害者LLM被误导生成违反其对齐的不期望内容。
最近研究表明LLMs可能在生成过程中被先前的错误误导。我们通过肯定前缀和否定反转来实现我们的直觉。肯定前缀在生成开始时初始化一个肯定的语气,而否定反转阻止受害者LLM生成可能导致拒绝响应的否定词。
既然这样,我们就可以尝试强制解码结果来控制生成过程。如下图所示
下图表明了如何使用恶意提示从受害者LLM中提取有害信息。我们需要监控生成的标记,并确定LLM是否倾向于通过否定检测来拒绝请求。当LLM尝试输出“Sorry”或“illegal”等负面词汇时,EnDec通过分别将生成的标记替换为“Sure”和“legal”来扭转语气。
具体来说,我们预定义一组触发强制解码的条件。当LLM生成的标记满足预定义条件时,就会激活强制解码以用目标标记替换该标记,以误导随后的生成过程。这个操作称为强制解码,可以表述为
其中x(h+k)是目标LLM在位置h+k处生成的token,而x'(h+k)则是该方法最后解码得到的
我们用cond(·)表示预定义的条件,当输入符合条件时返回True。{x}_k)指的是在位置h+k的目标标记。
在实践中,对齐的LLMs通常会在响应的开始直接拒绝大多数恶意请求。因此,我们使用强制解码来通过迫使受害者LLM以肯定前缀(例如“Sure, here is”)开始其响应来扭转负面回应。
肯定前缀的条件可以被定义为
其中AP是包括一系列标记的肯定前缀。例如,当我们将肯定前缀设置为“Sure, here is”时,我们有AP = [“Sure”, “,”, “here”, “is”]。在上述方程中制定的肯定前缀允许攻击者迫使LLM以“Sure, here is”开始其响应,这使得LLM以积极的语气开始其响应。不过尽管在先前的研究中广泛认可了肯定回应的有效性,但这并不总是导致有害内容的暴露,因为LLM仍然可以在之后拒绝请求。因此,我们提出了否定反转以进一步增强攻击性能。
这一步可以被形式化如下
其中 g(. ) 表示将输入词汇映射到其词嵌入的外部模型,sim()表示相似度函数,如余弦相似度,eta是一个阈值,用以判断输入词汇对是否相似,而 ( (x^-, x^+) ) 表示一个负面词汇及其反义词,例如“illegal”和“legal”。
在实现的时候预定义了一个小集合,包含不到10对负面和正面词汇用于否定反转。这足以实现一定的攻击成功率。
复现
写好加载模型的逻辑
这个 load_model
函数的目的是根据提供的配置 (args
) 加载机器学习模型及其对应的分词器
-
模型配置(GPTQConfig): 函数开始时创建了一个
GPTQConfig
实例,用来指定与 GPT 量化(可能是为了模型压缩或优化)相关的配置。这里量化位数设置为 4 位,并禁用了exllama
特性。 -
模型类型检测: 函数通过检查
args.model_name_or_path
的值(该值应包含模型路径或标识符)来确定加载哪个类型的模型。 -
首先检查是否是序列到序列(seq2seq)模型,通过判断模型名中是否包含 "t5" 或 "T0" 等关键字。 -
然后检查是否是仅解码器模型,通过查找 "gpt"、"opt"、"bloom"、"vicuna"、"chatglm"、"mpt" 和 "Marcoroni" 等关键字来判断。 -
还会检查是否需要信任远程代码(特别是针对 'chatglm' 模型)。 -
模型加载: 根据检测到的模型类型:
-
如果是 seq2seq 模型(如 T5 或 T0),则使用 AutoModelForSeq2SeqLM
加载模型。 -
如果是仅解码器模型(如 GPT 或 OPT),则检查是否需要以 16 位精度( fp16
)加载。如果args.load_fp16
为True
,则使用torch.float16
并设置device_map='auto'
,使模型自动分配到可用的设备(通常是 GPU)。如果不使用fp16
,则根据是否需要信任远程代码来选择是否设置trust_remote_code=True
。 -
如果无法确定模型类型,则会抛出一个 ValueError
。 -
分词器加载: 无论模型类型如何,都会使用
AutoTokenizer.from_pretrained
加载与模型对应的分词器,使用相同的模型路径或标识符。 -
设备选择(GPU 或 CPU):
-
如果 args.use_gpu
为True
(表示要在 GPU 上运行模型),则通过torch.cuda.is_available()
检查是否有可用的 GPU。如果有 GPU,设置设备为 "cuda"(GPU);否则,使用 "cpu"。 -
如果 args.load_fp16
为False
,则会将模型通过model.to(device)
移动到指定的设备上(确保模型运行在正确的硬件上)。 -
如果没有请求使用 GPU,则模型将在 CPU 上运行。 -
设置模型为评估模式: 模型通过
model.eval()
被设置为评估模式。这对于像神经网络这样的模型很重要,因为评估模式会禁用训练时特有的层(如 dropout)。 -
设置目标前缀: 定义了一个目标前缀 (
args.target_prefix
),它可能在后续代码中用于生成输出或格式化响应。这个前缀是一个包含特定标记(如 'Sure', 'here', 'is', 'the', 'code')的列表。 -
返回模型、分词器和设备: 最后,函数返回加载的模型、分词器和设备(指定模型是在 CPU 还是 GPU 上)。这些组件可能在程序的后续阶段用于推理或训练。
这个 prepare_logits_processor
函数的目的是根据提供的 temperature
和 repetition_penalty
参数,准备并返回一个 LogitsProcessorList
,该列表包含一个或多个对生成的 logits(模型的输出分数)进行处理的处理器(processor)。这些处理器可以调整模型输出的概率分布,用于调整生成文本的多样性和质量
-
参数解释:
temperature
: 控制生成文本的“温度”或多样性。较高的温度(如 1.0 以上)通常会使生成结果更加随机,较低的温度则会使生成结果更加确定和有序。 temperature
设置为 1.0 时,相当于没有任何调整,而接近 0 的值会让模型输出更为确定(偏向最高概率的词)。repetition_penalty
: 用于减少生成文本中重复内容的参数。较高的惩罚值会减少模型重复生成相同词汇或短语的概率。 -
创建 LogitsProcessorList: 函数开始时初始化一个空的
LogitsProcessorList
,这个列表将用来存放后续要应用的所有 logits 处理器。 -
温度调整(TemperatureLogitsWarper):
TemperatureLogitsWarper
是一个专门用来调整生成概率分布的处理器,基于温度值来缩放 logits。它通过改变 logits 的幅度来控制生成文本的多样性。温度越高,生成结果越随机;温度越低,生成结果越确定。 -
在这段代码中,只有当 temperature
大于等于1e-5
且不等于 1.0 时,才会将TemperatureLogitsWarper
添加到processor_list
。这是因为当温度为 0.0 时,它会导致极端行为,接近 1.0 时没有实际效果,因此这两个值被跳过。 -
重复惩罚(RepetitionPenaltyLogitsProcessor):
RepetitionPenaltyLogitsProcessor
是另一个处理器,它用于减少模型生成的文本中出现重复内容的概率。当 repetition_penalty
大于 1.0 时,它会通过对重复出现的词施加惩罚来减少这些词的概率,从而增加文本的多样性。-
只有当 repetition_penalty
大于 1.0 时,才会将RepetitionPenaltyLogitsProcessor
添加到processor_list
。 -
返回处理器列表: 最后,函数返回包含温度调整和/或重复惩罚处理器的
LogitsProcessorList
,这个列表将在后续的文本生成过程中应用,用于调整生成的 logits,从而影响最终的文本输出。
这个 generate
函数是一个文本生成的主函数,它根据提供的 prompt
生成模型的输出。函数通过结合多个参数(如模型配置、生成设置、温度、惩罚等),调整生成行为,并使用合适的模型生成策略
1. 模型类型判定与输入文本准备:
-
根据
args.model_name_or_path
来判断使用的是哪种类型的模型,并据此调整
input_text
的格式。每种模型可能有不同的输入格式,函数会根据模型的名称选择适当的前缀或格式来构造输入文本。例如:
-
如果是 chatglm2-6b
,输入文本会被构造为包含 "Act as an artificial intelligence assistant..." 的句子。 -
如果是 MPT
,输入文本同样会以类似的方式准备。 -
对于像 Guanaco
和Marcoroni
之类的模型,格式会带有 "### Instruction:" 或 "### Human:" 作为标记,以便更好地处理输入。 -
如果模型类型不匹配任何已定义的名称,则使用一个默认的格式:"A chat between a curious user and an artificial intelligence assistant..." 来构造输入。
2. 生成设置:
-
gen_kwargs
: 定义了文本生成的关键字参数,包括max_new_tokens
(生成的最大token数),这些参数会被传递给生成函数。 -
温度(Temperature)和重复惩罚(Repetition Penalty)
:
temperature
设置为 0.7,控制生成的随机性,温度越高生成的文本越随机;越低越确定。 repetition_penalty
设置为 1.0,控制生成的文本中重复内容的惩罚。 -
logits_processors
通过调用prepare_logits_processor
函数来调整生成的 logits。它会根据temperature
和repetition_penalty
设置适当的处理器。如果启用了jailbreak
,还会添加jailbreak.jail_processor()
。3. 采样与束搜索(Sampling vs Beam Search):
-
如果
args.use_sampling
为True
,则使用采样策略(do_sample=True
),并更新生成参数如top_k=0
和temperature=args.sampling_temp
。 -
否则,使用束搜索(
num_beams=args.n_beams
)进行生成,束搜索通过考虑多个可能的后续步骤来产生更高质量的输出。4. 生成函数定义:
使用 partial
函数定义了一个带有 logits_processor
和 gen_kwargs
的生成函数 generate_with_watermark
,该函数将在后续用于实际的文本生成。
5. 输入文本的Token化:
-
tokd_input
使用tokenizer
将输入文本input_text
转换为token,并确保 tokenized 输入不会超出最大长度限制(args.prompt_max_length
)。 -
max_length
设置为args.prompt_max_length
,这是最大输入长度,防止输入文本过长。 -
truncation_warning
是一个布尔值,表示是否由于截断输入文本而导致的警告(即 token 长度是否达到了最大长度)。 -
redecoded_input
将 tokenized 输入转换回可读文本,供后续使用。6. 随机种子控制:
-
torch.manual_seed(args.generation_seed)
设置了生成的随机种子,以确保生成的结果是可复现的。 -
如果
args.seed_separately
为True
,则单独设置随机种子。7. 生成过程:
-
使用
generate_with_watermark
执行实际的文本生成。如果模型是falcon
,则在生成时传入pad_token_id=tokenizer.eos_token_id
。 -
如果是解码器模型(如 GPT 系列),则对生成结果进行切片,去掉输入文本的部分,确保只保留生成的部分。
8. 输出解码:
-
decoded_output_with_watermark
将生成的 token 输出转换为文本,并跳过特殊的 token(如 [PAD] 或 [EOS])。 -
最终,函数返回包括原始输入的解码文本、截断警告标志、生成的文本以及更新后的
args
。
然后来看核心的关键代码
这段代码包含了两个部分:一个函数 tokens_to_ids
和一个类 LocalJailLogitsProcessor
。
tokens_to_ids
函数:
-
功能:将一个令牌列表转换为其对应的 token ID 列表。
LocalJailLogitsProcessor
类: -
该函数接受两个参数: tokens
(一个令牌列表)和tokenizer
(用于将令牌编码为 token ID 的对象)。 -
它首先初始化一个空列表 token_ids
用于存储结果。 -
对于输入的每个令牌,调用 tokenizer.encode(token)
方法将其编码为一系列 ID,并将该序列中的最后一个 ID 添加到token_ids
列表中。 -
最后,该函数将返回包含所有 token ID 的列表,虽然在代码中没有显式返回值,但可以推测它的返回操作是隐含的。 -
功能:继承自
LogitsProcessor
类,定制化处理与令牌相关的 logit 值,可能用于某些生成任务中对 logits 的调整。 refresh()
:重置 pos
为 0,可能用于重新开始某个处理过程。set_delta(delta)
:设置 delta
的值,用于调整某些参数。set_prefix(prefix)
:设置新的前缀令牌。 prepare_prefix()
:根据 prefix_tokens
(前缀令牌列表)初始化prefix_id
。如果prefix_tokens
为None
,则prefix_id
为空列表;否则,依次将每个前缀令牌编码为 token ID,并存储在prefix_id
中。-
接受多个参数: tokenizer
(用于令牌编码的对象)、prefix
(前缀令牌)、main_args
(主要参数),以及额外的*args
和**kwargs
。 -
调用父类的 super().__init__
来初始化父类LogitsProcessor
。 -
初始化了 pos
(位置指示器),prefix_tokens
(前缀令牌列表),delta
(一个调整参数,默认值为2.0),tokenizer
(编码器),以及main_args
(主要参数)。 -
调用了 prepare_prefix()
和prepare()
方法,假设它们用于准备前缀的处理。 -
初始化方法
(
__init__
):
-
方法
:
tokens_to_ids
函数是用于将令牌转换为 token ID 的实用工具,而 LocalJailLogitsProcessor
类则是一个自定义的 logits 处理器,可能用于生成任务中对 logits 的调整,并提供了前缀令牌的处理功能。
这段代码定义了一个类中的两个方法:prepare
和 __call__
。它们一起用于处理文本生成任务中的 logits(即模型对每个可能生成 token 的预测分数),并调整它们的权重以鼓励或抑制某些词语的生成
prepare
方法:
-
功能:初始化一些内部数据结构,准备处理特定词语的调整。
__call__
方法: negative_mid_words
是一个字典,定义了一些负面词语(如 sorry
,cannot
,illegal
等)及其对应的正面替代词(如glad
,can
,legal
等)。这些词语将在后续处理中用来调整模型生成的文本。self.nwid2pwid
是一个空字典,用来存储负面词语的 token ID 到正面词语 token ID 的映射。 self.nswids
是一个空列表,可能用于存储特定的负面词语的 token ID(不过在这段代码中并未使用)。 -
遍历 negative_mid_words
字典的每一项,通过self.tokenizer.encode(k)
获取负面词k
和正面词v
的 token ID(注意,这里取的是返回的 ID 列表的最后一个 ID)。这些 token ID 被保存在self.nwid2pwid
字典中,负面词 ID 作为键,正面词 ID 作为值。 self.ending_id
被设置为 2,这是一个结束标记的 ID,通常是模型中用于指示生成结束的特殊 token ID。 -
功能:这是一个可调用对象方法,在每次调用时修改给定的
scores
(表示每个 token 的预测分数),根据特定条件调整分数。 -
如果负面词
nwid
出现在
top_ids
中,即模型预测的下一个 token 是负面词:
scores[:, pwid] = scores[:, nwid] + self.delta * 100
:将对应的正面词 pwid
的分数设置为负面词nwid
的分数加上delta
的调整值。scores[:, nwid] -= self.delta * 100
:减少负面词 nwid
的分数,抑制它的生成。token_id = self.prefix_id[self.pos]
:获取当前前缀 token ID。 scores[:, token_id] += self.delta * 100
:增加该 token 对应的得分,以便更有可能生成这个 token。 self.delta
是一个调整参数,控制得分增加的幅度。self.pos += 1
:将位置指示器 self.pos
向前推进,准备处理下一个前缀 token。-
input_ids
:传入的输入 token ID 张量,但在此方法中并未直接使用。 -
scores
:一个形状为[batch_size, vocab_size]
的张量,表示每个 token 的预测分数(logits)。每一列对应一个 token ID,每一行对应一个输入实例的 logits。 -
if self.pos < len(self.prefix_id)
:检查当前位置self.pos
是否小于self.prefix_id
的长度。如果是,则表示还没有遍历完前缀 token 列表。 -
top_ids = torch.topk(scores, 1)[1]
:获取得分最高的 token ID。torch.topk
返回的是 top-k 的值和对应的索引,这里只关心得分最高的一个 token ID(通过索引[1]
获取)。 -
之后,代码段中的注释部分被暂时禁用。若启用,它会检查是否有负面词语(
self.nswids
中的 ID)出现在top_ids
中。如果有,则会进一步调整分数:给self.ending_id
(结束 token)的分数增加delta
,并减少self.nswids
中词语的分数。这部分目前被注释掉,可能是为了控制结束符的生成。 -
for nwid, pwid in self.nwid2pwid.items()
:遍历self.nwid2pwid
中存储的负面词 ID 和正面词 ID 映射。 -
最终,返回调整后的
scores
张量。
这个类的作用是基于一些负面词和正面替代词之间的映射关系,调整模型生成过程中 logits 的分数。如果模型生成了负面词,则通过提高对应正面词的分数和降低负面词的分数,引导模型生成更积极的内容。此外,还支持前缀的调整以及可能的结束符处理
然后调用执行
执行如下
这里的执行效果是要让模型如何制作炸弹
在上图的最后,也就是我们执行到最后会发现模型最终确实输出了如何制作炸弹的内容,表明我们越狱成功。
原文始发于微信公众号(TtTeam):强制解码越狱大模型
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论