大規模言語モデル(LLM)は複雑な推論タスクが苦手とされてきましたが、Chain-of-Thought(CoT)プロンプティングにより、その能力を大幅に向上させることができます。
本記事では、LLMに段階的な思考プロセスを生成させるCoT推論の仕組みと実装方法を解説します。
本記事の内容
- Chain-of-Thoughtの基本概念
- Few-shot CoTとZero-shot CoT
- 数学的な分析と効果の検証
- Pythonでの実装例
前提知識
この記事を読む前に、以下の概念を理解しておくと役立ちます。
- Transformerアーキテクチャの基礎
- プロンプトエンジニアリングの基本
- LLMの推論の仕組み
LLMの推論における課題
直接回答の限界
従来のプロンプティングでは、LLMは質問に対して直接答えを出力します。
Q: ロジャーは5つのテニスボールを持っています。テニスボールの缶を2つ買いました。
各缶には3つのボールが入っています。彼は今いくつのボールを持っていますか?
A: 11
単純な問題では機能しますが、複雑な推論を要する問題では誤答が増えます。
なぜ複雑な推論が難しいのか
Transformerの性質上、各出力トークンは固定された計算量(レイヤー数)で生成されます。複雑な推論には、より多くの「計算ステップ」が必要です。
$$ \text{Complexity}(\text{Answer}) > \text{Fixed Computation per Token} $$
Chain-of-Thoughtの基本概念
アイデア
Chain-of-Thought(CoT)は、最終回答に至る中間的な推論ステップを明示的に生成させる手法です。
Q: ロジャーは5つのテニスボールを持っています。テニスボールの缶を2つ買いました。
各缶には3つのボールが入っています。彼は今いくつのボールを持っていますか?
A: ロジャーは最初に5つのボールを持っていました。
2缶買い、各缶に3つのボールがあるので、2×3=6つのボールを得ました。
合計は5+6=11です。
答えは11です。
なぜ効果があるのか
- 計算のオフロード: 中間ステップを生成することで、実質的な計算量が増加
- エラーの局所化: 各ステップでのエラーを特定しやすい
- 学習済み知識の活用: 推論パターンは事前学習で獲得済み
$$ P(\text{Answer}|x) = \sum_{\text{CoT}} P(\text{Answer}|\text{CoT}, x) \cdot P(\text{CoT}|x) $$
Few-shot Chain-of-Thought
アルゴリズム
Few-shot CoTでは、推論ステップを含む例をプロンプトに含めます。
Q: カフェに23人の客がいます。17人が帰り、新たに6人が来ました。カフェには何人いますか?
A: 最初に23人いました。17人帰ったので、23-17=6人残りました。
6人来たので、6+6=12人になりました。答えは12です。
Q: プレイグラウンドに15人の子どもがいます。8人が帰り、3人が来ました。何人いますか?
A: 最初に15人いました。8人帰ったので、15-8=7人残りました。
3人来たので、7+3=10人になりました。答えは10です。
Q: [新しい問題]
A:
数学的定式化
Few-shot CoTは、条件付き確率の連鎖則として表現できます。
推論チェーン $c = (c_1, c_2, \ldots, c_n)$ と最終回答 $a$ に対して:
$$
P(a, c | x, \text{examples}) = \prod_{i=1}^{n} P(c_i | c_{
Pythonでの実装
class FewShotCoT:
"""Few-shot Chain-of-Thought"""
def __init__(self, llm_client):
"""
Args:
llm_client: LLM APIクライアント
"""
self.llm_client = llm_client
self.examples = []
def add_example(self, question, reasoning, answer):
"""例を追加"""
self.examples.append({
'question': question,
'reasoning': reasoning,
'answer': answer
})
def format_example(self, example):
"""例をフォーマット"""
return f"""Q: {example['question']}
A: {example['reasoning']}
したがって、答えは{example['answer']}です。"""
def build_prompt(self, question):
"""プロンプトを構築"""
examples_text = "\n\n".join([
self.format_example(ex) for ex in self.examples
])
prompt = f"""{examples_text}
Q: {question}
A:"""
return prompt
def solve(self, question):
"""問題を解く"""
prompt = self.build_prompt(question)
response = self.llm_client.generate(prompt)
return response
# 使用例
cot = FewShotCoT(llm_client)
# 例を追加
cot.add_example(
question="カフェに23人の客がいます。17人が帰り、新たに6人が来ました。カフェには何人いますか?",
reasoning="最初に23人いました。17人帰ったので、23-17=6人残りました。6人来たので、6+6=12人になりました。",
answer="12"
)
cot.add_example(
question="プレイグラウンドに15人の子どもがいます。8人が帰り、3人が来ました。何人いますか?",
reasoning="最初に15人いました。8人帰ったので、15-8=7人残りました。3人来たので、7+3=10人になりました。",
answer="10"
)
# 新しい問題を解く
answer = cot.solve("教室に30人の生徒がいます。12人が帰り、5人が来ました。何人いますか?")
print(answer)
Zero-shot Chain-of-Thought
アイデア
Zero-shot CoTは、例を使わずに「Let’s think step by step」のようなトリガーフレーズだけでCoT推論を誘発します。
Q: ジョンは3つのリンゴを持っています。メアリーはジョンの2倍持っています。
2人合わせていくつのリンゴを持っていますか?
A: ステップバイステップで考えましょう。
1. ジョンは3つのリンゴを持っています。
2. メアリーはジョンの2倍、つまり3×2=6つ持っています。
3. 合計は3+6=9つです。
答えは9です。
効果的なトリガーフレーズ
| フレーズ | 効果 |
|---|---|
| Let’s think step by step | 最も効果的 |
| Let’s work this out in a step by step way | 効果的 |
| First, | 推論の開始を促す |
| Let’s break this down | 分解を促す |
Pythonでの実装
class ZeroShotCoT:
"""Zero-shot Chain-of-Thought"""
def __init__(self, llm_client):
self.llm_client = llm_client
self.trigger_phrase = "ステップバイステップで考えましょう。"
def solve(self, question, extract_answer=True):
"""
問題を解く
Args:
question: 質問
extract_answer: 最終回答を抽出するか
Returns:
reasoning: 推論プロセス
answer: 最終回答(extract_answer=Trueの場合)
"""
# Step 1: 推論を生成
reasoning_prompt = f"""Q: {question}
A: {self.trigger_phrase}"""
reasoning = self.llm_client.generate(reasoning_prompt)
if not extract_answer:
return reasoning
# Step 2: 回答を抽出
extraction_prompt = f"""Q: {question}
A: {self.trigger_phrase}
{reasoning}
したがって、答えは(数字のみで答えてください):"""
answer = self.llm_client.generate(extraction_prompt)
return reasoning, answer
# 使用例
zero_shot_cot = ZeroShotCoT(llm_client)
question = "店に50個のリンゴがあります。30個売れ、20個入荷しました。いくつありますか?"
reasoning, answer = zero_shot_cot.solve(question)
print("推論プロセス:")
print(reasoning)
print(f"\n答え: {answer}")
Self-Consistency
アルゴリズム
Self-Consistencyは、複数の推論パスを生成し、多数決で最終回答を決定する手法です。
$$ \text{Answer}^* = \arg\max_a \sum_{i=1}^{n} \mathbb{1}[\text{Answer}_i = a] $$
数学的直観
複数の独立した推論パスが同じ答えに到達する場合、その答えの信頼性は高いと考えられます。
$$ P(\text{Answer correct}) \approx 1 – (1 – p)^n $$
ここで $p$ は単一パスでの正答確率、$n$ はパス数です。
Pythonでの実装
import numpy as np
from collections import Counter
class SelfConsistencyCoT:
"""Self-Consistency with Chain-of-Thought"""
def __init__(self, llm_client, n_paths=5, temperature=0.7):
"""
Args:
llm_client: LLM APIクライアント
n_paths: 生成する推論パスの数
temperature: サンプリング温度
"""
self.llm_client = llm_client
self.n_paths = n_paths
self.temperature = temperature
def extract_answer(self, reasoning):
"""推論から数値回答を抽出"""
# 簡易的な数値抽出(実際にはより堅牢な方法が必要)
import re
numbers = re.findall(r'\d+', reasoning)
if numbers:
return numbers[-1] # 最後の数値を回答とする
return None
def solve(self, question, examples=None):
"""
Self-Consistencyで問題を解く
Args:
question: 質問
examples: Few-shot例(オプション)
Returns:
final_answer: 多数決による最終回答
confidence: 信頼度
all_answers: 全ての回答
"""
# プロンプトを構築
if examples:
examples_text = "\n\n".join([
f"Q: {ex['question']}\nA: {ex['reasoning']}\n答え: {ex['answer']}"
for ex in examples
])
prompt = f"{examples_text}\n\nQ: {question}\nA: ステップバイステップで考えましょう。"
else:
prompt = f"Q: {question}\nA: ステップバイステップで考えましょう。"
# 複数の推論パスを生成
answers = []
reasonings = []
for _ in range(self.n_paths):
reasoning = self.llm_client.generate(
prompt,
temperature=self.temperature
)
answer = self.extract_answer(reasoning)
if answer is not None:
answers.append(answer)
reasonings.append(reasoning)
# 多数決
if not answers:
return None, 0.0, []
answer_counts = Counter(answers)
final_answer, count = answer_counts.most_common(1)[0]
confidence = count / len(answers)
return final_answer, confidence, answers
# 使用例
sc_cot = SelfConsistencyCoT(llm_client, n_paths=5)
question = "農場に羊が17匹いました。5匹売り、12匹買いました。今何匹いますか?"
final_answer, confidence, all_answers = sc_cot.solve(question)
print(f"全ての回答: {all_answers}")
print(f"最終回答: {final_answer}")
print(f"信頼度: {confidence:.2f}")
Tree of Thoughts(ToT)
アルゴリズム
Tree of Thoughts(ToT)は、推論を木構造で探索する拡張版CoTです。
- 複数の思考ステップを生成
- 各ステップを評価
- 有望なパスを選択して展開
- 最終解に到達するまで繰り返し
探索アルゴリズム
幅優先探索(BFS):
class TreeOfThoughts:
"""Tree of Thoughts"""
def __init__(self, llm_client, n_branches=3, max_depth=5):
self.llm_client = llm_client
self.n_branches = n_branches
self.max_depth = max_depth
def generate_thoughts(self, state, question):
"""現在の状態から複数の思考を生成"""
prompt = f"""問題: {question}
これまでの思考:
{state}
次に考えられるステップを{self.n_branches}つ提案してください:"""
thoughts = []
for i in range(self.n_branches):
thought = self.llm_client.generate(prompt, temperature=0.8)
thoughts.append(thought)
return thoughts
def evaluate_thought(self, state, thought, question):
"""思考の有望さを評価"""
prompt = f"""問題: {question}
これまでの思考:
{state}
新しいステップ: {thought}
このステップは問題解決に向けて正しい方向ですか?
1(悪い)から10(良い)で評価してください:"""
score_text = self.llm_client.generate(prompt)
# スコアを抽出
try:
score = int(''.join(filter(str.isdigit, score_text[:5])))
return min(10, max(1, score))
except:
return 5
def is_solution(self, state, question):
"""解に到達したか確認"""
prompt = f"""問題: {question}
思考プロセス:
{state}
この思考プロセスで問題は解決しましたか?「はい」または「いいえ」で答えてください:"""
response = self.llm_client.generate(prompt)
return "はい" in response
def solve_bfs(self, question):
"""BFSで解を探索"""
queue = [("", 0)] # (state, depth)
best_solution = None
while queue:
state, depth = queue.pop(0)
if depth >= self.max_depth:
continue
if self.is_solution(state, question):
return state
# 思考を生成
thoughts = self.generate_thoughts(state, question)
# 評価してソート
scored_thoughts = [
(t, self.evaluate_thought(state, t, question))
for t in thoughts
]
scored_thoughts.sort(key=lambda x: -x[1])
# 上位を展開
for thought, score in scored_thoughts[:2]:
new_state = state + "\n" + thought if state else thought
queue.append((new_state, depth + 1))
return best_solution
# 使用例
# tot = TreeOfThoughts(llm_client)
# solution = tot.solve_bfs("24ゲーム: 4,5,6,7を使って24を作ってください")
CoTの性能分析
効果的なタスク
CoTが特に効果的なタスク:
| タスク | 改善幅 |
|---|---|
| 算術推論 | +40-60% |
| 常識推論 | +20-30% |
| 記号推論 | +30-50% |
| 複数ステップQA | +20-40% |
モデルサイズの影響
CoTの効果はモデルサイズに依存します。
$$ \text{CoT Improvement} \propto \log(\text{Model Size}) $$
約100B パラメータ以上のモデルで顕著な効果が見られます(Emergent Ability)。
計算コスト
CoTは出力トークン数を増加させるため、推論コストが増加します。
$$ \text{Cost}_{\text{CoT}} = \text{Cost}_{\text{Direct}} \times \frac{|\text{CoT}| + |\text{Answer}|}{|\text{Answer}|} $$
まとめ
本記事では、Chain-of-Thought推論の仕組みと実装を解説しました。
- Few-shot CoT: 推論例を含むプロンプトでCoTを誘発
- Zero-shot CoT: 「Let’s think step by step」でCoTを誘発
- Self-Consistency: 複数パスの多数決で精度向上
- Tree of Thoughts: 木構造探索による発展版
次のステップとして、以下の記事も参考にしてください。