☰
用Python实现中文文本生成:马尔可夫链与代码创作实践
2026/10/10 12:56:37 网站建设 项目流程

1. 为什么程序员的“写作欲望”会落在代码上

这年头愿意写博客的程序员越来越少,但用代码“写东西”的程序员却越来越多。不是所有人都有耐心敲几千字的文章,但几乎每个写代码的人都体验过一种快感:把一堆规则交给机器,然后看着机器吐出成百上千种不同的结果。这种组合、生成、变换的过程,本质上就是一种创作——只不过画笔变成了逻辑,素材变成了数据。

我说的“用代码写故事”,不限于那种复杂的AI生成,也包含很多接地气的玩法:用脚本把古诗词随机拼成新句子,用爬虫抓取新闻做词频统计,用马尔可夫链生成一段“看起来像模像样”的演讲稿,甚至用终端命令行输出一份糊弄人用的“藏头诗”。这些东西不是新鲜事,但从零手写一遍,你的理解深度会完全不一样。

这篇文章想聊的,就是“代码写作”这条路怎么走。不是让你放弃人类创作,而是给你一把新的工具:怎么设计文本生成的规则,怎么选简单的模型,怎么把生成结果接入自己的项目和日常写作,以及我在实际折腾中踩过的那些坑。不管你是对自然语言处理感兴趣的初学者,还是想给自己的工作流加一个“灵感生成器”的干活人,这篇文章都能给你一些直接能用的东西。

2. 文本生成的技术路线怎么选

2.1 从“字典+规则”开始:最朴素的生成方式

我最早做一个“自动写高考作文开头”的小工具时,压根没想用什么深度学习模型。就一个思路:把好句子拆成骨架和词槽,再准备一组替换词,随机塞进去。

比如骨架是“在____的____中,我们迎来了____”,词槽里填“阳光”“风雨”“青春”“时代”“选择”这些词,跑一遍就能生成上百个不同组合。这种“模板填充”虽然看起来笨,但有两个好处:

  • 速度快,不依赖任何外部库,用Python的random模块就能跑。
  • 结果可控,因为骨架是你自己写的,不会生成逻辑不通的句子,偶尔还能撞出几个惊艳的表达。

适合什么场景?开头段、标语、口号、诗化表达、固定格式的公文开头。你在做一个需要批量产出文案的工具时,模板填充是成本最低的方案。

2.2 马尔可夫链:让“随机”变得像人话

模板填充的缺点很尴尬:换汤不换药,骨架不变就永远那个味儿。如果你想让机器生成“更自由”的句子,又不想上深度模型,马尔可夫链是最理想的一步。

马尔可夫链的核心思想其实不复杂:下一句话(或者下一个词)只跟当前的状态有关,跟更早的历史无关。放在文本生成里,就是“基于当前词预测下一个词”。

实现起来最简单的是bigram模型:把训练语料拆成所有相邻词对,统计每个词后面跟着哪些词、各自概率是多少,然后从一个起始词出发,按概率采样下一词,再以这个词为“当前词”继续采样,直到碰到句号或达到最大长度。

这种方案的惊喜感很强,因为它会生成你完全没想到的组合。我有一个拿它拼接《三国演义》和《红楼梦》语料的实验,出来的句子既不像三国也不像红楼,但又意外地“读得通”,有种奇怪的文学魅力。

2.3 能不能直接上深度学习

能,但我不建议一上来就上。你问GPT类模型当然生成质量高,可那需要额外调接口或本地部署模型。如果仅仅是做一个本地的、不联网的、几十行代码就能跑的工具,深度模型反而是杀鸡用牛刀。

再退一步说,你在学习阶段把马尔可夫链吃透,后面再看LSTM、Transformer这些模型,很多概念都能对上号——比如n-gram就是“让模型考虑前n个词”,Transformer的注意力机制本质上也是在学“哪些词对当前词更重要”。基础打不牢,直接上大模型容易变成只会调参,不明白原理。

3. 实操:用Python写一个中文文本生成器

3.1 准备语料和分词

中文不像英文天然有空格分词,生成前必须先分词。这一步推荐用jieba库,它能按词而不是单字来切分,生成结果会自然很多。

准备语料的时候要注意质量。我建议选公开版权的文本,或者自己写的文章。就拿《三国演义》第一回来做演示,文字干净、词汇丰富,适合测试。

import jieba import random from collections import defaultdict text = open('sanguo.txt', encoding='utf-8').read() # 去掉换行和空白,让文本连续 text = ''.join(text.split()) # 分词 words = list(jieba.cut(text))

这里有一个细节:''.join(text.split())是为了去掉原文中的换行符和多余空格。如果你不处理,分词会把\n当成一个独立的词,生成的句子就会莫名其妙地断行。

3.2 构建bigram状态表

我们需要统计每个词后面跟随的所有词及频次。用defaultdict会很合适:外层key是当前词,内层是下一个词的频次表。

bigram = defaultdict(defaultdict) for i in range(len(words) - 1): cur, nxt = words[i], words[i+1] if cur not in bigram or nxt not in bigram[cur]: bigram[cur][nxt] = 0 bigram[cur][nxt] += 1

这里有个实际体验:直接按“出现次数”存储就够用了,不需要再转成概率。采样的时候用random.choices把频次当作权重传进去即可。

3.3 按权重采样生成句子

生成函数长这样:

def generate_sentence(start_word=None, max_words=30): if start_word is None: start_word = random.choice(list(bigram.keys())) result = [start_word] current = start_word for _ in range(max_words): if current not in bigram: break candidates = list(bigram[current].keys()) weights = list(bigram[current].values()) current = random.choices(candidates, weights=weights, k=1)[0] result.append(current) if current in '。!?': break return ''.join(result)

注意两个边界条件:

  • 如果某个词只出现在句首、后面没跟任何词,current not in bigram判断就会跳出循环。
  • 终止标志位设置成“。!?”确实能让句子更像一句完整的话,这是我几次试验后加上的,不加的话生成结果容易是半截话。

实测用《三国演义》第一回作为语料,生成的结果会有一种半文半白的感觉,比如:

“话说天下大势遂分为三国之时,曹操曰今已至此,又深感玄德之德。”

虽然不是标准文言文,但语义上还真有点那个味道。这就是马尔可夫生成的特点:局部通顺,全局松散。

3.4 参数调优实战经验

max_words我建议设置在30到60之间。太短生成不了完整意思,太长容易绕来绕去、逻辑漂移。这个数字直接决定了句子“像不像人话”。

还有一个可以调的参数是朴素的“随机种子”。固定随机种子可以让生成结果可复现,适合调试。如果你想让每次生成都不重样,就保持随机状态不去设置。

再有就是尝试用n=3的trigram模型:统计每个双词对后面的词分布。这样生成结果的连贯性会明显提升,但需要一个额外的处理步骤——把相邻两个词拼成一个“状态对”。改动不大,但效果差异是肉眼可见的。

4. 进阶玩法:把生成文本变成工具

4.1 做一个命令行小工具

你不想每次都在Jupyter里跑代码,想把它变成一个随时可用的命令。那就加一个入口文件,让它可以接收参数:

python generate.py --corpus sangou.txt --start 刘备 --length 50

这里有一个我踩过的坑:Windows终端默认编码是GBK,Python读取UTF-8编码的文本文件时,如果没指定编码就会报错或者乱码。处理方式很简单,读取文件时显式传入encoding='utf-8',但命令行输出时偶尔也会遇到控制台编码问题。稳妥的办法是在代码开头加一句:

import sys sys.stdout.reconfigure(encoding='utf-8')

这句话是为了让Python在输出中文时,尽量绕开控制台编码的坑。实测在Windows的codedation下还是很管用的。

4.2 生成标题和摘要:应急的“灵感草稿机”

我写文章最痛苦的永远是开头那几句话,铺陈、切入、引人入胜——这些环节机器往往帮不上大忙,但“给出一个没想过的角度”这件事,马尔可夫生成器意外地能胜任。

方法是把语料换成你自己过去写过的文章,分词建表后,让它生成一个完全不受到你“当时思路”限制的新开场白。虽然大概率不能直接用,但经常能读出一两个你没用过的词、一种你没尝试过的语气。我管这叫“文本漂移”——在旧框架里漂移出新鲜感。

4.3 接一个小页面变成浏览器工具

纯命令行工具适合自己用,但如果想让别人也能玩,最轻量的方案是用Flask把生成逻辑包成一个网页接口。核心代码就几十行:

from flask import Flask, request, jsonify, render_template import random, jieba from collections import defaultdict app = Flask(__name__) # 这里把之前构建bigram的代码封装成函数 def build_bigram(filepath): ... @app.route('/') def index(): return render_template('index.html') @app.route('/generate') def generate(): start = request.args.get('start', '') length = int(request.args.get('length', 30)) sentence = generate_sentence(start, length) return jsonify({'result': sentence}) app.run(debug=True)

这里我提一个真实的体验:前端页面别做太花哨,一个输入框、一个按钮、一个输出区域就够了。我最初加了什么“历史记录”“收藏夹”功能,结果发现根本没人用——大部分人是输入一个起始词,生成三次,挑一句顺眼的拿走。这个工具的本质是“浪费随机性换灵感”,不是内容管理系统。

5. 常见问题排查与避坑指南

5.1 生成结果全是高频词,毫无亮点

这是bigram模型最容易出现的问题:“的”“是”“了”“在”这种功能词会频繁出现,因为它们在任何语料中都是高频的。如果你不想看到满屏的“的”,可以考虑一个笨办法:在构建bigram表时,直接把单字的、长度小于等于1的词过滤掉(当然还要小心过滤掉真正的单字名词,比如“马”“牛”)。我实际测试下来,过滤长度为1的词会让生成结果显得更“书面化”一些,但也会丢失一些生动的短句。

更稳妥的做法是设置一个最低频次阈值:例如只保留至少出现过2次的词对。这能显著减少噪声,副作用是语料太小的时候可能过滤掉很多东西、导致生成跳不出固定模式。

5.2 语料太少怎么办

马尔可夫链非常吃语料。我试验过,少于3万字的语料生成结果几乎没法看,句子之间来回就几个词组合。3万到10万字是一个“可以勉强玩玩”的区间,10万字以上就是非常舒服的状态。

如果你想拿自己的博客文章来做语料又不够多,可以试着拼合多个来源:你自己的文章加一些公开的新闻语料加一本公版小说,混合训练效果通常比单一语料好——词与词的跨域组合会带来意外之喜。

5.3 文件读取乱码

这几乎是处理中文文本时遇到最多的一个问题。原因要么是文件本身编码跟读取编码不一致,要么是终端输出编码问题。我建议养成两个习惯:

  • 所有语料文件统一保存为UTF-8编码。
  • 在代码里凡是open文件,都显式带encoding='utf-8'。

这样能规避九成以上的中文乱码问题,剩下的终端显示问题就靠sys.stdout.reconfigure处理。

5.4 生成失控:循环到同一段话

词表构建后,偶尔会出现某个词后面只跟着同几个词,导致文本陷入一段几字循环。这在马尔可夫链里很常见。处理方法有两个:

  • 增大语料规模。
  • 在采样时引入一个随机扰动:比如以5%的概率随机选择下一个词,而不是完全按频次采样。

第二种方法我管它叫“随机性抖动”,代价是句子会偶尔变得不像人话,但确实能打破循环。调试的时候把5%调成0%,看看到底是哪个词引起的循环,也挺有意思。

5.5 内容合规性提醒

用代码生成文本时,有两点需要特别注意:

  • 不要用别人有版权的长文来生成商业用途的内容。个人学习、研究没问题,拿来做产品就要谨慎。
  • 生成内容如果是对外发布的,一定要人工审核过一遍。机器生成的东西偶尔会出现语义不当、用词越界的情况,这不是模型本身“坏”,而是随机组合的必然结果。作为发布者,你有责任把关最终输出的内容。

6. 扩展方向:从马尔可夫走向更远

如果你把上面的项目做完了,还想继续深入,有三个方向可以参考。

第一,把n-gram里的n从2提到3或者4。trigram的结果会比bigram更顺滑,但状态表大小会指数级增长,语料需求也跟着上涨。建议一步步来,别一次跳太远。

第二,引入主题词的引导。初始词给一个,终词也给一个,在生成过程中把“距离终词过远”的路径剪掉。这听起来复杂,但在纯代码层面就是多一个距离计算函数而已,几分钟能写完,但生成效果会非常贴近你想要的方向感。

第三,接入现代语言模型。如果你已经会调接口,可以把马尔可夫生成的结果当作“粗糙草稿”,交给大模型润色。这种“老派方法生成初稿 + 新模型精修”的流程,我现在写产品文案偶尔还在用——比单独用大模型生成要有“质感”,因为它带有随机性带来的陌生感,而不是大模型常见的“正确但平庸”。

我个人实际用下来最大的体会是:用代码写文本这件事,主角从来不是代码,而是“组合出来的意外”。你在规则里设置好边界,剩下交给随机——很多时候你得到的不是你想写的句子,而是你没想到自己会喜欢的句子。这种惊喜感,是模板填充给不了、大模型也给不了的。它属于所有愿意折腾底层逻辑的人。

如果看完这篇你也手痒,建议别停留在读。随便找一本公版书,跑一遍分词,建一个bigram表,生成十句话。从你动手的那一分钟起,你就不再只是一个“看教程的人”,而是一个真正碰过文本生成的人了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询