Python实现小说标签自动分类:fastText+jieba实战

📅 发布时间:2026/10/3 7:10:08
Python实现小说标签自动分类:fastText+jieba实战
简介面向课程设计与自然语言处理入门者资源演示如何仅凭小说作品名称自动判断其类型标签。实现方案使用爬虫抓取多个小说网站高人气收藏榜数据借助结巴分词提高文本特征质量再训练快速文本分类模型形成从数据采集到分类评估的完整链路并配有设计报告文档详细说明数据来源、处理流程与效果。压缩包共15个文件、约7.2兆含3个功能脚本、7个文本语料及名字数据、训练得到的模型文件、说明文档与许可证等结构清晰便于复盘和二次开发。已有319人学习下载适合需要完成文本分类课程设计或快速搭建标签系统的读者可参考其中数据清洗、分词优化、模型训练与推理等环节并迁移到其他文本标签场景也可直接用模型对新书名进行预测。1. 一套能按作品名预测小说标签的 Python 包先看清它解决了什么用作品名字判断小说类型很多外行觉得靠语感内行知道这是一道典型的短文本分类题。这个 zip 包把整条链路给你补齐了爬虫拿语料、jieba 分词、fastText 训练、模型保存、预测脚本外加一份可直接当课设报告用的设计报告文档。核心场景是给一批小说作品名自动打上“玄幻、言情、都市、游戏”这类标签替代人工编辑的初筛。适合正在做 Python 标签分类课程设计的同学也适合想快速上手 fastText 文本分类、又不想从零攒数据的开发者。下载后解压就能看到 data 目录里的训练语料和已经训好的模型文件先跑通再改参数比从零写一个分类器省事很多。2. 数据准备四个来源的榜单语料与 dataset_collections.py 合并逻辑2.1 榜单语料的来源与取舍为什么用收藏榜不用推荐榜原项目里爬的是小说网站的收藏榜而不是推荐榜或月票榜。这个选择很关键。收藏榜代表“已经有很多人看完并收藏”的作品标签体系相对稳定一本书是什么类型读者和平台基本达成了一致。推荐榜和月票榜受活动、连载进度影响大今天上榜明天就掉抓回来的标签不稳定训练出来的模型容易学到短期噪声。语料从四个来源收集晋江、起点、创世以及另一个公开页面的榜单数据分别落在 jinjiang_names.txt、qidian_names.txt、chuangshi_names.txt、biquge_names.txt 里。这四个平台的题材叫法不统一比如起点叫“都市”晋江可能叫“现代言情”同一类书在不同来源里的标签字段完全不同。如果直接拼在一起训练最终模型会学到一堆近义标签预测结果看起来东一下西一下。所以合并之前必须先归一化标签。2.2 语料文件长什么样原始源文件和清洗后的训练行data 目录里其实有两种文件。一种是原始抓取结果比如 jinjiang_source.txt里面可能还带着页面标签和噪声另一种是解析清洗后的“作品名 标签”行模型真正消费的是后者。把目录里的内容大致分一下文件内容格式jinjiang_source.txt抓回来的页面原文带 HTML 或 JSON 混杂噪声jinjiang_names.txt晋江源解析结果每行“作品名 标签”空格分隔qidian_names.txt起点源解析结果同上chuangshi_names.txt创世源解析结果同上biquge_names.txt另一个来源解析结果同上novel_names.txt四源合并去重后的全集每行“作品名 标签”novel_names.modelfastText 训练完成的模型二进制模型文件清洗后的训练行大概长这样斗罗大陆 玄幻 斗破苍穹 玄幻 全职高手 游戏 雪中悍刀行 武侠 微微一笑很倾城 言情第一列是作品名第二列是标签中间用空格分开。注意 fastText 本身不关心你用的是空格还是 Tab但只要定了分隔符号训练脚本和预测脚本必须保持一致。2.3 dataset_collections.py合并四个来源并做标签归一化这个脚本要做的不只是“把四个文件拼成一个”。它承担三件事过滤无效行、统一近义标签、对同名作品去重。一个典型的实现是这样import glob from collections import Counter ALIAS { 纯爱: 言情, 耽美: 言情, 都市生活: 都市, 东方玄幻: 玄幻, 异世大陆: 玄幻, } def normalize_label(label: str) - str: label label.strip() return ALIAS.get(label, label) def merge_sources(source_files): rows [] for path in source_files: with open(path, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) ! 2: continue name, label parts[0], parts[1] label normalize_label(label) if not name or not label: continue rows.append((name, label)) rows list(set(rows)) return rows if __name__ __main__: files [ data/jinjiang_names.txt, data/qidian_names.txt, data/chuangshi_names.txt, data/biquge_names.txt, ] merged merge_sources(files) with open(data/novel_names.txt, w, encodingutf-8) as out: for name, label in merged: out.write(f{name} {label}\n) print(样本数:, len(merged)) print(标签分布:, Counter(label for _, label in merged).most_common())逻辑上要说明三个点。split()默认按任意空白切分能顺手清掉换行符和多余空格。但如果作品名本身包含空格len(parts) ! 2的判断会导致误杀。稳妥的做法是把语料统一改成 Tab 分隔然后用split(\t)。标签归一化用字典映射把“纯爱”“耽美”统一到“言情”。映射表要保守只合并真正同一类的标签不要为了减少类别数量强行把“玄幻”和“科幻”合并那样模型会失去区分能力。set()去重可以去掉完全重复的“名字 标签”组合但如果同一个作品名在不同来源被标成了不同题材这条不会去重反而会保留两条矛盾样本。遇到这种情况建议在 merge 前按作品名聚合再取投票最多的标签。2.4 数据质量检查合并前先做字符和标签的体检训练前做一次统计能直接暴露语料里最致命的问题。我用得比较多的检查项是文件编码、空行、标签数量、每类样本数量。import io from collections import Counter def inspect_data(path): labels [] names [] with io.open(path, encodingutf-8-sig) as f: for line in f: line line.rstrip(\n) if not line: continue parts line.split(\t) if len(parts) ! 2: print(格式异常:, line[:50]) continue names.append(parts[0]) labels.append(parts[1]) print(样本数:, len(names)) print(不同标签数:, len(set(labels))) print(标签分布 Top10:, Counter(labels).most_common(10)) inspect_data(data/novel_names.txt)如果打开文件时不是用utf-8-sig恰好文件带 BOM 头第一行作品名里会混进\ufeff训练时被当成一个特殊字符虽然不一定报错但会污染这条样本。用io.open加utf-8-sig能直接规避。数据体检之后如果某个标签只有几十条样本而其他标签有几千条这个类基本是训不出来的。常见做法是先对大类下采样或者把小类合并到相近的大类里再进模型。3. 训练环节fastText、jieba 分词与 train.py 的参数取舍3.1 为什么选择 fastText超短文本需要子词信息作品名一般只有两到十个字比普通的短文本还短。传统 TF-IDF 加 SVM 的做法的确能区分一些关键词比如书名里出现“帝尊”“苍穹”大概率是玄幻但很多书名没有代表性关键词全凭字序和子词组合传达类型信息。fastText 的核心优势有两个。第一是子词 n-gram一个词被拆成字符片段参与训练预测时如果遇到训练集没见过的书名只要部分字符片段出现过模型就不会直接“哑火”。第二是训练速度几万个短文本在 CPU 上几十秒到一两分钟就能训完。对于课设答辩场景这是极大的优势评委要求现场换参数重跑你也能快速出结果。为什么不直接用原始字符训练因为 fastText 官方实现默认按空格切词中文不切词的话“斗罗大陆”会被当成一个整体 token子词 n-gram 会基于整句生成信息利用效率很低。所以原项目引入了 jieba 分词库先把书名切好再用空格拼起来相当于人先把语言单元拆好把知识以词典的形式喂给了模型。3.2 train.py 逐段拆解从读数据到保存模型zip 里已经带了训好的 novel_names.model即使不重训也能直接跑预测。但如果你想改标签体系、加数据或者想在答辩时演示训练过程train.py 是核心入口。import fasttext import jieba import random from collections import Counter def load_corpus(path): X, y [], [] with open(path, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 2: continue name .join(parts[:-1]) label parts[-1] X.append(name) y.append(__label__ label) return X, y def tokenize_with_jieba(name): return .join(jieba.cut(name)) X, y load_corpus(data/novel_names.txt) data list(zip(X, y)) random.seed(42) random.shuffle(data) split int(len(data) * 0.9) train_data data[:split] valid_data data[split:] def write_temp(path, data): with open(path, w, encodingutf-8) as f: for name, label in data: f.write(f{tokenize_with_jieba(name)} {label}\n) write_temp(data/train.txt, train_data) write_temp(data/valid.txt, valid_data) model fasttext.train_supervised( inputdata/train.txt, lr1.0, dim100, epoch25, wordNgrams2, minCount1, losssoftmax, ) model.save_model(data/novel_names.model)这里有几个容易被忽略的细节。load_corpus里如果作品名本身带空格直接parts[0]会丢失名字后半部分。所以我用parts[:-1]重新拼回名字把parts[-1]当标签。如果语料统一改成 Tab 分隔这段能简化成name, label line.split(\t)。fastText 要求标签带__label__前缀且一行可以有多个标签。当前语料每行一个标签所以只加了一个前缀。分词在训练和预测时都必须做而且要用同一套词典、同一种模式。训练时用jieba.cut预测时也用jieba.lcut两者只是返回生成器还是列表的区别组合出的字符串保持一致。参数怎么调我一般记住几个原则lr1.0是默认值数据量不大时收敛快。如果数据集再翻一倍可以降到 0.5防止在训练集上过拟合。dim100是向量维度短文本任务用 50 到 100 就够了加到 300 不会有显著收益反而增加模型体积。epoch25对几万条数据稍微偏大10 到 20 更稳妥。如果发现验证集准确率在最后一个 epoch 反而下降就是训过头了。wordNgrams2会在相邻词之间生成组合特征对“斗罗 大陆”这种顺序敏感的书名有价值。minCount1表示低频词不丢弃。短文本任务里很多生僻词只在少数书里出现丢了它们等于丢线索。losssoftmax是单标签多分类标准配置。如果一本书允许同时属于多个题材换lossova。3.3 jieba 的词典策略通用词典、自定义词典和动态加词只调 fastText 参数是不够的分词质量直接决定训练数据长什么样。jieba 默认词典覆盖的是日常词汇对小说专有名词覆盖很差。“斗罗大陆”这四个字如果不加干预会被切成“斗罗”和“大陆”两个词虽然不影响训练但丢失了“斗罗”这个强特征。常见的做法是在项目里放一个 user_dict.txt每行一个词斗罗大陆 斗破苍穹 诛仙 凡人修仙传脚本开头加载import jieba jieba.load_userdict(user_dict.txt)加载后jieba.cut(斗罗大陆)会输出完整的“斗罗大陆”。除了文件词典还有一种动态调词方式jieba.suggest_freq(斗罗大陆, True)True表示提高该词被合并的概率。动态加词适合临时测试正式训练还是建议用load_userdict因为可以保证训练和预测加载同一份词典。我踩过的坑是训练脚本里加载了 user_dict.txt本地预测脚本却忘了加载结果同一本书训练和预测的特征空间对不上准确率直接从 85% 掉到 60% 以下。这事不是模型玄学是数据一致性问题。4. 预测与评估jieba_test.py 怎么把新名字映射到标签4.1 预测脚本的完整流程jieba_test.py 的预测流程不复杂加载模型、加载词典、对输入书名分词、调用 predict。把核心代码拆出来看import fasttext import jieba model fasttext.load_model(data/novel_names.model) jieba.load_userdict(user_dict.txt) def predict_novel_type(name): tokens jieba.lcut(name) text .join(tokens) labels, probs model.predict(text, k3) result [] for label, prob in zip(labels, probs): tag label.replace(__label__, ) result.append((tag, float(prob))) return result test_names [斗罗大陆, 全职高手, 微微一笑很倾城, 诡秘之主, 永安梦] for n in test_names: print(n, predict_novel_type(n))model.predict的第二个参数k3表示返回概率最高的三个标签。这个参数在课设答辩时特别有用评委大概率会问“分错的情况怎么办”这时候你直接展示 Top3 概率说明模型其实给出了候选集合只是置信度不高比单看一个硬分类结果有说服力得多。预测前必须分词这是和训练时保持特征空间一致的前提。fastText 把整句按空格切词如果不分词“斗罗大陆”四个字会被当成一个整体 token和训练语料的 token 分布完全对不上。用户输入的文本可能很短但分词这一步不能省。4.2 用阈值判断给“不确定”留一条退路实际跑完会发现有些书名天生模糊。“永安梦”听着像古言也可能落在玄幻。如果业务上允许“不确定”这个答案建议加一层阈值判断def predict_strict(name, threshold0.6): result predict_novel_type(name) top_label, top_prob result[0] if top_prob threshold: return 不确定, result return top_label, result阈值设成 0.6 是一个比较稳的起点。如果验证集准确率已经到 85% 以上可以把阈值降到 0.5提高对“确定样本”的召回。如果线下抽测感觉错误率偏高就提到 0.7。注意阈值不是模型参数而是业务参数取决于你更怕“给错答案”还是“给不出答案”。4.3 在验证集上量化准确率不靠感觉靠数字预测脚本之外还要有一份可量化的评估。训练时已经切出来 10% 的验证集存到了 data/valid.txt可以用它快速算准确率import fasttext import jieba model fasttext.load_model(data/novel_names.model) jieba.load_userdict(user_dict.txt) def evaluate(path): correct 0 total 0 with open(path, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 2: continue text .join(parts[:-1]) true_label parts[-1] labels, probs model.predict( .join(jieba.lcut(text)), k1) pred_label labels[0].replace(__label__, ) correct (pred_label true_label) total 1 print(f验证集准确率{correct / total:.3f}样本数{total}) evaluate(data/valid.txt)这个脚本的统计口径是只要 Top1 命中就算对。如果后续调整了标签合并规则这份脚本能马上告诉你改动是变好还是变坏。我一般会在改数据后先跑一遍 evaluate再跑训练再把 eval 结果记下来形成一个“数据改版记录”。5. 避坑与排查分词、环境、数据不平衡和路径问题的实际记录5.1 现象所有预测结果都集中在“其他”类原因训练语料里“其他”类样本数量碾压其他类或者标签归一化时把多个小类都映射到了同一类。fastText 的 softmax 会学到大类的先验分布大类持续占优小类几乎学不到特征。解决合并后先打印标签分布确认没有单一标签超过 50%。如果“其他”类占比太高对它做随机下采样让每个标签的训练样本量接近。小类如果实在凑不够样本合并到语义相近的大类里不要硬留一个只有几十条样本的标签。5.2 现象训练时准确率高一换电脑预测就崩原因训练环境加载了自定义词典 user_dict.txt预测环境忘了加载或训练用jieba.cut预测用了jieba.cut_for_search。两种分词模式对同一句的输出不一样导致特征不一致。解决把加载词典和分词的逻辑抽成一个公共函数训练和预测都调用同一个函数不要各自写一遍。换环境后第一件事是确认 user_dict.txt 和模型文件在同一个相对路径下或者统一用绝对路径。5.3 现象fasttext.load_model 报错“无法打开模型文件”原因脚本里的相对路径是相对当前工作目录的在 IDE 里运行没问题换到命令行直接跑 Python 文件时当前目录变了模型文件找不到。解决用pathlib把路径锚定到脚本所在目录。这个写法对 train.py 和预测脚本都适用from pathlib import Path BASE_DIR Path(__file__).resolve().parent model_path BASE_DIR / data / novel_names.model train_path BASE_DIR / data / train.txt另外在 Windows 上装 fasttext 如果出现安装失败可以试pip install fasttext-wheel接口和 fasttext 完全一致。这点我在不少机器上踩过不是项目代码问题是预编译包的锅。5.4 现象预测结果里出现带着下划线的奇怪标签原因原始页面的标签字段里带空格或斜杠清洗时没处理干净。fastText 把标签当作一个 token如果标签写成“玄幻 重生”训练时会被拆成两个 tokenpredict 得到的标签带上空格还有可能和文本分隔符冲突。解决在 dataset_collections.py 的normalize_label里加一道清洗把标签内部的分隔符替换成下划线import re def clean_label(raw): raw raw.strip() raw re.sub(r[\s/\\,、], _, raw) return raw再配合别名映射。处理完之后重新生成 novel_names.txt重新训练。标签清洗做得越干净后面的坑越少。5.5 现象训练速度越来越慢特征膨胀明显原因minCount1把所有低频词都保留词表非常大wordNgrams2又产生了一大批组合特征。数据量一大训练时间和内存开销都会翻倍。解决先调minCount3或5过滤掉只在少数书名里出现一次的词这对短文本任务影响很小但能显著降低词表规模。然后把epoch从 25 降到 10先跑通流程再逐步加回去。调参时不要一次改多个参数每次只动一个才能在验证集上看出真实差异。6. 进阶套路批量判断、结果落盘与版本化习惯6.1 把预测封装成命令行工具课设做完后这套代码的价值在复现和扩展。最常见的需求是批量判断把几百个待测书名放在一个文件里脚本逐行读取输出到结果文件。把前面定义的predict_strict拿来接一个命令行入口import argparse import fasttext import jieba model fasttext.load_model(data/novel_names.model) jieba.load_userdict(user_dict.txt) def predict_strict(name, threshold0.6): tokens jieba.lcut(name) labels, probs model.predict( .join(tokens), k3) top_label labels[0].replace(__label__, ) top_prob float(probs[0]) result [(labels[i].replace(__label__, ), float(probs[i])) for i in range(len(labels))] if top_prob threshold: return 不确定, result return top_label, result if __name__ __main__: parser argparse.ArgumentParser(description按书名预测小说类型) parser.add_argument(--input, requiredTrue, help待预测书名文件每行一个书名) parser.add_argument(--output, defaultpredict_result.txt) parser.add_argument(--model, defaultdata/novel_names.model) parser.add_argument(--threshold, typefloat, default0.6) args parser.parse_args() input_model fasttext.load_model(args.model) with open(args.input, encodingutf-8) as f, open(args.output, w, encodingutf-8) as out: for name in f: name name.strip() if not name: continue tag, probs predict_strict(name, args.threshold) top2 |.join([f{t}:{p:.2f} for t, p in probs[:2]]) out.write(f{name}\t{tag}\t{top2}\n) print(完成)这个脚本可以直接在命令行执行python predict_batch.py --input books.txt --output result.txt --threshold 0.6。输出结果里同时保留 Top2 概率方便后续人工复核。6.2 输出带概率的结果作为设计报告的证据批量输出我一般会用 Tab 分隔方便直接拖进 Excel 或做二次统计。典型的输出表是这样书名最终判断Top1 标签Top1 概率Top2 标签Top2 概率斗罗大陆玄幻玄幻0.92游戏0.05永安梦不确定言情0.45玄幻0.30答辩时拿这份表格去说明“模型在低置信度样本上会选择拒绝回答”比只贴一个准确率数字更有说服力。也可以基于这份结果做错误分析找出哪些书被明显错分再反推是分词问题还是语料缺标签。6.3 保留训练测试的版本化习惯我后来拿到这种带模型的资源第一件事不是立刻跑训练而是先看数据统计确认标签分布没有明显异常再跑一次合并且检查输出格式最后才动 train.py。数据集、模型、分词词典三者必须绑定换任何一个都要重新跑一遍评估最好在模型名字后面加日期或版本号比如 novel_names_v2.model。不然过两周你自己都分不清当前模型是哪份数据训出来的。调整分词词典后记得重新生成训练语料不能只在预测脚本里改词典。这些习惯说起来琐碎但确实能拦住一批最浪费时间的“玄学报错”。希望你这次能少踩几个坑把从数据到预测这条路一次跑通。本文还有配套的精品资源点击获取