MakiAi commited on
Commit
ccb34dc
·
2 Parent(s): a5130fa 4f511aa

Merge feature/context-aware-qa

Browse files
README.md CHANGED
@@ -86,6 +86,7 @@ license: mit
86
  - GeminiとLiteLLMを使用した効率的な評価システム
87
  - [📒ノートブックはこちら](https://colab.research.google.com/drive/1haO44IeseQ3OL92HlsINAgBI_yA1fxcJ?usp=sharing)
88
 
 
89
  ### WikipediaデータからのQ&Aデータセット生成(センテンスプールQA方式)
90
  - センテンスプールQA方式による高品質Q&Aデータセット生成
91
  - → 句点区切りの文をプールして文脈を保持しながらQ&Aペアを生成する新しいデータセット作成手法
@@ -93,6 +94,14 @@ license: mit
93
  - → 詳細は [`wikipedia-qa-dataset-generator.md`](sandbox/wikipedia-qa-dataset-generator.md) をご参照ください。
94
  - [📒ノートブックはこちら](https://colab.research.google.com/drive/1mmK5vxUzjk3lI6OnEPrQqyjSzqsEoXpk?usp=sharing)
95
 
 
 
 
 
 
 
 
 
96
  ## 🛠️ 環境構築
97
 
98
  1. リポジトリのクローン:
 
86
  - GeminiとLiteLLMを使用した効率的な評価システム
87
  - [📒ノートブックはこちら](https://colab.research.google.com/drive/1haO44IeseQ3OL92HlsINAgBI_yA1fxcJ?usp=sharing)
88
 
89
+
90
  ### WikipediaデータからのQ&Aデータセット生成(センテンスプールQA方式)
91
  - センテンスプールQA方式による高品質Q&Aデータセット生成
92
  - → 句点区切りの文をプールして文脈を保持しながらQ&Aペアを生成する新しいデータセット作成手法
 
94
  - → 詳細は [`wikipedia-qa-dataset-generator.md`](sandbox/wikipedia-qa-dataset-generator.md) をご参照ください。
95
  - [📒ノートブックはこちら](https://colab.research.google.com/drive/1mmK5vxUzjk3lI6OnEPrQqyjSzqsEoXpk?usp=sharing)
96
 
97
+ ### コンテキストアウェアリフレクティブQA生成システム
98
+ - リフレクティブな品質改善を行うQ&Aデータセット生成
99
+ - → 生成したQ&Aペアの品質を自動評価し、段階的に改善を行う新方式
100
+ - → 事実性、質問品質、回答の完全性を数値化して評価
101
+ - → 文脈情報を活用した高精度な質問生成と回答の整合性チェック
102
+ - → 詳細は [`context_aware_Reflexive_qa_generator_V2.md`](sandbox/context_aware_Reflexive_qa_generator_V2.md) をご参照ください。
103
+ - [📒ノートブックはこちら](https://colab.research.google.com/drive/1OYdgAuXHbl-0LUJgkLl_VqknaAEmAm0S?usp=sharing)
104
+
105
  ## 🛠️ 環境構築
106
 
107
  1. リポジトリのクローン:
sandbox/context_aware_Reflexive_qa_generator_V2.md ADDED
@@ -0,0 +1,1270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # コンテキストアウェアQA生成システム
2
+
3
+ このノートブックでは、Wikipediaの記事から高品質なQ&Aデータセットを生成するシステムを実装します。
4
+
5
+ ## 1. 環境セットップ
6
+
7
+ ```python
8
+ !curl https://ollama.ai/install.sh | sh
9
+
10
+ !echo 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections
11
+ !sudo apt-get update && sudo apt-get install -y cuda-drivers
12
+ ```
13
+
14
+ ```python
15
+ !nohup ollama serve &
16
+ ```
17
+
18
+ ```python
19
+ !ollama pull llama3.1:8b-instruct-fp16
20
+ ```
21
+
22
+ ```python
23
+ !pip install -q litellm tqdm loguru wikipedia transformers
24
+ !pip install -q datasets huggingface-hub
25
+ ```
26
+
27
+ ```python
28
+ import wikipedia
29
+ import json
30
+ from typing import List, Dict, Any
31
+ from loguru import logger
32
+ import re
33
+ from tqdm import tqdm
34
+
35
+ from google.colab import userdata
36
+ import os
37
+ ```
38
+
39
+ ```python
40
+
41
+ ```
42
+
43
+ ## 2. 基本設定
44
+
45
+ ```python
46
+ # モデル設定
47
+ MODEL_NAME = "ollama/llama3.1:8b-instruct-fp16"
48
+ # MODEL_NAME = "groq/llama3-8b-8192"
49
+ # MODEL_NAME = "gemini/gemini-1.5-flash-latest"
50
+ # MODEL_NAME = "together_ai/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo"
51
+ # 基本パラメータ
52
+ DEFAULT_CHUNK_SIZE = 200
53
+ DEFAULT_OVERLAP_SIZE = 500
54
+ DEFAULT_QA_PAIRS_PER_CHUNK = 5
55
+ API_BASE = "http://localhost:11434"
56
+
57
+ # Groqのセットアップ
58
+ # os.environ['GROQ_API_KEY'] = userdata.get('GROQ_API_KEY')
59
+ os.environ['HF_TOKEN'] = userdata.get('HF_TOKEN')
60
+ # os.environ["GEMINI_API_KEY"] = userdata.get('GEMINI_API_KEY')
61
+ # os.environ["TOGETHERAI_API_KEY"] = userdata.get('TOGETHERAI_API_KEY')
62
+ ```
63
+
64
+ ```python
65
+ from litellm import completion
66
+
67
+ response = completion(
68
+ model=MODEL_NAME,
69
+ messages=[{ "content": "東方地霊殿について教えて","role": "user"}],
70
+ api_base=API_BASE
71
+ )
72
+ print(response)
73
+ ```
74
+
75
+ ```python
76
+ # import os
77
+ # from litellm import completion
78
+
79
+ # user_message = "Hello, whats the weather in San Francisco??"
80
+ # messages = [{ "content": user_message,"role": "user"}]
81
+ # model_name = "together_ai/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo"
82
+ # response = completion(model=model_name, messages=messages)
83
+ # print(response)
84
+ ```
85
+
86
+ ```python
87
+ import re
88
+ import json
89
+ from typing import Optional, Any, Dict
90
+ from loguru import logger
91
+ from tenacity import retry, stop_after_attempt, wait_exponential
92
+ from typing import List, Dict, Any, Optional
93
+ from dataclasses import dataclass
94
+ import pandas as pd
95
+ from datetime import datetime
96
+ from pathlib import Path
97
+ from loguru import logger
98
+ import json
99
+ from tenacity import retry, stop_after_attempt, wait_exponential
100
+ from litellm import completion
101
+
102
+ import time
103
+
104
+
105
+
106
+ class JSONExtractor:
107
+ """LLMの出力からJSONを抽出するクラス"""
108
+
109
+ def extract_json(self, text: str) -> Optional[Dict[str, Any]]:
110
+ """テキストからJSONを抽出してパース"""
111
+ try:
112
+ # 最初の { から最後の } までを抽出
113
+ start = text.find('{')
114
+ end = text.rfind('}')
115
+ if start != -1 and end != -1:
116
+ json_str = text[start:end + 1]
117
+ return json.loads(json_str)
118
+ return None
119
+ except json.JSONDecodeError as e:
120
+ logger.warning(f"JSON parse error: {str(e)}")
121
+ return None
122
+
123
+ ```
124
+
125
+ ## 3. WikiTextProcessor クラスの実装
126
+
127
+ ```python
128
+ class WikiTextProcessor:
129
+ """Wikipediaテキストの処理とチャンク分割を行うクラス"""
130
+
131
+ @staticmethod
132
+ def get_wiki_text(topic: str, lang: str = "ja") -> str:
133
+ """指定されたトピックのWikipedia記事を取得"""
134
+ wikipedia.set_lang(lang)
135
+ try:
136
+ page = wikipedia.page(topic)
137
+ return page.content
138
+ except Exception as e:
139
+ logger.error(f"Error fetching {topic}: {e}")
140
+ return ""
141
+
142
+ @staticmethod
143
+ def clean_text(text: str) -> str:
144
+ """テキストのクリーニング"""
145
+ text = re.sub(r'\[\d+\]', '', text) # 参照記号の削除
146
+ text = re.sub(r'\n\s*\n', '\n', text) # 余分な改行の削除
147
+ return text.strip()
148
+
149
+ @staticmethod
150
+ def generate_summary(text: str) -> str:
151
+ """テキスト全体のサマリーを生成"""
152
+ from litellm import completion
153
+
154
+ summary_prompt = f"""
155
+ 以下のテキストの重要なポイントを3-5行で要約してください。
156
+ 重要な固有名詞や数値は必ず含めてください。
157
+
158
+ テキスト:
159
+ {text}
160
+ """
161
+
162
+ response = completion(
163
+ model=MODEL_NAME,
164
+ messages=[{"role": "user", "content": summary_prompt}],
165
+ max_tokens=200,
166
+ api_base=API_BASE
167
+ )
168
+
169
+ time.sleep(1)
170
+
171
+ return response.choices[0].message.content
172
+
173
+ @staticmethod
174
+ def split_into_chunks_with_context(
175
+ text: str,
176
+ summary: str,
177
+ max_chunk_size: int = DEFAULT_CHUNK_SIZE,
178
+ overlap_size: int = DEFAULT_OVERLAP_SIZE
179
+ ) -> List[Dict[str, str]]:
180
+ """テキストを文脈情報とオーバーラップ情報付きでチャンク分割"""
181
+ sentences = text.split('。')
182
+ chunks = []
183
+ current_chunk = []
184
+ current_size = 0
185
+ previous_text = "" # 前のチャンクのテキストを保持
186
+
187
+ # 平均文長を計算
188
+ avg_sentence_length = sum(len(s) + 1 for s in sentences) / len(sentences)
189
+ overlap_sentences = max(1, int(overlap_size / avg_sentence_length))
190
+
191
+ for i, sentence in enumerate(sentences):
192
+ sentence_size = len(sentence) + 1
193
+
194
+ if current_size + sentence_size > max_chunk_size and current_chunk:
195
+ # チャンクテキストの作成
196
+ chunk_text = '。'.join(current_chunk) + '。'
197
+
198
+ # チャンクを保存(オーバーラップテキストも含める)
199
+ chunks.append({
200
+ 'chunk_text': chunk_text,
201
+ 'overlap_text': previous_text, # 前のチャンクの最後の部分
202
+ 'summary': summary,
203
+ 'position': len(chunks) / (len(text) / max_chunk_size) # 概算位置
204
+ })
205
+
206
+ # 次のチャンクのためにオーバーラップを準備
207
+ previous_text = '。'.join(current_chunk[-overlap_sentences:]) + '。'
208
+
209
+ # オーバーラップを考慮して新しいチャンクを開始
210
+ current_chunk = current_chunk[-overlap_sentences:]
211
+ current_size = sum(len(s) + 1 for s in current_chunk)
212
+
213
+ current_chunk.append(sentence)
214
+ current_size += sentence_size
215
+
216
+ # 最後のチャンクを追加
217
+ if current_chunk:
218
+ chunk_text = '。'.join(current_chunk) + '。'
219
+ chunks.append({
220
+ 'chunk_text': chunk_text,
221
+ 'overlap_text': previous_text,
222
+ 'summary': summary,
223
+ 'position': 1.0
224
+ })
225
+
226
+ # 先頭チャンクのoverlap_textを調整(前方テキストがないため)
227
+ if chunks:
228
+ chunks[0]['overlap_text'] = '(先頭のため、オーバーラップテキストなし)'
229
+
230
+ return chunks
231
+
232
+ @staticmethod
233
+ def get_next_chunk_preview(
234
+ sentences: List[str],
235
+ current_position: int,
236
+ preview_sentences: int = 2
237
+ ) -> str:
238
+ """次のチャンクの冒頭部分を取得"""
239
+ if current_position + preview_sentences >= len(sentences):
240
+ return ""
241
+ preview = sentences[current_position:current_position + preview_sentences]
242
+ return '。'.join(preview) + '。'
243
+ ```
244
+
245
+ ## 4. QAGenerator クラスの実装
246
+
247
+ ```python
248
+ class QAGenerator2:
249
+ """Q&Aペアの生成を担当するクラス"""
250
+
251
+ @staticmethod
252
+ def generate_qa_pairs_with_context(
253
+ chunk_data: Dict[str, str],
254
+ num_pairs: int = DEFAULT_QA_PAIRS_PER_CHUNK,
255
+ max_retries: int = 3
256
+ ) -> List[Dict[str, str]]:
257
+ """文脈を考慮してQ&Aペアを生成"""
258
+ from litellm import completion
259
+
260
+ prompt = f"""
261
+ 以下のテキストから質問と回答のペアを{num_pairs}つ生成してください。
262
+ 質問は必ずメインテキストの内容から作成し、補足情報は質問を明確にするためだけに使用してください。
263
+
264
+ ## 全体の文脈(参考情報):
265
+ {chunk_data['summary']}
266
+
267
+ ## テキストの位置(参考情報):
268
+ テキスト全体の{int(chunk_data['position'] * 100)}%付近
269
+
270
+ ## オーバーラップ部分(参考情報):
271
+ {chunk_data.get('overlap_text', '(オーバーラップ部分なし)')}
272
+
273
+ ## メインテキスト(このテキストから質問を作成):
274
+ {chunk_data['chunk_text']}
275
+
276
+ 以下の条件をすべて満たすJSONを出力してください:
277
+
278
+ 1. 質問生成のルール:
279
+ - メインテキストの内容から質問を作成
280
+ - 質問文だけで回答が一意に決まるように具体的に作成
281
+ - 必要に応じて、文脈やオーバーラップ部分の情報を質問文に含めて明確化
282
+ - 例: 「このキャラクターは何をしましたか?」(×) → 「霊烏路空は地霊殿でどのような役割を担っていましたか?」(○)
283
+
284
+ 2. 回答生成のルール:
285
+ - メインテキストを主な情報源として使用
286
+ - 補足情報は回答の正確性を高めるためにのみ使用
287
+ - 500文字以下で簡潔に記述
288
+
289
+ 3. フォーマットのルール:
290
+ - 厳密なJSON形式で出力(最後の要素にカンマをつけない)
291
+ - すべての質問は日本語で記述
292
+ - すべての質問は「?」で終わる
293
+
294
+ 出力形式:
295
+ {
296
+ "qa_pairs": [
297
+ {"question": "具体的な質問1?", "answer": "メインテキストに基づく回答1"},
298
+ {"question": "具体的な質問2?", "answer": "メインテキストに基づく回答2"}
299
+ ]
300
+ }
301
+ """
302
+
303
+ for attempt in range(max_retries):
304
+ try:
305
+
306
+ response = completion(
307
+ model=MODEL_NAME,
308
+ messages=[{"role": "user", "content": prompt}],
309
+ max_tokens=1000,
310
+ temperature=0.7,
311
+ api_base=API_BASE
312
+ )
313
+ # response = completion(
314
+ # model=MODEL_NAME,
315
+ # messages=[{"role": "user", "content": prompt}],
316
+ # max_tokens=1000,
317
+ # temperature=0.7,
318
+ # api_base="http://localhost:11434"
319
+ # )
320
+ # response = completion(
321
+ # model=MODEL_NAME,
322
+ # messages=[{"role": "user", "content": prompt}],
323
+ # max_tokens=1000
324
+ # )
325
+
326
+ time.sleep(1)
327
+ result = json.loads(response.choices[0].message.content)
328
+ return result['qa_pairs']
329
+
330
+ except Exception as e:
331
+ logger.warning(f"Attempt {attempt + 1} failed: {e}")
332
+ if attempt == max_retries - 1:
333
+ logger.error("All attempts failed")
334
+ return []
335
+ ```
336
+
337
+ ```python
338
+
339
+ class QAGenerator:
340
+ """Q&Aペアの生成を担当するクラス"""
341
+
342
+ @staticmethod
343
+ def generate_qa_pairs_with_context(
344
+ chunk_data: Dict[str, str],
345
+ num_pairs: int = DEFAULT_QA_PAIRS_PER_CHUNK,
346
+ max_retries: int = 3
347
+ ) -> List[Dict[str, str]]:
348
+ """文脈を考慮してQ&Aペアを生成"""
349
+ from litellm import completion
350
+
351
+ json_format = '''
352
+ {
353
+ "qa_pairs": [
354
+ {"question": "東方地霊殿において霊烏路空はどのような役割を担っていましたか?", "answer": "霊烏路空は地霊殿の管理者として働いており、地下の秩序を維持する役割を担っていました。"},
355
+ {"question": "東方地霊殿の開発元である上海アリス幻樂団はどのような組織ですか?", "answer": "上海アリス幻樂団は、東方Projectシリーズを開発している同人サークルです。"}
356
+ ]
357
+ }
358
+ '''
359
+
360
+ prompt = f"""
361
+ 以下のテキストから質問と回答のペアを{num_pairs}つ生成してください。
362
+ 質問は必ずメインテキストの内容から作成し、補足情報は質問を明確にするためだけに使用してください。
363
+
364
+ ## 全体の文脈(参考情報):
365
+ {chunk_data['summary']}
366
+
367
+ ## テキストの位置(参考情報):
368
+ テキスト全体の{int(chunk_data['position'] * 100)}%付近
369
+
370
+ ## オーバーラップ部分(参考情報):
371
+ {chunk_data.get('overlap_text', '(オーバーラップ部分なし)')}
372
+
373
+ ## メインテキスト(このテキストから質問を作成):
374
+ {chunk_data['chunk_text']}
375
+
376
+ 以下の条件をすべて満たすJSONを出力してください:
377
+
378
+ 1. 質問生成の必須ルール:
379
+ - メインテキストの内容から質問を作成すること
380
+ - 各質問は完全に独立して理解可能であること
381
+ - 固有名詞を明示的に含めること
382
+ - 「この」「その」などの指示語を使用しないこと
383
+ - 「彼」「彼女」などの代名詞を使用しないこと
384
+ - 質問に登場する対象を常に具体的に明示すること
385
+
386
+ 2. 質問作成の禁止事項:
387
+ × 「このキャラクターは何をしましたか?」
388
+ × 「彼女の役割は何ですか?」
389
+ × 「その時何が起こりましたか?」
390
+ × 「ここで何が行われましたか?」
391
+
392
+ 3. 質問作成の好例:
393
+ ○ 「霊烏路空は地霊殿でどのような役割を担っていましたか?」
394
+ ○ 「東方地霊殿のストーリーで温泉はどのような意味を持っていましたか?」
395
+ ○ 「上海アリス幻樂団が開発した東方地霊殿の特徴は何ですか?」
396
+
397
+ 4. 回答生成のルール:
398
+ - メインテキストを主な情報源として使用する
399
+ - 補足情報は回答の正確性を高めるためにのみ使用する
400
+ - 500文字以下で簡潔に記述する
401
+ - 回答も代名詞を避け、具体的な固有名詞を使用する
402
+
403
+ 5. フォーマットのルール:
404
+ - 厳密なJSON形式で出力すること
405
+ - 最後の要素にカンマをつけないこと
406
+ - すべての質問は日本語で記述すること
407
+ - すべての質問は「?」で終わること
408
+
409
+ 出力形式:
410
+ {json_format}"""
411
+
412
+ json_extractor = JSONExtractor()
413
+
414
+ for attempt in range(max_retries):
415
+
416
+ try:
417
+ response = completion(
418
+ model=MODEL_NAME,
419
+ messages=[{"role": "user", "content": prompt}],
420
+ max_tokens=1000,
421
+ temperature=0.7,
422
+ api_base=API_BASE
423
+ )
424
+ time.sleep(1)
425
+
426
+ # response = completion(
427
+ # model=MODEL_NAME,
428
+ # messages=[{"role": "user", "content": prompt}],
429
+ # max_tokens=1000,
430
+ # temperature=0.7,
431
+ # api_base="http://localhost:11434"
432
+ # )
433
+ # response = completion(
434
+ # model=MODEL_NAME,
435
+ # messages=[{"role": "user", "content": prompt}],
436
+ # max_tokens=1000
437
+ # )
438
+
439
+ # LLMの応答を取得
440
+ llm_response = response.choices[0].message.content
441
+ logger.debug(f"LLM Response (Attempt {attempt + 1}):\n{llm_response}")
442
+
443
+ # JSONの抽出を試みる
444
+ result = json_extractor.extract_json(llm_response)
445
+
446
+ if result and 'qa_pairs' in result:
447
+ return result['qa_pairs']
448
+
449
+ logger.warning(f"Failed to extract valid JSON (Attempt {attempt + 1})")
450
+ logger.warning(f"Raw response:\n{llm_response}")
451
+
452
+ except Exception as e:
453
+ logger.warning(f"Attempt {attempt + 1} failed: {str(e)}")
454
+ if attempt == max_retries - 1:
455
+ logger.error("All attempts failed")
456
+ logger.error("Last LLM response:\n%s", response.choices[0].message.content if 'response' in locals() else "No response")
457
+ return []
458
+
459
+ return []
460
+
461
+
462
+ @staticmethod
463
+ def format_for_llama(qa_pairs: List[Dict[str, str]]) -> List[Dict[str, Any]]:
464
+ """Q&AペアをLlama 3.2のフォーマットに変換"""
465
+ formatted_data = []
466
+ for qa in qa_pairs:
467
+ conversation = [
468
+ {"role": "system", "content": "You are a helpful assistant."},
469
+ {"role": "user", "content": qa['question']},
470
+ {"role": "assistant", "content": qa['answer']}
471
+ ]
472
+ formatted_data.append({"conversations": conversation})
473
+ return formatted_data
474
+
475
+ ```
476
+
477
+ ```python
478
+ from dataclasses import dataclass
479
+ ```
480
+
481
+ ```python
482
+
483
+
484
+ @dataclass
485
+ class QAEvaluation:
486
+ """Q&Aペアの評価結果を保持するデータクラス"""
487
+ score: float
488
+ feedback: str
489
+ improvement_suggestions: List[str]
490
+ factuality_score: float # 事実との一致度
491
+ question_quality_score: float # 質問の質スコア
492
+ answer_completeness_score: float # 回答の完全性スコア
493
+
494
+ @dataclass
495
+ class QAImprovement:
496
+ """Q&Aペアの改善プロセスを記録するデータクラス"""
497
+ original_question: str
498
+ original_answer: str
499
+ improved_question: str
500
+ improved_answer: str
501
+ chunk_text: str
502
+ chunk_position: float
503
+ summary: str
504
+ evaluation: QAEvaluation
505
+ improvement_count: int
506
+ topic: str
507
+ timestamp: datetime
508
+
509
+
510
+
511
+ @dataclass
512
+ class QAEvaluation:
513
+ """Q&Aペアの評価結果を保持するデータクラス"""
514
+ score: float
515
+ feedback: str
516
+ improvement_suggestions: List[str]
517
+ factuality_score: float # 事実との一致度
518
+ question_quality_score: float # 質問の質スコア
519
+ answer_completeness_score: float # 回答の完全性スコア
520
+
521
+ @dataclass
522
+ class QAImprovement:
523
+ """Q&Aペアの改善プロセスを記録するデータクラス"""
524
+ original_question: str
525
+ original_answer: str
526
+ improved_question: str
527
+ improved_answer: str
528
+ chunk_text: str
529
+ chunk_position: float
530
+ summary: str
531
+ evaluation: QAEvaluation
532
+ improvement_count: int
533
+ topic: str
534
+ timestamp: datetime
535
+
536
+ class ReflexiveQAGenerator:
537
+ """リフレクションベースのQA生成・評価・記録システム"""
538
+
539
+ def __init__(self, model_name: str = "gpt-4"):
540
+ self.model_name = model_name
541
+ self.improvements: List[QAImprovement] = []
542
+ self.setup_output_directories()
543
+ self.json_extractor = JSONExtractor()
544
+
545
+ def setup_output_directories(self):
546
+ """出力ディレクトリの設定"""
547
+ self.output_dir = Path("qa_generation_output")
548
+ self.output_dir.mkdir(exist_ok=True)
549
+ self.current_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
550
+
551
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
552
+ def _get_evaluation_with_retry(self, qa_pair: Dict[str, str], context: Dict[str, str]) -> QAEvaluation:
553
+ """評価を取得する(リトライ機能付き)"""
554
+ prompt = self._create_evaluation_prompt(qa_pair, context)
555
+
556
+ response = completion(
557
+ model=self.model_name,
558
+ messages=[{"role": "user", "content": prompt}],
559
+ temperature=0.3,
560
+ api_base=API_BASE
561
+ )
562
+ time.sleep(1)
563
+
564
+ result = self.json_extractor.extract_json(response.choices[0].message.content)
565
+ if not result:
566
+ raise ValueError("有効なJSONが抽出できませんでした")
567
+
568
+ try:
569
+ return QAEvaluation(**result)
570
+ except Exception as e:
571
+ logger.error(f"QAEvaluation生成エラー: {str(e)}")
572
+ raise ValueError("QAEvaluation生成に失敗しました")
573
+
574
+ def evaluate_qa_pair(self, qa_pair: Dict[str, str], context: Dict[str, str]) -> QAEvaluation:
575
+ """Q&Aペアを評価し、詳細なフィードバックを生成"""
576
+ try:
577
+ return self._get_evaluation_with_retry(qa_pair, context)
578
+ except Exception as e:
579
+ logger.error(f"評価に完全に失敗: {str(e)}")
580
+ return QAEvaluation(
581
+ score=0.0,
582
+ factuality_score=0.0,
583
+ question_quality_score=0.0,
584
+ answer_completeness_score=0.0,
585
+ feedback="評価処理中に重大なエラーが発生しました",
586
+ improvement_suggestions=[]
587
+ )
588
+
589
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
590
+ def _get_llm_response_with_retry(self, prompt: str) -> str:
591
+ """LLMからのレスポンス取得(リトライ付き)"""
592
+ response = completion(
593
+ model=self.model_name,
594
+ messages=[{"role": "user", "content": prompt}],
595
+ temperature=0.3,
596
+ api_base=API_BASE
597
+ )
598
+ time.sleep(1)
599
+
600
+ result = self.json_extractor.extract_json(response.choices[0].message.content)
601
+ if not result:
602
+ raise ValueError(f"Invalid JSON in LLM response: {response.choices[0].message.content[:200]}...")
603
+
604
+ if not all(k in result for k in ['question', 'answer']):
605
+ raise ValueError(f"Missing required keys in JSON: {result}")
606
+
607
+ return result
608
+
609
+ def _improve_qa_pair_with_retry(
610
+ self,
611
+ qa_pair: Dict[str, str],
612
+ evaluation: QAEvaluation,
613
+ context: Dict[str, str]
614
+ ) -> Dict[str, str]:
615
+ """Q&Aペアの改善を試みる"""
616
+ prompt = self._create_improvement_prompt(qa_pair, evaluation, context)
617
+
618
+ try:
619
+ return self._get_llm_response_with_retry(prompt)
620
+ except Exception as e:
621
+ logger.error(f"Failed to improve QA pair after all retries: {str(e)}")
622
+ return qa_pair # 失敗した場合は元のQ&Aペアを返す
623
+
624
+
625
+
626
+ def improve_qa_pair(
627
+ self,
628
+ qa_pair: Dict[str, str],
629
+ evaluation: QAEvaluation,
630
+ context: Dict[str, str]
631
+ ) -> Dict[str, str]:
632
+ """評価結果に基づいてQ&Aペアを改善"""
633
+ try:
634
+ return self._improve_qa_pair_with_retry(qa_pair, evaluation, context)
635
+ except Exception as e:
636
+ logger.error(f"改善に完全に失敗: {str(e)}")
637
+ return qa_pair
638
+
639
+ def _create_evaluation_prompt(self, qa_pair: Dict[str, str], context: Dict[str, str]) -> str:
640
+ """評価用のプロンプトを生成"""
641
+ return f"""
642
+ 以下のQ&Aペアの品質を厳密に評価してください。
643
+
644
+ ## 記事の要約:
645
+ {context['summary']}
646
+
647
+ ## 現在のチャンク(テキスト全体の{int(context['position'] * 100)}%付近):
648
+ {context['chunk_text']}
649
+
650
+ ## 評価対象:
651
+ 質問: {qa_pair['question']}
652
+ 回答: {qa_pair['answer']}
653
+
654
+ 以下の基準で評価し、JSON形式で出力してください:
655
+ 改善提案は内容を正確に考慮した改善提案をしてください。
656
+
657
+ 1. 事実性 (factuality_score):
658
+ - 回答は与えられたコンテキストと完全に一致しているか
659
+ - サマリーの内容とも矛盾していないか
660
+ - スコアは0.0~1.0で評価
661
+
662
+ 2. 質問の質 (question_quality_score):
663
+ - 指示語や代名詞を避けているか
664
+ - 具体的な固有名詞を使用しているか
665
+ - 質問は文脈なしで理解可能か
666
+ - スコアは0.0~1.0で評価
667
+
668
+ 3. 回答の完全性 (answer_completeness_score):
669
+ - 回答は質問に対して適切で完全な情報を提供しているか
670
+ - 必要な文脈や詳細が含まれているか
671
+ - スコアは0.0~1.0で評価
672
+
673
+ 必ず以下の形式でJSONを出力してください:
674
+ {{
675
+ "factuality_score": 0.0~1.0の評価スコア,
676
+ "question_quality_score": 0.0~1.0の評価スコア,
677
+ "answer_completeness_score": 0.0~1.0の評価スコア,
678
+ "score": 3つのスコアの平均値,
679
+ "feedback": "詳細な評価コメント",
680
+ "improvement_suggestions": [
681
+ "改善提案1",
682
+ "改善提案2"
683
+ ]
684
+ }}
685
+
686
+ 注意: 必ず有効なJSONを出力してください。コードブロックや追加の説明は不要です。"""
687
+
688
+ def _create_improvement_prompt(
689
+ self,
690
+ qa_pair: Dict[str, str],
691
+ evaluation: QAEvaluation,
692
+ context: Dict[str, str]
693
+ ) -> str:
694
+ """改善用のプロンプトを生成"""
695
+ return f"""
696
+ Q&Aペアを以下の情報に基づいて改善してください。
697
+
698
+ ## 記事の要約:
699
+ {context['summary']}
700
+
701
+ ## 現在のチャンク(テキスト全体の{int(context['position'] * 100)}%付近):
702
+ {context['chunk_text']}
703
+
704
+ ## 現在のQ&Aペア:
705
+ 質問: {qa_pair['question']}
706
+ 回答: {qa_pair['answer']}
707
+
708
+ ## 評価スコア:
709
+ - 事実との一致度: {evaluation.factuality_score}
710
+ - 質問の質: {evaluation.question_quality_score}
711
+ - 回答の完全性: {evaluation.answer_completeness_score}
712
+
713
+ ## 評価フィードバック:
714
+ {evaluation.feedback}
715
+
716
+ ## 改善提案:
717
+ {json.dumps(evaluation.improvement_suggestions, ensure_ascii=False, indent=2)}
718
+
719
+ 以下の条件を満たす改善されたQ&Aペアを生成してください:
720
+ 1. 与えられたコンテキストの内容に完全に即した事実のみを含める
721
+ 2. 指示語や代名詞を避け、具体的な固有名詞を使用する
722
+ 3. 質問と回答は独立して理解可能にする
723
+ 4. サマリー情報も参考に、より正確な文脈を提供する
724
+
725
+ 必ず以下の形式でJSONを出力してください:
726
+ {{
727
+ "question": "改善された質問",
728
+ "answer": "改善された回答"
729
+ }}
730
+
731
+ 注意: 必ず有効なJSONを出力してください。コードブロックや追加の説明は不要です。"""
732
+
733
+ def generate_qa_pairs_with_reflection(
734
+ self,
735
+ chunk_data: Dict[str, str],
736
+ topic: str,
737
+ num_pairs: int = 3,
738
+ quality_threshold: float = 0.8,
739
+ max_improvement_attempts: int = 2
740
+ ) -> List[Dict[str, str]]:
741
+ """リフレクションを用いて高品質なQ&Aペアを生成"""
742
+ qa_generator = QAGenerator()
743
+ final_qa_pairs = []
744
+
745
+ # チャンク情報のログ出力
746
+ logger.info(f"=== チャンク情報 ===")
747
+ logger.info(f"位置: テキスト全体の{int(chunk_data['position'] * 100)}%付近")
748
+ logger.info(f"サマリー: {chunk_data['summary'][:100]}...")
749
+
750
+ initial_pairs = qa_generator.generate_qa_pairs_with_context(chunk_data, num_pairs)
751
+ logger.info(f"初期Q&Aペア数: {len(initial_pairs)}")
752
+
753
+ for pair_idx, qa_pair in enumerate(initial_pairs, 1):
754
+ current_pair = qa_pair
755
+ best_evaluation = None
756
+ improvement_count = 0
757
+
758
+ logger.info(f"--- Q&Aペア {pair_idx}/{len(initial_pairs)} の改善プロセス ---")
759
+ logger.info("初期Q&A:")
760
+ logger.info(f"Q: {qa_pair['question']}")
761
+ logger.info(f"A: {qa_pair['answer']}\n")
762
+
763
+ for attempt in range(max_improvement_attempts):
764
+ evaluation = self.evaluate_qa_pair(current_pair, chunk_data)
765
+
766
+ # 評価結果の詳細表示
767
+ logger.info(f"改善試行 {attempt + 1}/{max_improvement_attempts}")
768
+ logger.info(f"評価スコア:")
769
+ logger.info(f"- 事実との一致度: {evaluation.factuality_score:.2f}")
770
+ logger.info(f"- 質問の質: {evaluation.question_quality_score:.2f}")
771
+ logger.info(f"- 回答の完全性: {evaluation.answer_completeness_score:.2f}")
772
+ logger.info(f"- 総合スコア: {evaluation.score:.2f}")
773
+ logger.info(f"フィードバック: {evaluation.feedback}")
774
+ if evaluation.improvement_suggestions:
775
+ logger.info("改善提案:")
776
+ for i, suggestion in enumerate(evaluation.improvement_suggestions, 1):
777
+ logger.info(f" {i}. {suggestion}")
778
+
779
+ if not best_evaluation or evaluation.score > best_evaluation.score:
780
+ best_evaluation = evaluation
781
+ best_pair = current_pair
782
+ logger.info("✨ 新しいベストスコアを記録")
783
+
784
+ if evaluation.score >= quality_threshold:
785
+ logger.info(f"✅ 品質閾値 {quality_threshold} を達成")
786
+ break
787
+
788
+ current_pair = self.improve_qa_pair(current_pair, evaluation, chunk_data)
789
+ if current_pair != qa_pair: # 改善が行われた場合
790
+ logger.info("改善後のQ&A:")
791
+ logger.info(f"Q: {current_pair['question']}")
792
+ logger.info(f"A: {current_pair['answer']}")
793
+ improvement_count += 1
794
+
795
+ if best_evaluation and best_evaluation.score >= quality_threshold:
796
+ final_qa_pairs.append(best_pair)
797
+ logger.info(f"✅ Q&Aペア {pair_idx} を採用 (スコア: {best_evaluation.score:.2f})")
798
+
799
+ # 改善履歴を記録
800
+ improvement = QAImprovement(
801
+ original_question=qa_pair['question'],
802
+ original_answer=qa_pair['answer'],
803
+ improved_question=best_pair['question'],
804
+ improved_answer=best_pair['answer'],
805
+ chunk_text=chunk_data['chunk_text'],
806
+ chunk_position=chunk_data['position'],
807
+ summary=chunk_data['summary'],
808
+ evaluation=best_evaluation,
809
+ improvement_count=improvement_count,
810
+ topic=topic,
811
+ timestamp=datetime.now()
812
+ )
813
+ self.improvements.append(improvement)
814
+ else:
815
+ logger.warning(f"❌ Q&Aペア {pair_idx} は品質基準を満たさず不採用")
816
+
817
+ logger.info(f"=== 最終結果 ===")
818
+ logger.info(f"���成されたQ&Aペア数: {len(final_qa_pairs)}/{len(initial_pairs)}")
819
+ if final_qa_pairs:
820
+ avg_score = sum(imp.evaluation.score for imp in self.improvements[-len(final_qa_pairs):]) / len(final_qa_pairs)
821
+ logger.info(f"平均品質スコア: {avg_score:.2f}")
822
+
823
+ return final_qa_pairs
824
+
825
+ def save_to_csv(self, topic: str = "unknown") -> tuple[Path, Path]:
826
+ """改善履歴をCSVファイルに保存"""
827
+ csv_path = self.output_dir / f"qa_improvements_{self.current_timestamp}.csv"
828
+
829
+ records = []
830
+ for imp in self.improvements:
831
+ record = {
832
+ "topic": imp.topic,
833
+ "timestamp": imp.timestamp.isoformat(),
834
+ "chunk_position": imp.chunk_position,
835
+ "original_question": imp.original_question,
836
+ "original_answer": imp.original_answer,
837
+ "improved_question": imp.improved_question,
838
+ "improved_answer": imp.improved_answer,
839
+ "factuality_score": imp.evaluation.factuality_score,
840
+ "question_quality_score": imp.evaluation.question_quality_score,
841
+ "answer_completeness_score": imp.evaluation.answer_completeness_score,
842
+ "overall_score": imp.evaluation.score,
843
+ "feedback": imp.evaluation.feedback,
844
+ "improvement_suggestions": "; ".join(imp.evaluation.improvement_suggestions),
845
+ "improvement_count": imp.improvement_count,
846
+ "chunk_text": imp.chunk_text,
847
+ "summary": imp.summary
848
+ }
849
+ records.append(record)
850
+
851
+ df = pd.DataFrame(records)
852
+ df.to_csv(csv_path, index=False, encoding='utf-8')
853
+ logger.info(f"改善履歴をCSVに保存しました: {csv_path}")
854
+
855
+ # 基本的な統計情報を出力
856
+ stats = {
857
+ "total_qa_pairs": len(records),
858
+ "avg_improvement_count": df["improvement_count"].mean(),
859
+ "avg_final_score": df["overall_score"].mean(),
860
+ "improved_pairs": len(df[df["improvement_count"] > 0])
861
+ }
862
+
863
+ stats_path = self.output_dir / f"qa_stats_{self.current_timestamp}.json"
864
+ with open(stats_path, 'w', encoding='utf-8') as f:
865
+ json.dump(stats, f, ensure_ascii=False, indent=2)
866
+
867
+ return csv_path, stats_path
868
+
869
+ def format_for_llama(self, qa_pairs: List[Dict[str, str]]) -> List[Dict[str, str]]:
870
+ """Q&AペアをLlama形式に変換"""
871
+ formatted_data = []
872
+ for qa in qa_pairs:
873
+ conversation = [
874
+ {"role": "system", "content": "You are a helpful assistant."},
875
+ {"role": "user", "content": qa['question']},
876
+ {"role": "assistant", "content": qa['answer']}
877
+ ]
878
+ formatted_data.append({"conversations": conversation})
879
+ return formatted_data
880
+
881
+
882
+ class DatasetCreator:
883
+ """データセット生成の全体プロセスを管理するクラス"""
884
+
885
+ def __init__(self, config: Dict[str, Any] = None):
886
+ self.config = config or DEFAULT_CONFIG
887
+ self.wiki_processor = WikiTextProcessor()
888
+ self.qa_generator = ReflexiveQAGenerator(model_name=self.config["model_name"])
889
+ self.setup_directories()
890
+
891
+ def setup_directories(self):
892
+ """必要なディレクトリの作成"""
893
+ self.log_dir = Path("logs")
894
+ self.log_dir.mkdir(exist_ok=True)
895
+
896
+ def create_dataset(
897
+ self,
898
+ topics: List[str],
899
+ output_file: str = "qa_dataset.json"
900
+ ) -> None:
901
+ """データセット生成のメインプロセス"""
902
+ all_qa_pairs = []
903
+
904
+ for topic_idx, topic in enumerate(topics, 1):
905
+ logger.info(f"Processing topic {topic_idx}/{len(topics)}: {topic}")
906
+
907
+ try:
908
+ # Wikipedia記事の取得と前処理
909
+ text = self.wiki_processor.get_wiki_text(topic)
910
+ if not text:
911
+ logger.warning(f"No text found for topic: {topic}")
912
+ continue
913
+
914
+ text = self.wiki_processor.clean_text(text)
915
+ summary = self.wiki_processor.generate_summary(text)
916
+
917
+ # チャンク分割
918
+ chunks = self.wiki_processor.split_into_chunks_with_context(
919
+ text,
920
+ summary,
921
+ self.config["chunk_size"],
922
+ self.config["overlap_size"]
923
+ )
924
+
925
+ # chunks = chunks[:2]
926
+
927
+ # 各チャンクからQ&Aペアを生成
928
+ for chunk in tqdm(chunks, desc=f"Generating Q&A pairs for {topic}"):
929
+ qa_pairs = self.qa_generator.generate_qa_pairs_with_reflection(
930
+ chunk_data=chunk,
931
+ topic=topic,
932
+ num_pairs=self.config["qa_pairs_per_chunk"],
933
+ quality_threshold=self.config["quality_threshold"],
934
+ max_improvement_attempts=self.config["max_improvement_attempts"]
935
+ )
936
+
937
+ if qa_pairs:
938
+ formatted_pairs = self.qa_generator.format_for_llama(qa_pairs)
939
+ all_qa_pairs.extend(formatted_pairs)
940
+
941
+ except Exception as e:
942
+ logger.error(f"Error processing topic {topic}: {str(e)}")
943
+ continue
944
+
945
+ if all_qa_pairs:
946
+ # データセットの保存
947
+ output_path = output_file
948
+ with open(output_path, 'w', encoding='utf-8') as f:
949
+ json.dump(all_qa_pairs, f, ensure_ascii=False, indent=2)
950
+
951
+ # 改善履歴と統計情報の保存
952
+ csv_path, stats_path = self.qa_generator.save_to_csv()
953
+
954
+ logger.info(f"""
955
+ 生成結果を保存しました:
956
+ - データセット: {output_path}
957
+ - 改善履歴: {csv_path}
958
+ - 統計情報: {stats_path}
959
+ """)
960
+ else:
961
+ logger.warning("有効なQ&Aペアが生成されませんでした。")
962
+
963
+ ```
964
+
965
+ ## 5. QualityChecker クラスの実装
966
+
967
+ ```python
968
+ class QualityChecker:
969
+ """生成されたQ&Aペアの品質管理を行うクラス"""
970
+
971
+ @staticmethod
972
+ def validate_qa_pair(qa_pair: Dict[str, str]) -> bool:
973
+ """Q&Aペアの品質チェック"""
974
+ MIN_QUESTION_LENGTH = 10
975
+ MIN_ANSWER_LENGTH = 20
976
+ MAX_ANSWER_LENGTH = 500
977
+
978
+ # 必須キーの存在チェック
979
+ if not all(key in qa_pair for key in ['question', 'answer']):
980
+ return False
981
+
982
+ question = qa_pair['question']
983
+ answer = qa_pair['answer']
984
+
985
+ # 空文字チェック
986
+ if not question or not answer:
987
+ return False
988
+
989
+ if len(question) < MIN_QUESTION_LENGTH:
990
+ return False
991
+ if len(answer) < MIN_ANSWER_LENGTH or len(answer) > MAX_ANSWER_LENGTH:
992
+ return False
993
+ if not question.endswith('?'):
994
+ return False
995
+
996
+ return True
997
+
998
+ @staticmethod
999
+ def check_diversity(qa_pairs: List[Dict[str, str]], threshold: float = 0.7) -> bool:
1000
+ """Q&Aペアの多様性をチェック"""
1001
+ # 無効なペアを除外
1002
+ valid_pairs = [pair for pair in qa_pairs if QualityChecker.validate_qa_pair(pair)]
1003
+
1004
+ if not valid_pairs: # 有効なペアが1つもない場合
1005
+ return False
1006
+
1007
+ from difflib import SequenceMatcher
1008
+
1009
+ for i, qa1 in enumerate(valid_pairs):
1010
+ for j, qa2 in enumerate(valid_pairs[i+1:]):
1011
+ similarity = SequenceMatcher(
1012
+ None,
1013
+ qa1['question'] + qa1['answer'],
1014
+ qa2['question'] + qa2['answer']
1015
+ ).ratio()
1016
+
1017
+ if similarity > threshold:
1018
+ return False
1019
+ return True
1020
+
1021
+ ```
1022
+
1023
+ ## 6. DatasetCreator クラスの実装
1024
+
1025
+ ```python
1026
+
1027
+ ```
1028
+
1029
+ ## 7. 使用例
1030
+
1031
+
1032
+ ## 8. Hugging Faceへのアップロード (オプション)
1033
+
1034
+
1035
+ ## 9. パラメータチューニングのヒント
1036
+
1037
+ - チャンクサイズ(`chunk_size`):
1038
+ - 短すぎる: 文脈が失われる
1039
+ - 長すぎる: Q&Aペアが散漫になる
1040
+ - 推奨: 150-300文字
1041
+
1042
+ - オーバーラップサイズ(`overlap_size`):
1043
+ - 小さすぎる: 文脈の連続性が失われる
1044
+ - 大きすぎる: 重複が多くなりすぎる
1045
+ - 推奨: チャンクサイズの40-60%
1046
+
1047
+ - Q&Aペア生成数(`num_pairs`):
1048
+ - 少なすぎる: データセットが小さくなる
1049
+ - 多すぎる: 品質が低下する可能性
1050
+ - 推奨: チャンクあたり3-7ペア
1051
+
1052
+ ```python
1053
+ import json
1054
+ from datasets import Dataset, DatasetDict
1055
+ from huggingface_hub import HfApi
1056
+ from loguru import logger
1057
+ from typing import Dict, Any
1058
+
1059
+ class DatasetUploader:
1060
+ def __init__(self, username: str, dataset_name: str):
1061
+ self.username = username
1062
+ self.dataset_name = dataset_name
1063
+ self.repo_id = f"{username}/{dataset_name}"
1064
+
1065
+ def load_data(self, file_path: str) -> Dict[str, Any]:
1066
+ """JSONファイルからデータを読み込む"""
1067
+ logger.info(f"Loading data from {file_path}")
1068
+ try:
1069
+ with open(file_path, 'r', encoding='utf-8') as f:
1070
+ data = json.load(f)
1071
+ logger.success(f"Successfully loaded data from {file_path}")
1072
+ return data
1073
+ except Exception as e:
1074
+ logger.error(f"Error loading file: {e}")
1075
+ raise
1076
+
1077
+ def process_data(self, data: Dict[str, Any]) -> Dict[str, list]:
1078
+ """データを処理して必要な形式に変換"""
1079
+ logger.info("Processing data")
1080
+ processed_data = {
1081
+ "instruction": [],
1082
+ "input": [],
1083
+ "output": [],
1084
+ "system": []
1085
+ }
1086
+
1087
+ total_items = len(data)
1088
+ for idx, item in enumerate(data, 1):
1089
+ if idx % 1000 == 0:
1090
+ logger.info(f"Processing item {idx}/{total_items} ({(idx/total_items)*100:.1f}%)")
1091
+
1092
+ conversations = item["conversations"]
1093
+ system_prompt = next((conv["content"] for conv in conversations if conv["role"] == "system"), "")
1094
+ user_input = next((conv["content"] for conv in conversations if conv["role"] == "user"), "")
1095
+ assistant_output = next((conv["content"] for conv in conversations if conv["role"] == "assistant"), "")
1096
+
1097
+ processed_data["system"].append(system_prompt)
1098
+ processed_data["instruction"].append(user_input)
1099
+ processed_data["input"].append("")
1100
+ processed_data["output"].append(assistant_output)
1101
+
1102
+ logger.success(f"Successfully processed {total_items} items")
1103
+ return processed_data
1104
+
1105
+ def upload_to_hub(self, processed_data: Dict[str, list]) -> None:
1106
+ """データセットを作成してHugging Faceにアップロード"""
1107
+ logger.info("Creating dataset from processed data")
1108
+ try:
1109
+ # データセットの作成
1110
+ dataset = Dataset.from_dict(processed_data)
1111
+ logger.info(f"Created dataset with {len(dataset)} examples")
1112
+
1113
+ # トレーニング/テストセットの分割
1114
+ dataset_dict = dataset.train_test_split(test_size=0.1, seed=42)
1115
+ logger.info(f"Train set: {len(dataset_dict['train'])} examples")
1116
+ logger.info(f"Test set: {len(dataset_dict['test'])} examples")
1117
+
1118
+ # アップロード
1119
+ logger.info(f"Uploading dataset to {self.repo_id}")
1120
+ dataset_dict.push_to_hub(repo_id=self.repo_id)
1121
+ logger.success(f"Dataset uploaded successfully to: https://huggingface.co/datasets/{self.repo_id}")
1122
+
1123
+ except Exception as e:
1124
+ logger.error(f"Error uploading dataset: {e}")
1125
+ raise
1126
+ ```
1127
+
1128
+ # メインスクリプトの実装
1129
+
1130
+ ## 1. メイン実行ファイル(main.py)
1131
+
1132
+ ```python
1133
+ import json
1134
+ from loguru import logger
1135
+ from pathlib import Path
1136
+ from typing import List
1137
+ from datetime import datetime
1138
+ from datasets import load_dataset
1139
+
1140
+ # 設定値を変数として定義
1141
+ topic = "霊烏路空"
1142
+ output_file = "qa_dataset.json"
1143
+ chunk_size = 200
1144
+ overlap_size = 700
1145
+ qa_pairs = 5
1146
+ hf_upload = True # テスト用にTrueに設定
1147
+ hf_username = "MakiAi"
1148
+ hf_dataset_name = f"OKU_wiki_llama3.1_8b_inst_Reflexive_chunk{chunk_size}_overlap{overlap_size}"
1149
+ log_dir = "logs"
1150
+
1151
+ # ReflexiveQAGeneratorの追加設定
1152
+ quality_threshold = 0.8
1153
+ max_improvement_attempts = 2
1154
+ # model_name = "gpt-4"
1155
+
1156
+ def load_topics(file_path: str) -> List[str]:
1157
+ """トピックリストをファイルから読み込む"""
1158
+ with open(file_path, 'r', encoding='utf-8') as f:
1159
+ return [line.strip() for line in f if line.strip()]
1160
+
1161
+
1162
+ def main():
1163
+
1164
+ if Path(topic).exists():
1165
+ topics = load_topics(topic)
1166
+ logger.info(f"Loaded {len(topics)} topics from {topic}")
1167
+ else:
1168
+ topics = [topic]
1169
+ logger.info(f"Using single topic: {topic}")
1170
+
1171
+ try:
1172
+ # データセット生成
1173
+ creator = DatasetCreator(config={
1174
+ "chunk_size": chunk_size,
1175
+ "overlap_size": overlap_size,
1176
+ "qa_pairs_per_chunk": qa_pairs,
1177
+ "quality_threshold": quality_threshold,
1178
+ "max_improvement_attempts": max_improvement_attempts,
1179
+ "model_name": MODEL_NAME
1180
+ })
1181
+
1182
+ creator.create_dataset(
1183
+ topics=topics,
1184
+ output_file=output_file
1185
+ )
1186
+
1187
+ # Hugging Faceへのアップロード
1188
+ if hf_upload:
1189
+ if not all([hf_username, hf_dataset_name]):
1190
+ logger.error("Hugging Face upload requires username and dataset name")
1191
+ return
1192
+
1193
+ try:
1194
+ logger.info("Uploading dataset to Hugging Face Hub...")
1195
+ # DatasetUploaderのインスタンス化
1196
+ uploader = DatasetUploader(
1197
+ username=hf_username,
1198
+ dataset_name=hf_dataset_name
1199
+ )
1200
+
1201
+ # データの読み込み
1202
+ data = uploader.load_data(output_file)
1203
+
1204
+ # データの処理
1205
+ processed_data = uploader.process_data(data)
1206
+
1207
+ # Hugging Faceへのアップロード
1208
+ uploader.upload_to_hub(processed_data)
1209
+ logger.success("Successfully uploaded dataset to Hugging Face Hub")
1210
+ except Exception as e:
1211
+ logger.error(f"Failed to upload to Hugging Face Hub: {str(e)}")
1212
+
1213
+ except Exception as e:
1214
+ logger.error(f"Error during execution: {str(e)}")
1215
+ raise
1216
+
1217
+ if __name__ == "__main__":
1218
+ main()
1219
+ ```
1220
+
1221
+ ## 2. 実行方法例
1222
+
1223
+ ### 基本的な使用方法
1224
+
1225
+ ```bash
1226
+ # 単一トピックでの実行
1227
+ # python main.py --topics "霊烏路空"
1228
+
1229
+ # トピックリストファイルからの実行
1230
+ # python main.py --topics topics.txt --output utsuho_dataset.json
1231
+
1232
+ # パラメータのカスタマイズ
1233
+ # python main.py \
1234
+ # --topics topics.txt \
1235
+ # --chunk-size 250 \
1236
+ # --overlap-size 120 \
1237
+ # --qa-pairs 7
1238
+ ```
1239
+
1240
+ ### Hugging Faceへのアップロ���ドを含む実行
1241
+
1242
+ ```python
1243
+ # !python main.py \
1244
+ # --topics "霊烏路空" \
1245
+ # --hf-upload \
1246
+ # --hf-username MakiAi \
1247
+ # --hf-dataset-name OKU_wiki_llama3.1_8b_inst
1248
+ ```
1249
+
1250
+ ## 3. トピックリストファイル(topics.txt)の例
1251
+
1252
+ ```text
1253
+ 霊烏路空
1254
+ 物部布都
1255
+ 四季映姫・ヤマザナドゥ
1256
+ 八雲紫
1257
+ ```
1258
+
1259
+ ## 4. 実行結果の例
1260
+
1261
+ ```text
1262
+ 2024-10-30 15:30:12 | INFO | Loaded 4 topics from topics.txt
1263
+ 2024-10-30 15:30:12 | INFO | Processing topic 1/4: 霊烏路空
1264
+ 2024-10-30 15:30:15 | INFO | Generated summary for 霊烏路空
1265
+ 2024-10-30 15:30:20 | INFO | Created 8 chunks with overlap
1266
+ 2024-10-30 15:31:05 | INFO | Generated 35 valid Q&A pairs
1267
+ ...
1268
+ 2024-10-30 15:45:23 | SUCCESS | Generated 156 Q&A pairs in total
1269
+ 2024-10-30 15:45:25 | SUCCESS | Dataset saved to touhou_qa.json
1270
+ ```