前言
笔者写下此系列文章是希望在复习人工智能相关知识同时为想学此技术的人提供一定帮助。
本文不讲述如泛化,前向后向传播,过拟合等基础概念。
本文图片来源于网络,图片所有者可以随时联系笔者删除。
本文提供代码不代表该神经网络的全部实现,只是为了方便展示此模型的关键结构。
结合前系列有关CNN的文章,会发现卷积神经网络CNN的结构在处理图像和视觉识别任务方面似乎就是天配,不过对于文字领域呢,好像CNN的结构并不能很好用于序列数据(文字,音频),这个领域,就是RNN的主场。
与CNN不同,RNN被设计用来处理序列数据。它通过在网络的隐藏层中引入循环,使网络能够保留前一个状态的信息,并将这些信息用于当前状态的计算。这种设计使RNN特别适合处理语言翻译、自然语言处理、语音识别等需要理解数据序列中时间相关性的任务。RNN的这种能力来自于它的内部结构,允许它在处理每个序列元素时考虑之前的信息,从而捕捉到数据中的长期依赖。
正文
概述
RNN的核心概念
RNN的核心理念在于“记忆”。与传统的神经网络不同,它可以将之前处理的信息传递给下一步的处理过程。想象一下,你正在阅读一篇文章,理解每个句子时都会记得前面的内容,RNN正是通过类似的机制来处理数据的。
RNN的结构
RNN的结构可以简化为一个由节点(或称为神经元)组成的循环。在处理序列的每个元素时,这些节点会接收两个输入:当前元素和前一个元素的“记忆”。通过这种方式,网络能够“记住”之前处理过的信息,并利用这些信息来影响后续的处理过程。
RNN的应用
- 自然语言处理(NLP):如机器翻译、情感分析、语言模型等。
- 语音识别:将口语转换成文本。
- 时间序列预测:如股市预测、天气预报等。
RNN的挑战
一个很重要的问题是“长期依赖”问题。随着序列的增长,RNN越来越难以学习到距离当前点很远的点的信息。为了解决这个问题发展了一些更高级的RNN变体,经典的有如长短期记忆网络(LSTM)和门控循环单元(GRU),它们能够更有效地捕捉长期依赖。
深入
下图很好的揭示了RNN的结构
此结构RNN的输入同输出数一致,有时会有RNN的输出比输入多,或者输出比输入多(翻译任务),如下结构最容易理解。
其中隐藏状态是RNN在处理序列的每个时间点时所维护的内部状态,这些状态包含了之前时间点的信息。
但是我们知道,CNN可以用于图像,是因为图像本身就是一堆0101可以由计算机理解的内容,那文字呢。
单词编码(One-hot Encoding)后续再说更好的方式
最简单的文本表示方法。每个单词被赋予一个唯一的索引,然后根据这个索引在相应的位置放置1,其他位置为0。
例如,如果我们有一个词汇表{"the": 1, "cat": 2, "sat": 3},那么句子"the cat"可以被编码为[[1, 0, 0], [0, 1, 0]]。
这种方法简单直观,但是随着词汇表的扩大,向量会变得非常稀疏,这对于模型来说是低效的。
随机采样与相邻采样
- 随机采样和相邻采样是在处理序列数据,特别是在训练语言模型和其他基于序列的模型时,用来生成训练样本的两种策略。这两种采样方法决定了如何从一长串数据中提取出子序列来训练模型。
- 在随机采样中,每次从原始序列中随机选择一个子序列的起点。然后从这个起点开始,按照指定的序列长度提取子序列。这种方法的特点是每个批次中的样本序列是独立随机选取的,因此连续两个批次的样本之间不存在直接的依赖关系。
- 随机采样的优点在于它简单且能够让模型在每个训练周期(epoch)看到不同的序列组合,这有助于模型学习到更加泛化的特征。然而,随机采样的缺点是它可能会破坏序列数据中的时间依赖性,因为连续的数据可能会被分配到不同的批次中。
- 相邻采样则是另一种策略,它在选取下一个批次的样本时,会接着上一个批次的末尾开始。这意味着,如果第一个批次包含了序列的第1到第N个元素,那么第二个批次将包含第(N+1)到第2N个元素,以此类推。这种方法保持了序列内元素的连续性,有利于模型学习长距离的依赖关系。
- 相邻采样的优点是它保留了序列之间的时间依赖性,这对于时间序列分析、语言模型等任务至关重要。但缺点是模型在每个训练周期看到的数据顺序是相同的,可能会导致模型过拟合到特定的序列模式上。
由于RNN的结构,很容易会遇到梯度爆炸的问题。梯度裁剪是一种常用的技术,用于防止梯度爆炸,通过将梯度限制在一个阈值范围内来稳定训练过程。
import tensorflow as tf
optimizer = tf.keras.optimizers.Adam()
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
@tf.function
def train_step(model, input, target, loss_function, optimizer):
with tf.GradientTape() as tape:
predictions = model(input)
loss = loss_function(target, predictions)
gradients = tape.gradient(loss, model.trainable_variables)
# 梯度裁剪
clipped_gradients = [tf.clip_by_value(grad, -1.0, 1.0) for grad in gradients]
optimizer.apply_gradients(zip(clipped_gradients, model.trainable_variables))
困惑度(Perplexity)
困惑度是衡量语言模型性能的一种指标,它反映了模型对样本序列的预测能力。困惑度越低,表示模型的预测能力越强。对于给定的模型,可以通过计算交叉熵损失来计算困惑度。
def calculate_perplexity(loss):
perplexity = tf.exp(loss)
return perplexity
- 最佳情况下,模型总是把标签类别的概率预测为1,此时困惑度为1;
- 最坏情况下,模型总是把标签类别的概率预测为0,此时困惑度为正无穷;
- 基线情况下,模型总是预测所有类别的概率都相同,此时困惑度为类别个数。
如下为一个基本的RNN模型代码,采用Pytorch编写,而非Tensorflow
import torch
import torch.nn as nn
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
self.hidden_size = hidden_size
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
self.linear = nn.Linear(hidden_size, output_size)
def forward(self, x):
# 初始化隐藏状态
h0 = torch.zeros(1, x.size(0), self.hidden_size)
# 前向传播通过RNN
# 输出的形状:(batch_size, seq_length, hidden_size)
out, _ = self.rnn(x, h0)
# 只取序列的最后一步
out = out[:, -1, :]
out = self.linear(out)
return out
input_size = 10 # 输入大小
hidden_size = 20 # 隐藏层的大小
output_size = 1 # 输出大小
model = SimpleRNN(input_size, hidden_size, output_size)
print(model)
备注,隐藏层大小又称隐藏单元数,是指在神经网络中隐藏层中神经元的数量。每个隐藏单元可以被看作是在处理序列时网络能够记忆的一个特征。
- 较小的隐藏层可能会导致信息的丢失,因为网络的记忆能力有限,可能不足以捕捉处理复杂序列所需的所有信息。
- 较大的隐藏层则提供了更多的记忆容量,但可能导致过拟合,即模型在训练数据上表现很好,但在未见过的数据上表现不佳。