尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Python古诗词接龙:检索、打分与N-gram兜底实践
简介这份项目包面向希望用Python完成自然语言处理实战的初中级开发者围绕“诗歌接龙”场景整合文本分词、拼音转换、韵脚匹配与接龙算法等内容可用于课程设计、练手项目或NLP流程入门。压缩包共17个文件约5.86MB包含4个py源代码文件、4个xml配置/数据文件以及pk、dat模型数据、docx说明文档、txt与exe等辅助文件源码、数据和运行材料相对齐全。已有499人学习下载说明该案例具备一定参考价值。读者可结合说明文档梳理项目背景与功能模块阅读源码理解pypinyin、jieba等工具在音韵匹配和文本处理中的用法并观察基于规则或统计方法的接龙实现思路从而把NLP基础知识落到可运行的小项目里。1. 诗歌接龙这个需求用 Python 做起来比想象中讲究一个做课件工具的朋友把压缩包丢过来需求一句话输入一句诗自动往下接三句。我第一反应是这也太简单了python 里把古诗词切成句子建个「首字 → 句子」的字典拿上一句的最后一个字去查不就行了。真跑起来才发现NLP 里最难的一环从来不是「能不能查到」而是「查到的像不像」。严格按字接三步之内必断放宽到按韵接又接出一堆前言不搭后语的东西。这个诗歌接龙项目把整条链路拆成了预处理、索引、检索、生成、打分五段每段都留了可调参数属于「跑通十分钟、调好一个月」的类型。下面按我拆包的顺序从目录结构一路讲到怎么把它改成藏头诗生成器。2. 项目骨架与技术选型先看它凭什么只用 Python 就能跑2.1 解开压缩包后看到的目录结构先在干净目录里解压别直接扔进你那个装了几十个依赖的老环境后面出问题不好定位。解开后顶层目录大致是这样poem_relay/ ├── app/ │ ├── __init__.py │ ├── engine.py # 接龙主流程对外唯一入口 │ ├── matcher.py # 检索层负责「按字/音/韵」找候选 │ ├── generator.py # 字符级 N-gram检索不到时的兜底 │ ├── scorer.py # 候选打分与重排 │ ├── phonology.py # 平仄、韵部、同音字 │ └── cli.py # 命令行入口 ├── scripts/ │ ├── normalize.py # 原始语料清洗成 jsonl │ └── build_index.py # jsonl 灌进 sqlite 并建索引 ├── data/ │ ├── raw/ # 放原始 txt自己往里塞 │ └── index/ # 生成物lines.jsonl、poems.db ├── configs/ │ └── default.yaml ├── tests/ └── requirements.txt这个分层不是摆样子它对应一条非常明确的数据流data/raw里的原始文本被normalize.py洗成一行一句的lines.jsonlbuild_index.py再把它连同算好的韵部、平仄串一起灌进poems.db。运行期engine.py只碰poems.db完全不碰原始文本。好处是原始语料你可以随时换、随时加重建索引就行运行侧代码一行不用改。data/index/是生成物不该进版本库。我第一次拆的时候没注意lines.jsonl有几十兆poems.db更大git 一提交就后悔了。建议解压完第一件事就是看一眼有没有.gitignore没有就自己补上data/index/和__pycache__/。技术选型上这个项目刻意保持得很轻语料存储用 sqlite标准库自带不用起服务单文件好分发拼音和平仄用pypinyinHTTP 服务用FastAPI配置用PyYAML。没有 transformers没有 torch也没有向量库。这个选择在小体量接龙场景下是对的——你要的是「一定接得上、接得像」不是「生成从没见过的新句子」。真要上大模型那是最后一章改造的事。2.2 依赖清单与首次启动requirements.txt里的核心就四个pypinyin fastapi uvicorn pyyaml装依赖时我一般先建虚拟环境再装避免跟系统 Python 打架python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install -r requirements.txt这里不写死版本号是有意的pypinyin的Style.FINALS在不同版本上对ü的输出格式略有差别锁死版本反而容易在你机器上装不上。真遇到韵部算错先怀疑版本差异再看代码。启动顺序是固定的不能跳步先清洗语料再建索引最后才跑 CLI。因为engine.py启动时会直接sqlite3.connect(data/index/poems.db)库文件不存在就直接抛异常报错信息还不一定清楚。python scripts/normalize.py # data/raw/*.txt - data/index/lines.jsonl python scripts/build_index.py # lines.jsonl - data/index/poems.db python -m app.cli --start 春眠不觉晓 --steps 3正常跑通的话终端会连着打印起句和接出来的三句。如果只打印了起句就结束说明检索层一条候选都没找到先跳到第 5 章看命中率那一节。如果直接报no such table: lines那是索引没建成功回上一步看build_index.py的输出。2.3 语料从哪来、索引怎么落盘项目本身不带语料data/raw/是空的这点得说清楚很多人解压完发现跑不起来就是因为这个。常见做法是找一份公开的古诗词合集文本按「一行一首」或者「一行一句」的格式丢进去一行一首还是多行一首都不影响normalize.py里做了兼容它只按标点切句不关心原始换行。清洗逻辑在scripts/normalize.py里核心三步去噪、切句、过滤。# scripts/normalize.py节选 import re from pathlib import Path NOISE_RE re.compile(r[\u200b-\u200f\ufeff\u3000\s]) SENT_SPLIT_RE re.compile(r[。、,.!?;:]) HANZI_RE re.compile(r^[\u4e00-\u9fff]$) def iter_lines(raw_text: str): raw_text NOISE_RE.sub(, raw_text) for seg in SENT_SPLIT_RE.split(raw_text): seg seg.strip() # 只留纯汉字片段拼音注释、序号、书名号全在这一步被丢掉 if not HANZI_RE.match(seg): continue # 以五言、七言为主但允许四言到十六言的宽区间别一刀切死 if not (4 len(seg) 16): continue yield segNOISE_RE里那几个 Unicode 区间是有讲究的\ufeff是 BOM\u200b-\u200f是零宽字符\u3000是全角空格。这些东西肉眼看不见但会让seg[0]拿到一个非汉字整个接龙就断在第一步。SENT_SPLIT_RE里中文标点和英文标点都列了因为语料来源往往混杂。长度区间4 len(seg) 16是经验值。限得太严比如只留五言七言会丢掉大量词和古风句子候选池一小接龙三步就断放得太宽又会混进大段散文接出来的东西语感全无。我一般先用这个宽区间跑一遍看看库里有多少行再决定要不要收紧。建索引这边用 sqlite表结构很朴素但每一列都有用途# scripts/build_index.py节选 conn.execute( CREATE TABLE lines( id INTEGER PRIMARY KEY, text TEXT NOT NULL, head TEXT NOT NULL, -- 首字接龙入口 tail TEXT NOT NULL, -- 尾字接龙出口 pinyin TEXT, -- 首字拼音同音回退用 rhyme TEXT, -- 尾字韵部押韵判断用 pattern TEXT, -- 整句平仄串如 PPZZP title TEXT, author TEXT )) conn.execute(CREATE INDEX idx_head ON lines(head)) conn.execute(CREATE INDEX idx_tail ON lines(tail)) conn.execute(CREATE INDEX idx_pinyin ON lines(pinyin)) conn.execute(CREATE INDEX idx_rhyme ON lines(rhyme))head和pinyin两个索引是给检索层用的rhyme索引是给「按韵回退」和打分用的pattern使用频率最低但做平仄校验时不必再算一遍拼音。索引建完记得VACUUM一次sqlite 删删改改之后文件会虚胖VACUUM能把体积压回去三成左右。3. 核心链路拆解一句诗从进入到接出下一句中间过了几道手3.1 预处理清洗、切句、抽尾字与韵部预处理除了切句还有一件容易被忽略的事把每个字的读音信息提前算好存起来。这一步放在建索引阶段而不是运行期是因为pypinyin单次调用虽然快但一句诗七八个字、候选池上万句运行期现算会把响应时间从毫秒级拖到秒级。音韵模块单独放在app/phonology.py两个函数加两张表。第一张是韵母到韵部的归并表# app/phonology.py节选 from functools import lru_cache from pypinyin import lazy_pinyin, Style RHYME_GROUPS { a: a, ia: a, ua: a, o: o, e: o, uo: o, ie: o, ve: o, ai: ai, uai: ai, ei: ei, ui: ei, ao: ao, iao: ao, ou: ou, iu: ou, an: an, ian: an, uan: an, van: an, en: en, in: en, un: en, vn: en, ang: ang, iang: ang, uang: ang, eng: eng, ing: eng, ong: eng, iong: eng, } lru_cache(maxsize20000) def rhyme_of(char: str) - str: finals lazy_pinyin(char, styleStyle.FINALS, strictTrue)[0] finals finals.replace(ü, v).replace(u:, v) return RHYME_GROUPS.get(finals, finals)lru_cache那行是关键。汉字总共几千个缓存命中率极高加了之后建索引的时间能砍掉一半以上。strictTrue是必须的不加的话y、w开头的音节不会被还原成i、u月和夜这种本该同韵的字会被判成不同韵部。第二张表是平仄判定用声调数字做粗判lru_cache(maxsize20000) def tone_of(char: str) - str: P平 Z仄 ?存疑非汉字或取音失败 if not (\u4e00 char \u9fff): return ? py lazy_pinyin(char, styleStyle.TONE3, neutral_tone_with_fiveTrue)[0] digit py[-1] if digit in 12: return P if digit in 345: return Z return ?一二声归平、三四声归仄这是最粗的规则古音里的入声字早就分不出去了。所以项目里平仄只作为打分的加分项不作为硬约束——把它当硬约束候选池会瞬间塌缩到个位数。返回?而不是抛异常是因为总有生僻字取不到音这时候宁可不加分也别让整个流程挂掉。3.2 检索层两张倒排表撑起「字接字」检索层在app/matcher.py对外就三个方法对应三级回退。第一级最直接# app/matcher.py节选 class Matcher: def __init__(self, db_path: str): # check_same_threadFalse 是为 FastAPI 多线程准备的后面第 5 章会讲坑 self.conn sqlite3.connect(db_path, check_same_threadFalse) self.conn.row_factory sqlite3.Row self._lock threading.Lock() def by_head(self, char: str, limit: int 40): with self._lock: cur self.conn.execute( SELECT * FROM lines WHERE head ? ORDER BY RANDOM() LIMIT ?, (char, limit), ) rows cur.fetchall() return [self._to_cand(r, exact) for r in rows]ORDER BY RANDOM() LIMIT ?这一句看着随意其实决定了接龙体感。如果按 id 排序取前 40 条同样的输入永远得到同样的输出多接几次就腻了。随机取样保证同一个起句每次接出来的结果有差异但差异又限定在 40 条这个池子里不会跑飞。第二级是同音回退。古诗词里以字接字的天然概率其实不高尤其是尾字是「风」「中」「人」这类高频字时候选多到溢出而尾字是「醉」「瑟」这类字时严格按首字查经常一条都没有。def by_pinyin(self, char: str, limit: int 40): py lazy_pinyin(char, styleStyle.NORMAL)[0] with self._lock: cur self.conn.execute( SELECT * FROM lines WHERE pinyin ? ORDER BY RANDOM() LIMIT ?, (py, limit), ) rows cur.fetchall() return [self._to_cand(r, homophone) for r in rows]用Style.NORMAL取不带声调的拼音这样「舟」可以接到「州」「周」开头的句子。回退层级用一个matched_by字段标出来后面打分时同级回退要扣分——同音接上总比同韵接上更像回事。第三级是同韵回退问的是「上一句尾字的韵部有哪些句子的尾字也在这个韵部」def by_rhyme(self, rhyme: str, limit: int 40): with self._lock: cur self.conn.execute( SELECT * FROM lines WHERE rhyme ? ORDER BY RANDOM() LIMIT ?, (rhyme, limit), ) rows cur.fetchall() return [self._to_cand(r, rhyme) for r in rows]注意这里查的是尾字韵部不是首字。意思是「接不上字那我至少让新句子跟上一句押韵」读起来还有点诗的骨架。三级回退的顺序不能乱字 → 音 → 韵 → 生成每降一级风格一致性就差一档但接得上的概率高一大截。3.3 生成层字符级 N-gram 兜住检索不到的情况三级回退全空的时候语料小、生僻尾字、或者你只灌了几百首诗就得靠生成层。app/generator.py里是一个字符级 N-gram默认三元# app/generator.py节选 class CharNGram: def __init__(self, n: int 3): self.n n self.model defaultdict(Counter) def fit(self, lines): n self.n for line in lines: padded ^ * (n - 1) line $ for i in range(len(padded) - n 1): gram padded[i:i n - 1] nxt padded[i n - 1] self.model[gram][nxt] 1 return self def sample_next(self, prefix: str, temperature: float 1.0): gram prefix[-(self.n - 1):] counter self.model.get(gram) if not counter: return None chars list(counter) # 温度越低越保守越高越发散1/temperature 是常见的退火写法 weights [counter[c] ** (1.0 / max(temperature, 0.05)) for c in chars] return random.choices(chars, weightsweights, k1)[0] def gen_line(self, start: str, max_len: int 7, temperature: float 1.1): out start while len(out) max_len: nxt self.sample_next(out, temperature) if nxt is None or nxt $: break out nxt return out几个细节值得说。句首句尾补^和$是标准做法不补的话模型永远学不会「一句诗怎么开头、怎么收尾」生成出来的句子会一直延伸下去。temperature通过** (1/temperature)作用在计数上温度取 1.0 是原始分布取 0.7 会明显更保守、更像原句取 1.5 就容易出怪句子。默认给 1.1 是个折中值。生成层只在检索全空时启用而且生成出来的句子还要过一遍打分层分数太低会被丢弃、直接结束接龙。这是有意的设计宁可在第三句停下也别硬凑一句读不通的。我在自己的环境里试过把生成阈值调低结果接龙能接十句但第七句开始基本是乱码血泪经验。3.4 打分重排把「像诗」拆成可计算的几项候选拿到手是一堆无序的句子得排个序。app/scorer.py里的权重用一个 dataclass 装方便配置覆盖# app/scorer.py节选 dataclass class Weights: link: float 3.0 # 首字严格等于上一句尾字 rhyme: float 2.0 # 与上一句尾字同韵部 tone: float 1.0 # 首字与上一句尾字平仄相对 length: float 0.8 # 与上一句字数接近 repeat: float -6.0 # 命中已用过的句子重罚 overlap: float -0.4 # 与最近两句的字重叠轻罚 def score(cand, prev: str, used: list[str], w: Weights) - float: s 0.0 if cand.head prev[-1]: s w.link if cand.rhyme rhyme_of(prev[-1]): s w.rhyme if tone_of(cand.head) ! tone_of(prev[-1]) and ? not in (tone_of(cand.head), tone_of(prev[-1])): s w.tone if abs(len(cand.text) - len(prev)) 1: s w.length if cand.text in used: s w.repeat recent set(.join(used[-2:])) s w.overlap * len(set(cand.text) recent) return srepeat给到 -6.0 是有原因的link才 3.0重复惩罚必须远大于任何正向加分否则一条既接得上又押韵的重复句照样能排第一你会看到同一句诗反复出现。overlap用最近两句的字做交集是为了压住「用字雷同」——不压的话接出来的四句可能全在「月、风、花、夜」这几个字里打转。重排完不是直接取第一名而是在 top-5 里随机取一个ranked sorted(cands, keylambda c: score(c, prev, used, w), reverseTrue) top [c for c in ranked[:5] if c.text not in used] or ranked[:1] pick random.choice(top)这个 top-k 随机采样是整个项目里最便宜也最有效的一个技巧。取 top-1 会变成确定性输出娱乐性归零取 top-20 又会经常抽到明显不搭的句子。5 是试出来的手感值。4. 引擎接口与可调参数怎么把它当库用、当服务用4.1 引擎入口relay 方法签名与返回结构app/engine.py是整个项目对外唯一需要关心的类构造只要一个 db 路径# app/engine.py节选 class PoemRelay: def __init__(self, db_path: str, weights: Weights | None None, gen_temperature: float 1.1): self.matcher Matcher(db_path) self.weights weights or Weights() self.gen CharNGram(n3) self.gen_temperature gen_temperature self._gen_ready False def relay(self, start: str, steps: int 3, allow_generate: bool True) - list[str]: used [start.strip()] for _ in range(steps): prev used[-1] cands self.matcher.by_head(prev[-1]) \ or self.matcher.by_pinyin(prev[-1]) \ or self.matcher.by_rhyme(rhyme_of(prev[-1])) if not cands and allow_generate: cands self._fallback_generate(prev) if not cands: break ranked sorted(cands, keylambda c: score(c, prev, used, self.weights), reverseTrue) top [c for c in ranked[:5] if c.text not in used] or ranked[:1] used.append(random.choice(top).text) return used返回的是一个list[str]第一个元素就是你传进去的起句后面是接出来的句子。这样设计的好处是调用方不用额外拼装。如果中途接不下去列表长度会小于steps 1不会抛异常——调用方需要自己判断长度别假设一定接满。allow_generate这个开关很实用。做演示、做课件的时候我会关掉它保证输出全部来自真实语料可溯源做娱乐向应用时再打开让接龙尽量接满。4.2 CLI 与 HTTP 两种跑法命令行入口在app/cli.py参数不多但够用# app/cli.py节选 def main(): ap argparse.ArgumentParser(description古诗词接龙) ap.add_argument(--start, requiredTrue, help起始诗句) ap.add_argument(--steps, typeint, default3, help往下接几句) ap.add_argument(--db, defaultdata/index/poems.db) ap.add_argument(--no-generate, actionstore_true, help关闭 N-gram 兜底) ap.add_argument(--seed, typeint, defaultNone, help固定随机种子便于复现) args ap.parse_args() if args.seed is not None: random.seed(args.seed) engine PoemRelay(args.db) for i, line in enumerate(engine.relay(args.start, args.steps, not args.no_generate)): print((起句 if i 0 else f第{i}句) line)--seed是我自己加的一行项目原始版本没有。理由很实际调权重的时候如果每次结果都变你根本判断不了改动是好是坏。固定种子之后同一个起句在同一个种子下的输出稳定改Weights里的某个值再跑一遍差异就一目了然。这个习惯值得从第一天就养成。当服务用就三行FastAPI直接挂# app/server.py from fastapi import FastAPI from pydantic import BaseModel from app.engine import PoemRelay app FastAPI(titlepoem-relay) engine PoemRelay(data/index/poems.db) class RelayReq(BaseModel): start: str steps: int 3 allow_generate: bool True app.post(/relay) def relay(req: RelayReq): lines engine.relay(req.start, req.steps, req.allow_generate) return {lines: lines, count: len(lines)}启动命令是uvicorn app.server:app --host 0.0.0.0 --port 8000。engine在模块级初始化全局共用一个连接所以Matcher里必须有那把锁——这一点在第 5 章会展开讲很多人第一次并发压测就栽在这儿。4.3 配置项动哪几个开关效果变化最明显configs/default.yaml里的参数按影响程度排我个人经验是这样的参数位置调大的效果建议区间weights.link打分更倾向严格按字接接不上的概率上升2.0 ~ 4.0weights.rhyme打分更倾向押韵语义连贯度可能下降1.0 ~ 3.0weights.repeat打分重复句更难被选中必须为较大负值-10 ~ -4generator.n生成阶数越高越像原句泛化越差2 ~ 4generator.temperature生成越高越发散越低越保守0.7 ~ 1.5第一个该动的是repeat不是link。很多人的直觉是「接不上就降低 link」结果降完之后接上了但接出来的是刚用过的那句。先把重复惩罚拉满再谈别的。第二个该动的是generator.n如果你的语料只有几万句n3 已经能明显看出过拟合n2 反而更稳。配置加载部分很简单yaml.safe_load读进来之后覆盖Weights的字段import yaml from app.scorer import Weights with open(configs/default.yaml, encodingutf-8) as f: cfg yaml.safe_load(f) w Weights(**cfg.get(weights, {})) engine PoemRelay(cfg[engine][db], weightsw)注意Weights(**...)这里yaml 里的键名必须跟 dataclass 字段名完全一致多一个键就TypeError。我第一次改配置时手滑加了个rhyme_bonus报错信息是unexpected keyword argument对着 yaml 找了五分钟才反应过来。5. 排查与避坑跑不通、接不上、接出来像废话5.1 数据与编码类问题现象CLI 能跑起句正常打印但一句都接不上relay返回的列表长度永远是 1。原因语料里混进了 BOM 或零宽字符。最常见的是从 Windows 环境拷过来的 txt第一行开头带\ufeff另一种是网页复制时带进来的\u200b。这类字符被HANZI_RE过滤掉了但如果它们在字符串中间切句之后seg[0]拿到的可能不是你以为的那个字。解决读文件时统一用encodingutf-8-sig它会把 BOM 自动吃掉NOISE_RE里显式列出\ufeff和\u200b-\u200f区间。验证方法很土但有效建完索引后跑一句 SQLSELECT COUNT(*) FROM lines WHERE LENGTH(head) ! 1正常应该返回 0。返回非 0 就是有脏字符混进了首字列。现象索引建完lines表里行数比原始文本里的句子数少了一大截。原因HANZI_RE用的是^[\u4e00-\u9fff]$这个区间不含扩展区的生僻字也不含异体字。语料里带校勘符号、注音括号、或者把「裏」写成「里」的版本都会被滤掉。解决先统计被滤掉的比例。加一行日志把HANZI_RE不匹配的片段打到data/index/reject.log里抽前 200 行看看是什么。如果只是零星符号忽略如果整段整段被滤说明语料格式跟预期不符得回去调整SENT_SPLIT_RE而不是放宽汉字区间。5.2 依赖与运行环境类问题现象pypinyin装上了但同一句话每次建索引算出来的韵部不一样或者「月」和「夜」被判成不同韵。原因Style.FINALS的strict参数行为在不同版本间有差异。不加strictTrue时拼音的声母还原规则不同ü的输出可能是ü、v或u:三种形式之一。解决代码里统一做一次归一化.replace(ü, v).replace(u:, v)这两句别省。更稳的做法是在build_index.py开头打印一次pypinyin.__version__和几个测试字的输出把环境差异暴露在构建阶段而不是等到运行期才发现韵部全错。现象FastAPI 服务单请求正常两个人同时请求就报SQLite objects created in a thread can only be used in that same thread。原因sqlite3.connect默认check_same_threadTrue连接只能在创建它的线程里用。而 FastAPI 的同步路由跑在线程池里每次请求可能落到不同线程。解决两个方向。一是check_same_threadFalse加一把threading.Lock包住所有查询项目里就是这么做的简单但串行二是每次请求新开连接成本略高但并发更真。语料量在十万级的时候前者完全够用别过度设计。现象跑着跑着 Python 进程内存飙到几个 G机器开始卡。原因normalize.py里把整个lines.jsonl读成一个 list 再处理或者engine.py把全表SELECT *拉进内存排序。句子数上百万的时候光 Python 字符串对象就能吃掉几个 G。解决流式读取for line in open(...)逐行处理写 jsonl 也用追加模式。排序只在候选集几十到几百条里做绝不全局排序。sqlite 那边的LIMIT一定要带别图省事写fetchall()不带限制。5.3 效果类问题接得上但不像诗现象能接满四句但读起来像词语接龙完全没有诗的语感。原因Weights里link权重过高压过了rhyme和tone。系统在拼命保证「首字接得上」代价是放弃了所有其他约束。解决把link从 3.0 降到 2.0rhyme提到 2.5同时打开tone。然后固定--seed跑 20 个起句人工看一遍。我的经验是rhyme只要略高于link输出就会有明显变化因为押韵带来的「像诗感」比字面接龙强得多。现象接出来的句子跟上一句毫无关系情绪、场景全断。原因第三级「按韵回退」命中太多matched_byrhyme的候选跟上一句只是押韵语义上完全无关。项目里对回退层级没有区分扣分。解决在打分函数开头按matched_by加一个层级惩罚exact加 0homophone减 0.5rhyme减 1.5generated减 3.0。这样只有在字接不上时才会退到押韵而且一旦有更好的候选就能压过去。这条改动很小但对输出质量的影响是肉眼可见的。现象同一句诗在一次接龙里出现两次或者连续几次调用结果高度雷同。原因repeat惩罚不够狠或者候选池太小导致top-5里去重后只剩一两条。解决repeat给到 -6.0 以下并且在top里做一次硬过滤重复句直接不进候选。同时检查候选池大小如果by_head返回经常小于 5 条说明语料太少或索引没建全这时候调权重是治标。6. 二次开发把接龙引擎改成藏头诗、对联和风格化生成接龙只是这个引擎最基础的一种用法把「约束」和「打分」拆开看它能做的事情不少。藏头诗是最容易改的一个因为约束从「尾字接首字」变成了「指定每一句的首字」def gen_acrostic(self, chars: str, per_line: int 7) - list[str]: out, prev [], None for ch in chars: cands self.matcher.by_head(ch, limit200) if not cands: cands [self.gen.gen_line(ch, max_lenper_line)] cands [self._to_cand(t) for t in cands] if prev is not None: cands sorted(cands, keylambda c: score(c, prev, out, self.weights), reverseTrue) pick cands[0] out.append(pick.text) prev pick.text return out跟接龙的区别就两处候选来源从「上一句尾字」换成「你指定的字」并且因为首字被锁死了link这一项打分的意义就没了实际起作用的是rhyme、tone、length和overlap。想让藏头诗更有整体感overlap得调成正数——让四句共享一些意象字读起来才像一首诗而不是四句拼贴。对联方向要更复杂一点核心是「对仗」。接龙只要求字数一致、平仄相对对联还要求词性对齐。做法是在建索引阶段多存一列词性串用jieba.posseg逐字打标然后把score扩展成位置敏感的版本def pair_score(a_pattern: str, b_pattern: str, pos_a: str, pos_b: str) - float: s 0.0 if len(a_pattern) ! len(b_pattern): return -99.0 # 字数不等直接判死 for pa, pb, ta, tb in zip(pos_a, pos_b, a_pattern, b_pattern): if pa pb: # 词性相同加分 s 0.6 if ta ! tb and ? not in (ta, tb): # 平仄相对加分 s 1.0 return s / len(a_pattern)这段代码要跑起来还需要在索引里补词性列工程量比藏头诗大。我的建议是先只做「字数相等 平仄相对」这两条出来效果已经能唬人再补词性。一上来就追词性对齐容易卡在jieba对古汉语分词效果一般这件事上翻车概率很高。再往上一步是风格化。项目自带的 N-gram 只能模仿字面搭配模仿不了风格。想让接出来的句子偏某个朝代、某个体裁最直接的路径是把generator.py换成一个小的字符级 LSTM 或者微调过的轻量语言模型接口保持gen_line(start, max_len, temperature)不变上层代码一行不用动。这个替换点设计得很干净也是我拆这个项目时最欣赏的一处生成层是可插拔的检索和打分完全不依赖它内部怎么实现。改造完必须验证不然你根本不知道是变好了还是只是变了个样。我一般跑一个四项指标的小评估def eval_relay(engine, seeds: list[str], steps: int 4) - dict: total rhyme_ok dup 0 seen set() for seed in seeds: lines engine.relay(seed, steps) total max(len(lines) - 1, 1) for a, b in zip(lines, lines[1:]): if rhyme_of(a[-1]) rhyme_of(b[-1]): rhyme_ok 1 for ln in lines[1:]: if ln in seen: dup 1 seen.add(ln) return { 接满率: sum(len(engine.relay(s, steps)) - 1 for s in seeds) / (len(seeds) * steps), 韵脚一致率: rhyme_ok / total, p a hrefhttps://download.csdn.net/download/weixin_45204213/12011750 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
RELATED

相关推荐

SecureCRT便携配置、中文编码与会话批量生成指南

SecureCRT便携配置、中文编码与会话批量生成指南

简介:SecureCRT汉化中文8.5便携版面向需要串口调试、SSH/Telnet远程运维与网络设备配置的工程师及学习者,主打免安装、解压即用,并支持导入其他版本配置,可减少换机或重装环境时的迁移成本。资源包共135个文件、约19.97MB&#xf…

📅 2026/11/6 5:28:31
OpenSSL 1.1.1 编译集成与TLS握手排错实战

OpenSSL 1.1.1 编译集成与TLS握手排错实战

简介:OpenSSL_1.1.1 面向在 Windows 32 位环境下开发或维护 SSL/TLS 通信程序的 C/C 开发者,提供一套可直接取用的运行库、开发头文件、链接库与离线帮助文档,用于证书生成、加解密调用及安全网络通信功能集成。压缩包约 16.22MB,…

📅 2026/11/6 5:28:31
YOLOv5单目测距落地指南:标定、地平面法与工程避坑

YOLOv5单目测距落地指南:标定、地平面法与工程避坑

简介:这是一份面向计算机视觉学习者以及自动驾驶、机器人导航方向开发者的 YOLOv5 单目测距 Python 实现包,重点解决仅凭单目摄像头估算目标距离、减少深度传感硬件依赖的问题。压缩包共 132 个文件,约 13.87MB,其中 yaml、yml 用…

📅 2026/11/6 5:28:31
MORE NEWS

更多资讯

📰

记忆机制:对话内与跨对话的记忆

很多人对大模型记忆有一个根深蒂固的误解:以为对话多了,模型就"认识"你了,就"记得"你上周说过什么。这个错觉很危险,因为它把工程的归工程的、模型的归模型的,全搅在了一起。今天我把这件事讲清楚…

📰

激光共聚焦显微镜:从原理到应用的全面解析

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

📰

一文读懂Prometheus+Grafana监控体系(进阶篇)

本文深入探讨PrometheusGrafana监控体系(进阶篇),涵盖背景分析、原理剖析、实战步骤、配置示例、优化建议和避坑指南。最近在处理DevOps与运维相关项目时,PrometheusGrafana监控体系(进阶篇)成为团队讨论最…

📰

关于ELK日志分析平台搭建(进阶篇),你需要知道的一切

本文深入探讨ELK日志分析平台搭建(进阶篇),涵盖背景分析、原理剖析、实战步骤、配置示例、优化建议和避坑指南。很多团队在DevOps与运维场景中都会遇到与ELK日志分析平台搭建(进阶篇)相关的挑战。本文结合生产环境经验…

📰

Pinyon Shift 原生渲染器深度解析:从PM4命令流到EDRAM、Resolve与纹理解码的完整指南

【免费下载链接】pinyon-shift A playable preview of Forza Horizon static recompilation 项目地址: https://gitcode.com/gh_mirrors/pi/pinyon-shift 点击查看 免费下载 Pinyon Shift 把 Xbox 360 版《Forza Horizon》重新编译为 PC 原生程序,其核心…

📰

英语副词详解

一、什么是副词?副词是修饰动词、形容词、其他副词、介词短语、从句或整个句子的词。它的核心功能是提供额外的信息,通常回答以下问题:如何 (How)? 何时 (When)? 何地 (Where)? 到什么程度 (To what extent)? 以及为什么 (Why)?你可以把…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬