如何用小内存电脑训练大数据的bpe,16g内存训练200g数据集默认是一次性读入内存训练

你说得对:

现在的"断点续跑"只记录了 哪些文件已经读完 ,但 没有把已读到的 SMILES 真正落盘

一旦中途崩溃,下次重启时虽然会跳过已标记文件,却把之前累积的 all_smiles 全丢了,于是 BPE 字典还是空的。

要真正做到"读一点、更新一点字典",需要把 "读完一个文件 → 把这份文件的 SMILES 追加到磁盘临时文件 → 更新断点日志" 做成原子操作。

下面给出一个最小改动方案,你直接替换对应函数即可。


  1. 加一个临时 SMILES 文件路径
python 复制代码
SMILES_TMP = os.path.join(OUTPUT_DIR, "smiles.tmp")   # 追加写入
  1. 改写 read_zinc20_smiles()
    逻辑:
  • 不再把 SMILES 攒在内存列表,而是 每读完一个文件就立刻追加到 smiles.tmp,再记日志。
  • 函数返回 临时文件路径(而不是列表),后面 BPE 训练直接按行读即可。
python 复制代码
def read_zinc20_smiles():
    """流式读取,每文件追加落盘;返回临时文件路径"""
    try:
        import duckdb
    except ImportError:
        os.system("python -m pip install -q duckdb")
        import duckdb

    parquet_files = [f for f in os.listdir(ZINC20_DIR) if f.lower().endswith('.parquet')]
    if not parquet_files:
        print("警告:未找到任何 parquet 文件")
        return None

    done = load_done()
    print(f"共 {len(parquet_files)} 个文件,已处理 {len(done)} 个")

    conn = duckdb.connect(':memory:')
    conn.execute("SET memory_limit='8GB'")
    conn.execute("SET temp_directory='./duckdb_tmp'")
    conn.execute("SET threads=4")

    # 追加模式打开临时文件
    with open(SMILES_TMP, "a", encoding="utf-8") as fout:
        for file in tqdm(parquet_files, desc="读取 ZINC20"):
            if file in done:
                continue
            path = os.path.join(ZINC20_DIR, file)
            try:
                q = conn.execute(
                    "SELECT smiles FROM read_parquet(?) WHERE smiles IS NOT NULL",
                    [path]
                )
                while True:
                    rows = q.fetchmany(CHUNK_SIZE)
                    if not rows:
                        break
                    for row in rows:
                        fout.write(row[0] + "\n")
                mark_done(file)      # 真正落盘后再记日志
                gc.collect()
            except Exception as e:
                print(f"  文件 {file} 出错:{e}")
    conn.close()
    print(f"SMILES 已追加写入 {SMILES_TMP}")
    return SMILES_TMP
  1. 修改 process_dataset()
    原来 sampled_smiles 是一个列表,现在改成 从临时文件里直接读行 即可(内存仍可控)。
    如果还想保留随机采样,可以:
python 复制代码
def process_dataset():
    smiles_file = read_zinc20_smiles()          # 返回临时文件路径
    if smiles_file is None:
        return

    # 1. 采样(如果 SAMPLE_RATIO < 1.0)
    sampled_smiles = []
    with open(smiles_file, encoding="utf-8") as f:
        all_smiles = [line.strip() for line in f]
    sampled_smiles = sample_data(all_smiles, SAMPLE_RATIO) if SAMPLE_RATIO < 1.0 else all_smiles

    # 2. 加载分词器
    tokenizer = rustmolbpe.SmilesTokenizer()
    tokenizer.load_vocabulary(VOCAB_PATH)
    print(f"词汇表大小:{tokenizer.vocab_size}")

    # 3. 分词并写 jsonl
    tok_file = os.path.join(OUTPUT_DIR, "zinc20_tokenized_rustmolbpe.jsonl")
    with open(tok_file, "w", encoding="utf-8") as f:
        for smi in tqdm(sampled_smiles, desc="rustmolbpe 分词"):
            try:
                ids = tokenizer.encode(smi)
                toks = [tokenizer.id_to_token(i) for i in ids]
                f.write(json.dumps(
                    {"smiles": smi, "tokens": toks, "token_ids": ids},
                    ensure_ascii=False) + "\n"
                )
            except Exception as e:
                print(f"  分词失败:{smi} -> {e}")

    # 4. 额外保存纯 SMILES(BPE 训练用)
    bpe_file = os.path.join(OUTPUT_DIR, "zinc20_bpe_train.txt")
    with open(bpe_file, "w", encoding="utf-8") as f:
        for smi in tqdm(sampled_smiles, desc="写 BPE 训练文件"):
            f.write(smi + "\n")

    print(f"分词结果:{tok_file}")
    print(f"BPE 训练文件:{bpe_file}")
    print(f"完成,共 {len(sampled_smiles)} 条")
  1. 后续增量训练 BPE
    如果你想 真正"每读完一个文件就更新一次 BPE 字典" ,可以把 smiles.tmp 当成 持续增长的语料池,每处理完一个文件就:
python 复制代码
subprocess.run([
    "python", "-m", "rustmolbpe", "train",
    "--input", SMILES_TMP,
    "--model_prefix", os.path.join(OUTPUT_DIR, "bpe_vocab"),
    "--vocab_size", "8000",
    "--character_coverage", "1.0"
])

这样即使中断,下次重启时字典也已经包含了之前所有文件学到的子词。


一句话总结:
把 SMILES 实时追加到磁盘临时文件,再记断点日志,就能真正做到"读一点、字典更新一点、断点续跑"。

可以"直接在循环里调",但强烈不建议------原因有三点,正好对应你问的"为什么要这样":

  1. 子进程 = 天然内存隔离

    duckdb 已经吃了几 GB,Python 对象也占了不少;

    如果把 BPE 训练库(通常是 C++/Rust 扩展)再塞进同一个进程

    训练时还要额外拼 huge 的 frequency map、suffix array,很容易 OOM

    丢给 subprocess.run 相当于单独再起一个干净进程,主进程内存立刻降回来。

  2. 训练算法是"全局"的,不是"增量"的

    SentencePiece / BPE 都要求一次性看到全部语料 才能统计最优合并。

    每读一个文件就 train 一次,得到的只是局部语料 的子词表,

    后面再读新文件时旧表作废 ,必须重训------等于白跑。

    所以业界惯例都是:
    先把所有文本追加到一个大文件 → 最后一次性 train

  3. 代码最简单、最稳

    subprocess 调官方 CLI 不用关心 C++ 扩展的 ABI、线程、GIL;

    参数、日志、错误码都现成的,出 bug 也更容易定位。

    如果你硬要在循环里调 Python API,得自己管:

    • 全局 SentencePieceTrainer.train(...) 的输入句柄;
    • 每次训练完手动 .clear() 内部静态缓存;
    • 还要把新表 merge 回旧表(SentencePiece 没有官方 merge 接口)。
      这些坑省下来,不如让 CLI 一把梭。

总结:

"追加到临时文件 → 最后一次性 subprocess 训练" 是目前最省内存、最正确、最少代码 的做法;

在循环里反复 train 既慢又得不到全局最优词表,除非你自己实现真正的在线增量 BPE(论文级工作量)。

"外排训练"并不是 BPE/SentencePiece 里的官方术语,而是中文社区里一种口语化简称
把"训练语料"先"外部排序(external sort)",再"分段加载"到内存里做训练 ,从而避免一次性把所有原始文本读进内存 。核心思想就是"外排 + 分段训练"。


为什么需要"外排"

  1. BPE 训练第一步要统计所有 n-gram 频次,语料大到内存装不下时,只能:

    • 先把每块语料在内存里算局部频次 → 落盘成临时文件;
    • 对所有临时文件按"子串"做外部归并排序 → 得到全局频次表;
    • 最后只把全局高频片段装进内存,继续后面的合并训练。
  2. SentencePiece 官方已把这套流程封装好:

    当语料 > 几十 GB 时,只要打开 --train_extremely_large_corpus=true

    它就会自动外排 + 分段训练,峰值内存只有几百 MB 。


一句话总结

"外排训练"就是:
先外排序,再分段读,内存只留高频表,大语料也能把 BPE 跑下来。

相关推荐
acrelwwj2 分钟前
智慧照明新引擎,ASL600 4GWJ开启城市照明精细化管理新时代
大数据·经验分享·物联网
FserSuN5 分钟前
2026年AI工程师指南
人工智能
是枚小菜鸡儿吖6 分钟前
CANN 的安全设计之道:AI 模型保护与隐私计算
人工智能
leo03087 分钟前
科研领域主流机械臂排名
人工智能·机器人·机械臂·具身智能
人工智能AI技术27 分钟前
GitHub Copilot免费替代方案:大学生如何用CodeGeeX+通义灵码搭建AI编程环境
人工智能
Chunyyyen28 分钟前
【第三十四周】视觉RAG01
人工智能·chatgpt
是枚小菜鸡儿吖29 分钟前
CANN 算子开发黑科技:AI 自动生成高性能 Kernel 代码
人工智能·科技
hqyjzsb36 分钟前
盲目用AI提效?当心陷入“工具奴”陷阱,效率不增反降
人工智能·学习·职场和发展·创业创新·学习方法·业界资讯·远程工作
2501_9436953336 分钟前
高职大数据技术专业,怎么参与开源数据分析项目积累经验?
大数据·数据分析·开源