#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 小狸 AI - 通用知识预训练数据生成器 ==================================== 生成通用中文知识文本(非闲聊/非角色),用于模型预训练。 每批并发调用 Astron API 批量生成,目标 200 万条。 用法: python3 gen_pretrain.py # 启动生成 python3 gen_pretrain.py --count 50000 # 指定本轮目标 python3 gen_pretrain.py --resume # 断点续跑 """ import sys, os, json, time, random, hashlib, argparse 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 # ── 知识领域(20 大类,覆盖百科级知识) ──────────────── KNOWLEDGE_DOMAINS = { "天文宇宙": [ "太阳系的行星特征", "恒星的生命周期", "黑洞的形成与性质", "银河系的结构", "宇宙大爆炸理论", "星座与观测", "月球与潮汐", "彗星与小行星", "太空探索历史", "国际空间站", "火星探测任务", "引力波", ], "地球科学": [ "板块构造理论", "火山喷发原理", "地震成因与预警", "岩石分类", "矿物与矿石", "水循环过程", "气候带分布", "洋流与天气", "冰川与极地", "沙漠生态系统", "喀斯特地貌", "化石形成", ], "生命科学": [ "细胞结构与功能", "DNA与基因", "光合作用过程", "人体器官系统", "免疫系统工作原理", "病毒与细菌区别", "生态系统食物链", "进化论", "恐龙灭绝假说", "珍稀动物保护", "昆虫分类", "鸟类迁徙", ], "中国历史": [ "夏商周三代", "秦统一六国", "汉朝丝绸之路", "三国鼎立", "唐朝盛世", "宋朝科技成就", "元朝疆域", "明朝航海", "清朝康乾盛世", "鸦片战争", "辛亥革命", "抗日战争", ], "世界历史": [ "古埃及文明", "古希腊城邦", "罗马帝国", "大航海时代", "工业革命", "法国大革命", "第一次世界大战", "第二次世界大战", "冷战时期", "联合国成立", "人类登月", "互联网起源", ], "地理探索": [ "世界七大洲", "四大洋特征", "最长河流排名", "最高山峰", "亚马逊雨林", "撒哈拉沙漠", "珊瑚礁生态", "火山湖形成", "世界著名瀑布", "极光形成原理", "时区划分", "地图投影", ], "物理定律": [ "牛顿三大定律", "万有引力", "电磁感应", "相对论浅说", "量子力学入门", "光的折射反射", "声音传播特性", "热能传导方式", "浮力与压强", "简单机械原理", "电路基础知识", "核能原理", ], "化学元素": [ "元素周期表", "水的化学性质", "空气成分", "酸碱中和", "氧化还原反应", "常见化学元素", "分子与原子", "晶体结构", "有机化合物", "高分子材料", "催化剂作用", "化学反应类型", ], "数学原理": [ "勾股定理证明", "圆周率计算史", "黄金分割比例", "微积分基础思想", "概率论起源", "拓扑学入门", "斐波那契数列", "二进制原理", "对称与群论", "统计与平均数", "几何发展史", "数学建模", ], "科技发明": [ "印刷术发明", "蒸汽机改良", "电灯发明过程", "电话通信原理", "无线电技术", "晶体管与芯片", "计算机发展史", "互联网诞生", "人工智能简史", "机器人发展", "GPS定位原理", "基因编辑技术", ], "文化艺术": [ "中国书法流派", "水墨画技法", "京剧角色分类", "交响乐结构", "芭蕾舞起源", "电影发展史", "摄影构图原理", "建筑风格演变", "敦煌莫高窟", "文艺复兴艺术", "印象派绘画", "现代艺术流派", ], "植物世界": [ "植物分类系统", "开花植物特征", "树木年轮秘密", "食虫植物", "药用植物知识", "蕨类植物特点", "多肉植物养护", "水生植物适应", "果实种子传播", "植物激素作用", "菌类不是植物", "藻类多样性", ], "动物王国": [ "哺乳动物特征", "爬行动物演化", "两栖动物生命周期", "鸟类飞行原理", "鱼类分类", "海洋哺乳动物", "昆虫社会结构", "蛛形纲特征", "濒危动物保护", "动物伪装术", "动物迁徙路线", "宠物饲养常识", ], "医学健康": [ "人体骨骼结构", "心脏血液循环", "消化系统工作原理", "呼吸系统功能", "营养均衡膳食", "疫苗工作原理", "抗生素使用原则", "常见疾病预防", "睡眠科学研究", "运动健身原理", "心理健康知识", "急救基本常识", ], "工程技术": [ "桥梁结构类型", "摩天大楼建造", "地铁隧道挖掘", "水力发电原理", "风力发电技术", "太阳能利用", "高铁技术原理", "飞机制造原理", "船舶设计", "芯片制造工艺", "光纤通信", "3D打印技术", ], "语言文学": [ "汉语文字演变", "成语典故来源", "古诗词格律", "小说发展流派", "修辞手法分类", "甲骨文发现", "四大名著简介", "世界文学名著", "诗歌韵律基础", "写作技巧", "语言学基础", "翻译原则", ], "哲学思想": [ "儒家核心思想", "道家哲学", "佛学基本概念", "西方哲学起源", "逻辑学基础", "伦理学基本问题", "美学原理", "辩证法思想", "知行合一", "中庸之道", "存在主义简介", "实用主义哲学", ], "经济常识": [ "货币起源与职能", "供求关系原理", "市场经济学基础", "国际贸易概念", "GDP的含义", "通货膨胀原因", "银行职能", "股票基本原理", "保险基础知识", "个人理财入门", "经济危机历史", "数字经济趋势", ], "节日文化": [ "春节传统习俗", "中秋节起源", "端午节纪念", "重阳节含义", "圣诞节文化", "感恩节由来", "万圣节传统", "世界节日一览", "二十四节气", "传统婚礼习俗", "茶文化历史", "饮食文化地域差异", ], "日常生活": [ "垃圾分类知识", "交通安全规则", "消防安全常识", "食品安全知识", "环保节能技巧", "家居收纳方法", "时间管理技巧", "沟通表达能力", "礼仪基本规范", "旅行准备清单", "天气预测常识", "手机使用安全", ], } ALL_TOPICS = [] for domain, topics in KNOWLEDGE_DOMAINS.items(): for t in topics: ALL_TOPICS.append((domain, t)) def build_prompt(domain: str, topics: list, batch_size: int = 10) -> str: """构建批量生成prompt""" topic_list = "\n".join(f"{i+1}. {t}" for i, t in enumerate(topics)) return f"""你是一个百科全书作者,请用中文撰写 {batch_size} 条高质量的科普段落。 领域:{domain} 子话题: {topic_list} 要求: 1. 每条 60-150 字,言之有物,包含具体知识 2. 语言流畅、客观准确,适合用于训练语言模型 3. 每条独立成段,不互相引用 4. 不要用"首先/其次/总之"等连接词开头 5. 末尾不要问句,用句号结尾 6. 不要出现"小狸"等角色名 输出格式(严格按此格式): 【1】第一条知识段落内容。 【2】第二条知识段落内容。 【3】第三条知识段落内容。 ... 请开始生成:""" def parse_passages(text: str) -> list: """解析批量返回的文本""" passages = [] for line in text.strip().split('\n'): line = line.strip() if not line: continue # 匹配 【N】 或 [N] 或 N. 或 N、开头 for sep in ['】', ']', '.', '、', ' ']: if sep in line[:8]: content = line[line.index(sep)+len(sep):].strip() if len(content) >= 30: passages.append(content) break # fallback: 如果没有匹配到格式,尝试按句号分割 if not passages and len(text) >= 50: # 尝试按编号提取 import re matches = re.findall(r'(?:【(\d+)】|\[(\d+)\]|(\d+)[\.、])\s*(.+?)(?=(?:【|\n|$))', text + '\n', re.DOTALL) for m in matches: content = m[3].strip() if m[3] else (m[2] if m[2] else '') if len(content) >= 30: passages.append(content) return passages def generate_batch(domain: str, topics: list, batch_size: int = 10, temperature: float = 0.7) -> list: """一次 API 调用生成多条知识段落""" prompt = build_prompt(domain, topics, batch_size) text = call_astron(prompt, temperature=temperature) if not text: return [] return parse_passages(text) def format_check(text: str) -> float: """对生成文本做简单质量评分 0-1""" score = 0.5 # 长度惩罚 if len(text) < 30: score -= 0.3 if len(text) > 200: score -= 0.1 # 质量信号 has_period = text.rstrip().endswith(('。', '!', '?')) if has_period: score += 0.2 no_role = '小狸' not in text and '喵' not in text if no_role: score += 0.2 no_qmark = '?' not in text and '?' not in text if no_qmark: score += 0.1 return min(1.0, max(0.0, score)) # ── 主生成器 ──────────────────────────────────────────── class PretrainDataGenerator: 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 / 'pretrain_knowledge.json' self.temp_dir = self.save_dir / '_temp_batches' self.temp_dir.mkdir(exist_ok=True) # 加载已有进度 self.existing = set() self.existing_texts = [] if self.output_path.exists(): with open(self.output_path, 'r', encoding='utf-8') as f: self.existing_texts = json.load(f) for item in self.existing_texts: h = hashlib.md5(item['text'][:80].encode()).hexdigest() self.existing.add(h) self.total_generated = len(self.existing_texts) # 已保存的临时批次 self._load_temp_batches() print(f"📊 已有数据: {self.total_generated} 条") def _load_temp_batches(self): for f in sorted(self.temp_dir.glob('batch_*.json')): with open(f, 'r', encoding='utf-8') as fh: batch = json.load(fh) added = 0 for item in batch: h = hashlib.md5(item['text'][:80].encode()).hexdigest() if h not in self.existing: self.existing.add(h) self.existing_texts.append(item) added += 1 self.total_generated += added def add_batch(self, passages: list): if not passages: return 0 # 去重 + 质量过滤 new_items = [] for p in passages: p = p.strip() if len(p) < 30: continue score = format_check(p) if score < 0.4: continue h = hashlib.md5(p[:80].encode()).hexdigest() if h not in self.existing: self.existing.add(h) new_items.append({"text": p}) if not new_items: return 0 # 保存临时批次 batch_id = time.strftime('%Y%m%d_%H%M%S') + f'_{random.randint(1000,9999)}' temp_file = self.temp_dir / f'batch_{batch_id}.json' with open(temp_file, 'w', encoding='utf-8') as f: json.dump(new_items, f, ensure_ascii=False) self.existing_texts.extend(new_items) self.total_generated = len(self.existing_texts) # 每 5000 条合并一次主文件 if self.total_generated % 5000 < len(new_items): self.merge() return len(new_items) def merge(self): """合并临时文件到主输出""" all_temps = sorted(self.temp_dir.glob('batch_*.json')) merged = list(self.existing_texts) with open(self.output_path, 'w', encoding='utf-8') as f: json.dump(merged, f, ensure_ascii=False, indent=2) # 清理旧临时文件 for f in all_temps[:-100]: # 保留最近100个 f.unlink() print(f" 💾 合并保存: {self.output_path.name} ({len(merged)} 条)") def get_progress(self): return self.total_generated def main(): parser = argparse.ArgumentParser(description="通用预训练数据生成器") parser.add_argument("--count", type=int, default=2000000, help="目标条数") parser.add_argument("--resume", action="store_true", help="断点续跑") parser.add_argument("--workers", type=int, default=5, help="并发数") parser.add_argument("--batch", type=int, default=12, help="每轮每线程生成条数") args = parser.parse_args() save_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pretrain_data_gen') gen = PretrainDataGenerator(save_dir) if args.resume: print(f"📌 断点续跑,当前进度: {gen.get_progress()}") target = args.count remaining = target - gen.get_progress() if remaining <= 0: print(f"✅ 已达成目标 {target} 条!") return print(f"🎯 目标: {target} 条 | 还需: {remaining} 条") print(f"⚡ 并发: {args.workers} | 每线程: {args.batch} 条/轮") print(f"{'='*60}") workers = args.workers batch_per_call = args.batch total_done = gen.get_progress() t_start = time.time() round_num = 0 try: while total_done < target: round_num += 1 # 分配领域和话题 round_tasks = [] for _ in range(workers): domain = random.choice(list(KNOWLEDGE_DOMAINS.keys())) topics = random.sample(KNOWLEDGE_DOMAINS[domain], min(batch_per_call, len(KNOWLEDGE_DOMAINS[domain]))) temp = random.choice([0.6, 0.7, 0.75, 0.8, 0.85]) round_tasks.append((domain, topics, temp)) # 5 并发 all_passages = [] with ThreadPoolExecutor(max_workers=workers) as executor: futures = { executor.submit(generate_batch, d, t, batch_per_call, temp): i for i, (d, t, temp) in enumerate(round_tasks) } for f in as_completed(futures): passages = f.result() all_passages.extend(passages) added = gen.add_batch(all_passages) total_done = gen.get_progress() # 统计 elapsed = time.time() - t_start rate = total_done / max(elapsed, 1) * 60 eta_sec = (target - total_done) / max(rate / 60, 0.01) print(f" 第{round_num}轮: +{added}条 | " f"累计: {total_done}/{target} ({total_done/target*100:.1f}%) | " f"速率: {rate:.0f}条/分 | " f"预计剩余: {eta_sec/3600:.1f}h") if total_done >= target: break except KeyboardInterrupt: print("\n⏹️ 用户中断") # 最终合并 gen.merge() total_time = time.time() - t_start final_rate = total_done / max(total_time, 1) * 60 print(f"\n{'='*60}") print(f" ✅ 完成!") print(f" 总条数: {total_done}") print(f" 总耗时: {total_time/3600:.1f}h") print(f" 平均速率: {final_rate:.0f}条/分") print(f" 输出: {gen.output_path}") print(f"{'='*60}") # 统计领域分布 print("\n📊 样例展示 (前5条):") with open(gen.output_path, 'r', encoding='utf-8') as f: data = json.load(f) for i, item in enumerate(data[:5]): print(f" [{i+1}] {item['text'][:80]}...") if __name__ == "__main__": main()