🍔 RNN 概述
1.1 循环神经网络
🐼 循环神经网络(Recurrent Nearal Networks, RNN)是一种专门用于处理序列数据的神经网络架构。它通过引入循环连接,使得网络能够捕捉序列中的时间依赖性和上下文信息。
在RNN中,每个时间步的隐藏层不仅接收当前输入,还接收来自上一时间步隐藏层的输出,这种机制允许网络“记忆”过去的信息,从而有效处理如文本、语音、时间序列等序列数据。
RNN广泛应用于自然语言处理(如机器翻译、情感分析)、语音识别、时间序列预测等领域,展现了强大的序列建模能力。
1.2 自然语言处理
🐼 自然语言处理(Nature language Processing, NLP)研究的主要是通过计算机算法来理解自然语言。对于自然语言来说,处理的数据主要就是人类的语言,例如:汉语、英语、法语等,由于该类型的数据不像我们前面接触的过的结构化数据、或者图像数据可以很方便的进行数值化。所以,在本章节,我们主要学习如何将文本数据进行数值化的词嵌入技术、以及如何对文本数据建模的循环网络模型。
最后,我们会通过使用学习到的技术完成文本生成任务,即:输入一个词,由模型预测出指定长度的歌词。
🍔 词嵌入层
学习目标
🍀 知道词嵌入概念
🍀 掌握PyTorch词嵌入api
我们在进行文本数据处理时,需要将文本进行数据值化,然后进行后续的训练工作。词嵌入层的作用就是将文本转换为向量的。
2.1 词嵌入层的使用
词嵌入层首先会根据输入的词的数量构建一个词向量矩阵,例如: 我们有 100 个词,每个词希望转换成 128 维度的向量,那么构建的矩阵形状即为: 100*128,输入的每个词都对应了一个该矩阵中的一个向量。
在 PyTorch 中,我们可以使用 nn.Embedding 词嵌入层来实现输入词的向量化。接下来,我们将会学习如何将词转换为词向量。
其步骤如下:
💘 先将语料进行分词,构建词与索引的映射,我们可以把这个映射叫做词表,词表中每个词都对应了一个唯一的索引;
💘 然后使用 nn.Embedding 构建词嵌入矩阵,词索引对应的向量即为该词对应的数值化后的向量表示。
例如,我们的文本数据为: "北京冬奥的进度条已经过半,不少外国运动员在完成自己的比赛后踏上归途。",接下来,我们看下如何使用词嵌入层将其进行转换为向量表示。
步骤如下:
💘 首先,将文本进行分词;
💘 然后,根据词构建词表;
💘 最后,使用嵌入层将文本转换为向量表示。
nn.Embedding 对象构建时,最主要有两个参数:
- num_embeddings 表示词的数量
- embedding_dim 表示用多少维的向量来表示每个词
nn.Embedding(num_embeddings=10, embedding_dim=4)
接下来,我们就实现下刚才的需求💯 :
import torch import torch.nn as nn import jieba if __name__ == '__main__': text = '北京冬奥的进度条已经过半,不少外国运动员在完成自己的比赛后踏上归途。' # 1. 文本分词 words = jieba.lcut(text) print('文本分词:', words) # 2. 构建词表 index_to_word = {} word_to_index = {} # 分词去重并保留原来的顺序 unique_words = list(set(words)) for idx, word in enumerate(unique_words): index_to_word[idx] = word word_to_index[word] = idx # 3. 构建词嵌入层 # num_embeddings: 表示词表词的总数量 # embedding_dim: 表示词嵌入的维度 embed = nn.Embedding(num_embeddings=len(index_to_word), embedding_dim=4) # 4. 文本转换为词向量表示 print('-' * 82) for word in words: # 获得词对应的索引 idx = word_to_index[word] # 获得词嵌入向量 word_vec = embed(torch.tensor(idx)) print('%3s\t' % word, word_vec)
程序输出结果💯 :
文本分词: ['北京', '冬奥', '的', '进度条', '已经', '过半', ',', '不少', '外国', '运动员', '在', '完成', '自己', '的', '比赛', '后', '踏上', '归途', '。'] ---------------------------------------------------------------------------------- 北京 tensor([-0.9270, -0.2379, -0.6142, -1.4764], grad_fn=<EmbeddingBackward>) 冬奥 tensor([ 0.3541, -0.4493, 0.7205, 0.1818], grad_fn=<EmbeddingBackward>) 的 tensor([-0.4832, -1.4191, 0.6283, 0.0977], grad_fn=<EmbeddingBackward>) 进度条 tensor([ 1.4518, -0.3859, -0.6866, 1.1921], grad_fn=<EmbeddingBackward>) 已经 tensor([ 0.3793, 1.6623, -0.2279, -0.2272], grad_fn=<EmbeddingBackward>) 过半 tensor([ 0.0732, 1.4832, -0.7802, 0.6884], grad_fn=<EmbeddingBackward>) , tensor([ 0.6126, 1.0175, -0.4427, 0.6719], grad_fn=<EmbeddingBackward>) 不少 tensor([ 1.0787, -0.2942, -1.0300, -0.6026], grad_fn=<EmbeddingBackward>) 外国 tensor([-0.0484, -0.1542, 1.0033, -1.2332], grad_fn=<EmbeddingBackward>) 运动员 tensor([-1.3133, 0.3807, 0.3957, 1.1283], grad_fn=<EmbeddingBackward>) 在 tensor([ 0.0146, -1.7078, -0.9399, 1.5368], grad_fn=<EmbeddingBackward>) 完成 tensor([-0.1084, -0.0734, -0.1800, -0.5065], grad_fn=<EmbeddingBackward>) 自己 tensor([ 0.8480, -0.4750, -0.1357, 0.4134], grad_fn=<EmbeddingBackward>) 的 tensor([-0.4832, -1.4191, 0.6283, 0.0977], grad_fn=<EmbeddingBackward>) 比赛 tensor([0.0928, 0.8925, 1.1197, 2.5525], grad_fn=<EmbeddingBackward>) 后 tensor([ 0.8835, 0.7304, 1.3754, -1.7842], grad_fn=<EmbeddingBackward>) 踏上 tensor([ 1.0809, -0.3135, 0.6346, 0.3923], grad_fn=<EmbeddingBackward>) 归途 tensor([ 0.1834, -1.2411, -0.9244, -0.0265], grad_fn=<EmbeddingBackward>) 。 tensor([ 0.0290, 0.1881, -1.3138, 0.6514], grad_fn=<EmbeddingBackward>)
2.2 关于词嵌入层的思考
我们的词嵌入层默认使用的是均值为 0,标准差为 1 的正态分布进行初始化,也可以理解为是随机初始化。
🐻 有些同学可能就想,这个用来表示词的文本真的能够表达出词的含义吗?
- nn.Embedding 中对每个词的向量表示都是随机生成的
- 当一个词输入进来之后,会使用随机产生的向量来表示该词
- 该词向量参与到下游任务的计算
- 下游任务计算之后,会和目标结果进行对比产生损失
- 接下来,通过反向传播更新所有的网络参数,这里的参数就包括了 nn.Embedding 中的词向量表示
这样通过反复的前向计算、反向传播、参数更新,最终我们每个词的向量表示就会变得更合理。
2.3 小节
🍬 本小节主要讲解了在自然语言处理任务中,经常使用的词嵌入层的使用。它的主要作用就是将输入的词映射为词向量,便于在网络模型中进行计算。
🍬 需要注意的是,:词嵌入层中的向量表示是可学习的,并不是固定不变的。
🍔 循环网络层
学习目标
🍀 掌握RNN网络原理
🍀 掌握PyTorch RNN api
我们前面学习了词嵌入层,可以将文本数据映射为数值向量,进而能够送入到网络进行计算。但是,还存在一个问题,文本数据是具有序列特性的,例如: "我爱你", 这串文本就是具有序列关系的,"爱" 需要在 "我" 之后,"你" 需要在 "爱" 之后, 如果颠倒了顺序,那么可能就会表达不同的意思。
为了能够表示出数据的序列关系我们需要使用循环神经网络(Recurrent Nearal Networks, RNN) 来对数据进行建模,RNN 是一个具有记忆功能的网络,它作用于处理带有序列特点的样本数据。
本小节,我们将会带着大家深入学习 RNN 循环网络层的原理、计算过程,以及在 PyTorch 中如何使用 RNN 层。
3.1 RNN 网络原理
3.1.1 RNN计算过程
当我们希望使用循环网络来对 "我爱你" 进行语义提取时,RNN 是如何计算过程是什么样的呢?
上图中 h 表示隐藏状态, 每一次的输入都会有包含两个值: 上一个时间步的隐藏状态、当前状态的输入值,输出当前时间步的隐藏状态。
上图中,为了更加容易理解,虽然我画了 3 个神经元, 但是实际上只有一个神经元,"我爱你" 三个字是重复输入到同一个神经元中。
接下来,我们举个例子来理解上图的工作过程,假设我们要实现文本生成,也就是输入 "我爱" 这两个字,来预测出 "你",其如下图所示:
我们将上图展开成不同时间步的形式,如下图所示:
我们首先初始化出第一个隐藏状态,一般都是全0的一个向量,然后将 "我" 进行词嵌入,转换为向量的表示形式,送入到第一个时间步,然后输出隐藏状态 h1,然后将 h1 和 "爱" 输入到第二个时间步,得到隐藏状态 h2, 将 h2 送入到全连接网络,得到 "你" 的预测概率。
那么,你可能会想,循环网络只能有一个神经元吗?
我们的循环网络网络可以有多个神经元,如下图所示:
我们依次将 "我爱你" 三个字分别送入到每个神经元进行计算,假设词嵌入时,"我爱你" 的维度为 128,经过循环网络之, "我爱你" 三个字的词向量维度就会变成 4. 所以, 我们理解了循环神经网络的的神经元个数会影响到输出的数据维度。
3.1.2 如何计算神经元内部
上述公式中:
- Wih 表示输入数据的权重
- bih 表示输入数据的偏置
- Whh 表示输入隐藏状态的权重
- bhh 表示输入隐藏状态的偏置
最后对输出的结果使用 tanh 激活函数进行计算,得到该神经元你的输出。
3.2 PyTorch RNN 层的使用
接下来,我们学习 PyTorch 的 RNN 层的用法.
注意: RNN 层输入的数据为三个维度: (seq_len, batch_size, input_size).
示例代码如下💯 :
import torch import torch.nn as nn # 1. RNN 送入单个数据 def test01(): # 输入数据维度 128, 输出维度 256 rnn = nn.RNN(input_size=128, hidden_size=256) # 第一个数字: 表示句子长度 # 第二个数字: 批量个数 # 第三个数字: 表示数据维度 inputs = torch.randn(1, 1, 128) hn = torch.zeros(1, 1, 256) output, hn = rnn(inputs, hn) print(output.shape) print(hn.shape) # 2. RNN层送入批量数据 def test02(): # 输入数据维度 128, 输出维度 256 rnn = nn.RNN(input_size=128, hidden_size=256) # 第一个数字: 表示句子长度 # 第二个数字: 批量个数 # 第三个数字: 表示数据维度 inputs = torch.randn(1, 32, 128) hn = torch.zeros(1, 32, 256) output, hn = rnn(inputs, hn) print(output.shape) print(hn.shape) if __name__ == '__main__': test01() test02()
程序输出结果💯 :
torch.Size([1, 1, 256]) torch.Size([1, 1, 256]) torch.Size([1, 32, 256]) torch.Size([1, 32, 256])
3.3 小节
🍬 在本章节中我们学习了 RNN 层及其原理,并学习了 PyTorch 中 RNN 网络层的基本使用。
🍔 案例-文本生成
学习目标
🍀 掌握文本生成模型构建流程
文本生成任务是一种常见的自然语言处理任务,输入一个开始词能够预测出后面的词序列。本案例将会使用循环神经网络来实现周杰伦歌词生成任务。
数据集如下:
想要有直升机
想要和你飞到宇宙去
想要和你融化在一起
融化在宇宙里
我每天每天每天在想想想想著你
这样的甜蜜
让我开始相信命运
感谢地心引力
让我碰到你
漂亮的让我面红的可爱女人
数据集共有 5819 行。
4.1 构词词典
我们在进行自然语言处理任务之前,首要做的就是就是构建词表。所谓的词表就是将语料进行分词,然后给每一个词分配一个唯一的编号,便于我们送入词嵌入层。
最终,我们的词典主要包含了:
- word_to_index: 存储了词到编号的映射
- index_to_word: 存储了编号到词的映射
一般构建词表的流程如下:
- 语料清洗, 去除不相关的内容
- 对语料进行分词
- 构建词表
接下来, 我们对周杰伦歌词的语料数据按照上面的步骤构建词表💯:
# 构建词典 def build_vocab(): file_name = 'data/jaychou_lyrics.txt' # 1. 清洗文本 clean_sentences = [] for line in open(file_name, 'r'): line = line.replace('〖韩语Rap译文〗','') # 去除中文、英文、数字、部分标点符号外的其他字符 line = re.sub(r'[^\u4e00-\u9fa5 a-zA-Z0-9!?,]', '', line) # 连续空格替换成1个 line = re.sub(r'[ ]{2,}', '', line) # 去除两侧空格、换行 line = line.strip() # 去除单字的行 if len(line) <= 1: continue # 去除重复行 if line not in clean_sentences: clean_sentences.append(line) # 2. 预料分词 index_to_word, all_sentences = [], [] for line in clean_sentences: words = jieba.lcut(line) all_sentences.append(words) for word in words: if word not in index_to_word: index_to_word.append(word) # 词到索引映射 word_to_index = {word: idx for idx, word in enumerate(index_to_word)} # 词的数量 word_count = len(index_to_word) # 句子索引表示 corpus_idx = [] for sentence in all_sentences: temp = [] for word in sentence: temp.append(word_to_index[word]) # 在每行歌词之间添加空格隔开 temp.append(word_to_index[' ']) corpus_idx.extend(temp) return index_to_word, word_to_index, word_count, corpus_idx def test01(): index_to_word, word_to_index, word_count, corpus_idx = build_vocab() print(word_count) print(index_to_word) print(word_to_index) print(corpus_idx)
4.2 构建数据集对象
我们在训练的时候,为了便于读取语料,并送入网络,所以我们会构建一个 Dataset 对象,并使用该对象构建 DataLoader 对象,然后对 DataLoader 对象进行迭代可以获取语料,并将其送入网络。
话不多说,代码演示💯:
class LyricsDataset: def __init__(self, corpus_idx, num_chars): # 语料数据 self.corpus_idx = corpus_idx # 语料长度 self.num_chars = num_chars # 词的数量 self.word_count = len(self.corpus_idx) # 句子数量 self.number = self.word_count // self.num_chars def __len__(self): return self.number def __getitem__(self, idx): # 修正索引值到: [0, self.word_count - 1] start = min(max(idx, 0), self.word_count - self.num_chars - 2) x = self.corpus_idx[start: start + self.num_chars] y = self.corpus_idx[start + 1: start + 1 + self.num_chars] return torch.tensor(x), torch.tensor(y) def test02(): _, _, _, corpus_idx = build_vocab() lyrics = LyricsDataset(corpus_idx, 5) lyrics_dataloader = DataLoader(lyrics, shuffle=False, batch_size=1) for x, y in lyrics_dataloader: print('x:', x) print('y:', y) break
4.3 构建网络模型
我们用于实现《歌词生成》的网络模型,主要包含了三个层:
- 词嵌入层: 用于将语料转换为词向量
- 循环网络层: 提取句子语义
- 全连接层: 输出对词典中每个词的预测概率
我们前面学习了 Dropout 层,它具有正则化作用,所以在我们的网络层中,我们会对词嵌入层、循环网络层的输出结果进行 Dropout 计算。
示例代码如下💯 :
class TextGenerator(nn.Module): def __init__(self, vocab_size): super(TextGenerator, self).__init__() # 初始化词嵌入层 self.ebd = nn.Embedding(vocab_size, 128) # 循环网络层 self.rnn = nn.RNN(128, 128, 1) # 输出层 self.out = nn.Linear(128, vocab_size) def forward(self, inputs, hidden): # 输出维度: (1, 5, 128) embed = self.ebd(inputs) # 正则化层 embed = F.dropout(embed, p=0.2) # 修改维度: (5, 1, 128) output, hidden = self.rnn(embed.transpose(0, 1), hidden) # 正则化层 embed = F.dropout(output, p=0.2) # 输入维度: (5, 128) # 输出维度: (5, 5682) output = self.out(output.squeeze()) return output, hidden def init_hidden(self): return torch.zeros(1, 1, 128) def test03(): index_to_word, word_to_index, word_count, corpus_idx = build_vocab() _, _, _, corpus_idx = build_vocab() lyrics = LyricsDataset(corpus_idx, 5) lyrics_dataloader = DataLoader(lyrics, shuffle=False, batch_size=1) model = TextGenerator(word_count) for x, y in lyrics_dataloader: hidden = model.init_hidden() print(x.shape) model(x, hidden) break
4.4 构建训练函数
前面的准备工作完成之后, 我们就可以编写训练函数。训练函数主要负责编写数据迭代、送入网络、计算损失、反向传播、更新参数,其流程基本较为固定。
由于我们要实现文本生成,文本生成本质上,输入一串文本,预测下一个文本,也属于分类问题,所以,我们使用多分类交叉熵损失函数。优化方法我们学习过 SGB、AdaGrad、Adam 等,在这里我们选择学习率、梯度自适应的 Adam 算法作为我们的优化方法。
训练完成之后,我们使用 torch.save 方法将模型持久化存储。
def train(): # 构建词典 index_to_word, word_to_index, word_count, corpus_idx = build_vocab() # 数据集 lyrics = LyricsDataset(corpus_idx, 32) # 初始化模型 model = TextGenerator(word_count) # 损失函数 criterion = nn.CrossEntropyLoss() # 优化方法 optimizer = optim.Adam(model.parameters(), lr=1e-3) # 训练轮数 epoch = 200 # 迭代打印 iter_num = 300 # 训练日志 train_log = 'lyrics_training.log' file = open(train_log, 'w') # 开始训练 for epoch_idx in range(epoch): # 数据加载器 lyrics_dataloader = DataLoader(lyrics, shuffle=True, batch_size=1) # 训练时间 start = time.time() # 迭代次数 iter_num = 0 # 训练损失 total_loss = 0.0 for x, y in lyrics_dataloader: # 隐藏状态 hidden = model.init_hidden() # 模型计算 output, hidden = model(x, hidden) # 计算损失 loss = criterion(output, y.squeeze()) # 梯度清零 optimizer.zero_grad() # 反向传播 loss.backward() # 参数更新 optimizer.step() iter_num += 1 total_loss += loss.item() message = 'epoch %3s loss: %.5f time %.2f' % \ (epoch_idx + 1, total_loss / iter_num, time.time() - start) print(message) file.write(message + '\n') file.close() # 模型存储 torch.save(model.state_dict(), 'model/lyrics_model_%d.bin' % epoch)
4.5 构建预测函数
到了最后一步,我们从磁盘加载训练好的模型,进行预测。预测函数,输入第一个指定的词,我们将该词输入网路,预测出下一个词,再将预测的出的词再次送入网络,预测出下一个词,以此类推,知道预测出我们指定长度的内容。
def predict(start_word, sentence_length): # 构建词典 index_to_word, word_to_index, word_count, _ = build_vocab() # 构建模型 model = TextGenerator(vocab_size=word_count) # 加载参数 model.load_state_dict(torch.load('model/lyrics_model_200.bin')) # 隐藏状态 hidden = model.init_hidden() # 词转换为索引 word_idx = word_to_index[start_word] generate_sentence = [word_idx] for _ in range(sentence_length): output, hidden = model(torch.tensor([[word_idx]]), hidden) word_idx = torch.argmax(output) generate_sentence.append(word_idx) for idx in generate_sentence: print(index_to_word[idx], end='') print() if __name__ == '__main__': predict('分手', 50)
程序运行结果:
分手的话像语言暴力 我已无能为力再提起 决定中断熟悉 周杰伦 周杰伦 一步两步三步四步望著天 看星星 一颗两颗三颗四颗 连成线一步两步三步四步望著天 看星星 一颗两颗三颗四颗
4.6 小节
本小节,带着大家使用学习到的循环神经网络的知识,构建了一个《歌词生成》的项目。
该项目的实现流程如下:
🍬 构建词汇表
🍬 构建数据对象
🍬 编写网络模型
🍬 编写训练函数
🍬 编写预测函数
佬~若文章对您有帮助,留下您的关注哟~您的关注是我最大的动力!