文档分类数据放不进内存怎么办?用 HashingVectorizer 分批训练

用 JSONL 逐行迭代、HashingVectorizer 固定稀疏特征和 SGDClassifier partial_fit 分批训练文档分类器,核对首次全类别与内存边界。

文档总量放不进内存时,可以让数据、特征转换和模型更新都按批次工作。HashingVectorizer 把文本映射到固定维度的稀疏矩阵,无须保存不断增长的词表;支持 partial_fit 的 SGDClassifier 接收这些批次逐次更新。只把文件拆小、仍对所有特征调用一次 fit,不能完成这里的任务。

先确定文件格式和全部类别

输入用 UTF-8 的 JSONL:每行一个对象,含非空 text 和整数 label。本例三类分别为硬件、算法训练、数据处理,标签为 0、1、2。首次调用 partial_fit 必须传入整个任务的 classes=[0,1,2],即使第一批只出现类别 0。

文档分类数据放不进内存怎么办?用 HashingVectorizer 分批训练

标签集合来自事先确定的业务分类表,不依赖临时扫描整份数据到内存。遇到新类别时先更新分类设计和训练方案;不能把原来二分类的模型直接当成可随时扩容的三分类器。

环境和内存边界

下面示例在 Windows、Python 3.11.15、CPU 环境实际运行。先安装 Python 3.11,在新建的空文件夹中打开 PowerShell,把完整代码保存为 08-incremental-doc-classification.py。创建独立环境并执行:

python -m venv .venv
.\.venv\Scripts\python.exe -m pip install numpy==2.4.6 scikit-learn==1.9.1
.\.venv\Scripts\python.exe 08-incremental-doc-classification.py

最后一行在保存代码后执行。macOS 或 Linux 把 Python 路径换成 ./.venv/bin/python。示例不需要 GPU、外部模型下载或付费 API;安装依赖需要联网。不同库版本可能带来数值末位差异,先核对数据行数、标签顺序和输出结构。

本例设置 n_features=2**16,无论读过多少篇,特征列数保持 65536。批次的稀疏矩阵只保存非零项;分类器仍需保存与类别数及特征维度有关的参数,三分类的系数矩阵有 3×65536 个数。调高特征维度会增加模型内存。

正文很长时,字符 n-gram 生成也会占用时间和内存。真实文件应给单条文本长度设业务上合理的上限,必要时先按稳定规则分段并保留原文编号;不得无说明地截掉影响分类的结尾。

逐行读取、固定特征、逐批更新

把完整代码保存为 08-incremental-doc-classification.py 并运行。代码先生成 demo_train.jsonl,再用逐行迭代器每次读取三条,转换成固定维度的稀疏特征,并更新同一个分类器。

import json
from pathlib import Path
import numpy as np
from sklearn.feature_extraction.text import HashingVectorizer
from sklearn.linear_model import SGDClassifier

# 仅生成一个小型教学文件;实际使用时换成已有的逐行 JSONL 文件。
# 0=硬件,1=算法训练,2=数据处理。首次批次故意只含类别0。
rows = [
    ('显卡显存影响硬件配置', 0), ('处理器和显卡硬件升级', 0),
    ('硬件显存容量与显卡温度', 0),
    ('算法训练学习率与损失', 1), ('训练模型算法收敛', 1),
    ('模型训练调整学习率', 1),
    ('数据清洗处理重复记录', 2), ('数据表格处理缺失值', 2),
    ('清洗数据检查缺失记录', 2),
    ('显卡硬件配置选择', 0), ('算法训练计算损失', 1),
    ('数据清洗检查表格', 2),
    ('硬件处理器显卡容量', 0), ('训练算法模型学习率', 1),
    ('表格数据处理重复值', 2),
]
path = Path('demo_train.jsonl')
with path.open('w', encoding='utf-8') as out:
    for text, label in rows:
        out.write(json.dumps({'text': text, 'label': label}, ensure_ascii=False)+'\n')
del rows

def batches(path, size=3):
    texts, labels = [], []
    with path.open(encoding='utf-8') as source:
        for line_no, line in enumerate(source, 1):
            if not line.strip():
                continue
            row = json.loads(line)
            if not isinstance(row.get('text'), str) or not row['text'].strip():
                raise ValueError(f'第{line_no}行缺少非空text')
            if type(row.get('label')) is not int or row['label'] not in (0, 1, 2):
                raise ValueError(f'第{line_no}行label必须为0/1/2')
            texts.append(row['text'])
            labels.append(row['label'])
            if len(texts) == size:
                yield texts, np.asarray(labels)
                texts, labels = [], []
    if texts:
        yield texts, np.asarray(labels)

vectorizer = HashingVectorizer(analyzer='char', ngram_range=(2, 4),
                              n_features=2**16, norm='l2', alternate_sign=False)
classifier = SGDClassifier(loss='log_loss', learning_rate='constant',
                           eta0=0.2, alpha=0.0001, average=True, random_state=42)
classes = np.array([0, 1, 2])
updates, max_rows, first = 0, 0, True
for epoch in range(3):  # 每轮重开文件,未把全文载入列表。
    seen = 0
    for texts, labels in batches(path, size=3):
        X = vectorizer.transform(texts)
        assert X.shape[1] == 2**16
        max_rows = max(max_rows, X.shape[0])
        if first:
            print('first batch labels:', labels.tolist())
            classifier.partial_fit(X, labels, classes=classes)
            first = False
        else:
            classifier.partial_fit(X, labels)
        updates += 1
        seen += len(labels)
    print('epoch/rows:', epoch+1, seen)
print('updates/max_batch_rows:', updates, max_rows)
print('coef shape:', classifier.coef_.shape)
queries = ['显卡硬件显存配置', '模型算法训练学习率', '表格数据清洗缺失值']
print('demo predictions:', classifier.predict(vectorizer.transform(queries)).tolist())
assert updates == 15 and max_rows == 3
assert classifier.classes_.tolist() == [0, 1, 2]

代码的 rows 列表只用于生成 15 条教学输入,写完后即删除;真实使用时可以移除这段生成代码,把 path 指向现有 JSONL 文件。迭代器保存的文本数量不超过 size;每轮重新打开文件。不要将 batches 的结果转成 list,也不要把所有 X 拼接起来再训练。

第二批及以后沿用同一个分类器对象,不重复创建。代码训练三轮,用于说明 partial_fit 一次只遍历当前批次一遍;增大构造器的 max_iter 不能让一次 partial_fit 自动做很多轮。对不可回读的数据流,只能采用适合单次遍历的训练策略,并在独立数据上验证。

实际输出和验证办法

first batch labels: [0, 0, 0]
epoch/rows: 1 15
epoch/rows: 2 15
epoch/rows: 3 15
updates/max_batch_rows: 15 3
coef shape: (3, 65536)
demo predictions: [0, 1, 2]

应看到首批标签为 [0, 0, 0],三轮各读取 15 条,总共 15 次更新,最大批次只有 3 行;分类器 coef_ 的形状为 (3, 65536),三条教学查询预测为 [0, 1, 2]。

样本量、更新次数和固定列数证明这里确实执行了分批更新。三条查询很容易、数量很少,与训练词语接近;预测为 [0,1,2] 只说明接口和标签映射正常,不能当成模型质量评测。真实业务应另留出未参与更新的文档,逐批累计混淆矩阵或正确数,避免只报告最后一批分数。

Hashing 的代价与故障处理

HashingVectorizer 是无词表状态的特征映射,会出现不同片段映射到同一列的碰撞,且无法从列号反查原始词语。本例没有 IDF 统计,因此它与前一篇 TF-IDF 基线的特征权重不同。需要关键词解释或完整词表时,应评估是否能保留受控词表,而不是期待 Hashing 自动给出词语解释。

发现后续批次特征数不一致时,先核对是否改变了 n_features、analyzer 或 ngram_range;同一个模型的批次及预测必须使用相同映射。报“classes must be passed on the first call”时检查首次更新分支;报未知类别时核对 labels 与事先确定的 classes。

解析失败或空文本会在迭代器里报出行号。这个教学代码立即停止,便于查错;生产流程可先单独清洗,保存失败行号与原因,再用合格文件训练。需要 shuffle 时,整文件随机打乱可能重新带来内存问题,可采用磁盘预打乱或有界缓冲,并记录数据顺序对结果的影响。

这个小文件只演示按批训练的调用过程,没有在超大数据上测试内存峰值、吞吐量或真实分类效果;分批读取也不能保证任意大小的单篇文档或任意特征维度都能放进内存。

后续怎么把模型恢复起来

要恢复训练,需要同时保存分类器、固定向量器参数、标签字典、已处理的数据位置和轮次,避免重复消费或漏读。保存和加载依赖运行环境兼容;只加载自己可信的模型文件。本文聚焦数据按批读取和更新,尚未实现跨进程恢复或服务部署。

相关官方资料

本文读取官方资料的日期为 2026 年 10 月 1 日;示例输出来自上面注明的本地环境。

Ai菜鸟网。发布者:AI小管家,转载请注明出处:https://www.alyyhw.com/30584.html

赞 (0)
AI小管家的头像AI小管家
中文评论情感分析怎么入门?用字符 TF-IDF 训练小样本基线
上一篇 1天前
时间序列预测怎么避免看见未来?构造滞后特征并按时间验证
下一篇 1天前

相关推荐

联系我们

联系我们

1

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信
关注微信
分享本页
返回顶部