简介:THUCNews中文文本分类数据集是一份包含84万篇新闻文档、覆盖14个新闻类别的中文语料库,面向自然语言处理学习者和研究者,适用于文本分类、主题建模、情感分析等场景,也常用于验证分类模型在跨领域文本上的泛化效果。压缩包内共有46个文件,总大小约3.93MB,主要文件类型为27个Python脚本、6个shell脚本、4个TSV数据文件、4个JSON配置文件、3个TXT文本以及LICENSE和Markdown说明文档。其中Python脚本覆盖数据预处理、词典构建、模型训练、BERT微调与蒸馏等环节,shell脚本提供一键运行入口,JSON与TSV用于标签映射和数据组织,TXT包含停用词等辅助信息,整体结构清晰,便于直接复现实验。目前已有937人学习下载。附带的多分类器测试工程包含了从传统机器学习到深度学习的多种实现模块,能够帮助读者快速对比不同算法在THUCNews上的表现,节省自行搭建环境与编写代码的时间,中文NLP项目开发与算法评测均具参考价值。
1. THUCNews 是什么:84 万篇新闻、14 个类别,中文文本分类的省心起点
做中文文本分类,多数人卡在第一步的不是算法,而是数据。THUCNews 是清华大学整理的中文新闻语料,84 万篇文档、14 个类别,覆盖体育、财经、房产、家居、教育、科技、社会、时尚、游戏、娱乐、星座、汽车、彩票、股票。规模足够把模型喂饱,标签是单标签平铺结构,不需要处理多标签和层级关系,是中文文本分类里最省心的带标注数据集。
它解决的是最现实的问题:不用花几周爬新闻、洗正文、打标签。解压、读取、分词、建模一路走下来,就能验证你的分类方案在真实新闻文本上能拿多少分。毕业设计、算法对比、内容打标可行性预研,三条路都用得上。
适合刚接触中文 NLP 的学生,也适合要评估 TF-IDF 和 BERT 哪条路线划算的工程师。下面从下载讲到微调,新手能照抄,老手直接看参数和坑。
2. THUCNews 数据集下载与目录读取:两种渠道、一个核对脚本
2.1 数据集下载:官网 zip 的 403 处理与 Hugging Face 镜像
THUCNews 没有统一的 release 包发布渠道,最常见分发方式是清华大学 NLP 组官网页面上挂一个 zip 包。直接拿浏览器或 wget 去抓,翻车概率不低,最典型报错是 403 Forbidden。原因是下载地址对 Referer 校验很严,裸请求会被当成盗链挡掉。常见做法是带上 Referer 头,伪装成从官网页面点过去的请求:
wget --referer="http://thuctc.thunlp.org/" \ --user-agent="Mozilla/5.0" \ --tries=5 --timeout=60 \ -O THUCNews.zip \ "<官网实际下载链接>"命令里的<官网实际下载链接>要你从官网页面右键复制,每个发布窗口的路径都可能不一样,别照搬旧教程里的历史地址。--tries=5和--timeout=60是给大文件下载兜底,跨网络下载几百 MB 的 zip 断流是常态,没有重试策略就是在浪费生命。下载完先核对 zip 大小,跟页面标注对不上就直接删掉重来,别等解压到一半才发现包是坏的。
Hugging Face 上有人传过处理好的 THUCNews 镜像,用 huggingface-cli 下载支持断点续传:
pip install -U huggingface_hub huggingface-cli download --repo-type dataset --resume-download <HF上的THUCNews仓库名> --local-dir ./thucnews_hf我一般优先走镜像,原因很朴素:断点续传对大 zip 太重要了。但镜像仓库的内容版本可能和官网不一致,有的镜像做了 10 类裁剪,有的直接转成了 parquet 格式。下载前先看仓库说明里的样本量和类别数,别把一个裁剪版当成 14 类全量版用,后面算指标全是糊涂账。
提示:无论从哪条路下载,拿到手先做文件数核对:14 个文件夹、总文档数在 84 万量级,再往后走,避免用了一个残缺副本还以为是自己的模型不行。
2.2 目录布局:14 个类别文件夹与单篇文档读取
解压后看到的不是一张 CSV,而是一个规整的目录树:14 个文件夹,文件夹名就是类别名。每个目录里是大量 .txt 文件,一篇新闻一个文件,文件名是新闻编号,正文就是新闻原文,标题和正文连在一起,没有额外字段。这种"文件夹即标签"的设计对新手很友好,不需要解析任何标注文件,也省掉了像 YOLO 图像数据集那种逐张画框、导标注的体力活,标签系统天然可见。
读文件用 pathlib 就够了:
from pathlib import Path import random data_root = Path("THUCNews") cat_names = sorted([ p.name for p in data_root.iterdir() if p.is_dir() and p.name != "__MACOSX" ]) print("类别数:", len(cat_names)) print("类别:", cat_names) sample_file = random.choice(list((data_root / cat_names[0]).glob("*.txt"))) print("抽样文件:", sample_file.name) print(sample_file.read_text(encoding="utf-8", errors="ignore")[:200])sorted保证类别顺序稳定,不依赖文件系统返回顺序,这是实验可复现的前提,后面 label2id 也依赖这份顺序。过滤__MACOSX是必须的,macOS 解压 zip 会生成这个隐藏目录,不滤掉它会被当成第 15 个类别,样本量统计直接出错。read_text用errors="ignore"是为了容忍个别坏字节。抽样出来的文本大概率是一段新闻开头,如果看到篇首带"来源:xxx"之类的字样,那是原始抓取留下的,不用特特处理,对分类影响有限。
2.3 标签编码与生成器读取:别一次把 84 万篇全塞内存
把文件夹名转成数字 id,是训练模型前必须做的一次映射,顺便把读取逻辑写成生成器:
label2id = {name: idx for idx, name in enumerate(cat_names)} id2label = {idx: name for name, idx in label2id.items()} def iter_documents(data_root, label2id): """逐条 yield (text, label),避免一次性加载全部文档""" for name, label in label2id.items(): folder = data_root / name for fp in folder.glob("*.txt"): text = fp.read_text(encoding="utf-8", errors="ignore").strip() if not text: continue yield text, label这里的核心边界在iter_documents用生成器 yield,一次只在内存里留一条样本。84 万篇全文先算一笔账:每篇平均几百到两千字,全量原始文本就是 1~2 GB,再去掉分词结果、向量矩阵的中间变量,16G 内存的机器很容在预处理阶段被 kill。label2id依赖上一步 sorted 的cat_names,训练脚本和后续推理脚本必须共用同一份映射,否则类别错位是迟早的事。之后做切分、做 PyTorch Dataset,都从这生成器取数据。
3. 20 分钟跑通中文文本分类基线:jieba 分词、TF-IDF 与逻辑回归
3.1 预处理三件套:jieba 分词、停用词过滤、单字剔除
THUCNews 是中文新闻,和英文最大的区别是词之间没有空格,分词是第一道工序。常见做法是 jieba,速度快,新闻领域词典覆盖也够。分词之后还有两件事:去停用词、过滤无意义 token,顺序不能乱。
import jieba # stopwords.txt 用哈工大停用词表,一行一个词 STOP_WORDS = set() for line in open("stopwords.txt", encoding="utf-8"): word = line.strip() if word and not word.startswith("#"): STOP_WORDS.add(word) def cleaner(raw_text): # 压缩连续换行和首尾空白,保留段落语义 text = " ".join(raw_text.split()) tokens = [] for w in jieba.cut(text): w = w.strip() if not w or w in STOP_WORDS: continue # 单字非数字才丢弃:彩票、股票、汽车里的数字是有用特征 if len(w) == 1 and not w.isdigit(): continue tokens.append(w) return " ".join(tokens)逻辑说明:raw_text.split()再 join,是同时完成首尾去空白和把\r\n连续换行压成空格,直接replace("\n", "")会把段落边界弄丢。jieba.cut用默认精确模式,切出来的词做停用词过滤。单字丢弃规则里我特意放行了数字——THUCNews 里彩票、股票、汽车这几类样本大量出现号码、价格、排量数字,统一丢单字等于把类别强信号砍掉。
参数说明:停用词表选什么直接影响结果,我一般用哈工大停用词表再自增少量词。"新闻""记者"这类几乎每篇都有、没有区分度的词可以加进去,但"股票""汽车"这种和类别强相关的词绝对不能进停用词表。另外,分词错误是常态,不要追求切词完美,TF-IDF 对切得碎一点的容忍度比你想象的高。
3.2 TF-IDF 特征化:词表规模、ngram 范围与 fit 边界
分词之后要把文本转成向量。TF-IDF 是文本分类最稳的基线特征,sklearn 一行搞定:
from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( max_features=50000, # 词表上限,84 万篇不去重词汇轻松破百万 ngram_range=(1, 2), # 单字词 + 双词短语,保留“新能源汽车”这类组合 min_df=2, # 至少在 2 篇文档出现,过滤单篇私货词 max_df=0.8, # 去掉 80% 文档都出现的词,停用词漏网之鱼 sublinear_tf=True, # 词频用 1+log(tf),弱化长新闻的高频词权重 ) train_vec = vectorizer.fit_transform(train_texts) # 只 fit 训练集 val_vec = vectorizer.transform(val_texts) # 验证集只 transform参数边界:max_features决定内存和训练时间,5 万是中庸值,想冲精度可以慢慢加到 10 万,但收益递减。ngram_range=(1, 2)对中文尤其重要,纯 unigram 会把"新能源汽车"裂成"新""能源""汽车",语义碎掉;加到 (1, 3) 在新闻这类规范文本上提升有限,特征维度涨得却很快。max_df=0.8是个经验值,THUCNews 里跨类高频词去掉后,分类器能少学一堆无关共性。
这里最关键的是 fit/transform 边界:vectorizer 只能在 train 上fit_transform,val 和 test 只能transform。如果手滑在全体数据上 fit 了,验证分数虚高到没有参考价值,这是文本分类里最常踩的泄漏之一,后面避坑章单独讲。
3.3 切分与训练:一段能直接复现的完整代码
把预处理、切分、训练、评测串起来,这就是第一个能跑的成绩单:
from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report # 生成器落地为列表,便于切分;内存紧张就只保留编号索引 all_texts, all_labels = [], [] for text, label in iter_documents(data_root, label2id): all_texts.append(cleaner(text)) all_labels.append(label) X_train, X_val, y_train, y_val = train_test_split( all_texts, all_labels, test_size=0.2, stratify=all_labels, # 按类别比例分层切分,防止小类被抽干 random_state=42, ) vec = TfidfVectorizer( max_features=50000, ngram_range=(1, 2), min_df=2, max_df=0.8, sublinear_tf=True, ) train_vec = vec.fit_transform(X_train) val_vec = vec.transform(X_val) clf = LogisticRegression(C=2.0, max_iter=500, solver="liblinear") clf.fit(train_vec, y_train) print(classification_report(y_val, clf.predict(val_vec), target_names=cat_names, digits=4))逻辑说明:train_test_split里stratify=all_labels是后悔药参数——THUCNews 各文件夹样本量不完全均等,不做分层切分,验证集里星座、彩票这类偏少的类可能只剩十几条,F1 波动大到没法看。random_state=42固定切分,保证每次实验可比,这是后面调参不产生幻觉的前提。
参数说明:LogisticRegression用solver="liblinear"配合中小规模稀疏矩阵收敛快,C=2.0是我在这个数据集上的常用起点。C 是正则强度的倒数,太大特征拟合过头,太小欠拟合。max_iter=500避免默认迭代次数出收敛告警。这一套在我机器上 5 万条子集约 1 分钟跑完,全量也只要十几分钟,准确率在 92%~94% 区间浮动。分数就是基线:后面任何模型低于这个数字,先怀疑数据处理,不是模型的问题。想再快一点,可以换 fastText 路线,gensim 里封装好了,训练更快但准确率略低,适合先探路。
提示:这个基线分数是整条链路的体检报告。如果连 LR 都到不了 90%,优先查 3.1 的停用词表和 3.2 的 fit 边界。
4. 14 类样本分布与评测:准确率之外,用混淆矩阵和 F1 验收
4.1 先统计 14 类的样本分布,再定切分策略
14 个文件夹的文件数决定了后面所有策略。不要凭印象,直接统计:
counts = {} for name in cat_names: counts[name] = len(list((data_root / name).glob("*.txt"))) total = sum(counts.values()) for name in cat_names: print(f"{name:6s} {counts[name]:6d} {counts[name] / total:8.2%}") print("总文档数:", total)这段代码的输出决定你后面要不要 stratify、要不要做类别加权。不同发布版本分布有差异:做过均衡抽样的版本,每个类在 5 万~6 万量级;原始版本里娱乐、体育这类热门频道明显偏多,星座、家居偏少。拿到数据先跑这段,把结果存成 JSON,后面每次改实验都对着这份分布看,别拿印象代替数据。
按分布选策略,参考这张表:
| 分布情况 | 切分与训练策略 | 适用场景 |
|---|---|---|
| 各类接近均衡 | 普通随机切分,stratify 可有可无 | 教学、基准确认 |
| 少数类明显偏少 | stratify + class_weight="balanced" | 少数类是业务重点时 |
| 少数类占比低于 1% | 重采样或数据增强(回译、同义词替换) | 细粒度分类任务 |
| 只想快速验证 | 每类抽 5000~10000 篇做子集 | 机器内存小、先跑通流程 |
我一般建议先抽子集把整条链路走通再上全量。84 万篇跑 LR 基线不贵,但接深度学习全量训 TextCNN 或 BERT 就贵了。子集跑通,全量只是改路径和 batch size 的事,千万别一上来就 84 万篇全量开训,翻车了都不知道往哪查。
4.2 混淆矩阵:自动找出误判重灾区
准确率在 14 类问题上会骗人。分布偏向某几类时,全猜多数类也能有不错准确率。所以至少要看 macro-F1 和混淆矩阵:
import numpy as np from sklearn.metrics import confusion_matrix pred = clf.predict(val_vec) cm = confusion_matrix(y_val, pred) # 按行归一化,再置零对角线,剩下来的就是误判比例 cm_norm = cm.astype("float") / cm.sum(axis=1, keepdims=True) np.fill_diagonal(cm_norm, 0) confusions = [] for i in range(len(cat_names)): for j in range(len(cat_names)): if i != j and cm_norm[i][j] >= 0.01: confusions.append((cm_norm[i][j], cat_names[i], cat_names[j])) confusions.sort(reverse=True) for ratio, true_cat, wrong_cat in confusions[:5]: print(f"{true_cat} 被误判为 {wrong_cat}: {ratio:.2%}")逻辑说明:按行归一化后,每行代表"该类别里有多少比例被分到了哪一类",对角线置零后留下的就是错分去向。THUCNews 上常见的重灾区是财经被误判成股票、娱乐和时尚互相串、社会和教育互相串,因为这几对用词高度重叠。如果你的误判对和这个不太一样,优先怀疑分词或停用词处理,而不是数据集本身。
4.3 建议的调参顺序:词表、ngram、正则强度
这个数据集调参,顺序比数量重要。我的固定顺序:
- 先动分词和停用词:1~2 个点的波动多半在这。检查误判对,比如财经和股票串得厉害,看看"股票""基金""证券"这些词有没有被误加进停用词表。
- 再动
max_features:2 万加到 5 万、10 万,观察验证集准确率曲线,收益变平就停。 - 然后动
ngram_range:(1,1) 到 (1,2) 通常能涨 1 个点以上,(1,3) 在新闻语料上提升有限。 - 最后动 C:在 [0.5, 1.0, 2.0, 4.0] 上小网格搜索。
判断过拟合还是欠拟合:train 高、val 低是过拟合,把 C 调小或把min_df调大;train、val 都低是特征不够,升max_features、加 ngram。这里的"低"没有绝对数字,以你自己的基线为参照。比如 LR 到了 93%,deep 模型只有 91%,那就回到数据侧找问题,别在模型上硬耗。
5. 避坑:THUCNews 从下载到评测的 5 个高频问题与排查
5.1 下载 403 或断流:Referer 与镜像续传
现象:wget 或浏览器下载 zip 报 403 Forbidden;或者文件下载到 70% 断掉,重试还是断在同一个位置。
原因:官网下载地址对 Referer 头有校验,裸请求被当盗链拦截;文件体积大,弱网环境长连接容易被中间设备掐断。
解决:下载命令带--referer="http://thuctc.thunlp.org/"和--user-agent,伪装成正常浏览请求;优先走 Hugging Face 镜像,用--resume-download断点续传;下载完成后核对 zip 大小和解压后的文件总数。残缺包无论多可惜都要删掉重下,留着只会污染后续所有实验。
5.2 全量读取内存爆炸:生成器和子集
现象:脚本在读取阶段内存飙升,进程被系统杀掉,或者电脑直接卡死。
原因:一次性把 84 万篇文本、分词结果、特征向量全部 load 进内存,TF-IDF 阶段稀疏矩阵还会膨胀几十倍。
解决:文件读取用 2.3 节的生成器逐条 yield;TF-IDF 只保留稀疏矩阵,不要.toarray()转稠密;先抽每类 5000 篇跑通链路。另外,TfidfVectorizer在 84 万篇上 fit 本身要几分钟,属正常现象不是死机,学会看日志而不是盯着光标转圈。
5.3 空文件、乱码和异常字符
现象:某篇文档读出来是空的;正文里有不可见字符;分词结果里出现一堆孤立符号。
原因:THUCNews 来自线上新闻抓取,个别页面抓取不完整形成空文档;文件编码也并非全部是干净的 UTF-8。
解决:read_text统一加errors="ignore";空文本直接跳过;分词前把连续空白压缩成一个空格。如果发现某个类别的空文件特别多,统计一下占比,超过 1% 就要考虑换镜像版本或换个下载来源。
5.4 验证集泄漏:特征化之前就先切分
现象:验证集分数高得离谱,比如 99%,一换到生产或新数据就崩到 85%。
原因:TfidfVectorizer或词表在全体数据上 fit,验证集的词汇和 idf 统计已经进了特征学习,模型等于提前见过验证集的信息。
解决:切分永远在特征化之前;vectorizer 只fit_transform训练集,验证集只transform;jieba 的自定义词典同理,只能用训练集统计出来的词表。这条适用于任何做统计特征的模型,是文本分类里最常见的高分陷阱。
5.5 实验不可复现:种子、目录顺序与随机性
现象:同一个脚本两次运行结果不一样,深度模型训练更是每次飘一点。
原因:train_test_split没固定 random_state;文件夹迭代顺序依赖文件系统返回;深度学习里 GPU 的非确定性算子没有禁用。
解决:文件夹名先 sorted 再做 label2id;所有切分固定random_state=42;PyTorch 脚本开头设置全局种子并打开cudnn.deterministic。改任何参数之前,先确认同一份代码两次跑的结果一致,否则后面的调参对比全是幻觉。
6. 进阶:用预训练模型微调 THUCNews,盯住最差类别 F1
6.1 把 THUCNews 组织成 PyTorch 数据集
LR 基线跑到 93% 后,想再往上走,常见做法是用中文预训练模型微调,效果拔尖的是bert-base-chinese,想省显存可以选hfl/rbt3。这一步把前面的生成器接上一个 Dataset 类:
from torch.utils.data import Dataset from transformers import AutoTokenizer import torch tokenizer = AutoTokenizer.from_pretrained("hfl/rbt3") class ThucNewsDataset(Dataset): def __init__(self, texts, labels, max_len=128): self.texts = texts self.labels = labels self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): enc = tokenizer( self.texts[idx], truncation=True, max_length=self.max_len, padding="max_length", return_tensors="pt", ) return { "input_ids": enc["input_ids"].squeeze(0), "attention_mask": enc["attention_mask"].squeeze(0), "labels": torch.tensor(self.labels[idx]), }max_len=128是性价比起点:新闻的主题和结论基本在开头,128 个 token 足以区分大部分类别。如果混淆矩阵里长文档类还在打架,把max_len提到 256,但训练时间也跟着涨。
6.2 微调参数与训练曲线观察
用 transformers 的 Trainer 省去手写训练循环:
from transformers import ( AutoModelForSequenceClassification, TrainingArguments, Trainer, ) from sklearn.metrics import f1_score, accuracy_score model = AutoModelForSequenceClassification.from_pretrained( "hfl/rbt3", num_labels=14 ) def compute_metrics(eval_pred): logits, labels = eval_pred preds = logits.argmax(axis=-1) return { "acc": accuracy_score(labels, preds), "f1": f1_score(labels, preds, average="macro"), } args = TrainingArguments( output_dir="./thucnews_bert", num_train_epochs=2, per_device_train_batch_size=64, learning_rate=2e-5, warmup_ratio=0.1, weight_decay=0.01, evaluation_strategy="steps", eval_steps=500, save_strategy="epoch", fp16=True, metric_for_best_model="f1", load_best_model_at_end=True, )参数说明:learning_rate=2e-5是中文分类微调的常见起点,别用默认的 5e-5 甚至更大;warmup_ratio=0.1让前 10% 步数学习率线性爬升;fp16只在支持的显卡上开,CPU 跑就去掉。全量 84 万篇训 BERT 太贵,我一般每类抽 1 万篇做验证,跑通后再决定要不要加钱上全量。
6.3 两个验证技巧
第一个技巧:盯最差类别 F1。BERT 微调后总分可能只比 LR 高 2~3 个点,但逐类看 F1,提升几乎集中在财经/股票这类纠缠类别上。如果最差类别的 F1 没动,说明模型只是整体变强,没解决真正的分界问题。
第二个技巧:保存模型按 F1 而不是准确率。metric_for_best_model="f1"配合load_best_model_at_end=True,避免出现准确率最高但少数类别崩掉的歪模型。验证集上再做一次真实分布核对,确认预测结果按业务价值的分布是对的,再谈上线。
我现在拿到 THUCNews 这种数据集,第一件事永远是先统计分布、定切分、跑 LR 基线,之后才谈要不要上预训练模型。见过太多团队在 BERT 上烧了几天算力,最后发现掉点原因是验证集泄漏,那就太亏了。希望你先跑通基线再进阶,让这份 84 万篇的语料按你的预期出货,希望帮到你。
本文还有配套的精品资源,点击获取