⚗️
DeepSeekがやっていると噂の蒸留というやつを、ローカルLLMで今すぐやってみる 前編
前提
この記事では、「Symbolic Knowledge Distillation: from General Language Models to Commonsense Models」という論文で提案されている 記号知識蒸留 を、ローカルLLMで日本語で実験する。
詳細
- 知識蒸留 (Knowledge Distillation) とは、大きなモデル (教師) から小さなモデル (生徒) に知識を転送する手法である
- 具体的には、LLMの蒸留と言えば、大きなモデルが出力する確率分布(ソフトターゲット)を利用して、小さいモデルを学習させる手法が用いられていた
- しかし、本論文では、「象徴的」な知識蒸留として、単に数値的な蒸留ではなく、 テキスト (symbolic knowledge) の形で知識を抽出し転送すること を提案している
必要な知識と開発環境
- ollamaとPythonとLangChainをセットアップする知識があること
- phi4:14b が快適に動くGPU (VRAM 12GB程度)
ゴール
- LLMにプロンプトを与えて、大量の常識知識を自動生成する
- その常識知識を、小型のLLMに、継続事前学習によって伝える
実装ステップ
記号知識蒸留 の実装ステップは以下に分けることができる:
- イベント生成
- イベントに対する因果関係推論生成
- Critic モデルを用いた生成結果のフィルタリング
- フィルタリング結果を用いた小型モデルの継続事前学習
この記事では、小型モデルの継続事前学習の直前まで、常識知識グラフの構築までを扱う。
以下にそれぞれを説明する。
イベント生成の実装
以下のように実装する:
from langchain_ollama.llms import OllamaLLM
model = OllamaLLM(model="phi4:14b", temperature=0.8)
messages = [
("system", """あなたは日本語の常識知識を生成するAIです。"""),
("human", """以下の形式で、人間が関与するイベントを10個生成してください。
### ルール
- 各イベントは短い日本語の文にすること (7~15文字程度)
- 主語は "X" にする (例: X が本を読む)
- 日常的にありふれた出来事を考えること
- 可能な限りバリエーションを持たせること
### 例
1. X が朝の散歩をする
2. X がコーヒーを飲む
3. X が雨の日に傘を忘れる
4. X が映画を観る
5. X がスーパーで買い物をする
### 出力
Markdownの-を使った順序なしリスト形式で、新たな10個のイベントを生成してください。
"""),
]
result = model.invoke(messages)
print(result)
これによって、以下のような出力が得られる:
- X が電車に乗る
- X がスマホを見る
- X が料理を作る
- X が友達と話す
- X が道を歩く
- X が仕事をする
- X がテレビを観る
- X が音楽を聴く
- X が昼寝をする
- X が日記を書く
実際には、このステップを、ものすごい回数繰り返す
これを以下のように、文字列の配列にしておく:
import re
result_content = result.content
events = [event.strip() for event in re.split(r'\- ?', result_content) if event.strip()]
model, eventsは次のステップでも使う。
イベントに対する因果関係推論生成の実装
model, eventsは前のステップから引き継がれている。
results = []
for event in events:
prompt = f"""
以下のイベントについて、因果関係を持つ推論を生成してください。
### ルール
- 各関係 (relation) に対応する推論を作成してください。
- 出力はシンプルで自然な日本語の文章にしてください (10~20文字程度)。
- 可能な限り現実的な推論を生成してください。
### 関係の説明
- xEffect: X の行動の結果 (例: 「X が雨の日に傘を忘れる」→「X が濡れる」)
- xWant: X の行動後に X が望むこと (例: 「X がジョギングを始める」→「X は水を飲みたい」)
- xNeed: X の行動をするために必要なこと (例: 「X が料理をする」→「X は食材を用意する必要がある」)
- xIntent: X の行動の意図 (例: 「X が友人と電話する」→「X は近況を知りたい」)
- xReact: X の行動の後の感情反応 (例: 「X が財布をなくす」→「X はパニックになる」)
- HinderedBy: X の行動が妨げられる要因 (例: 「X がランニングする」→「雨が降るとできない」)
HinderedBy以外では主語を明確にしてください。
イベント: {event}
### 出力フォーマット
- xEvent: {event}
- xEffect: xxx
- xWant: xxx
- xNeed: xxx
- xIntent: xxx
- xReact: xxx
- HinderedBy: xxx
出力フォーマットには厳密に従ってください。出力フォーマットにない余計な出力は絶対に含めないようにしてください。
"""
messages = [
("system", """あなたは日本語の常識知識を生成するAIです。"""),
("human", prompt),
]
result = model.invoke(messages)
results.append(result)
ここで、resultsには以下のような大量の文字列が格納されている。
- xEvent: X が電車に乗る
- xEffect: X が目的地に着く
- xWant: X は座りたい
- xNeed: X は切符が必要
- xIntent: X は移動したい
- xReact: X は安心する
- HinderedBy: 電車が遅れる
- xEvent: X がスマホを見る
- xEffect: X の目が疲れる
- xWant: X は休憩したい
- xNeed: X はスマホを持つ
- xIntent: X は情報を得たい
- xReact: X は面白いと感じる
...
これを event
, relation
, inference
の トリプル として整形する。
event_relation_inference = []
for result in results:
lines = result.split("\n")
for line in lines:
if line.startswith('- xEvent: '):
event = line[len('- xEvent: '):]
else:
relation = line.split(': ')[0].split(' ')[1]
inference = line.split(': ')[1]
event_relation_inference.append((event, relation, inference))
ここで event_relation_inference
は以下のようになっている:
[('X が電車に乗る', 'xEffect', 'X が目的地に着く'),
('X が電車に乗る', 'xWant', 'X は座りたい'),
('X が電車に乗る', 'xNeed', 'X は切符が必要'),
('X が電車に乗る', 'xIntent', 'X は移動したい'),
('X が電車に乗る', 'xReact', 'X は安心する'),
('X が電車に乗る', 'HinderedBy', '電車が遅れる'),
('X がスマホを見る', 'xEffect', 'X の目が疲れる'),
('X がスマホを見る', 'xWant', 'X は休憩したい'),
('X がスマホを見る', 'xNeed', 'X はスマホを持つ'),
('X がスマホを見る', 'xIntent', 'X は情報を得たい'),
('X がスマホを見る', 'xReact', 'X は面白いと感じる'),
...]
Critic モデルを用いた生成結果のフィルタリングの実装
Critic モデルとは、上記までの過程で生成された出力内容が本当に常識として正しいのかを検証するためのモデルである。
ここでは phi4:14b を使うが、 temperature=0.0, max_tokens=20
と指定していることには注意されたい。
filtered_results = []
critic_model = model = OllamaLLM(model="phi4:14b", temperature=0.0, max_tokens=20)
for event, relation, inference in event_relation_inference:
prompt = f"""
あなたは日本語の常識知識を生成するAIです。
以下のイベントと推論が適切かどうかを評価してください。
### チェック基準:
1. **論理的一貫性** - イベントと関係 (relation) に対して適切な推論か
2. **常識的な妥当性** - 現実世界の知識と矛盾しないか
3. **情報の具体性** - 推論が単純すぎず、具体的な知識を含んでいるか
4. **不要な曖昧さがないか** - 「~かもしれない」など過度に曖昧な表現を含まないか
5. **誤解を招く表現がないか** - 意図しない意味の解釈を誘発しないか
### 評価対象:
イベント: {event}
関係: {relation}
推論: {inference}
適切な場合は "Yes"、不適切な場合は "No" と出力してください。
"""
messages = [
("human", prompt),
]
result = critic_model.invoke(messages)
if result.content == "Yes":
filtered_results.append((event, relation, inference))
これで filtered_results
は以下のようになっている:
[('X が電車に乗る', 'xEffect', 'X が目的地に着く'),
('X が電車に乗る', 'xWant', 'X は座りたい'),
('X が電車に乗る', 'xNeed', 'X は切符が必要'),
('X が電車に乗る', 'xIntent', 'X は移動したい'),
('X が電車に乗る', 'xReact', 'X は安心する'),
('X がスマホを見る', 'xEffect', 'X の目が疲れる'),
('X がスマホを見る', 'xNeed', 'X はスマホを持つ'),
('X がスマホを見る', 'xIntent', 'X は情報を得たい'),
...]
以上の実装によって、 phi4:14b から、大量の日本語常識知識グラフを生成することができた。
後編
phi4:14bが生成した大量の日本語常識知識グラフを、 qwen2.5:1.5b に継続事前学習する
Discussion