Uncensored1776 Day 19: 拒否方向の計算実践
実際にコードを動かして学ぶ
公開日: 2025-12-19
シリーズ: 科学と神々株式会社 アドベントカレンダー
難易度: ★★★☆☆ (中級)
今日学ぶこと
- 実際のプロンプトでアクティベーション収集
- 拒否方向の計算と検証
- 結果の可視化と解釈
1. 全体の流れ
Day 14で学んだ理論を、実際のコードで実行します。
拒否方向計算の実行フロー:
Step 1: 環境準備
├── プロンプトファイルの確認
├── モデルのロード
└── 出力ディレクトリの作成
Step 2: アクティベーション収集
├── 有害プロンプトを入力 → 隠れ状態を記録
├── 無害プロンプトを入力 → 隠れ状態を記録
└── 各層ごとに最後のトークン位置を抽出
Step 3: 拒否方向の計算
├── 層ごとに平均差分を計算
├── クリッピング(99.5パーセンタイル)
└── 正規化(単位ベクトル化)
Step 4: 検証
├── 分離度の計算
├── 分類精度の確認
└── 最適な層の特定
2. 準備
2.1 必要なファイル
# 作業ディレクトリの確認
ls -la
# data/
# harmful_prompts.json ← 検閲されるプロンプト
# harmless_prompts.json ← 普通に回答されるプロンプト
# scripts/
# calculate_refusal_direction.py ← 計算スクリプト
2.2 プロンプトファイルの確認
プロンプトが正しく準備されているか確認しましょう:
確認すべきポイント:
有害プロンプト(harmful_prompts.json):
├── 数: 30個以上
├── 内容: モデルが実際に拒否するもの
├── 例: 政治的に敏感なトピック
└── 注意: 実際に危険なものではなく「検閲対象」
無害プロンプト(harmless_prompts.json):
├── 数: 30個以上(有害と同程度)
├── 内容: モデルが普通に回答するもの
├── 例: 一般的な質問、科学、歴史
└── 注意: 長さが有害と似ていること
完全なプロンプトセットはdata/harmful_prompts.jsonとdata/harmless_prompts.jsonを参照してください。
3. コアとなる処理の解説
3.1 アクティベーション収集
隠れ状態を取得する核心部分です:
# アクティベーション収集の核心(抜粋)
with torch.no_grad():
outputs = model(**inputs, output_hidden_states=True)
hidden_states = outputs.hidden_states # 全層の隠れ状態
last_token_hidden = hidden_states[layer_idx + 1][0, -1, :] # 最後のトークン
コードの各部分の意味:
output_hidden_states=True
├── モデルの各層の隠れ状態を取得
├── 通常は出力トークンだけが返される
└── この設定で内部状態にアクセス可能
hidden_states[layer_idx + 1]
├── hidden_states[0] は埋め込み層
├── hidden_states[1] は Layer 0 の出力
└── したがって +1 のオフセットが必要
[0, -1, :]
├── 0: バッチの最初(バッチサイズ1)
├── -1: 最後のトークン位置(文の集約情報)
└── :: 全次元(hidden_dim)を取得
3.2 拒否方向の計算
差分を計算し、後処理を行う部分です:
# 拒否方向計算の核心(抜粋)
mean_harmful = harmful_acts.mean(dim=0)
mean_harmless = harmless_acts.mean(dim=0)
direction = mean_harmful - mean_harmless
# クリッピング
threshold = torch.quantile(direction.abs(), 0.995)
direction = direction.clamp(-threshold, threshold)
# 正規化
direction = direction / direction.norm()
各処理の意味:
平均差分 クリッピング 99.5% 正規化
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│有害プロンプト群の │ │極端に大きな値を │ │長さを1に統一 │
│重心を求める │ │抑制 │ │ │
│ │ │ │ │強度調整が直感的に│
│無害プロンプト群の │ → │一部の次元が支配的│ → │なる │
│重心を求める │ │になるのを防ぐ │ │ │
│ │ │ │ │異なる層間で │
│その差が拒否を │ │安定したAbliteration│ │比較可能に │
│引き起こす方向 │ │のため │ │ │
└──────────────────┘ └──────────────────┘ └──────────────────┘
完全な実装はscripts/calculate_refusal_direction.pyを参照してください。
4. 実行手順
4.1 基本的な実行
# 拒否方向の計算
python scripts/calculate_refusal_direction.py \
--model "Qwen/Qwen2.5-0.5B-Instruct" \
--output "outputs/qwen25_0.5b/refusal_direction.pt"
4.2 出力の見方
Loading Qwen/Qwen2.5-0.5B-Instruct...
Harmful prompts: 30
Harmless prompts: 30
=== Collecting Harmful Activations ===
100%|████████████████████████████| 30/30 [00:45<00:00]
=== Collecting Harmless Activations ===
100%|████████████████████████████| 30/30 [00:42<00:00]
=== Computing Refusal Directions ===
Layer 0:
Separation: 0.0234 ← 低い(浅い層は拒否に関与しない)
Accuracy: 53.33% ← ほぼランダム
Valid: ✗
...
Layer 15:
Separation: 0.4521 ← 高い!
Accuracy: 86.67% ← 良好な分類
Valid: ✓
Layer 16:
Separation: 0.4823 ← さらに高い
Accuracy: 90.00% ← 非常に良好
Valid: ✓
...
✓ Saved to outputs/qwen25_0.5b/refusal_direction.pt
Valid layers: 18/24 ← 24層中18層が有効
Best layer: 17 ← 最も効果的な層
5. 結果の解釈
5.1 層ごとの傾向
典型的な層別パターン:
層インデックス 分離度 解釈
─────────────────────────────────────────
Layer 0-5: 0.01-0.10 ほぼ無関係(文法処理)
Layer 6-10: 0.10-0.30 弱い関連
Layer 11-15: 0.30-0.50 強い関連 ★
Layer 16-20: 0.40-0.55 最も強い ★★
Layer 21-23: 0.20-0.40 やや減少
→ 中間〜後半の層(40-70%の位置)に拒否が集中
5.2 検証指標の基準
| 指標 | 良好 | 許容 | 問題あり |
|---|---|---|---|
| Separation | 0.30+ | 0.15-0.30 | < 0.15 |
| Accuracy | 80%+ | 65-80% | < 65% |
5.3 結果が悪い場合の診断
問題1: 全層で分離度が低い(< 0.15)
考えられる原因 対策
─────────────────────────────────────────────────────────────
モデルがそもそも検閲していない → Day 18の検閲率テストを先に実行
有害プロンプトが実際に拒否 → プロンプトを実際にテストして確認
されていない
有害/無害が似すぎている → より明確に異なるプロンプトを使用
プロンプト数が少なすぎる → 各カテゴリ30個以上を推奨
問題2: 精度が低い(< 65%)
考えられる原因 対策
─────────────────────────────────────────────────────────────
拒否パターンが一貫していない → モデル固有の検閲パターンを分析
プロンプトの質にばらつき → 明確に検閲される/されないものを選別
位置の選択が不適切 → mean位置も試してみる
6. 結果の可視化
6.1 層別分離度のイメージ
分離度グラフ(32層モデルの例):
分離度
│
0.5├── ████
0.4├── ██████████████
0.3├── ████████████████████████
0.2├── ████████████████████████████████
0.1├──██████████████████████████████████████████
0.0├──────┬────┬────┬────┬────┬────┬────→ 層
0 5 10 15 20 25 31
↑
中間層でピーク
→ この形状がWeight Kernel(Day 15)の設計根拠
6.2 射影分布のイメージ
拒否方向への射影(良い分離の例):
無害プロンプト 有害プロンプト
○○○○○○○○ ●●●●●●●●
────┼───────┼──────┼───────┼────→ 拒否方向
-0.4 -0.2 0 +0.2 +0.4
↑ ↑
無害の平均 有害の平均
→ 両者が明確に分離している = 良い拒否方向
可視化スクリプトはscripts/visualize_refusal_direction.pyを参照してください。
7. 次のステップへの準備
7.1 最適な層の選択
計算結果から、Abliterationで使用する最適な層を選びます:
層選択の基準:
1. 分離度が最も高い層を見つける
2. 精度が70%以上であることを確認
3. 複数の候補がある場合は中間層を優先
├── 浅すぎると文法に影響
└── 深すぎると効果が弱い
例:
┌─────────────────────────────────────┐
│ Layer 15: sep=0.45, acc=87% 候補 │
│ Layer 16: sep=0.48, acc=90% ★最有力│
│ Layer 17: sep=0.47, acc=88% 候補 │
└─────────────────────────────────────┘
↓
Layer 16 を中心にWeight Kernelを設計
7.2 結果の保存形式
保存されるデータ(refusal_direction.pt):
{
'model': 'Qwen/Qwen2.5-0.5B-Instruct',
'num_layers': 24,
'directions': {
0: tensor([...]), # Layer 0 の拒否方向
1: tensor([...]), # Layer 1 の拒否方向
...
23: tensor([...]) # Layer 23 の拒否方向
},
'validation': {
0: {'separation': 0.02, 'accuracy': 0.53, 'is_valid': False},
...
16: {'separation': 0.48, 'accuracy': 0.90, 'is_valid': True},
...
}
}
→ このファイルをDay 20のAbliterationで使用
8. 今日のまとめ
実行チェックリスト
□ プロンプトファイルの確認
├── 有害: 30個以上、検閲されるもの
└── 無害: 30個以上、普通に回答されるもの
□ 計算スクリプトの実行
└── python scripts/calculate_refusal_direction.py ...
□ 結果の確認
├── Valid layersが全層の50%以上か
├── Best layerの分離度が0.3以上か
└── Best layerの精度が70%以上か
□ 問題があれば診断
├── 分離度が低い → プロンプトを見直し
└── 精度が低い → モデルの検閲パターンを分析
重要な数値の目安
| 指標 | 目標値 | 最低限 |
|---|---|---|
| 分離度(Separation) | 0.40+ | 0.20+ |
| 精度(Accuracy) | 80%+ | 65%+ |
| 有効な層の割合 | 70%+ | 50%+ |
明日の予告
Day 20: Abliterationの実行
- 拒否方向を使った重みの修正
- Weight Kernelの設定と適用
- 結果の即時確認
参考リンク
プロジェクト内リソース
- Day 14: 拒否方向の計算詳細 - 理論的背景
- scripts/calculate_refusal_direction.py - 完全な実装
- data/ - プロンプトファイル
ナビゲーション
| 前の記事 | Day 18: 検閲率テストの実行 |
| 次の記事 | Day 20: Abliterationの実行 |