☰
短文本分类实战:用Python和FastText从书名判断小说类型
2026/10/7 8:55:31 网站建设 项目流程

简介:面向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多个来源汇总后的数据清洗后的统一训练输入先确认分隔符是空格还是 Tab
novel_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 就能跑完。课程设计的环境往往没有 GPU,BERT 类模型在这种场景下是杀鸡用牛刀,预测延迟还高。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 fasttext==0.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", encoding="utf-8", errors="ignore") 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", encoding="utf-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( input="fasttext_train.txt", lr=0.6, dim=100, epoch=20, wordNgrams=2, minCount=1, ) # 返回 (样本数, P@1, R@1),单标签多分类下两者基本一致 print("验证集指标:", model.test("fasttext_valid.txt")) model.save_model("novel_names.model")

几个关键点单独说明一下:

line.split(" ", 1)只切第一刀,防止书名或标签内部含空格时把样本拆碎。去重那一步必不可少,同一本书名在起点和创世都出现,不去重会让训练和验证集重叠,评估分数虚高。shuffle必须紧跟随机种子,保证课设可复现,否则换一台机器跑结果就对不上报告里的数字。

下面是训练参数的实际含义和调参方向,这个表和设计报告里的“参数分析”章节直接相关:

参数默认值作用课设常见取值
lr0.1学习率,过大会震荡,过小收敛慢0.5 ~ 1.0
dim100词向量维度,短文本不需要太大100
epoch5遍历训练集的次数15 ~ 25
wordNgrams1词 n-gram,设 2 可捕获相邻词的组合关系2
minCount5过滤低频词,课设语料小要调低1 ~ 3

我一般先用lr=0.6, epoch=20, wordNgrams=2跑一版,看验证集指标再微调。不要一上来就加大维度,dim=200在短文本上收益很小,训练时间却明显变长。

3.3 用训练好的模型做第一次预测

训练完成后再写一个独立的预测脚本,加载novel_names.model直接预测。注意预测时的分词方式必须和训练时保持一致。

import fasttext import jieba model = fasttext.load_model("novel_names.model") def predict(title, k=3): words = " ".join(w for w in jieba.cut(title) if w.strip()) labels, probs = model.predict(words, k=k) return [(l.replace("__label__", ""), float(p)) for l, p in zip(labels, probs)] for title in ["剑来", "斗罗大陆", "魔道祖师"]: print(title, predict(title))

k=3的作用是返回概率最高的前三个标签,方便你看模型自己的“犹豫程度”。比如一本偏冷门的书,第一名和第二名概率都很接近,说明模型也没把握,这时候就不能硬取第一个结果。

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", encoding="gbk", errors="ignore") 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 fasttext==0.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, k=1): words = " ".join(w for w in jieba.cut(title) if w.strip()) if not words: words = title labels, probs = model.predict(words, k=k) return labels[0].replace("__label__", ""), probs[0] with open("titles.txt", "r", encoding="utf-8") as f, \ open("result.txt", "w", encoding="utf-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, k=1) 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, k=3) 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(host="0.0.0.0", port=8000)

启动后用 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(f"sample count: {result[0]}") print(f"P@1: {result[1]:.4f}") print(f"R@1: {result[2]:.4f}")

第三步,根据指标调整参数。验证集分数偏低时,优先加epoch和wordNgrams,不要动dim。Epoch 从 20 加到 30 通常有 1 到 2 个点的提升,wordNgrams从 2 加到 3 对短文本也有细微帮助,但训练时间会明显增加。如果加了之后分数不升反降,就该回到数据清洗环节看标签是否有噪声,而不是继续调参。

操作时有一个细节值得留意:评估用的fasttext_valid.txt必须和训练集来自同一次洗牌切分,不能单独重新构造。很多课设报告上写的“准确率 92%”其实是拿训练集测出来的,答辩时老师多问两句就露馅。正确做法是像前面代码演示的那样,shuffle之后按 8:2 切分,先写训练集再写验证集,最后才训练。

从那以后,我每次拿到任何做文本分类的课设包,都会强制自己先走一遍“数据分布检查 → 切分验证集 → 固定随机种子 → 再训练”的流程,这个顺序救过我好几次,至少能让分数经得起复查。希望帮到你。

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

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

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

立即咨询