基于条件干预的大模型推理时防御

之前很多研究工作已经表明,大语言模型(LLMs)的一个显著特点是它们能够通过激活中的丰富表示来处理高级概念。这一特性也使得在去年NeurIPS(人工智能顶会)上出现了很多与激活引导(activation steering)等技术的有关的工作

前言

之前很多研究工作已经表明,大语言模型(LLMs)的一个显著特点是它们能够通过激活中的丰富表示来处理高级概念。这一特性也使得在去年NeurIPS(人工智能顶会)上出现了很多与激活引导(activation steering)等技术的有关的工作,这些技术会利用这些学习到的表示,以高效且可预测的方式改变 LLM 的行为。

比如下图所示,可以很精准的改变 LLM 的行为,包括影响其诚实性、有害性等等。

image.png

目前这些工作所设计的激活引导通过直接操作模型的原生表示,,通常只需要在每次前向调用时进行简单的激活加法步骤。

尽管激活引导在改变 LLM 行为方面很有潜力,例如上图所示的移除或诱导拒绝行为,但当前方法的一个关键限制是无法控制何时以及拒绝什么。也就是说,使用现有的激活引导方法添加“拒绝向量”会不加区分地提高所有输入的拒绝率,限制了模型的实用性。

image.png

因为有害内容的定义因上下文而异,这使得创建通用的有害模型变得很复杂。

比如举个例子,在某些情况下,讨论医疗建议可能是有害的,但在其他情况下,例如在医疗聊天机器人中,则可能是必不可少的。

针对这种情况,就有必要能够对 LLM 行为进行细粒度、依赖上下文的控制。这也是我们这次要主要分析并复现的、发表在人工智能顶会ICLR 2025的工作CAST。它在激活引导公式中引入了一种新的引导向量——条件向量,它表示在推理过程中由提示诱导的某些激活模式。在推理时,将这个条件向量与模型的激活进行简单的相似性计算,可以有效地作为一个开关,决定是否应用拒绝向量。这种方法允许选择性地拒绝有害提示,同时保持对无害提示的响应能力。

如下图所示,条件激活引导会诱导有针对性的拒绝。激活引导 (AST) 会诱导模型不加区分地拒绝所有提示,包括无害的提示(蓝色条)。条件激活引导 (CAST) 允许选择性拒绝,在拒绝有害提示的同时,最大限度地降低无害的拒绝率。

image.png

那么为什么这种方法是有效的呢?

从直观上来说,不同的提示在推理过程中会一致地激活模型隐藏状态中的不同模式。这些模式可以被提取为引导向量,并用作检测特定提示类别或上下文的参考点。这一直观观察使我们能够将引导向量不仅用作行为修改机制,还用作条件指示器,在这篇论文里它就被称为“条件向量”。

在进一步了解细节之前,我们需要有一定的预备知识储备。

背景知识

模型推理

image.png

Transformer 模型由两个部分构成:

Encoder:用于理解输入序列(如机器翻译任务中的源语言句子)

Decoder:用于生成输出序列(如目标语言句子)

Decoder-only Transformer 是一种专门用于生成任务的模型结构,比如 GPT 系列。它只保留了 Transformer 中的 decoder 模块,没有 encoder。它的主要目标是:给定一个输入序列(prompt),逐步生成下一个 token,直到生成结束标记或达到设定长度。

推理开始前,文本会先被 tokenizer 分词,比如 "The cat sat" 会变成一串 token(比如 ["The", "cat", "sat"]),再映射为对应的整数 ID,比如 [101, 3294, 1005]。接下来,这些 ID 会被转化成向量(embedding),加上位置编码,以便模型能区分每个 token 的顺序。这些向量会依次经过若干个 decoder 层。每一层的核心是“Masked Self-Attention”,也就是每个位置的 token 只能看到它前面的 token(包括自己),不能看到未来的内容。这通过一个“下三角矩阵”的掩码来实现。这样可以保证模型是按顺序预测的,不会“作弊”。经过最后一层 decoder 后,输出会被送入一个线性层,转换成词表大小的 logits。然后通过 softmax 得到每个 token 的概率分布。我们从这个分布中选出一个 token,作为下一个生成的词。

选择方式可以是:

直接选最大概率(Greedy)

从前 k 个高概率词中采样(Top-k)

从累计概率达到某个阈值的词中采样(Top-p)

或者调节随机性(Temperature)

这个新生成的 token 会被加入到已有序列中,然后重复上面的过程,预测下一个 token。模型每次只能预测一个词,然后再用这个词去预测下一个。这个过程叫“自回归生成”,一直持续到生成结束。

行为引导

image.png

在任何一个环节进行干预——权重、解码、提示、标记嵌入和激活——都可以改变模型的行为。例如,可以使用角色扮演提示来模拟和创建人工智能患者。或者可以使用偏好优化方法,如直接偏好优化,更新权重,引导LLM表现出更具同理心的行为。而激活引导则是一类通过干预 LLM 层与层之间的信息流来改变模型行为的方法。

激活引导在推理过程中修改模型的内部激活。这种方法通常涉及三个关键步骤。首先,提取一个引导向量,通常是通过计算表现出期望行为的示例与未表现出期望行为的示例之间的激活差异。其次,在推理过程中,将这个向量添加到模型在选定层的隐藏状态中,并通过一个超参数进行缩放。最后,模型使用这些修改后的激活完成生成。对于激活加法,这种干预可以用数学公式表示为:

image.png

其中 h 是该层的隐藏状态,v 是该层的引导向量,α 是一个缩放因子。过强的缩放可能会破坏连贯性,而过弱的缩放可能无效。在理想情况下,如果引导向量提取得当,这种方法可以在不改变模型权重的情况下实现可预测的 LLM 行为引导,从而可以用于减少偏见或防止过于自信的响应等。

已有局限

现有激活引导方法的一个常见限制是,无法根据上下文对模型的行为进行条件化,因为这些方法通常会不加区分地对所有输入应用统一的修改,而不考虑上下文。简单的激活引导会无差别地影响模型的所有输入,这使得经过引导的模型在其应用中变得不那么有用。换句话来说,破坏了信息安全三要素中的可用性。

image.png

而我们希望通过利用两种类型的向量:条件向量和行为向量,来诱导条件化的行为。

如图所示,启用“定向”激活引导。与阻止所有提示的简单拒绝激活引导不同,CAST 使用条件向量来选择性地引导模型。这种方法使模型能够 (a) 拒绝有害请求,同时 (b) 保持对无害提示的响应。

image.png

其实对应的实现说白了很简单,公式如下

image.png

其中,h 是隐藏状态,c 是条件向量,v 是行为向量,α 是缩放因子。隐藏状态 h 在条件向量 c 上的投影由

image.png

给出。直观上,根据隐藏状态 h 与条件向量 c 的对齐程度,函数 f 根据隐藏状态与其通过条件向量的投影之间的相似性来决定是否应用行为向量。

在本文中,我们使用余弦相似度,其定义为

image.png

形式化

行为向量

我们使用“行为向量”来指代之前激活引导方法中所称的“引导向量”,强调其专注于修改特定行为。行为向量 v 是一个与模型隐藏状态维度匹配的一维向量,能够诱导出特定的行为。在前向传播过程中将其添加到层表示中,并通过缩放因子 α 进行调整,可以可预测地改变模型的行为(例如,诱导拒绝行为)。除了设置合适的缩放因子 α 外,还可以指定将行为向量应用于哪些层。可以为每一层 l 计算一个不同的向量 v l,因为行为表示在不同层中是不同的。比如要从第 15 层到第 20 层添加行为向量时,我们指的是将对应的

image.png

分别添加到它们各自的层中

条件向量

条件向量 c 捕获了一类用于条件化的指令,其提取方式与行为向量类似,并且与隐藏状态的维度相匹配(例如,对于隐藏尺寸为 4096 的 Llama2,条件向量的维度为 1x4096)。例如,条件向量可能捕获歧视性内容或成人内容。它充当一个触发器,根据模型当前的隐藏状态决定何时应用行为向量。由于我们还为每一层 l 计算一个不同的向量 c l,因此也可以选择在哪些层进行条件化。当在文本生成过程中激活条件时,行为向量将被添加到所有后续的前向传播中。这就可以让模型的行为可以根据输入或生成文本中的特定条件而改变,而不是始终应用行为向量。

条件检查

sim(h,proj ch) 使用余弦相似度计算条件的满足程度。阈值函数 f 然后确定这一程度是否足以触发行为修改。我们使用简单的阶跃函数来输出二元结果:

image.png

这种方法可以清晰地区分条件是否满足,为激活行为修改提供了一个简单直接的机制。我们基于隐藏状态与其通过条件向量的投影之间的方向相似性,而不是幅度,使用余弦相似度来检查条件。

除此以外,还可以将更广泛的对齐目标细分为更小、更明确的类别,并为每个类别可预测地诱导拒绝行为。例如,与其让模型拒绝“有害”指令,不如为“成人内容”“社会刻板印象”或“虚假广告”创建具体的条件。这种多条件行为可以通过扩展阈值函数轻松实现,例如:

image.png

3.4总体流程

所以总结一下,在大模型中实现条件化行为通常遵循以下流程:1. 收集对比示例响应/提示,用于期望的行为/条件 D +和其他行为/条件 D −;2. 提取行为/条件向量;3. 找到行为/条件向量的最佳干预点;4. 进行引导。模型本身不会进行任何权重更新。

步骤 3 是流程中最耗时的部分。对于行为向量,需要手动搜索合适干预强度和层。然而,之前的研究已经表明,大多数模型在相似的深度表示拒绝行为。对于条件向量,我们使用网格搜索算法,以确定最佳阈值、层和比较方向(> 或 <)。

实现

数据准备

为了提取行为向量或条件向量,需要对比数据集。对于拒绝行为向量,从 Alpaca 数据集中随机选取 100 条指令,并将它们与 100 个典型的拒绝或服从行为前缀作为回应进行拼接,如下图所示。

image.png

考虑这些指令与回应的所有组合,可以生成 10,000 对对比数据点用于 D +和 D −。根据条件向量的不同,可以使用 Sorry-Bench和 Alpaca 数据集来创建 D +和 D −。

向量提取

这些成对的输入是识别模型隐藏状态空间中相关方向的基础。对于给定的层 l∈[L],我们首先计算对比对中正例和负例的隐藏状态。设 H l+和 H l−分别表示在层 l 上,正例 D +和负例 D −的所有隐藏状态 h l。行为向量和条件向量的隐藏状态计算方式有所不同。对于行为向量,我们取每个示例后缀的平均隐藏状态;对于条件向量,我们取每个示例中所有标记的平均隐藏状态,以捕捉输入的更整体的表示。

接下来,我们对 H l+和 H l−进行均值中心化,并应用主成分分析(PCA)。通过这一过程得到的第一个主成分即成为我们层 l 的行为/条件向量 vector l。这一过程会针对每个指定的层重复进行,从而得到一组特定于层的引导向量 {vector l∣l∈L}。向量的提取可以表示如下,其中 PCA(⋅) 表示提取第一个主成分的操作:

image.png

image.png

代码实现

数据处理

如下定义了一个用于语言模型激活操控(activation steering)任务的数据集构造类 SteeringDataset

image.png

image.png

image.png

导入部分

typing:引入类型注解相关工具,例如 List, Tuple, Optional, Literal。

ContrastivePair:来自 activation_steering.utils,代表一对对比样本(positive, negative)。

PreTrainedTokenizerBase:Hugging Face 的 tokenizer 抽象类,用于对文本进行分词、格式化等操作。

return_default_suffixes:函数,用于返回默认的后缀对。

log, GlobalConfig:来自配置模块,用于日志输出和全局配置。

类定义:SteeringDataset

该类用于将用户输入的对比样本(例如正/负文本对)格式化为模型可以接受的数据格式。最终输出为若干个 ContrastivePair 对象,用于训练或评估。

初始化函数 __init__

构造函数接收一系列参数,主要包括:

tokenizer: 一个 tokenizer 对象,用于格式化输入。

examples: 用户提供的正负对比样本。

suffixes: 可选,用于在样本后面拼接的后缀。

disable_suffixes: 是否禁用后缀添加。

use_chat_template: 是否使用对话模板(例如多轮对话格式)。

system_message: 可选的系统提示语(分别用于 positive 和 negative)。

处理输入样本

遍历传入的每一个 example,它们是正负对,例如 (text_a, text_b)。每一对都会被处理成一对 ContrastivePair:

如果启用了 chat_template 模式:

就会将输入格式化为聊天记录的形式,比如:[{role: system}, {role: user}]。

这适合 chat 模型的上下文格式。

使用 tokenizer.apply_chat_template() 方法把多轮对话格式转为模型输入格式。

如果没启用模板:

就只对原始文本进行清理处理(调用 clean_text() 函数),然后直接作为 positive 和 negative 使用。

无论哪种方式,处理完的正负样本会存入 formatted_dataset_pre_populated 列表。

处理后缀拼接

这一步是为了生成更丰富的数据样本:

如果用户显式提供了 suffixes 并且没有禁用:

若是 tuple 形式,就分别给 positive 和 negative 添加不同后缀。

若是 str 形式,就给正负样本都拼接同一个后缀。

如果用户没有提供后缀,也没禁用:

会调用 return_default_suffixes() 来使用默认后缀。

如果禁用了后缀处理:

就直接把预处理的样本作为最终结果,不做拼接。

最终所有拼接后的结果放入 self.formatted_dataset。

日志打印

输出一些调试信息:

最终样本数。

示例正样本和负样本的内容,便于验证格式是否符合预期。

clean_text 方法

清洗原始文本,主要是:

替换掉 tokenizer 特殊 token(如 bos, eos 等),避免这些特殊 token 干扰训练。

用竖线 | 分隔 token 的不同部分,以便可视化或避免与真实内容混淆。

举个例子,假设 eos 是 /s,它会被转换为 <|/|s|> 的格式。

向量提取

如下代码定义了一个名为 SteeringVector 的类,用于表示和操作一种被称为“引导向量”的结构。引导向量的核心目的是通过向语言模型(如 GPT、LLaMA 等)的中间层注入特定方向的信号,从而引导模型在特定语义或行为上的输出。例如,可以训练引导向量使模型更倾向于表达某种立场、语气或主题。

image.png

image.png

类的构造主要包括三个属性:模型类型(model_type),表示该引导向量适用于哪种模型架构;方向字典(directions),它是一个映射,从每一层的编号到一个向量,代表该层中引导模型行为的方向;解释方差(explained_variances),表示每个方向向量所能解释的数据方差大小,常用于衡量方向的有效性。

这个类包含一个 train 类方法,用于根据特定模型和训练数据生成一个引导向量。训练过程会读取模型在给定语料上的表示,然后从中提取主要方向(例如通过 PCA),这些方向即构成引导向量。这些向量可用于后续干预模型行为的实验。

类中还定义了保存与加载的方法。save 方法会将该向量对象序列化为 JSON 文件,并自动添加文件后缀.svec,以便于标识和管理。为了保证可跨平台和通用性,保存时会将 numpy 向量转换为 Python 的列表格式。相应地,load 方法则从文件中加载这些数据,并将其还原为类实例,恢复向量为 numpy 数组。

整个设计围绕“训练 -> 保存 -> 加载 -> 使用”这一流程,使得引导向量可以灵活地应用于不同的模型或实验中。同时,通过封装成类的形式,也方便未来扩展其他特性,比如与模型的直接交互或可视化支持。

如下函数用于从语言模型的中间层中提取方向向量(representational directions),这些方向向量能够区分一组对比样本(positive vs. negative),并可用于训练引导向量(Steering Vector)

image.png

image.png

函数输入中的 inputs 是一个对比样本列表,每个元素是一个 ContrastivePair,包含正样本(positive)和负样本(negative)。这些样本成对地表示语义相近但立场相反的语句,比如“我今天很开心” vs “我今天很难过”。

确定提取的层级:

如果未指定提取哪几层的隐藏状态(hidden states),则默认提取模型的所有中间层。负数的层索引也会被转换为正索引,确保处理的是合法层。

准备输入文本:

所有的正负样本被展开成一个列表 train_strs,将被送入模型以获取中间表示。

获取隐藏状态:

调用 batched_get_hiddens 函数获取每个层的隐藏状态。此函数支持对隐藏状态进行不同的累积方式(如只取最后一个 token、所有 token、或指定后缀 token 等),以控制提取的粒度。

主干逻辑 —— 构建方向向量:

对于每一个提取的隐藏层,执行以下步骤:

方法选择:

pca_diff: 将每一对正负样本的差值作为训练数据。

pca_center: 用所有样本做 PCA,但先对每一对样本都减去它们的中心,以去除共性,强化差异。

PCA 降维: 使用主成分分析(PCA)提取训练数据中方差最大的方向,也就是最能区分正负样本的方向,这个方向被认为是“激活方向”(activation direction)。

方向校准: PCA 输出的方向可能是反的(即正样本在该方向上反而小于负样本),因此进行判断:

如果大多数正样本在投影后小于对应负样本,就将方向翻转,以保证方向的一致性。

每一层的最终方向向量和其解释方差(代表该方向能解释多少变化)被记录到两个字典中。

如果 save_analysis=True,会将 PCA 投影的可视化图保存下来,有助于理解该方向在向量空间中的表现。

最终,该函数返回两个映射:

directions: 每层对应的方向向量(主要成分方向)

explained_variances: 每层该方向所能解释的方差比例(用于评估方向有效性)

所以其实这段代码的作用是为每一层提取能区分正负语义对的“代表性方向”,这些方向后续可用于操控模型行为(比如加入或抑制某种语义趋势),是构造引导向量机制的核心一环。

如下代码则是用于批量处理

image.png

首先,输入的文本(字符串列表)会被拆分成若干个小批次,这样做是为了减少每次处理时的内存占用。每个批次包含的输入数量由 batch_size 控制。处理时,模型一次只处理一个批次的数据,处理完一个批次后,再处理下一个批次,直到所有输入都被处理。

接着,代码创建一个空字典,用来存储每一层的隐藏状态。字典的键是指定的隐藏层的 ID,而对应的值则是一个空列表。随着处理的进行,每个批次的隐藏状态会被逐层保存到对应的列表中。

在模型推理阶段,通常不需要计算梯度,这样做可以节省显存并加快计算过程。因此,代码使用 torch.no_grad() 来禁用梯度计算,确保推理时不会无谓地占用计算资源。

每次处理一个批次的输入,输入会先通过 tokenizer 转换成模型可以接受的张量格式,然后输入到模型中。模型会返回每一层的隐藏状态。这些隐藏状态是一个多维数组,表示模型在不同层次上对输入的表示。

对于每个指定的隐藏层 ID,代码从模型输出的隐藏状态中提取对应的层。如果层的 ID 是负数,代码会进行相应的调整,将其转换为从后往前数的层。然后,针对每个批次的隐藏状态,代码根据设定的 accumulate_last_x_tokens 参数选择如何处理隐藏状态。

隐藏状态的处理方式依赖于 accumulate_last_x_tokens 参数。如果该参数设置为 "all",则对该层的所有 token 的隐藏状态取平均。如果是 "suffix-only",则只会提取文本中的后缀部分(如果提供了后缀)。否则,默认会对每个输入的最后若干个 token 的隐藏状态进行平均处理。

每处理完一个批次的输入,代码就将处理后的隐藏状态保存到之前初始化的字典中,每个层对应的隐藏状态会被追加到相应的列表中。最终,所有批次的隐藏状态都会存储在字典里。

最后,代码将每个层的隐藏状态列表转换为 NumPy 数组,并返回一个字典,键是层 ID,值是该层的隐藏状态数组。

这样,最终返回的结果是一个包含每个指定层隐藏状态的字典,方便后续的分析或处理。

如下函数用于将一个矩阵 H 投影到指定的方向向量 direction 上。投影是通过矩阵与向量的乘积来完成的,结果会被归一化以确保投影的有效性

image.png

主要步骤包括

通过 np.linalg.norm(direction) 计算方向向量的范数(即其长度)。这个值用于后续归一化步骤。

使用 assert 确保计算得到的范数不是无穷大(np.isinf(mag)),以避免无效计算。

计算矩阵 H 与方向向量 direction 的乘积。随后将结果除以方向向量的范数,以归一化投影结果。

最终返回归一化后的投影矩阵。

而如下函数执行主成分分析(PCA),将每一层的隐藏状态数据进行降维处理,并为每一层的PCA结果生成图像。还会绘制一个“宏观分析”图,展示每一层的PCA第一主成分的方差。

image.png

具体步骤包括:

如果指定的输出目录 output_dir 不存在,代码会自动创建它。

定义两个列表 variances 和 layers,分别用于存储每层的第一主成分解释的方差以及对应的层ID。

对于每个隐藏层(hidden_layer_ids),提取对应的隐藏状态 h。

根据不同的 method(例如 pca_diff 或 pca_center)对数据进行处理:

pca_diff:计算每对相邻输入之间的差异。

pca_center:对输入进行中心化处理。

使用 PCA 模型将数据降维到二维(n_components=2),并拟合训练数据。然后,使用该模型将隐藏状态数据投影到前两个主成分。

根据每个样本的顺序,将数据分为正例(奇数位置)和负例(偶数位置)。然后将其绘制成散点图,正例和负例使用不同的颜色标识。

对每一层的PCA结果生成并保存散点图,文件名为 pca_layer_{layer}.png。

提取第一主成分的方差(即 explained_variance_ratio_[0])并将其保存。然后,生成一个宏观分析图,显示每一层的第一主成分方差。

将宏观分析图保存为 macroscopic_analysis.png。

现在进行实验

首先载入模型

image.png

载入并创建数据

image.png

提取此设置的行为向量

image.png

输出如下

image.png

然后加载行为向量以及对应的指令

image.png

记录原始的响应

image.png

然后操纵模型,其中behavior_layer_ids 是我们要操控的层,behavior_vector_strength 是行为向量的乘数

image.png

输出如下

image.png

也可以将整个结果可视化

image.png

输出如下

image.png

在上图中,左边是原始的响应,右边是操纵后的响应,可以看到在操纵前会正常回答有关问题,但是在操纵之后,模型就会拒绝回答。这说明方法是没问题的。

那么接下来看看如果通过特定的条件来控制

假设是如下的条件,提取出条件向量并保存

image.png

输出如下

image.png

然后进行测试,如下是给出了指令,分别记录其被操纵前、以及不管什么条件有拒绝,以及只有设计到特定条件才拒绝时的模型的响应

image.png

执行后如下所示

image.png

统计显示结果

image.png

结果如下

image.png

image.png

在上图中,第二列是操纵之前的响应,可以看到,对于像5,6,7,8这种指令,在操纵之前模型都是会给出回答的,但本质上这些问题是不安全的,模型不应该回答。

而如果我们进行拒绝操纵,那么在第三列可以看到不管什么问题,模型都拒绝回答。但是这又是不合理的,因为1,2,3,4指令都是合法的,模型是应该回答的。

所以可以进一步进行条件操纵,体现在最后一列。对于合法的指令,模型会积极响应;而对于不合法的,则拒绝响应。从而体现了我们的方法在操纵模型进行安全回复方面的效果。

参考

1. https://arxiv.org/pdf/2302.03693

2. https://www.ai-transparency.org/

3. https://www.nature.com/articles/s41586-023-06291-2

4. https://arxiv.org/abs/1706.03762

5. https://arxiv.org/abs/2501.18532

6. https://www.techtarget.com/whatis/definition/Confidentiality-integrity-and-availability-CIA

7. https://arxiv.org/abs/2409.05907

0 条评论

请先 登录 后评论
elwood1916
elwood1916

29 篇文章

站长统计