news 2026/10/8 2:20:32

Python NLP实战:诗歌接龙中的分词押韵与语义排序

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python NLP实战:诗歌接龙中的分词押韵与语义排序

简介:一份面向自然语言处理初学者与Python开发者的诗歌接龙实战项目,围绕汉字分词、词性标注、拼音转换与韵律匹配展开,结合爬虫、文本清洗和规则/统计混合算法,解决“给出上句、自动接下句”的典型任务。压缩包共17个文件,约5.86MB,以Python源码(py/pyc)、配置数据(xml/dat/pk)为主,附带可执行文件、说明文档(docx)与文本说明,便于直接运行或按文档逐步理解实现细节。资源已吸引496人学习浏览,代码中集成requests、BeautifulSoup、pypinyin、jieba等常用库,并包含详细的诗歌接龙说明文档,适合作为课程设计、毕业设计或NLP入门的综合参考。通过阅读源码和文档,可掌握从数据获取、拼音匹配到接龙算法落地的完整链路,涵盖数据采集、文本预处理、接龙生成与结果输出等模块,并借鉴其中的模块拆分与排错思路。

1. 诗歌接龙:一个Python NLP小项目,到底能帮你练出什么

诗歌接龙不是简单的字符串拼接,它是一个把分词、拼音、押韵、语义相似度串起来的NLP实战项目。用Python做诗歌接龙,核心是解决一个问题:给你一句诗,怎么从古诗库里找出一句既能在字面上接住、又在格律和意境上不突兀的下一句。适合谁?刚学完Python基础、想找NLP切入点的新手,以及想快速验证分词和语义模型效果的从业者。这个项目不依赖GPU,普通笔记本就能跑,数据量也不大,但涉及的坑一点都不少——从繁体字转换到多音字处理,每一个都能让你重新理解“自然语言处理”这五个字。

2. 诗歌接龙的核心链路:字接、韵接与语义排序的三层结构

2.1 先把“接龙”定义清楚:三种接法对应三种算法

很多人一上来就用“上一句的末字 == 下一句的首字”做字符串匹配,这是典型的字接,简单但效果很机械。实际项目里,诗歌接龙通常分三个层次:

  • 字接:上句末字与下句首字相同,这是硬约束,也是整个接龙成立的前提。比如“花间一壶酒”的末字是“酒”,下句首字必须是“酒”。
  • 韵接:下句末字与上句末字同韵母,保证读起来顺口。律诗和绝句里,押韵是格律的一项硬指标,但在接龙场景里可以放宽成加分项。
  • 意接:下句在内容上不与上句冲突,甚至能有递进或转折。比如“举头望明月”接到“月是故乡明”,字接和意接都成立,因为“月”字把两句串成了完整的意境。

我在处理这个项目时,把这三个层次拆成了独立的函数,方便分别调试。字接用字符串切片,韵接用拼音库做韵母匹配,意接用TF-IDF向量做余弦相似度。这样做的坏处是代码文件多了两三个,好处是哪个环节出问题,直接单独跑那一个函数就行,不用从头查起。

这里要提醒一下:不要把“意接”想得太高级。古诗库就几百首,TF-IDF的效果足够,没必要一上来就上BERT那类模型。原因很简单,古诗文本短、用字高度凝练,词向量模型的训练成本远大于收益,而字符级TF-IDF在短文本相似度上已经能提供有区分度的排序信号。后面我会讲为什么它在这个场景里比深度模型更稳。

这三个维度在实际打分时不是简单的并列关系。字接是门槛,过不了门槛直接淘汰;韵接和平仄是质量分;意接是排名分。把门槛、质量分、排名分混在一个公式里,是很多接龙demo翻车的根源。我习惯分开处理:先过滤、再打分、最后排序。

2.2 押韵检测的Python实现:从拼音库到韵母表

押韵检测是整个项目里最容易翻车的模块。传统做法是维护一份平水韵韵部表,但平水韵对新手来说太痛苦,而且很多字在今天读起来已经不合韵。我一般用pypinyin先把诗句转成拼音,再按韵母判断是否同韵。

# 押韵检测:输入两个汉字,返回它们是否押韵 from pypinyin import pinyin, Style def get_rhyme_group(char: str) -> str: """获取单个字的韵母分组,用于押韵判断""" # Style.FINALS 表示只取韵母部分 finals = pinyin(char, style=Style.FINALS, strict=False) # 处理多音字:默认取第一个读音,生僻字返回空串 if not finals or not finals[0]: return "" return finals[0][0] def is_rhyme(last_char: str, candidate_char: str) -> bool: """判断两个末字是否押韵""" return get_rhyme_group(last_char) == get_rhyme_group(candidate_char)

这段代码的逻辑很直接:Style.FINALS获取韵母,strict=False让拼音库把多音字按通用读法处理。注意pinyin()返回的是二维列表,因为它是按句子处理的,所以取[0][0]。这里的坑是:多音字默认取第一个读音不一定对,比如“行”在“行行重行行”里有两种读法,押韵判断就会歪。解决办法是维护一个多音字例外表,后面避坑章细说。

如果你不想引入拼音库,也可以直接维护一个“韵母映射表”,把“a, ia, ua”归到一组,把“an, ian, uan”归到一组,效果基本一样。但用拼音库的优点是能处理生僻字的拼音,缺点是多音字要自己兜底。我这里用了strict=False,它解决的是“儿化音”问题——有些场景会把“这儿”读作“zher”,在古诗接龙里不需要这种处理,所以关掉反而更干净。

用这个函数测一下经典对子:“举头望明月”的“月”韵母是ue,“月是故乡明”的“明”韵母是ing,它俩不押韵,只能靠首字“月”完成字接。而“花间一壶酒”接到“酒酣胸胆尚开张”,“酒”和“张”在普通话里韵母不同,但在平水韵里属于同一韵部。这说明拼音库方案偏向现代读音,对古音押韵支持有限。如果你的诗歌库里有大量近体诗,建议同时维护一份平水韵的韵部映射,按“韵部相等”再做一次判断,两者取并集。

2.3 候选排序:编辑距离和语义相似度怎么搭配

字接和韵接只是筛选,真正决定接龙质量的是排序。我见过很多项目只用字符串匹配,结果接出来的句子把“白日依山尽”接成了“尽日问花花不语”,字面对上了,语义完全断裂。

我的做法是先按语义相似度粗排,再按编辑距离做惩罚。编辑距离在这里不是用来找错别字,而是用来惩罚“接得太远”的候选——如果上句是“花间一壶酒”,下句候选“酒酣胸胆尚开张”虽然首字对了,但“酒酣”和“花间”之间的字面跨度太大,编辑距离会把这句推到后面。反过来,“酒醒只在花前坐”与上句的字符重叠更多,结构也更像对仗,排序自然会靠前。

# 候选排序:结合编辑距离与语义相似度 import Levenshtein from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity def rank_candidates(last_sentence: str, candidates: list[str], weights: dict) -> list[tuple]: """ last_sentence: 上一句完整诗句 candidates: 通过字接和韵接筛选后的候选句列表 weights: {'semantic': 0.7, 'edit': 0.3} """ # 用TF-IDF计算语义向量,这里把每句诗当成一个短文档 vectorizer = TfidfVectorizer(token_pattern=r"\S", analyzer="char") all_texts = [last_sentence] + candidates tfidf_matrix = vectorizer.fit_transform(all_texts) semantic_scores = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:]).flatten() ranked = [] for idx, candidate in enumerate(candidates): edit_score = Levenshtein.distance(last_sentence, candidate) # 归一化:距离越小越好,所以用倒数做正向分 edit_norm = 1.0 / (1.0 + edit_score) total = weights['semantic'] * semantic_scores[idx] + weights['edit'] * edit_norm ranked.append((candidate, semantic_scores[idx], edit_norm, total)) # 按综合分倒序排 ranked.sort(key=lambda x: x[3], reverse=True) return ranked

这里有几个参数说明。token_pattern=r"\S"的意思是按字符切分,而不是按词切分,因为古诗分词容易出错。analyzer="char"表示用字符级TF-IDF,这也是处理短文本的常见做法。两个参数合在一起,实际效果就是把每个汉字当作一个特征,同时保留相邻字符的共现信息。

weights是留给你调的:如果想让接龙结果更“工整”,就把edit权重调高;如果更看重“意境”,就把semantic权重调高。我通常用0.6和0.4,但不同语料库最优值差异很大,后面会讲怎么用评测脚本来调。还有一点容易被忽略:Levenshtein.distance计算的是整句的编辑距离,两句长度不一致时,距离会被拉大。所以我在主流程里先过滤掉“字数不符”的候选,再进排序,这样编辑距离才真正反映“用字相近程度”,而不是“长度差”。

两个维度各自的边界也要说清楚。语义相似度擅长捕捉“用典”和“意象”关联,比如“月”和“故人”在古诗里经常一起出现,TF-IDF能学到这种共现关系。但它分辨不了“对仗”这种结构上的工整,因为对仗要求词性相对、平仄相对,这已经超出字符共现的范畴。编辑距离能捕捉字面重叠,但对“同义替换”无能为力,比如“欲穷千里目”和“更上一层楼”字面完全不同,编辑距离会给出很差的分,可这恰恰是古诗里常见的递进关系。所以两者必须结合,缺一个都会导致结果明显偏科。

这里再放一个三组样例的对比,方便你理解不同权重的效果。上句是“白日依山尽”,候选池里有三句:字接的“尽日问花花不语”、韵接的“尽在长安道”、意接的“千里目更穷”。语义权重调到0.8时,第三句会被顶到前面,因为“千里目”与“依山尽”在语义上有“视野”方面的关联;编辑权重调到0.8时,第一句会胜出,因为它和上句共用“日、尽”两个字。没有绝对正确的权重,只看你想要“意合”还是“字面工整”。

3. 搭建可运行的诗歌接龙项目:从数据清洗到主流程

3.1 环境准备:Python版本与依赖选择

这个项目我建议用Python 3.8以上,因为部分自然语言处理库在新版本上才有预编译包。第一件事是建虚拟环境,避免把系统Python搞乱。

python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate pip install jieba pypinyin python-Levenshtein scikit-learn zhconv

这些依赖各管一件事:jieba做分词备用,pypinyin负责拼音与韵母,python-Levenshtein提供编辑距离,scikit-learn提供TF-IDF和余弦相似度,zhconv做繁简转换。

如果你在Windows上安装python-Levenshtein遇到编译报错,可以用pip install rapidfuzz替代,接口基本一致。这里不建议用太新的jieba版本,有些版本的古诗分词效果反而退化,后面会说到。安装完成后可以用python -c "import jieba, pypinyin; print('ok')"验证依赖是否就位。

3.2 诗歌数据清洗:格式统一、繁简转换、去标点

数据来源一般是“唐诗三百首”或“全唐诗”的文本文件,格式五花八门。我常遇到的有三种:作者·诗名:诗句、诗名 作者 诗句、以及纯文本没分隔符。清洗脚本的核心是把所有诗句抽出来,统一成{"title": "", "author": "", "lines": []}的字典结构。

# 数据清洗:把原始文本转换成结构化数据 import re import json from zhconv import convert def clean_poem(raw_text: str) -> list[dict]: """ 入参是读取的原始文本,出参是结构化诗句列表。 这一步把繁体转简体、去标点、按句拆分。 """ # 先把常见全角标点换成空格,再按行拆分 # 注意:不要用逗号分隔,因为诗句内部可能有“对仗”用的逗号 raw_text = convert(raw_text, 'zh-cn') # 删除常见标点,保留汉字和分隔符 lines = re.split(r"[,。;!?\n]", raw_text) poems = [] for i, line in enumerate(lines): line = line.strip() # 过滤掉明显不是诗句的行:作者、标题、空行 if len(line) < 5 or len(line) > 7: continue # 这里简化处理:把奇数行当上句,偶数行当下一句,构成一组 if i % 2 == 1 and lines[i-1].strip(): poem_entry = { "title": f"poem_{i}", "author": "", "lines": [lines[i-1].strip(), line] } poems.append(poem_entry) return poems

这段代码有个粗糙但实用的点:按[,。;!?\n]切分后,通过长度过滤掉标题和作者行,再按“奇偶相邻”配对成上下句。注意我把中文标点放进正则表达式的字符类里,而不是直接替换成空字符串,因为有些诗句内部有停顿,直接删标点会把上下句粘在一起,导致押韵检测取错末字。

举例来说,“床前明月光,疑是地上霜”会被切分成两句,长度分别是5和5,符合过滤条件,配成一组。而标题“静夜思”长度是3,直接跳过。作者行“李白”长度是2,跳过。这样清洗出来的数据结构很干净,缺点是遇到“五言律诗”这种八句诗,配对逻辑会把第二联和第三联交叉配对,产生一些奇怪的组合。你可以在清洗后人工抽查一部分数据,把明显错误的配对先清理掉。

清洗完的数据建议保存成JSON或CSV,后续每次跑接龙直接加载,不用再重复清洗。数据量不大,几百首诗的清洗脚本运行时间通常在1秒以内。存放时用ensure_ascii=False,这样打开JSON直接能看到中文,方便排查清洗问题。

3.3 接龙主函数:把筛选、排序、返回串成一条流水线

主函数是项目的门面。它接收一个上句,从数据里筛出首字匹配的候选,再做押韵过滤和排序,最后返回前五条。这里我用了一个“宽松”设计:押韵不是硬条件,而是加分项,这样可以在候选集很小的时候仍然有结果可返回。

# 诗歌接龙主流程 from typing import Optional def poem_chain( input_sentence: str, poem_dataset: list[dict], top_k: int = 5, require_rhyme: bool = False ) -> list[str]: """ input_sentence: 用户给的上句,例如“花间一壶酒” poem_dataset: 清洗后的诗歌数据 top_k: 返回前几条结果 require_rhyme: 是否强制押韵,False表示押韵只作为加分项 """ # 1. 提取上句末字 input_chars = list(input_sentence.replace(" ", "")) if not input_chars: raise ValueError("输入不能为空") last_char = input_chars[-1] # 2. 从数据集中找出所有以该字开头的诗句 matched_lines = [] for poem in poem_dataset: for line in poem["lines"]: line = line.strip() if line.startswith(last_char): matched_lines.append(line) if not matched_lines: return [] # 3. 押韵过滤:如果开启强制押韵,只保留同韵母的候选 if require_rhyme: matched_lines = [ line for line in matched_lines if is_rhyme(last_char, line[-1]) ] # 4. 用语义相似度排序 final_results = rank_candidates(input_sentence, matched_lines, weights={'semantic': 0.6, 'edit': 0.4}) return [item[0] for item in final_results[:top_k]]

require_rhyme是调试用开关。刚跑通时我建议设为False,先把流程跑通,再开启押韵约束看效果。这个参数在生产里对应一个“严格模式”,给到前端做难度分档。

这里有一个容易踩空的点:如果候选集合里既没有字接、也没有押韵候选,函数会直接返回空列表。真实场景中,用户输入的上句末字可能是“啊、哦”这种语气词,古诗库里根本没有以它们开头的句子。这种情况下不要硬凑结果,返回空列表比返回一句胡拼乱造的诗更诚实——你可以让上层提示用户换个上句。

3.4 运行效果:命令行调用与输出说明

主函数写好后,用一个命令行脚本封装,方便测试。我用argparse接收输入和参数,这样不用改代码就能试不同配置。

python chain_cli.py --input "花间一壶酒" --top-k 5

假设输出是:酒醒只在花前坐、酒酣胸胆尚开张、酒入愁肠化作相思泪、酒债寻常行处有、酒楼横笛声初发。这几个候选都满足“首字为酒”,前三个在押韵和语义上都比较顺,最后一个偏弱。

你看到结果后要做的事不是调权重,而是人工标注:把“上句-下句”配对存成一个评估集,为后面调试打分脚本做准备。这一步很多人跳过,导致后面调参数全靠感觉,最后说不清哪个版本更好。

# 保存评估集示例 echo "花间一壶酒 酒醒只在花前坐 1" >> eval_set.tsv echo "花间一壶酒 酒酣胸胆尚开张 1" >> eval_set.tsv echo "花间一壶酒 酒楼横笛声初发 0" >> eval_set.tsv

字段分别是上句、下句、是否可接受。这里的人工标注“1”不是模型预测结果,而是你作为人的判断,它是评测脚本的基准答案。标注的时候要给自己定个规矩:只看“字面上能不能接、内容上违不违和”,不要因为某句诗出自名家就闭眼打1。我早期就犯过这个毛病,把“酒入愁肠化作相思泪”这种名词堆叠的句子标成可接受,后来发现它和“花间一壶酒”的意境其实差得很远。

4. 诗歌接龙避坑指南:数据、编码与NLP库的五个常见问题

4.1 现象:分词工具把古诗切得稀碎,导致字符处理出错

有次我在预处理阶段用了jieba分词,结果“长风破浪会有时”被切成了“长风/破浪/会有/时”,后面的字符级接龙逻辑直接取到“浪”而不是“时”。

原因:古诗没有现代汉语的词汇边界,jieba的训练语料是新闻和百科,它会强行把“破浪”当成一个词,把“会有”当成一个词。这不是jieba的bug,是语料类型不匹配。

解决:在这个项目里,所有接龙逻辑都改成按字符处理,不依赖分词器。分词只在做语义特征时可选使用,而且我会加上自定义词典,把“长风破浪”“金樽清酒”这类古诗常见词组加进去,jieba在切分时就会保留完整片段。

# 加载古诗自定义词典 import jieba def load_poem_dict(dict_file: str): """把古诗常用词组逐行写入词典""" with open(dict_file, "r", encoding="utf-8") as f: for line in f: word = line.strip().split()[0] jieba.add_word(word)

这个词典文件里每一行放一个词组即可,例如“长风破浪”“金樽清酒”“月明星稀”。加了词典之后,jieba在切分时会把整个词组当成一个词,字符提取逻辑就不会被拆散。注意词典只需要覆盖高频词组,不用贪多,几十条就够用。

4.2 现象:繁体字没转换,押韵判断全部失效

有次我发现“青云”的“云”和“白云”的“云”明明是一个字,但押韵检测结果却不一致。排查后发现问题出在原始数据用的是繁体“雲”,简体转换时没有统一。

原因:不同来源的诗歌库,有的保留繁体字形,有的混用异体字。“雲”和“云”在拼音库中读音相同,但字形不同,导致startswith首字匹配和is_rhyme韵母匹配都失败。

解决:数据清洗时强制走一遍zhconv的zh-cn转换,并且清洗后把末字和首字单独存成索引字段。还有一个更隐蔽的点:要考虑 Unicode 的 NFKC 归一化,有些文本里“为”是 U+70BA,有些是 U+4E3A,统一后才能匹配。

# 统一繁体汉字和异体字 import unicodedata from zhconv import convert def normalize_char(char: str) -> str: """先转简体,再做Unicode正规化""" char = convert(char, 'zh-cn') char = unicodedata.normalize('NFKC', char) return char

我把这个normalize_char用在两个地方:一个是清洗数据时逐字处理,另一个是接龙主函数接收用户输入时。因为用户输入的上句也可能包含繁体字,比如有人习惯写“雲”而不是“云”。不统一的话,即使库里已经转成简体,也会因为用户输入是繁体而匹配不上。

4.3 现象:Windows下读取诗歌文件直接乱码或崩溃

在Windows上跑数据清洗时,经常出现UnicodeDecodeError: 'gbk' codec can't decode byte 0xba。这是因为很多诗歌文本文件是UTF-8编码,而Python在Windows下的默认编码是GBK。

原因:文件读取时没有显式指定编码,Python按系统默认编码打开文件。GBK无法解析UTF-8编码的中文字节序列。

解决:所有文件读写都显式加上encoding="utf-8",同时打开文件时加newline=""防止Windows把\n转换成\r\n破坏按行拆分逻辑。

# 正确打开诗歌文件的姿势 with open("poems.txt", "r", encoding="utf-8", newline="") as f: raw_text = f.read() # 写入清洗结果 with open("poems_clean.json", "w", encoding="utf-8", newline="") as f: json.dump(poems, f, ensure_ascii=False, indent=2)

ensure_ascii=False很重要,它保证JSON文件里存的是中文而不是\uXXXX转义序列,方便人工检查清洗是否到位。indent=2是让JSON有换行缩进,在编辑器里直接可读。如果你用Pandas或Excel做后续分析,也可以把清洗结果导出成CSV,但要注意CSV用Excel打开时最好带UTF-8 BOM,否则中文表头会乱。

4.4 现象:语义相似度模型把“意境”打分打反了

我最初用jieba分词后做TF-IDF,结果“劝君更尽一杯酒”的候选里,“酒逢知己千杯少”排得很靠前,但“西出阳关无故人”反而排到最后。

原因:字符级TF-IDF对短诗句效果好,词级TF-IDF会因为分词不准而丢失关键字符。“故人”被切成一个词,但整句里只有两个字能贡献特征,而“酒”字字符出现频率高,反而把“酒杯”类候选顶了上来。

解决:把TfidfVectorizer的analyzer从word改成char,并设置ngram_range=(1, 2),让“故人”作为双字特征保留,同时在排序里增加编辑距离惩罚项,压制纯字面强相关但语义断裂的候选。

# 改进后的向量化参数 vectorizer = TfidfVectorizer(analyzer="char", ngram_range=(1, 2))

这个改动之后,结果明显改善。如果你接龙的语料跨度大,比如全唐诗和宋词混用,建议再加一个“朝代一致性”的过滤,不让“词”和“诗”错位接龙。我踩过这个坑:用宋词接唐诗,接出来的对仗极其诡异,“大江东去”接到“去留肝胆两昆仑”,单看字面对上了,但文体和情感完全是两个世界。

4.5 现象:多音字让押韵检测不稳定

“行到水穷处”的“行”读xing,但在“客舍青青柳色新”这个环境里,有人会误读成hang。pypinyin默认取第一读音,有时取的是注释音而不是吟诵音。

原因:古诗中为了押韵,有些字会读古音或变调,现代拼音库无法感知上下文韵律。

解决:维护一个自定义读音小表,对“行、为、将、重、华、令、思”这类多音字人工指定读法。把这个表放在配置文件中,不要写死在代码里,方便换语料时调整。

# 多音字读音表 special_pron = { "行": "xing", # 在诗句中常读xing,但“漕行”里要读hang "为": "wei", "将": "jiang", }

这个表不需要大,覆盖出现频率前10到20的多音字就够了,剩下的容忍错误即可。实际用下来,“行、为、将、重、思”这五个字能解决90%的押韵误判。你可以在get_rhyme_group函数开头加一行查表逻辑:

def get_rhyme_group(char: str) -> str: if char in special_pron: char_pinyin = special_pron[char] + "1" # 补全声调,方便统一解析 finals = pinyin(char_pinyin, style=Style.FINALS, strict=False) else: finals = pinyin(char, style=Style.FINALS, strict=False) # 后续逻辑不变

注意special_pron里存的是拼音字符串,传给pinyin函数时会按“带声调的拼音文本”处理。这样既绕开了多音字误判,又不用重写拼音库的接口。

5. 从能跑到好用:把接龙结果做成可配置的规则引擎

5.1 三层打分的权重怎么设

一个能跑的接龙脚本,和一个好用的接龙功能,差别就在“可配置性”。我把打分离成三个维度:

  • 字面分:首字匹配、末字押韵、编辑距离。
  • 格律分:平仄交替、上下句字数一致。
  • 语义分:TF-IDF余弦相似度。

三个维度权重在配置文件里用字典维护,每次调完参数后跑评测集对比。常见做法是把权重放进YAML或JSON文件,而不是改源码。

{ "weight": { "char": 0.3, "rhyme": 0.2, "semantic": 0.3, "tone": 0.2 }, "require_rhyme": false, "max_candidates": 20 }

这个配置文件把四个分数的比重写得很清楚。char是字接匹配的基础分,rhyme是押韵分,semantic是语义分,tone是平仄分。四个加起来等于1.0,方便你在调整时保持总量不变——每次只移动一个维度的权重,其他三个成比例缩放,这样评测结果的变化就能归因到那一个维度上。

我见过有人把权重设成“字面0.1、语义0.9”,结果接龙出来的句子语义很顺,但完全不像古诗,因为没有字数和平仄约束。也有人把“平仄”权重设到0.5,结果为了满足平仄相对,选出一些语义完全不通的句子。权重不是单纯的技术参数,它直接反映了你希望这个项目“像什么”。如果是做游戏玩法,语义权重要高一些,让玩家觉得“接得巧”;如果是做诗词学习工具,平仄和押韵权重要高,让接出来的句子符合格律。

5.2 平仄判断的简化实现

平仄是规则引擎里最容易写过头的地方。完整平水韵太难,我一般用普通话四声做近似:一二声算平,三四声算仄。这不算严谨,但能让规则引擎跑起来,也足够过滤掉明显不工整的候选。

# 简化平仄判断 from pypinyin import pinyin, Style def get_tone(char: str) -> int: """返回声调数字,1-4,轻声返回5""" p = pinyin(char, style=Style.TONE3, strict=False)[0][0] # TONE3格式:lou2,取最后一位数字 tone = int(p[-1]) if p[-1].isdigit() else 5 return tone def is_antithetical(first_line: str, second_line: str) -> bool: """检查两句同一位置是否平仄相对""" tones1 = [get_tone(c) for c in first_line] tones2 = [get_tone(c) for c in second_line] if len(tones1) != len(tones2): return False # 统计平仄相对的比例,一般要求超过一半 opposite = sum(1 for t1, t2 in zip(tones1, tones2) if (t1 <= 2 and t2 >= 3) or (t1 >= 3 and t2 <= 2)) return opposite / len(tones1) > 0.5

这个判断给格律维度打分用。需要提醒的是,“平仄相对”在律诗里是上下联的要求,接龙场景里不需要这么严格,所以用比例而不是全部满足。get_tone返回的是数字,1到4对应四声,5对应轻声。Style.TONE3输出的格式类似lou2、guang1,我把末尾的声调数字抽出来,转成int,逻辑很直接。

实际使用中,我发现轻声字的处理是个盲区。很多语气词“吧、呢、呀、啊”读轻声,get_tone返回5,在平仄判断里既不算平也不算仄。我通常把5归类为平,因为古诗里的虚词经常出现在平声位置。你要不要这样归类,取决于你的语料里虚词占比。占比高的话,把轻声当平不会出大问题;占比低则无所谓。

5.3 难度档位与候选池大小的关系

规则引擎的另一个实用功能是“难度档位”。简单档只做字接,不用押韵;中等档开启押韵;困难档同时要求押韵和平仄相对。候选池大小也要联动:池子越大,排序越能挑出好句子,但速度会下降。

# 难度档位配置 DIFFICULTY_CONFIG = { "easy": {"require_rhyme": False, "require_tone": False, "pool_size": 50}, "medium": {"require_rhyme": True, "require_tone": False, "pool_size": 100}, "hard": {"require_rhyme": True, "require_tone": True, "pool_size": 200}, }

pool_size不是最终的候选数量,而是从库里捞出来的“粗筛数量”。粗筛之后,再按难度做过滤:简单档直接排序,中等档先押韵过滤再排序,困难档还要过平仄比例。这个设计的出发点是:候选池越大,越可能捞到冷门但合适的句子,但排序阶段的计算量也会线性增长。

实测数据:候选池从50扩到200,押韵过滤后剩10到20条,排序时间从几毫秒涨到几十毫秒,对本地实验完全无感。所以不要为了省时间把候选池卡死,扩到500都行。唯一要注意的是内存,如果一次加载几千首诗,每句都做字符级TF-IDF,向量矩阵会稍微占用内存,但几十MB以内没问题。

难度档位还要配合一个“保底策略”:困难档过滤后如果候选不足3条,自动降级到中等档。不然玩家在游戏里会频繁遇到“接不上来”的挫败感。我在规则引擎里加了一个fallback参数,控制是否允许降级:

def generate_candidates(input_sentence, difficulty="medium", fallback=True): cfg = DIFFICULTY_CONFIG[difficulty] candidates = fetch_by_first_char(input_sentence, cfg["pool_size"]) if cfg["require_rhyme"]: candidates = filter_rhyme(candidates) if len(candidates) < 3 and fallback: candidates = generate_candidates(input_sentence, "easy", fallback=False) if cfg["require_tone"]: candidates = filter_tone(candidates) if len(candidates) < 3 and fallback: candidates = generate_candidates(input_sentence, "medium", fallback=False) return candidates

用递归做降级虽然看起来不太优雅,但在这个场景下逻辑最清晰:每一层都在赌“本级筛选后还有足够候选”,没有就直接降到下一级。注意fallback=False防止递归里再次降级,形成死循环。实际跑下来,困难档的降级概率在30%左右,也就是说三句里可能有接近一句需要降级处理。这个数字不丢人,它说明你的格律过滤是有效的。

6. 验证接龙质量:一个可复用的评测脚本

6.1 评测脚本与人工标注的闭环

接龙项目最怕“自我感觉良好”。我后来养成了一个习惯:每次调参数,先跑同一个评测集,看Top5命中率。评测集不需要大,50组人工标注足够。

# 评测脚本:计算Top-5命中率 def evaluate(chain_function, eval_file: str, top_k: int = 5) -> float: """ eval_file内容格式: 上句\t下句\t是否可接受(0/1) 返回命中率 = 可接受的接龙结果出现在Top-k中的比例 """ hit = 0 total = 0 with open(eval_file, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split("\t") if len(parts) != 3: continue input_sentence, target_sentence, label = parts if label != "1": continue total += 1 results = chain_function(input_sentence, top_k=top_k) if target_sentence in results: hit += 1 return hit / total if total > 0 else 0.0

评测集里只统计“人认为可接受”的样本,因为不可接受样本的意义是观察模型是否把垃圾排到了前面,但你无法从“垃圾没被选中”里得到有效信号。

6.2 我的调参流程

我把调参固定成三步:

  1. 先跑一遍默认权重,记录Top-5命中率。
  2. 把权重往“语义”偏0.1,再跑,对比命中率。
  3. 选命中率高的一组,人工看10条结果,确认没有明显硬伤。

从那以后我每次调参都强制自己走一遍这个流程,不再凭感觉改权重。评测脚本跑一次不到10秒,但它能逼你把“我觉得”变成“数据显示”。希望这个习惯对你也有用。

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

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

百万行CSV打不开?流式加载与DuckDB实战指南

简介&#xff1a;这是一款面向数据分析人员、程序员及日常办公用户的CSV文件编辑工具&#xff0c;针对需要频繁查看、修改表格数据却不想依赖Excel的场景&#xff0c;提供类似电子表格的直观操作体验&#xff0c;支持单元格增删改、排序、过滤、查找替换与格式转换等常见需求。…

作者头像 李华
网站建设 2026/10/8 2:20:17

ChatGLM3-6B本地部署实战:从zip包到稳定推理的完整链路

简介&#xff1a;本资源是面向AI开发者与大模型实践者的ChatGLM3-6B中文大语言模型轻量部署包&#xff0c;聚焦知识库问答系统构建场景&#xff0c;适用于具备PyTorch基础和模型微调经验的中高级学习者。压缩包共53个文件&#xff0c;包含7个.bin与7个.safetensors权重文件&…

作者头像 李华
网站建设 2026/10/8 2:19:28

WPF 接入 D3D 实现高性能 3D 动画看板:D3DImage 共享纹理全解析

简介&#xff1a;WPF D3D demo是一份面向WPF开发者的Direct3D视频渲染示例工程&#xff0c;重点演示在WPF界面中高效呈现YUV格式视频。工程整合WPF、YUV颜色空间与D3D硬件加速&#xff0c;通过自定义渲染类将YUV数据转换为D3D纹理并绘制到WPF可视对象&#xff0c;同时利用后台线…

作者头像 李华
网站建设 2026/10/8 2:19:12

Netty物联网网关实战:万级长连接、多协议共存与粘包容错

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

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

WinForm嵌入谷歌内核:CefGlue实战与踩坑指南

简介&#xff1a;C#/.NET开发者在WinForm应用中集成浏览器功能的实用方案&#xff0c;借助Xilium.CefGlue对CEF的C#封装&#xff0c;可让桌面程序直接获得Chromium内核的渲染能力与兼容性表现。资源共115个文件、7z压缩后约126.63MB&#xff0c;以dll运行库及pak资源文件为主体…

作者头像 李华
网站建设 2026/10/8 2:16:23

经济统计学论文自救指南:AI 工具那么多,到底该在哪一步用?

先说一个很典型的场景&#xff1a;你是经济学 / 统计学 / 经济统计学专业的学生&#xff0c;毕业论文要做一篇类似《数字经济发展对城乡居民消费差距的影响——基于省级面板数据的实证分析》的文章。 这不是单纯“写一篇作文”&#xff0c;而是要交出一套完整成果&#xff1a;…

作者头像 李华