news 2026/10/6 16:28:55

古诗自动生成与情感分析:从词向量到RNN的NLP实战全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
古诗自动生成与情感分析:从词向量到RNN的NLP实战全流程

简介:这份资源是一套围绕机器学习与自然语言处理的古诗自动生成与情感分析系统完整项目,适合具备一定编程基础的自然语言处理学习者、课程设计或毕业设计人员参考。资源按语料爬取、数据处理、数据分析、规则作诗、机器学习写诗等模块组织,既包含网络爬虫脚本与文本清洗工具,也提供基于格律规则和基于序列模型的两种写诗方案,并配有情感分析与词频统计的可视化图表。压缩包共一百五十六个文件,以程序脚本、文本语料、模型检查点文件及训练配置文件为主,另含说明文档、词向量文件等,整体约三百三十七兆字节。内附训练至五万余步的模型检查点,可直接加载用于推理或继续训练,省去从零训练的时间成本。目前已有二百九十五人学习下载,适合希望系统复现古诗生成技术、深入研究情感分析的读者。

1. 机器学习写诗:从语料到模型落地的完整链路

选“古诗自动生成与情感分析”做机器学习课程设计的人不在少数,但多数项目停在“能吐几句诗”的演示层面,押韵靠人工调、情感分析只输出一个百分比就交差。我拆这套资源时最大的感受是,它把一整条链路铺开了:从爬取古诗语料、清洗去重、词频统计,到用 Word2Vec 训练出 vectors_poem.bin,再到规则作诗打底、RNN 模型生成诗句并做情感分类,每一步都有对应的数据文件。很适合拿来当 NLP 期末项目或机器学习实战练手。想看落地步骤的人,建议先装好 Python 3.6 以上环境,以及 TensorFlow、gensim、jieba,后面跟着操作会顺利很多;熟手可以直接跳到模型加载和数据流转那几节,坑基本都在那。

2. 语料爬取与清洗:生成质量的上限藏在数据里

2.1 采集选型:轻量 requests 还是重型 Scrapy

很多初学者一上来就搭 Scrapy 工程,其实对一个几千首规模的古诗语料集来说,requests + BeautifulSoup 往往更划算。Scrapy 的异步下载器、中间件和 Item Pipeline 适合大规模持续爬取,但要维护工程结构,配置下载延迟和去重规则也得多写不少代码;而古诗正文页结构固定,用 requests 配合 Session 复用连接,几十行就能跑完整个采集流程。这个选择背后是成本和收益的权衡:语料数量足够支撑词向量训练即可,没必要为采集规模牺牲调试效率。

import requests from bs4 import BeautifulSoup import time from urllib.parse import urljoin HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} def parse_poem_page(url, session=None): session = session or requests.Session() resp = session.get(url, headers=HEADERS, timeout=10) resp.encoding = "utf-8" # 古诗类站点很多用 GBK,手动指定编码防乱码 if resp.status_code != 200: return None soup = BeautifulSoup(resp.text, "html.parser") title = soup.select_one("h1").text.strip() content_node = soup.select_one(".poem-content") # 正文容器按实际页结构调整 content = content_node.text.strip() if content_node else "" return {"title": title, "content": content, "url": url} def crawl_list(start_url, max_pages=50): session = requests.Session() page_urls = [start_url] results = [] for page_url in page_urls: resp = session.get(page_url, headers=HEADERS, timeout=10) resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "html.parser") for a in soup.select("a[href]"): href = urljoin(start_url, a["href"]) if "/poem/" in href and href not in page_urls: page_urls.append(href) time.sleep(1) # 低频请求,避免对目标站点造成压力 return results

代码里 timeout=10 保证单个页面卡住时不会拖垮整个采集流程;resp.encoding 手动指定为 utf-8 是因为 requests 的编码探测对中文站点的命中率不稳定,一旦误判成 ISO-8859-1,后面清洗时才发现乱码就晚了。crawl_list 中的 time.sleep(1) 不是摆设,它把请求频率压到每秒一次以内,既降低被识别为爬虫的概率,也让上百页的采集过程出现问题时更容易定位是哪一批数据出错。

2.2 去重与清洗:HTML 标签、全角空格和噪声标点

原始网页里常见的全角空格、明码实体、作者注释都不该进训练集。清洗顺序建议固定为:先剥 HTML 标签,再清全角空格和换行,最后过滤不在常用汉字和基础标点范围内的字符。去重环节要以“标题 + 正文”作为联合主键,因为不同版本的同一首诗正文可能略有差异,只按正文去重会把真正需要保留的版本误删。

import re from collections import OrderedDict ALLOWED = set(",。!?、;:""''《》()") def clean_poem_text(raw_text): # 先去标签,再清全角空格与换行 text = re.sub(r"<[^>]+>", "", raw_text) text = text.replace("\u3000", "").replace("\n", "").replace(" ", "") # 白名单过滤:只保留汉字和常用标点 text = "".join(ch for ch in text if ("\u4e00" <= ch <= "\u9fa5") or ch in ALLOWED) return text.strip(",。;") def dedup_poems(poems): seen = set() unique = OrderedDict() for item in poems: key = (item["title"], item["content"]) if key not in seen: seen.add(key) unique[key] = item return list(unique.values())

这里用白名单保留标点而不是黑名单过滤,原因是黑名单总是漏掉各种特殊空格和不可见字符;只保留 \u4e00-\u9fa5 区间还能让繁体和异体字提前暴露出来,方便在分词前决定是否统一转换。去重用 OrderedDict 保持原始采集顺序,避免训练集被随机打乱后情感标签在类别分布上出现意外偏移。

2.3 词频统计与关键词提取:先摸清语料的语言特征

清洗后的文本我习惯先用 jieba 分词,再做一次词频统计。这一步看似与分析无关,实际上决定了后面规则作诗的韵脚表怎么建、情感分类的特征从哪里来。古诗和现代汉语的分词结果差异很大:“床前明月光”会被切成“床前 / 明月 / 光”,而不是按现代汉语词典切成“床 / 前 / 明月光”,这恰恰符合古诗逐字对仗的特点,Word2Vec 也能学到更好的字词共现关系。

import jieba from collections import Counter from tqdm import tqdm def analyze_corpus(poem_texts): word_counter = Counter() for text in tqdm(poem_texts, desc="cutting"): words = jieba.lcut(text, cut_all=False) word_counter.update([w for w in words if len(w) > 1 and w != " "]) return word_counter.most_common(30)

cut_all=False 是 jieba 的精确模式,比全模式更符合中文分词习惯;取长度大于 1 的词是为了筛掉单字虚词,避免“之”“乎”这类高频但语义弱的字干扰统计。跑完词频你会发现“春风”“明月”“归”“愁”“花”这类词稳定占据前列,它们既是规则作诗的素材库,也是情感分析里最值得关注的特征。如果手头有 SPSS,也可以把词频表导进去做对应分析,但在关键词提取这个任务上,jieba.analyse 已经足够;SPSS 更适合做因子分析这类多维统计,属于加分项而不是必需品。

提示:爬取前先确认目标站点的协议页面,控制请求频率。爬虫只是获取语料的工具,数据清洗环节做得越干净,后面词向量和情感分类越省力。

3. 词向量与情感标签:让机器理解古诗的两种方式

3.1 Word2Vec 训练参数与 vectors_poem.bin 的加载

传统机器学习项目里,字词通常被当作离散 ID 输入模型,但“月”和“明月”在 ID 空间里毫无关联。Word2Vec 的思路是用共现关系学习连续向量,让语义相近的词在向量空间里距离更近,这正是 NLP 处理古诗这类高语境文本时最关键的一步。资源里的 vectors_poem.bin 就是训练好的词向量文件,可以用 gensim 直接加载。

from gensim.models import Word2Vec, KeyedVectors sentences = [list(jieba.cut(text)) for text in cleaned_poems] model = Word2Vec(sentences, vector_size=128, window=5, min_count=2, sg=1, epochs=30) model.wv.save_word2vec_format("vectors_poem.bin", binary=True) # 加载已经训练好的词向量 wv = KeyedVectors.load_word2vec_format("vectors_poem.bin", binary=True, encoding="utf-8") print(wv.most_similar("月", topn=10)) print(wv.similarity("春风", "东风"))

训练参数里 sg=1 表示 Skip-gram,在小规模语料上它通常比 CBOW 更稳,因为它对低频词的向量估计更充分;window=5 控制上下文窗口,古诗句子本身短,窗口开太大反而会把前后两句的噪声拉进共现关系;min_count=2 过滤掉只出现一次的生僻字,避免单点噪声污染词典。vector_size=128 对几千首古诗的规模已经够用,加到 256 收益很小,训练时间却明显上升。

加载 .bin 文件时最容易踩的坑是编码参数。训练时 gensim 默认按二进制格式写入,加载时如果不指定 encoding="utf-8",遇到生僻字就会抛 UnicodeDecodeError;更稳妥的做法是再加一个 unicode_errors="ignore" 参数,让个别乱码字符不打断整体加载。加载成功后先跑一下 most_similar,如果“月”的邻居词里出现大量与月无关的字符,说明训练用的语料里混入了噪声,应该回头检查分词和清洗环节,而不是继续往下走。

3.2 情感标签构造与极简情感分类器

语料里若本身有情感标签,直接作为训练目标;如果没有,常见的做法是对一小部分数据做人工标注。我一般会抽 2000 首诗,按“愁、喜、其他”三类打标,不求覆盖全部语料,只求让分类器见过足够多的模式。每条诗需要从词向量变成定长特征:把诗中所有词向量做平均,得到一个 128 维向量,再送进逻辑回归。

import numpy as np from sklearn.linear_model import LogisticRegression def poem_to_vec(tokens, wv): vectors = [wv[word] for word in tokens if word in wv] if not vectors: return np.zeros(wv.vector_size) return np.mean(vectors, axis=0) X = np.array([poem_to_vec(tokens, wv) for tokens in cutted_poems]) y = np.array(labels) clf = LogisticRegression(max_iter=200, class_weight="balanced", C=1.0) clf.fit(X_train, y_train)

class_weight="balanced" 是针对情感类别不均衡的标准处理,它会按类别频率自动放大少数类的损失权重;max_iter=200 保证逻辑回归在小数据集上收敛,默认的 100 有时会报收敛告警。C=1.0 是正则化强度的逆,C 越小正则越强,对 128 维特征来说 1.0 是个不需要怎么调的起点。把词向量直接求均值会丢掉语序信息,所以这个分类器上限不高,但作为机器学习模型的基线已经够用。

3.3 数据集划分:先防数据泄露,再防类别失衡

古诗数据集的划分不能简单 shuffle。常见做法是按朝代或诗人分组:训练集放唐代诗人,验证集和测试集放宋代诗人,这样能避免同一个诗人的风格被同时用于训练和评测,造成虚高的准确率。词向量和情感分类器用的应该是同一份划分,确保模型没有见过测试集里任何诗作。划分后还要检查各类别占比,如果“愁”类占了 70%,只靠 class_weight 还不够,可以在训练时对少数类做简单过采样。

提示:词向量训练完先打印相似词再继续,这是最便宜的体检方式。如果“春风”和“东风”的相似度低于 0.5,说明语料质量或参数设置有问题,这时候回头重做远比训练完再排查省时间。

4. 规则作诗与 RNN 生成:从韵脚约束到模型采样

4.1 规则引擎:让机器先学会押韵

机器学习算法写诗,本质是在学“下一个字是什么”的条件概率分布;但纯概率采样出来的句子经常不押韵,因为语料里五言、七言、不同韵部的分布被平均掉了。规则作诗的价值是提供一个可解释的基准:先确定题目意象,再按韵脚表选字,用固定的句式模板拼装。这一步不追求语义,只保证格式和韵脚基本正确。

# 韵脚词表由第 2 章词频统计结果整理而来 rhyme_table = {"ang": ["光", "长", "香", "霜"], "an": ["山", "寒", "残", "阑"]} def pick_rhyme_word(rhyme, candidates): for word in candidates: if word[-1] in rhyme_table.get(rhyme, set()): return word return random.choice(rhyme_table[rhyme]) # 候选词无匹配时回退到韵脚表

pick_rhyme_word 的逻辑是逐字检查候选词的最后一个字是否落在指定韵部里。回退到 rhyme_table 随机选,是保证规则引擎在遇到生僻意象时不会直接报错中断;代价是回退次数多了,产出的诗句会越来越模板化,这正好把“需要机器学习来救场”的位置暴露出来。

4.2 字级 RNN 生成模型:从数据流到 checkpoint

古诗生成更适合用字级模型而不是词级模型。七言诗每句只有七个字,常用汉字在两千字左右,字级模型既避开了生僻词导致的 OOV 问题,又能让模型自己学到字与字之间的搭配习惯。训练数据是把每首诗切分成字序列,输入“[CLS] 床 前 明 月 光”,输出错位一位的“床 前 明 月 光 [SEP]”,模型的任务就是逐步预测下一个字。

import tensorflow as tf vocab_size = len(char_to_id) embedding_dim = 128 rnn_units = 256 model = tf.keras.Sequential([ # mask_zero=True 让 padding 位不参与 loss 计算 tf.keras.layers.Embedding(vocab_size, embedding_dim, mask_zero=True), tf.keras.layers.LSTM(rnn_units, return_sequences=True), tf.keras.layers.Dense(vocab_size) ]) model.compile(optimizer=tf.keras.optimizers.Adam(1e-3), loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)) model.fit(train_dataset, epochs=30)

Embedding 层把字 ID 映射成 128 维向量,这里的向量是模型自己学出来的,和 Word2Vec 预训练向量是两套东西;LSTM 层的 rnn_units=256 决定隐藏状态容量,古诗语料规模小,256 比 512 更容易收敛。from_logits=True 告诉损失函数 Dense 层输出的是未经 softmax 的 logits,避免在 softmax 后再算一次交叉熵造成数值不稳定。资源里那些 model.ckpt-4000 到 model.ckpt-53493 文件,就是这种结构在不同训练步数时保存下来的权重快照,步数越大通常意味着训练越充分。

加载 checkpoint 时要注意的是,旧版 TensorFlow 1.x 训练的模型需要先实例化完全相同的变量才能恢复权重。常见做法是用 tf.train.Saver 加 Session:

saver = tf.train.Saver() with tf.Session() as sess: saver.restore(sess, "model.ckpt-53493")

如果资源里只有 .data 文件而没有模型结构定义文件,你得自己把 create_model() 原样写出来,先跑一次前向计算让变量初始化,再执行 restore。这个顺序搞反了,报错信息会显示变量找不到或变量名不匹配。

4.3 用温度采样控制随机性,用韵脚约束收尾

模型输出的 logits 不能直接 argmax,否则连续时间步会不断选同一个高概率字,生成的诗句很快就出现“寒”“残”“愁”扎堆。温度采样的作用是把 logits 除以一个系数后再转成概率分布,temperature 小于 1 时分布更尖锐,大于 1 时更平均。

def temperature_sample(logits, temperature=0.8): logits = logits / max(temperature, 1e-8) # 防止 temperature 为 0 导致除零 exp_logits = np.exp(logits - np.max(logits)) # 减去最大值防止上溢出 probs = exp_logits / exp_logits.sum() return np.random.choice(len(probs), p=probs)

对古诗生成,temperature 在 0.8 到 0.95 之间都比较合理。0.8 生成的诗更稳妥但偏平庸,0.95 偶尔会出现惊艳的搭配,代价是整体质量波动大。配合韵脚约束的常见做法是:生成到每句最后一个字时,把所有不押韵候选字的 logits 设为负无穷,强制模型从目标韵部里选字,这样既能保住押韵,又保留模型对内容的选择自由。

5. 常见问题与避坑:四个我翻过车的加载与训练细节

5.1 checkpoint 加载失败:先建模型再 restore

现象:saver.restore 时报错,提示变量找不到或变量名不匹配,有时候还会出现 “Evaluator returned parameters matching a different pattern”。

原因:资源里的 model.ckpt-53493 是训练过程中保存的权重快照,它只记录变量名和值,不包含网络结构。如果加载前没有用完全一致的模型结构初始化变量,TensorFlow 恢复权重时找不到对应变量名,自然就崩了。目录里那些名为 checkpoint 的小文件其实只是索引文本,真正的大体量模型体是 model.ckpt-*.data-00000-of-00001。

解决:先完整定义 create_model(),并让模型在一个 batch 上跑一次前向计算,完成变量初始化后再执行 saver.restore。如果是 TensorFlow 2.x,用 model.load_weights 也要保持同样的顺序,先建模再加载。

5.2 词向量缺词和乱码:分词版本不一致在作祟

现象:加载 vectors_poem.bin 后,most_similar(“月”) 返回的结果里出现“frame”“error”这类英文字符,或者查询某个常见汉字时报 KeyError。

原因:训练词向量时用的 jieba 分词版本和现在使用的版本不一致,同一个句子被切成了不同的 token;另一个可能是加载时没有指定正确的编码参数,生僻字被错误解码成了乱码。

解决:把训练环境里的依赖版本写死到 requirements.txt,至少锁定 jieba 和 gensim 的主版本号。加载 .bin 时固定写上 binary=True, encoding="utf-8", unicode_errors="ignore"。跑之前先打印词表数量,如果键数量比语料里的汉字总量还少,说明分词环节出了问题,先对比前后两版分词的差异。

5.3 生成结果重复字多、韵脚生硬

现象:temperature 调到 1.2 后,生成的句子不断重复“帘”“寒”“残”,韵脚倒是全押上了,但整首诗翻来覆去就是几个意象。

原因:温度过高让概率分布趋于平均,模型在低置信区域乱选,高频字被反复抽中;温度过低又会让采样退化成近似 argmax,同一个时间步总选同一批常用字。另一个深层原因是训练语料只有几千首,模型见过的“寒”“残”搭配太多了,它在概率上确实更偏好这些组合。

解决:把 temperature 固定在 0.85 附近,并对已经生成的字符做惩罚,每次采样前把已出现字符对应的 logits 乘一个小于 1 的系数(比如 0.6),逼着模型换着选字。这个技巧不改变整体分布,但能明显减少同一句里重复字出现的次数。

5.4 情感分析精度卡在 60%:特征太粗糙,类别又失衡

现象:情感分类器在测试集上准确率始终在 60% 左右,而且分类结果大量偏向“愁”类,凡是被分错的喜诗大多被错判成了愁。

原因:词向量求平均特征丢掉了语序信息,“唯见江心秋月白”和“秋月白唯见江心”在平均向量后几乎没有区别;同时训练集里愁诗比例偏高,模型学到了偏向多数类的决策边界,class_weight 已经拉了一把,但特征容量本身限制了上限。

解决:先用朴素过采样把“喜”类样本复制到与“愁”类同等数量级,再用 TF-IDF 加权词向量替代简单平均,权重高的重要字词在句向量中占比更大。如果还想往上提,就要把情感分类从逻辑回归换成带注意力机制的 LSTM 序列模型,让每个时间步的字在输出时获得不同权重。这个改动是机器学习实战里很典型的一次“特征工程 + 模型复杂度”组合升级。

6. 进阶验证:用相似词与困惑度给生成模型体检

深度学习模型的决策过程像个黑匣子,但古诗生成模型可以通过两个外部指标来验证学得好不好:词向量空间的语义质量,以及生成序列的困惑度。这两个指标不需要额外标注数据,跑起来也快,适合放在训练完的第一时间。

词向量语义体检用 gensim 的 most_similar 就能做。我用一个固定检查表,每次训练完对比看结果是否稳定:

查询词期望邻居词检查要点
月明月、霜、雪、秋是否出现自然意象群
愁酒、离别、客、孤是否学到抽象情感的搭配
春风东风、杨柳、花是否区分了季节相关词

如果“愁”的邻居词里出现大量颜色词或数字词,说明语料中混入了噪声上下文,要回头查分词和清洗;如果相似词基本合理,说明词向量空间是可信的,后面所有下游任务都有了基础。

困惑度计算更直接:模型对测试集里每首诗输出的 log 概率取平均,再做指数转换。数值越低,模型对语料的拟合越好。

def perplexity(log_probs): return np.exp(-np.mean(log_probs))

困惑度能反映模型整体的拟合程度,但它有个典型盲区:模型可能只学会了高频套话,比如“花落知多少”这类常见搭配,给出很低困惑度,却无法生成新意。所以困惑度必须要和人工评分结合,我一般会让三个人对 20 首生成诗打“是否押韵、是否有意象、是否通顺”三项,分数和困惑度一起记录。

我把这个项目跑完的教训是:词向量体检、韵脚约束、人工评分这三件事必须同时做,缺一个都会让模型看起来“能写诗”,实际读起来不是空洞就是重复。从那以后每次拿到这种带 checkpoint 和词向量的课程设计资源,我都会先按这套流程验证一遍,确认数据、分词版本和模型结构对得上,再决定要不要训练新模型。希望帮到你。

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

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

图形因果模型:从相关到因果推断的实用指南

1. 为什么相关关系没有说服力&#xff1a;从两个案例说起 去年有个做电商数据分析的朋友来找我&#xff0c;他特别困惑&#xff1a;统计模型显示"页面加载时长"和"用户购买率"的相关系数高达0.82&#xff0c;他们技术团队花了两周把加载时长优化了近一半&a…

作者头像 李华
网站建设 2026/10/6 16:26:57

实时流处理链路实战:四大组件分工与踩坑指南

1. 为什么我劝你别再单点部署流处理链路&#xff1a;一个误判引发的改造先说个真实经历。两年前我负责一个实时运营看板项目&#xff0c;业务方要求"用户点击行为发生后5秒内出现在大屏上"。当时团队图省事&#xff0c;用了最简单的方案&#xff1a;业务系统直接往Ka…

作者头像 李华
网站建设 2026/10/6 16:25:55

Windows 上编译 AirPlay 服务端:源码结构、FFmpeg 解码与避坑指南

简介&#xff1a;这是一份面向Windows平台开发者的AirPlay服务端程序源码包&#xff0c;围绕Air Media Server项目展开&#xff0c;适合具备网络编程与多媒体处理基础、希望自建AirPlay接收端的中高级开发者。资源核心为libairplaysdk与xindawn相关实现&#xff0c;可用于将iOS…

作者头像 李华
网站建设 2026/10/6 16:25:55

基于MCP与Senparc.AI的网页端代码推荐服务实战:从SSE流式到Monaco集成

如果你觉得"网页端 AI 代码推荐 调大模型接口 把结果流式打回去"&#xff0c;那后面大概率会吃大亏。我最初交付的第一版就是这样&#xff1a;编辑器里取几行代码、拼进 prompt、等补全。内测时推荐十次里只有两三次能真正落盘&#xff0c;剩下全在"看图说话&…

作者头像 李华
网站建设 2026/10/6 16:22:43

运维转网安:技能平移与转型实操指南

干运维几年&#xff0c;很多人心里都会冒出一个念头&#xff1a;天天在机房和服务器之间打转&#xff0c;抬头看安全团队的人&#xff0c;总觉得人家做的才是“有门槛的事”。2026年了&#xff0c;我身边越来越多运维老哥开始认真考虑转网安这件事。打开招聘软件一刷&#xff0…

作者头像 李华
网站建设 2026/10/6 16:22:11

反向传播与PyTorch自动求导:从原理到手写实现

新手学深度学习&#xff0c;最容易卡住的地方就是反向传播。代码里一行 loss.backward() &#xff0c;背后却藏着整个神经网络训练的发动机。很多人调了几个月参数&#xff0c;遇到梯度为 NaN 、loss不下降、训练半天不收敛的问题时&#xff0c;回头看反向传播的原理才恍然…

作者头像 李华