MakiAi commited on
Commit
75c17b1
·
1 Parent(s): edd1061

✨ feat: LLM評価システムとレポート生成機能を追加

Browse files

- LLMの回答品質を自動評価するシステムを実装しました。
- 質問、模範解答、LLM回答を比較し、4段階評価(1〜4点)を行います。
- `litellm`, `tqdm`, `loguru`ライブラリを使用し、エラー処理と進捗表示、ログ出力機能を追加しました。
- 評価結果をJSONファイルとCSVファイル、HTMLレポートとして出力する機能を追加しました。
- 主要な機能をクラス(LLMEvaluator, EvaluationPrompts, EvaluationParser, ResultExporter, ReportGenerator)に分割し、可読性と保守性を向上させました。
- リトライデコレータ(`retry_on_error`)を実装し、API呼び出し時のエラーに柔軟に対応します。
- プロンプト管理、結果解析、データエクスポート、レポート生成といった機能を個別のクラスにモジュール化しました。
- HTMLレポートにはサマリー、スコア分布、詳細結果を含みます。
- Jupyter Notebook形式で記述されており、Google Colab環境での実行を想定しています。
- 使用するには、`GEMINI_API_KEY`環境変数にGemini APIキーを設定する必要があります。

Files changed (1) hide show
  1. sandbox/llm-evaluator-notebook.md +469 -0
sandbox/llm-evaluator-notebook.md ADDED
@@ -0,0 +1,469 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LLM評価システム実装ガイド
2
+
3
+ ## はじめに
4
+
5
+ このノートブックでは、LLM(大規模言語モデル)の回答品質を自動的に評価するためのシステムを実装します。このシステムは、質問、模範解答、LLMの回答を比較し、4段階のスケールで評価を行います。
6
+
7
+ ### 目的
8
+ - LLMの回答品質を定量的に評価する
9
+ - 評価プロセスを自動化し、大規模なデータセットの処理を可能にする
10
+ - 評価結果を分析可能な形式で出力する
11
+
12
+
13
+ ### 評価基準
14
+ システムは以下の4段階スケールで評価を行います:
15
+ - **4点**: 優れた回答(完全で詳細な回答)
16
+ - **3点**: おおむね役立つ回答(改善の余地あり)
17
+ - **2点**: あまり役に立たない回答(重要な側面の欠落)
18
+ - **1点**: 全く役に立たない回答(無関係または不十分)
19
+
20
+ ### 必要要件
21
+ - Python 3.7以上
22
+ - Google Colab環境
23
+ - Gemini API Key
24
+ - 評価対象のQAデータセット(JSON形式)
25
+
26
+ それでは、実装の詳細に進みましょう。
27
+
28
+ ## 1. 環境セットアップ
29
+
30
+ 必要なライブラリをインストールします。
31
+
32
+ ```python
33
+ !pip install litellm tqdm loguru
34
+ ```
35
+
36
+ 必要なライブラリをインポートします。
37
+
38
+ ```python
39
+ import json
40
+ import pandas as pd
41
+ from litellm import completion
42
+ import os
43
+ from tqdm import tqdm
44
+ import time
45
+ from google.colab import userdata
46
+ from loguru import logger
47
+ import sys
48
+ from functools import wraps
49
+ ```
50
+
51
+ ## 2. ユーティリティ関数の実装
52
+
53
+ ### 2.1 リトライデコレータの実装
54
+
55
+ エラーハンドリングとリトライ機能を提供するデコレータクラスを実装します。
56
+
57
+ ```python
58
+ def retry_on_error(max_retries=5, wait_time=30):
59
+ """
60
+ 関数実行時のエラーを処理し、指定回数リトライするデコレータ
61
+
62
+ Args:
63
+ max_retries (int): 最大リトライ回数
64
+ wait_time (int): リトライ間隔(秒)
65
+
66
+ Returns:
67
+ function: デコレートされた関数
68
+ """
69
+ def decorator(func):
70
+ @wraps(func)
71
+ def wrapper(*args, **kwargs):
72
+ for attempt in range(max_retries):
73
+ try:
74
+ return func(*args, **kwargs)
75
+ except Exception as e:
76
+ if attempt == max_retries - 1:
77
+ logger.error(f"最大リトライ回数に達しました: {str(e)}")
78
+ raise
79
+ logger.warning(f"エラーが発生しました。{wait_time}秒後にリトライします。(試行 {attempt + 1}/{max_retries}): {str(e)}")
80
+ time.sleep(wait_time)
81
+ return None
82
+ return wrapper
83
+ return decorator
84
+ ```
85
+
86
+ ## 3. 評価システムのコアコンポーネント
87
+
88
+ ### 3.1 プロンプト管理クラス
89
+
90
+ 評価に使用するプロンプトを管理するクラスを実装します。
91
+
92
+ ```python
93
+ class EvaluationPrompts:
94
+ """評価プロンプトを管理するクラス"""
95
+
96
+ @staticmethod
97
+ def get_judge_prompt():
98
+ return """
99
+ あなたはLLMの回答を評価する審査員です。
100
+ 質問と模範解答、そしてLLMの回答のセットを評価してください。
101
+
102
+ 評価は1から4の整数スケールで行ってください:
103
+ 1: 全く役に立たない回答:質問に対して無関係か、部分的すぎる
104
+ 2: あまり役に立たない回答:質問の重要な側面を見落としている
105
+ 3: おおむね役立つ回答:支援を提供しているが、改善の余地がある
106
+ 4: 優れた回答:関連性があり、直接的で、詳細で、質問で提起されたすべての懸念に対応している
107
+
108
+ 以下のフォーマットで評価を提供してください:
109
+
110
+ Feedback:::
111
+ 評価理由: (評価の根拠を説明してください)
112
+ 総合評価: (1から4の整数で評価してください)
113
+
114
+ これから質問、模範解答、LLMの回答を提示します:
115
+
116
+ 質問: {question}
117
+ 模範解答: {correct_answer}
118
+ LLMの回答: {llm_answer}
119
+
120
+ フィードバックをお願いします。
121
+ Feedback:::
122
+ 評価理由: """
123
+ ```
124
+
125
+ ### 3.2 評価結果パーサークラス
126
+
127
+ ```python
128
+ class EvaluationParser:
129
+ """評価結果を解析するクラス"""
130
+
131
+ @staticmethod
132
+ def extract_score(response_text):
133
+ """
134
+ 評価テキストからスコアを抽出する
135
+
136
+ Args:
137
+ response_text (str): 評価テキスト
138
+
139
+ Returns:
140
+ int or None: 抽出されたスコア
141
+ """
142
+ try:
143
+ score_text = response_text.split("総合評価:")[1].strip()
144
+ score = int(score_text.split()[0])
145
+ return score
146
+ except:
147
+ logger.error(f"スコア抽出に失敗しました: {response_text}")
148
+ return None
149
+
150
+ @staticmethod
151
+ def extract_reason(evaluation_text):
152
+ """
153
+ 評価テキストから評価理由を抽出する
154
+
155
+ Args:
156
+ evaluation_text (str): 評価テキスト
157
+
158
+ Returns:
159
+ str: 抽出された評価理由
160
+ """
161
+ try:
162
+ reason = evaluation_text.split("評価理由:")[1].split("総合評価:")[0].strip()
163
+ return reason
164
+ except:
165
+ logger.warning("評価理由の抽出に失敗しました")
166
+ return ""
167
+ ```
168
+
169
+ ### 3.3 LLM評価クラス
170
+
171
+ ```python
172
+ class LLMEvaluator:
173
+ """LLMの回答を評価するメインクラス"""
174
+
175
+ def __init__(self, model_name="gemini/gemini-pro"):
176
+ """
177
+ 評価器を初期化する
178
+
179
+ Args:
180
+ model_name (str): 使用するLLMモデル名
181
+ """
182
+ self.model_name = model_name
183
+ self.prompts = EvaluationPrompts()
184
+ self.parser = EvaluationParser()
185
+ logger.info(f"評価器を初期化しました。使用モデル: {model_name}")
186
+
187
+ @retry_on_error()
188
+ def evaluate_response(self, question, correct_answer, llm_answer):
189
+ """
190
+ 個々の回答を評価する
191
+
192
+ Args:
193
+ question (str): 質問
194
+ correct_answer (str): 模範解答
195
+ llm_answer (str): LLMの回答
196
+
197
+ Returns:
198
+ dict: 評価結果
199
+ """
200
+ prompt = self.prompts.get_judge_prompt().format(
201
+ question=question,
202
+ correct_answer=correct_answer,
203
+ llm_answer=llm_answer
204
+ )
205
+
206
+ try:
207
+ response = completion(
208
+ model=self.model_name,
209
+ messages=[{"role": "user", "content": prompt}]
210
+ )
211
+ evaluation = response.choices[0].message.content
212
+ score = self.parser.extract_score(evaluation)
213
+
214
+ if score:
215
+ logger.debug(f"評価完了 - スコア: {score}")
216
+
217
+ return {
218
+ 'score': score,
219
+ 'evaluation': evaluation
220
+ }
221
+ except Exception as e:
222
+ logger.error(f"評価中にエラーが発生しました: {str(e)}")
223
+ raise
224
+
225
+ @retry_on_error()
226
+ def evaluate_dataset(self, json_file_path, output_file="evaluation_results.json"):
227
+ """
228
+ データセット全体を評価する
229
+
230
+ Args:
231
+ json_file_path (str): 評価対象のJSONファイルパス
232
+ output_file (str): 評価結果の出力先ファイルパス
233
+
234
+ Returns:
235
+ dict: 評価結果と分析データを含む辞書
236
+ """
237
+ logger.info(f"データセット評価を開始します: {json_file_path}")
238
+
239
+ with open(json_file_path, 'r', encoding='utf-8') as f:
240
+ data = json.load(f)
241
+
242
+ results = []
243
+ qa_pairs = data['qa_pairs_simple']
244
+ total_pairs = len(qa_pairs)
245
+
246
+ logger.info(f"合計 {total_pairs} 件のQAペアを評価します")
247
+
248
+ progress_bar = tqdm(qa_pairs, desc="評価進捗", unit="件")
249
+ for qa in progress_bar:
250
+ eval_result = self.evaluate_response(
251
+ qa['question'],
252
+ qa['correct_answer'],
253
+ qa['llm_answer']
254
+ )
255
+
256
+ if eval_result:
257
+ results.append({
258
+ 'question': qa['question'],
259
+ 'correct_answer': qa['correct_answer'],
260
+ 'llm_answer': qa['llm_answer'],
261
+ 'score': eval_result['score'],
262
+ 'evaluation': eval_result['evaluation']
263
+ })
264
+
265
+ # 進捗状況を更新
266
+ progress_bar.set_postfix(
267
+ completed=f"{len(results)}/{total_pairs}",
268
+ last_score=eval_result['score']
269
+ )
270
+
271
+ time.sleep(1) # API制限考慮
272
+
273
+ # 結果を分析
274
+ scores = [r['score'] for r in results if r['score'] is not None]
275
+ analysis = {
276
+ 'total_evaluations': len(results),
277
+ 'average_score': sum(scores) / len(scores) if scores else 0,
278
+ 'score_distribution': {
279
+ '1': scores.count(1),
280
+ '2': scores.count(2),
281
+ '3': scores.count(3),
282
+ '4': scores.count(4)
283
+ }
284
+ }
285
+
286
+ # 分析結果をログに出力
287
+ logger.success("評価が完了しました")
288
+ logger.info(f"総評価数: {analysis['total_evaluations']}")
289
+ logger.info(f"平均スコア: {analysis['average_score']:.2f}")
290
+ logger.info("スコア分布:")
291
+ for score, count in analysis['score_distribution'].items():
292
+ percentage = (count / len(scores) * 100) if scores else 0
293
+ logger.info(f"スコア {score}: {count}件 ({percentage:.1f}%)")
294
+
295
+ # 結果をJSONとして保存
296
+ output_data = {
297
+ 'analysis': analysis,
298
+ 'detailed_results': results
299
+ }
300
+
301
+ with open(output_file, 'w', encoding='utf-8') as f:
302
+ json.dump(output_data, f, ensure_ascii=False, indent=2)
303
+
304
+ logger.info(f"評価結果を保存しました: {output_file}")
305
+ return output_data
306
+ ```
307
+
308
+ ### 3.4 データエクスポートクラス
309
+
310
+ ```python
311
+ class ResultExporter:
312
+ """評価結果をエクスポートするクラス"""
313
+
314
+ @staticmethod
315
+ def export_to_csv(evaluation_results, output_file="evaluation_results.csv"):
316
+ """
317
+ 評価結果をCSVファイルに出力する
318
+
319
+ Args:
320
+ evaluation_results (dict): 評価結果
321
+ output_file (str): 出力ファイルパス
322
+
323
+ Returns:
324
+ pd.DataFrame: 出力したデータフレーム
325
+ """
326
+ logger.info("CSV出力を開始します")
327
+ results = evaluation_results['detailed_results']
328
+ parser = EvaluationParser()
329
+
330
+ csv_data = []
331
+ for result in results:
332
+ csv_data.append({
333
+ '質問': result['question'],
334
+ '正解': result['correct_answer'],
335
+ 'LLMの回答': result['llm_answer'],
336
+ '評価理由': parser.extract_reason(result['evaluation']),
337
+ '総合評価': result['score']
338
+ })
339
+
340
+ df = pd.DataFrame(csv_data)
341
+ df.to_csv(output_file, index=False, encoding='utf-8-sig')
342
+
343
+ logger.success(f"CSVファイルを出力しました: {output_file}")
344
+ return df
345
+ ```
346
+
347
+ ### 3.5 レポート生成クラス
348
+
349
+ ```python
350
+ class ReportGenerator:
351
+ """評価レポートを生成するクラス"""
352
+
353
+ @staticmethod
354
+ def generate_html_report(evaluation_results, model_name, output_file="evaluation_report.html"):
355
+ """
356
+ HTML形式の評価レポートを生成する
357
+
358
+ Args:
359
+ evaluation_results (dict): 評価結果
360
+ model_name (str): 評価に使用したモデル名
361
+ output_file (str): 出力ファイルパス
362
+ """
363
+ logger.info("HTMLレポート生成を開始します")
364
+
365
+ analysis = evaluation_results['analysis']
366
+ results = evaluation_results['detailed_results']
367
+ df = pd.DataFrame(results)
368
+
369
+ html_content = f"""
370
+ <html>
371
+ <head>
372
+ <title>LLM Evaluation Report</title>
373
+ <style>
374
+ body {{ font-family: Arial, sans-serif; margin: 20px; }}
375
+ .summary {{ background-color: #f0f0f0; padding: 20px; margin-bottom: 20px; }}
376
+ .distribution {{ margin-bottom: 20px; }}
377
+ table {{ border-collapse: collapse; width: 100%; }}
378
+ th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
379
+ th {{ background-color: #4CAF50; color: white; }}
380
+ tr:nth-child(even) {{ background-color: #f2f2f2; }}
381
+ </style>
382
+ </head>
383
+ <body>
384
+ <h1>LLM Evaluation Report</h1>
385
+
386
+ <div class="summary">
387
+ <h2>Summary</h2>
388
+ <p>Total Evaluations: {analysis['total_evaluations']}</p>
389
+ <p>Average Score: {analysis['average_score']:.2f}</p>
390
+ <p>Model: {model_name}</p>
391
+ </div>
392
+
393
+ <div class="distribution">
394
+ <h2>Score Distribution</h2>
395
+ <table>
396
+ <tr>
397
+ <th>Score</th>
398
+ <th>Count</th>
399
+ <th>Percentage</th>
400
+ </tr>
401
+ {''.join(f'<tr><td>{score}</td><td>{count}</td><td>{(count/analysis["total_evaluations"]*100):.1f}%</td></tr>'
402
+ for score, count in analysis['score_distribution'].items())}
403
+ </table>
404
+ </div>
405
+
406
+ <div class="details">
407
+ <h2>Detailed Results</h2>
408
+ {df.to_html()}
409
+ </div>
410
+ </body>
411
+ </html>
412
+ """
413
+
414
+ with open(output_file, 'w', encoding='utf-8') as f:
415
+ f.write(html_content)
416
+
417
+ logger.success(f"HTMLレポートを生成しました: {output_file}")
418
+ ```
419
+
420
+ ## 4. メイン実行部分
421
+
422
+ ```python
423
+ def main():
424
+ # APIキーの設定
425
+ os.environ['GEMINI_API_KEY'] = userdata.get('GEMINI_API_KEY')
426
+
427
+ # 評価器の初期化
428
+ evaluator = LLMEvaluator(model_name="gemini/gemini-1.5-flash-latest")
429
+
430
+ try:
431
+ # データセットを評価
432
+ logger.info("評価プロセスを開始します")
433
+ results = evaluator.evaluate_dataset("qa_with_llm.json")
434
+
435
+ # 結果のエクスポート
436
+ exporter = ResultExporter()
437
+ df = exporter.export_to_csv(results)
438
+ logger.info("最初の数行のデ���タ:")
439
+ logger.info("\n" + str(df.head()))
440
+
441
+ # レポート生成
442
+ report_generator = ReportGenerator()
443
+ report_generator.generate_html_report(results, evaluator.model_name)
444
+ logger.success("すべての処理が完了しました")
445
+
446
+ except Exception as e:
447
+ logger.error(f"処理中にエラーが発生しました: {str(e)}")
448
+ raise
449
+
450
+ if __name__ == "__main__":
451
+ main()
452
+ ```
453
+
454
+ ## 5. 使用方法
455
+
456
+ 1. Google Colabで新しいノートブックを作成します。
457
+ 2. 必要なライブラリをインストールします。
458
+ 3. 上記のコードを順番にセルにコピーして実行します。
459
+ 4. GEMINI_API_KEYを設定します。
460
+ 5. 評価したいQAデータセットのJSONファイルを用意します。
461
+ 6. メイン実行部分を実行します。
462
+
463
+ ## 6. 注意点
464
+
465
+ - 評価には時間がかかる場合があります。
466
+ - API制限に注意してください。
467
+ - データセットは指定のJSON形式に従う必要があります。
468
+ - エラー発生時は自動的にリトライします。
469
+