用Python实现中文书名短文本标签分类:从N-gram到FastText

发布时间:2026/10/3 3:03:17
用Python实现中文书名短文本标签分类:从N-gram到FastText
简介面向Python自然语言处理初学者及课程设计开发者这套资源提供了一种通过作品名称预测小说类型标签分类的完整实现方案。项目基于FastText文本分类模型配合爬虫采集小说网站收藏榜数据作为训练语料使用jieba分词提升准确率可帮助读者快速掌握从数据采集、预处理到模型训练与评估的典型流程。压缩包共15个文件大小7.21MB主要包含txt格式的原始语料与模型文件、py格式的爬虫与训练脚本、docx格式的设计报告以及md说明文档结构清晰便于按模块学习。目前已有319人学习下载。资源附带完整设计报告Word和项目源码涵盖数据集收集、FastText训练、分词优化等关键环节既可用作课程设计参考也能为后续文本分类任务提供可直接改造的代码基础。1. 用书名判断小说类型一个短文本标签分类问题值得认真做给一千个书名判断小说类型人工能猜个七七八八但把它写成规则就会翻车以“XX之XX”命名的既可能是玄幻也可能是古言《我在异界开网吧》这种书名更是让关键字规则直接失灵。这个需求本质上是一个短文本标签分类问题——输入小说作品名字输出玄幻、都市、悬疑、科幻等类型标签用于书库打标、推荐系统冷启动和编辑审稿辅助。用 Python 做这件事不需要上重模型关键是数据口径、短文本特征和置信度兜底这三件事下文按这个顺序展开。适合的读者是刚接触标签分类的入门者以及被书名打标方案选型卡住的一线开发。2. 攒训练数据三种来源、一套清洗脚本标签分类的第一道坎不是模型而是没有标签。书名分类不像通用情感分析有现成数据集网文平台的公开榜单就是最容易被忽略的免费资源。这三条路可以并行公开榜单抓取、种子书单人工标注、多源合并清洗。数据量不需要很大几千本带标签的书名已经能训练出一个可用的分类器重点在标签口径统一。2.1 公开榜单抓取把平台分类直接当训练标签起点、晋江、番茄这些平台的公开榜单页面会同时给出书名和分类抓取思路完全一样按分类页拉书名把分类当标签存下来。要注意抓取频率和对方页面的结构示例里用一个示意 JSON 接口实际接入时按目标站点的真实返回改解析部分。import requests import time HEADERS {User-Agent: Mozilla/5.0 (compatible; book-corpus/0.1)} def fetch_books_by_label(url_tpl, label, pages5, delay1.5): url_tpl 是带 page 占位符的分类页模板label 是要存储的标签。 books [] for page in range(1, pages 1): try: resp requests.get(url_tpl.format(pagepage), headersHEADERS, timeout10) except requests.RequestException as exc: print(f[warn] page {page} error: {exc}) break if resp.status_code ! 200: print(f[warn] page {page} status{resp.status_code}) break # 以 JSON 接口为例实际页面可能是 HTML需要按结构解析 for item in resp.json().get(books, []): books.append({title: item[title], label: label}) time.sleep(delay) # 对榜单页保持节奏别集中打 return books samples fetch_books_by_label( https://example.com/rank?catxuanhuanpage{}, label玄幻, pages3, delay1.5, )代码逻辑很简单把“按标签拉书名”封装成函数错误处理是必要的榜单页随时可能改结构或暂时不可达。delay 参数控制请求间隔小批量抓取时 1 到 2 秒足够再慢一点损失也不大。抓回来的数据统一成(title, label)的结构后续清洗、合并都围绕这个结构做。抓取前先看对方有没有提供公开导出或开放接口优先用官方渠道而不是硬抓页面。2.2 种子书单人工标注小样本起步的最快路径人工标注不丢人。抓来的榜单分类是平台口径平台分的“玄幻”可能还混着“奇幻”你需要一份干净的小种子集做验证集。人工标 500 个书名的成本大约半小时到一小时换来一份可信的测试集这个投入一定值得。# seed_labels.csv # title,label # 斗破苍穹,玄幻 # 庆余年,架空历史 # 白夜行,悬疑 # 三体,科幻import pandas as pd seed pd.read_csv(seed_labels.csv) print(seed[label].value_counts())先value_counts()看一眼每个类别的样本量少于 20 条的类别建议先合并到上级类。种子集不要贪多保证准确即可标注时注意时间跨度尽量混入老书和新书不然模型会学到“某个时期的命名习惯”而不是“这类书的命名习惯”。这份种子集还有个用途——作为多源数据冲突时的仲裁标准比任何平台口径都干净。2.3 清洗与合并标签映射、去重与文件组织书名里混着“全本”“番外”“第一部”这类噪声标签又有“东方玄幻”“玄幻魔法”这种同义差异。写一套清洗脚本统一口径这一步直接决定模型上限。import re import unicodedata def normalize_label(label: str) - str: 把平台标签映射到统一口径是标签分类的命门。 mapping { 东方玄幻: 玄幻, 玄幻魔法: 玄幻, 都市生活: 都市, 现代都市: 都市, 科幻末世: 科幻, 未来世界: 科幻, } return mapping.get(label.strip(), label.strip()) def clean_title(raw: str) - str: raw unicodedata.normalize(NFKC, raw) # 全角转半角 raw re.sub(r.*?|\(.*?\), , raw) # 去掉括号说明 raw re.sub(r[【】「」\[\]], , raw) # 去掉装饰性括号 raw re.sub(r(番外|外传|第[一二三四五六七八九十百0-9][卷章篇部]).*, , raw) return raw.strip()逻辑说明normalize_label做标签层面的合并比在特征层面解决同义标签问题便宜得多clean_title的顺序有讲究先 NFKC 全角转半角再删括号最后删“番外”后缀顺序反了容易误伤“第 X 卷”在书名中部的合法用法。清洗完之后抽 20 条人工检查确认没把“凡人修仙传之仙界篇”这种系列作切坏。多源合并时同一本书可能出现在两个平台标签还互相矛盾。按规范化后的书名去重冲突时以种子集的人工标签为准没有种子标签的多数来源投哪个就取哪个。去重逻辑单独输出一份dedup_log.csv方便回溯哪些样本被合并了。文件组织一般用data/raw/存抓取原始结果data/clean/存清洗后的 CSVlabel_map.json存标签映射表模型产物放model/。组织清楚了后面迭代时才不用重新爬数据。3. 特征工程书名太短分词反而帮倒忙给长文本做分类分词加词频是标准动作但换到书名上分词器基本派不上用场。书名通常只有 2 到 15 个字分词器对“斗破苍穹”“大王饶命”“赘婿”这类专名和自造词基本无能为力一句话没有就没有上下文语境。这个阶段的核心决定是放弃词级特征改用字符级特征。3.1 为什么不在书名上分词缺少语境词表形同虚设拿一段十万字的小说明显做分类分词后每个词都有稳定的统计量词频能支撑模型训练。但书名只有十个字左右时词的统计量稀疏到无法训练而且分词结果大多是不可解释的碎片一部“斗破苍穹”被切成“斗破”“苍穹”两个字组本身不具备类型区分度。字符 n-gram 不一样它把“斗破苍穹”切出“斗破”“破苍”“苍穹”三组二元片段“苍穹”这种字组在玄幻里出现频率远高于都市文模型能慢慢学会这类模式。还有一个隐藏优势是泛化能力。新书用了完全没见过的自造词分词器会把它切成单字丢失语义而字符 n-gram 仍然能和训练样本共享一部分片段。比如“斗破”没出现过但“破苍穹”里的“苍穹”出现过特征就有了。短文本分类里字符级方案比词级稳定得多这是经验里最值得先确认的结论。3.2 字符 n-gram TF-IDF短文本分类的默认组合选用TfidfVectorizer直接做字符 n-gram 特征化参数设定对最终效果影响很大下面这组参数是短文本场景下的常见起点。from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( analyzerchar_wb, ngram_range(2, 4), min_df2, max_df0.9, sublinear_tfTrue, lowercaseTrue, ) X vectorizer.fit_transform(cleaned_title_list) print(X.shape) # (样本数, 特征数)特征全是字符 n-gram参数说明analyzerchar_wb在词边界内滑动字符窗口避免跨词拼出不存在的字组。中文书名没有空格分词效果和char接近但输出更干净。ngram_range(2, 4)2-gram 能捕捉“重生”“系统”“赘婿”3-gram 能捕捉“重生之”“赘婿的”4-gram 捕捉一些整体结构。再往上在短文本里重复率太低基本上属于噪声。min_df2只出现一次的字组过滤掉能明显降维并防过拟合。max_df0.9几乎所有书名都含的通用字组比如“之”在 TF-IDF 里权重已经很低这里再兜底一次。sublinear_tfTrue用 1log(tf) 压低高频字的绝对优势对短文本尤其重要。fit_transform后打印X.shape是个好习惯。书名样本一般在几千到几万条特征数常常到几万维属于合理范围不需要额外降维。3.3 加入长度、标点与关键词后缀等手工特征光靠 n-gram 会丢失一些全局信号比如书名长度、“之”字结构、是否含数字、是否以“记/传/录”结尾。这些信号对人工判断很直觉模型却容易被 n-gram 稀释掉显式拼进去更直接。import numpy as np import re from scipy.sparse import hstack def handmade_features(titles): n len(titles) feats np.zeros((n, 5)) for i, t in enumerate(titles): feats[i, 0] len(t) # 书名长度 feats[i, 1] 1 if 之 in t else 0 # “之”字结构常见于古言、玄幻 feats[i, 2] 1 if re.search(r\d, t) else 0 # 数字常见于悬疑、末世 feats[i, 3] 1 if any(k in t for k in [重生, 系统, 开局]) else 0 feats[i, 4] 1 if any(t.endswith(e) for e in [记, 传, 录, 志]) else 0 return feats H handmade_features(cleaned_title_list) X_full hstack([X, H]) print(X_full.shape)逻辑说明手工特征不是必须的但加上之后在少量样本上通常能提升 1 到 3 个点的 macro-F1。hstack要求两侧样本数一致、顺序对齐数据清洗后不要重新打乱再拼。拼完的特征矩阵直接存成.npz下次训练直接加载避免每次重复计算。特征做到这步调整空间已经不大下一步把精力放到模型选型上。4. 建模与调参逻辑回归打底FastText收尾书名标签分类的特征维度不高、样本量不大模型选型的核心不是追求多复杂的结构而是“在小数据上稳定、可解释、好部署”。实际中我最常用的是两套方案sklearn 的逻辑回归作为基线FastText 作为最终生产模型。4.1 多分类逻辑回归小数据上的可靠基线逻辑回归在小样本、高维稀疏特征上表现稳定而且训练速度快参数解释直观。from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report X_train, X_test, y_train, y_test train_test_split( X_full, y, test_size0.2, stratifyy, random_state42 ) clf LogisticRegression( C1.0, solverliblinear, max_iter2000, class_weightbalanced, random_state42, ) clf.fit(X_train, y_train) y_pred clf.predict(X_test) print(classification_report(y_test, y_pred, zero_division0))参数说明solverliblinear对几千样本几万特征在几秒内跑完class_weightbalanced用类别频率的倒数给样本加权是处理类别不平衡最省事的一招C 先用 1.0等看过 F1 再往 0.1 到 10 的方向搜。classification_report要逐类看 precision 和 recall书名分类通常会出现某类 precision 高但 recall 低的组合原因不是模型坏而是这个类的命名特征太窄具体要看混淆矩阵。4.2 FastText自带的子词就是书名特征FastText 在短文本分类上是性价比很高的选择它把字符子词嵌入学进向量里相当于把上一章的字符 n-gram 思路内置到模型训练中而且对未登录词更加友好。import fasttext # 训练数据格式__label__玄幻 斗破苍穹 with open(train_fasttext.txt, w, encodingutf-8) as f: for title, label in zip(train_titles, y_train): f.write(f__label__{label} {title}\n) model fasttext.train_supervised( inputtrain_fasttext.txt, lr0.5, epoch25, dim100, wordNgrams2, minn2, maxn4, losssoftmax, labelPrefix__label__, thread4, )参数说明lr0.5是默认值可以直接用epoch 在短文本任务 20 到 30 轮通常收敛按验证集 F1 早停即可。wordNgrams2让模型把相邻词作为特征等价于 2-gramminn和maxn控制子词范围读入 2 到 4 个字符的片段和 sklearn 那边的char_wb思路一致。losssoftmax适合标签数几十个的情况标签上百个再考虑hs层次 softmax。训练后model.test(valid.txt)会输出样本数、P1、R1对单标签分类来说这两个值相等。两个方案的差异如下对比项sklearn 逻辑回归FastText特征来源Tfidf 字符 n-gram子词嵌入 词 n-gram训练速度秒级到分钟级秒级模型体积稀疏矩阵几 MB10 MB 左右解释性系数可查、可调试语义向量解释弱适合场景调特征、看误判原因批量打标、部署简单4.3 评估别只盯准确率macro-F1、混淆矩阵与置信度分布标签分类最常见的评估误区是只看整体准确率。类型分布天然不均衡“都市”类样本多模型只要全猜都市就能拿到不错的准确率但玄幻、悬疑的召回会很难看。所以要看 macro-F1也就是每个类的 F1 取平均类别不平衡下不会虚高。from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt cm confusion_matrix(y_test, y_pred, labelsclf.classes_) disp ConfusionMatrixDisplay(cm, display_labelsclf.classes_) disp.plot(xticks_rotation45) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)看混淆矩阵比看总分重要。玄幻和仙侠的混淆、悬疑和科幻的混淆是用户最容易挑刺的地方。如果错误大量集中在某一对类考虑合并标签。还要看置信度分布从predict_proba里取出每个预测的最大概率画直方图低置信度样本比例高说明特征不足这时候的优先级是补数据、调特征最后才是换模型。换更大的模型在书名分类上收益有限因为输入的信息量就这么多。FastText 和 sklearn 的对比要公平固定同一个训练/验证划分两份模型分别测试直接对比 macro-F1。这一点很容易被忽视换特征、换模型时划分一变对比结果就失真了。5. 避坑清单从 zip 解压到上线的六个翻车点与对策这章是实际落地过程中的血泪经验。问题分布在全链路数据包解压、标签口径、类别边界、数据增强、类别不平衡、模型选型。每一条都按现象、原因、解决三步写。5.1 zip 解压后中文乱码训练脚本读不到文件现象Windows 上把项目 zip 包解压后data/raw下中文文件名的 CSV 变成乱码或者打开 CSV 后中文标题全部变成“锟斤拷”。更隐蔽的情况是包被打了伪加密标记普通解压工具直接弹密码框。原因打包时用了 GBK 编码存文件名Python 的 zipfile 模块默认按 cp437 解码文件名遇到中文就错乱内容本身如果是 GBK 写盘的旧 CSV用 UTF-8 读取也会乱码。伪加密则是 zip 的加密标志位被置位但实际没有加密工具会误判。解决解压时用 zipfile 手动逐条解出按编码恢复文件名。import zipfile with zipfile.ZipFile(book-type-classifier.zip) as zf: for info in zf.infolist(): # 文件名可能是 gbk 编码存进 zip 的先尝试解码修复 try: name info.filename.encode(cp437).decode(gbk) except (UnicodeDecodeError, UnicodeEncodeError): name info.filename zf.extract(info, unpacked) # 若 name 与实际路径不一致再按需重命名更省事的做法是用 7-Zip 打开自动识别编码解压一次完事但流程要脚本化时上面的处理躲不掉。CSV 读取用encodingutf-8-sig或encodinggbk都试一遍看哪种不乱码没有绝对规律取决于打包人的环境。5.2 多源标签口径不一致模型学出“来源特征”现象从两个平台抓数据训练出的模型对某一类特别准换到另一个平台的新书上一测掉点严重。原因平台 A 把“玄幻魔法”叫玄幻平台 B 可能把它叫奇幻数据的来源差异被模型学成了隐藏特征模型其实在记来源而不是记类型。解决进入训练前强制统一标签映射表。把合并后的数据按来源、标签列个透视表肉眼排查同一批书名在两个来源下的标签差异冲突样本以种子集为准。训练完再做一次按来源划分的评估如果某个来源的准确率明显偏低回查映射表。标签映射这种基础文件用 JSON 单独维护不要散落在脚本里。5.3 类型边界模糊玄幻与仙侠、悬疑与科幻互相混淆现象混淆矩阵里玄幻和仙侠互相错认悬疑里混入科幻编辑人工看也觉得“这本书两类都算”。原因类型边界本身是主观的跨类作品真实存在。这时候不是模型坏了而是标签体系设得太细。解决两个方向选一个。要么合并近似类为“幻想”“悬疑科幻”等上位类要么接受这种不确定性把 top3 预测展示给人工而不是只给一个结果。标签分类的交付物不是唯一正确答案而是一组候选加置信度这比硬要模型给一个高置信度的“正确”标签更务实。5.4 数据增强做过头生成一堆不像书名的书名现象为了凑样本对“爽文”类做同义词替换增强结果生成“赘婿从退货开始逆袭”这类平台根本不会上架的书名模型在真实新书上反而掉点。原因书名的“像不像书名”本身就是模型要学的分布随机替换破坏了原始命名分布模型被带偏。解决书名增强只做低风险操作比如后缀“记/传/录”互换、在已有种子书名的前后缀里做拼接增强样本占总样本比例控制在 20% 以内每 50 条人工抽检一次。短文本增强宁缺毋滥这是经验里最深刻的一条。5.5 小样本类别被大类别吞掉class_weight 救不回来现象“体育”“游戏”类只有几十个样本开class_weightbalanced后 recall 涨了precision 却跌到看不懂或者模型干脆把所有书都判成“都市”。原因类别不平衡时加权只是改变了决策阈值特征本身不够时小类别会被噪声主导。几十个样本很难撑起一个类别的分布。解决最务实的做法是给样本量低于下限的类别合并或标记为“其他”让主类别先做准等以后数据攒够了再拆出来训练。小类别不是目标输出时坚决不硬训。5.6 全量调用大模型打标效果好但账单和延迟不划算现象有人会绕过训练直接把书名丢给大模型 API回传一个标签首日效果惊艳到了百万级书库时发现 API 费用和限流撑不住。原因标签分类是高频低价值动作单次判断不值得单次 API 调用。大模型真正的价值在处理边界歧义而不是批量重复劳动。解决批量用本地小模型跑只有当置信度低于阈值时才丢给大模型或人工做二次判断这样既能用上大模型的语义能力又把调用量压到总量的 10% 到 20%。这个思路在下一章展开。6. 落地技巧置信度门槛配合 top3 候选让编辑做兜底训练完模型只是第一步如何接入业务才是决定这个方案值不值得投入的关键。我现在的做法是永远不返回单一标签而是返回 top3 候选和各自的置信度置信度低于阈值时直接标记为“待人工确认”。import numpy as np TOPK 3 CONF_THRESHOLD 0.6 def predict_topk(model, vectorizer, title, classes, topkTOPK): vec vectorizer.transform([title]) proba model.predict_proba(vec)[0] top_idx np.argsort(proba)[::-1][:topk] return [(classes[i], round(float(proba[i]), 3)) for i in top_idx] def tag_book(model, vectorizer, title): candidates predict_topk(model, vectorizer, title) if candidates[0][1] CONF_THRESHOLD: return {decision: needs_review, candidates: candidates} return {decision: candidates[0][0], candidates: candidates}这段代码把单标签模型的输出改造成结构化结果方便接进后台的待审核列表。阈值设 0.6 是经验值数据噪声大时调到 0.7 更稳代价是“待人工确认”的比例会上升需要结合编辑部的人力成本来衡量。输出直接落成 JSON入库、审核、展示都能复用。每次训练完都固定划分和阈值把混淆矩阵图和 top3 抽检结果一起归档下次调特征后直接对比。书名标签分类做得再漂亮最终检验标准不是测试集多个零点几个点而是随机抽 50 本新书人工盲测能猜对多少。这个习惯帮我发现过不少次“测试集虚高、新书掉点”的问题根源基本都是数据口径漂移。如果只能从这篇笔记带走一个技巧就是置信度门槛加 top3 候选。它能同时解决类型边界模糊和人工审核成本两个问题也是这个标签分类方案能真正用起来的最后一环。希望帮到你。本文还有配套的精品资源点击获取