☰
图书分类实战:从特征工程到深度学习模型全解析
2026/10/15 13:11:43 网站建设 项目流程

1. 图书分类任务到底在做什么

图书分类这个事,乍一听像是图书馆管理员才关心的活计,但放到机器学习语境里,它其实是一个非常典型的多分类任务。你手头有一堆书,每本书有书名、作者、简介、标签、出版社、出版年份等信息,目标是预测这本书属于哪个类别——文学、科技、历史、艺术、少儿等等。听起来简单,但真正动手做一遍,你会发现从数据清洗到特征工程、从模型选型到调参优化,每一步都有坑。

我之所以拿图书分类来练手,是因为它的数据形态足够丰富:既有结构化字段(出版年份、页数),又有文本字段(书名、简介),还有类别型字段(出版社、作者国籍)。这意味着你可以在同一个任务里同时练到特征工程、传统机器学习模型和深度学习模型三条技术路线,性价比极高。

这篇文章适合谁看?如果你已经学过逻辑回归、决策树这些基础算法,但不知道真实项目里怎么把它们串起来,那这篇内容就是写给你的。如果你已经做过一些Kaggle入门赛,想看看工业界实际做文本分类时有哪些细节考量,同样可以拿走不少东西。我会从最朴素的特征工程开始,一步步过渡到GBDT,再切到深度学习方案,每一步都给出可复现的操作和踩坑记录。

2. 数据理解与预处理:别急着上模型

2.1 先搞清楚你手里有什么

拿到图书数据的第一件事不是写代码,而是看数据。我见过太多人一上来就train_test_split然后跑模型,结果准确率卡在60%上不去,回头一看发现类别标签里有重复项、文本字段里混着HTML标签、出版年份有负数。

假设我们的数据集包含以下字段:

字段名类型说明
title文本书名
author类别作者
publisher类别出版社
pub_year数值出版年份
pages数值页数
summary文本内容简介
category类别目标标签

第一步操作:统计每个类别的样本数量。如果发现某个类别只有十几本书,而最多的类别有几千本,那这就是典型的类别不平衡问题。处理方式后面会讲,但你必须先知道它存在。

第二步:检查缺失值。文本字段缺失和数值字段缺失的处理策略完全不同。summary缺失率超过30%的话,这个字段可能就不太能用了;pages缺失可以用中位数填充,但要注意填充后是否引入偏差。

2.2 文本清洗的实操细节

图书简介这类文本,常见的问题包括:

  • HTML标签残留:<p>这是一本...</p>
  • 特殊符号:&amp;、&nbsp;
  • 多余空白:连续空格、换行符
  • 中英文混排:有些简介里夹杂英文原版书名

清洗代码大概长这样:

import re def clean_text(text): if not isinstance(text, str): return "" text = re.sub(r'<[^>]+>', '', text) # 去HTML标签 text = re.sub(r'&[a-zA-Z]+;', ' ', text) # 去HTML实体 text = re.sub(r'\s+', ' ', text) # 合并空白 text = text.strip() return text

注意:不要过度清洗。有些人会把所有标点符号都去掉,结果把“科幻小说”和“科幻小说集”之间的区别也抹掉了。标点在某些场景下是有信息量的。

2.3 标签处理与数据集划分

标签如果是中文类别名,建议先映射成整数编码。但要注意保存映射关系,后面预测结果要还原回可读标签。

划分数据集时,分层采样是必须的。sklearn的train_test_split自带stratify参数,直接用就行。如果类别极度不平衡,还要考虑是否对少数类做上采样或对多数类做下采样。我的经验是:先不采样,跑一版基线看看效果,如果少数类的召回率惨不忍睹,再考虑采样策略。

3. 特征工程:传统方法的灵魂所在

3.1 文本特征:从词袋到TF-IDF

图书分类里最强的信号往往来自文本字段。最朴素的做法是词袋模型,但词袋的问题是高频词会主导特征。TF-IDF通过引入逆文档频率来压制这种情况。

from sklearn.feature_extraction.text import TfidfVectorizer tfidf = TfidfVectorizer( max_features=5000, ngram_range=(1, 2), min_df=3, max_df=0.9, sublinear_tf=True )

参数解释一下:max_features=5000控制特征维度,太大容易过拟合,太小信息不够;ngram_range=(1,2)表示同时考虑单个词和相邻词对,能捕捉“科幻 小说”这样的组合;min_df=3过滤掉出现少于3次的词,降噪;max_df=0.9过滤掉出现在90%以上文档中的词,这些词没有区分度;sublinear_tf=True用对数缩放词频,避免某个词重复出现太多次导致权重爆炸。

3.2 类别特征编码:目标编码的坑与技巧

出版社、作者这类高基数类别特征,直接用One-Hot会导致维度爆炸。目标编码是常用替代方案:用该类别的目标均值来替换类别值。但直接替换会引入标签泄露,必须配合交叉验证。

from sklearn.model_selection import KFold def target_encode(train_df, test_df, col, target, n_splits=5): kf = KFold(n_splits=n_splits, shuffle=True, random_state=42) train_df[col + '_te'] = 0 for train_idx, val_idx in kf.split(train_df): means = train_df.iloc[train_idx].groupby(col)[target].mean() train_df.loc[val_idx, col + '_te'] = train_df.iloc[val_idx][col].map(means) overall_mean = train_df[target].mean() train_df[col + '_te'].fillna(overall_mean, inplace=True) test_means = train_df.groupby(col)[target].mean() test_df[col + '_te'] = test_df[col].map(test_means).fillna(overall_mean) return train_df, test_df

实操心得:目标编码在树模型上效果通常不错,但在线性模型上要谨慎使用,因为它会改变特征的分布形态。

3.3 数值特征处理:分箱还是标准化

出版年份和页数这类数值特征,直接扔给树模型没问题,但给线性模型或神经网络就需要标准化。另外,出版年份可以考虑做分箱,比如按十年为一个区间,因为“2000年出版”和“2001年出版”在类别上的差异可能并不大,但“2000年”和“1990年”可能差异明显。

分箱的另一个好处是能处理非线性关系。比如页数在100-300之间可能是小说,300-500可能是教材,500以上可能是工具书,这种阶梯式关系用分箱比原始数值更容易被模型捕捉。

4. GBDT模型实战:从LightGBM调参到特征重要性分析

4.1 为什么选GBDT而不是随机森林

GBDT和随机森林都是树集成方法,但GBDT通过逐步拟合残差来降低偏差,通常在结构化数据上表现更好。LightGBM作为GBDT的高效实现,训练速度快、内存占用低,还支持类别特征直接输入,非常适合图书分类这种混合了文本和类别特征的任务。

4.2 特征拼接与模型训练

把TF-IDF矩阵、目标编码后的类别特征、数值特征拼在一起:

from scipy.sparse import hstack import lightgbm as lgb X_train = hstack([tfidf_train, other_features_train]).tocsr() X_val = hstack([tfidf_val, other_features_val]).tocsr() params = { 'objective': 'multiclass', 'num_class': num_classes, 'metric': 'multi_logloss', 'boosting_type': 'gbdt', 'num_leaves': 63, 'learning_rate': 0.05, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 5, 'verbose': -1 } dtrain = lgb.Dataset(X_train, label=y_train) dval = lgb.Dataset(X_val, label=y_val, reference=dtrain) model = lgb.train(params, dtrain, num_boost_round=1000, valid_sets=[dval], callbacks=[lgb.early_stopping(50)])

4.3 调参的优先级

LightGBM参数很多,但真正影响大的就那么几个。我的调参顺序是:

  1. num_leaves:控制模型复杂度,从31开始试,逐步增加到63、127
  2. learning_rate和n_estimators:两者要配合,学习率低就需要更多轮次
  3. min_data_in_leaf:防止过拟合的关键,类别不平衡时适当调大
  4. feature_fraction和bagging_fraction:增加随机性,提升泛化

踩坑记录:有一次我把num_leaves设到255,训练集准确率99%,验证集只有72%。后来降到63,验证集反而升到78%。树模型不是越复杂越好。

4.4 特征重要性分析与迭代

训练完之后用model.feature_importance()看哪些特征贡献大。通常TF-IDF里的某些关键词会排在前列,但也要关注类别特征和目标编码特征。如果发现某个特征重要性异常高,要检查是否存在标签泄露。

5. 深度学习方案:从TextCNN到预训练模型微调

5.1 什么时候该上深度学习

传统方法在几千到几万条数据上通常够用。但如果数据量超过十万条,或者文本语义复杂(比如简介里大量使用隐喻、反讽),深度学习的优势就体现出来了。另外,深度学习方案对特征工程的依赖更低,端到端训练更省心。

5.2 TextCNN快速上手

TextCNN是文本分类里性价比很高的架构:嵌入层 + 多尺度卷积 + 全局池化 + 全连接。代码不长,但效果往往比TF-IDF+GBDT好一截。

import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim, num_classes, filter_sizes, num_filters): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.convs = nn.ModuleList([ nn.Conv2d(1, num_filters, (fs, embed_dim)) for fs in filter_sizes ]) self.dropout = nn.Dropout(0.5) self.fc = nn.Linear(num_filters * len(filter_sizes), num_classes) def forward(self, x): x = self.embedding(x) # (B, L, D) x = x.unsqueeze(1) # (B, 1, L, D) x = [torch.relu(conv(x)).squeeze(3) for conv in self.convs] x = [torch.max_pool1d(f, f.size(2)).squeeze(2) for f in x] x = torch.cat(x, dim=1) x = self.dropout(x) return self.fc(x)

filter_sizes一般取2、3、4,分别对应二元、三元、四元词组;num_filters取64或128。嵌入维度128或256都行,数据量小的时候用128。

5.3 预训练模型微调:效果最好但成本最高

如果算力允许,用预训练语言模型做微调通常能拿到最好的效果。做法很简单:加载预训练模型,把分类头换成你的类别数,然后用小学习率微调。

from transformers import AutoModelForSequenceClassification, Trainer model = AutoModelForSequenceClassification.from_pretrained( 'bert-base-chinese', num_labels=num_classes )

注意:微调时学习率要设得很小,通常2e-5到5e-5。太大容易灾难性遗忘,把预训练学到的语言知识全冲掉。

5.4 深度学习方案的坑

  • 序列长度:图书简介长短不一,截断太短丢信息,太长显存吃不消。建议统计一下长度分布,取95分位数作为max_len。
  • 类别不平衡:深度学习对不平衡更敏感,可以在损失函数里加类别权重,或者用Focal Loss。
  • 过拟合:小数据集上微调预训练模型极易过拟合,早停和权重衰减要配上。

6. 模型评估与常见问题排查

6.1 别只看准确率

图书分类里类别不平衡是常态,准确率会被多数类主导。必须看宏平均F1和加权平均F1,以及每个类别的混淆矩阵。如果某个类别的召回率特别低,说明模型根本没学会识别它。

6.2 常见问题速查表

问题现象可能原因解决方向
训练集准确率高,验证集低过拟合增加正则、减少模型复杂度、早停
所有样本预测为多数类类别不平衡严重类别权重、重采样、Focal Loss
验证集准确率波动大数据量小或划分不合理交叉验证、分层采样
某些类别F1始终为0特征区分度不够检查该类别文本特征、增加外部特征
深度学习模型不收敛学习率过大或过小学习率预热、网格搜索

6.3 我踩过的三个坑

第一个坑:TF-IDF的max_features设太大。我一开始设了50000,结果LightGBM训练慢不说,验证集还过拟合。后来降到5000,效果反而更好。特征不是越多越好,关键是信噪比。

第二个坑:目标编码没做交叉验证。直接在整个训练集上算类别均值,导致标签泄露,验证集F1虚高到0.95,上线后直接掉到0.7。后来改成K折目标编码才正常。

第三个坑:深度学习模型没做学习率预热。前几个epoch损失直接爆炸,后来加了warmup,前10%的步数线性增加学习率,训练就稳了。

7. 方案选型与落地建议

7.1 三条路线的对比

维度特征工程+GBDTTextCNN预训练模型微调
数据需求几千条即可几万条几千条(靠预训练)
训练成本低(CPU可跑)中(需GPU)高(需GPU)
特征工程依赖高依赖低几乎不需要
可解释性强弱弱
典型F10.75-0.850.82-0.900.88-0.95

7.2 我的推荐路径

如果你刚开始做图书分类,我建议按这个顺序走:先用TF-IDF+LightGBM跑一版基线,把特征工程和评估流程跑通;然后上TextCNN,感受一下端到端训练的效果;最后如果算力允许,用预训练模型微调冲一波高分。每一步都要保留可复现的实验记录,方便对比。

7.3 后续扩展方向

图书分类做完之后,可以往几个方向延伸:一是做多标签分类,因为一本书可能同时属于“科幻”和“悬疑”;二是做层次分类,先分大类再分小类;三是引入封面图像特征,用多模态方案进一步提升效果。这些扩展都能在现有代码基础上迭代,不用推倒重来。

最后分享一个小技巧:不管用哪种方案,都先把验证集固定下来,所有实验都在同一个验证集上对比。我见过太多人每次调参都重新划分数据,结果实验之间根本没法比,白白浪费大量时间。

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

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

立即咨询