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_features5000, ngram_range(1, 2), min_df3, max_df0.9, sublinear_tfTrue )参数解释一下max_features5000控制特征维度太大容易过拟合太小信息不够ngram_range(1,2)表示同时考虑单个词和相邻词对能捕捉“科幻 小说”这样的组合min_df3过滤掉出现少于3次的词降噪max_df0.9过滤掉出现在90%以上文档中的词这些词没有区分度sublinear_tfTrue用对数缩放词频避免某个词重复出现太多次导致权重爆炸。3.2 类别特征编码目标编码的坑与技巧出版社、作者这类高基数类别特征直接用One-Hot会导致维度爆炸。目标编码是常用替代方案用该类别的目标均值来替换类别值。但直接替换会引入标签泄露必须配合交叉验证。from sklearn.model_selection import KFold def target_encode(train_df, test_df, col, target, n_splits5): kf KFold(n_splitsn_splits, shuffleTrue, random_state42) 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, inplaceTrue) 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, labely_train) dval lgb.Dataset(X_val, labely_val, referencedtrain) model lgb.train(params, dtrain, num_boost_round1000, valid_sets[dval], callbacks[lgb.early_stopping(50)])4.3 调参的优先级LightGBM参数很多但真正影响大的就那么几个。我的调参顺序是num_leaves控制模型复杂度从31开始试逐步增加到63、127learning_rate和n_estimators两者要配合学习率低就需要更多轮次min_data_in_leaf防止过拟合的关键类别不平衡时适当调大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-IDFGBDT好一截。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_idx0) 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, dim1) 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_labelsnum_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.957.2 我的推荐路径如果你刚开始做图书分类我建议按这个顺序走先用TF-IDFLightGBM跑一版基线把特征工程和评估流程跑通然后上TextCNN感受一下端到端训练的效果最后如果算力允许用预训练模型微调冲一波高分。每一步都要保留可复现的实验记录方便对比。7.3 后续扩展方向图书分类做完之后可以往几个方向延伸一是做多标签分类因为一本书可能同时属于“科幻”和“悬疑”二是做层次分类先分大类再分小类三是引入封面图像特征用多模态方案进一步提升效果。这些扩展都能在现有代码基础上迭代不用推倒重来。最后分享一个小技巧不管用哪种方案都先把验证集固定下来所有实验都在同一个验证集上对比。我见过太多人每次调参都重新划分数据结果实验之间根本没法比白白浪费大量时间。