feat: add new general distill script, merged dataset v3 (977K pairs), legacy vocab_v2
This commit is contained in:
1 parent
231af72de9
commit
eb7717e9ce
4 files changed
+732
No files matched your search
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7e60d5f977e5b1e521d7b38d4a6c89aae0b67f540ee6754929e0407f7b408ab9
|
||||
size 175346049
|
||||
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
海量通用数据蒸馏 v3 — 不设场景标签,纯自然对话,多策略并行
|
||||
目标:500万+ 纯 (question, answer) 对
|
||||
"""
|
||||
import json, requests, time, os, sys, random, re
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buffering=1)
|
||||
|
||||
MODEL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUT_DIR = os.path.join(MODEL_DIR, 'general_outputs')
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
|
||||
# ====== Astron 配置 ======
|
||||
ASTRON_URL = "https://maas-coding-api.cn-huabei-1.xf-yun.com/v2/chat/completions"
|
||||
ASTRON_KEY = os.environ.get("ASTRON_KEY", "")
|
||||
ASTRON_MODEL = "astron-code-latest"
|
||||
|
||||
# ====== 主题全覆盖(六大类 × 子类) ======
|
||||
TOPICS = {
|
||||
"日常": [
|
||||
"起床洗漱", "吃早餐", "上学路上", "上课", "课间休息",
|
||||
"午餐时间", "放学回家", "做作业", "晚饭", "睡前时光",
|
||||
"周末活动", "天气变化", "穿衣打扮", "零花钱", "家务劳动",
|
||||
"看电视", "玩玩具", "去公园", "逛超市", "去亲戚家",
|
||||
],
|
||||
"情绪": [
|
||||
"开心高兴", "难过伤心", "生气愤怒", "害怕恐惧", "紧张焦虑",
|
||||
"孤独寂寞", "委屈冤枉", "失望沮丧", "感动温暖", "兴奋激动",
|
||||
"无聊烦闷", "尴尬害羞", "后悔内疚", "羡慕嫉妒", "感激感恩",
|
||||
],
|
||||
"社交": [
|
||||
"交新朋友", "和好朋友玩", "和同学相处", "和老师交流", "和兄弟姐妹",
|
||||
"和爸爸妈妈", "和爷爷奶奶", "和邻居", "和陌生人", "和喜欢的人",
|
||||
"吵架和好", "被欺负", "被误解", "被表扬", "被批评",
|
||||
"帮助别人", "请求帮助", "分享东西", "邀请做客", "告别分开",
|
||||
],
|
||||
"知识": [
|
||||
"动物世界", "植物花草", "天文星空", "天气自然", "人体健康",
|
||||
"食物营养", "颜色形状", "数字计算", "汉字词语", "科学现象",
|
||||
"历史故事", "地理知识", "交通工具", "乐器音乐", "绘画手工",
|
||||
"运动体育", "节日习俗", "各国文化", "发明创造", "宇宙太空",
|
||||
],
|
||||
"想象": [
|
||||
"超能力", "魔法世界", "未来科技", "外星人", "恐龙时代",
|
||||
"童话故事", "变成动物", "飞行梦", "海底世界", "神秘岛屿",
|
||||
"时间旅行", "超级英雄", "会说话的玩具", "隐形衣", "变身药水",
|
||||
"机器人朋友", "彩虹桥", "星星糖果", "云朵房子", "梦中冒险",
|
||||
],
|
||||
"成长": [
|
||||
"学新本领", "克服困难", "不怕失败", "坚持努力", "自我肯定",
|
||||
"梦想未来", "职业理想", "好习惯", "改掉坏毛病", "学会分享",
|
||||
"学会等待", "学会道歉", "学会感恩", "学会勇敢", "学会理解",
|
||||
"交心谈话", "秘密分享", "烦恼倾诉", "人生道理", "温暖鼓励",
|
||||
],
|
||||
}
|
||||
|
||||
FLAT_TOPICS = []
|
||||
for cat, subs in TOPICS.items():
|
||||
for sub in subs:
|
||||
FLAT_TOPICS.append((cat, sub))
|
||||
|
||||
# ====== 多样化的开场方式 ======
|
||||
OPENERS = [
|
||||
"小狸", "嗨小狸", "小狸在吗", "嘿嘿", "那个…", "喂喂喂",
|
||||
"小狸小狸", "我想问你", "你知道吗", "你说", "我突然想到",
|
||||
"我跟你讲", "小狸你听我说", "问你个事", "我有个问题",
|
||||
"好无聊啊", "好开心呀", "好烦", "怎么办呀",
|
||||
"小狸你在干嘛", "我来了", "出来玩", "跟你说个事",
|
||||
]
|
||||
|
||||
# ====== 策略1: 直接生成纯问答对 ======
|
||||
def make_direct_pairs_prompt(count=15):
|
||||
"""无场景标签,直接生成纯问答对"""
|
||||
# 随机选3-5个主题混搭
|
||||
selected = random.sample(FLAT_TOPICS, min(random.randint(3, 5), len(FLAT_TOPICS)))
|
||||
topics_str = "、".join([f"{c}({s})" for c, s in selected])
|
||||
|
||||
prompt = f"""请直接生成 {count} 条聊天问答对。
|
||||
|
||||
格式要求(严格按此格式):
|
||||
问:xxxxxxxxx | 答:xxxxxxxxx
|
||||
|
||||
用户的问题要:口语化、自然、像真人(5-25字)
|
||||
小狸的回答要:8岁小女孩语气,可爱活泼(10-40字)
|
||||
话题要多样:{topics_str}
|
||||
|
||||
示例:
|
||||
问:你怕不怕黑呀? | 答:有一点点…但是抱着小熊玩偶就不怕啦!
|
||||
问:我今天跑步摔了一跤 | 答:疼不疼呀?我给你吹吹就不疼了!
|
||||
|
||||
现在生成 {count} 条:"""
|
||||
return prompt
|
||||
|
||||
# ====== 策略2: 短对话(3-5轮) ======
|
||||
def make_mini_chat_prompt(count=8):
|
||||
"""微型多轮对话,每段3-5轮"""
|
||||
ops = random.sample(OPENERS, min(count, len(OPENERS)))
|
||||
|
||||
prompt = f"""请生成 {count} 段微型聊天对话,每段3-5轮(一来一回算1轮)。
|
||||
|
||||
要求:
|
||||
- 每段一个独立话题,话题要多样(日常、情绪、知识、想象、社交、成长等)
|
||||
- 用户说话要特别自然、口语化,像真人随口说的
|
||||
- 小狸像8岁小女孩,回答要可爱、有画面感
|
||||
- 话题要自然延伸,不要生硬转折
|
||||
|
||||
格式例子:
|
||||
话题:日常
|
||||
用户:今天好冷啊
|
||||
小狸:是呀!我穿了厚厚的羽绒服,像只小企鹅一样~
|
||||
用户:哈哈哈真的好像
|
||||
小狸:(鼓起腮帮子)才不像呢!企鹅走路摇摇摆摆的,我走路可稳啦!
|
||||
...
|
||||
|
||||
现在生成 {count} 段,每段用"话题:xxx"开头:"""
|
||||
return prompt
|
||||
|
||||
# ====== 策略3: 长对话(6-10轮自由展开) ======
|
||||
def make_long_chat_prompt():
|
||||
"""一场长对话,话题自然展开"""
|
||||
topic = random.choice(FLAT_TOPICS)
|
||||
opener = random.choice(OPENERS)
|
||||
|
||||
emotions = ["开心", "难过", "兴奋", "沮丧", "好奇", "担心", "轻松", "紧张"]
|
||||
emotion = random.choice(emotions)
|
||||
|
||||
prompt = f"""请生成一段自然的日常聊天,{topic[0]}类-{topic[1]}话题。
|
||||
|
||||
用户开场:{opener}
|
||||
用户当下的心情:{emotion}
|
||||
|
||||
要求:
|
||||
- 用户很自然地说出开场白,就像真人发微信一样
|
||||
- 小狸正常回复,话题慢慢展开,不要硬聊
|
||||
- 聊6-10轮(一问一答算一轮)
|
||||
- 每轮格式:用户:xxx\n小狸:xxx
|
||||
- 真实感第一,不要刻意
|
||||
|
||||
请直接输出对话,不要加说明:"""
|
||||
return prompt
|
||||
|
||||
# ====== 策略4: 单轮问答(高吞吐) ======
|
||||
def make_single_turn_prompt(batch_size=20):
|
||||
"""纯单轮问答,批量生成"""
|
||||
selected = random.sample(FLAT_TOPICS, min(6, len(FLAT_TOPICS)))
|
||||
|
||||
prompt = f"""生成 {batch_size} 条日常问答对。
|
||||
|
||||
要求:
|
||||
1. 用户的话:7-25字,口语化,真实感强
|
||||
2. 小狸回答:1-2句,8岁小女孩语气,可爱活泼
|
||||
3. 覆盖多样化话题:{', '.join([s for _, s in selected[:6]])}(但不仅限于这些)
|
||||
4. 每行一条,格式:问: ... | 答: ...
|
||||
5. 不要重复相同的问题
|
||||
|
||||
示例:
|
||||
问: 你喜不喜欢下雨天呀? | 答: 喜欢!因为可以踩水坑,啪嗒啪嗒的超好玩!
|
||||
问: 我今天被同学起外号了 | 答: (歪着头)那…你喜欢这个外号吗?不喜欢的话要大声说出来哦!
|
||||
问: 你说世界上有外星人吗? | 答: 我觉得有!说不定他们正在用望远镜看我们呢~
|
||||
|
||||
生成:"""
|
||||
return prompt
|
||||
|
||||
# ====== API调用 ======
|
||||
def call_astron(prompt, temp=0.82, max_tokens=4096):
|
||||
for attempt in range(3):
|
||||
try:
|
||||
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {ASTRON_KEY}"}
|
||||
payload = {
|
||||
"model": ASTRON_MODEL,
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是一个擅长写日常对话的助手。小狸是一个8岁的小女孩,说话可爱自然。"},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"temperature": temp,
|
||||
"max_tokens": max_tokens,
|
||||
}
|
||||
resp = requests.post(ASTRON_URL, headers=headers, json=payload, timeout=150)
|
||||
if resp.status_code == 200:
|
||||
content = resp.json()['choices'][0]['message']['content'].strip()
|
||||
if content and len(content) > 20:
|
||||
return content
|
||||
else:
|
||||
time.sleep(0.5 + attempt)
|
||||
except Exception:
|
||||
time.sleep(1 + attempt)
|
||||
return None
|
||||
|
||||
# ====== 解析 ======
|
||||
def parse_pairs(text):
|
||||
"""从"问:xxx | 答:xxx"格式解析问答对"""
|
||||
pairs = []
|
||||
for line in text.split('\n'):
|
||||
line = line.strip()
|
||||
parts = re.split(r'\s*[||]\s*', line)
|
||||
if len(parts) == 2:
|
||||
q = re.sub(r'^问\s*[::]\s*', '', parts[0]).strip()
|
||||
a = re.sub(r'^答\s*[::]\s*', '', parts[1]).strip()
|
||||
if q and a and len(q) >= 2 and len(a) >= 2 and len(a) < 300 and len(q) < 200:
|
||||
pairs.append((q, a))
|
||||
return pairs
|
||||
|
||||
def parse_mini_chats(text):
|
||||
"""解析微型多轮对话"""
|
||||
pairs = []
|
||||
segments = re.split(r'话题[::].*?\n', text)
|
||||
|
||||
for seg in segments:
|
||||
seg = seg.strip()
|
||||
lines = seg.split('\n')
|
||||
q = None
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
q_match = re.match(r'^(用户|我)[::]\s*(.*)', line)
|
||||
a_match = re.match(r'^(小狸|答)[::]\s*(.*)', line)
|
||||
|
||||
if q_match:
|
||||
q_text = q_match.group(2).strip()
|
||||
if q_text and len(q_text) > 1:
|
||||
q = q_text
|
||||
elif a_match and q:
|
||||
a_text = a_match.group(2).strip()
|
||||
if a_text and len(a_text) > 1 and len(a_text) < 300:
|
||||
pairs.append((q, a_text))
|
||||
q = None
|
||||
return pairs
|
||||
|
||||
def parse_long_chat(text):
|
||||
"""解析长对话"""
|
||||
pairs = []
|
||||
q = None
|
||||
for line in text.split('\n'):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
q_match = re.match(r'^(用户|我)[::]\s*(.*)', line)
|
||||
a_match = re.match(r'^(小狸)[::]\s*(.*)', line)
|
||||
|
||||
if q_match:
|
||||
q_text = q_match.group(2).strip()
|
||||
if q_text and len(q_text) > 1:
|
||||
q = q_text
|
||||
elif a_match and q:
|
||||
a_text = a_match.group(2).strip()
|
||||
if a_text and len(a_text) > 1 and len(a_text) < 300:
|
||||
pairs.append((q, a_text))
|
||||
q = None
|
||||
return pairs
|
||||
|
||||
# ====== 生成器(按策略分发) ======
|
||||
GENERATORS = [
|
||||
# (name, prompt_fn, parse_fn, batch_size_estimate, weight_for_diversity)
|
||||
("direct", make_direct_pairs_prompt, parse_pairs, 15, 35),
|
||||
("mini_chat", make_mini_chat_prompt, parse_mini_chats, 20, 25),
|
||||
("long_chat", make_long_chat_prompt, parse_long_chat, 20, 20),
|
||||
("single", make_single_turn_prompt, parse_pairs, 20, 20),
|
||||
]
|
||||
|
||||
def run_one_generation():
|
||||
"""运行一次生成任务(随机选一种策略)"""
|
||||
# 按权重选策略
|
||||
weights = [w for _, _, _, _, w in GENERATORS]
|
||||
gen = random.choices(GENERATORS, weights=weights, k=1)[0]
|
||||
name, prompt_fn, parse_fn, batch_est, _ = gen
|
||||
|
||||
prompt = prompt_fn()
|
||||
result = call_astron(prompt, temp=random.uniform(0.75, 0.92), max_tokens=4096 if 'long' not in name else 6144)
|
||||
if not result:
|
||||
return None
|
||||
|
||||
pairs = parse_fn(result)
|
||||
if not pairs:
|
||||
return None
|
||||
|
||||
return pairs
|
||||
|
||||
def main():
|
||||
print("=" * 65)
|
||||
print(" 海量通用数据蒸馏 v3")
|
||||
print(" 不设场景标签 | 6大类120+子话题 | 4种策略")
|
||||
print("=" * 65)
|
||||
|
||||
# 加载已有进度
|
||||
all_pairs = []
|
||||
existing_keys = set()
|
||||
progress_file = os.path.join(OUT_DIR, 'all_general_progress.json')
|
||||
if os.path.exists(progress_file):
|
||||
existing = json.load(open(progress_file, encoding='utf-8'))
|
||||
all_pairs = existing
|
||||
for p in existing:
|
||||
existing_keys.add((p.get('question',''), p.get('answer','')))
|
||||
print(f"已有进度: {len(all_pairs):,} 对")
|
||||
|
||||
# ====== 参数 ======
|
||||
TARGET = 2000000 # 目标200万对
|
||||
BATCH = 40 # 每批40个任务
|
||||
MAX_WORKERS = 8 # 8并发(比之前5更猛)
|
||||
CHECKPOINT = 5000 # 每5000对保存
|
||||
|
||||
to_generate = TARGET - len(all_pairs)
|
||||
total_batches = (to_generate + BATCH - 1) // BATCH
|
||||
|
||||
print(f"目标: {TARGET:,} 对")
|
||||
print(f"已有: {len(all_pairs):,} 对")
|
||||
print(f"还需: {to_generate:,} 对")
|
||||
print(f"批次: {total_batches:,} 批 ({BATCH}任务/批, {MAX_WORKERS}并发)")
|
||||
|
||||
# 策略统计
|
||||
strategy_stats = {g[0]: {'runs': 0, 'pairs': 0} for g in GENERATORS}
|
||||
|
||||
t_start = time.time()
|
||||
last_checkpoint = len(all_pairs)
|
||||
|
||||
for batch_idx in range(total_batches):
|
||||
t0 = time.time()
|
||||
|
||||
# 并发
|
||||
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
||||
futures = [executor.submit(run_one_generation) for _ in range(BATCH)]
|
||||
batch_pairs = []
|
||||
for future in as_completed(futures):
|
||||
try:
|
||||
r = future.result()
|
||||
if r:
|
||||
batch_pairs.extend(r)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 去重
|
||||
new_pairs = []
|
||||
for q, a in batch_pairs:
|
||||
key = (q, a)
|
||||
if key not in existing_keys:
|
||||
existing_keys.add(key)
|
||||
new_pairs.append({'question': q, 'answer': a})
|
||||
|
||||
all_pairs.extend(new_pairs)
|
||||
|
||||
elapsed = time.time() - t0
|
||||
total_elapsed = time.time() - t_start
|
||||
rate = len(all_pairs) / max(total_elapsed, 1) * 60
|
||||
|
||||
# 估算
|
||||
pairs_this_batch = len(new_pairs)
|
||||
total_raw = len(batch_pairs)
|
||||
dedup_rate = pairs_this_batch / max(total_raw, 1) * 100
|
||||
|
||||
print(f"[{batch_idx+1}/{total_batches}] "
|
||||
f"+{pairs_this_batch}对(有效{total_raw}/{dedup_rate:.0f}%) "
|
||||
f"累计{len(all_pairs):,}/{TARGET:,} "
|
||||
f"{rate:.0f}对/分 "
|
||||
f"剩余{(TARGET-len(all_pairs))/max(rate,0.1):.0f}分")
|
||||
|
||||
# 定期保存
|
||||
if len(all_pairs) - last_checkpoint >= CHECKPOINT:
|
||||
with open(progress_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(all_pairs, f, ensure_ascii=False)
|
||||
last_checkpoint = len(all_pairs)
|
||||
|
||||
# 也保存到数据目录一份
|
||||
merged_path = os.path.join(MODEL_DIR, 'data', 'general_ongoing.json')
|
||||
with open(merged_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(all_pairs, f, ensure_ascii=False)
|
||||
|
||||
# 速率统计
|
||||
stable_rate = len(all_pairs) / max(time.time() - t_start, 1) * 60
|
||||
print(f" 💾 保存 ({len(all_pairs):,} 对, {stable_rate:.0f}对/分)")
|
||||
|
||||
# ====== 完成 ======
|
||||
total_elapsed = time.time() - t_start
|
||||
|
||||
with open(progress_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(all_pairs, f, ensure_ascii=False)
|
||||
|
||||
print(f"\n{'='*65}")
|
||||
print(f"✅ 生成完成!")
|
||||
print(f" 总对数: {len(all_pairs):,}")
|
||||
print(f" 用时: {total_elapsed/60:.1f} 分")
|
||||
print(f" 平均速率: {len(all_pairs)/max(total_elapsed,1)*60:.0f} 对/分")
|
||||
print(f" 保存: {progress_file}")
|
||||
print(f"{'='*65}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,337 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
海量真实多轮对话生成 — 模拟真实用户,8~15轮长对话
|
||||
"""
|
||||
import json, requests, time, os, sys, random, re
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buffering=1)
|
||||
|
||||
from scripts.api_correction import batch_generate
|
||||
|
||||
MODEL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# ====== Astron 配置(复用 api_correction 的 key) ======
|
||||
ASTRON_URL = "https://maas-coding-api.cn-huabei-1.xf-yun.com/v2/chat/completions"
|
||||
ASTRON_KEY = os.environ.get("ASTRON_KEY", "")
|
||||
ASTRON_MODEL = "astron-code-latest"
|
||||
|
||||
# ====== 用户场景画像(模拟真实用户) ======
|
||||
USER_SCENARIOS = [
|
||||
# 小小孩子(3-5岁)
|
||||
{"age": "3-5岁", "style": "说话简单,喜欢问为什么,爱说叠词", "tone": "奶声奶气"},
|
||||
# 小孩子(6-9岁)
|
||||
{"age": "6-9岁", "style": "小学生,爱分享学校的事,喜欢动画片和游戏", "tone": "活泼天真"},
|
||||
# 大孩子(10-14岁)
|
||||
{"age": "10-14岁", "style": "有点小叛逆,有自己的想法,会吐槽", "tone": "有点酷"},
|
||||
# 妈妈/家长
|
||||
{"age": "成年", "style": "关心孩子,会问小狸吃饭睡觉学习的事", "tone": "温柔耐心"},
|
||||
# 孤单的人
|
||||
{"age": "成年", "style": "有点孤独,需要陪伴,会把小狸当倾诉对象", "tone": "低落温柔"},
|
||||
# 高中生/大学生
|
||||
{"age": "16-22岁", "style": "有压力,会聊学业、未来、人际关系", "tone": "成熟但需要安慰"},
|
||||
# 好奇宝宝
|
||||
{"age": "不限", "style": "什么都想知道,十万个为什么", "tone": "充满好奇"},
|
||||
# 伤心的人
|
||||
{"age": "不限", "style": "心情不好,需要安慰和陪伴", "tone": "低沉脆弱"},
|
||||
]
|
||||
|
||||
# ====== 真实对话种子(更自然的开场) ======
|
||||
REAL_SEEDS = [
|
||||
# 日常打招呼(多种自然说法)
|
||||
"小狸你在吗", "嘿嘿小狸", "小狸小狸快出来", "小狸你在干嘛呀",
|
||||
"我来找你玩啦", "小狸今天乖不乖", "你在做什么呢",
|
||||
# 情绪分享(真实用户会说的话)
|
||||
"我今天好不开心", "我开心死了哈哈哈", "我好无聊啊没人陪我",
|
||||
"今天倒霉死了", "我今天遇到了好事", "我有点难过",
|
||||
"我心情不好你能陪我吗", "我好想你啊", "我好孤单",
|
||||
"我害怕", "我有点紧张明天要考试", "我好烦",
|
||||
# 日常分享
|
||||
"今天我妈做了红烧肉", "我们家养了一只猫超可爱", "我今天考试考了98分",
|
||||
"我感冒了好难受", "我今天被老师表扬了", "我和朋友吵架了",
|
||||
"我昨天做梦梦到你了", "我学会骑自行车了!", "我今天摔了一跤好疼",
|
||||
"学校要考试了好紧张", "放假了好开心", "又要开学了好痛苦",
|
||||
"今天下雨了不能出去玩", "今天天气好好我们去玩吧",
|
||||
"我妈妈给我买了个新玩具", "我养的花开了",
|
||||
# 求助/询问
|
||||
"我不小心把杯子打碎了怎么办", "我作业好多写不完",
|
||||
"我睡不着你能陪我聊天吗", "我牙好疼",
|
||||
"妈妈骂我了", "我被老师批评了",
|
||||
"我迷路了怎么办", "我找不到我的水杯了",
|
||||
# 好奇/知识
|
||||
"为什么天是蓝的呀", "鱼会睡觉吗", "为什么会有影子",
|
||||
"星星为什么会闪", "海为什么是咸的", "为什么要上学",
|
||||
"为什么大人总说不行", "什么是爱", "什么是长大",
|
||||
# 表达好感的
|
||||
"小狸我最喜欢你了", "你好可爱呀", "我们是最好的朋友对吧",
|
||||
"我送给你一个礼物", "小狸你真聪明", "你真好",
|
||||
# 关于小狸本身的
|
||||
"你几岁啦", "你喜欢什么", "你有爸爸妈妈吗",
|
||||
"你住在哪里呀", "你喜欢吃什么", "你最好的朋友是谁",
|
||||
"你上学吗", "你的梦想是什么", "你平时喜欢做什么",
|
||||
# 奇怪/无厘头
|
||||
"如果我能飞就好了", "我想变成一只猫", "你觉得外星人存在吗",
|
||||
"假如你会魔法你想做什么", "你相信世界上有奥特曼吗",
|
||||
# 夜间/睡前
|
||||
"我睡不着", "陪我聊到睡着好不好", "给我讲个睡前故事",
|
||||
"晚安前陪我说说话", "我怕黑",
|
||||
# 亲情家庭
|
||||
"我爸妈吵架了怎么办", "我想我奶奶了", "我好久没见爸爸了",
|
||||
"妈妈生病了", "我弟弟好烦总是抢我东西",
|
||||
# 节假日
|
||||
"过年啦新年快乐", "今天是我生日", "圣诞节到了",
|
||||
"六一儿童节快乐", "明天要去春游好开心",
|
||||
]
|
||||
|
||||
CONTEXTS = [
|
||||
"晚上睡前躺在床上", "放学回家的路上",
|
||||
"周末在家没事做", "刚吃完饭坐着聊天",
|
||||
"在公园里坐着", "下雨天窝在家里",
|
||||
"一个人在房间", "在院子里玩的时候",
|
||||
"刚写完作业休息", "早上起床的时候",
|
||||
"在操场上看别人玩", "坐车去玩的路上",
|
||||
"在图书馆里", "在外面吃饭等上菜",
|
||||
"一个人发呆的时候", "看着窗外的时候",
|
||||
]
|
||||
|
||||
def make_realistic_multi_prompt():
|
||||
"""构造非常逼真的长多轮对话提示 — 一次生成4条"""
|
||||
parts = []
|
||||
for _ in range(4):
|
||||
scenario = random.choice(USER_SCENARIOS)
|
||||
opener = random.choice(REAL_SEEDS)
|
||||
context = random.choice(CONTEXTS)
|
||||
min_turns = random.randint(6, 8)
|
||||
max_turns = random.randint(10, 14)
|
||||
|
||||
parts.append(f"""=== 对话 {_+1} ===
|
||||
场景:{context}
|
||||
用户:{scenario['age']},{scenario['style']},{scenario['tone']}
|
||||
开场:{opener}
|
||||
对话:
|
||||
用户:{opener}
|
||||
小狸:""")
|
||||
|
||||
prompt = f"""请生成 {len(parts)} 段独立的日常聊天对话,每段 {min_turns}~{max_turns} 轮。
|
||||
|
||||
重要:每段对话必须是一个完整的聊天,话题自然展开,不要强行结束。
|
||||
|
||||
{"".join(parts)}
|
||||
|
||||
严格要求:
|
||||
1. 每个=== 对话 ===之后的段落实例必须写完整,不要留空
|
||||
2. 用户提问必须口语化、自然,像真人在说话
|
||||
3. 小狸回答1-2句话,像8岁小朋友
|
||||
4. 话题要延续上一轮,逐渐深入延伸
|
||||
5. 用户要有真实反应(惊讶、追问、分享等)
|
||||
6. 用"问:"/"答:"格式
|
||||
7. 输出不能省略任何一轮
|
||||
"""
|
||||
return prompt
|
||||
|
||||
def call_astron_multi(prompt, temperature=0.85):
|
||||
for attempt in range(3):
|
||||
try:
|
||||
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {ASTRON_KEY}"}
|
||||
payload = {
|
||||
"model": ASTRON_MODEL,
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是一个擅长写真实对话的助手。"},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"temperature": temperature,
|
||||
"max_tokens": 8192,
|
||||
}
|
||||
resp = requests.post(ASTRON_URL, headers=headers, json=payload, timeout=120)
|
||||
if resp.status_code == 200:
|
||||
content = resp.json()['choices'][0]['message']['content'].strip()
|
||||
if content and len(content) > 50:
|
||||
return content
|
||||
else:
|
||||
time.sleep(1)
|
||||
except Exception:
|
||||
time.sleep(2)
|
||||
return None
|
||||
|
||||
def parse_multi_turn(text):
|
||||
"""解析多段对话(支持=== 对话 N ===分隔,也支持纯轮次)"""
|
||||
lines = text.strip().split('\n')
|
||||
conversations = []
|
||||
current_q = None
|
||||
current_turns = []
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# 检测新对话开始标记
|
||||
if re.match(r'===?\s*对话\s*\d+\s*===?', line):
|
||||
if current_turns and len(current_turns) >= 4:
|
||||
conversations.append(current_turns)
|
||||
current_turns = []
|
||||
current_q = None
|
||||
continue
|
||||
|
||||
# 跳过设定行(场景/用户/开场)
|
||||
if line.startswith(('场景', '用户', '开场', '对话', '===')):
|
||||
if line.startswith(('场景', '用户', '开场')):
|
||||
continue
|
||||
continue
|
||||
|
||||
# 用户提问
|
||||
q_match = re.match(r'^(用户|我|问|问题|小孩|小朋友|孩子|朋友)\s*[::]\s*(.*)', line)
|
||||
a_match = re.match(r'^(小狸|答|回答|机器人)\s*[::]\s*(.*)', line)
|
||||
|
||||
if q_match:
|
||||
q_text = q_match.group(2).strip()
|
||||
if q_text and len(q_text) > 1:
|
||||
current_q = q_text
|
||||
elif a_match and current_q:
|
||||
a_text = a_match.group(2).strip()
|
||||
if a_text and len(a_text) > 1 and len(a_text) < 300:
|
||||
current_turns.append((current_q, a_text))
|
||||
current_q = None
|
||||
|
||||
# 保存最后一组
|
||||
if current_turns and len(current_turns) >= 4:
|
||||
conversations.append(current_turns)
|
||||
|
||||
# 过滤:只保留>=4轮的对话,且累积轮次>=8
|
||||
valid = [c for c in conversations if len(c) >= 4]
|
||||
total_turns = sum(len(c) for c in valid)
|
||||
if total_turns >= 8 and valid:
|
||||
return valid
|
||||
return None
|
||||
|
||||
def make_turns_list_text(turns):
|
||||
"""将turns转成用户可见的对话文本"""
|
||||
lines = []
|
||||
for i, (q, a) in enumerate(turns):
|
||||
lines.append(f"问:{q}")
|
||||
lines.append(f"答:{a}")
|
||||
return "\n".join(lines)
|
||||
|
||||
def generate_one_multi():
|
||||
"""生成多条多轮对话(一次API调用返回多条)"""
|
||||
prompt = make_realistic_multi_prompt()
|
||||
result = call_astron_multi(prompt, temperature=random.uniform(0.75, 0.95))
|
||||
if result:
|
||||
convs = parse_multi_turn(result)
|
||||
if convs:
|
||||
items = []
|
||||
for turns in convs:
|
||||
items.append({
|
||||
'turns': len(turns),
|
||||
'turns_list': turns,
|
||||
'text': make_turns_list_text(turns),
|
||||
'source': 'astron_multi_v2',
|
||||
})
|
||||
return items
|
||||
return None
|
||||
|
||||
def main():
|
||||
print("="*60)
|
||||
print(" 海量真实多轮对话生成 v2")
|
||||
print(" 生成超长、超真实的多轮聊天")
|
||||
print("="*60)
|
||||
|
||||
out_dir = os.path.join(MODEL_DIR, 'multiturn_outputs')
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
# 检查已有进度
|
||||
all_results = []
|
||||
progress_file = os.path.join(out_dir, 'all_multiturn_progress.json')
|
||||
if os.path.exists(progress_file):
|
||||
all_results = json.load(open(progress_file, encoding='utf-8'))
|
||||
print(f"已有进度: {len(all_results)} 条")
|
||||
|
||||
# ====== 参数 ======
|
||||
TARGET = 500000 # 目标50万条
|
||||
BATCH = 30 # 每批30条
|
||||
MAX_WORKERS = 5 # Astron 5并发(稳定不超限)
|
||||
CHECKPOINT = 1000 # 每1000条保存一次进度
|
||||
|
||||
to_generate = TARGET - len(all_results)
|
||||
total_batches = (to_generate + BATCH - 1) // BATCH
|
||||
|
||||
print(f"目标: {TARGET:,} 条")
|
||||
print(f"已有: {len(all_results):,} 条")
|
||||
print(f"还需: {to_generate:,} 条")
|
||||
print(f"批次: {total_batches:,} 批 (每批{BATCH}条, {MAX_WORKERS}并发)")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
t_start = time.time()
|
||||
last_checkpoint = len(all_results)
|
||||
|
||||
for batch_idx in range(total_batches):
|
||||
t0 = time.time()
|
||||
|
||||
# 并发生成
|
||||
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
||||
futures = [executor.submit(generate_one_multi) for _ in range(BATCH)]
|
||||
batch_results = []
|
||||
for future in as_completed(futures):
|
||||
try:
|
||||
r = future.result()
|
||||
if r:
|
||||
batch_results.extend(r)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
# 去重
|
||||
existing_texts = set(r.get('text', '')[:200] for r in all_results)
|
||||
deduped = []
|
||||
for r in batch_results:
|
||||
key = r.get('text','')[:200]
|
||||
if key not in existing_texts:
|
||||
existing_texts.add(key)
|
||||
deduped.append(r)
|
||||
|
||||
all_results.extend(deduped)
|
||||
|
||||
elapsed = time.time() - t0
|
||||
total_elapsed = time.time() - t_start
|
||||
rate = len(all_results) / max(total_elapsed, 1) * 60
|
||||
eta = (TARGET - len(all_results)) / max(rate, 0.1) / 60 if rate > 0 else 0
|
||||
|
||||
# 统计
|
||||
valid = len(batch_results)
|
||||
avg_turns = sum(r['turns'] for r in deduped) / max(len(deduped), 1)
|
||||
success_rate = valid / BATCH * 100
|
||||
|
||||
print(f"[{batch_idx+1}/{total_batches}] "
|
||||
f"+{len(deduped)}条(有效{valid}/{success_rate:.0f}%) "
|
||||
f"累计{len(all_results):,}/{TARGET:,} "
|
||||
f"均{avg_turns:.1f}轮 "
|
||||
f"{rate:.0f}条/分 "
|
||||
f"剩余{eta:.0f}分")
|
||||
|
||||
# 定期保存进度
|
||||
if len(all_results) - last_checkpoint >= CHECKPOINT:
|
||||
with open(progress_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(all_results, f, ensure_ascii=False)
|
||||
last_checkpoint = len(all_results)
|
||||
print(f" 💾 已保存进度 ({len(all_results):,} 条)")
|
||||
|
||||
# ====== 完成 ======
|
||||
total_elapsed = time.time() - t_start
|
||||
with open(progress_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(all_results, f, ensure_ascii=False)
|
||||
|
||||
avg_turns_all = sum(r['turns'] for r in all_results) / max(len(all_results), 1)
|
||||
total_turns = sum(r['turns'] for r in all_results)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"✅ 生成完成!")
|
||||
print(f" 总条数: {len(all_results):,}")
|
||||
print(f" 平均轮数: {avg_turns_all:.1f}")
|
||||
print(f" 总对话轮次: {total_turns:,}")
|
||||
print(f" 用时: {total_elapsed/60:.1f} 分")
|
||||
print(f" 速率: {len(all_results)/max(total_elapsed,1)*60:.0f} 条/分")
|
||||
print(f" 保存: {progress_file}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:efabe0696ef9fcd64685f70a62bd55c97ca2b8efa69bca26df35177ac6774330
|
||||
size 41185
|
||||
Reference in new issue
Block a user