MakiAi commited on
Commit
edd1061
·
2 Parent(s): eaab111 bdf1acf

Merge feature/remove-llm-evaluator

Browse files
Files changed (1) hide show
  1. sandbox/llm-evaluator-notebook (1).md +0 -469
sandbox/llm-evaluator-notebook (1).md DELETED
@@ -1,469 +0,0 @@
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
-