资讯动态

中文文本分类实战:从TF-IDF+SVM到RoBERTa的完整流程与避坑指南

发布时间:2026/10/9 2:42:26 来源:尧图企业网站定制
简介基于搜狗新闻语料库的中文文本分类完整实践项目覆盖传统机器学习与预训练模型两条技术路线适合自然语言处理初学者、人工智能及相关专业学生用于课程设计、毕业设计或项目演示。包内共15个文件以Python源码为主包含数据切分、模型训练、结果可视化等脚本另附词典、停用词表、说明文档与训练曲线图便于对照理解分类流程。约208KB轻量易部署下载后即可本地运行调试。已有79人学习下载代码均经过测试可直接复现也可在此基础上扩展多分类、调参或接入其他预训练模型是快速上手中文文本分类的实用参考。1. 搜狗新闻语料库上的中文文本分类为什么先跑通传统方法再上预训练模型拿到一份搜狗新闻语料库目标是在上面做中文文本分类很多人的第一反应是直接上一个预训练模型但真正动手做过就会发现这条路并不比传统机器学习方法省心。搜狗新闻语料库的特点是数据量大、类别多、文本长度差异悬殊直接从预训练模型起步很容易在数据清洗和训练资源上卡住。比较务实的做法是先建一条传统机器学习基线用 TF-IDF 加 SVM 把完整流程跑通把任务变成一份可评测、可对比的问题然后再用预训练模型去冲击更高分数。这套思路适合正在做课程项目、实训作业或想系统掌握中文 NLP 落地步骤的读者它不挑显卡也不依赖某个特定的运行环境只要有一台能跑 Python 的机器就能开始。本文会按数据准备、传统方法、预训练模型、踩坑记录这条线完整拆开讲。2. 准备搜狗新闻语料库解压、编码清洗与标签压缩的一线做法2.1 语料库的目录结构与标签来源搜狗新闻语料库最常见的形态是一个嵌套目录外层是按新闻类别划分的文件夹。这里的关键点在于这份数据本质上是从新闻网页抓取来的原始格式不是干净的 CSV 或 JSON而是带一组自定义标记的纯文本。部分版本的样本长这样doc urlhttp://sports.sohu.com/.../url docnoxxx/docno content比赛进行到第十分钟.../content /doc新版 SogouCS 数据集则使用url、docno、contenttitle、content这四个闭合标签并且所有字段值做了反转义处理读起来更像一个半结构化文本。很多资料包里的版本是已经按类目拆好的目录每个目录名对应一个类别目录下每个文件是一篇新闻。这种情况下标签的直接来源就是文件所在目录名不需要额外标注。拿到压缩包后先别急着写训练代码。第一步是确认数据规模每个类别的文件数量、总样本量、是否存在空文件、是否存在同一篇新闻的重复样本。常见做法是写一个快速统计脚本把目录结构、文件数、平均字符数打印出来先给数据打个分。我一般会把类别数量少于 100 条的子目录直接标注为弱类别后面统一处理。mkdir -p data/raw data/processed tar -zxvf sogou_news_dataset.tar.gz -C data/raw find data/raw -type f | wc -l统计完文件总数后再按目录聚合看类目分布这时你就能判断这份资料能不能直接用来训练。如果类别高度不均衡比如“体育”类有 10 万篇而“军事”类只有 2000 篇那么后面做评估时就要以宏平均 F1 为主而不是只看整体准确率。2.2 编码清洗与数据切分脚本搜狗新闻语料库的文本编码几乎都是 GBK 或 GB18030。用默认 UTF-8 直接打开必乱码或报错这是新手翻车频率最高的地方。正确做法是在读取时显式指定编码并统一转成 UTF-8 落盘后续所有处理环节都只用 UTF-8 文本。清洗规则要从新闻网页特点反推正文里混着大量 HTML 实体、空白符、URL、特殊符号还有可能的重复标题。这些噪声对传统机器学习方法和预训练模型的影响路径不同。对 TF-IDF 来说URL 和特殊符号会被当作特征参与计算等于引入了无关维度对 BERT 这类模型来说过长的噪声文本会浪费有限的序列长度。import re import pandas as pd from pathlib import Path def clean_text(text: str) - str: text re.sub(r[^], , text) text re.sub(rhttps?://\S|www\.\S, , text) text re.sub(r\s, , text) text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、《》\s], , text) return text.strip() def build_dataset(root: Path): rows [] for label_dir in root.iterdir(): if not label_dir.is_dir(): continue label label_dir.name for fp in label_dir.glob(*.txt): raw fp.read_text(encodinggb18030, errorsignore) text clean_text(raw) if len(text) 20: # 过滤过短文本避免空样本进入训练集 rows.append({label: label, text: text}) return pd.DataFrame(rows) df build_dataset(Path(data/raw)) df.to_csv(data/processed/news_clean.csv, indexFalse) print(df[label].value_counts())这里的errorsignore是双刃剑。它能跳过个别损坏字节但也可能让文本尾部直接断掉。比较稳的做法是先按 GB18030 解码失败样本单独挑出来尝试 GBK 再失败就丢弃。len(text) 20这个阈值来自经验新闻正文低于 20 个字符的样本要么是抓取残缺要么是纯标题对分类任务几乎没有正向贡献。清洗后用label做一次value_counts()如果看到某个类别的样本数低到个位数直接决定它是否值得保留。切分时强烈建议用train_test_split而不是手动按比例随机抽。手动抽容易出现多个类别只进了训练集但没进测试集的情况。这时要设置stratify参数保证每个类别的比例在训练集、验证集、测试集中保持一致。from sklearn.model_selection import train_test_split train, tmp train_test_split( df, test_size0.2, stratifydf[label], random_state42 ) val, test train_test_split( tmp, test_size0.5, stratifytmp[label], random_state42 ) print(train[label].value_counts()) print(test[label].value_counts())random_state固定下来是保证实验可复现的关键。后面对比传统方法和预训练模型的分数时如果切分不固定两次结果之间的差异就混入了数据分布的随机性无法准确判断是模型变强了还是运气变好了。2.3 类别压缩与样本量下采样搜狗新闻语料库原始类目有几十个用于课程项目或实训时类别太多会带来两个问题一是部分类目样本不足训练不充分二是评估指标变得极其分散报告不好写。常见做法是把类目压缩到 10 个以内或者只保留样本量最大的前 10 类其余归入“其他”。样本量不均衡也是必须处理的。假设“体育”类有 10 万条“旅游”类只有 3000 条训练时模型会天然偏向大类别。对这个任务我一般不会直接做上采样复制因为新闻文本重复太多会让模型记住文本本身而非类别规律。更稳妥的做法是做下采样把大类压到与小类相近的量级或者采用类别权重。def downsample(df: pd.DataFrame, max_per_class: int 10000): sampled [] for label, group in df.groupby(label): if len(group) max_per_class: group group.sample(max_per_class, random_state42) sampled.append(group) return pd.concat(sampled) df downsample(pd.read_csv(data/processed/news_clean.csv)) df.to_csv(data/processed/news_balanced.csv, indexFalse)每个类别最多保留 1 万条一方面压缩了后续训练时间另一方面避免大类别主导决策边界。如果后面用预训练模型这个下采样策略还能大幅缩短单轮训练时间让你有更多机会调参而不是干等。3. 传统机器学习基线jieba TF-IDF 支持向量机的训练流程与关键参数3.1 为什么要分词而不是直接按字建模中文文本分类的第一步永远是分词原因在于 TF-IDF 这类词袋模型以词为特征单位。按字建模虽然能避开分词错误但字级别的特征无法表达“机器学习”和“机器”“学习”之间的语义差别特征维度也会更高。搜狗新闻语料库的文本是正式书面语句子结构完整分词质量整体较高非常适合 jieba 分词。这里有个容易被忽视的细节分词前要决定是否去停用词。新闻语料里“的”“了”“在”这类高频功能词在 TF-IDF 的 IDF 机制下权重本来就会被压低但它们依然占据特征矩阵的大量维度。常见做法是准备一份中文停用词表在分词后直接过滤。停用词表的质量直接决定特征维度的干净程度这一步不建议省。import jieba import jieba.analyse STOP_WORDS set() with open(data/stopwords.txt, encodingutf-8) as f: for line in f: STOP_WORDS.add(line.strip()) def tokenize(text: str): words jieba.lcut(text) return [w for w in words if w not in STOP_WORDS and len(w.strip()) 0]jieba.lcut返回的是一个列表相比jieba.cut的生成器在后续 DataFrame 操作中更直接。停用词过滤时只判断w not in STOP_WORDS还不够数字、单个字母、无意义字符也要一并剔除。新闻文本里经常出现“2024年”“第10届”这类片段如果不过滤它们会成为高维稀疏特征对模型预测没有帮助。3.2 TF-IDF 参数与支持向量机训练脚本特征工程部分的核心是TfidfVectorizer的参数设置。搜狗新闻语料库文本较长如果完全不限制特征数量特征矩阵会膨胀到几万甚至几十万维内存占用飙升的同时训练速度也急剧下降。这里有几个参数是必调的max_features、min_df、sublinear_tf。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.svm import LinearSVC from sklearn.pipeline import Pipeline vectorizer TfidfVectorizer( tokenizertokenize, max_features50000, min_df2, max_df0.95, sublinear_tfTrue, ngram_range(1, 2) ) model Pipeline([ (tfidf, vectorizer), (clf, LinearSVC(C1.0, class_weightbalanced, max_iter5000)) ])min_df2表示词至少出现在 2 篇文档中才被纳入特征这会去噪max_df0.95表示词出现在超过 95% 的文档中则丢弃这能剔除“新闻”“记者”“报道”这类在所有类别中都高频出现的词。sublinear_tfTrue是把词频取对数压缩防止长文本的词频优势过大。ngram_range(1, 2)加入二元词组特征对“深度学习”“人工智能”这类复合词有增量帮助。训练时用LinearSVC而不是传统的 SVC 高斯核核心原因是新闻多分类任务的样本量大线性核完全够用且训练速度能差出两个数量级。class_weightbalanced会自动调整类别权重对不均衡数据很友好。X_train train[text].tolist() y_train train[label].tolist() model.fit(X_train, y_train) from sklearn.metrics import classification_report y_pred model.predict(test[text].tolist()) print(classification_report(test[label].tolist(), y_pred, digits4))第一次跑通时重点不是刷分数而是确认整条链路没有断点。文本要能从 CSV 走进 TF-IDF 矩阵再走进分类器。如果跑不起来八成问题出在tokenizertokenize这一行TfidfVectorizer接收的是字符串列表分词函数返回的也必须是字符串列表返回生成器会报错。我一般会先拿 100 条样本做小验证再全量训练避免浪费半小时后才发现错误。3.3 评估指标怎么读宏平均与微平均的差别传统机器学习方法跑完后会输出一份classification_report这里的macro avg和weighted avg要会区分。宏平均是先按类别算出 F1 再平均每个类别的权重相同。在类别不均衡的搜狗新闻语料库上宏平均更能反映模型对小类别的区分能力。加权平均则按样本量加权大类别表现好会掩盖小类别的失败。如果宏平均 F1 明显低于加权平均说明小类别的分类质量差。这时先不要换模型检查是不是类别样本量太少或类别文本本身高度相似。比如“教育”和“文化”两个类别在新闻语境下边界模糊模型分错是正常的强行提升往往要靠引入更多外部特征。我还建议在训练时把预测概率而不是预测标签存下来这一点在传统机器学习模型上容易被忽略。LinearSVC默认不输出概率需要开predict_proba必须改用CalibratedClassifierCV包装。对多数课程项目来说直接打印分类报告就够了但如果后面想画 ROC 曲线或做阈值调优概率输出是必需品。4. 预训练模型进阶选型中文 RoBERTa 在分类任务上的实操要点4.1 选哪类中文预训练模型BERT 还是 RoBERTa搜狗新闻语料库做分类预训练模型的选型主要集中在两个方向bert-base-chinese和基于哈工大讯飞联合发布的hfl/chinese-roberta-wwm-ext。这两个模型在中文 NLP 任务上都是主流选择。我的经验是如果你没有特殊理由直接用chinese-roberta-wwm-ext。它采用全词掩码策略对中文这种词边界明确的语言更友好在新闻分类这类长文本任务上通常比原始 BERT 高出 1 到 2 个百分点的 F1。RoBERTa相比 BERT 的主要变化是去掉了下一句预测任务采用了动态掩码和更大的训练批次。这些改进体现在下游任务上就是更稳定的收敛曲线。对有一定显卡资源但不算宽裕的场景这是最适合起步的中文预训练模型。选型号时要注意本地的 transformers 版本要够新旧版本对AutoModelForSequenceClassification的支持不够完善。实际加载时用AutoModel系列 API 而不是直接BertForSequenceClassification这样以后想换成bert-base-chinese或hfl/rbt3只需要改一行配置。from transformers import AutoTokenizer, AutoModelForSequenceClassification model_name hfl/chinese-roberta-wwm-ext tokenizer AutoTokenizer.from_pretrained(model_name) id2label {i: label for i, label in enumerate(train[label].unique())} label2id {v: k for k, v in id2label.items()} num_labels len(id2label) model AutoModelForSequenceClassification.from_pretrained( model_name, num_labelsnum_labels, id2labelid2label, label2idlabel2id )num_labels必须和压缩后的类别数一致不一致时模型最后一层维度会不匹配。id2label和label2id传进去后模型输出的 logits 更容易对应回类别名评估时省掉一层手工映射。4.2 Tokenizer、截断与标签编码的实操脚本预训练模型的输入处理和传统机器学习完全不同。传统方法直接把分词后的列表交给 TF-IDF预训练模型则需要把文本转成input_ids、attention_mask和token_type_ids三个向量。搜狗新闻语料库的新闻正文一般比较长必须处理截断逻辑。有一个关键决策点截断策略是截头部还是截尾部或者是头尾各留一段。BERT 类模型有最大序列长度限制bert-base-chinese是 512 token。新闻正文动辄一两千字直接截断会丢失大量信息。常见做法是取开头 256 个字符加结尾 128 个字符拼接因为新闻的首段通常包含导语末尾通常包含结论这两个位置信息密度最高。def truncate_text(text, max_len380): if len(text) max_len: return text head text[:int(max_len * 0.6)] tail text[-int(max_len * 0.4):] return head tail def encode_texts(texts, tokenizer, max_len512): encoded tokenizer( texts, paddingTrue, truncationTrue, max_lengthmax_len, return_tensorspt ) return encoded传给 HF Tokenizer 时我建议直接传原始字符串列表而不是预先分词的结果。因为 AutoTokenizer 内部会自动调用分词器你手动用 jieba 分词后再传进去反而会让模型看到被空格切碎的文本影响语义完整性。训练前要把标签列映射成数字索引。这一步很容易出错的地方是直接LabelEncoder然后忘记保存映射关系。预测新样本时映射反了结果会完全混乱。train[label_id] train[label].map(label2id) val[label_id] val[label].map(label2id) X_train train[text].tolist() y_train train[label_id].tolist()保存模型时要把label2id和id2label一起存下来或者直接用model.config里的字段否则部署阶段你就得对着预测结果猜数字代表什么意思。4.3 训练超参数的落法batch size、warmup 与早停预训练模型的训练环节是最玄学的部分但好在已经有大量可参考的默认配置。搜狗新闻语料库分类任务常见做法是建立在 transformers 的Trainer之上把训练循环交给库处理把精力集中在超参数上。from transformers import Trainer, TrainingArguments training_args TrainingArguments( output_dir./results, num_train_epochs3, per_device_train_batch_size16, per_device_eval_batch_size64, warmup_ratio0.1, evaluation_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modelf1, save_total_limit2, fp16True, logging_steps200 ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasetval_dataset, ) trainer.train()per_device_train_batch_size要根据显卡显存调整。12GB 显存跑chinese-roberta-wwm-ext序列长度 512 时 batch size 16 比较稳低于 8GB 建议降到 8。fp16True能省接近一半显存但如果 CPU 训练则必须关掉。warmup_ratio0.1表示前 10% 的训练步数用来让学习率从 0 线性增长到目标值这对微调预训练模型很关键能避免前期剧烈震荡打乱预训练权重。load_best_model_at_endTrue配合metric_for_best_modelf1让训练结束后自动加载验证集上 F1 最高的 checkpoint。这一点很重要最后一个 epoch 的模型往往不是最好的尤其是在学习率衰减策略下验证集最优值通常出现在中间阶段。如果训练完成后直接拿最终模型去测测试集分数会比实际能拿到的低。5. 中文文本分类避坑从数据泄露到显存溢出的六条排查记录5.1 训练集测试集重叠导致准确率虚高现象模型在测试集上的准确率达到 0.98但实际拿一条新新闻去预测结果明显不对。原因是数据准备阶段没有做去重同一篇新闻在原始语料里出现了多次切分后一部分进了训练集、一部分进了测试集模型等于直接背下了答案。解决在做任何切分之前先对文本做哈希去重。常见做法是计算每篇文本的 MD5 值删除重复项后再切分。更严格的做法是计算文本的 SimHash 或编辑距离但课程项目阶段 MD5 去重已经够用。注意去重必须在文件级别做而不是在目录级别因为重复样本可能分布在不同的类别目录下。5.2 读取时报UnicodeDecodeError怀疑资料包损坏现象用open(xxx, encodingutf-8)读文件直接抛异常报错指向第某个字节位置。原因不是文件损坏而是搜狗新闻语料库的原始编码是 GBK 或 GB18030遇到 UTF-8 解码器自然失败。解决读文件时统一用encodinggb18030。GB18030 是 GBK 的超集兼容性最好。个别文件如果是 UTF-8 编码也不冲突GB18030 能正确解码大部分 UTF-8 文本。如果还报错就加errorsignore把这些坏字节跳过。这一步处理后生成的新 CSV 全部写 UTF-8 编码之后所有环节都不再碰编码问题。5.3 类别不均衡被宏平均 F1 惩罚报告很难看现象模型整体准确率有 0.91但macro avg F1只有 0.67。原因是某些小类别的新闻只有几百条模型押注大类别的倾向很强。搜狗新闻语料库的原始分布就是如此体育、财经、娱乐这些类别样本量远超其他类别。解决在传统机器学习路线中启用class_weightbalanced在预训练模型路线中计算类别权重并传入损失函数。数据侧可以做下采样但下采样后总样本量会变少模型能力上限会相应降低。我一般会先做下采样到每类最多 8000 条再看宏平均 F1如果还不行才考虑类别权重叠加。5.4 预训练模型训练时 CUDA 显存溢出现象CUDA out of memory报错训练在第 1 步就停止。常见于batch_size32 序列长度 512 的配置显卡显存只有 8GB。解决优先降低per_device_train_batch_size到 8 甚至 4这是最直接的止血方式。其次可以缩短max_length到 384搜狗新闻截断到 384 token 通常还能保留 80% 以上的有效信息。再不行就开gradient_accumulation_steps4相当于用 4 小步凑一个大批次稳定性和显存压力都能兼顾。5.5 分词器对新闻标题和正文的语义权重一样现象模型预测结果里“体育”和“娱乐”两个类别经常混淆抽出来看新闻内容发现标题讲的是体育明星的娱乐八卦正文也掺杂两类内容。原因新闻标题的信息密度远高于正文但放进预训练模型时两者被无差别编码。传统 TF-IDF 方法同样存在这个问题标题和正文的特征权重一致。解决常见做法是把标题和正文拼接时加分隔符并适当增加标题在文本中的比重。拼接时用title 。 content比直接拼接更符合中文习惯分隔符不会被分词器当噪声跳过。严格的方案是分别编码标题和正文再融合但课程项目做到前一种就够了分数提升通常在 1 到 2 个点。5.6 预训练模型在短文本上反而不如传统方法现象对“湖人 121-114 击败凯尔特人”这类短新闻传统 TF-IDF SVM 分得很准预训练模型反而给出奇怪的结果。原因在于预训练模型依赖上下文理解短文本缺少足够上下文时其优势发挥不出来。解决如果测试集中短文本占比高不要盲目迷信预训练模型。可以把两者的预测结果做集成或者至少在报告中诚实地对比两种方法在不同文本长度上的表现差异。常见做法是把测试集按文本长度分桶分别统计 F1这样能写进项目报告作为结论支撑。6. 让分数再进一步类别阈值调整与交叉验证验证技巧6.1 用交叉验证替代单次切分减少随机性随机切分一次后的测试集分数只能作为参考。如果你的课程资料需要提交一份有说服力的报告建议直接用 10 折交叉验证。搜狗新闻语料库的类别分布无论怎么随机切分都可能在某一次切分中偏向容易或困难的子集交叉验证能大幅度减少这种单次切分带来的误差。from sklearn.model_selection import StratifiedKFold from sklearn.metrics import f1_score skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for fold, (train_idx, test_idx) in enumerate(skf.split(X, y)): X_train_fold [X[i] for i in train_idx] # 此处复用 3.2 的 TF-IDF SVM 管线 model.fit(X_train_fold, [y[i] for i in train_idx]) y_pred model.predict([X[i] for i in test_idx]) fold_f1 f1_score([y[i] for i in test_idx], y_pred, averagemacro) print(ffold {fold}, macro F1: {fold_f1:.4f})首次跑交叉验证时先跑 5 折而不是 10 折单折训练一轮 TF-IDF SVM 可能只要几分钟但换成预训练模型就要按小时计算。折数越大方差越小但时间成本线性上升。从 5 折起步看各折之间的 F1 波动范围如果最大值和最小值超过 5 个点说明数据分布很不稳定增加折数才有意义。6.2 预测概率阈值调整不完全依赖 argmax多数分类模型默认取 logits 最大的类作为预测结果但新闻分类场景里某些类别的置信度天然偏低。比如“军事”与“国际”常常在特征空间里靠得很近argmax 只会给你一个类别但概率为 0.45 的区域往往是模型把握不大的地方。获取概率后可以对每个类别设定一个最低置信度阈值低于阈值时判定为“不确定”或者转向第二置信类别。这一步不能保证提升总体 F1但能让你看清模型的可信边界在哪里写入实验报告时是一张很有用的置信度分布表。from sklearn.calibration import CalibratedClassifierCV calibrated CalibratedClassifierCV(model, cv3) calibrated.fit(X_train, y_train) proba calibrated.predict_proba(X_test)加了CalibratedClassifierCV后训练时间会翻几倍但对最终报告的价值在于你可以画出每个类别的置信度直方图找出模型最犹豫的类别对。这也直接回答了一个常见问题到底哪些类别最难分。从我的习惯来看这个分析比单纯调参更能体现工程深度。6.3 一份可复现的验证清单项目收尾前按下面这份清单检查一遍能规避大部分低级问题第一确认训练集、验证集、测试集之间没有文本级别重复这个用哈希去重做。第二确认所有文本都转成了 UTF-8 编码原始文件保持只读。第三固定随机种子包括数据切分、模型初始化、训练器参数。第四记录两类方法的参数量和推理时间传统方法单条预测应该在毫秒级预训练模型在 GPU 上是 10 毫秒级在 CPU 上会接近百毫秒级。第五保留所有中间产物包括清洗后的 CSV、TF-IDF 特征矩阵的稀疏存储版、预训练模型的 checkpoint。我做这类项目养成的习惯是每跑一次实验就把参数和 F1 分数记录在一个单独的文本文件里不依赖记忆不依赖 Jupyter 输出。因为调参过程中的失败组合往往比成功组合更有价值翻看记录时能看到分数从 0.81 到 0.84 是靠哪个参数变过来的。这套方法在你换数据集、换题目时能直接复用。中文文本分类的核心链路就那么长把数据和评估管好模型反而不是最大的瓶颈希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑