#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 小狸 AI - 预训练数据生成器 (v5) ================================== 两阶段生成: Phase 1 - 种子直出: 5并发 API 直接生成完整多轮对话 每API调用 = 1条高质量对话 (15-25轮, 2000-4000字) 速率: ~600条/h, 17h出1万条种子 Phase 2 - 语义扩展: 基于种子主题,API重新"编剧"出新对话 每条种子 → 10个变体(换角度/深度/角色) 速率: ~600条/h, ~7天扩展至10万+条 数据格式: JSONL, 每条: {"text":"...", "domain":"...", "concept":"..."} """ import sys, os, json, time, random, hashlib, argparse, re from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from api_correction import call_astron # ═══════════════════════════════════════════════════════════ # 1. 角色定义 # ═══════════════════════════════════════════════════════════ TEACHER_SYSTEM = "你是一位AI导师,回答专业详细,包含具体数据和原理。每轮回答150-500字。" # ═══════════════════════════════════════════════════════════ # 2. 深度学习领域图谱 # ═══════════════════════════════════════════════════════════ DOMAINS = { "量子物理": ["量子叠加态","量子纠缠","量子隧穿","海森堡测不准原理","波粒二象性","量子计算","量子场论","狄拉克方程"], "相对论": ["狭义相对论","质能方程","时空弯曲","引力波","黑洞物理","虫洞理论","暗物质","暗能量"], "人工智能": ["深度学习原理","Transformer","强化学习","大语言模型","扩散模型","多模态学习","注意力机制","模型量化"], "计算机科学": ["计算理论","操作系统","分布式系统","数据库","编译器","密码学","网络协议","计算机视觉"], "分子生物学": ["DNA复制","CRISPR","中心法则","蛋白质折叠","表观遗传学","RNA干扰","免疫系统","合成生物学"], "数学": ["微积分","傅里叶分析","微分方程","抽象代数","数论","拓扑学","概率论","图论"], "宇宙学": ["大爆炸","恒星演化","星系形成","宇宙微波背景","暗物质粒子","暴胀模型","引力波天文学","哈勃常数危机"], "神经科学": ["神经元","突触可塑性","大脑皮层","记忆机制","睡眠与意识","视觉系统","脑机接口","情感神经科学"], "化学": ["化学键","反应动力学","热力学","催化原理","光谱学","纳米化学","电化学","高分子化学"], "工程科技": ["航天工程","半导体","通信工程","机器人学","电力系统","材料工程","机械设计","核工程"], "经济学": ["一般均衡","博弈论","信息不对称","行为经济学","货币理论","计量经济学","拍卖理论","机制设计"], "哲学": ["先验哲学","权力意志","存在主义","语言游戏","证伪主义","范式理论","正义论","功利主义"], "天文学": ["赫罗图","超新星","中子星","系外行星","星系分类","脉冲星","恒星形成","宇宙射线"], "地球科学": ["板块构造","地震学","火山学","古生物学","气候系统","海洋环流","大气动力学","行星地质"], "生物技术": ["基因测序","PCR","冷冻电镜","核磁共振","光遗传学","类器官","纳米医学","基因敲除"], } ALL_DOMAINS = list(DOMAINS.keys()) # ═══════════════════════════════════════════════════════════ # 3. Phase 1: API直接生成种子 # ═══════════════════════════════════════════════════════════ def build_seed_prompt(domain: str, concept: str) -> str: return f"""请围绕【{domain}】领域的【{concept}】概念,生成一段师生深度对话。 要求: 1. 学生:从基础概念问起,逐步深入,包含追问、质疑、要求举例 2. 导师:每轮回答150-500字,专业准确,有数据支撑 3. 对话演进:基础定义→核心机制→实验证据→应用场景→前沿进展 4. 至少15轮对话 每轮格式(严格): 学生:[问题] 导师:[回答] 不要额外说明,直接开始对话。""" def generate_seed(domain: str, concept: str) -> dict: """一次API调用生成一条种子对话""" prompt = build_seed_prompt(domain, concept) text = call_astron(prompt, temperature=random.uniform(0.7, 0.9), system_prompt=TEACHER_SYSTEM, max_tokens=8192) if not text: return None # 解析 turns = [] for line in text.strip().split('\n'): line = line.strip() if not line: continue if re.match(r'^学生[::]', line): content = re.sub(r'^学生[::]\s*', '', line) turns.append(('user', content)) elif re.match(r'^导师[::]', line): content = re.sub(r'^导师[::]\s*', '', line) turns.append(('assistant', content)) if len(turns) < 10: return None lines = [] for role, content in turns: lines.append(f"<|im_start|>{role}\n{content}<|im_end|>") return { "text": '\n'.join(lines), "domain": domain, "concept": concept, } # ═══════════════════════════════════════════════════════════ # 4. Phase 2: 语义扩展 # ═══════════════════════════════════════════════════════════ VARIATION_PROMPTS = [ "用更通俗的方式重新讲一遍,多用日常类比", "从更高级的视角讲,假设学生已有基础,重点讲深层机制和数学原理", "换个角度,从历史发展脉络讲,突出关键实验和突破性发现", "从工程应用角度讲,重点讲实际怎么用、有什么技术瓶颈", "从批判性视角讲,有哪些争议、局限性、未解决的问题", "假设学生是高中水平,用最通俗的语言和大量类比来解释", "从跨学科视角讲,这个概念与其他领域有什么联系", "以问答竞赛的方式讲,学生提尖锐问题,导师用证据回击", ] def generate_variation(seed: dict, seed_text: str, prompt_template: str) -> dict: """基于种子主题生成一个语义变体""" domain = seed["domain"] concept = seed["concept"] instruction = prompt_template prompt = f"""请围绕【{domain}】领域的【{concept}】概念,生成一段师生深度对话。 {instruction} 要求: 1. 至少15轮对话 2. 导师回答150-500字,专业详细 3. 对话要有递进:定义→机制→证据→应用→前沿 每轮格式(严格): 学生:[问题] 导师:[回答] 直接开始对话,不要额外说明。""" text = call_astron(prompt, temperature=random.uniform(0.75, 0.95), system_prompt=TEACHER_SYSTEM, max_tokens=8192) if not text: return None turns = [] for line in text.strip().split('\n'): line = line.strip() if not line: continue if re.match(r'^学生[::]', line): content = re.sub(r'^学生[::]\s*', '', line) turns.append(('user', content)) elif re.match(r'^导师[::]', line): content = re.sub(r'^导师[::]\s*', '', line) turns.append(('assistant', content)) if len(turns) < 10: return None lines = [] for role, content in turns: lines.append(f"<|im_start|>{role}\n{content}<|im_end|>") return { "text": '\n'.join(lines), "domain": domain, "concept": concept, "variation": prompt_template[:30], } # ═══════════════════════════════════════════════════════════ # 5. 数据管理 # ═══════════════════════════════════════════════════════════ class DataStore: def __init__(self, save_dir: str): self.save_dir = Path(save_dir) self.save_dir.mkdir(parents=True, exist_ok=True) self.output_path = self.save_dir / 'dialogues.jsonl' self.seed_path = self.save_dir / 'seeds.jsonl' self.existing_hashes = set() self.count = 0 self.load() def load(self): for path in [self.output_path, self.seed_path]: if path.exists(): with open(path, 'r') as f: for line in f: line = line.strip() if line: try: item = json.loads(line) h = hashlib.md5(item['text'][:100].encode()).hexdigest() self.existing_hashes.add(h) self.count += 1 except: pass print(f"📂 已有: {self.count:,} 条") def add_seeds(self, items: list) -> int: if not items: return 0 new = [] for item in items: h = hashlib.md5(item['text'][:100].encode()).hexdigest() if h not in self.existing_hashes: self.existing_hashes.add(h) new.append(item) if not new: return 0 with open(self.seed_path, 'a') as f: for item in new: f.write(json.dumps(item, ensure_ascii=False) + '\n') self.count += 1 return len(new) def add_variations(self, items: list) -> int: if not items: return 0 new = [] for item in items: h = hashlib.md5(item['text'][:100].encode()).hexdigest() if h not in self.existing_hashes: self.existing_hashes.add(h) new.append(item) if not new: return 0 with open(self.output_path, 'a') as f: for item in new: f.write(json.dumps(item, ensure_ascii=False) + '\n') self.count += 1 return len(new) def get_seeds(self): """读取所有种子""" seeds = [] if self.seed_path.exists(): with open(self.seed_path, 'r') as f: for line in f: line = line.strip() if line: seeds.append(json.loads(line)) return seeds def total_count(self): return self.count # ═══════════════════════════════════════════════════════════ # 6. 质量评分 # ═══════════════════════════════════════════════════════════ def score(text: str) -> float: turns = (text.count('<|im_start|>user') + text.count('<|im_start|>assistant')) // 2 chars = len(text) s = 0.0 if turns >= 20: s += 0.20 elif turns >= 12: s += 0.15 elif turns >= 8: s += 0.10 else: s += 0.05 if chars >= 4000: s += 0.20 elif chars >= 2000: s += 0.15 elif chars >= 1200: s += 0.10 else: s += 0.05 avg = chars / max(turns, 1) if avg >= 200: s += 0.15 elif avg >= 100: s += 0.10 uc = text.count('<|im_start|>user') ac = text.count('<|im_start|>assistant') ratio = min(uc, ac) / max(uc, ac, 1) if ratio >= 0.8: s += 0.10 unique = len(set(text)) if unique >= 150: s += 0.10 return max(0, min(1, s)) # ═══════════════════════════════════════════════════════════ # 7. 卷统计 # ═══════════════════════════════════════════════════════════ def print_stats(store: DataStore, t_start: float, phase: str, target: int): elapsed = max(time.time() - t_start, 1) rate = store.count / elapsed * 3600 eta_h = (target - store.count) / max(rate, 0.01) seed_count = 0 if store.seed_path.exists(): with open(store.seed_path) as f: seed_count = sum(1 for _ in f if _.strip()) tokens_est = store.count * 6000 print(f" [{time.strftime('%H:%M:%S')}] " f"Phase {phase} | " f"累计: {store.count:,}/{target:,} " f"({store.count/max(target,1)*100:.1f}%) | " f"速率: {rate:.0f}条/h | " f"种子: {seed_count} | " f"tokens: {tokens_est/1e9:.2f}B | " f"预计: {eta_h/24:.1f}d", flush=True) # ═══════════════════════════════════════════════════════════ # 8. 主函数 # ═══════════════════════════════════════════════════════════ def main(): parser = argparse.ArgumentParser() parser.add_argument("--target", type=int, default=100000, help="目标条数") parser.add_argument("--phase", type=int, default=1, choices=[1, 2], help="1=生成种子, 2=语义扩展") parser.add_argument("--concurrency", type=int, default=5, help="API并发数") args = parser.parse_args() save_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pretrain_data') store = DataStore(save_dir) target = args.target if store.count >= target: print(f"✅ 已达目标 {target:,} 条") return print(f"\n{'='*50}") print(f" Phase {args.phase}: {'种子生成' if args.phase == 1 else '语义扩展'}") print(f" 目标: {target:,} 条") print(f" 并发: {args.concurrency}") print(f" 当前: {store.count:,}") print(f"{'='*50}\n") t_start = time.time() if args.phase == 1: # Phase 1: 纯API种子生成 seeds = store.get_seeds() # 从种子中的(domain, concept)对去重 used = set() for s in seeds: used.add((s.get("domain",""), s.get("concept",""))) print(f" 已有 {len(used)} 个不同(领域,概念)种子\n") while store.count < target: batch_items = [] tasks = [] for _ in range(args.concurrency): # 选一个未用过的(领域,概念) for attempt in range(20): d = random.choice(ALL_DOMAINS) c = random.choice(DOMAINS[d]) if (d, c) not in used or attempt > 15: break used.add((d, c)) tasks.append((d, c)) with ThreadPoolExecutor(max_workers=args.concurrency) as ex: futures = {ex.submit(generate_seed, d, c): (d, c) for d, c in tasks} for f in as_completed(futures): result = f.result() if result and score(result["text"]) >= 0.5: batch_items.append(result) if batch_items: added = store.add_seeds(batch_items) n = len(batch_items) print(f" +{n}条种子 | 领域: {batch_items[0]['domain']}, " f"概念: {batch_items[0]['concept']}...", flush=True) print_stats(store, t_start, "1", target) if store.count >= target: break elif args.phase == 2: # Phase 2: 语义扩展 seeds = store.get_seeds() if not seeds: print("❌ 没有种子数据,先跑 Phase 1") return print(f" 种子库: {len(seeds)} 条\n") used_seeds = set() expansion_tasks = [] while store.count < target: if not expansion_tasks: # 把所有种子打乱,生成扩展任务 random.shuffle(seeds) for s in seeds: if s["text"] in used_seeds: continue used_seeds.add(s["text"]) for vp in VARIATION_PROMPTS: expansion_tasks.append((s, vp)) print(f" 生成 {len(expansion_tasks)} 个扩展任务\n") # 取一批任务 batch = [] while len(batch) < args.concurrency and expansion_tasks: batch.append(expansion_tasks.pop(0)) if not batch: break with ThreadPoolExecutor(max_workers=args.concurrency) as ex: futures = {ex.submit(generate_variation, s, s["text"], vp): (s, vp) for s, vp in batch} for f in as_completed(futures): result = f.result() if result and score(result["text"]) >= 0.5: store.add_variations([result]) # 报告 seed_count = 0 if store.seed_path.exists(): with open(store.seed_path) as f: seed_count = sum(1 for _ in f if _.strip()) elapsed = max(time.time() - t_start, 1) rate = store.count / elapsed * 3600 eta_h = (target - store.count) / max(rate, 0.01) if rate > 0 else 0 tokens_est = store.count * 6000 done_tasks = max(1, len(used_seeds) * len(VARIATION_PROMPTS) - len(expansion_tasks)) print(f" [{time.strftime('%H:%M:%S')}] " f"累计: {store.count:,}/{target:,} " f"({store.count/max(target,1)*100:.1f}%) | " f"速率: {rate:.0f}条/h | " f"种子: {seed_count} | " f"扩展任务进度: {done_tasks} | " f"tokens: {tokens_est/1e9:.2f}B | " f"预计: {eta_h/24:.1f}d", flush=True) if store.count >= target: break # 最终报告 total_time = time.time() - t_start final_rate = store.count / max(total_time, 1) * 3600 tokens = store.count * 6000 path = store.output_path if args.phase == 2 else store.seed_path size_mb = os.path.getsize(path) / 1024 / 1024 if path.exists() else 0 print(f"\n{'='*50}") print(f" ✅ Phase {args.phase} 完成!") print(f" 总条数: {store.count:,}") print(f" 文件: {path}") print(f" 大小: {size_mb:.1f}MB") print(f" 估算tokens: {tokens/1e9:.2f}B") print(f" 耗时: {total_time/3600:.1f}h") print(f" 平均速率: {final_rate:.0f}条/h") print(f"{'='*50}") # 样例 print("\n📊 样例:") with open(path) as f: for i, line in enumerate(f): if i >= 2: break if line.strip(): item = json.loads(line.strip()) text = item["text"] turns = text.count("<|im_start|>") // 2 print(f"\n 条目{i+1}: {turns}轮 | {len(text):,}字 | {item.get('domain','')}") lines = text.split("\n")[:4] for l in lines: if l.strip(): role = "👤" if "user" in l else "🧑‍🏫" c = l.split("<|im_start|>")[1].split("<|im_end|>")[0].strip() print(f" {role} {c[:100]}") print(f" ...({turns}轮)") if __name__ == "__main__": main()