MakiAi commited on
Commit
7512c36
·
1 Parent(s): 4161346

🎉 feat: LLM評価システムの実装

Browse files

- LLMの回答品質を自動的に評価するシステムを実装しました。
- 質問、模範解答、LLMの回答を比較し、4段階のスケールで評価する機能を実装しました。
- エラーハンドリングとリトライ機能、ログ機能、カスタマイズ可能な評価基準、多様な出力フォーマット(CSV, HTML)などを備えています。
- モジュール化された設計で、異なるLLMモデルにも対応可能です。
- Google Colab環境での実行を想定し、Gemini API Keyの使用を前提としています。
- 詳細なドキュメントと使用方法を記載したREADMEファイルを含んでいます。
- CSVとHTML形式での評価レポート生成機能を追加しました。
- HuggingFace Hubへのアップロード機能を追加しました。

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