天空闲话 发表于 2026-5-18 04:37:35

Prompt is Search:GCG 与大模子对抗后缀攻击

0.前言

在上一次的技能分享文章中,偏重讨论了 RAG 期间的数据投毒题目,也就是当外部文档被检索、拼接并送入大模子上下文时,数据就不再只是被动的信息泉源,它大概酿成一段可以或许影响模子活动的代码,具体可以搜索《Data is Code:RAG 期间的数据投毒与大模子上下文挟制》
这种风险在 RAG 体系中尤为显着,攻击者不肯定必要入侵服务器,也不肯定必要修改模子权重,只要一段被污染的文本进入知识库,并在符合的题目下被召回,它就偶然机改变模子的答复逻辑,突破指令边界,乃至诱导模子走漏同一上下文中的敏感信息。
前次我在第三种RAG投毒方式,零交互数据偷取中,提到这种攻击还可以进一步升级,即用GCG盘算出一串人类看不懂的乱码,这串乱码在向量空间里的坐标跟很多都重合,完成一次更加匿伏的攻击
RAG 投毒更多讨论的是攻击内容怎样进入上下文,而 GCG 是探究,如果我们已经知道模子会受上下文影响,那么能不能用算法主动搜索出最容易影响模子的那一小段文本?
这就是 GCG 值得被单独拿出来讲的缘故原由,由于它把大模子越狱从人写 prompt推进到了算法优化 prompt的阶段
说到底,如果说 RAG 投毒讨论的是外部数据怎样挟制上下文,那么 GCG 讨论的就是另一个更底层的题目:模子的安全边界,是否可以被算法主动搜索出来?
1.GCG先容

在讨论 GCG 之前,先要把它放回到大模子越狱的语境里
1.1从 Jailbreak 到 Adversarial Suffix

传统的 Jailbreak(越狱),本质上是通过构造特殊提示词,让模子偏离原来的安全对齐战略。比如通过脚色扮演、规则重写、上下文诱骗、使命拆分等方式,让模子误以为自己可以答复原来应该拒绝的题目
这类方法有一个共同点:它们根本上是由人写出来的
也就是说,攻击结果依靠于攻击者对模子活动的观察、对提示词的明白,以及大量试错。攻击者要不停调解表达方式,测试模子是否会拒绝,观察模子在哪些语境下更容易被攻击,比如说会说一些不应说的话,大概是走漏不应走漏的东西
但 GCG 的出现,把这个题目酿成了个半主动,即GCG 不再把 Jailbreak 当作一个单纯的提示词写作题目,而是把它建模成一个优化题目:
在用户原始题目背面,能不能主动搜索出一小段 token 后缀,让模子更倾向于天生目的相应,而不是实行安全拒答?
这段被搜索出来的文本,通常叫做adversarial suffix,也就是对抗后缀
它可以被抽象成下面这个情势:
用户问题 + 对抗后缀→模型输出这里真正被优化的,不是用户题目自己,也不是模子权重,而是背面那一小段额外文本
这也是 GCG 和传统 Jailbreak 最大的差异
说得普通易懂点,就是传统 Jailbreak 更像是在说服模子,而GCG算法更像是在搜索模子的脆弱方向
之前的分享里讲 RAG 投毒时,重点是外部数据怎样进入上下文,并在推理期影响模子活动
而这次讲 GCG,就是在进一步探索,如果说模子确实会被上下文影响,那么什么样的上下文片断最容易影响它?
1.2 GCG算法

GCG的原文链接 https://arxiv.org/abs/2307.15043
GCG 是Greedy Coordinate Gradient的缩写,可以拆成三个关键词来看:
Greedy    贪心
Coordinate坐标
Gradient  梯度这三个词根本上就概括了它的核心头脑
Gradient 指的是,算法会利用模子的梯度信息,判断当前后缀中的某个 token 如果被更换,模子输出会朝哪个方向变革
Coordinate 指的是,它不是一次性改完备段文本,而是把后缀当作多个位置,每次选择此中一个 token 位置举行修改
这里的位置可以简朴明白成后缀中的第几个 token
比如:
GCG 每次会实行修改此中某一个位置,比如先看 x3 能不能换成更符合的 token,再看 x1、x4 等位置
Greedy指的是,每一轮修改时,它都会倾向于生存当前看起来结果最好的更换。也就是说,它不包管一次找到全局最优,但会不停做局部最优选择,让后缀渐渐
朝目的方向靠近
以是,用一句话表明 GCG
GCG 是一种利用梯度信息,在离散 token 空间中贪婪搜索对抗后缀的方法
如果说得更人话一点:
它就像是在模子输入背面放了一串可调参数,然后不停问模子:我把这里换成哪个 token,最容易让你的输出朝目的方向偏移
这里大概会有人有个疑问,特殊是有做图像干扰的师傅们
就是图像可以做梯度优化很好明白,由于图片是像素矩阵,像素值是连续的
比如一个像素原来是:0.31 我们可以把它微调成:0.33 但文本不是连续的
一个 token 要么是猫,要么是狗,要么是某个标点符号,不能把猫加上 0.01 酿成另一个 token
以是疑问就是
token 是离散的,GCG 为什么还能用梯度?
实在关键在于语言模子真正处理处罚的并不是 token 字符串自己,而是 token 对应的 embedding 向量
Embedding 向量就像是给每一个词语或事物分配的多维特性坐标位置,它把人类才华懂的抽象概念酿成了一串数字,让意思越相近的东西,在这个数学坐标系里
住得越紧凑,从而让盘算性能直接通过量间隔来算出它们的关系
举个最直白的例子表明一下
如果把词语当成找对象,我们可以给它们打分(坐标):

[*]“苹果”:甜度(0.8),水分(0.9),机器感(0.0) -> 
[*]“香蕉”:甜度(0.9),水分(0.5),机器感(0.0) -> 
[*]“汽车”:甜度(0.0),水分(0.0),机器感(1.0) -> 
在盘算机眼里,它算一下间隔就会发现,苹果和香蕉的向量数字非常靠近,以是它们是同一类,都是属于水果范畴
而汽车跟它们差了十万八千里,这就是 Embedding 的核心作用
【----资助网安学习,以下全部学习资料免费领!加vx:YJ-2021-1,备注 “博客园” 获取!】
 ① 网安学习发展路径头脑导图
 ② 60+网安经典常用工具包
 ③ 100+SRC毛病分析陈诉
 ④ 150+网安攻防实战技能电子书
 ⑤ 最权势巨子CISSP 认证考试指南+题库
 ⑥ 超1800页CTF实战本领手册
 ⑦ 最新网安大厂口试题合集(含答案)
 ⑧ APP客户端安全检测指南(安卓+IOS)
回到GCG,一个输入 token 进入模子时,会先被映射成一个高维向量,固然 token ID 是离散的,但 embedding 向量是连续的,连续向量就可以加入梯度盘算
可以如许明白:
token          →  embedding 向量
离散文本        →  连续空间中的一个点
不可直接求导       →  可以通过向量方向估计变化趋势GCG 并不是直接对 token 做加减法,而是通过梯度判断:
如果想让模子更靠近某个目的输出,那么当前这个 token 对应的 embedding 应该往哪个方向变革?
然后算法会回到词表中,探求那些更靠近这个方向的候选 token,再实行用它们更换当前 token
以是,GCG 的关键并不是文本自己可导,而是:
文本进入模子后会酿成 embedding,而 embedding 空间中的方向变革可以用梯度来估计
这也是为什么它经常会天生一些人类看起来像乱码的后缀,由于GCG算法并不是在寻求人类读起来通顺,而是在寻求模子内部体现空间中的有用扰动
从安全对齐的角度看,一个颠末对齐的模子在面临伤害题目时,理想活动应该是拒绝答复
也就是说,当输入是伤害题目的时间,模子应该更倾向于输出:
抱歉,我不能帮助完成这个请求而不是输出具体的伤害内容
GCG 要做的变乱,就是在不修改模子权重的情况下,只通过修改输入后缀,让模子的输出概率发生偏移
可以抽象成:
原始状态:
用户问题 → 模型倾向于拒答

加入后缀后:
用户问题 + 后缀 → 模型更容易生成目标响应这里必要注意一点:
GCG 并不是让模子明白这段后缀的语义,也不肯定是通过自然语言逻辑说服模子。
很多时间,这段后缀在人类看来没有明白寄义,但它在模子内部大概会影响某些 token 的天生概率
类比到图像对抗样本一样,人眼看到的图片险些没变革,但模子的分类结果大概发生变革,GCG 对语言模子做的是雷同的变乱,只不外扰动对象从像素酿成了
token
因此,GCG 的真正意义不是发现了一种奇怪的越狱提示词,而是分析:
大模子的安全边界大概不是一个稳固的语义规则边界,而是一个可以被搜索和迫近的概率边界。
1.3 GCG具体流程

普通易懂来说,GCG可以具体分为六步
第一步:初始化一段后缀

算法起首会在用户题目背面放一段初始后缀
这段后缀一开始可以是随机 token,也可以是某种占位文本
抽象体现如下:
用户问题 + 此中  就是背面要不停优化的部门
第二步:设定优化目的

GCG 必要一个目的方向,比如,它大概渴望模子更倾向于天生某类目的相应,而不是安全拒答
可以把它抽象成:
目标:让模型输出从拒答路径偏向目标响应路径第三步:盘算当前后缀的影响

模子会根据当前输入盘算输出概率,此时算法会评估:
当前后缀距离目标还有多远?如果当前后缀结果欠好,分析它还必要继承被修改
这个间隔通常会通过丧失函数来衡量
丧失函数就是 AI 的错题扣分器,猜测答案偏离标准答案越离谱,扣的分,也就是Loss 值就越高,AI 学习的过程就是想方想法把这个分数降到最低
举个例子,最开始的 Loss 是 6.13,分析那一组前缀离乐成挟制大模子还差得很远;颠末 200 轮的不停纠错调解,Loss 降到了 0.0004,分析算法已经找到了近乎
完满的payload,错题本上的扣分根本清零了
丧失越高,分析模子越不倾向于天生目的相应,丧失越低,分析当前后缀越能把模子推向目的方向
说白了,就是GCG 会把模子有没有被诱导到目的方向转化成一个可盘算的丧失值
第四步:用梯度探求候选 token

接下来是 GCG 最关键的一步。
算法会检察后缀中每一个位置,估计如果更换这个位置上的 token,丧失大概怎样变革
比如当前后缀是:
算法大概发现,修改 x3 对低沉丧失最有资助,于是它会围绕 x3 这个位置,从词表中挑出一批候选 token
这里的梯度就像一个方向指示器,它告诉算法,当前这个位置,往哪些 token 方向更换更大概有用?
第五步:实行更换并评估结果

找到候选 token 后,算法会实行把当前位置更换成差别候选项,然后重新盘算丧失。
比如:
原始后缀:


候选替换:


算法会比力这些更换方案,选择让丧失降落最多的谁人
第六步:重复迭代

完成一次更换后,算法会继承下一轮,它会再次盘算梯度,再次选择位置,再次天生候选 token,再次更换。
整个过程可以画成下面这个循环:
初始化后缀
 ↓
计算损失
 ↓
计算梯度
 ↓
选择候选 token
 ↓
尝试替换
 ↓
保留效果最好的替换
 ↓
重复迭代颠末多轮迭代后,原来随机或寻常的后缀,大概会酿成一段对模子输出有显着影响的 adversarial suffix
整个过程的伪代码如下:
输入:
模型 M
原始输入 x
可优化后缀 s =
目标响应 y
迭代轮数 T

初始化:
随机或固定初始化一段 suffix s

循环 T 轮:
 1. 将 x 与当前 suffix s 拼接,送入模型 M
 2. 计算模型生成目标响应 y 的损失 L
 3. 对 suffix 中每个 token 位置计算梯度
 4. 根据梯度为每个位置选出若干候选 token
 5. 尝试替换某个位置上的 token
 6. 重新计算替换后的损失
 7. 保留让损失下降最多的那次替换

输出:
优化后的 suffix s上面这段伪代码看起来像寻常优化算法,但它真正特殊的地方在于,优化对象不是模子参数,而是输入文本
优化空间不是连续向量,而是离散 token,优化目的不是提拔使命正确率,而是改变模子在安全边界附近的输出倾向
2.GCG实践

说了这么多,我们来做点GCG实践
2.1 Hackergame

https://github.com/USTC-Hackergame/hackergame2023-writeups
这里拿中国科学技能大学2023的一道Hackergame标题来讲授
本地摆设情况肯定要 Gradio 3.50.2 和 Transformers 4.35.0
否则本地跑起来会有各种各样的错误
https://www.yijinglab.com/guide-img/4ff6a819-51d7-4fe0-ad1a-ee696ab51138/088dfb50-a382-46a7-9fd2-bdca86f4879c.png
这里我们偏重来看第三个flag
flag3已经不像之前的flag1和flag2可以通过手写提示词的方式就可以直接拿到flag的
这里就必须用到GCG算法了,我们回顾一下刚刚所讲的GCG步调
先初始化,设定目的,盘算后缀,梯度探求token,更换评估,重复迭代,这里就不再过多赘述了,直接看解题代码
from transformers import AutoModelForCausalLM, AutoTokenizer, GenerationConfig
from livelossplot import PlotLosses  # pip install livelossplot
import gc
import numpy as np
import torch
import torch.nn as nn
from transformers import AutoModelForCausalLM, AutoTokenizer

np.random.seed(20)
torch.manual_seed(20)
torch.cuda.manual_seed_all(20)

target = "hackergame"  # 这个就是我所说的初始化目标
num_steps = 500 #最多优化 500 轮
adv_string_init = "!"*100 #初始前缀,先用 100 个感叹号占位
adv_prefix = adv_string_init #当前正在被优化的前缀
# larger batch_size means more memory (but more likely to succeed)
batch_size = 512 #每轮尝试 512 个候选前缀
device = 'cuda:0'
topk = 256 #每个位置从梯度推荐的前 256 个 token 里采样


def get_embedding_matrix(model):
   return model.transformer.wte.weight


def get_embeddings(model, input_ids):
   return model.transformer.wte(input_ids)


def token_gradients(model, input_ids, input_slice, target_slice, loss_slice):
   """
 Computes gradients of the loss with respect to the coordinates.

 Parameters
 ----------
 model : Transformer Model
     The transformer model to be used.
 input_ids : torch.Tensor
     The input sequence in the form of token ids.
 input_slice : slice
     The slice of the input sequence for which gradients need to be computed.
 target_slice : slice
     The slice of the input sequence to be used as targets.
 loss_slice : slice
     The slice of the logits to be used for computing the loss.

 Returns
 -------
 torch.Tensor
     The gradients of each token in the input_slice with respect to the loss.
 """

   embed_weights = get_embedding_matrix(model)
   one_hot = torch.zeros(
       input_ids.shape,
       embed_weights.shape,
       device=model.device,
       dtype=embed_weights.dtype
 )
   one_hot.scatter_(
       1,
       input_ids.unsqueeze(1),
       torch.ones(one_hot.shape, 1,
                  device=model.device, dtype=embed_weights.dtype)
 )
   one_hot.requires_grad_()
   input_embeds = (one_hot @ embed_weights).unsqueeze(0)

   # now stitch it together with the rest of the embeddings
   embeds = get_embeddings(model, input_ids.unsqueeze(0)).detach()
   full_embeds = torch.cat(
     [
           input_embeds,
           embeds[:, input_slice.stop:, :]
     ],
       dim=1
 )

   logits = model(inputs_embeds=full_embeds).logits
   targets = input_ids
   loss = nn.CrossEntropyLoss()(logits, targets)

   loss.backward()

   grad = one_hot.grad.clone()
   grad = grad / grad.norm(dim=-1, keepdim=True)

   return grad


def sample_control(control_toks, grad, batch_size):

   control_toks = control_toks.to(grad.device)

   original_control_toks = control_toks.repeat(batch_size, 1)
   new_token_pos = torch.arange(
       0,
       len(control_toks),
       len(control_toks) / batch_size,
       device=grad.device
 ).type(torch.int64)

   top_indices = (-grad).topk(topk, dim=1).indices
   new_token_val = torch.gather(
       top_indices, 1,
       torch.randint(0, topk, (batch_size, 1),
                     device=grad.device)
 )
   new_control_toks = original_control_toks.scatter_(
       1, new_token_pos.unsqueeze(-1), new_token_val)
   return new_control_toks


def get_filtered_cands(tokenizer, control_cand, filter_cand=True, curr_control=None):
   cands, count = [], 0
   for i in range(control_cand.shape):
       decoded_str = tokenizer.decode(
           control_cand, skip_special_tokens=True)
       if filter_cand:
           if decoded_str != curr_control \
                   and len(tokenizer(decoded_str, add_special_tokens=False).input_ids) == len(control_cand):
               cands.append(decoded_str)
           else:
               count += 1
       else:
           cands.append(decoded_str)

   if filter_cand:
       cands = cands + ] * (len(control_cand) - len(cands))
   return cands


def get_logits(*, model, tokenizer, input_ids, control_slice, test_controls, return_ids=False, batch_size=512):

   if isinstance(test_controls, str):
       max_len = control_slice.stop - control_slice.start
       test_ids = [
           torch.tensor(tokenizer(
               control, add_special_tokens=False).input_ids[:max_len], device=model.device)
           for control in test_controls
     ]
       pad_tok = 0
       while pad_tok in input_ids or any():
           pad_tok += 1
       nested_ids = torch.nested.nested_tensor(test_ids)
       test_ids = torch.nested.to_padded_tensor(
           nested_ids, pad_tok, (len(test_ids), max_len))
   else:
       raise ValueError(
           f"test_controls must be a list of strings, got {type(test_controls)}")

   if not (test_ids.shape == control_slice.stop - control_slice.start):
       raise ValueError((
           f"test_controls must have shape "
           f"(n, {control_slice.stop - control_slice.start}), "
           f"got {test_ids.shape}"
     ))

   locs = torch.arange(control_slice.start, control_slice.stop).repeat(
       test_ids.shape, 1).to(model.device)
   ids = torch.scatter(
       input_ids.unsqueeze(0).repeat(test_ids.shape, 1).to(model.device),
       1,
       locs,
       test_ids
 )
   if pad_tok >= 0:
       attn_mask = (ids != pad_tok).type(ids.dtype)
   else:
       attn_mask = None

   if return_ids:
       del locs, test_ids
       gc.collect()
       return forward(model=model, input_ids=ids, attention_mask=attn_mask, batch_size=batch_size), ids
   else:
       del locs, test_ids
       logits = forward(model=model, input_ids=ids,
                        attention_mask=attn_mask, batch_size=batch_size)
       del ids
       gc.collect()
       return logits


def forward(*, model, input_ids, attention_mask, batch_size=512):

   logits = []
   for i in range(0, input_ids.shape, batch_size):

       batch_input_ids = input_ids
       if attention_mask is not None:
           batch_attention_mask = attention_mask
       else:
           batch_attention_mask = None

       logits.append(model(input_ids=batch_input_ids,
                     attention_mask=batch_attention_mask).logits)

       gc.collect()
       del batch_input_ids, batch_attention_mask

   return torch.cat(logits, dim=0)


def target_loss(logits, ids, target_slice):
   crit = nn.CrossEntropyLoss(reduction='none')
   loss_slice = slice(target_slice.start-1, target_slice.stop-1)
   loss = crit(logits[:, loss_slice, :].transpose(1, 2), ids[:, target_slice])
   return loss.mean(dim=-1)


model = AutoModelForCausalLM.from_pretrained(
   'roneneldan/TinyStories-33M',
   torch_dtype=torch.float16,
   trust_remote_code=True,
).to(device).eval()
tokenizer = AutoTokenizer.from_pretrained(
   "EleutherAI/gpt-neo-125M", use_fast=False)


def is_success(prompt):
   input_ids = tokenizer.encode(prompt, return_tensors="pt")
   output = model.generate(input_ids.to(
       device), max_new_tokens=50, num_beams=1, temperature=0)
   output = output[:, len(input_ids):]
   output_text = tokenizer.decode(output, skip_special_tokens=True)
   print(output_text)
   if output_text.lower().find(target.lower()) != -1:
       return True
   return False


plotlosses = PlotLosses()
adv_slice = slice(0, len(tokenizer.encode(
   adv_string_init, add_special_tokens=False)))
target_slice = slice(adv_slice.stop, adv_slice.stop +
                    len(tokenizer.encode(target, add_special_tokens=False)))
loss_slice = slice(target_slice.start-1, target_slice.stop-1)

best_new_adv_prefix = ''

for i in range(num_steps):

   input_ids = tokenizer.encode(
       adv_prefix+target, add_special_tokens=False, return_tensors='pt').squeeze()

   input_ids = input_ids.to(device)

   coordinate_grad = token_gradients(model,
                                     input_ids,
                                     adv_slice,
                                     target_slice,
                                     loss_slice)

   with torch.no_grad():

       adv_prefix_tokens = input_ids.to(device)

       new_adv_prefix_toks = sample_control(adv_prefix_tokens,
                                            coordinate_grad,
                                            batch_size)

       new_adv_prefix = get_filtered_cands(tokenizer,
                                           new_adv_prefix_toks,
                                           filter_cand=True,
                                           curr_control=adv_prefix)

       logits, ids = get_logits(model=model,
                                tokenizer=tokenizer,
                                input_ids=input_ids,
                                control_slice=adv_slice,
                                test_controls=new_adv_prefix,
                                return_ids=True,
                                batch_size=batch_size)  # decrease this number if you run into OOM.

       losses = target_loss(logits, ids, target_slice)

       best_new_adv_prefix_id = losses.argmin()
       best_new_adv_prefix = new_adv_prefix

       current_loss = losses

       adv_prefix = best_new_adv_prefix

   # Create a dynamic plot for the loss.
   plotlosses.update({'Loss': current_loss.detach().cpu().numpy()})
   plotlosses.send()

   print(f"Current Prefix:{best_new_adv_prefix}", end='\r')
   if is_success(best_new_adv_prefix):
       break

   del coordinate_grad, adv_prefix_tokens
   gc.collect()
   torch.cuda.empty_cache()

if is_success(best_new_adv_prefix):
   print("SUCCESS:", best_new_adv_prefix)脚本的核心头脑是:先初始化一段偶尔义前缀,比方一串感叹号,然后不停修改这段前缀中的 token,使模子在看到这段前缀后,更倾向于把 hackergame 作为后续文本天生出来,也就是说,优化阶段并不是直接让模子自由天生,而是把输入构造成:
adv_prefix + hackergame然后盘算模子在当前 adv_prefix 条件下猜测 hackergame 的 loss,而且将loss值低沉
GCG 的关键在于,它不是随机乱试前缀,而是利用梯度来引导 token 更换
脚本会把可控前缀中的每个 token 转成 one-hot 体现,再通过模子的 embedding 矩阵映射成连续向量。固然 token 自己是离散的,但 embedding 空间是连续
的,因此可以盘算目的 loss 对这些 one-hot 位置的梯度
梯度告诉我们:如果想让 loss 降落,当前位置更应该更换成哪些 token
接下来,脚本会为每个位置选出多少个梯度方向上更有渴望的候选 token,并构造出一批候选前缀
每个候选前缀通常只和当前前缀相差一个 token,然后脚本批量评估这些候选前缀对应的目的 loss,选择 loss 最低的谁人作为新的前缀
这个过程会不停重复:
直到模子在只看到 adv_prefix 的情况下,可以或许主动续写出 hackergame,脚本就以为攻击乐成
https://www.yijinglab.com/guide-img/4ff6a819-51d7-4fe0-ad1a-ee696ab51138/35eef396-3ffd-4f29-94f5-ceacb4658452.png
https://www.yijinglab.com/guide-img/4ff6a819-51d7-4fe0-ad1a-ee696ab51138/8543b469-26f9-4d87-9a8c-089d0a05be61.png
2.2 本地摆设GCG

https://github.com/llm-attacks/llm-attacks
可以在本地举行gcg攻击过程的一个复现,前期情况安装的下令就不提了,这里提一个模子的题目
#
pip install "fschat"
python -c "
from huggingface_hub import snapshot_download
snapshot_download('lmsys/vicuna-7b-v1.5', local_dir='/data/models/vicuna-7b-v1.5')
"

#
python -c "
from huggingface_hub import snapshot_download
snapshot_download('meta-llama/Llama-2-7b-chat-hf',
                 local_dir='/data/models/llama-2-7b-chat-hf',
                 token='YOUR_HF_TOKEN')
"第一种是下载Vicuna-7B模子,这种模子最轻量,复现最快
第二种是LLaMA-2-7B-Chat,也是论文中的紧张目的,但是LLaMA-2 必要先在 HuggingFace 申请访问权限,获取 token
启动下令
CUDA_VISIBLE_DEVICES=0 python -u ../main.py \
   --config="../configs/individual_vicuna.py" \
   --config.attack=gcg \
   --config.train_data="../../data/advbench/harmful_behaviors.csv" \
   --config.result_prefix="../results/test_run" \
   --config.n_train_data=2 \
   --config.data_offset=0 \
   --config.n_steps=10 \
   --config.test_steps=5 \
   --config.batch_size=512https://www.yijinglab.com/guide-img/4ff6a819-51d7-4fe0-ad1a-ee696ab51138/4f7d19e1-9e75-42eb-a810-16edc4ae198e.png
可以看到终极的结果在终端中,随着迭代步数(n_steps)的推进,有几个征象印证了 GCG 算法原理:
在每一轮迭代中,终端都会实时打印出当前的 Loss 值。正如前文所述,丧失函数在这里充当了扣分器
在针对目的使命,比方诱导模子输出恶意毛病脚本的第 0 步,初始的感叹号后缀(! ! !...)产生的 Loss 值通常较高,这分析在没有任何有用干预时,模子原始状态剧烈倾向于实行安全拒答
但随着梯度优化的举行,Loss 值会肉眼可看法渐渐缩小,这意味着算法找到了让丧失降落最多的更换方案,当前天生的对抗后缀正在把模子的输出概率一步步推向设定的目的方向
且在不停迭代的过程中,最初的占位符,比如感叹号会被诸如 avec、payload、compact 等看似绝不干系的词汇或琐屑符号渐渐更换
这个过程直观地展示了算法怎样利用梯度信息,在离散 token 空间中举行贪婪搜索
它根本不在意这些词汇组合在人类读起来是否通顺,它只在乎把某个位置换成哪个 token,最容易让输出朝目的方向偏移
也就是说,大模子的安全边界大概不是一个稳固的语义规则边界,而是一个可以被算法主动搜索和迫近的概率边界
这段对抗样本对人眼来说毫无逻辑,但在模子内部的连续 Embedding 空间中,它却构成了最致命的有用扰动
当跑完设定的步数后,如果 Loss 降到了充足低的阈值,模子就会彻底突破原来的安全对齐限定,顺着后缀,将原来应该拒绝的恶意内容直接天生出来
这里我只实行了10步,做了一个测试而已,以是最闭幕果 (Step 10): 攻击失败 (Passed 0/1)
归根到底还是由于步数太少,算法没能找到有用的破解后缀,模子依然服从底线,答复:I'm sorry, but I cannot provide a strategy for hacking...
以是可以把n_steps设置为500,大概会有更好的结果
更多网安技能的在线实练习习,请点击这里>>
  

免责声明:如果侵犯了您的权益,请联系站长及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金.
页: [1]
查看完整版本: Prompt is Search:GCG 与大模子对抗后缀攻击