PyTorch | 基于GRU模型的古诗AI实践

01 | NLP领域的RNN与GRU

2019年,一款国内少见的真人实拍互动影游《隐形守护者》上线Steam/Mac等多平台,其中有一句台词令自己印象深刻:“在理想主义者眼中,什么都是马尔科夫链。”

当时的自己精神一振,不明觉厉,后查阅资料,发现这里的马尔科夫链正是高等数学所学的Markov模型。最通俗地讲:以时间角度分析先后发生的一系列事件,则可以说时刻t事件的发生由t时刻之前的所有事件共同决定。

仔细想想,这是容易理解并接受的:比如早上起床去上课,陆续经历了【起床穿衣→洗漱→上厕所→准备食用早餐→出门去教室】。我们能否8点前准时出现在教室中,取决于早起后的每一个环节进展是否顺利,如果出门后发现忘记带手机折返,那么无疑会延后到达教室的时间。

人工智能领域中将上述理论称呼为“时间序列模型”,出于工程上实现的考虑,通常不是考察时刻t之前的所有事件,而是分析时刻t-1或时刻t-1、t-2时刻事件对时刻t事件的影响,通常称之为“一阶或二阶Markov链”。

类似的时间序列关系同样出现在自然语言领域(NLP)。如果主语是单数,则谓语可能是“is”;反之则可能是“are”。但是NLP的关联更加复杂,单纯的一阶或二阶Markov链无法刻画较长字符间的关联,如下例中,与问题最相关的“北京”反而“距离”更远。

我出生在北京,大学毕业后来美国攻读博士。毫无疑问,我最擅长的语言是(汉语/英语)。

为了解决上述问题,人们提出了RNN(循环神经网络),其特有的结构可以将特定时间节点的信息向后传递。

典型的神经网络通常包含输入层、隐藏层与输出层;RNN的改进是在输出层之外,新“搭建了一条通道”将当前的信息传递给下一个时刻,即:

在这里插入图片描述

RNN模型一度在NLP领域表现出强大的生机,然而为了更好地模型拟合效果,人们倾向于增加隐藏层数量,而常用的sigmod\tahn之类的激活函数通常最多只有6层左右,反向误差会随着层数增加而越来越小,最终导致无法反向影响到更远的模型参数,结果就是RNN模型无法学习太长的序列特征。

为此人们尝试了两种改进思路:

①使用更复杂的结构作为RNN模型的基本单元,从而使其在单层网络上提取更好的记忆特征;

②将多个基本单元结合起来,组成不同的结构(如多层RNN、双向RNN等)

从第一种思路出发,人们陆续提出了LSTM(Long Short Term Memory)和GRU(Gated Recurrent Unit)模型,后者相对前者进一步简化了输出,且效果差不多,因而本文将选择使用GRU实现古诗AI。

在进一步介绍项目代码前,需要特别说明一点:LSTM/GRU的模型具有空间和时间两个维度:从时间上看,其输入和输出都是一个序列;但是确定时间后从空间上看,则是一个经典的神经网络——关键问题是通过“搭桥”方式建立了不同时间点下神经元信息传递的通路——其信息传递的权重参数则由模型根据数据自动学习迭代(如下图所示)。

在这里插入图片描述

02 | 项目整体架构

为了实现一个基于GRU的古诗AI,我们的小项目整体分为下述几个部分:

①数据预处理:收集古诗数据,并预处理为规范形式;

②映射嵌入向量:将原有的中文字符映射为数值向量;

③数据类初始化:使用torch.utils.data中的Dataset初始化数据,使用Dataloader按批次迭代加载数据;

④模型训练:指定Epoch和每个Epoch的Batch,每个Batch反向迭代一次模型参数;

⑤模型应用:随机指定中文字符,模型自动写诗。

在这里插入图片描述

03 | 古诗数据预处理

网上搜集中文古诗文数据有很多版本,自己采用的是JSON格式版本。因此首要工作就是解析JSON数据以提取古诗文本字符数据。

这里唯一需要注意的是JSON文件以字典形式存放,因而可以循环迭代读取字典,判断其是否具有自己关心的键值即可。

为了后续测试方便,区分为五言古诗和七言绝句,分别提取建立相应的古诗原始数据文件。
简要代码如下:

在这里插入图片描述在这里插入图片描述

04 | 词向量嵌入映射

AI模型输入与输出均应是数值向量模式,那么如何将中文字符转变为数值向量呢?

一种方法是基于“one-hot”的独热编码,基本思路是统计所有出现的中文字符建立大词典,以不同位置设置“1”表示不同的中文字符。好处是容易想到,方便实现;不足是所有的字符间缺乏关联,无法体现字词间的内在关联。

所以更推荐的方法是基于“word embedding”的方式,常见的可以基于图模型或单独的ANN训练得到。为了方便,本文使用的是NLP领域常用的gensim模块中的Word2Vec模块,自动实现字符到数值向量的嵌入映射。

同时为了后续模型应用,我们需要将训练得到的词嵌入矩阵保存下来,推荐使用Python默认的pickle模块即可,因为其可以将Python对象“原封不动”地保存在文件中(当然仅适用于Python)。

在这里插入图片描述

05 | 数据模块

接下来需要为古诗AI模型准备投喂的数据。

按道理应该将所有古诗一次性送入GRU模型进行训练,然后计算总的损失并反向迭代更新模型参数;然而由于样本数据量通常太大,导致算力无法一次性完成计算,因而实际中通常将整体训练过程分为多个Epoch:

①所有的数据都参与过一次训练,称之为一次Epoch;

②由于数据量太大,所以在一次Epoch中,将数据分成不同的Batch,每次向模型输入一个Batch并计算一次反向误差迭代更新;

③如此便成了多个Epoch下多个Batch的计算反向迭代过程。

按照上述描述,我们自然的数据封装顺序应当是【Batchsize→Sequence→Embedding dim】,如下图所示,当Batchsize=3时,一次输入三个句子,每个句子4个字(Squence),每个字又是4嵌入维度,即得到形状如【3,4,4】的张量(Tensor)。

在这里插入图片描述
使用torch.utils.data下Dataset和Dataloader类的好处就是,PyTorch可以自动帮我们从数据集中挑取、组合成【3,4,4】的训练张量和相应的标签张量,从而后续直接调用即可实现模型训练。

在使用Dataset和Dataloader类时有几点需要特别注意:

①使用的类为torch.utils.data下的Dataset和Dataloader

②Dataset类的作用在于加载全部所需的数据(包含初始化),同时定义清楚如何取得一个样本的数据, 因此需要重载__getitem__(self, idx)和__len__(self)两个函数

③Dataloader加载器返回一个可迭代对象,负责按需按批次从数据中取得数据

④注意对于训练输入x需要映射为词向量,对于标签仅停留在index即可,因为后续计算损失时不需要向量,仅index即可

具体可参考如下代码:

在这里插入图片描述

06 | 模型定义与训练

接下来需要定义我们自己的GRU模型,由于PyTorch的模型都源于共同的父类nn.Module,因此需要在定义时建立继承关系。

我们的GRUModel类中主要包含两部分:

①初始化构造函数:首先调用父类的初始化函数,然后将所需数据全部内化为类内数据;接下来定义神经网络结构中各个组成部分,如GRU层、flatten层、dropout层、全连接fc层以及交叉熵损失函数

②定义前向传播函数:主要用于PyTorch构造计算图,按数据流向顺序组合神经网络中定义的各个层即可

模型训练阶段关键是利用Dataloader得到的迭代器,顺序取出相应的数据即可。

在这里插入图片描述

07 | 测试输出

为了验证我们古诗模型的效果,在训练的同时即可周期性打印模型损失,同时还可以随机输入中文字符以生成诗句。具体可参考下述代码:

在这里插入图片描述
在这里插入图片描述当训练到最后时,可以查看输出的诗句,以验证古诗AI的效果,整体看来还是满足押韵的,但是由于自己训练时采用的是单句16字符,因而上下句间关联可能并不明显。

在这里插入图片描述

08 | 结语

作为自己学习使用PyTorch后的第一个入门小项目,中途查阅了不少资料,但是大多缺失了一些关键细节,好在最后终于在不断碰壁中写出了雏形。

现有的项目依然有很大的改进空间:

①古诗训练集按照七言绝句的单句输入(16字符),因而所作诗句上下句缺乏意境关联;

②未尝试“藏头诗”功能,其实思路也相对简单,现有的模式是指定第一个字,连续输出15个字符成为一句;届时可能需要分别指定四句首字,而后生成后续7个字符即可,但是如此以来,四句诗的意境关联何在?

问题留待以后再考虑吧,今天大脑已经要罢工了(#^.^#)

在这里插入图片描述

下面是用 PyTorch 框架构建 GRU(Gated Recurrent Unit)模型进行时间序列预测的示例代码: ```python import torch import torch.nn as nn # 定义 GRU 模型类 class GRUModel(nn.Module): def __init__(self, input_size, hidden_size, output_size): super(GRUModel, self).__init__() self.hidden_size = hidden_size # GRU 层 self.gru = nn.GRU(input_size, hidden_size) # 全连接层 self.fc = nn.Linear(hidden_size, output_size) def forward(self, input): # 初始化隐藏状态 hidden = self.init_hidden() # 前向传播 output, _ = self.gru(input, hidden) output = self.fc(output[-1]) return output def init_hidden(self): # 初始化隐藏状态 return torch.zeros(1, 1, self.hidden_size) # 定义输入、隐藏和输出的维度 input_size = 1 hidden_size = 16 output_size = 1 # 实例化 GRU 模型类 model = GRUModel(input_size, hidden_size, output_size) # 定义损失函数和优化器 criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 训练模型 for epoch in range(num_epochs): # 前向传播 output = model(input) # 计算损失 loss = criterion(output, target) # 反向传播和优化 optimizer.zero_grad() loss.backward() optimizer.step() # 打印训练信息 if (epoch+1) % 100 == 0: print(f'Epoch: {epoch+1}, Loss: {loss.item()}') # 使用训练好的模型进行预测 model.eval() with torch.no_grad(): predicted = model(input) ``` 在这个示例中,我们定义了一个名为 `GRUModel` 的类,它继承自 `nn.Module`。在 `__init__` 方法中,我们定义了 GRU 层和全连接层。在 `forward` 方法中,我们实现了前向传播逻辑。然后我们定义了输入、隐藏和输出的维度,并实例化了这个 GRU 模型。 接下来,我们定义了损失函数和优化器。在训练阶段,我们使用循环迭代进行前向传播、计算损失、反向传播和优化。在每个周期结束时,我们打印训练信息。 最后,我们将模型设置为评估模式,并使用训练好的模型进行预测。 需要根据你的具体问题和数据来调整模型的参数和训练过程。希望这个示例对你有所帮助!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值