1. 从“Hello World”到“诗与远方”为什么我们需要一个Python诗歌生成器在编程学习的漫漫长路上我们写过无数个“Hello World”调试过数不清的语法错误也尝试过用Python处理表格、分析数据、甚至写个小游戏。但你是否想过代码除了解决那些“硬核”问题还能不能有点“诗和远方”这就是“Python-Poem-Maker”这个项目想探讨的有趣话题。它不是一个严肃的生产力工具而是一个融合了编程逻辑与语言美学的创意实验。通过这个项目我们不只是学习几个新的库更是尝试让冷冰冰的代码去触碰人类最感性的表达形式之一——诗歌。你可能会问这有什么用从实用角度看它确实不能帮你优化数据库查询也不能直接提升你的KPI。但它的价值在于“过程”而非“结果”。首先它能极大地激发学习兴趣。当你厌倦了枯燥的算法题和业务逻辑时用代码写一首诗是一种绝佳的调剂和成就感来源。其次它综合运用了字符串处理、列表操作、随机选择、文件读写乃至简单的自然语言处理概念是对基础语法一次生动而全面的实践。最后它打开了“计算创造力”的大门让你思考机器如何理解、模仿甚至创造具有美感的文本这本身就是人工智能领域一个迷人的分支。无论你是刚学完Python基础语法的新手想找一个有趣的项目来巩固知识还是已经有一定经验的开发者希望在周末搞点好玩的Side Project放松一下亦或是你对自然语言生成和计算语言学抱有好奇心这个项目都提供了一个绝佳的起点。它不要求你有多高深的算法知识核心逻辑可能几百行代码就能实现但其中蕴含的思考和扩展空间却是无限的。2. 核心原理拆解机器如何“学会”写诗在动手写代码之前我们必须先想清楚一个根本问题一首诗是怎么被“制造”出来的对于人类诗人需要灵感、阅历、情感和对语言的精妙掌控。对于机器我们则需要将其分解为可计算、可模拟的步骤。一个典型的诗歌生成器其核心原理通常围绕以下几个层面展开。2.1 文本的“原材料”处理语料库与分词任何文本生成模型都需要“学习材料”我们称之为语料库。对于诗歌生成器语料库就是大量的诗歌文本。你可以从公开的古诗词数据库、现代诗歌网站甚至自己收集喜欢的诗集文本来构建。这一步的关键是数据清洗比如去除无关的标点、注释、作者信息只保留纯净的诗句文本。拿到纯净文本后下一步是“分词”。对于英文分词相对简单通常以空格和标点为界。但对于中文诗歌分词就是一门学问了。“白日依山尽”是分成“白日/依/山尽”还是“白/日/依/山/尽”不同的分词粒度会直接影响后续模型学习到的“语言单元”。对于古典诗歌由于句式固定五言、七言有时我们可以直接按字切分将每个汉字视为一个基本单元这样能更好地捕捉到诗歌的韵律和字词搭配。对于现代诗则可能需要借助jieba这类中文分词库。选择哪种方式取决于你想生成什么风格的诗歌。# 示例一个简单的中文按字分词函数 def character_level_tokenize(poem_text): # 去除所有空白字符和特定标点 import re cleaned_text re.sub(r\s, , poem_text) # 去除空白 cleaned_text re.sub(r[。、“”‘’《》【】], , cleaned_text) # 去除常见中文标点保留字 # 按字分割成列表 tokens list(cleaned_text) return tokens # 示例诗句 sample_poem 床前明月光疑是地上霜。 tokens character_level_tokenize(sample_poem) print(tokens) # 输出[床, 前, 明, 月, 光, 疑, 是, 地, 上, 霜]2.2 建模诗歌的“记忆”N-gram与状态转移对于入门级的诗歌生成器马尔可夫链Markov Chain和N-gram模型是经典且易于实现的选择。其核心思想非常直观通过分析语料库中文本序列的统计规律来预测下一个词或字出现的概率。什么是N-gram简单说它就是连续的N个词或字组成的单元。比如“床前明月光”它的2-gramBi-gram就是[‘床前’ ‘前明’ ‘明月’ ‘月光’]。模型通过统计语料中所有N-gram出现的频率学习到诸如“当‘床前’出现后‘明月’出现的概率很高”这样的规律。当我们生成诗歌时就可以从一个随机的起始词开始根据当前已有的最后N-1个词从统计概率表中随机选择或按概率加权选择下一个词如此循环从而“生成”新的文本序列。# 示例构建一个简单的Bi-gram模型字级别 from collections import defaultdict, Counter import random def build_bigram_model(token_list): 构建二元模型记录每个字后面可能跟随的字及其出现次数 model defaultdict(Counter) for i in range(len(token_list) - 1): current_char token_list[i] next_char token_list[i 1] model[current_char][next_char] 1 return model def generate_poem_with_bigram(model, start_char, length20): 使用二元模型生成诗歌 poem [start_char] current_char start_char for _ in range(length - 1): # 获取当前字可能的下一个字及其频次 next_char_options model[current_char] if not next_char_options: # 如果当前字没有后续字随机选择一个字重启或结束 break # 根据频次随机选择下一个字频次越高被选中的概率越大 next_chars, weights zip(*next_char_options.items()) next_char random.choices(next_chars, weightsweights)[0] poem.append(next_char) current_char next_char return .join(poem) # 使用之前的tokens构建模型 bigram_model build_bigram_model(tokens) # 从‘床’开始生成10个字的“诗” generated_line generate_poem_with_bigram(bigram_model, 床, 10) print(generated_line) # 由于语料极小输出可能很奇怪但原理如此注意以上示例仅用于演示原理。在实际应用中你需要用成千上万首诗歌作为语料库模型才能学到有意义的规律生成看起来“通顺”的句子。同时直接按概率随机选择可能会生成循环或无意义的序列通常需要引入一些随机性控制和终止条件。2.3 引入“格律”与“押韵”的约束对于古典诗词格律和押韵是灵魂。要让机器写出像样的诗词必须在生成过程中加入这些硬性约束。这比自由生成要复杂得多。格律指诗句的平仄格式。例如五言律诗有固定的平仄谱。我们可以预先定义好这些谱子在生成每个字时不仅要考虑N-gram的概率还要检查其声调平仄是否符合当前位置的要求。这需要有一个汉字到平仄的映射表。押韵指诗句末尾字的韵母相同或相近。我们需要一个韵脚字典。在生成时对于需要押韵的句子其最后一个字必须从指定的韵部中选择。例如生成一首押“ang”韵的诗那么每一联的末字都应该是“江”、“阳”、“香”等同韵字。加入这些约束后生成算法就从简单的概率采样变成了一个“满足多重约束的搜索问题”。我们可以使用回溯算法尝试填入一个字如果它同时满足N-gram概率通顺度和平仄/押韵要求就继续生成下一个字如果不满足则回退到上一步尝试其他选择。这个过程计算量会大很多但生成的诗歌会规整得多。3. 项目实战构建你的第一个诗歌生成器理解了原理我们开始动手搭建。我们将构建一个相对简单的现代诗生成器它基于N-gram模型暂不处理复杂的格律但可以尝试加入简单的押韵提示。我们将项目结构分为几个清晰的模块。3.1 环境搭建与依赖安装首先确保你有一个可用的Python环境3.6及以上版本。我强烈建议使用虚拟环境来管理项目依赖避免污染全局环境。# 创建并激活虚拟环境以venv为例 python -m venv poem_env # Windows poem_env\Scripts\activate # Linux/Mac source poem_env/bin/activate # 安装核心依赖 # 主要用到 jieba 进行中文分词requests 和 beautifulsoup4 用于可能的爬虫采集语料 pip install jieba requests beautifulsoup4对于编辑器VSCode或PyCharm都是绝佳选择。在VSCode中记得安装Python扩展并配置好刚刚创建的虚拟环境作为解释器。3.2 语料收集与预处理模块我们首先需要一个data_processor.py模块来处理原始语料。# data_processor.py import re import jieba import os from collections import defaultdict class DataProcessor: def __init__(self, corpus_dir./corpus): self.corpus_dir corpus_dir self.poems [] # 存储清洗后的每首诗字符串列表 self.all_lines [] # 存储所有诗句 def load_from_txt(self, filepath): 从txt文件加载诗歌每行一首诗或一句 with open(filepath, r, encodingutf-8) as f: raw_lines f.readlines() for line in raw_lines: line line.strip() if line: # 简单清洗去除空格、特殊符号按句号、问号等分句 # 这里假设一行是一首完整的诗用标点分句 sentences re.split(r[。], line) cleaned_sentences [s.strip() for s in sentences if s.strip()] if cleaned_sentences: self.poems.append(cleaned_sentences) self.all_lines.extend(cleaned_sentences) print(f从 {filepath} 加载了 {len(cleaned_sentences)} 首诗共 {len(self.all_lines)} 个诗句。) def tokenize_lines(self, use_jiebaTrue): 对所有诗句进行分词 tokenized_lines [] for line in self.all_lines: if use_jieba: # 使用jieba精确模式分词 tokens list(jieba.cut(line, cut_allFalse)) else: # 按字分词 tokens list(line) # 过滤掉纯标点符号的token tokens [t for t in tokens if re.search(r[\u4e00-\u9fff], t)] if tokens: tokenized_lines.append(tokens) return tokenized_lines # 使用示例 if __name__ __main__: processor DataProcessor() # 假设你有一个‘poems.txt’文件里面每行是一首古诗 processor.load_from_txt(poems.txt) tokenized_data processor.tokenize_lines(use_jiebaFalse) # 古典诗按字分词 print(f前3个分词后的诗句{tokenized_data[:3]})实操心得语料质量决定生成质量。网络爬取的诗歌数据往往格式混乱包含大量注释、标题、作者名。预处理阶段花再多时间都值得。一个技巧是可以专门寻找整理好的开源古诗词数据集如chinese-poetry项目它们通常已经是结构化的JSON或纯文本能省去大量清洗工作。3.3 核心模型构建模块接下来是poem_model.py这里我们实现一个基于N-gram的模型并加入简单的押韵感知。# poem_model.py import random from collections import defaultdict, Counter import pickle # 用于保存和加载模型 class NGramPoemModel: def __init__(self, n3): self.n n # n-gram的n值例如3表示tri-gram self.model defaultdict(Counter) # 核心模型数据结构 self.start_tokens [] # 记录所有可以作为诗句开头的 (n-1)-gram self.rhyme_dict self._build_simple_rhyme_dict() # 简易押韵字典 def _build_simple_rhyme_dict(self): 构建一个简易的押韵字典示例实际需要更全的韵表 # 这里只是一个示例将一些同韵字分组 rhyme_groups { a: [啊, 吧, 妈, 发, 拉], o: [哦, 波, 摸, 佛, 罗], ang: [昂, 帮, 忙, 方, 郎, 光, 霜, 乡] } # 反转字典方便通过字查韵母 char_to_rhyme {} for rhyme, chars in rhyme_groups.items(): for char in chars: char_to_rhyme[char] rhyme return char_to_rhyme def train(self, tokenized_lines): 使用分词后的诗句列表训练模型 for tokens in tokenized_lines: if len(tokens) self.n: continue # 跳过太短的句子 # 记录句首的 (n-1)-gram self.start_tokens.append(tuple(tokens[:self.n-1])) # 滑动窗口统计 n-gram for i in range(len(tokens) - self.n 1): ngram tuple(tokens[i:iself.n-1]) next_token tokens[iself.n-1] self.model[ngram][next_token] 1 print(f模型训练完成。共学习到 {len(self.model)} 个不同的 {self.n-1}-gram 上下文。) def generate_line(self, start_withNone, max_len15, rhyme_groupNone): 生成一行诗 if start_with: # 如果指定了开头词则以此构建初始上下文 if len(start_with) self.n - 1: prefix tuple(start_with) (None,) * (self.n - 1 - len(start_with)) prefix tuple([p for p in prefix if p is not None]) else: prefix tuple(start_with[-(self.n-1):]) else: # 随机选择一个句首作为开始 if not self.start_tokens: prefix tuple() else: prefix random.choice(self.start_tokens) line list(prefix) while len(line) max_len: current_ctx tuple(line[-(self.n-1):]) if len(line) self.n-1 else tuple(line) next_options self.model.get(current_ctx) if not next_options: break # 没有找到可接续的词结束生成 # 如果有押韵要求并且这是行末接近最大长度则优先选择押韵的字 if rhyme_group and len(line) max_len - 2: filtered_options {k: v for k, v in next_options.items() if self.rhyme_dict.get(k) rhyme_group} if filtered_options: next_options filtered_options # 按权重随机选择下一个词 choices, weights zip(*next_options.items()) next_word random.choices(choices, weightsweights)[0] line.append(next_word) return .join(line) def save_model(self, filepath): 保存模型到文件 with open(filepath, wb) as f: pickle.dump({model: self.model, start_tokens: self.start_tokens, n: self.n}, f) def load_model(self, filepath): 从文件加载模型 with open(filepath, rb) as f: data pickle.load(f) self.model data[model] self.start_tokens data[start_tokens] self.n data[n] print(f模型已从 {filepath} 加载。) # 使用示例 if __name__ __main__: # 假设有处理好的tokenized_data # from data_processor import DataProcessor # processor DataProcessor() # processor.load_from_txt(poems.txt) # tokenized_data processor.tokenize_lines(use_jiebaFalse) # 模拟数据 tokenized_data [list(床前明月光), list(疑是地上霜), list(举头望明月), list(低头思故乡)] model NGramPoemModel(n3) model.train(tokenized_data) # 生成一行诗 line1 model.generate_line(max_len7) print(f生成的诗句{line1}) # 尝试押‘ang’韵 line2 model.generate_line(max_len7, rhyme_groupang) print(f押‘ang’韵的诗句{line2})3.4 诗歌组装与生成模块有了生成单句的能力我们需要一个poem_generator.py来组织成完整的诗。这里我们可以定义不同的诗歌结构比如四行绝句、两行对联或者自由体。# poem_generator.py from poem_model import NGramPoemModel import random class PoemGenerator: def __init__(self, model): self.model model def generate_couplet(self, first_line_hintNone): 生成一副对联两句 line1 self.model.generate_line(start_withfirst_line_hint, max_lenrandom.randint(5, 9)) # 第二句可以尝试与第一句末字押韵这里简化处理只是接着生成 line2 self.model.generate_line(max_lenlen(line1)) # 控制长度相近 return f{line1}\n{line2} def generate_quatrain(self, rhyme自由): 生成一首四行诗绝句。rhyme参数可以是‘自由’或指定韵母如‘ang’ poem_lines [] rhyme_group rhyme if rhyme ! 自由 else None for i in range(4): # 可以设计为第一、二、四句押韵这里简化如果指定韵脚每句都尝试押韵 line self.model.generate_line(max_len5 if i%20 else 7, rhyme_grouprhyme_group) # 模拟五言/七言交替 poem_lines.append(line) return \n.join(poem_lines) def generate_free_verse(self, num_lines6, max_line_len12): 生成一首自由体现代诗 poem_lines [] for _ in range(num_lines): line_len random.randint(3, max_line_len) line self.model.generate_line(max_lenline_len) poem_lines.append(line) return \n.join(poem_lines) # 主程序入口 def main(): # 1. 加载或训练模型 model NGramPoemModel(n3) # 尝试加载已保存的模型如果不存在则训练 try: model.load_model(poem_model.pkl) except FileNotFoundError: print(未找到已保存模型开始训练...) # 这里需要你实际的数据处理流程 # from data_processor import DataProcessor # processor DataProcessor() # processor.load_from_txt(your_corpus.txt) # tokenized_data processor.tokenize_lines(use_jiebaTrue) # 现代诗用jieba分词 # model.train(tokenized_data) # model.save_model(poem_model.pkl) print(模拟模型训练完成。) # 为演示我们手动加一些简单数据 mock_data [list(春天的风), list(夏天的雨), list(秋天的叶), list(冬天的雪)] model.train(mock_data) # 2. 创建生成器 generator PoemGenerator(model) # 3. 生成诗歌 print( 生成对联 ) print(generator.generate_couplet()) print(\n 生成四行诗自由韵) print(generator.generate_quatrain()) print(\n 生成自由体诗 ) print(generator.generate_free_verse()) if __name__ __main__: main()运行这个主程序你就能看到你的诗歌生成器开始“创作”了。最初的输出可能看起来有些滑稽甚至不通顺这是因为语料库太小模型学到的规律有限。随着你喂给它成千上万首高质量的诗歌它的“文学素养”会逐渐提高。4. 从玩具到工具优化方向与进阶思考一个能跑通的Demo只是起点。要让这个诗歌生成器变得更有趣、更强大我们可以在多个方向上深入。4.1 提升生成质量超越N-gramN-gram模型简单有效但缺点也很明显它只有短期记忆取决于n的大小无法把握长距离的语义关联和诗歌的整体意境。为了生成更连贯、更有“诗味”的文本我们可以探索更高级的模型。循环神经网络RNN/LSTM这是深度学习文本生成的经典起点。RNN及其变体LSTM、GRU能够处理序列数据并拥有一定的“记忆”能力可以学习诗句中更长距离的依赖关系。使用Keras或PyTorch你可以用几百行代码搭建一个字符级或词级的LSTM诗歌生成模型。训练这样的模型需要更多的数据数万行诗句和GPU计算资源但生成效果通常比N-gram更流畅。Transformer与预训练模型这是当前自然语言生成的SOTAstate-of-the-art。你可以使用Hugging Face的transformers库加载一个中文预训练模型如GPT-2的中文变体、CPM、或ERNIE然后在你的诗歌语料上进行微调。这种方法能生成语法正确、语义连贯性极强的文本甚至能模仿特定诗人的风格。但模型庞大训练和推理对硬件要求高。强化学习与约束满足对于格律诗可以将平仄、押韵、对仗等要求建模为“约束”或“奖励信号”。使用强化学习算法让模型在生成每个字时不仅考虑语言模型的概率还要考虑满足这些约束所能获得的“奖励”从而引导模型生成既通顺又合规的诗歌。这是一个非常有挑战性但也极具价值的研究方向。4.2 丰富交互与功能设计一个命令行工具可以变得更有交互性甚至拥有图形界面。Web应用使用Flask或FastAPI快速搭建一个后端服务提供RESTful API来生成诗歌。然后用HTML/CSS/JavaScript写一个简单的前端页面用户可以选择诗歌类型五言、七言、现代诗、输入关键词如“月亮”、“离别”、点击生成按钮结果就能优雅地展示在网页上。这是展示你全栈能力的好机会。风格控制让用户可以选择生成“李白风格”的豪放诗还是“李清照风格”的婉约词。实现思路可以是将语料库按诗人或风格分类分别训练多个模型。或者在输入中给模型一个“风格提示符”style token在训练时让模型学会将提示符与对应的风格关联起来。图像生成结合这是当前AIGC的热点。你可以用生成的诗歌作为提示词prompt调用像Stable Diffusion这样的文生图模型API为每一首生成的诗歌配上一幅AI创作的画。这样一个“诗画一体”的应用会非常吸引人。4.3 工程化与部署考量如果你想把这个项目长期运行或分享给他人就需要考虑工程化的问题。模型持久化与加载正如我们代码中用pickle做的训练好的模型要保存到文件。在生产环境中模型文件可能很大需要考虑存储和加载速度。对于深度学习模型通常保存模型权重.h5或.pt文件和结构定义。性能优化N-gram模型查询速度快但内存占用随语料增大而增长。可以使用更高效的数据结构如前缀树Trie或数据库。对于深度学习模型则需要进行模型量化、剪枝或使用更高效的推理框架如ONNX Runtime。错误处理与日志在generate_line函数中当next_options为空时我们直接break了。更好的做法是定义一个回退机制fallback比如使用(n-1)-gram甚至更短的上下文重新尝试或者引入一个通用的“接龙词库”。同时记录生成过程中的关键决策和异常便于调试。部署为服务使用Docker将你的应用及其所有依赖Python环境、模型文件、代码打包成一个镜像。然后可以轻松地部署到任何支持Docker的云服务器或容器平台上。结合Web框架你就拥有了一个在线的诗歌生成服务。5. 避坑指南与常见问题在开发和实验过程中我踩过不少坑这里总结几个最常见的问题和解决方案。5.1 生成文本的重复与循环这是N-gram和简单RNN模型最常见的问题。模型可能会陷入一个短循环不断重复“明月明月明月”这样的片段。原因概率分布过于集中或者模型缺乏有效的“记忆重置”机制。解决方案引入随机性Temperature在根据概率选择下一个词时不要直接按概率随机而是引入一个“温度”参数来调整概率分布的平滑程度。高温1.0会让分布更均匀增加随机性低温1.0会让分布更尖锐增加确定性。适当调高温度可以打破循环。Top-k / Top-p采样这是更先进的采样策略。Top-k采样只从概率最高的k个候选词中随机选择Top-p核采样则从累积概率达到p的最小候选词集合中选择。这两种方法都能有效避免选择那些概率极低的奇怪词同时也能防止模型总是选择那个最高概率的词而导致重复。增加惩罚在生成过程中记录已出现的N-gram如果即将生成的词会导致重复的N-gram出现则降低其选择概率。5.2 生成结果毫无意义或语法混乱原因语料库质量差、数据量不足、分词方式不当或者模型容量太小如n值太小。解决方案清洗语料确保你的诗歌文本是干净的没有杂乱的符号、英文、数字。对于古诗词注意区分正文和注释。扩大数据量N-gram模型严重依赖数据量。至少需要数万行诗句才能看到像样的效果。可以去GitHub上搜索“Chinese poetry dataset”寻找开源数据集。调整分词和N值对于古典诗尝试按字分词n2或3。对于现代诗使用jieba分词并尝试更大的n值如4或5让模型看到更长的上下文。使用更强大的模型如果条件允许升级到LSTM或Transformer模型它们捕捉长距离依赖的能力强得多。5.3 处理生僻字与未登录词模型在生成时可能会遇到“未登录词”Out-Of-Vocabulary OOV即语料库中从未出现过的字或词组合。原因语料覆盖不全或者使用了封闭词表。解决方案使用字模型对于中文字的总数是有限的常用汉字约3500个。使用字符级模型可以极大减少OOV问题因为几乎所有字都会在大型语料中出现。引入回退平滑在N-gram模型中如果当前上下文没有记录可以回退到(n-1)-gram甚至更简单的unigram模型去估计概率。这叫做“回退平滑”Backoff Smoothing或“插值平滑”Interpolation。子词切分对于词级模型可以采用Byte Pair Encoding (BPE)或WordPiece等子词切分方法将未知词拆分成已知的子词单元。5.4 项目依赖与环境问题一个常见的坑是代码在自己电脑上运行良好换一台机器或一段时间后就报错了。锁定依赖版本在项目根目录创建requirements.txt文件并使用pip freeze requirements.txt生成精确的版本列表。别人部署时使用pip install -r requirements.txt就能复现完全相同的环境。使用虚拟环境再次强调为每个项目创建独立的虚拟环境是Python开发的最佳实践可以避免包版本冲突。处理路径问题代码中不要使用绝对路径如C:\Users\...\data.txt。使用相对路径或者通过配置文件、环境变量来指定资源文件如语料库、模型文件的位置。可以使用os.path.join函数来构建跨平台的路径。这个项目就像一把钥匙它打开了一扇名为“计算创造力”的门。门后的世界广阔无垠从简单的概率统计到复杂的深度学习从文本生成到多模态艺术创作。最重要的是在整个过程中你不仅练习了Python编程更实践了数据处理、模型设计、算法调优和软件工程的全流程。下次当你调试业务代码感到疲惫时不妨运行一下你的诗歌生成器看看它今天又为你“创作”了怎样的惊喜或许能给你带来片刻的轻松与灵感。