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.
This commit is contained in:
2026-07-07 17:42:24 +08:00
parent d5affe4458
commit 81f6fd58e0
96 changed files with 53920 additions and 17 deletions

View File

@@ -19,7 +19,7 @@ from .baselines import FlatBaseline
from .task import ContinuousSequenceTask
from .trainer import Trainer
from .language_data import CharTokenizer, CharDataset, load_shakespeare
from .language_data import CharTokenizer, CharDataset, load_shakespeare, load_chinese_corpus, load_textbook_corpus, load_corpus
from .language_model import (
LanguageConfig,
JSpaceLanguageModel,
@@ -88,6 +88,7 @@ __all__ = [
"ContinuousSequenceTask", "Trainer",
# 语言建模
"CharTokenizer", "CharDataset", "load_shakespeare",
"load_chinese_corpus", "load_textbook_corpus", "load_corpus",
"LanguageConfig", "JSpaceLanguageModel",
"ExperienceReplay", "EWCOptimizer", "ExpertPlasticity",
# J-lens 可解释性

View File

@@ -19,14 +19,35 @@ from torch.utils.data import Dataset
@dataclass
class CharTokenizer:
"""字符级 tokenizer支持训练时新字符的动态加入"""
"""字符级 tokenizer支持训练时新字符的动态加入
未知字符统一映射到 idx 0unk。构建时 idx 0 固定是 '<unk>'
"""
chars: list[str]
char_to_idx: dict[str, int]
idx_to_char: dict[int, str]
@classmethod
def from_text(cls, text: str) -> "CharTokenizer":
chars = sorted(set(text))
def from_text(cls, text: str, max_vocab: int | None = None) -> "CharTokenizer":
"""从文本构建 tokenizer。
Args:
text: 训练文本
max_vocab: 最大词汇表大小。如果字符种类超过此数,
按字符频率排序只保留前 max_vocab-1 个高频字符,
其余映射到 <unk>。None 则保留全部。
"""
from collections import Counter
freq = Counter(text)
all_chars = sorted(freq.keys(), key=lambda c: (-freq[c], c))
if max_vocab is not None and len(all_chars) >= max_vocab:
# 保留高频字符idx 0 是 <unk>
kept = all_chars[:max_vocab - 1]
else:
kept = all_chars
chars = ['<unk>'] + kept
char_to_idx = {c: i for i, c in enumerate(chars)}
idx_to_char = {i: c for i, c in enumerate(chars)}
return cls(chars, char_to_idx, idx_to_char)
@@ -36,7 +57,7 @@ class CharTokenizer:
return len(self.chars)
def encode(self, text: str) -> list[int]:
# 未知字符用 0假设第一个字符是常见的或后续加 unk token
# 未知字符映射到 0 (<unk>)
return [self.char_to_idx.get(c, 0) for c in text]
def decode(self, ids: list[int]) -> str:
@@ -68,6 +89,133 @@ class CharDataset(Dataset):
return x, y
def load_chinese_corpus(data_dir: Path | None = None) -> str:
"""加载中文语料(唐诗宋词 + 论语片段,公共领域)。
如果 data_dir 有 chinese.txt 则从文件加载,否则用内嵌样本。
"""
if data_dir is not None:
text_path = data_dir / "chinese.txt"
if text_path.exists():
return text_path.read_text(encoding="utf-8")
return """学而时习之,不亦说乎?有朋自远方来,不亦乐乎?
人不知而不愠,不亦君子乎?
吾日三省吾身:为人谋而不忠乎?与朋友交而不信乎?传不习乎?
温故而知新,可以为师矣。
学而不思则罔,思而不学则殆。
知之为知之,不知为不知,是知也。
三人行,必有我师焉。择其善者而从之,其不善者而改之。
己所不欲,勿施于人。
巧言令色,鲜矣仁。
君子坦荡荡,小人长戚戚。
敏而好学,不耻下问,是以谓之文也。
知者乐水,仁者乐山。知者动,仁者静。知者乐,仁者寿。
质胜文则野,文胜质则史。文质彬彬,然后君子。
朝闻道,夕死可矣。
德不孤,必有邻。
床前明月光,疑是地上霜。
举头望明月,低头思故乡。
白日依山尽,黄河入海流。
欲穷千里目,更上一层楼。
春眠不觉晓,处处闻啼鸟。
夜来风雨声,花落知多少。
红豆生南国,春来发几枝。
愿君多采撷,此物最相思。
千山鸟飞绝,万径人踪灭。
孤舟蓑笠翁,独钓寒江雪。
离离原上草,一岁一枯荣。
野火烧不尽,春风吹又生。
锄禾日当午,汗滴禾下土。
谁知盘中餐,粒粒皆辛苦。
故人西辞黄鹤楼,烟花三月下扬州。
孤帆远影碧空尽,唯见长江天际流。
朝辞白帝彩云间,千里江陵一日还。
两岸猿声啼不住,轻舟已过万重山。
日照香炉生紫烟,遥看瀑布挂前川。
飞流直下三千尺,疑是银河落九天。
两个黄鹂鸣翠柳,一行白鹭上青天。
窗含西岭千秋雪,门泊东吴万里船。
月落乌啼霜满天,江枫渔火对愁眠。
姑苏城外寒山寺,夜半钟声到客船。
清明时节雨纷纷,路上行人欲断魂。
借问酒家何处有,牧童遥指杏花村。
向晚意不适,驱车登古原。
夕阳无限好,只是近黄昏。
大江东去,浪淘尽,千古风流人物。
故垒西边,人道是,三国周郎赤壁。
乱石穿空,惊涛拍岸,卷起千堆雪。
江山如画,一时多少豪杰。
明月几时有,把酒问青天。
不知天上宫阙,今夕是何年。
但愿人长久,千里共婵娟。
寻寻觅觅,冷冷清清,凄凄惨惨戚戚。
乍暖还寒时候,最难将息。
落霞与孤鹜齐飞,秋水共长天一色。
渔舟唱晚,响穷彭蠡之滨。
天行健,君子以自强不息。
地势坤,君子以厚德载物。
知足者富,强行者有志。
不失其所者久,死而不亡者寿。
上善若水。水善利万物而不争。
处众人之所恶,故几于道。
道可道,非常道。名可名,非常名。
无名天地之始,有名万物之母。
千里之行,始于足下。
祸兮福之所倚,福兮祸之所伏。
君子之交淡如水,小人之交甘若醴。
玉不琢,不成器;人不学,不知道。
路漫漫其修远兮,吾将上下而求索。
亦余心之所善兮,虽九死其犹未悔。
莫等闲,白了少年头,空悲切。
三十功名尘与土,八千里路云和月。
人生自古谁无死,留取丹心照汗青。
臣心一片磁针石,不指南方不肯休。
山重水复疑无路,柳暗花明又一村。
纸上得来终觉浅,绝知此事要躬行。
问渠那得清如许,为有源头活水来。
等闲识得东风面,万紫千红总是春。
海内存知己,天涯若比邻。
同是天涯沦落人,相逢何必曾相识。
会当凌绝顶,一览众山小。
读书破万卷,下笔如有神。
天生我材必有用,千金散尽还复来。
长风破浪会有时,直挂云帆济沧海。
"""
def load_shakespeare(data_dir: Path | None = None) -> str:
"""加载 Shakespeare 文本。
@@ -145,3 +293,50 @@ Made glorious summer by this sun of York;
And all the clouds that lour'd upon our house
In the deep bosom of the ocean buried.
"""
def load_textbook_corpus(corpus_dir: Path | None = None) -> str:
"""加载九年义务教育课本语料(从 corpus/*.txt 读取)。
语料由 scripts/fetch_corpus_incremental.py 从维基百科 API 抓取,
覆盖语文/数学/英语/化学/政治/历史/地理/物理/生物九个科目。
维基百科返回繁体中文,这里统一转成简体。
如果 corpus_dir 不存在或为空,返回空字符串。
"""
if corpus_dir is None:
corpus_dir = Path(__file__).parent.parent / 'corpus'
if not corpus_dir.exists():
return ''
# 繁简转换
try:
from opencc import OpenCC
cc = OpenCC('t2s') # 繁体转简体
convert = cc.convert
except ImportError:
convert = lambda x: x # 无 opencc 则不转换
parts = []
for txt_file in sorted(corpus_dir.glob('*.txt')):
content = txt_file.read_text(encoding='utf-8').strip()
if content:
parts.append(convert(content))
return '\n\n'.join(parts)
def load_corpus(data_dir: Path | None = None,
corpus_dir: Path | None = None) -> str:
"""加载完整训练语料Shakespeare + 中文经典 + 九年义务教育课本)。
用于构建包含中英文字符的 tokenizer 和训练。
"""
parts = [
load_shakespeare(data_dir),
load_chinese_corpus(data_dir),
]
textbook = load_textbook_corpus(corpus_dir)
if textbook:
parts.append(textbook)
return '\n\n'.join(parts)