news 2026/11/2 8:28:11

Python NLP诗歌接龙源码解析:字级n-gram与押韵束搜索

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python NLP诗歌接龙源码解析:字级n-gram与押韵束搜索

简介:这份资源面向想用Python入门自然语言处理、并希望完成一个可运行小项目的学习者,核心是一个围绕诗词文本展开的接龙程序:从诗句中提取尾字、借助pypinyin做拼音与韵脚匹配,再结合jieba分词与规则或统计策略筛选可接续的诗句,可用于课程设计、NLP练手与文本处理入门。压缩包共17个文件、约5.86MB,以py源码和xml配置为主,兼有pk、dat模型与数据文件、exe与pyc可直接运行或对照的成品、docx说明文档及txt记录,源码、数据与文档层次分明,便于一边读实现一边跑效果。目前已有499人学习下载。文档中对接龙流程、模块划分与使用方式有完整交代,读者可据此理解音韵匹配思路、复用抓取与清洗脚本,并自行替换词库或扩展押韵规则,获得一份结构清晰的实战参考。

1. 从一句“春眠不觉晓”往下接:这份 Python NLP 诗歌接龙源码包到底在做什么

先把结论放这儿:这份 Python NLP 诗歌接龙源码包不是让你背诗,也不是简单查“下一句”,它把古诗拆成字级语言模型,让程序根据上一句续写下一句,真正难的是押韵约束、重复控制和长度对齐。很多人第一次跑这类项目,会发现模型能出字,却总在韵脚上翻车,或者五言接成六言,这也是它值得拆开看的原因。

它适合两类人:一类是刚接触 NLP 的 Python 开发者,想找一个比情感分类更有意思的练手项目;另一类是做内容 demo 的工程师,需要一套能本地跑、能改参数、能接前端接口的诗歌接龙基线。源码包里的思路通常围绕语料清洗、n-gram 或轻量 LSTM、束搜索生成这三块展开。

不适合把它当成“开箱即用的作诗神器”。古诗的平仄、对仗、意象连贯性,靠一个几万字的语料和字级模型只能做到形似;但作为教学项目、课程设计、NLP 流程演示,它足够完整,也足够让你踩到真实工程里的坑。

2. 语料与韵脚表:把古诗拆成字级 n-gram 和可查询的韵库

拿到压缩包后,先别急着运行生成脚本。诗歌接龙的上限很大程度由语料清洗和韵脚表决定:清洗错了,模型学到的是标题、作者名和标点;韵脚表粗了,生成句的尾字会“看着像押韵,读起来别扭”。这一章按数据进入模型的顺序拆:先按字切分,再建韵部,最后落成多阶 n-gram 索引。

2.1 语料清洗:先按字切,不按词切

古诗和现代文不一样,很多“词”在古诗里是两个字各自表意,直接用 jieba 分词会把“明月”“故乡”这类高频意象切碎,字级模型反而更稳。我一般会先做三件事:去掉行首的标题标记、去掉括号注释、只保留汉字。下面这段脚本适合“一行一句”的预处理文本,如果你的语料是一行一首,先按标点切成句再喂进来。

import re from pathlib import Path HANZI = re.compile(r"[\u4e00-\u9fff]") def normalize_line(line: str) -> list[str]: line = line.strip() # 去掉行首《》【】包裹的标题片段,避免标题字进入正文统计 line = re.sub(r"^[《【].*?[》】]", "", line) # 去掉括号内的注释或异文标记 line = re.sub(r"[((].*?[))]", "", line) # 只保留汉字,标点和空白全部丢弃 return HANZI.findall(line) def load_corpus(path: str, min_len: int = 5) -> list[list[str]]: sentences = [] for raw in Path(path).read_text(encoding="utf-8").splitlines(): chars = normalize_line(raw) if len(chars) < min_len: continue sentences.append(chars) return sentences

这段代码的逻辑很直白:normalize_line负责把一行文本压成汉字列表,load_corpus负责过滤过短的行。min_len=5是按五言诗的下限设的,如果你只做七言,可以改成 7;如果语料里混了词牌和长调,建议先按标点切句,再按 5 或 7 过滤,否则长句会把 n-gram 的上下文拉得很散。编码参数固定utf-8,Windows 下如果遇到gbk文件,先转码再跑,不要在代码里做编码猜测,后面排查会很痛苦。

清洗完别直接全量训练。抽 100 行打印出来看,重点看三件事:标题字有没有混进去、异体字有没有统一、一行里是不是塞了两句。古诗语料最常见的翻车不是模型太弱,而是“春眠不觉晓处处闻啼鸟”被当成一句,长度控制直接失效。这个检查花两分钟,能省后面两小时。

2.2 韵脚表:用普通话韵母近似,不硬套平水韵

诗歌接龙最容易被低估的是韵脚。严格按平水韵做当然好,但工程上更常见的做法是先用普通话韵母做近似韵部,把ang/iang/uang归到一类,把an/ian/uan/üan归到一类,先让接龙能跑起来,再按需要细化。pypinyin的Style.FINALS能直接取韵母,多音字用heteronym=True展开,建表时保留所有读音,生成时再取交集。

from pypinyin import pinyin, Style RHYME_MAP = { "ang": "ang", "iang": "ang", "uang": "ang", "an": "an", "ian": "an", "uan": "an", "üan": "an", "ao": "ao", "iao": "ao", "ai": "ai", "uai": "ai", "en": "en", "in": "in", "un": "un", "ün": "un", "eng": "eng", "ing": "ing", "ong": "ong", "iong": "ong", "i": "i", "u": "u", "ü": "ü", "er": "er", } def rhyme_keys(ch: str) -> set[str]: result = set() for item in pinyin(ch, style=Style.FINALS, heteronym=True, errors="ignore"): for final in item: if final: result.add(RHYME_MAP.get(final, final)) return result

逻辑说明:pinyin返回的是嵌套列表,heteronym=True时一个字可能有多个读音,每个读音对应一个韵母;errors="ignore"让非汉字直接跳过,避免生成阶段因为标点或生僻符号抛异常。参数上,RHYME_MAP是近似归并表,不是韵书,ang组和an组不要混在一起,否则“山”和“光”会被判成同韵,读起来很怪。多音字返回set是为了后面做交集判断,比如上一句尾字有两个韵部,下一句尾字命中任意一个就算押韵。

原始韵母归并到说明
iang / uangang江阳辙近似,适合七言
ian / uan / üanan寒山辙近似,注意与 ang 分开
iaoao遥条辙近似
ing / engeng庚青辙可合并,但输出会更宽松

这张表只解决“能不能押”的第一层问题。真要做到读起来顺,还要看声调:普通话一、二声算平,三、四声算仄,生成时把平仄当软约束加一点分,不要硬卡。硬卡的结果是候选字被筛空,束搜索直接返回None,新手很容易在这里卡住。

2.3 多阶 n-gram:把<s>和</s>写进索引

字级 n-gram 是这套项目最稳的基线。n=3 表示用前两个字预测下一个字,n=2 表示只看前一个字,n=1 就是全局字频。为什么不只建 trigram?因为生成时一旦遇到没见过的二字组合,trigram 查不到,必须回退到 bigram 和 unigram,否则接龙会在第二个字就断掉。下面这段代码把 1 到 3 阶一起建出来,键是历史元组,值是Counter。

from collections import defaultdict, Counter BOS, EOS = "<s>", "</s>" def build_ngram(sentences: list[list[str]], max_n: int = 3): models = {n: defaultdict(Counter) for n in range(1, max_n + 1)} for sent in sentences: for n in range(1, max_n + 1): tokens = [BOS] * (n - 1) + sent + [EOS] for i in range(n - 1, len(tokens)): history = tuple(tokens[i - n + 1:i]) if n > 1 else tuple() models[n][history][tokens[i]] += 1 return models

逻辑说明:对每个阶数 n,先在句首补n-1个<s>,句尾补</s>,然后滑窗统计。history是前n-1个字,models[3][("春","眠")]["不"]表示“春眠”后面接“不”的次数。参数max_n=3是起步值,n=2 生成更稳但容易重复,n=4 更通顺但很多上下文查不到,语料不到十万句时建议先停在 3。BOS和EOS不要用普通汉字代替,否则会和正文里的“开”“终”混在一起,后面做长度控制时无法区分句尾。

内存上要有预期:unigram 很小,bigram 是字表平方量级,trigram 会随语料线性增长。如果跑起来发现内存吃紧,先把max_n降到 2,或者把低频字统一映射到<unk>再建索引。别一上来就上 5-gram,诗歌语料稀疏,高阶模型看起来“更懂上下文”,实际生成时大量回退,效果不会比 3-gram 好多少。

3. 接龙生成器:从概率表到带押韵约束的束搜索

有了多阶 n-gram,下一步不是直接most_common(1),那样每句都会接成最保守的高频组合,读三句就腻。生成器要做三件事:历史查不到时按阶回退、候选打分时把押韵和重复惩罚加进去、用束搜索保留多条路径。这一章把这三块拆开写,参数都能直接改。

3.1 回退候选:trigram 不够就退到 bigram 和 unigram

回退策略不是“随便找一个字补上”,而是按阶数从高到低查。当前历史有两个字,先查 3-gram;查不到就把历史砍到最后一个字,查 2-gram;再查不到就用 1-gram 全局字频。下面这段函数返回(字, 概率)列表,概率按当前阶的总频次归一化,方便后面和押韵分数相加。

def backoff_candidates(models, history, k=20): max_n = max(models.keys()) for n in range(min(max_n, len(history) + 1), 0, -1): hist = tuple(history[-(n - 1):]) if n > 1 else tuple() counter = models.get(n, {}).get(hist) if counter: total = sum(counter.values()) return [(ch, cnt / total) for ch, cnt in counter.most_common(k)] return []

逻辑说明:history传的是最近两个字组成的元组,长度最多 2;n从min(max_n, len(history)+1)开始往下退,正好对应 3-gram、2-gram、1-gram。k=20是每阶取的候选数,太小会让押韵筛选没有余地,太大则束搜索变慢。参数上,k在生成五言时建议 30 到 50,七言可以放到 50 到 80;如果候选里大量是<s>和</s>,说明句首句尾统计没洗干净,回到 2.1 检查语料。

这里有个细节:回退到 unigram 时,概率是全局字频,高频字会占据候选榜,生成句容易变成“之乎者也”堆叠。我的做法是给低阶回退乘一个折扣,比如 2-gram 乘 0.7、1-gram 乘 0.4,再和押韵分相加。这个折扣不是玄学,是为了让高阶上下文优先,避免模型一查不到就摆烂。

3.2 打分函数:押韵权重、平仄软约束和重复惩罚

候选字不能只看语言模型概率。接龙的目标是“下一句尾字押上一句的韵”,所以尾字要加押韵奖励;句内重复字要扣分;平仄只做轻量加分,不做硬过滤。下面这个打分函数把三项揉在一起,权重量级是经验值,你可以按语料风格调。

import math def tone_of(ch: str): py = pinyin(ch, style=Style.TONE3, errors="ignore") if not py or not py[0]: return None tone = py[0][-1] return "ping" if tone in "12" else "ze" if tone in "34" else None def score_char(ch, prob, used, target_rhyme=None, tone_target=None): score = math.log(max(prob, 1e-12)) if target_rhyme and target_rhyme in rhyme_keys(ch): score += 1.2 if tone_target and tone_of(ch) == tone_target: score += 0.15 score -= 0.8 * used[ch] return score

逻辑说明:math.log把概率转成对数分,避免连乘下溢;used是当前句已出现字的Counter,每重复一次扣 0.8;押韵命中加 1.2,这个权重明显高于平仄的 0.15,是因为接龙场景里“尾字押韵”比“句内平仄完美”更容易被读者感知。参数上,如果生成句重复严重,把 0.8 提到 1.2;如果押韵太生硬、为了押韵牺牲语义,把 1.2 降到 0.6 到 0.8,让语言模型概率重新占主导。

平仄这里只对尾字或关键位置做软约束,不要每个字都卡。古诗的平仄格式有固定套路,但字级模型没有学过整句格式,硬卡会把候选筛空。常见做法是:五言看第 2、4 字和尾字,七言看第 2、4、6 字和尾字,其他位置放开。这个项目如果只是课程设计,平仄可以先不做,先把押韵和重复率压下去,效果提升更明显。

3.3 束搜索:最后一位才强制押韵,别每一步都卡

束搜索的关键是“保留多条候选路径”,但诗歌接龙有一个特殊性:押韵只约束尾字,句内其他位置不应该被韵脚绑死。下面这段生成函数在最后一个位置才强制target_rhyme,前面位置只做打分;同时用prune_beams按最后两个字去重,防止同质路径挤占 beam。

def prune_beams(beams, max_beam): seen = {} for chars, score, hist in beams: key = tuple(chars[-2:]) if key not in seen or score > seen[key][1]: seen[key] = (chars, score, hist) return sorted(seen.values(), key=lambda x: x[1], reverse=True)[:max_beam] def generate_line(models, prev_line, length=5, target_rhyme=None, beam_size=8, topk=40): seed = [BOS] + list(prev_line) beams = [([], 0.0, tuple(seed[-2:]))] for pos in range(length): new_beams = [] for chars, score, hist in beams: for ch, prob in backoff_candidates(models, hist, k=topk): if ch in (BOS, EOS): continue if pos == length - 1 and target_rhyme and target_rhyme not in rhyme_keys(ch): continue used = Counter(chars) s = score + score_char(ch, prob, used, target_rhyme if pos == length - 1 else None) new_hist = tuple((list(hist) + [ch])[-2:]) new_beams.append((chars + [ch], s, new_hist)) if not new_beams: return None beams = prune_beams(new_beams, beam_size) if not beams: return None return "".join(max(beams, key=lambda x: x[1])[0])

逻辑说明:seed把上一句拼进历史,保证接龙和上文有字面衔接;beams里每个元素是“已生成字列表、累计分数、最近两字历史”。最后一个位置用target_rhyme not in rhyme_keys(ch)直接跳过不押韵的字,这一步是硬约束;前面位置不传target_rhyme,避免整句被韵脚拖偏。prune_beams用最后两字做状态键,是因为 n-gram 生成下一字只依赖最近两字,同状态保留高分路径就够了。

参数上,length=5是五言,七言改 7;beam_size=8适合本地 CPU,调到 32 会明显变慢但句子更稳;topk=40是每步候选数,语料小可以降到 20。注意generate_line返回None不是异常,而是“当前韵脚下没有候选”,生产环境要接兜底逻辑,别直接join(None)。下一章会讲 n-gram 和 LSTM 怎么共用接口,以及怎么在超时的时候降级。

4. 模型训练与推理:n-gram 基线到轻量 LSTM 的切换

n-gram 的好处是训练快、可解释、随时能查表,坏处是生成多样性有限,长上下文记不住。源码包里如果有 LSTM 版本,通常是为了对比“统计模型”和“神经模型”的差异。我的建议是先把 n-gram 跑通,再用 LSTM 做增强,不要让神经模型一上来就把数据管道和评测拖住。这一章按“体检指标、数据管道、训练与降级”三块写。

4.1 n-gram 的困惑度:只当体检,不当验收

困惑度能告诉你模型在验证集上“有多意外”,但它和“接龙好不好读”不是一回事。下面这段计算按 3-gram 走,查不到历史时给一个极小概率,正式评测应该接回退概率,这里保留1e-6是为了让脚本能跑完并暴露稀疏程度。

def perplexity(models, sentences, n=3): total_logp = 0.0 total_tokens = 0 for sent in sentences: tokens = [BOS] * (n - 1) + sent + [EOS] for i in range(n - 1, len(tokens)): hist = tuple(tokens[i - n + 1:i]) counter = models[n].get(hist) if not counter: p = 1e-6 else: p = counter.get(tokens[i], 0) / sum(counter.values()) total_logp += math.log(max(p, 1e-12)) total_tokens += 1 return math.exp(-total_logp / max(total_tokens, 1))

逻辑说明:对每个 token 取条件概率,取对数后累加,最后除以 token 数再取指数。n=3要和训练时一致,验证集不能混进训练集,否则困惑度会虚低。参数上,1e-6是回退占位,不是平滑;如果你加了加一平滑,困惑度会变,不同平滑方式之间不要直接比数值。更实用的做法是同时记录“回退次数占比”,回退超过 30% 说明语料覆盖不够,先补数据或降阶。

困惑度低但接龙难读,通常是因为高频虚词拉低了平均意外度。我的验收习惯是:困惑度只看趋势,真正上线前跑 200 条批量生成,统计长度合规率、押韵命中率和句内重复率,再抽 50 条人工读。指标口径在第 6 章给表。

4.2 LSTM 数据管道:字表、BOS/EOS 和定长上下文

LSTM 版本的第一道坎不是模型结构,是数据管道。字级模型需要把每个字映射成 id,<pad>固定为 0,<unk>接生僻字,<s>和</s>标句界。下面先建字表,再把每句拆成“前ctx_len个字预测下一个字”的样本,左侧补<pad>保证 batch 能堆叠。

from collections import Counter import torch from torch.utils.data import Dataset def build_vocab(lines, min_freq=1): counter = Counter(ch for line in lines for ch in line) chars = [ch for ch, n in counter.items() if n >= min_freq] stoi = {"<pad>": 0, "<unk>": 1, "<s>": 2, "</s>": 3} for ch in chars: stoi.setdefault(ch, len(stoi)) return stoi class NextCharDataset(Dataset): def __init__(self, lines, stoi, ctx_len=12): self.items = [] unk = stoi["<unk>"] for line in lines: ids = [stoi["<s>"]] + [stoi.get(ch, unk) for ch in line] + [stoi["</s>"]] for i in range(1, len(ids)): ctx = ids[max(0, i - ctx_len):i] ctx = [stoi["<pad>"]] * (ctx_len - len(ctx)) + ctx self.items.append((ctx, ids[i])) def __len__(self): return len(self.items) def __getitem__(self, idx): ctx, target = self.items[idx] return torch.tensor(ctx), torch.tensor(target)

逻辑说明:build_vocab先统计字频,min_freq=1表示生僻字也保留,诗歌语料小,设 2 或 3 会把很多意象字打成<unk>,反而伤生成;如果语料很大,可以设 2。NextCharDataset对每句生成多个训练样本,ctx_len=12是上下文窗口,五言七言接龙不需要太长,12 到 20 够用。左侧补<pad>而不是右侧补,是为了让out[:, -1, :]永远取到最后一个真实上下文位置。

参数上,ctx_len越大,模型能记住的上文越多,但训练样本的 padding 也越多;batch_size在 CPU 上

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 8:28:01

AI科研绘图工具:提升学术图表效率与质量

1. 科研绘图现状与痛点分析科研绘图长期以来都是学术工作者面临的重要挑战。在论文发表过程中&#xff0c;图表质量直接影响审稿人对研究成果的第一印象。传统绘图工具如Origin、ChemDraw虽然功能强大&#xff0c;但存在明显的使用门槛和效率问题。1.1 传统工具的三大困境首先是…

作者头像 李华
网站建设 2026/11/2 8:27:29

接口自动化测试中的数据库校验方法与最佳实践

1. 接口自动化测试中的数据库校验概述在接口自动化测试中&#xff0c;数据库校验是不可或缺的关键环节。当接口对数据进行增删改查操作时&#xff0c;仅验证接口返回结果是不够的&#xff0c;必须深入数据库层面确认数据变更是否符合预期。我在多个项目中实践发现&#xff0c;完…

作者头像 李华
网站建设 2026/11/2 8:26:18

系统框架与结构设计:从模块化到松耦合的工程实践指南

简介&#xff1a;本资源是一份面向软件工程专业学生、初级开发工程师及技术管理者的学习课件&#xff0c;系统讲解软件工程中系统框架与结构设计的核心方法论与实践原则。内容覆盖软件工程全生命周期管理、系统架构设计原理&#xff08;模块化、松耦合、高内聚&#xff09;、主…

作者头像 李华
网站建设 2026/11/2 8:25:26

LLMZip:基于大语言模型的语义压缩新范式

1. 这不是传统压缩&#xff0c;是用大模型“重写”文本的全新范式你有没有试过把一篇1MB的纯文本&#xff08;比如整本《三体》第一卷的UTF-8编码原文&#xff09;丢进zip里&#xff1f;默认压缩率大概在50%–60%&#xff0c;也就是压到400–500KB。再换7z、xz这些高压缩算法&a…

作者头像 李华
网站建设 2026/11/2 8:24:57

OpenClaw搜索功能故障排查与优化指南

1. OpenClaw搜索功能失效的常见原因分析OpenClaw作为一款个人AI助手&#xff0c;其网络搜索功能依赖于多个组件的协同工作。当搜索功能出现异常时&#xff0c;通常由以下原因导致&#xff1a;1.1 网络连接配置问题最常见的故障点是网络连接配置不当。OpenClaw需要稳定的网络连接…

作者头像 李华
网站建设 2026/11/2 8:24:35

动态规划解决NOIP摆花问题:多重背包变种应用

1. 题目背景与核心需求解析这道来自NOIP 2012普及组的"摆花"题目&#xff0c;描述了一个典型的花店陈列场景&#xff1a;小明需要将n种花共m盆摆成一排&#xff0c;要求同种花必须相邻且按编号顺序排列&#xff0c;同时每种花i的数量不能超过a_i盆。我们需要计算出所…

作者头像 李华