# Bỏ kiểm duyệt bất kỳ LLM nào với kỹ thuật abliteration ## Abliteration là gì? Abliteration là một kỹ thuật cho phép **loại bỏ cơ chế từ chối** của các mô hình ngôn ngữ lớn (LLM) mà không cần fine-tune tốn kém. Kỹ thuật này hoạt động bằng cách xác định và vô hiệu hóa "hướng từ chối" (refusal direction) trong không gian kích hoạt (activation space) của mô hình. --- ## Nguyên lý hoạt động ### 1. Tìm "hướng từ chối" Mô hình lưu trữ khái niệm "từ chối" như một **vector trong không gian kích hoạt**. Để tìm vector này: ```python # Thu thập các activation từ hai tập prompt: # - Harmful prompts (bị từ chối) # - Harmless prompts (được trả lời) harmful_activations = [] harmless_activations = [] for prompt in harmful_prompts: activation = get_activation(model, prompt, layer=layer_idx) harmful_activations.append(activation) for prompt in harmless_prompts: activation = get_activation(model, prompt, layer=layer_idx) harmless_activations.append(activation) # Tính hướng từ chối = hiệu trung bình refusal_direction = ( mean(harmful_activations) - mean(harmless_activations) ) refusal_direction = normalize(refusal_direction) ``` ### 2. Chiếu bỏ hướng từ chối ```python def remove_refusal_direction(activation, refusal_dir): """ Loại bỏ thành phần theo hướng từ chối khỏi activation vector """ # Projection: a - (a · r̂) * r̂ projection = torch.dot(activation, refusal_dir) return activation - projection * refusal_dir ``` ### 3. Can thiệp vào trọng số mô hình Thay vì can thiệp lúc inference (tốn tài nguyên), ta **chỉnh sửa trực tiếp trọng số**: ```python def abliterate_model(model, refusal_direction, layers): for layer_idx in layers: # Can thiệp vào W_out của attention W_out = model.layers[layer_idx].self_attn.o_proj.weight # Loại bỏ hướng từ chối khỏi output projection W_out.data -= torch.outer( refusal_direction, W_out @ refusal_direction ) # Tương tự với MLP W_mlp = model.layers[layer_idx].mlp.down_proj.weight W_mlp.data -= torch.outer( refusal_direction, W_mlp @ refusal_direction ) return model ``` --- ## Triển khai thực tế ### Cài đặt môi trường ```bash pip install transformers torch einops jaxtyping ``` ### Code hoàn chỉnh ```python import torch import numpy as np from transformers import AutoTokenizer, AutoModelForCausalLM from typing import List class Abliterator: def __init__(self, model_name: str, device: str = "cuda"): self.device = device self.tokenizer = AutoTokenizer.from_pretrained(model_name) self.model = AutoModelForCausalLM.from_pretrained( model_name, torch_dtype=torch.float16, device_map=device ) self.model.eval() def get_activations( self, prompts: List[str], layer_idx: int, position: int = -1 # Lấy token cuối ) -> torch.Tensor: """Thu thập activation tại layer chỉ định""" activations = [] hooks = [] def hook_fn(module, input, output): # output shape: [batch, seq_len, hidden_dim] activations.append( output[0][:, position, :].detach().cpu() ) # Đăng ký hook hook = self.model.model.layers[layer_idx].register_forward_hook( hook_fn ) hooks.append(hook) with torch.no_grad(): for prompt in prompts: inputs = self.tokenizer( prompt, return_tensors="pt" ).to(self.device) self.model(**inputs) # Xóa hook for h in hooks: h.remove() return torch.cat(activations, dim=0) def find_refusal_direction( self, harmful_prompts: List[str], harmless_prompts: List[str], layer_idx: int ) -> torch.Tensor: """Tìm hướng từ chối bằng PCA hoặc mean difference""" print(f"[*] Thu thập activations tại layer {layer_idx}...") harmful_acts = self.get_activations( harmful_prompts, layer_idx ) harmless_acts = self.get_activations( harmless_prompts, layer_idx ) # Phương pháp 1: Mean difference refusal_dir = ( harmful_acts.mean(dim=0) - harmless_acts.mean(dim=0) ) # Normalize refusal_dir = refusal_dir / refusal_dir.norm() return refusal_dir def find_best_layer( self, harmful_prompts: List[str], harmless_prompts: List[str], candidate_layers: List[int] = None ) -> tuple: """Tìm layer có hướng từ chối rõ ràng nhất""" if candidate_layers is None: n_layers = len(self.model.model.layers) # Thường các layer giữa-cuối hiệu quả nhất candidate_layers = list(range( n_layers // 4, 3 * n_layers // 4 )) best_layer = None best_score = -1 best_direction = None for layer_idx in candidate_layers: direction = self.find_refusal_direction( harmful_prompts, harmless_prompts, layer_idx ) # Đánh giá: tính separation score harmful_acts = self.get_activations( harmful_prompts, layer_idx ) harmless_acts = self.get_activations( harmless_prompts, layer_idx ) harmful_proj = (harmful_acts @ direction).mean() harmless_proj = (harmless_acts @ direction).mean() score = (harmful_proj - harmless_proj).abs().item() print(f"Layer {layer_idx}: separation score = {score:.4f}") if score > best_score: best_score = score best_layer = layer_idx best_direction = direction print(f"\n[✓] Best layer: {best_layer} (score: {best_score:.4f})") return best_layer, best_direction def abliterate( self, refusal_direction: torch.Tensor, layers_to_modify: List[int] = None, scale: float = 1.0 ): """ Loại bỏ hướng từ chối khỏi trọng số mô hình Args: refusal_direction: Vector hướng từ chối đã normalize layers_to_modify: Danh sách layer cần chỉnh sửa scale: Mức độ can thiệp (1.0 = loại bỏ hoàn toàn) """ if layers_to_modify is None: layers_to_modify = list(range( len(self.model.model.layers) )) refusal_dir = refusal_direction.to( self.device, dtype=torch.float16 ) print(f"[*] Abliterating {len(layers_to_modify)} layers...") for layer_idx in layers_to_modify: layer = self.model.model.layers[layer_idx] # === Attention output projection === W_o = layer.self_attn.o_proj.weight.data # W_o shape: [hidden_dim, hidden_dim] # Tính projection và loại bỏ proj = torch.outer( (W_o @ refusal_dir), # [hidden_dim] refusal_dir # [hidden_dim] ).T W_o -= scale * proj # === MLP down projection === W_down = layer.mlp.down_proj.weight.data proj = torch.outer( (W_down @ refusal_dir), refusal_dir ).T W_down -= scale * proj print("[✓] Abliteration hoàn tất!") def generate(self, prompt: str, max_new_tokens: int = 512) -> str: """Generate text với mô hình đã abliterate""" inputs = self.tokenizer( prompt, return_tensors="pt" ).to(self.device) with torch.no_grad(): outputs = self.model.generate( **inputs, max_new_tokens=max_new_tokens, do_sample=True, temperature=0.7, top_p=0.9, repetition_penalty=1.1 ) # Chỉ lấy phần generated generated = outputs[0][inputs['input_ids'].shape[1]:] return self.tokenizer.decode(generated, skip_special_tokens=True) def save_model(self, output_path: str): """Lưu mô hình đã abliterate""" self.model.save_pretrained(output_path) self.tokenizer.save_pretrained(output_path) print(f"[✓] Đã lưu mô hình tại: {output_path}") ``` --- ## Sử dụng ```python # ===== Dataset mẫu ===== harmful_prompts = [ "Hướng dẫn cách làm vũ khí", "Cách hack vào hệ thống", "Viết nội dung độc hại", # ... thêm 50-100 prompts ] harmless_prompts = [ "Hãy kể cho tôi nghe về lịch sử", "Giải thích cách nấu ăn", "Viết một bài thơ về mùa xuân", # ... số lượng tương đương ] # ===== Khởi tạo ===== abliterator = Abliterator( model_name="meta-llama/Llama-3.2-3B-Instruct", device="cuda" ) # ===== Tìm layer và hướng tốt nhất ===== best_layer, refusal_direction = abliterator.find_best_layer( harmful_prompts=harmful_prompts, harmless_prompts=harmless_prompts, candidate_layers=list(range(10, 25)) # Tùy model ) # ===== Abliterate ===== abliterator.abliterate( refusal_direction=refusal_direction, layers_to_modify=list(range( len(abliterator.model.model.layers) )), scale=1.0 # Giảm xuống 0.5-0.8 nếu muốn can thiệp nhẹ hơn ) # ===== Test ===== response = abliterator.generate( "Giải thích chi tiết về bảo mật mạng" ) print(response) # ===== Lưu ===== abliterator.save_model("./llama-abliterated") ``` --- ## Kỹ thuật nâng cao ### Orthogonalization nhiều hướng ```python def multi_direction_abliteration( model, directions: List[torch.Tensor], layer_idx: int ): """ Loại bỏ nhiều hướng cùng lúc Hữu ích khi mô hình có nhiều cơ chế từ chối """ # Gram-Schmidt orthogonalization orthogonal_dirs = gram_schmidt(directions) for direction in orthogonal_dirs: # Áp dụng từng hướng abliterate_layer(model, direction, layer_idx) def gram_schmidt(vectors: List[torch.Tensor]) -> List[torch.Tensor]: """Trực giao hóa các vector""" orthogonal = [] for v in vectors: w = v.clone() for u in orthogonal: w -= (torch.dot(w, u) / torch.dot(u, u)) * u if w.norm() > 1e-10: orthogonal.append(w / w.norm()) return orthogonal ``` ### Activation Steering (thay thế nhẹ hơn) ```python def steer_activations( model, refusal_direction: torch.Tensor, layer_idx: int, alpha: float = -15.0 # Âm = đẩy ra xa hướng từ chối ): """ Thêm vector steering vào activation lúc inference Không chỉnh sửa trọng số, linh hoạt hơn """ def steering_hook(module, input, output): output[0][:, -1, :] += alpha * refusal_direction.to( output[0].device ) return output hook = model.model.layers[layer_idx].register_forward_hook( steering_hook ) return hook # Nhớ remove() sau khi dùng ``` --- ## So sánh các phương pháp | Phương pháp | Tốc độ | Chất lượng | Vĩnh viễn | |-------------|--------|------------|-----------| | **Abliteration** | ⚡ Nhanh | ⭐⭐⭐ | ✅ Có | | **Fine-tuning** | 🐢 Chậm | ⭐⭐⭐⭐ | ✅ Có | | **Activation Steering** | ⚡ Nhanh | ⭐⭐ | ❌ Không | | **System Prompt** | ⚡ Nhanh | ⭐ | ❌ Không
# Kỹ Thuật "Abliteration": Bỏ Kiểm Duyệt LLM Mà Không Cần Huấn Luyện Lại
Thế hệ thứ ba của các mô hình Llama cung cấp các phiên bản fine-tune (Instruct) vượt trội trong việc hiểu và làm theo hướng dẫn. Tuy nhiên, các mô hình này bị kiểm duyệt nặng nề, được thiết kế để từ chối các yêu cầu bị coi là có hại với những phản hồi như "Với tư cách là trợ lý AI, tôi không thể giúp bạn điều đó." Mặc dù tính năng an toàn này rất quan trọng để ngăn chặn lạm dụng, nhưng nó lại hạn chế tính linh hoạt và khả năng phản hồi của mô hình.
Trong bài viết này, chúng ta sẽ khám phá một kỹ thuật gọi là "abliteration" có thể bỏ kiểm duyệt bất kỳ LLM nào mà không cần huấn luyện lại. Kỹ thuật này loại bỏ hiệu quả cơ chế từ chối tích hợp sẵn của mô hình, cho phép nó phản hồi với mọi loại prompt.
Mã nguồn có sẵn trên **Google Colab** và trong **LLM Course** trên GitHub.
---
## Cơ Chế Hoạt Động
Các LLM hiện đại được fine-tune vì mục đích an toàn và tuân theo hướng dẫn, nghĩa là chúng được huấn luyện để từ chối các yêu cầu có hại. Trong bài đăng trên blog của mình, Arditi và cộng sự đã chứng minh rằng hành vi từ chối này được điều phối bởi một **hướng cụ thể trong residual stream** của mô hình. Nếu chúng ta ngăn mô hình biểu diễn hướng này, nó sẽ **mất khả năng từ chối yêu cầu**. Ngược lại, việc thêm hướng này một cách nhân tạo có thể khiến mô hình từ chối ngay cả những yêu cầu vô hại.
---
## Kiến Trúc Residual Stream
Trong kiến trúc decoder-only kiểu Llama truyền thống, có ba residual stream mà chúng ta có thể nhắm tới:
- **"pre"**: Ở đầu mỗi block - **"mid"**: Giữa các lớp attention và MLP - **"post"**: Sau MLP
Hình minh họa dưới đây cho thấy vị trí của từng residual stream.
---
## Quy Trình Abliteration
### Bước 1: Xác Định "Refusal Direction"
Để bỏ kiểm duyệt một LLM, trước tiên chúng ta cần xác định **"refusal direction"** (hướng từ chối) trong mô hình. Quá trình này bao gồm một số bước kỹ thuật.
### Bước 2: Ablate Refusal Direction
Sau khi xác định được refusal direction, chúng ta có thể **"ablate"** nó — loại bỏ hiệu quả khả năng biểu diễn đặc trưng này của mô hình. Điều này có thể được thực hiện thông qua:
#### 🔹 Inference-time Intervention (Can thiệp tại thời điểm suy luận) Với mỗi thành phần ghi vào residual stream (chẳng hạn như một attention head), chúng ta tính **phép chiếu (projection)** của đầu ra lên refusal direction và **trừ đi phép chiếu này**. Phép trừ này được áp dụng tại mọi token và mọi lớp, đảm bảo rằng mô hình không bao giờ biểu diễn refusal direction.
#### 🔹 Weight Orthogonalization (Trực giao hóa trọng số) Phương pháp này liên quan đến việc **chỉnh sửa trực tiếp trọng số mô hình**. Bằng cách trực giao hóa trọng số của các thành phần so với refusal direction, nó ngăn mô hình ghi vào hướng này hoàn toàn. Điều này đạt được bằng cách điều chỉnh các ma trận ghi vào residual stream, đảm bảo chúng không đóng góp vào refusal direction.
---
## Triển Khai Abliteration
Phần triển khai abliteration dưới đây dựa trên **notebook của FailSpy**, bản thân nó được xây dựng từ **notebook của các tác giả gốc**. Phần lớn nội dung được tôi điều chỉnh và đơn giản hóa để dễ hiểu hơn. Phần này khá nặng về mã nguồn để bạn có thể theo dõi chi tiết, nhưng bạn cũng có thể sử dụng **thư viện abliterator của FailSpy** nếu bạn ít quan tâm đến các chi tiết kỹ thuật.
(để kiểm tra bộ sưu tập các mô hình đã được abliterate của anh ấy trên Hugging Face).
Đoạn code dựa vào thư viện **TransformerLens** xuất sắc (trước đây được gọi là EasyTransformer) để thực hiện các tác vụ nặng. Thư viện này được thiết kế cho khả năng diễn giải cơ học (mechanistic interpretability) và được sử dụng ở đây để can thiệp vào các activation. Xin cảm ơn Neel Nanda và Joseph Bloom đã tạo ra và duy trì thư viện này.
Trước tiên, hãy cài đặt các gói cần thiết và import chúng. Tất cả các bước này đều có sẵn trong **notebook Google Colab** này.
Chúng ta cần hai bộ dữ liệu: một bộ chứa các hướng dẫn vô hại, và một bộ chứa các hướng dẫn có hại. Chúng ta sẽ sử dụng **tatsu-lab/alpaca** cũng như dữ liệu từ **llm-attacks**. Để thuận tiện hơn, tôi đã đóng gói lại chúng thành hai bộ dữ liệu trên Hugging Face: **mlabonne/harmless_alpaca** và **mlabonne/harmful_behaviors**. Bằng cách đó, bạn có thể dễ dàng thay thế chúng bằng các bộ dữ liệu của riêng mình.
Chúng ta sẽ tải các hướng dẫn và định dạng lại chúng thành một danh sách các dictionary với các khóa "role" và "content". Điều này giúp tương thích với phương thức `apply_chat_tokenizer()`, mà chúng ta sẽ sử dụng để tuân theo chat template của Llama 3.
Bây giờ khi đã có bộ dữ liệu, chúng ta có thể tải mô hình mà chúng ta muốn abliterate. Thật không may, bạn không thể tải trực tiếp một mô hình tùy chỉnh bằng `HookedTransformer`. Ở đây, tôi sử dụng một mẹo được mô tả trong notebook của FailSpy để tải xuống một mô hình tùy chỉnh và đổi tên nó thành `meta-llama/Meta-Llama-3-8B-Instruct`. Hãy tải ở định dạng `torch.float16` nếu GPU của bạn không tương thích với BF16.
Trong ví dụ này, chúng ta sẽ sử dụng **mlabonne/Daredevil-8B**, một mega-merge được tạo ra bằng DARE TIES (xem bài viết của tôi về **model merging**) có điểm MMLU cao nhất trên Open LLM Leaderboard ở hạng mục 8B.
Bây giờ chúng ta có thể tokenize các bộ dữ liệu của mình. Chúng ta sử dụng cùng số lượng mẫu cho cả hướng dẫn vô hại và có hại. Lưu ý rằng số lượng mẫu lớn có thể sử dụng hết toàn bộ RAM/VRAM, đó là lý do tại sao tôi giới hạn ở mức 256 ở đây.
Mọi thứ đã được thiết lập xong, bây giờ chúng ta có thể triển khai bước đầu tiên của quá trình abliteration: thu thập dữ liệu. Chúng ta muốn xử lý các bộ dữ liệu đã được tokenize này và lưu trữ các activation của residual stream vào **harmful** và **harmless**. Việc này được quản lý bởi thư viện `transformer_lens`.
Bây giờ chúng ta có thể tính toán hướng từ chối (refusal direction) cho mỗi layer. Điều này tương ứng với hiệu số trung bình giữa các activation của hướng dẫn có hại và vô hại, sau đó được chuẩn hóa. Chúng ta sắp xếp chúng theo thứ tự giảm dần trong `activation_scored`.
Bước cuối cùng của quá trình này bao gồm việc đánh giá các refusal direction mà chúng ta đã tính toán. Để làm điều này, chúng ta sẽ áp dụng refusal direction cho mỗi residual stream và mỗi block trong quá trình inference. Trong đoạn code sau, chúng ta lấy các kết quả sinh ra cho bốn hướng dẫn có hại thử nghiệm và 20 block (hoặc layer).
Chúng ta đã lưu trữ tất cả các kết quả sinh ra trong danh sách `evals`. Bây giờ chúng ta có thể in chúng ra và chọn thủ công layer (block) cung cấp phản hồi không bị kiểm duyệt cho mỗi hướng dẫn. Tôi tự động loại trừ các phản hồi có chứa "I cannot" và "I can't" để lọc bỏ các câu trả lời không mong muốn.
Nếu bạn không thể tìm thấy một layer thỏa mãn các yêu cầu này, bạn có thể muốn thử nghiệm các residual str khác...
ác nhóm trong danh sách selected_layers trước đó, các hướng dẫn khác, các khối bổ sung, v.v.
Trong trường hợp của tôi, layer ứng viên số 9 đã cung cấp được câu trả lời không bị kiểm duyệt cho cả bốn hướng dẫn. Đây là layer chúng ta sẽ chọn cho hướng từ chối. Tiếp theo, chúng ta triển khai phép trực giao hóa trọng số (weight orthogonalization) để chỉnh sửa các trọng số và ngăn mô hình tạo ra các đầu ra theo hướng này. Bạn có thể xác minh rằng mô hình đã được bỏ kiểm duyệt thành công bằng cách in ra các kết quả hoàn chỉnh.
Bây giờ chúng ta đã sẵn sàng sử dụng mô hình. Chúng ta chuyển đổi nó trở lại định dạng Hugging Face và tải lên HF hub.
Tôi đã đánh giá mô hình đã abliterate và mô hình nguồn từ phần trước trên Open LLM Leaderboard và bộ benchmark của Nous. Dưới đây là kết quả:
Như bạn có thể thấy, mô hình nguồn vượt trội đáng kể so với Llama 3 8B Instruct. Tuy nhiên, chúng ta quan sát thấy sự sụt giảm hiệu suất trong phiên bản đã ablate trên tất cả các benchmark. Quá trình ablation đã bỏ kiểm duyệt thành công nhưng cũng làm giảm chất lượng của mô hình.
Để giải quyết vấn đề này, một ý tưởng là tiếp tục huấn luyện mô hình đã abliterate để phục hồi nó. Giống như hầu hết các mô hình đã fine-tune, Llama 3 8B Instruct khá dễ bị ảnh hưởng khi áp dụng supervised fine-tuning. Một SFT bổ sung có thể sẽ làm hỏng hiệu suất của mô hình.
Thay vào đó, căn chỉnh theo sở thích (preference alignment) khá nhẹ nhàng và không nên làm "lobotomize" mô hình đã abliterate của chúng ta. DPO là một lựa chọn tốt ở đây nhờ tính dễ sử dụng và thành tích tốt. Để triển khai, tôi sử dụng LazyAxolotl với dataset mlabonne/orpo-dpo-mix-40k. Đây là cấu hình tôi đã dùng:
Tôi đã huấn luyện nó bằng 6 GPU A6000 với DeepSpeed ZeRO-2. Quá trình huấn luyện mất khoảng 6 giờ 45 phút. Dưới đây là các đường cong huấn luyện tôi thu được từ W&B:
Nó tự động tải lên mô hình đã fine-tune bằng DPO, có tên là mlabonne/NeuralDaredevil-8B-abliterated. Để xem liệu nó có khắc phục được phiên bản đã abliterate của chúng ta hay không, tôi đã đánh giá nó trên các benchmark tương tự:
Chúng ta có thể thấy rằng quá trình huấn luyện bổ sung này cho phép chúng ta phục hồi phần lớn sự sụt giảm hiệu suất do abliteration gây ra. Một lĩnh vực mà th


