告别机械化的“硬语言”让AI学会创作中国古诗前言计算机能够理解“硬语言”——编程语言的语法规则是确定的、无歧义的编译器可以按照机械化的规则解析每一行代码的含义。然而当我们试图让计算机理解汉语或英语这样的“软语言”时传统方法便捉襟见肘了。汉语的含义和形式灵活多变新词层出不穷同一个词语在不同语境下可能表达截然不同的意思。如何让计算机不仅理解语言还能像人类一样“创作”语言这正是本文要深入探讨的核心问题。一、自然语言处理让计算机“学会”人类语言1.1 什么是自然语言处理自然语言处理Natural Language Processing, NLP的目标是让计算机理解人类语言进而完成对人类有帮助的事情。与编程语言的“硬”特性不同自然语言是一种“软语言”——它的含义和形式会灵活变化并且会不断出现新的词语或新的含义。这让NLP成为人工智能领域最具挑战性也最富有魅力的方向之一。要让计算机理解自然语言使用常规方法是无法办到的。传统的前馈神经网络假设输入之间相互独立对于文本这种具有内在序列结构的数据显得力不从心。1.2 词表示方法的演进在深入RNN之前我们需要先理解一个核心问题计算机如何“表示”一个词的语义1基于同义词词典的方法早期NLP采用构建同义词词典的方式。通过将同义词或近义词归入同一类别并根据“整体-部分”或“上位-下位”关系构建层级的树状图形成一个庞大的“单词网络”以此教会计算机单词之间的关系从而计算出单词的“相似度”。这种方法存在明显缺陷需要人工逐个定义单词之间的相关性费时费力新词不断出现语言不断变化词典维护成本极高表现力有限难以覆盖语言的丰富性。2基于计数的方法随着大数据时代的到来研究人员开始从海量文本语料中自动提取语言的“本质”。最基础的做法就是统计“词频”分词对文本内容进行切分找出基本单元词关联ID为每个单词分配唯一ID构建“词表”词向量化用固定长度的向量表示单词也称为词的“分布式表示”。具体做法是统计每个词周围出现了哪些词、出现了多少次称为“上下文”将这些词频统计结果构成向量这个向量就可以表示当前词称为“词向量”word vector。所有词对应的向量汇总起来形成一个矩阵被称为“共现矩阵”co-occurrence matrix。然而这种方法对所有词进行向量化表示的计算复杂度极高难以在大规模语料上实用。3基于推理的方法基于推理的方法另辟蹊径希望在已知上下文的前提下“推测”当前位置的词是什么。利用神经网络接收上下文信息作为输入通过模型计算输出各个单词可能出现的概率从而根据上下文预测该出现的单词。这正是Word2Vec等现代词嵌入技术的核心思想。1.3 词嵌入自然语言的向量化革命词嵌入Word Embedding是将词映射到向量的技术也是让计算机“理解”单词含义的关键一步。词向量是用于表示单词意义的向量可以看作词的特征向量。为什么词嵌入比传统方法更优传统词袋模型存在三大缺陷高维稀疏性词汇表规模达数十万时向量维度爆炸且90%以上元素为0语义缺失无法捕捉词语之间的语义关联性词序忽略完全丢失“狗咬人”与“人咬狗”的语义差异。而词嵌入将词映射为稠密、低维的连续向量使语义相近的词在向量空间中距离更近。以五维词向量为例每个词都用一串浮点数表示词词向量你[0.3307, 0.4814, 1.4244, -0.3161, 0.0859]我[0.9491, -0.1030, 2.5101, 0.1955, -0.0147]他[0.2709, -0.2052, -0.6052, -0.5227, 0.9971]另一种表示方式是独热向量one-hot vector。独热向量很容易构建但它不是一个好的选择任意两个不同词的独热向量之间的余弦相似度为0无法编码词之间的相似性随着词汇量的增大独热向量的大小会线性增长消耗大量存储与计算资源。二、RNN的数学原理与架构解析2.1 为什么需要RNN文本是连续的具有序列特性——词序的改变往往意味着意义的根本变化。比如“狗咬人”和“人咬狗”表达的是完全不同的意思。然而前馈型神经网络feedforward neural network假设输入之间相互独立信息仅在一个方向上传播从输入层到隐藏层再到输出层。虽然这种网络结构简单易懂但它有一个致命问题——不能很好地处理时间序列数据。更确切地说单纯的前馈网络无法充分学习时序数据中蕴含的依赖关系。2.2 RNN的核心原理循环神经网络Recurrent Neural Network, RNN专门设计用于识别序列数据中的模式其核心在于循环连接机制每个时间步的输出不仅依赖于当前输入还依赖于前一个时间步的隐藏状态Hidden State。RNN层具有环路通过这个环路数据可以在层内循环传递。向时序数据 $(x_0, x_1, \dots, x_t)$ 输入层中相应的会输出 $(h_0, h_1, \dots, h_t)$。将RNN的环路按时间步展开后可以看到各个时刻的RNN层接收当前输入 $x_t$ 和前一时刻的输出 $h_{t-1}$隐藏状态根据这两部分信息计算当前时刻的输出 $h_t$。核心数学公式httanh(ht−1WhxtWxb)httanh(ht−1WhxtWxb)其中$h_t$ 是当前时刻的隐藏状态相当于网络的“记忆”$x_t$ 是当前时刻的输入向量$W_h$ 是与前一时刻隐藏状态运算的权重矩阵$W_x$ 是与当前输入运算的权重矩阵$b$ 是偏置项$\tanh$ 是激活函数输出范围在[-1, 1]之间。RNN层的输出 $h_t$ 既作为当前时刻的输出又传递给下一时刻作为“记忆”。这种设计让RNN能够“记住”之前的信息并用这些信息来影响当前时间步的输出。2.3 RNN的参数共享机制RNN的一个关键特性是参数共享在不同时间步之间模型使用相同的权重矩阵 $W_h$ 和 $W_x$。这意味着无论序列有多长模型的总参数量保持不变。这种设计显著减少了模型复杂度并确保模型能够处理任意长度的序列。2.4 RNN的局限性传统RNN并非完美无缺。在处理长序列时RNN面临两个核心问题梯度消失当序列很长时通过时间反向传播BPTT的梯度会逐渐衰减导致远距离依赖信息无法有效传递模型难以捕捉长期依赖关系梯度爆炸在反向传播过程中梯度可能指数级增长导致网络权重不稳定训练难以收敛。为了解决这些问题研究人员提出了LSTM长短期记忆网络和GRU门控循环单元等改进架构。LSTM通过引入遗忘门、输入门和输出门三个门控机制来控制信息流同时维护独立的“细胞状态”以存储长期信息。GRU则是LSTM的简化版本将输入门和遗忘门合并为“更新门”参数量更少、训练更快。三、RNN的PyTorch实现3.1 torch.nn.RNN API详解在PyTorch中torch.nn.RNN类用于构建循环神经网络。其关键参数如下参数含义默认值input_size输入特征的数量必填hidden_size隐藏层的特征数量必填num_layersRNN的层数堆叠层数1nonlinearity激活函数类型可选tanh或relutanhbatch_first是否将batch_size放在输入张量的第一位Falsedropout除最后一层外各层应用dropout的概率0bidirectional是否使用双向RNNFalse调用方式import torch import torch.nn as nn # 初始化RNN层 rnn nn.RNN(input_size8, hidden_size16, num_layers2) # 准备输入数据 # input的形状: [seq_len, batch_size, input_size] input torch.rand(1, 3, 8) # 序列长度1批量大小3特征数8 # 初始化隐藏状态 # hx的形状: [num_layers, batch_size, hidden_size] hx torch.randn(2, 3, 16) # 2层RNN批量大小3隐藏维度16 # 前向传播 output, hn rnn(inputinput, hxhx) print(output.shape) # torch.Size([1, 3, 16]) 输出序列 print(hn.shape) # torch.Size([2, 3, 16]) 最终隐藏状态参数解释num_layers2表示堆叠两个RNN层第二个RNN接收第一个RNN的输出并计算最终结果当batch_firstFalse默认时输入形状为(seq_len, batch_size, input_size)设置batch_firstTrue后形状变为(batch_size, seq_len, input_size)输出output包含每个时间步的隐藏状态形状与输入序列长度对应hn只保留最后一个时间步的隐藏状态。3.2 词嵌入层详解在将文本输入RNN之前需要先将文字转换为数值向量。PyTorch提供了torch.nn.Embedding模块来实现词嵌入embed nn.Embedding(num_embeddingslen(vocab), embedding_dimembedding_dim)num_embeddings词表的大小词汇量embedding_dim词向量的维度即每个词被映射成的向量长度。词嵌入矩阵实际上是一个可训练的参数矩阵形状为[num_embeddings, embedding_dim]。通过词索引查表即可获得对应的词向量word_vec embed(torch.tensor(idx)) # 通过索引获取词向量四、实战案例基于RNN的古诗生成器理论部分讲完了接下来我们用PyTorch完整实现一个古诗生成模型。这个项目将串联前面讲解的所有知识点文本预处理→词嵌入→RNN序列建模→模型训练→文本生成。4.1 数据预处理数据预处理是整个项目的基石。我们需要将原始的诗歌文本转换成模型可以处理的数值形式。import torch import torch.nn as nn import jieba import re from torch.utils.data import Dataset, DataLoader import torch.optim as optim def process_poems(file_path): 古诗数据预处理函数 Args: file_path: 古诗文件路径 Returns: sequences: 每首诗转换后的索引序列列表 word2idx: 词到索引的映射字典 vocab: 词表列表 poems [] # 保存处理后的诗字符列表形式 char_set set() # 保存所有不重复的字 with open(file_path, r, encodingutf-8) as f: for line in f: # 逐行处理去掉标点符号与两侧空白 # 这里我们去掉常见的标点符号逗号、句号、问号、感叹号、冒号 line re.sub(r[。、], , line).strip() # 按字分割并添加到字符集合中用于构建词表 char_set.update(list(line)) # 按字符存储保留顺序 poems.append(list(line)) # 构建词表所有不重复的字符 未知词标记UNK vocab list(char_set) [UNK] # 创建词到索引的映射字典 word2idx {word: idx for idx, word in enumerate(vocab)} # 将每首诗转换为索引序列 sequences [] for poem in poems: seq [word2idx.get(word, word2idx[UNK]) for word in poem] sequences.append(seq) return sequences, word2idx, vocab # 调用预处理函数 sequences, word2idx, vocab process_poems(data/poems.txt)数据集下载https://pan.baidu.com/s/13qx0ximJZUTX8HWQ-kd7AA?pwd7spw代码解读首先读取诗歌文件逐行处理使用正则表达式re.sub删除所有中文标点符号将每首诗按字符分割成列表同时收集所有不重复的字符构建词表添加UNK标记用于处理训练集中未出现的新词最终输出索引序列便于模型读取。4.2 构建训练数据集对于语言模型任务我们需要构建“输入-目标”对给定前seq_len个字符作为输入预测下一个字符作为目标。例如对于诗句“兰叶春葳蕤桂华秋皎洁欣欣此生意自尔为佳节谁知林栖”如果设定seq_len24输入前24个字符“兰叶春葳蕤桂华秋皎洁欣欣此生意自尔为佳节谁知林栖”目标后24个字符“叶春葳蕤桂华秋皎洁欣欣此生意自尔为佳节谁知林栖者”class PoetryDataset(Dataset): 自定义诗歌数据集 继承自torch.utils.data.Dataset实现__len__和__getitem__方法 def __init__(self, sequences, seq_len): Args: sequences: 所有诗的索引序列列表 seq_len: 序列长度每个训练样本包含的字符数 self.seq_len seq_len self.data [] # 对每首诗滑动窗口生成训练样本 for seq in sequences: for i in range(0, len(seq) - self.seq_len): # 输入从i开始的seq_len个字符 # 目标从i1开始的seq_len个字符即预测下一个字符 self.data.append((seq[i:iself.seq_len], seq[i1:i1self.seq_len])) def __len__(self): return len(self.data) def __getitem__(self, idx): x torch.LongTensor(self.data[idx][0]) y torch.LongTensor(self.data[idx][1]) return x, y # 创建数据集实例序列长度设为24 dataset PoetryDataset(sequences, 24)4.3 搭建RNN模型模型架构采用经典的三层结构词嵌入层 → RNN层 → 全连接层。class PoetryRNN(nn.Module): 基于RNN的古诗生成模型 架构词嵌入层 - RNN层 - 全连接输出层 def __init__(self, vocab_size, embedding_dim128, hidden_size256, num_layers1): Args: vocab_size: 词表大小词汇量 embedding_dim: 词嵌入维度将每个词映射为embedding_dim维向量 hidden_size: RNN隐藏层维度 num_layers: RNN堆叠层数 super().__init__() # 1. 词嵌入层将词索引转换为稠密向量 # 形状变换[batch, seq_len] - [batch, seq_len, embedding_dim] self.embed nn.Embedding( num_embeddingsvocab_size, embedding_dimembedding_dim ) # 2. RNN层处理序列数据捕捉时序依赖关系 self.rnn nn.RNN( input_sizeembedding_dim, # 输入特征数 词嵌入维度 hidden_sizehidden_size, # 隐藏状态维度 num_layersnum_layers # RNN堆叠层数 ) # 3. 全连接层将RNN输出映射为词表大小的概率分布 # 形状变换[seq_len, batch, hidden_size] - [seq_len, batch, vocab_size] self.linear nn.Linear( in_featureshidden_size, out_featuresvocab_size ) def forward(self, input, hxNone): 前向传播 Args: input: 输入序列形状 [seq_len, batch_size] hx: 初始隐藏状态形状 [num_layers, batch_size, hidden_size] Returns: output: 输出概率分布形状 [seq_len, batch_size, vocab_size] hidden: 最终隐藏状态形状 [num_layers, batch_size, hidden_size] # Step 1: 词嵌入 embed self.embed(input) # [seq_len, batch, embedding_dim] # Step 2: RNN处理 output, hidden self.rnn(embed, hx) # output: [seq_len, batch, hidden_size] # Step 3: 全连接层输出 output self.linear(output) # [seq_len, batch, vocab_size] return output, hidden # 设备配置优先使用GPU加速 device torch.device(cuda if torch.cuda.is_available() else cpu) print(f当前使用设备: {device}) # 实例化模型 model PoetryRNN( vocab_sizelen(vocab), # 词表大小 embedding_dim256, # 词嵌入维度 hidden_size512, # 隐藏层维度 num_layers2 # RNN层数 ).to(device)模型架构解读词嵌入层将离散的词索引转换为连续的稠密向量让模型能够学习词之间的语义关系RNN层核心序列处理模块通过循环连接保持“记忆”捕捉诗歌中的韵律和语义依赖全连接层将RNN的输出映射为词表大小的向量经过softmax后即为各词的概率分布。4.4 模型训练训练语言模型本质上是一个多分类任务在每个时间步模型需要从整个词表中选择正确的下一个词。def train(model, dataset, lr, epoch_num, batch_size, device): 模型训练函数 Args: model: 待训练的模型 dataset: 训练数据集 lr: 学习率 epoch_num: 训练轮数 batch_size: 批量大小 device: 计算设备CPU/GPU model.train() # 设置为训练模式启用dropout等 # 创建数据加载器自动进行批次划分和随机打乱 dataloader DataLoader(dataset, batch_sizebatch_size, shuffleTrue) # 损失函数交叉熵损失 # 适用于多分类问题内部结合了LogSoftmax和NLLLoss loss_fn nn.CrossEntropyLoss() # 优化器Adam # Adam结合了动量和自适应学习率的优点是深度学习的默认选择 optimizer optim.Adam(model.parameters(), lrlr) for epoch in range(epoch_num): loss_accumulate 0 # 累加当前epoch的损失 for batch_count, (x, y) in enumerate(dataloader): # 将数据移动到指定设备 x, y x.to(device), y.to(device) # x.shape: [batch, seq_len] # y.shape: [batch, seq_len] # ----- 前向传播 ----- # 需要将x转置为[seq_len, batch]格式因为RNN默认batch_firstFalse output, _ model(x.transpose(0, 1)) # output.shape: [seq_len, batch, vocab_size] # ----- 计算损失 ----- # CrossEntropyLoss期望输入形状为[batch, vocab_size]或[seq_len*batch, vocab_size] # 因此将output转置为[batch, seq_len, vocab_size]再计算 loss_value loss_fn(output.transpose(0, 1).transpose(1, 2), y) # ----- 反向传播 ----- optimizer.zero_grad() # 清空梯度 loss_value.backward() # 计算梯度 optimizer.step() # 更新参数 # 累加损失用于监控 loss_accumulate loss_value.item() # 打印训练进度 print(fEpoch {epoch:02d} | Loss: {loss_accumulate/len(dataloader):.6f}) # 开始训练 train( modelmodel, datasetdataset, lr1e-3, # 学习率 epoch_num20, # 训练20轮 batch_size32, # 批量大小 devicedevice )训练细节解析交叉熵损失CrossEntropyLoss衡量模型预测的概率分布与真实标签分布之间的差异。在分类问题中交叉熵损失能够准确反映模型的预测质量。Adam优化器Adam能够根据训练过程动态调整学习率——训练初期梯度噪声较大时进行较大幅度调整训练稳定后进行精细微调。这使Adam成为深度学习的首选优化器。维度转置由于RNN默认batch_firstFalse输入需要是[seq_len, batch]格式因此调用x.transpose(0, 1)进行转置。4.5 文本生成模型训练完成后就可以让它“创作”诗歌了。生成过程采用自回归方式每生成一个字符后将其作为输入继续生成下一个字符直到达到预设长度。def generate_poem(model, word2idx, vocab, start_token, line_num4, line_length7): 生成古诗 Args: model: 训练好的模型 word2idx: 词到索引的映射 vocab: 词表 start_token: 起始字符 line_num: 生成的行数 line_length: 每行诗句的字数 Returns: 生成的诗句字符串 model.eval() # 设置为评估模式禁用dropout和batch norm poem [] # 记录生成结果 current_line_length line_length # 当前句剩余需要生成的字符数 # 获取起始字符的索引 start_idx word2idx.get(start_token, word2idx[UNK]) # 如果起始字符在词典中添加到结果列表 if start_idx ! word2idx[UNK]: poem.append(vocab[start_idx]) current_line_length - 1 # 初始化输入张量和隐藏状态 input_tensor torch.LongTensor([[start_idx]]).to(device) # [1, 1] hidden None # 初始隐藏状态为NoneRNN内部会自动初始化为全零 with torch.no_grad(): # 禁用梯度计算加速推理并节省显存 for _ in range(line_num): # 遍历每一行 for _ in range(2): # 每行两句 while current_line_length 0: # 生成一句中的每个字 # 前向传播 output, hidden model(input_tensor, hidden) # output.shape: [1, 1, vocab_size] # 计算概率分布并采样 # squeeze(0)去掉序列维度softmax转换为概率 prob torch.softmax(output[0, 0], dim-1) # 从概率分布中随机采样增加生成多样性 next_token torch.multinomial(prob, 1) # 记录生成的字符 poem.append(vocab[next_token.item()]) # 更新输入为刚生成的字符 input_tensor next_token.unsqueeze(0) # [1, 1] current_line_length - 1 # 一句结束重置计数器并添加标点 current_line_length line_length poem.append() # 单句结尾添加逗号 poem.append(\n) # 每行结束后换行 # 注意这里需要重置current_line_length但上面已经重置了 # 将列表转换为字符串并返回 return .join(poem) # 测试生成 generated_poem generate_poem( model, word2idx, vocab, start_token春, # 以春字开篇 line_num4, # 生成4行 line_length7 # 每行7字七言绝句 ) print(generated_poem)生成策略解析自回归生成模型每次只预测下一个字符然后将预测结果作为下一时间步的输入如此循环直至生成完整诗歌随机采样使用torch.multinomial从概率分布中随机采样而非总是选择概率最高的字符。这样做可以增加生成结果的多样性避免重复输出同一内容温度系数调优可选改进在实际应用中可以通过softmax(logits / temperature)来控制生成的“创造性”。temperature越高生成结果越多样但也可能更不连贯temperature越低生成结果越保守但也可能更稳定。五、进阶优化方向5.1 使用LSTM/GRU替代标准RNN前文提到标准RNN存在梯度消失问题。一个简单的改进是用LSTM或GRU替换RNN层# 使用LSTM替代RNN self.lstm nn.LSTM( input_sizeembedding_dim, hidden_sizehidden_size, num_layersnum_layers ) # 或使用GRU更轻量训练更快 self.gru nn.GRU( input_sizeembedding_dim, hidden_sizehidden_size, num_layersnum_layers )LSTM vs GRU 选择指南LSTM通过遗忘门、输入门和输出门三个门控机制控制信息流并维护独立的细胞状态能够稳定捕捉长距离依赖适合长序列任务GRULSTM的简化版本合并输入门和遗忘门为更新门参数量减少约25-30%训练速度快30-40%在多数任务上性能与LSTM相当。5.2 双向RNN如果任务需要同时利用前文和后文信息如命名实体识别可以使用双向RNNself.rnn nn.RNN( input_sizeembedding_dim, hidden_sizehidden_size, num_layersnum_layers, bidirectionalTrue # 启用双向 ) # 注意bidirectionalTrue时hidden_size会翻倍5.3 模型调优技巧增加模型深度通过增加num_layers参数堆叠多层RNN可以增强模型的表达能力使用Dropout正则化在RNN层间添加dropout防止过拟合梯度裁剪使用torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5)防止梯度爆炸学习率调度使用torch.optim.lr_scheduler在训练过程中动态调整学习率。六、总结本文从自然语言处理的基础概念出发系统讲解了词嵌入技术和循环神经网络RNN的核心原理并完整实现了一个基于PyTorch的古诗生成系统。总结要点如下1. 词嵌入是NLP的基石。相比传统独热编码稠密词向量能够捕捉词语间的语义相似性是实现计算机“理解”语言的关键技术。2. RNN通过循环机制解决序列建模问题。其核心公式 $h_t \tanh(h_{t-1}W_h x_tW_x b)$ 精妙地实现了信息的传递与记忆。尽管存在梯度消失/爆炸的局限性但RNN及其变体LSTM、GRU仍是序列处理任务的基石。3. 古诗生成是一个经典的序列生成任务。通过“词嵌入层 → RNN层 → 全连接层”的标准架构配合滑动窗口构建训练样本可以训练出具有一定创作能力的“AI诗人”。4. 自回归生成策略与随机采样是文本生成的核心技巧在保证语义连贯性的同时增加了生成结果的多样性。随着大模型时代的到来Transformer架构凭借其并行计算能力和全局依赖捕捉能力在许多NLP任务中取代了RNN。但RNN轻量、高效、易于理解的特点使其在教育、边缘计算和资源受限场景中依然具有不可替代的价值。理解RNN的原理也为学习更复杂的Transformer模型打下了坚实的基础。