Files
JspaceAI/main_chat.py
XiuchengWu 81f6fd58e0 feat: add scripts for fetching and training educational corpus
- Implemented `fetch_corpus.py` to scrape Chinese educational content from Wikipedia, covering subjects like Chinese, Math, English, Chemistry, Politics, History, Geography, Physics, and Biology.
- Developed `fetch_corpus_incremental.py` for incremental fetching of missing entries, with automatic retries for rate limiting and support for resuming interrupted downloads.
- Created `train_chat.py` for training a dialogue model, including corpus cleaning, vocabulary management, and staged training with adaptive learning rates.
2026-07-07 17:42:24 +08:00

252 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
JspaceAI —— 对话版(只有语言,控制台交互)
基于字符级语言模型 + 自主进化EWC + 经验回放 + 专家可塑性)。
交互式对话:用户输入文本,模型生成回复,同时持续学习。
模式:
--mode chat: 交互对话(默认)
--mode train: 先在 Shakespeare 语料上预训练若干步,再进入对话
--mode generate: 给定提示词一次性生成
运行:
python main_chat.py
python main_chat.py --mode train --steps 100
python main_chat.py --mode generate --prompt "To be"
"""
from __future__ import annotations
import argparse
import torch
import numpy as np
from pathlib import Path
from jspaceai import (
LanguageConfig, JSpaceLanguageModel, EvolutionTrainer,
CharTokenizer, load_corpus,
)
from train_chat import clean_corpus
def get_config(vocab_size: int) -> LanguageConfig:
return LanguageConfig(
vocab_size=vocab_size,
embed_dim=48, input_dim=24,
workspace_dim=96, expert_dim=48,
num_experts=10, num_wells=6,
ode_steps=3, dt=0.1, tau_w=0.5,
jacobian_sparsity=24, noise_std=0.002,
use_rk4=True, use_layer_norm=True,
)
def load_or_init_model(device: str):
"""加载已保存的模型或初始化新模型"""
text = clean_corpus() # 清洗后语料(繁简统一+过滤)
tokenizer = CharTokenizer.from_text(text) # 不截断,保留所有字符
config = get_config(tokenizer.vocab_size)
model = JSpaceLanguageModel(config).to(device)
mp = Path('outputs/chat_model.pt')
if mp.exists():
try:
ckpt = torch.load(mp, map_location=device, weights_only=False)
model.load_state_dict(ckpt['model'])
print(f"已加载模型: {mp}(上次保存的对话状态)")
except Exception:
print("模型加载失败,全新初始化")
else:
print("全新初始化(首次对话)")
return model, config, tokenizer, text
def save_model(model, config, tokenizer):
Path('outputs').mkdir(exist_ok=True)
torch.save({
'model': model.state_dict(),
'config': config,
'tokenizer_chars': tokenizer.chars,
}, 'outputs/chat_model.pt')
def train(model, tokenizer, text, n_steps: int, device: str):
"""在 Shakespeare 语料上预训练
训练时临时关闭 RK4 用 Euler 加速(快 4 倍),训练完恢复 RK4。
"""
print("\n" + "=" * 60)
print(f"预训练 {n_steps}Shakespeare 语料)")
print("=" * 60)
config = model.config
# 训练时临时关 RK4 加速Euler 快 4 倍)
original_rk4 = config.use_rk4
for expert in model.experts:
expert.use_rk4 = False
print(f"训练模式: Euler加速训练后恢复 RK4")
trainer = EvolutionTrainer(
model, config, lr=5e-3, ewc_lambda=0.05, device=device,
)
chunks = [text[i:i+200] for i in range(0, len(text), 200)]
trainer.evolve(
chunks, tokenizer,
seq_len=64, batch_size=8,
consolidate_every=50, generate_every=50,
max_steps=n_steps, prompt_text="To be",
)
# 恢复 RK4
for expert in model.experts:
expert.use_rk4 = original_rk4
save_model(model, config, tokenizer)
print(f"\n模型已保存: outputs/chat_model.pt")
def generate_response(model, tokenizer, prompt: str, n_new: int = 100,
temperature: float = 0.8, top_k: int = 5) -> str:
"""生成回复"""
# 把用户输入编码(未知字符用 0
prompt_ids = tokenizer.encode(prompt)
if not prompt_ids:
prompt_ids = [0]
generated = model.generate(
prompt_ids, n_new=n_new, temperature=temperature, top_k=top_k,
)
return tokenizer.decode(generated)
def online_learn(model, config, tokenizer, text: str, user_input: str, device: str):
"""在线学习用户输入 + 回放一段 Shakespeare 防遗忘"""
import torch.nn.functional as F
# 把用户输入作为新语料学习
user_tokens = tokenizer.encode(user_input)
if len(user_tokens) < 4:
return # 太短不学
# 重复用户输入凑够 seq_len
seq_len = 48
if len(user_tokens) < seq_len:
user_tokens = user_tokens * (seq_len // len(user_tokens) + 1)
user_seq = user_tokens[:seq_len]
token_tensor = torch.tensor([user_seq], dtype=torch.long).to(device)
# forward + next-token loss
model.train()
model.zero_grad()
logits, _ = model(token_tensor)
pred = logits[:, :-1]
target = token_tensor[:, 1:]
loss = F.cross_entropy(
pred.reshape(-1, config.vocab_size),
target.reshape(-1),
)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
# 手动 SGD stepEvolutionTrainer 内部有 EWC这里简化用直接 step
with torch.no_grad():
for p in model.parameters():
if p.grad is not None:
p -= 5e-3 * p.grad
model.eval()
return loss.item()
def chat(model, config, tokenizer, text, device: str):
"""交互对话循环"""
print("\n" + "=" * 60)
print("JspaceAI 对话模式")
print("=" * 60)
print("输入文本与模型对话,模型会持续学习你的输入。")
print("命令: /quit 退出 /save 保存 /reset 重置 /train N 预训练N步")
print("=" * 60 + "\n")
model.eval()
while True:
try:
user_input = input("你: ").strip()
except (EOFError, KeyboardInterrupt):
print("\n再见")
break
if not user_input:
continue
if user_input.startswith('/'):
cmd = user_input.lower()
if cmd in ('/quit', '/exit', '/q'):
save_model(model, config, tokenizer)
print(f"(状态已保存到 outputs/chat_model.pt)")
print("再见")
break
elif cmd == '/save':
save_model(model, config, tokenizer)
print(f"(已保存到 outputs/chat_model.pt)")
continue
elif cmd == '/reset':
model = JSpaceLanguageModel(config).to(device)
print("(模型已重置为随机初始化)")
continue
elif cmd.startswith('/train'):
parts = cmd.split()
n = int(parts[1]) if len(parts) > 1 else 50
train(model, tokenizer, text, n, device)
model.eval()
continue
else:
print("未知命令。可用: /quit /save /reset /train N")
continue
# 在线学习用户输入
loss = online_learn(model, config, tokenizer, text, user_input, device)
# 生成回复
response = generate_response(
model, tokenizer, user_input,
n_new=80, temperature=0.8, top_k=5,
)
print(f"AI: {response}")
if loss is not None:
print(f" (学习 loss={loss:.3f})")
def generate_once(model, tokenizer, prompt: str, n_new: int = 200):
"""一次性生成"""
model.eval()
response = generate_response(model, tokenizer, prompt, n_new=n_new,
temperature=0.7, top_k=5)
print(f"提示: {prompt}")
print(f"生成: {response}")
def main():
p = argparse.ArgumentParser(description='JspaceAI 对话版')
p.add_argument('--mode', default='chat',
choices=['chat', 'train', 'generate'],
help='运行模式: chat=交互, train=预训练, generate=一次性生成')
p.add_argument('--steps', type=int, default=600, help='train 模式步数')
p.add_argument('--prompt', default='To be', help='generate 模式提示词')
p.add_argument('--device', default='cpu', help='设备 (cpu/cuda/mps/auto)')
args = p.parse_args()
dev = args.device
if dev == 'auto':
dev = 'cuda' if torch.cuda.is_available() else (
'mps' if torch.backends.mps.is_available() else 'cpu')
model, config, tokenizer, text = load_or_init_model(dev)
if args.mode == 'chat':
chat(model, config, tokenizer, text, dev)
elif args.mode == 'train':
train(model, tokenizer, text, args.steps, dev)
elif args.mode == 'generate':
generate_once(model, tokenizer, args.prompt)
if __name__ == '__main__':
main()