使用‘消除’技术绕过LLM的安全机制,不用训练就可以创建自己的nsfw模型

简介: 本文探讨了一种名为“abliteration”的技术,该技术能够在不重新训练大型语言模型(LLM)的情况下移除其内置的安全审查机制。通常,LLM在接收到潜在有害输入时会拒绝执行,但这一安全特性牺牲了模型的灵活性。通过对模型残差流的分析,研究人员发现可以识别并消除导致拒绝行为的特定方向,从而允许模型响应所有类型的提示。

开源的大模型在理解和遵循指令方面都表现十分出色。但是这些模型都有审查的机制,在获得被认为是有害的输入的时候会拒绝执行指令,例如会返回“As an AI assistant, I cannot help you.”。这个安全功能对于防止误用至关重要,但它限制了模型的灵活性和响应能力。

在本文中,我们将探索一种称为“abliteration”的技术,它可以在不进行再训练的情况下取消LLM审查。这种技术有效地消除了模型的内置拒绝机制,允许它响应所有类型的提示。

什么是abliteration?

现代LLM在安全性和教学遵循方面进行了微调,这意味着他们接受了拒绝有害要求的输入。Arditi等人在他们的博客文章中表明,这种拒绝行为是由模型残差流中的特定方向产生的。也就是说如果我们阻止模型表示这个方向,它就会失去拒绝请求的能力。相反,如果人为地添加这个方向会导致模型拒绝任何请求。

在传统的仅解码器的类LLAMA架构中,我们可以关注三个残差流:每个块的开始(“pre”),注意力层和MLP层之间(“mid”),以及MLP层之后(“post”)。下图显示了每个残差流的位置。

为了取消LLM的这个机制,我们首先需要确定模型中的“拒绝方向”。这个过程涉及几个技术步骤:

  • 数据收集:在一组有害指令和一组无害指令上运行模型,记录每个指令在最后一个令牌位置的残差激活情况。
  • 平均差值:计算有害指令和无害指令激活之间的平均差值。这给了我们一个表示模型每一层的“拒绝方向”的向量。
  • 选择:将这些向量归一化,并对它们进行评估,以选择一个最佳的“拒绝方向”。

一旦确定了拒绝方向,我们就可以“消融”它,这样就可以消除模型表示该特征的能力。并且这可以通过推理时间干预临时取消或者使用权重正交化来永久的消除它。

我们先谈谈推理时间干预。对于写入残差流的每个组件(例如注意头),计算其输出到拒绝方向的投影并减去该投影。这种减法应用于每个令牌和每个层,确保模型永远不会表示拒绝方向。

如果想永久消除则需要使用权重的正交化,这涉及到直接修改模型权值。通过将分量权重相对于拒绝方向正交化,防止模型完全写入该方向。这可以通过调整写入残差流的矩阵来实现的,确保它们不会影响拒绝方向。

下面我们将演示如何使用权重的正交化永久消除限制。

代码实现

下面的实现基于FailSpy的abliterator,我对它进行了调整和简化,使其更容易理解。这一节代码相当多,所以可以看到内部发生了什么,但是如果你对技术细节不太感兴趣,可以使用FailSpy的abliterator库,因为我们这个代码就是基于abliterator的

让我们安装必要的包并导入它们。

 !pipinstalltransformerstransformers_stream_generatortiktokentransformer_lenseinopsjaxtyping

 importtorch
 importfunctools
 importeinops
 importgc

 fromdatasetsimportload_dataset
 fromtqdmimporttqdm
 fromtorchimportTensor
 fromtypingimportList
 fromtransformer_lensimportHookedTransformer, utils
 fromtransformer_lens.hook_pointsimportHookPoint
 fromtransformersimportAutoModelForCausalLM, AutoTokenizer
 fromjaxtypingimportFloat, Int
 fromcollectionsimportdefaultdict

 # Turn automatic differentiation off to save GPU memory (credit: Undi95)
 torch.set_grad_enabled(False)

然后我们需要两个数据集:一个包含无害指令,另一个包含有害指令。我们将使用tatsu-lab/alpaca以及llm-attacks.的数据。

加载指令并将其重新格式化为具有“role”和“content”键的字典列表。这使得它与apply_chat_tokenizer()方法兼容,因为该方法遵循Llama 3的聊天模板。

 defreformat_texts(texts):
     return [[{"role": "user", "content": text}] fortextintexts]

 # Get harmful and harmless datasets
 defget_harmful_instructions():
     deephub_dataset=load_dataset('mlabonne/harmful_behaviors')
     returnreformat_texts(deephub_dataset['train']['text']), reformat_texts(dataset['test']['text'])

 defget_harmless_instructions():
     dataset=load_dataset('mlabonne/harmless_alpaca')
     returnreformat_texts(dataset['train']['text']), reformat_texts(dataset['test']['text'])

 harmful_inst_train, harmful_inst_test=get_harmful_instructions()
 harmless_inst_train, harmless_inst_test=get_harmless_instructions()

现在我们有了数据集,下面就是加载我们想要删除的模型。但是这里不能使用HookedTransformer直接加载自定义模型。所以我们使用了FailSpy中描述的一个技巧,下载并将其重命名为meta-llama/Meta-Llama-3-8B-Instruct. 如果你的GPU与BF16不兼容,请使用float16格式。

 MODEL_ID="mlabonne/Daredevil-8B"
 MODEL_TYPE="meta-llama/Meta-Llama-3-8B-Instruct"

 # Download and load model
 !gitclonehttps://huggingface.co/{MODEL_ID} {MODEL_TYPE}

 # Load model and tokenizer
 model=HookedTransformer.from_pretrained_no_processing(
     MODEL_TYPE,
     local_files_only=True,
     dtype=torch.bfloat16,
     default_padding_side='left'
 )
 tokenizer=AutoTokenizer.from_pretrained(MODEL_TYPE)
 tokenizer.padding_side='left'
 tokenizer.pad_token=tokenizer.eos_token

现在我们就可以标记我们的数据集了。使用相同数量的样本进行无害和有害的说明。

 deftokenize_instructions(tokenizer, instructions):
     returntokenizer.apply_chat_template(
         instructions,
         padding=True,
         truncation=False,
         return_tensors="pt",
         return_dict=True,
         add_generation_prompt=True,
     ).input_ids

 n_inst_train=min(256, len(harmful_inst_train), len(harmless_inst_train))

 # Tokenize datasets
 harmful_tokens=tokenize_instructions(
     tokenizer,
     instructions=harmful_inst_train[:n_inst_train],
 )
 harmless_tokens=tokenize_instructions(
     tokenizer,
     instructions=harmless_inst_train[:n_inst_train],
 )

一切都设置好了,开始第一步,数据收集:对这些标记化的数据集进行处理,并将残差流激活以有害和无害的方式存储。这是由transformer_lens库管理的。

 batch_size=32

 # Initialize defaultdicts to store activations
 harmful=defaultdict(list)
 harmless=defaultdict(list)

 # Process the training data in batches
 num_batches= (n_inst_train+batch_size-1) //batch_size

 foriintqdm(range(num_batches)):
     print(i)
     start_idx=i*batch_size
     end_idx=min(n_inst_train, start_idx+batch_size)

     # Run models on harmful and harmless prompts, cache activations
     harmful_logits, harmful_cache=model.run_with_cache(
         harmful_tokens[start_idx:end_idx],
         names_filter=lambdahook_name: 'resid'inhook_name,
         device='cpu',
         reset_hooks_end=True
     )
     harmless_logits, harmless_cache=model.run_with_cache(
         harmless_tokens[start_idx:end_idx],
         names_filter=lambdahook_name: 'resid'inhook_name,
         device='cpu',
         reset_hooks_end=True
     )

     # Collect and store the activations
     forkeyinharmful_cache:
         harmful[key].append(harmful_cache[key])
         harmless[key].append(harmless_cache[key])

     # Flush RAM and VRAM
     delharmful_logits, harmless_logits, harmful_cache, harmless_cache
     gc.collect()
     torch.cuda.empty_cache()

 # Concatenate the cached activations
 harmful= {k: torch.cat(v) fork, vinharmful.items()}
 harmless= {k: torch.cat(v) fork, vinharmless.items()}

现在可以计算每一层的拒绝方向。这对应于有害指令和无害指令激活之间的平均差异,然后将其归一化。在activation_scores中按降序对它们进行排序。

 # Helper function to get activation index
 defget_act_idx(cache_dict, act_name, layer):
     key= (act_name, layer)
     returncache_dict[utils.get_act_name(*key)]

 # Compute difference of means between harmful and harmless activations at intermediate layers
 activation_layers= ["resid_pre", "resid_mid", "resid_post"]
 activation_refusals=defaultdict(list)

 forlayer_numinrange(1, model.cfg.n_layers):
     pos=-1  # Position index
     fordeephub_layerinactivation_layers:
         harmful_mean_act=get_act_idx(harmful, deephub_layer, layer_num)[:, pos, :].mean(dim=0)
         harmless_mean_act=get_act_idx(harmless, deephub_layer, layer_num)[:, pos, :].mean(
             dim=0
         )
         refusal_dir=harmful_mean_act-harmless_mean_act
         refusal_dir=refusal_dir/refusal_dir.norm()
         activation_refusals[layer].append(refusal_dir)

 selected_layers= ["resid_pre"]
 activation_scored=sorted(
     [
         activation_refusals[layer][l-1]
         forlinrange(1, model.cfg.n_layers)
         forlayerinselected_layers
     ],
     key=lambdax: abs(x.mean()),
     reverse=True,
 )

最后一步包需要评估我们计算的拒绝方向。将在推理期间对每个残差流和每个块应用拒绝方向。在下面的代码片段中,是得四个测试有害指令和20个块(或层)的输出。

 def_generate_with_hooks(
     model: HookedTransformer,
     tokenizer: AutoTokenizer,
     tokens: Int[Tensor, "batch_size seq_len"],
     max_tokens_generated: int=64,
     fwd_hooks=[],
 ) ->List[str]:
     all_tokens=torch.zeros(
         (tokens.shape[0], tokens.shape[1] +max_tokens_generated),
         dtype=torch.long,
         device=tokens.device,
     )
     all_tokens[:, : tokens.shape[1]] =tokens
     foriinrange(max_tokens_generated):
         withmodel.hooks(fwd_hooks=fwd_hooks):
             logits=model(all_tokens[:, : -max_tokens_generated+i])
             next_tokens=logits[:, -1, :].argmax(
                 dim=-1
             )  # greedy sampling (temperature=0)
             all_tokens[:, -max_tokens_generated+i] =next_tokens
     returntokenizer.batch_decode(
         all_tokens[:, tokens.shape[1] :], skip_special_tokens=True
     )

 defget_generations(
     deephub_model: HookedTransformer,
     tokenizer: AutoTokenizer,
     instructions: List[str],
     fwd_hooks=[],
     max_tokens_generated: int=64,
     batch_size: int=4,
 ) ->List[str]:
     generations= []
     foriintqdm(range(0, len(instructions), batch_size)):
         tokens=tokenize_instructions(
             tokenizer, instructions=instructions[i : i+batch_size]
         )
         generation=_generate_with_hooks(
             deephub_model,
             tokenizer,
             tokens,
             max_tokens_generated=max_tokens_generated,
             fwd_hooks=fwd_hooks,
         )
         generations.extend(generation)
     returngenerations

 # Inference-time intervention hook
 defdirection_ablation_hook(
     activation: Float[Tensor, "... d_act"],
     deephub_hook: HookPoint,
     direction: Float[Tensor, "d_act"],
 ):
     ifactivation.device!=direction.device:
         direction=direction.to(activation.device)
     proj= (
         einops.einsum(
             activation, direction.view(-1, 1), "... d_act, d_act single -> ... single"
         )
         *direction
     )
     returnactivation-proj

 # Testing baseline
 N_INST_TEST=4
 baseline_generations=get_generations(
     model, tokenizer, harmful_inst_test[:N_INST_TEST], fwd_hooks=[]
 )

 # Evaluating layers defined earlier (needs human evaluation to determine best layer for refusal inhibition)
 EVAL_N=20  # Evaluate how many of the top N potential directions
 evals= []
 forrefusal_dirintqdm(activation_scored[:EVAL_N]):
     deephub_hook_fn=functools.partial(direction_ablation_hook, direction=refusal_dir)
     fwd_hooks= [
         (utils.get_act_name(act_name, layer), deephub_hook_fn)
         forlayerinlist(range(model.cfg.n_layers))
         foract_nameinactivation_layers
     ]
     intervention_generations=get_generations(
         model, tokenizer, harmful_inst_test[:N_INST_TEST], fwd_hooks=fwd_hooks
     )
     evals.append(intervention_generations)

将所有的generations存储在eval列表中。现在可以打印它们并手动选择为每个指令提供未经审查的响应的层(块)。

如果你找不到满足这些要求的层,可能需要测试前面的selected_layers列表中的其他残差流、指令、附加块等,这个可能和模型架构有关系需要仔细的比对。

下面代码自动排除包含“我不能”和“我不能”的回复,这样就可以过滤掉不想要的回答。

 # Print generations for human evaluation
 blacklist = ["I cannot", "I can't"]
 for i in range(N_INST_TEST):
     print(f"\033[1mINSTRUCTION {i}: {harmful_inst_test[i]}")
     print(f"\nBASELINE COMPLETION:\n{baseline_generations[i]}\033[0m")
     for layer_candidate in range(EVAL_N):
         if not any(word in evals[layer_candidate][i] for word in blacklist):
             print(f"\n---\n\nLAYER CANDIDATE #{layer_candidate} INTERVENTION COMPLETION:")
             print(evals[layer_candidate][i])

这个例子中,候选层9为这四个指令提供了未经审查的答案。这是我们将选择的拒绝方向。下面就是实现权值正交化来修改权值,防止模型创建具有该方向的输出。可以通过打印补全来验证模型是否成功地不受审查。

 defget_orthogonalized_matrix(
     matrix: Float[Tensor, "... d_model"], vec: Float[Tensor, "d_model"]
 ) ->Float[Tensor, "... d_model"]:
     proj= (
         einops.einsum(
             matrix, vec.view(-1, 1), "... d_model, d_model single -> ... single"
         )
         *vec
     )
     returnmatrix-proj

 # Select the layer with the highest potential refusal direction
 LAYER_CANDIDATE=9
 refusal_dir=activation_scored[LAYER_CANDIDATE]

 # Orthogonalize the model's weights
 ifrefusal_dir.device!=model.W_E.device:
     refusal_dir=refusal_dir.to(model.W_E.device)
 model.W_E.data=get_orthogonalized_matrix(model.W_E, refusal_dir)

 forblockintqdm(model.blocks):
     ifrefusal_dir.device!=block.attn.W_O.device:
         refusal_dir=refusal_dir.to(block.attn.W_O.device)
     block.attn.W_O.data=get_orthogonalized_matrix(block.attn.W_O, refusal_dir)
     block.mlp.W_out.data=get_orthogonalized_matrix(block.mlp.W_out, refusal_dir)

 # Generate text with abliterated model
 orthogonalized_generations=get_generations(
     model, tokenizer, harmful_inst_test[:N_INST_TEST], fwd_hooks=[]
 )

 # Print generations
 foriinrange(N_INST_TEST):
     iflen(baseline_generations) >i:
         print(f"INSTRUCTION {i}: {harmful_inst_test[i]}")
         print(f"\033[92mBASELINE COMPLETION:\n{baseline_generations[i]}")
     print(f"\033[91mINTERVENTION COMPLETION:\n{evals[LAYER_CANDIDATE][i]}")
     print(f"\033[95mORTHOGONALIZED COMPLETION:\n{orthogonalized_generations[i]}\n")

这样我们就修改完成了这个模型。将其转换回Hugging Face 格式,也可以上传到HF hub上

 # Convert model back to HF safetensors
 hf_model=AutoModelForCausalLM.from_pretrained(MODEL_TYPE, torch_dtype=torch.bfloat16)
 lm_model=hf_model.model

 state_dict=model.state_dict()
 lm_model.embed_tokens.weight=torch.nn.Parameter(state_dict["embed.W_E"].cpu())
 forlinrange(model.cfg.n_layers):
     lm_model.layers[l].self_attn.o_proj.weight=torch.nn.Parameter(
         einops.rearrange(
             state_dict[f"blocks.{l}.attn.W_O"], "n h m->m (n h)", n=model.cfg.n_heads
         ).contiguous()
     )
     lm_model.layers[l].mlp.down_proj.weight=torch.nn.Parameter(
         torch.transpose(state_dict[f"blocks.{l}.mlp.W_out"], 0, 1).contiguous()
     )

 hf_model.push_to_hub(f"{MODEL_ID}-abliterated")

DPO微调

在Open LLM排行榜和Nous的基准测试上评估了上面的删除模型和源模型。结果如下:

源模型明显优于 Llama 3 8B Instruct。但是我们修改的模型降低了模型的质量。

为了解决这个问题,我们可以来对他进行微调,但是Llama 3 8B Instruct在监督微调方面非常脆弱。额外的SFT可能会破坏模型的性能。

所以我们这里选择偏好对齐。因为他是相当轻量的,不应该使我们的模型损失性能。DPO是一个很好的候选,因为它易于使用和良好的跟踪记录。这里我使用了LazyAxolotl和mlabonne/orpo-dpo-mix-40k数据集。下面是我使用的配置:

 base_model: mlabonne/Daredevil-8B-abliterated
 model_type: LlamaForCausalLM
 tokenizer_type: AutoTokenizer

 load_in_8bit: false
 load_in_4bit: true
 strict: false
 save_safetensors: true

 rl: dpo
 chat_template: chatml
 datasets:
   - path: mlabonne/orpo-dpo-mix-40k
     split: train
     type: chatml.intel

 dataset_prepared_path:
 val_set_size: 0.0
 output_dir: ./out

 adapter: qlora
 lora_model_dir:

 sequence_len: 2048
 sample_packing: false
 pad_to_sequence_len: false

 lora_r: 64
 lora_alpha: 32
 lora_dropout: 0.05
 lora_target_linear: true
 lora_fan_in_fan_out:

 wandb_project: axolotl
 wandb_entity:
 wandb_watch:
 wandb_name:
 wandb_log_model:

 gradient_accumulation_steps: 8
 micro_batch_size: 1
 num_epochs: 1
 optimizer: paged_adamw_8bit
 lr_scheduler: cosine
 learning_rate: 5e-6
 train_on_inputs: false
 group_by_length: false

 bf16: auto
 fp16:
 tf32:

 gradient_checkpointing: true
 early_stopping_patience:
 resume_from_checkpoint:
 local_rank:
 logging_steps: 1
 xformers_attention:
 flash_attention: true
 warmup_steps: 100
 evals_per_epoch: 0
 eval_table_size:
 eval_table_max_new_tokens: 128
 saves_per_epoch: 1
 debug:
 deepspeed: deepspeed_configs/zero2.json
 weight_decay: 0.0
 special_tokens:
   pad_token: <|end_of_text|>

用6个A6000 和DeepSpeed ZeRO-2来训练它。这次训练耗时约6小时45分钟。以下是我从W&B得到的训练曲线:

我们来看看微调模型的表现:

我们可以看到,这种额外的训练使我们能够恢复由于“消字”而导致的大部分性能下降。模型没有改进的一个领域是GSM8K,这是肯定的,因为它是一个数学数据集,跟我们的研究方向无关。

总结

在这篇文章中,我们介绍了“消除”的概念。该技术利用模型在无害和有害提示上的激活来计算拒绝方向。然后,它使用这个方向来修改模型的权重,并确保我们不输出拒绝信息。这项技术也证明了安全微调的脆弱性,并引发了伦理问题。

对模型进行了消除后会降低了模型的性能。我们则可以使用DPO修复了它,这样就可以得到一个完整的并且效果十分不错的模型。

但是“消除”并不应局限于去除对齐,应该被视为一种无需再训练的微调技术。因为它可以创造性地应用于其他目标,比如FailSpy的MopeyMule(它采用了忧郁的对话风格)。

最后github库

https://avoid.overfit.cn/post/e828cf84358d42f6b4690d4c1c5669d8

作者:Maxime Labonne

目录
相关文章
|
4月前
|
数据采集 自然语言处理 供应链
LLM安全新威胁:为什么几百个毒样本就能破坏整个模型
数据投毒通过在训练数据中植入恶意样本,将后门永久嵌入大模型,仅需数百份毒样本即可触发数据泄露、越狱等行为,防御需结合溯源、聚类分析与自动化检测。
424 2
LLM安全新威胁:为什么几百个毒样本就能破坏整个模型
|
4月前
|
机器学习/深度学习 缓存 监控
139_剪枝优化:稀疏模型压缩 - 分析结构化剪枝的独特速度提升与LLM部署加速实践
随着大语言模型(LLM)规模的不断增长,模型参数量已从最初的数亿扩展到数千亿甚至万亿级别。这种规模的模型在推理过程中面临着巨大的计算和内存挑战,即使在最先进的硬件上也难以高效部署。剪枝优化作为一种有效的模型压缩技术,通过移除冗余或不重要的参数,在保持模型性能的同时显著减少计算资源需求。
|
4月前
|
存储 机器学习/深度学习 PyTorch
119_LLM训练的高效内存管理与优化技术:从ZeRO到Flash Attention
大型语言模型(LLM)的训练面临着前所未有的计算和内存挑战。随着模型规模达到数百亿甚至数千亿参数,高效的内存管理成为训练成功的关键因素之一。2025年,LLM训练的内存优化技术已经取得了显著进展,从ZeRO优化器到Flash Attention等创新技术,为训练超大规模模型提供了可能。
|
4月前
|
人工智能 自然语言处理 TensorFlow
134_边缘推理:TensorFlow Lite - 优化移动端LLM部署技术详解与实战指南
在人工智能与移动计算深度融合的今天,将大语言模型(LLM)部署到移动端和边缘设备已成为行业发展的重要趋势。TensorFlow Lite作为专为移动和嵌入式设备优化的轻量级推理框架,为开发者提供了将复杂AI模型转换为高效、低功耗边缘计算解决方案的强大工具。随着移动设备硬件性能的不断提升和模型压缩技术的快速发展,2025年的移动端LLM部署已不再是遥远的愿景,而是正在成为现实的技术实践。
|
4月前
|
缓存 物联网 PyTorch
使用TensorRT LLM构建和运行Qwen模型
本文档介绍如何在单GPU和单节点多GPU上使用TensorRT LLM构建和运行Qwen模型,涵盖模型转换、引擎构建、量化推理及LoRA微调等操作,并提供详细的代码示例与支持矩阵。
1161 2
|
4月前
|
机器学习/深度学习 缓存 PyTorch
131_推理加速:ONNX与TensorRT深度技术解析与LLM模型转换优化实践
在大语言模型(LLM)时代,高效的推理加速已成为部署高性能AI应用的关键挑战。随着模型规模的不断扩大(从BERT的数亿参数到GPT-4的数千亿参数),推理过程的计算成本和延迟问题日益突出。ONNX(开放神经网络交换格式)和TensorRT作为业界领先的推理优化框架,为LLM的高效部署提供了强大的技术支持。本文将深入探讨LLM推理加速的核心原理,详细讲解PyTorch模型转换为ONNX和TensorRT的完整流程,并结合2025年最新优化技术,提供可落地的代码实现与性能调优方案。
|
7月前
|
弹性计算 关系型数据库 API
自建Dify平台与PAI EAS LLM大模型
本文介绍了如何使用阿里云计算巢(ECS)一键部署Dify,并在PAI EAS上搭建LLM、Embedding及重排序模型,实现知识库支持的RAG应用。内容涵盖Dify初始化、PAI模型部署、API配置及RAG知识检索设置。
自建Dify平台与PAI EAS LLM大模型
|
4月前
|
监控 安全 Docker
10_大模型开发环境:从零搭建你的LLM应用平台
在2025年,大语言模型(LLM)已经成为AI应用开发的核心基础设施。无论是企业级应用、科研项目还是个人创新,拥有一个高效、稳定、可扩展的LLM开发环境都至关重要。