☰
中文文本分类入门实践:从新闻数据集到TF-IDF与FastText
2026/10/18 18:34:34 网站建设 项目流程

简介:源自今日头条新闻聚合平台的中文新闻文本数据集,带有明确的类别标签,面向NLP研究者、算法工程师及文本分类学习者,可用于训练和评估多类新闻分类模型,覆盖国际、国内、娱乐、体育等常见主题。资源压缩包共4个文件,包括Python数据处理脚本、两份Markdown格式的使用说明文档以及一个核心数据压缩包,整体大小约25.67MB。借助脚本和文档,用户可以快速完成数据解压、格式查看与文本清洗,进而开展分词、TF-IDF或词向量表示、模型构建与超参数调优等实验;配套说明也降低了数据集的上手门槛。该数据既适合入门者系统了解文本分类全流程,也适合进阶者作为基准数据验证不同模型效果或研究类别分布问题。目前已有1003人学习下载。

1. 中文新闻文本分类数据集:为什么拿它当 NLP 入门的第一份语料

做中文文本分类最烦的不是模型不会写,而是找不到一份量够、标签相对干净、能直接跑通全流程的数据集。这份新闻文本分类数据集(目录名 toutiao-text-classfication-dataset-master)就是专门解决这个问题的:压缩包里是十几万条带分类标签的新闻标题,覆盖民生、文化、娱乐、体育等常见资讯频道,拿来做文本分类入门实验、特征工程验证、模型基线对比都合适。适合刚接触 NLP 的学生,也适合要快速验证业务分类方案的开发者——先用它把整个管线打通,再迁移到自己的数据上,成本低很多。

2. 数据集结构与读取:先搞清楚标题和标签是怎么排布的

2.1 压缩包里有什么:文件清单与各自用途

拿到 zip 后先别急着解压数据,先看压缩包里的 readme.md 和 ss.md。常见情况下,release 包会包含以下几个文件:

文件用途
get_data.py数据下载与更新的脚本,网络不通时可以手动下载数据包
readme.md数据集说明,包含字段格式、类别编号映射
ss.md补充说明或示例,不一定每个版本都有
toutiao_cat_data.txt.zip真正的数据文件,解压后是一个较大的 txt

这里的重点是:主数据文件是 toutiao_cat_data.txt,不是 CSV。读它之前必须确认编码和分隔符。readme 里通常会写明每一列的含义,常见做法是用 tab 分隔,字段依次是新闻 ID、分类编号、标题、关键词、摘要描述。如果你拿到手的文件把 readme 弄丢了,也别瞎猜,用最原始的 head 命令看前几行:

head -5 toutiao_cat_data.txt

head 输出会直接告诉你分隔符长什么样。如果是制表符,在终端里看起来会有大段空白,这时就能确认sep="\t"。这一步看起来不起眼,却是后面所有工作的基础——把分隔符认错,pandas 会把整行读成一列,后面全乱套。

2.2 用 pandas 读取数据:分隔符、编码和行数的坑

确认格式后,用 pandas 读取就很直接。我一般先读前几千行验证,而不是一把梭读全量,避免文件坏了或分隔符不对时白等几分钟:

import pandas as pd column_names = ["news_id", "label", "title", "keywords", "desc"] df = pd.read_csv("toutiao_cat_data.txt", sep="\t", header=None, names=column_names, nrows=5000, encoding="utf-8") print(df.head()) print(df.info())

代码说明:sep="\t"指定制表符分隔;header=None表示源文件没有列名;names手动指定列名,这里五个列名是按常见格式给的,具体以 readme 为准;nrows=5000先读五千条验证结构;encoding先试 utf-8,如果出现乱码或报UnicodeDecodeError,再换成gbk或utf-8-sig。

运行后如果 df 正常分出 5 列,说明格式猜对了。如果只有一列,大概率是文件实际用逗号或其他符号分隔,回去看一眼 head 输出重新设 sep。确认格式后就可以全量读取,15 万条文本数据占用的内存不大,普通电脑都扛得住。但如果你在服务器上用 Jupyter 边跑边看,内存占用会随变量缓存累积,建议每跑完一步就释放中间变量,比如del df或import gc; gc.collect()。我遇到过因为不清理变量,前后跑了几十次实验把 16G 内存吃满的尴尬。

2.3 类别分布:先看清 15 个类有多少样本

文本分类任务里,标签分布直接影响模型效果和评估指标。先用 value_counts 统计每个类别的数量:

label_counts = df["label"].value_counts().sort_index() print(label_counts) label_counts.plot(kind="bar", figsize=(12, 4))

从统计结果你能立刻看出:有没有样本极少的类别,比如某些类别不到几百条;有没有多到离谱的类别,比如某类占了三成。这个分布决定了后面要不要做采样、要不要用 class_weight、评估时看「准确率」还是「Macro-F1」。如果训练集和测试集划分时不按比例分层,小类别很容易在测试集里缺失,导致 F1 无法计算。

除了数量,还要确认标签编号是否连续。有些版本的数据 label 编号从 1 到 17,中间可能有缺号;有些把类别编号和文本分类的 15 类对应。为了避免后续 target_names 错位,谨记先value_counts().sort_index()看全貌,再对照 readme 里的映射表。我见过不少同学拿到数据直接train_test_split(test_size=0.2)就开跑,结果小类别在测试集里只剩几条,评估结果波动很大。后面第四章会专门演示 stratify 参数,先记住:文本分类的类别分布几乎不可能均匀,划分时必须按标签分层。

3. 文本预处理与特征表示:清洗、分词和 TF-IDF 参数怎么设

3.1 标题清洗:删 URL、删符号,但别过度删除

新闻标题里夹杂着 URL、#话题#、@用户名、数字和全角标点。这些噪声对分类有干扰,但也不是全得删。我的习惯是先用正则把这些明显噪声删掉,再统一空格:

import re def clean_title(text): if not isinstance(text, str): return "" text = re.sub(r"https?://\S+|www\.\S+", "", text) # 删 URL text = re.sub(r"#.+?#", "", text) # 删话题标签 text = re.sub(r"@\S+", "", text) # 删 @用户 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9]", " ", text) # 非中英文数字变空格 text = re.sub(r"\s+", " ", text).strip() return text df["clean_title"] = df["title"].apply(clean_title)

正则说明:\S+匹配非空白连续串,URL 和 @用户都可以一次匹配掉;[\u4e00-\u9fa5]保留中文字符,a-zA-Z0-9保留英文和数字,其余全变成空格,再压缩多余空格。注意这里保留数字是因为新闻标题里“2025新规”这类数字对类别判断有用,如果你想验证数字是否影响效果,可以再出一版全删数字的对照实验。

有些同学会把所有数字也删掉,这个要看场景。新闻标题里的年份、数量词有时和类别强相关,比如“2025年养老金上调”容易被分到民生类,数字删掉后反而丢失线索。所以我不建议模板化地全删,留着做对比实验更有说服力。

3.2 分词:jieba 的默认模式就够用,但要去停用词

中文标题分词用 jieba 是主流做法。默认全模式会分出大量重复词,用精确模式或搜索引擎模式都行,我一般用 lcut 精确模式:

import jieba stopwords = set() with open("stopwords.txt", encoding="utf-8") as f: for line in f: stopwords.add(line.strip()) def tokenize(text): words = jieba.lcut(text) return " ".join(w for w in words if w not in stopwords and len(w.strip()) > 0) df["tokens"] = df["clean_title"].apply(tokenize)

说明:stopwords.txt 可以自己维护一份,包含“的、了、在、是”等常见虚词,也可以在网上找现成的中文停用词表。分词结果用空格连接是为了后面 TfidfVectorizer 直接拿字符串作为输入。如果你发现某些领域词被切错,比如“有机食品”被切成“有机”和“食品”,可以把自定义词加入jieba.add_word(),这在第四章会用到。

分词粒度对结果影响很大。jieba 默认词典偏向通用语料,新闻中的“双减”“碳中和”这类词如果被切碎,特征表达力会下降。遇到验证集效果明显差于训练集时,优先怀疑分词粒度,而不是急着换模型。

3.3 TfidfVectorizer 参数怎么设:max_features 和 ngram_range 是核心

TF-IDF 是文本分类的经典基线。sklearn 的 TfidfVectorizer 参数很多,关键是控制特征维度和稀疏性:

from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( max_features=80000, ngram_range=(1, 2), min_df=2, max_df=0.8, sublinear_tf=True ) corpus = df["tokens"].tolist() X = vectorizer.fit_transform(corpus) print(X.shape)

参数说明:max_features=80000限制特征总数,防止词表过大造成内存爆炸;ngram_range=(1,2)表示同时使用单个词和相邻两词组成的短语,比如“人工智能”这种组合词会被拆成“人工”“智能”“人工智能”三个特征,能抓到一部分词组语义;min_df=2过滤只在一条新闻里出现过的词,避免长尾噪声;max_df=0.8过滤出现在 80% 以上文档里的词,这类词基本都是公共词,区分度低;sublinear_tf=True对词频做 log 压缩,削弱高频词的绝对优势。

这里最容易翻车的操作是X.toarray()把稀疏矩阵转成稠密矩阵。15 万条样本 × 8 万维特征,转成稠密矩阵直接爆内存。正确做法是保持稀疏矩阵格式,直接交给逻辑回归或 SVM 训练。

3.4 第一个基线:逻辑回归 + TF-IDF

特征搞定后,用最简单的逻辑回归就能跑出一个不错的基线:

from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report y = df["label"].values X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) clf = LogisticRegression(C=1.0, max_iter=1000) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print(classification_report(y_test, y_pred, digits=3))

划分时stratify=y很重要,它保证训练集和测试集中每个类别的比例一致,避免小类别在测试集中缺失。逻辑回归默认采用 L2 正则,C=1.0表示正则强度适中,如果你的验证集效果不稳定,可以尝试把 C 调大到 3.0 或 10.0,观察准确率和 F1 的变化。max_iter=1000是因为高维稀疏特征下默认的 100 次迭代经常不收敛,训练时会打印警告,把它加到自己舒服的值为止。

跑完之后分类报告里会有每个类别的 precision、recall、f1-score 以及 overall accuracy。以我的经验,这个数据集上用 TF-IDF + 逻辑回归,准确率能做到 80% 以上,macro-F1 通常也在 0.78 到 0.83 之间。这个数字不用羡慕,它只是告诉你怎么往下一步调优。

4. 分类模型训练:从线性 SVM 到 FastText 的对比与调优

4.1 为什么线性 SVM 比非线性核函数更适合新闻标题

逻辑回归之后,线性 SVM 是性价比最高的升级选择。原因是:新闻标题经 TF-IDF 后特征维度高而且样本数量大,线性分类器在这类数据上训练速度快、泛化能力不差。我见过有人在这个数据集上跑 RBF 核 SVM,跑了半小时没出结果,最后放弃。血泪经验是:不要轻易上非线性核。

scikit-learn 里推荐用LinearSVC而不是SVC(kernel='linear'),前者基于 liblinear,能处理大规模稀疏数据,后者基于 libsvm,不适合稀疏高维大样本。代码上只有一行差别,训练时间却有量级差异。

4.2 LinearSVC 训练与参数调整

from sklearn.svm import LinearSVC svm = LinearSVC(C=1.0, max_iter=2000) svm.fit(X_train, y_train) y_pred_svm = svm.predict(X_test) print(classification_report(y_test, y_pred_svm, digits=3))

LinearSVC 的核心参数是 C 和 loss。C 是正则化系数的倒数,C 越小正则越强,C 太大容易过拟合;loss='squared_hinge'是默认值,理论上收敛比'hinge'更稳,训练也更平稳。如果你的数据量不大,也可以试loss='hinge',但训练时间会稍长。

C 参数在 0.1 到 10 之间往往都是可用的,具体取值带点玄学,建议直接用验证集网格搜索,别凭感觉。训练时如果看到ConvergenceWarning: Liblinear failed to converge,不要慌,两种处理方式:增大 max_iter,比如 4000 到 6000;或者调小 C,让模型没那么激进。我通常先调 C,因为单纯增大 max_iter 在 liblinear 里可能只是把问题延后,不解决本质的优化困难。

4.3 指标选择:准确率会骗人,Macro-F1 更可信

类别不平衡时,模型会把高频类学好、低频类学差。准确率只统计总体预测正确的比例,低频类全错也能有很高的准确率。评估多分类文本模型,我习惯同时打印准确率和 macro-F1:

from sklearn.metrics import accuracy_score, f1_score print("accuracy:", accuracy_score(y_test, y_pred_svm)) print("macro-f1:", f1_score(y_test, y_pred_svm, average="macro"))

macro-F1 是把每个类别的 F1 算出来再平均,对每个类别一视同仁。如果你的 macro-F1 比 accuracy 低很多,说明低频类别确实拉胯。这时可以去看分类报告里哪类 recall 特别低,后续针对该类别补充数据或调整权重。

4.4 FastText:训练快但需要准备好输入格式

深度学习模型不是必须一开始就上。FastText 作为文本分类利器,优势是训练速度极快、支持 n-gram 特征、能在 CPU 上跑,在你换 BERT 之前很值得做一次对比。使用 fasttext 库之前,要把训练数据整理成它要求的格式:每行是一个样本,类别带__label__前缀,后面跟着分词后的文本,用空格隔开。

import fasttext with open("fasttext_train.txt", "w", encoding="utf-8") as f: for tokens, label in zip(df["tokens"], df["label"]): line = f"__label__{label} {tokens}\n" f.write(line) model = fasttext.train_supervised( input="fasttext_train.txt", epoch=20, lr=0.5, wordNgrams=2, minCount=2 )

参数说明:epoch=20表示训练轮数,FastText 训练快,20 轮也就几分钟;lr=0.5是初始学习率,太大会震荡,太小收敛慢;wordNgrams=2开启词二元组特征,中文标题里“新能源汽车”切成“新能源”“汽车”后,二元组能保留“新能源_汽车”的共现信息,对分类帮助明显;minCount=2过滤低频词,和 TF-IDF 的 min_df 同理。

模型训完,预测时注意 fasttext 返回的是带__label__前缀的结果:

result = model.predict("新春 走基层 记者 探访 农村") print(result) # (('__label__5',), array([...]))

要拿类别编号就取int(result[0][0].replace("__label__", ""))。FastText 的调参空间比 TF-IDF 多,wordNgrams 从 1 调到 2 往往就有明显增益,再往上 3 会牺牲训练速度。另一个容易踩的点是:训练集里如果有空行或空文本,fasttext 会直接报错,写文件前先过滤掉 tokens 为空的样本:

df = df[df["tokens"].str.strip() != ""]

5. 避坑与常见问题:标签错位、重复样本和内存爆炸

5.1 标签编号与类别名错位,分类报告里的类名全是错的

现象:训练出的模型没问题,但打印 classification_report 时,类别名和数字对不上,看起来某个类别效果极差,另一个类别又诡异的好。

原因:数据集的类别编号不是从 0 开始,而是从 1 开始;或者 readme 里的映射表和代码里的目标名单顺序不一致。很多人直接用了target_names=list(range(1, 18)),把编号当顺序,于是整个报告张冠李戴。

解决:第一步,跑sorted(df["label"].unique())看真实类别编号。第二步,去 readme 里找映射表,构建一个字典,例如{8: "娱乐", 5: "体育"},再传入 classification_report 的 target_names。要养成习惯,永远不要靠猜测把 label 数字映射到名字。

5.2 训练集和测试集标题重复,准确率虚高到吓人

现象:某同学做基线,随机划分后用 TF-IDF + 逻辑回归准确率直接到 96%,但用同一个模型在另一批新数据上效果掉到 70%。他觉得模型没问题,其实是训练集和测试集里有大量重复标题。

原因:原始数据集是从新闻站点爬下来的,同一事件可能有多条相同标题,也可能不同频道转载同一条新闻。随机划分时,相同标题可能同时进入训练集和测试集,模型相当于「背题」而不是「理解」。

解决:划分之前先对标题去重。最简单的方式是按标题分组,保证同标题只落在一个集合里:

df = df.drop_duplicates(subset=["title"], keep="first") print(df.shape)

如果标题重复率高,去掉后样本量会减少几千到几万条,这个取舍很值得。更稳妥的办法是给标题做一次 MD5 哈希,把哈希值作为分组键做 GroupShuffleSplit,再用索引去划分,这样能保证同一标题的变体也不会串集。我在实验里直接用 drop_duplicates,够用。

5.3 jieba 分词全量跑太慢,内存还持续上涨

现象:对 15 万条新闻标题做jieba.cut,跑了十几分钟还没结束,任务管理器里内存占用一直在涨,最后可能直接把笔记本卡死。

原因:jieba 默认词典包含大量词,加载就占了几十 MB;而逐条把生成器转列表,还会积累中间结果。另外jieba.cut每次调用都要重新检查词典,线程上也有开销。

解决:先把停用词表和自定义词一次性加载好,然后分批量处理,不要一次全部做;开启并行模式能显著提速,但要注意 jieba.enable_parallel 在 Windows 下有时不生效,Linux 下更稳定。

import jieba jieba.enable_parallel(4) # 开启 4 线程并行,Windows 慎用 def tokenize_batch(texts): return [" ".join(w for w in jieba.lcut(t) if w not in stopwords) for t in texts] df["tokens"] = tokenize_batch(df["clean_title"].tolist()) jieba.enable_parallel(0) # 用完记得关闭

如果你发现分词结果里数字被错误切分,可以加jieba.add_word("新能源汽车")这类自定义词,把领域词固定下来。不过自定义词加太多会拖慢分词速度,一般只加对分类影响大的词。

5.4 TfidfVectorizer 内存爆炸,甚至还没训练就 OOM

现象:vectorizer.fit_transform(corpus)正常,但接下来X.toarray()直接把内存耗完,Jupyter 内核直接死掉。

原因:稀疏矩阵存储经验是 8 万维特征 × 15 万条样本,用稀疏矩阵只存非零元素;一旦转稠密矩阵,就要存储 120 亿个元素,每个 float 8 字节,那是几百 GB 的运算量。

解决:永远不要对 TF-IDF 结果调用 toarray。sklearn 的分类器都支持稀疏矩阵输入,直接传 X_train 和 X_test。如果特征维度太大导致训练慢,用max_features=50000或30000重新试,先出结果再谈提升。如果还是不放心,可以用 HashingVectorizer 做特征哈希,牺牲词频可解释性换内存:

from sklearn.feature_extraction.text import HashingVectorizer vectorizer = HashingVectorizer(n_features=2**20, alternate_sign=False) X = vectorizer.fit_transform(corpus)

HashingVectorizer 不存储词表,n_features 可以手动定,内存占用更可控,缺点是你无法从模型系数里看出哪些词贡献大。

5.5 准确率不低但 F1 很差,原来是类别不平衡的锅

现象:某个类别样本只有几百条,模型预测时几乎全预测成高频类,结果这个类别的 recall 和 F1 惨不忍睹,但整体准确率却有 85%。

原因:模型在优化的目标函数上对每个样本一视同仁,高频类贡献的损失大,自然被优先学习。低频类学不到足够的判别信息,预测时容易被吞掉。

解决:两种常见修正。一是给分类器加 class_weight:

from sklearn.svm import LinearSVC svm = LinearSVC(class_weight="balanced", C=1.0, max_iter=2000)

二是做类别重采样,但文本数据重采样容易改变标题分布,不建议直接复制。更推荐的是按类别计算权重,在评估时以 macro-F1 为准,先不追求所有类一致,把注意力放在提升低频类的 recall。如果你后续用深度学习,可以在损失函数里对类别权重做矩阵乘法,这是进阶玩法。

6. 进阶:用混淆矩阵和错误样本反推标签质量

6.1 先跑一个快速模型,把错误样本找出来

微调 BERT 之前,花十分钟做一次数据质量验证,能省下后续很多无意义的调参。我习惯的做法是:用线性 SVM 或逻辑回归先跑一遍,然后找出预测错误的样本,人工抽查。这个步骤表面看是浪费时间,实际上能快速暴露标签噪声和特征泄漏。

import pandas as pd err = pd.DataFrame({ "title": df.loc[test_index, "title"], "true_label": y_test, "pred_label": y_pred_svm }) err = err[err["true_label"] != err["pred_label"]] print(err.head(20))

如果发现同一标题在不同行被标成两个类别,说明原始数据标签有噪声;如果错误样本集中在特定类别,说明该类别的标题表达本身有歧义。这些发现比模型调参更有价值。

6.2 用混淆矩阵定位易混淆类别

from sklearn.metrics import confusion_matrix cm = confusion_matrix(y_test, y_pred_svm) # cm 第 i 行是真实类别,第 j 列是预测类别

观察混淆矩阵中非对角线上的大数值,往往能找到几组高频互混类别,比如「民生」和「社会」这类语义高度重叠的频道。知道了这一点,你可以去 readme 里确认两个类别是否真的能区分,而不是盲目增加数据集规模。

6.3 让数据验证成为固定习惯

从那以后,我每次拿到新数据集,都会强制走一遍:标签分布统计、标题去重、错误样本抽查。哪怕急着调模型,也先跑完这三步再动手。这套流程帮我避开了无数次因数据问题导致的实验翻车,也让我养成了「先怀疑数据,再怀疑模型」的直觉。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询