短文本分类实战:用Python和FastText从书名判断小说类型
简介面向Python文本分类初学者与课程设计人群这套资源以小说作品名称为输入借助FastText模型自动判断其类型标签有效减少人工打标成本。项目先爬取起点、晋江等小说网站收藏榜的高人气书名结合多源数据构建训练集再通过jieba分词优化特征整个流程从数据获取到模型训练均可复现。压缩包共15个文件约7.21MB主要包括Python源码数据采集、分词测试、训练、设计报告Word、多份小说名称文本以及训练好的模型文件并配有说明文档结构清晰便于对照学习。已有319人学习适合作为自然语言处理入门实践或毕业设计参考也可用于快速验证文本分类思路。通过阅读报告与运行代码可掌握爬虫清洗、FastText建模、分词调优等关键技巧获得一套可直接改用的文本分类基线方案。1. 文本分类实战用 Python 通过作品名字判断小说类型这个课设包值得拆开看“通过作品名字判断小说类型”——这句话第一眼看上去像文字游戏但放到标签分类场景里就是一个非常标准的短文本分类任务。这个资源是一套已经跑通的课程设计工程爬虫脚本采集晋江、起点、创世、笔趣阁四个站点的收藏榜数据把“书名类型标签”整理成训练语料先用 jieba 分词再丢给 FastText 训练产出novel_names.model最后加载模型就能对任意新书名做预测。它把数据采集、中文分词、文本分类训练和设计报告串成了一条完整链路。对 Python 刚入门、想找一个能讲清原理的课设选题的人或者正忙着给机器学习和 NLP 课程交作业的人这个包有直接参考价值对只想快速上手 FastText 的人里面的数据和训练脚本也能省掉大量收集语料的时间。2. 先理解数据与选型为什么是收藏榜、FastText 和 jieba 的组合拿到压缩包后我建议先别急着双击train.py。大多数课设包的问题不在代码跑不起来而在你对数据一无所知就开跑最后翻车了都不知道去查哪。这一章把资源里的数据文件、FastText 选型理由和 jieba 扮演的角色讲清楚后面复现才不会像拆盲盒。2.1 收藏榜就是免费的标注数据数据目录里每个文件是干什么的FastText 监督学习需要一个带标签的文本来训练。这个项目用爬虫去采集小说网站的收藏榜背后的逻辑很直接收藏榜代表真实读者用鼠标投票的结果能被大量收藏的书标签通常是清晰的。随机抓取全站书籍反而容易拿到标签混乱的样本。data目录下的文件各有分工我拆包后第一件事就是把它们对应到训练链路里文件来源在训练里的角色常见注意点jinjiang_source.txt晋江文学城收藏榜女频作品主力言情/纯爱/古言等标签原始页面可能用 GBK 编码qidian_names.txt起点中文网收藏榜男频作品名玄幻/都市/仙侠主要来源可能混入连载状态等额外字段chuangshi_names.txt创世中文网收藏榜男频补充来源和起点存在书名重合需要去重biquge_names.txt笔趣阁收藏榜覆盖面广但格式最乱行格式不统一清洗要最小心novel_names.txt多个来源汇总后的数据清洗后的统一训练输入先确认分隔符是空格还是 Tabnovel_names.model训练产物加载后直接预测新书名对 fasttext 版本有兼容要求douluo_chapters.txt示例文本用来测试“非标准书名”预测效果内容是章节名分词规则和书名不同我见过的课设里最常用的样本格式是把“书名”和“标签”用空格或 Tab 分开一行一个样本斗破苍穹 玄幻 凡人修仙传 仙侠 魔道祖师 纯爱dataset_collections.py就是负责把原始抓取整理成这种格式的脚本。拿到源码后建议先读它的清洗逻辑看它是按“第一个空格前面是书名后面是标签”切分还是按 Tab 切列这直接决定你后续写的处理脚本能不能对齐它的输出。2.2 FastText 的分类原理短文本靠 n-gram 弥补信息量在选模型这件事上这个项目选 FastText 是合理的。书名只有两到八个字属于典型的超短文本不需要像 BERT 那样用深层 Transformer 去建模长距离依赖。FastText 的核心做法是把一句话里的词向量做平均同时引入子词 n-gram 信息。对评论、新闻这类长文本来说这种平均做法会丢掉词序但书名本身就那么几个词词序丢失的影响被大幅削弱而 n-gram 又能捕捉“全职高手”“都市修仙”这种共现组合正好打在短文本分类的痛点上。另一个现实原因是训练成本。几万条样本、十几个标签在普通笔记本上训练只需要几十秒CPU 就能跑完。课程设计的环境往往没有 GPUBERT 类模型在这种场景下是杀鸡用牛刀预测延迟还高。FastText 的监督训练格式也很直白标签用__label__前缀标记__label__玄幻 斗破 苍穹 __label__仙侠 凡人 修仙 传train.py做的绝大部分工作就是把上一节那种“书名 标签”的原始数据改造成上面这种格式。2.3 jieba 在这里不是锦上添花是收益来源FastText 本身不认中文分词它只按空格切 token。中文书名没有天然空格所以必须先用 jieba 把“斗破苍穹”切成“斗破 苍穹”模型才知道“斗破”和“苍穹”是两个特征。这个步骤如果省掉整个句子变成一个词泛化能力等于零。可以用资源里的jieba_test.py先验证分词效果import jieba titles [斗破苍穹, 凡人修仙传, 重生之都市修仙, 天官赐福] for title in titles: print(title, →, /.join(jieba.cut(title)))实际输出依赖 jieba 词典版本大致是下面这种形态斗破苍穹 → 斗破苍穹 凡人修仙传 → 凡人/修仙/传 重生之都市修仙 → 重生/之/都市/修仙 天官赐福 → 天官赐福这里有个很实际的经验专有名词最好加到用户词典里防止“斗罗大陆”被拆成“斗罗/大陆”之后模型把两个毫不相关的特征硬拼起来。正常做法是维护一个小说名词表一行一个词jieba.load_userdict(data/novel_terms.txt)分词结果直接影响后续 FastText 的特征质量这不是玄学是文本分类任务里最基础的收益来源。3. 复现训练流程从原始文件到 novel_names.model 的完整命令理解数据之后就可以动手复现了。这一章按实际执行顺序走一遍装环境、跑预处理、训练模型、做第一次预测。3.1 环境准备版本选对Windows 下能少折腾两小时建议直接用 Python 3.8 到 3.10 之间的版本。太新的 Python 在 Windows 上编译 fasttext 的 C 扩展经常报错。用虚拟环境隔离依赖是基本操作python -m venv novel-cls # Linux/Mac 激活 source novel-cls/bin/activate # Windows 激活 novel-cls\Scripts\activate pip install fasttext0.9.2 jieba requests flask提示Windows 用户如果pip install fasttext编译失败优先找对应 Python 版本的预编译 wheel或者降到 3.8/3.9 再装。硬刚编译工具链会浪费大量时间。安装完验证一下python -c import fasttext, jieba; print(env ok)3.2 train.py 做了什么数据清洗、分词、切分验证集、训练这个课程设计的train.py核心逻辑可以归纳成下面这个骨架。我在实际拆包时会把不同来源文件先并到一起再做去重和标签分布统计最后才切验证集顺序不能乱。import random import jieba import fasttext from collections import Counter raw_files [ data/jinjiang_source.txt, data/qidian_names.txt, data/chuangshi_names.txt, data/biquge_names.txt, ] def read_pairs(path): pairs [] with open(path, r, encodingutf-8, errorsignore) as f: for line in f: line line.strip() if not line: continue # 兼容 Tab 和空格两种分隔方式 parts line.split(\t) if \t in line else line.split( , 1) if len(parts) 2: continue pairs.append((parts[0].strip(), parts[1].strip())) return pairs pairs [] for p in raw_files: pairs.extend(read_pairs(p)) # 同一本书可能在不同网站出现先去重再划分 pairs list(dict.fromkeys(pairs)) # 查看标签分布这一步能提前发现类别失衡 print(Counter(label for _, label in pairs).most_common()) random.seed(42) random.shuffle(pairs) split int(len(pairs) * 0.8) train_pairs, valid_pairs pairs[:split], pairs[split:] def write_fasttext(path, pairs): with open(path, w, encodingutf-8) as f: for title, label in pairs: words .join(w for w in jieba.cut(title) if w.strip()) f.write(f__label__{label} {words}\n) write_fasttext(fasttext_train.txt, train_pairs) write_fasttext(fasttext_valid.txt, valid_pairs) model fasttext.train_supervised( inputfasttext_train.txt, lr0.6, dim100, epoch20, wordNgrams2, minCount1, ) # 返回 (样本数, P1, R1)单标签多分类下两者基本一致 print(验证集指标:, model.test(fasttext_valid.txt)) model.save_model(novel_names.model)几个关键点单独说明一下line.split( , 1)只切第一刀防止书名或标签内部含空格时把样本拆碎。去重那一步必不可少同一本书名在起点和创世都出现不去重会让训练和验证集重叠评估分数虚高。shuffle必须紧跟随机种子保证课设可复现否则换一台机器跑结果就对不上报告里的数字。下面是训练参数的实际含义和调参方向这个表和设计报告里的“参数分析”章节直接相关参数默认值作用课设常见取值lr0.1学习率过大会震荡过小收敛慢0.5 ~ 1.0dim100词向量维度短文本不需要太大100epoch5遍历训练集的次数15 ~ 25wordNgrams1词 n-gram设 2 可捕获相邻词的组合关系2minCount5过滤低频词课设语料小要调低1 ~ 3我一般先用lr0.6, epoch20, wordNgrams2跑一版看验证集指标再微调。不要一上来就加大维度dim200在短文本上收益很小训练时间却明显变长。3.3 用训练好的模型做第一次预测训练完成后再写一个独立的预测脚本加载novel_names.model直接预测。注意预测时的分词方式必须和训练时保持一致。import fasttext import jieba model fasttext.load_model(novel_names.model) def predict(title, k3): words .join(w for w in jieba.cut(title) if w.strip()) labels, probs model.predict(words, kk) return [(l.replace(__label__, ), float(p)) for l, p in zip(labels, probs)] for title in [剑来, 斗罗大陆, 魔道祖师]: print(title, predict(title))k3的作用是返回概率最高的前三个标签方便你看模型自己的“犹豫程度”。比如一本偏冷门的书第一名和第二名概率都很接近说明模型也没把握这时候就不能硬取第一个结果。4. 复现踩坑避坑清单编码乱码、分词空串、类别失衡和模型版本逐个排掉这部分是我实际跑这类课耗时最常遇到的问题每一条都按“现象 → 原因 → 解决”整理建议对照排查。4.1 原始文件打开全是乱码训练完准确率只有个位数现象读jinjiang_source.txt或biquge_names.txt时书名字符全变成“鍝?鏂?澶у笀”之类的乱码训练出来的模型预测结果接近随机。原因国内不少小说站页面是 GBK 或 GB18030 编码爬虫直接按 UTF-8 存文本读出来必然乱码。解决先探测编码再决定用什么编码读取。import chardet with open(data/biquge_names.txt, rb) as f: raw f.read(10000) print(chardet.detect(raw)) # 如果检测结果接近 GB2312 / GBK用 gbk 重新读 with open(data/biquge_names.txt, r, encodinggbk, errorsignore) as f: lines f.readlines()统一转成 UTF-8 保存一份后面所有脚本都从这份清洗后的文件读取不要在训练脚本里混用编码。4.2 书名分词后变成空串预测时报“empty input”现象个别书名全是特殊字符或者纯表情符号jieba.cut之后没有任何可用词FastText 的predict报输入为空。原因网文站收藏榜里会混入一些非标准作品名比如“????”、“.’”这类噪音数据。jieba对它无能为力。解决分词后过滤空词同时设置兜底逻辑切不出词就退回原始书名def clean_words(title): words [w for w in jieba.cut(title) if w.strip()] if not words: return title # 兜底避免空输入 return .join(words)这个兜底看着简单但在线上批量预测时能避免一整批数据因为一两个脏样本直接崩溃。4.3 热门标签垄断稀有标签永远预测不出来现象训练完成后用验证集测试玄幻和都市准确率很高但灵异、科幻这类标签几乎一个都预测不出来。原因收藏榜本身热门题材占多数类别分布严重不均衡。FastText 在这种数据上学到的倾向就是“无脑输出大类”。解决一个可行的课设级方案是对少数类做重复采样。比较朴素的写法是手动控制重复次数from collections import Counter counts Counter(label for _, label in pairs) max_count max(counts.values()) resampled [] for title, label in pairs: resampled.append((title, label)) repeat min(max_count // counts[label] - 1, 3) for _ in range(repeat): resampled.append((title, label))把重复次数限制在 3 以内避免小众标签被过度重复后模型过拟合。更本质的解法是去对应标签集中的网站补爬数据比如晋江对纯爱和古言标签的覆盖量远比起点更适合做女频分类。4.4 fasttext 版本不一致模型加载后直接报错现象在一台机器上训练完novel_names.model换到另一台机器或更新 fasttext 版本后load_model直接抛异常或者加载成功但预测结果面目全非。原因fasttext 的模型文件格式与 C 核心版本强相关不同版本之间不能保证完全兼容。解决训练和加载环境统一固定版本。pip install fasttext0.9.2同时把版本号写进 README 或设计报告里避免课设答辩换机器演示时当场翻车。4.5 一个书名对应多个标签直接训练导致标签互相干扰现象部分原始数据里一本书有多个类型比如“惊悚/悬疑/冒险”如果整行直接塞给 FastText模型会把这个组合当成一个独立标签预测时反而拆不出来。原因FastText 的单标签训练格式里一行只能有一个__label__多标签需要特殊处理。解决训练前把多标签拆成多行或者只保留第一个主标签取决于你的评估目标。课设场景建议只保留主标签报告也更好交代清楚。5. 把模型接进自己的代码批量预测、阈值过滤和 HTTP 接口训练出模型只是第一步课设里真正让演示出效果的是“输入一个书名就能看到分类结果”的闭环。这一章把模型包装成可复用的调用方式从批量文件到服务接口都覆盖。5.1 批量预测一次性处理几千个书名实际使用时很少一个一个调predict一般是直接读一个文件批量预测后写回结果。这个脚本可以作为课设的“在线预测模块”直接交付。import fasttext import jieba model fasttext.load_model(novel_names.model) def predict_title(title, k1): words .join(w for w in jieba.cut(title) if w.strip()) if not words: words title labels, probs model.predict(words, kk) return labels[0].replace(__label__, ), probs[0] with open(titles.txt, r, encodingutf-8) as f, \ open(result.txt, w, encodingutf-8) as out: for line in f: title line.strip() if not title: continue label, score predict_title(title) out.write(f{title}\t{label}\t{score:.4f}\n)这里把score也写进结果文件是为了后面做质量控制。很多细节都在批次处理时才能暴露出来比如空行、特殊字符、首尾空格都需要在循环里处理干净。5.2 置信度阈值宁可说“不确定”也别硬切标签书名本身只有几个字模型经常出现“第一名 0.42、第二名 0.38”这种模糊状态。这时候强行给出一个标签反而会让演示变尴尬更专业的做法是设置一个阈值低于它就输出“不确定”。THRESHOLD 0.6 def classify(title): label, score predict_title(title, k1) if score THRESHOLD: return f{title}\t不确定\t{score:.4f} return f{title}\t{label}\t{score:.4f}阈值怎么定我的习惯是拿验证集跑一遍把不同阈值下的误判样本数打出来再选一个“宁可漏判也不误判”的临界点。课设报告里能放这样一张阈值对比表比单纯贴准确率更有说服力。5.3 封装成 HTTP 接口让别人也能调用这个分类器如果要让模型真正被别人调用最常见的方式是用 Flask 包一层接口。请求传一个书名返回预测标签和对应概率。from flask import Flask, request, jsonify import fasttext import jieba app Flask(__name__) model fasttext.load_model(novel_names.model) app.route(/predict, methods[POST]) def predict(): data request.get_json() title data.get(title, ) words .join(w for w in jieba.cut(title) if w.strip()) or title labels, probs model.predict(words, k3) predictions [ {label: l.replace(__label__, ), score: round(float(p), 4)} for l, p in zip(labels, probs) ] return jsonify({title: title, predictions: predictions}) if __name__ __main__: app.run(host0.0.0.0, port8000)启动后用 curl 测一下curl -X POST http://127.0.0.1:8000/predict \ -H Content-Type: application/json \ -d {title: 斗罗大陆}返回结果里按概率从高到低排列调用方可以根据自己的业务需求决定取第一名还是综合前几名。这个接口结构简单写进课程设计的功能模块部分完全够用。6. 进阶玩法换一套自己的标签体系重训一次只需五到八分钟如果你不想止步于复现想把模型嫁接到自己的场景比如给短文案打分类、给视频标题打标签这个资源里的训练链路完全可以复用只需要替换数据源和标签集。我自己试过一次把原来的四站小说数据换成某个素材站点的标题数据后训练时间仍然控制在十分钟以内调整成本非常低。做法分三步。第一步准备你自己的样本文件格式沿用“文本 标签”一行一个。第二步把上一章的清洗和训练脚本跑一遍最后把验证集指标打印出来做对比。重点是用model.test来评估而不是靠肉眼猜result model.test(fasttext_valid.txt) print(fsample count: {result[0]}) print(fP1: {result[1]:.4f}) print(fR1: {result[2]:.4f})第三步根据指标调整参数。验证集分数偏低时优先加epoch和wordNgrams不要动dim。Epoch 从 20 加到 30 通常有 1 到 2 个点的提升wordNgrams从 2 加到 3 对短文本也有细微帮助但训练时间会明显增加。如果加了之后分数不升反降就该回到数据清洗环节看标签是否有噪声而不是继续调参。操作时有一个细节值得留意评估用的fasttext_valid.txt必须和训练集来自同一次洗牌切分不能单独重新构造。很多课设报告上写的“准确率 92%”其实是拿训练集测出来的答辩时老师多问两句就露馅。正确做法是像前面代码演示的那样shuffle之后按 8:2 切分先写训练集再写验证集最后才训练。从那以后我每次拿到任何做文本分类的课设包都会强制自己先走一遍“数据分布检查 → 切分验证集 → 固定随机种子 → 再训练”的流程这个顺序救过我好几次至少能让分数经得起复查。希望帮到你。本文还有配套的精品资源点击获取