Python实现NLP诗歌接龙:从条件概率到文本生成实战

📅 发布时间:2026/9/29 19:03:11
Python实现NLP诗歌接龙:从条件概率到文本生成实战
简介这是一份面向自然语言处理NLP初学者的实战项目压缩包聚焦“诗歌接龙”趣味场景涉及中文分词、拼音转换、语义匹配与规则/统计接龙算法适合想通过完整代码学习Python编程和NLP基础流程的开发者。包体共17个文件约5.86MB核心包括4个Python源码文件如分词、拼音转换、接龙算法模块、4个XML配置、2个PK与2个DAT数据文件用于保存诗词语料或模型参数并附1个docx说明文档可系统了解项目架构。另含1个exe可执行程序便于不熟悉环境的读者直接运行体验。压缩包整体结构清晰已有496人学习下载热门度良好。通过阅读源码和文档读者能掌握用jieba、pypinyin等库处理中文文本的方法理解基于韵脚规则或统计模型实现诗句接龙的设计思路是一份少有的将NLP理论落地为趣味项目的优质参考资料。1. 诗歌接龙凭什么成为 Python 与 NLP 练习的“最好战地”很多新手拿到“python实现NLP项目-诗歌接龙.zip”这类资源第一反应是打开代码跑一遍发现能接两句就关掉。我的建议是先别急着碰代码先想清楚为什么偏偏是“诗歌接龙”最适合当 NLP 入门项目因为它把中文分词、文本索引、条件概率、生成策略这几块最核心的 NLP 概念压缩到一个几分钟能跑完的小程序里。你不需要 GPU不需要买语料也不需要读懂 Transformer 原文就能把一个看得见摸得着的自然语言处理流程跑通。适合的人群很明确刚装好 Python、想从“语法学习”跨到“项目实战”的人以及打算做文本生成类应用、想先验证最小可行方案的同学。这门项目做完你对“NLP 不是黑匣子”这句话的体会比看十篇入门博客都管用。2. 把“接龙”翻译成 NLP 建模先想清楚“接”这件事到底怎么发生2.1 严格定义问题接龙模型在求一个条件概率“接龙”在程序眼里不是一个文字游戏而是一个标准文本生成任务。给定上一句诗 S要求生成下一句 S并且必须满足S 的首字符等于 S 的尾字符。写成条件概率形式就是P(S | S, first_char(S) last_char(S))这个约束条件看着简单但它决定了整个项目的走向。很多入门教程一上来就讲 Transformer、BERT却把“末字必须对上”这个硬约束放在一边结果模型跑出来语义通顺但根本接不上龙。我的经验是先把这个约束写死在数据结构里再去谈用什么模型生成整个项目的复杂度会立刻降一个量级。具体到实现上最简洁的做法是建立两个字典索引。第一个字典是“尾字 → 以该字开头的诗句列表”第二个是“首字 → 诗句列表”。接龙生成时只需要查第一个字典拿到候选集再从中挑一句输出。这个做法的本质是把每个尾字当成一个条件候选集就是这个条件成立的所有样本。你不需要真的去算一个神经网络的概率分布一个哈希索引已经帮你完成了条件概率的过滤。2.2 为什么 Python 是中文 NLP 项目最稳的选择选 Python 做这类项目不是因为它快而是因为它生态完整、排查问题方便。拿这个诗歌接龙场景来说常用的库就三四个jieba 做分词regex 做文本清洗pickle 做索引持久化最多再配一个 numpy 做向量计算。这些库在 Windows、macOS、Linux 上都有预编译的 wheel 包pip install 一行装完不需要自己编译源码。还有一个现实因素中文文本处理极易踩编码坑。Python 3 的字符串默认是 Unicode比 Python 2 时代省去大量 decode/encode 的麻烦。配合 IDE 的 UTF-8 设置和终端的环境变量基本能避免一半的乱码问题。加上 Python 的交互式命令行和 Jupyter Notebook清洗语料阶段可以边写边看结果这对调试中文正则表达式尤其重要。你写一个re.sub去掉标点马上能看到效果不用反复编译整个项目。不过我也要提醒一句Python 的优势是开发效率不是运行效率。诗歌接龙这种数据量在几万行以内的小项目Python 的性能瓶颈完全感受不到。但如果你将来把这个项目扩展到全唐诗十万首以上甚至要做实时交互记得把索引构建和生成逻辑分开生成阶段只加载序列化后的索引文件不要每次启动都重新解析原始文本。2.3 规则接龙和语言模型接龙入门阶段我推荐走哪条路接龙实现大致分两派。一派是纯规则/统计派建立末字索引用随机抽样或频率加权选下一句。另一派是深度学习派用 RNN、LSTM 或 GPT 类模型做端到端生成。你如果去搜现在市面上的“NLP 项目实战”大多会往第二派引动辄调用几十 GB 的预训练模型。但我的建议是入门阶段先走规则派至少把第一版跑通再决定要不要升级到模型派。原因有三。第一规则派代码量小、逻辑透明出了问题你能用 print 一步步查。第二诗歌接龙的硬约束——首尾字匹配——恰恰是深度模型最不擅长保证的点模型生成的句子再流畅也经常出现张冠李戴。第三语料需求不同规则派只要几千句诗就能玩出效果模型派至少需要几十万条平行语料才谈得上微调对于个人项目来说数据门槛太高。等规则派跑通后你可以把它当成一个基线系统。将来想引入 word2vec 做语义排序也好想接一个 GPT 模型重新排序候选也罢都是在基线上做增强每一步都有对比参照。这个路线比一上来就砸钱训模型要稳得多。3. 用 Python 把诗歌接龙跑起来清洗、索引、生成三步走3.1 诗句语料清洗从原始 TXT 到“一首一行”几乎所有诗歌语料下载下来都不能直接用。常见问题是全诗是分段存放的每行可能含标题、作者、标点、空格甚至夹杂数字标注。清洗的第一步就是把每一首完整的诗拆成单独的诗句行。对于接龙任务来说我们不关心一首诗的作者和题目只关心每一句的长度和首尾字。# clean_poems.py # 功能把原始语料清洗成“每行一句诗”的干净文本 import re from pathlib import Path def clean_poem_file(src_path: str, dst_path: str) - int: lines Path(src_path).read_text(encodingutf-8).splitlines() cleaned [] for raw in lines: # 去掉全角空格、数字以及常见中英文标点 line re.sub(r[0-9\u3000。、“”《》\s], , raw.strip()) # 过滤非诗句行一般单句诗在 515 个字之间 if 5 len(line) 15: cleaned.append(line) Path(dst_path).write_text(\n.join(cleaned), encodingutf-8) return len(cleaned) if __name__ __main__: # 用法python clean_poems.py 原始语料.txt 清洗后.txt import sys n clean_poem_file(sys.argv[1], sys.argv[2]) print(f清洗完成保留 {n} 行诗句)这段代码有两个关键参数值得留意。第一个是正则表达式里的字符范围\u3000它匹配全角空格很多语料是从 PDF 或网页复制下来的段落开头会带全角缩进不处理会导致诗句首字带不可见字符后续建索引时“首字”永远匹配不上。第二个是长度过滤 515这个范围覆盖五言绝句和七言律诗的主体句式如果你接龙的语料包含词牌长短句范围要放宽到 325。清洗完的输出文件应该是每行一句诗没有空行没有标点。你可以用wc -l数一下行数如果清洗出来的句子数量不到原始文件行数的三分之一说明过滤条件可能过严检查一下是否把合法诗句也滤掉了。常见的坑是原始语料里一首诗就是一行整诗长度超过 15 个字直接被丢弃这种情况要把清洗逻辑改成“按标点或换行切分后再过滤”而不是直接对整行做长度判断。3.2 建立“末字到首字”双索引一个问题最核心的数据结构清洗结束后接龙器的地基是索引。我们需要两个字典head_index 负责把首字映射到诗句列表tail_index 把尾字映射到诗句列表。生成接龙时用到 head_indextail_index 是在做反向验证或统计语料覆盖度时用的两个都建好以后排查问题会方便很多。# build_index.py # 功能读取清洗后的诗句建立首字/尾字双索引并序列化 from collections import defaultdict import pickle def build_index(poem_path: str): with open(poem_path, encodingutf-8) as f: poems [line.strip() for line in f if line.strip()] head_index defaultdict(list) tail_index defaultdict(list) for poem in poems: head_index[poem[0]].append(poem) tail_index[poem[-1]].append(poem) # head_index、tail_index 都是 dict[str, list[str]] # 序列化后二次启动不需要重新解析文本 with open(index.pkl, wb) as f: pickle.dump({head: dict(head_index), tail: dict(tail_index)}, f) print(f索引构建完成首字覆盖 {len(head_index)} 个尾字覆盖 {len(tail_index)} 个) return head_index, tail_index这里有个细节值得说明为什么要用defaultdict(list)而不是普通 dict因为同一个尾字可能对应几十句诗用 defaultdict 可以避免每次都要判断“这个键在不在字典里”的样板代码。构建完成后要用dict(head_index)转回普通字典再序列化否则反序列化时代码其他模块拿到的也是 defaultdict行为会依赖初始化函数不利于后续维护。索引构建完成后你可以快速做一次数据体检统计 head_index 里有多少个键、平均每个键对应多少个候选句。我一般要求单字候选不少于 10 句如果某个字只有一两句候选接龙到那个字时基本就会断链这会在项目后期成为生成质量的瓶颈。遇到这种情况要么补充语料要么在生成逻辑里加入后面的兜底策略。3.3 生成函数从一句诗开始循环接出新句子索引建好剩下的就是循环。每一轮迭代做四件事取当前句的尾字从 head_index 取出候选列表过滤掉已经用过的句子再从候选里随机选一个作为下一句。整个过程可以写成独立的生成函数参数全部暴露出来方便后面调参。# dragon_chain.py # 功能核心接龙生成器支持随机种子、候选池大小、历史去重 import random def generate_chain( start_line: str, head_index: dict[str, list[str]], max_len: int 6, top_k: int 5, seed: int | None None, ) - list[str]: rng random.Random(seed) seen {start_line} chain [start_line] current_char start_line[-1] while len(chain) max_len: candidates head_index.get(current_char, []) # 筛选掉已经出现过的句子防止循环 unseen [c for c in candidates if c not in seen] if not unseen: break # 随机打乱后只取前 top_k 句避免每次都选到同一句 rng.shuffle(unseen) pool unseen[:top_k] nxt rng.choice(pool) seen.add(nxt) chain.append(nxt) current_char nxt[-1] return chain生成逻辑里有三个参数直接决定结果形态。max_len是总句数设置为 4 会得到一首“五绝式”的四句接龙设置为 8 会得到一首较长的排律式输出。top_k是候选池大小值越小输出越稳定值越大越容易产生惊喜但也越容易翻车出现语义不搭的句子。seed是随机种子调试阶段固定一个值能复现结果比如seed42时每次跑结果都一样方便你对比不同参数组合的效果。这段代码最容易被忽略的是seen集合。没有去重的话生成到第三四句很可能又接回开头那句循环往复变成死循环。我见过不少初版代码把max_len设成 10输出却只有三句半就是因为候选池里能接的句子来回就那几句不去重会让生成器原地打转。去重之后如果候选被耗尽宁可断链也不要硬凑。生硬凑出来的句子会打破整首诗的一致性比少一句更伤体验。3.4 搭一个命令行接龙台把以上所有步骤串起来现在把清洗、索引、生成三段逻辑合成一个可交互的程序。这个入口文件负责接收用户输入、调用生成函数、格式化输出。不需要做 GUI命令行交互已经足够验证核心逻辑。# main.py # 功能命令行接龙台运行后输入一句诗输出一段接龙 from build_index import build_index from dragon_chain import generate_chain if __name__ __main__: head_index, tail_index build_index(poems_clean.txt) start input(请给一句诗515字).strip() if not (5 len(start) 15): print(输入长度不在常见诗句范围内接龙仍会继续但效果可能受影响。) result generate_chain(start, head_index, max_len6, top_k5, seedNone) for i, line in enumerate(result, 1): print(f{i}. {line})运行方式很简单在项目目录下执行python main.py程序会先构建索引首次运行会花几秒然后等待输入。比如输入“床前明月光”系统提取尾字“光”从 head_index 里找到所有以“光”开头的句子随机选一句接上再取那句的尾字继续循环直到达到六句上限或候选耗尽。这一步跑通后你手里的已经不是一段演示代码而是一个可以交互的 NLP 小工具。下一步要做的就是调参让它的输出从“能接得上”变成“读着像诗”。4. 把接龙调到“有点诗味”三个必须掌握的核心旋钮4.1 随机性控制为什么同一句开头每次结果都不同接龙系统如果每次把同一个尾字都映射到同一句候选玩两次就会腻。随机性是这类生成项目的基本盘但随机得没有章法输出就会变成什么都能接、什么都没逻辑。这里需要区分两个概念随机选句和概率加权选句。前者的每个候选概率相等后者的候选会被打分按分数高低作为抽样权重。闪烁阶段建议先用等概率随机跑通后再引入权重。等概率随机的好处是结果分布均匀你更容易判断语料库本身的质量而不是被排序逻辑干扰。当你发现某些候选句永远不被选中时再考虑加权。一个简单的加权方案是优先选择和当前句字数相同的候选。五言接五言、七言接七言是现代诗接龙“看着舒服”的最低要求。temperature 这个概念在神经网络生成里常见在规则派里同样有对应实现把候选按长度相似度打分用 softmax 缩放分数后再抽样。temperature 低输出更保守temperature 高连不太相关的句子也可能被选中。在诗歌接龙这个场景里我一般把 temperature 控制在 0.8 到 1.2 之间低于 0.8 输出会变得很无聊一直选最短的那句。4.2 句式一致性五言和七言不能混搭新手最容易忽略的问题是不控制每句长度。五言诗的节奏是二三七言是四三混在同一首诗里读起来会非常难受。规则派解决这个问题很简单生成候选时先按长度过滤。# 在 generate_chain 里增加一个 length 参数 def generate_chain( start_line: str, head_index: dict[str, list[str]], max_len: int 6, top_k: int 5, line_len: int 0, # 0 表示不限制5 或 7 表示统一句式 seed: int | None None, ) - list[str]: rng random.Random(seed) seen {start_line} chain [start_line] current_char start_line[-1] while len(chain) max_len: candidates head_index.get(current_char, []) if line_len 0: candidates [c for c in candidates if len(c) line_len] unseen [c for c in candidates if c not in seen] if not unseen: break rng.shuffle(unseen) pool unseen[:top_k] nxt rng.choice(pool) seen.add(nxt) chain.append(nxt) current_char nxt[-1] return chain加了line_len参数之后调用端可以根据用户输入推断格式如果输入是七个字就把 line_len 设为 7强制后续接龙全部七言。这样输出的整首诗节奏统一接龙感更强。代价是候选池变小、断链更早所以语料里七言诗的比例不够时容易接两句就停了。这种情况我不建议硬放宽长度限制而是去补语料。4.3 断链兜底给“接不上”留一张后悔药规则派断链不可避免。无论语料多大总有一个生僻尾字在 head_index 里查不到。这时程序如果直接停体验很差如果乱接又破坏了首尾字约束。我建议按三级策略兜底第一级取当前句的倒数第二个字再查一次第二级用拼音相似的字替代尾字查找第三级如果都不行直接结束生成保证已有的诗句不崩。拼音兜底需要引入 pypinyin 库这不是标准库需要单独安装。好处是把“光”查到同音字“广”“逛”等对应的候选集虽然语义差了点但至少能续下去。做成一个可选参数use_phonetic_fallbackFalse默认不开启开启后断链率能降低不少。“后悔药”这个思路在整个 NLP 项目里都通用任何生成系统都要考虑失败分支不要让用户看到一半的输出在半句话处戛然而止。诗歌接龙断链最多就是少接两句还算好收拾等将来你去做对话系统、文本摘要没有兜底策略体验崩塌的速度会远超你的预期。5. 诗歌接龙避坑从环境到逻辑的四条血泪经验5.1 现象明明末字对上了首字索引却查不到辛苦清洗完语料跑生成时发现输入“床前明月光”系统提示 head_index 里找不到“光”字。手动打印 head_index 的 keys里面写着“光”没有异常但 get 就是返回空。排查到最后发现清洗阶段去除了标点但原始语料里包含的是全角符号正则只匹配了半角逗号导致“床前明月光”清洗后变成“床前明月光”末尾保留了一个全角逗号尾字取到的是“”而不是“光”。首尾字错位索引当然对不上。解决方法是清洗阶段统一把中英文标点全部列进正则表达式不要偷懒只写几个常见的。顺带加一个保险清洗完成后抽样打印 20 行结果肉眼看一遍数据质量再建索引。这一步很土但比任何调试工具都有效。5.2 现象同一句诗反复出现接龙成了复读机生成结果前三句分别是“床前明月光”“光照雪衣明”“明月照积雪”第四句又回到“床前明月光”形成死循环。原因是候选池里能接“光”的句子就那两三句去重逻辑虽然在但第三句的尾字仍然能回到“光”继续触发同一批候选周而复始。另一个重复来源是 tail_index 里同一个候选被多个尾字共用比如“明月”既是首字也是尾字整个环路特别短。解决思路有两个层面一是尽早去重把已经用掉的字放进 seen 集合再次出现时跳过二是如果整条链上已经出现过某个尾字下一轮不要再选以它为结尾的候选强制引入新的尾字。后者效果更好相当于给状态机加了一个历史约束。5.3 现象Windows 终端里中文全部变成乱码在 Windows 下用 CMD 运行 main.py输出的诗句变成一串“锟斤拷”。原因有两个一是 Python 默认编码在不同版本下有差异二是 CMD 的代码页是 GBK和程序的 UTF-8 输出不匹配。解决方法是运行前在命令行执行set PYTHONUTF81或者在 main.py 开头指定sys.stdout.reconfigure(encodingutf-8)强制让输出流使用 UTF-8。代码里所有文件读写操作都明确加上encodingutf-8参数不要依赖系统默认编码。这个坑在 macOS 和 Linux 上不明显但 Windows 用户占了一半以上处理不好会让还没入门的用户直接放弃项目。如果你用的是 PyCharm顺手把 File Encoding 和 Console Encoding 都设置成 UTF-8能少很多折腾。5.4 现象pip 安装 jieba 或 pypinyin 时反复失败环境配置阶段最容易翻车。pip 在默认源下载慢、超时导致安装 jieba 失败。通常的解决方法是换国内镜像源比如清华源或阿里源。还有一个冷门原因某些 Python 版本在 Windows 上缺少 vc 运行库导致编译部分依赖失败。解决方法是安装官方发布的 Microsoft C Build Tools或者从 pypi 下载预编译 wheel 文件本地装。我建议在项目根目录放一个requirements.txt里面列齐依赖库和版本号。这样换电脑、换环境时一条pip install -r requirements.txt就能复现不用一个个装也能避免“在你电脑上能跑在我电脑上不行”的尴尬。6. 给自己的接龙器装一把“质量尺子”用数据说诗好不好接龙跑通、调参调顺之后最容易陷入的误区是靠肉眼判断结果好不好。还“挺像诗”“这句读不通”这类主观评价既不能横向比较也没法定位问题。我习惯的做法是写一个轻量评估脚本用三个客观指标衡量接龙质量首尾字命中率、句式一致率、重复率。这三个指标满足后再让真人读一读主观评价才有意义。# evaluate.py # 功能抽样多条接龙结果统计三个质量指标 import random from build_index import build_index from dragon_chain import generate_chain def evaluate(head_index, n_samples: int 200, max_len: int 6, line_len: int 0): # 从索引里随机取一批起始句尽可能覆盖不同尾字 start_chars random.sample(list(head_index.keys()), min(n_samples, len(head_index))) stats {hit: 0, consistent: 0, repeat: 0, total_links: 0} for ch in start_chars: start_line random.choice(head_index[ch]) result generate_chain(start_line, head_index, max_lenmax_len, line_lenline_len) for prev, nxt in zip(result, result[1:]): stats[total_links] 1 if prev[-1] nxt[0]: stats[hit] 1 if line_len 0 or len(nxt) line_len: stats[consistent] 1 if nxt in result[: result.index(nxt)]: stats[repeat] 1 # 输出三个百分比指标 hit_rate stats[hit] / max(1, stats[total_links]) consistent_rate stats[consistent] / max(1, stats[total_links]) repeat_rate stats[repeat] / max(1, stats[total_links]) print(f首尾字命中率: {hit_rate:.1%}) print(f句式一致率: {consistent_rate:.1%}) print(f句子重复率: {repeat_rate:.1%}) if __name__ __main__: head_index, _ build_index(poems_clean.txt) evaluate(head_index, n_samples200, max_len6, line_len7)这个脚本输出的三个百分比就是你的接龙器目前的真实水平。首尾字命中率正常应该在 100%因为生成逻辑本身就保证了这一点如果低于 100%说明你的数据里有隐形字符干扰句式一致率在开启线长限制后也应该接近 100%如果掉下去就是候选池耗尽导致突破了限制重复率受语料影响较大超过 5% 就该考虑去重策略或者补语料。我拿到一个新语料第一件事永远是跑这个评估而不是急着调模型。评估数字告诉我的是语料覆盖够不够、清洗是否彻底、参数设得合不合理。比如首尾命中率正常但重复率很高那问题一定出在候选池多样性上正确答案是扩展语料或放宽候选池如果句式一致率只有 60%那就是语料里五言七言比例失衡。用数据定位问题比一句句读诗猜原因要快得多。这套验证思路不仅适用于接龙任何文本生成项目都可以迁过去——摘要、扩写、对话只要做健壮三项指标硬约束命中率、风格一致率、重复率。最后说一个我的习惯一直是把“诗意”交给语料本身让程序只负责在好诗句之间做选择不负责凭空创造。接龙器再牛也只能在语料边界内跳舞。希望这篇笔记能帮你在 NLP 实战路上少走几段弯路把第一个真正属于你自己的自然语言处理项目跑起来。本文还有配套的精品资源点击获取