3251|9

553

帖子

3

TA的资源

纯净的硅(初级)

楼主
 

《动手学深度学习(PyTorch版)》7、循环神经网络 [复制链接]

一、循环神经网络产生的背景

到目前为止,我们遇到过两种类型的数据:表格数据和图像数据。对于图像数据,我们设计了专门的卷积神经网络架构来为这类特殊的数据结构建模。到目前为止我们默认数据都来自于某种分布, 并且所有样本都是独立同分布的 (independently and identically distributed,i.i.d.)。 然而,大多数的数据并非如此。 例如,文章中的单词是按顺序写的,如果顺序被随机地重排,就很难理解文章原始的意思。 同样,视频中的图像帧、对话中的音频信号以及网站上的浏览行为都是有顺序的。 因此,针对此类数据而设计特定模型,可能效果会更好。

另一个问题来自这样一个事实: 我们不仅仅可以接收一个序列作为输入,而是还可能期望继续猜测这个序列的后续。 例如,一个任务可以是继续预测2,4,6,8,10,…。

为了处理这种序列信息,循环神经网络(recurrent neural network,RNN)就从此诞生了。循环神经网络通过引入状态变量存储过去的信息和当前的输入,从而可以确定当前的输出。

 

二、序列模型的统计工具

处理序列数据需要统计工具和新的深度神经网络架构。 为了简单起见,我们以 如下所示的股票价格(富时100指数)为例。

其中,用xt表示价格,即在时间步(time step) t∈Z+时,观察到的价格xt。 请注意,t对于本文中的序列通常是离散的,并在整数或其子集上变化。 假设一个交易员想在t日的股市中表现良好,于是通过以下途径预测xt:

 

1、自回归模型

为了实现这个预测,可以使用回归模型,但是有一个主要的问题要解决:输入数据的数量, 输入xt−1,…,x1本身因t而异。也就是说,输入数据的数量这个数字将会随着我们遇到的数据量的增加而增加, 因此需要一个近似方法来使这个计算变得容易处理。

解决这个问题,由两种策略:

第一种策略,假设在现实情况下相当长的序列 xt−1,…,x1可能是不必要的, 因此我们只需要满足某个长度为τ的时间跨度, 即使用观测序列xt−1,…,xt−τ。 当下获得的最直接的好处就是参数的数量总是不变的, 至少在t>τ时如此,这就使我们能够训练一个上面提及的深度网络。 这种模型被称为自回归模型(autoregressive models), 因为它们是对自己执行回归。

第二种策略,如 下图所示, 是保留一些对过去观测的总结ht, 并且同时更新预测x^t和总结ht。 这就产生了基于x^t=P(xt∣ht)估计xt, 以及公式ht=g(ht−1,xt−1)更新的模型。 由于ht从未被观测到,这类模型也被称为 隐变量自回归模型(latent autoregressive models)。

序列的估计值公式:

 

2、马尔可夫模型

在自回归模型的近似法中, 我们使用xt−1,…,xt−τ 而不是xt−1,…,x1来估计xt。 只要这种是近似精确的,我们就说序列满足马尔可夫条件(Markov condition)。 特别是,如果τ=1,得到一个 一阶马尔可夫模型(first-order Markov model), P(x)由下式给出:

 

3、因果关系

原则上,将P(x1,…,xT)倒序展开也没什么问题。 毕竟,基于条件概率公式,我们总是可以写出:

事实上,如果基于一个马尔可夫模型, 我们还可以得到一个反向的条件概率分布。 然而,在许多情况下,数据存在一个自然的方向,即在时间上是前进的。 很明显,未来的事件不能影响过去。 因此,如果我们改变xt,可能会影响未来发生的事情xt+1,但不能反过来。 也就是说,如果我们改变xt,基于过去事件得到的分布不会改变。 因此,解释P(xt+1∣xt)应该比解释P(xt∣xt+1)更容易。 

 

三、训练

在了解了上述统计工具后,让我们在实践中尝试一下! 首先,我们生成一些数据:使用正弦函数和一些可加性噪声来生成序列数据, 时间步为1,2,…,1000。

import torch
from torch import nn
from d2l import torch as d2l

T = 1000  # 总共产生1000个点
time = torch.arange(1, T + 1, dtype=torch.float32)
x = torch.sin(0.01 * time) + torch.normal(0, 0.2, (T,))
d2l.plot(time, [x], 'time', 'x', xlim=[1, 1000], figsize=(6, 3))
d2l.plt.show()

接下来,我们将这个序列转换为模型的特征-标签(feature-label)对。 基于嵌入维度τ,我们将数据映射为数据对yt=xt 和xt=[xt−τ,…,xt−1]。 这比我们提供的数据样本少了τ个, 因为我们没有足够的历史记录来描述前τ个数据样本。 一个简单的解决办法是:如果拥有足够长的序列就丢弃这几项; 另一个方法是用零填充序列。 在这里,我们仅使用前600个“特征-标签”对进行训练。

tau = 4
features = torch.zeros((T - tau, tau))
for i in range(tau):
    features[:, i] = x[i: T - tau + i]
labels = x[tau:].reshape((-1, 1))

batch_size, n_train = 16, 600
# 只有前n_train个样本用于训练
train_iter = d2l.load_array((features[:n_train], labels[:n_train]),
                            batch_size, is_train=True)

使用一个相当简单的架构训练模型: 一个拥有两个全连接层的多层感知机,ReLU激活函数和平方损失。

# 初始化网络权重的函数
def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.xavier_uniform_(m.weight)

# 一个简单的多层感知机
def get_net():
    net = nn.Sequential(nn.Linear(4, 10),
                        nn.ReLU(),
                        nn.Linear(10, 1))
    net.apply(init_weights)
    return net

# 平方损失。注意:MSELoss计算平方误差时不带系数1/2
loss = nn.MSELoss(reduction='none')

模型训练:

def train(net, train_iter, loss, epochs, lr):
    trainer = torch.optim.Adam(net.parameters(), lr)
    for epoch in range(epochs):
        for X, y in train_iter:
            trainer.zero_grad()
            l = loss(net(X), y)
            l.sum().backward()
            trainer.step()
        print(f'epoch {epoch + 1}, '
              f'loss: {d2l.evaluate_loss(net, train_iter, loss):f}')

net = get_net()
train(net, train_iter, loss, 5, 0.01)

训练结果:

 

四、预测

由于训练损失很小,因此我们期望模型能有很好的工作效果。 让我们看看这在实践中意味着什么。 首先是检查模型预测下一个时间步的能力, 也就是单步预测(one-step-ahead prediction)。

onestep_preds = net(features)
d2l.plot([time, time[tau:]],
         [x.detach().numpy(), onestep_preds.detach().numpy()], 'time',
         'x', legend=['data', '1-step preds'], xlim=[1, 1000],
         figsize=(6, 3))

 

五、文本预处理

对于序列数据处理问题,上面评估了所需的统计工具和预测时面临的挑战。 这样的数据存在许多种形式,文本是最常见例子之一。

我们将解析文本的常见预处理步骤。 这些步骤通常包括:

  • 将文本作为字符串加载到内存中。
  • 将字符串拆分为词元(如单词和字符)。
  • 建立一个词表,将拆分的词元映射到数字索引。
  • 将文本转换为数字索引序列,方便模型操作。

1、读取数据集

首先,我们从H.G.Well的时光机器中加载文本。 这是一个相当小的语料库,只有30000多个单词,但足够我们小试牛刀, 而现实中的文档集合可能会包含数十亿个单词。 下面的函数将数据集读取到由多条文本行组成的列表中,其中每条文本行都是一个字符串。 为简单起见,我们在这里忽略了标点符号和字母大写。

2、词元化

下面的tokenize函数将文本行列表(lines)作为输入, 列表中的每个元素是一个文本序列(如一条文本行)。 每个文本序列又被拆分成一个词元列表,词元(token)是文本的基本单位。 最后,返回一个由词元列表组成的列表,其中的每个词元都是一个字符串(string)。

def tokenize(lines, token='word'):  #[url=home.php?mod=space&uid=472666]@save[/url] """将文本行拆分为单词或字符词元"""
    if token == 'word':
        return [line.split() for line in lines]
    elif token == 'char':
        return [list(line) for line in lines]
    else:
        print('错误:未知词元类型:' + token)

tokens = tokenize(lines)
for i in range(11):
    print(tokens[i])

3、词表

词元的类型是字符串,而模型需要的输入是数字,因此这种类型不方便模型使用。 现在,让我们构建一个字典,通常也叫做词表(vocabulary), 用来将字符串类型的词元映射到从0开始的数字索引中。 我们先将训练集中的所有文档合并在一起,对它们的唯一词元进行统计, 得到的统计结果称之为语料(corpus)。 然后根据每个唯一词元的出现频率,为其分配一个数字索引。 很少出现的词元通常被移除,这可以降低复杂性。 另外,语料库中不存在或已删除的任何词元都将映射到一个特定的未知词元“<unk>”。 我们可以选择增加一个列表,用于保存那些被保留的词元, 例如:填充词元(“<pad>”); 序列开始词元(“<bos>”); 序列结束词元(“<eos>”)。

class Vocab:  #@save
    """文本词表"""
    def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
        if tokens is None:
            tokens = []
        if reserved_tokens is None:
            reserved_tokens = []
        # 按出现频率排序
        counter = count_corpus(tokens)
        self._token_freqs = sorted(counter.items(), key=lambda x: x[1],
                                   reverse=True)
        # 未知词元的索引为0
        self.idx_to_token = ['<unk>'] + reserved_tokens
        self.token_to_idx = {token: idx
                             for idx, token in enumerate(self.idx_to_token)}
        for token, freq in self._token_freqs:
            if freq < min_freq:
                break
            if token not in self.token_to_idx:
                self.idx_to_token.append(token)
                self.token_to_idx[token] = len(self.idx_to_token) - 1

    def __len__(self):
        return len(self.idx_to_token)

    def __getitem__(self, tokens):
        if not isinstance(tokens, (list, tuple)):
            return self.token_to_idx.get(tokens, self.unk)
        return [self.__getitem__(token) for token in tokens]

    def to_tokens(self, indices):
        if not isinstance(indices, (list, tuple)):
            return self.idx_to_token[indices]
        return [self.idx_to_token[index] for index in indices]

    @property
    def unk(self):  # 未知词元的索引为0
        return 0

    @property
    def token_freqs(self):
        return self._token_freqs

def count_corpus(tokens):  #@save
    """统计词元的频率"""
    # 这里的tokens是1D列表或2D列表
    if len(tokens) == 0 or isinstance(tokens[0], list):
        # 将词元列表展平成一个列表
        tokens = [token for line in tokens for token in line]
    return collections.Counter(tokens)

 

我们首先使用时光机器数据集作为语料库来构建词表,然后打印前几个高频词元及其索引。

vocab = Vocab(tokens)
print(list(vocab.token_to_idx.items())[:10])

如下: 

[('<unk>', 0), ('the', 1), ('i', 2), ('and', 3), ('of', 4), ('a', 5), ('to', 6), ('was', 7), ('in', 8), ('that', 9)]

现在,我们可以将每一条文本行转换成一个数字索引列表。

for i in [0, 10]:
    print('文本:', tokens[i])
    print('索引:', vocab[tokens[i]])

 

六、循环神经网络

循环神经网络(recurrent neural networks,RNNs) 是具有隐状态的神经网络。

假设我们在时间步t有小批量输入Xt∈Rn×d。 换言之,对于n个序列样本的小批量, Xt的每一行对应于来自该序列的时间步t处的一个样本。 接下来,用Ht∈Rn×h 表示时间步t的隐藏变量。 与多层感知机不同的是, 我们在这里保存了前一个时间步的隐藏变量Ht−1, 并引入了一个新的权重参数Whh∈Rh×h, 来描述如何在当前时间步中使用前一个时间步的隐藏变量。 具体地说,当前时间步隐藏变量由当前时间步的输入 与前一个时间步的隐藏变量一起计算得出:

从相邻时间步的隐藏变量Ht和 Ht−1之间的关系可知, 这些变量捕获并保留了序列直到其当前时间步的历史信息, 就如当前时间步下神经网络的状态或记忆, 因此这样的隐藏变量被称为隐状态(hidden state)。 由于在当前时间步中, 隐状态使用的定义与前一个时间步中使用的定义相同, 因此 上面公式的计算是循环的(recurrent)。 于是基于循环计算的隐状态神经网络被命名为 循环神经网络(recurrent neural network)。 在循环神经网络中执行 上面计算的层 称为循环层(recurrent layer)。

有许多不同的方法可以构建循环神经网络, 由 上式定义的隐状态的循环神经网络是非常常见的一种。 对于时间步t,输出层的输出类似于多层感知机中的计算:

循环神经网络的参数包括隐藏层的权重 Wxh∈Rd×h,Whh∈Rh×h和偏置bh∈R1×h, 以及输出层的权重Whq∈Rh×q 和偏置bq∈R1×q。 值得一提的是,即使在不同的时间步,循环神经网络也总是使用这些模型参数。 因此,循环神经网络的参数开销不会随着时间步的增加而增加。

后面还有循环神经网络的反向传播,涉及到非常多的理论计算,还暂时无法全部理解,感觉后面越来越难了,就先不进行介绍了,由兴趣的同学可以自行去看看。

 

最新回复

Wit兄,咱就是一个小迷弟。。。。。。你这是抬举我呀。。。。   详情 回复 发表于 2024-11-12 12:40
点赞 关注(1)

回复
举报

6828

帖子

0

TA的资源

五彩晶圆(高级)

沙发
 

最后的这个循环神经网络还是比较麻烦的

点评

挺麻烦的,没有那么好懂,理论公式太多了,难以理解。  详情 回复 发表于 2024-11-11 20:24
 
 

回复

1388

帖子

1

TA的资源

五彩晶圆(初级)

板凳
 

LSTM(Long Short Term Memory)要介绍不?。。。。。。。。

点评

请hellokitty_bean兄给我们科普以下?  详情 回复 发表于 2024-11-11 20:25
 
 
 

回复

1388

帖子

1

TA的资源

五彩晶圆(初级)

4
 

个人感觉,至少要介绍RNN面临的问题,然后针对这些面临的问题又采取了哪些方法来解决

点评

你是想说CNN遇到的问题吧?  详情 回复 发表于 2024-11-11 20:26
 
 
 

回复

1388

帖子

1

TA的资源

五彩晶圆(初级)

5
 

Wit兄加油哦。。。。。。。。。。。。。。。。。。。。。每天跟着你的步伐进步也蛮好

点评

谦虚了兄弟,您才是大神  详情 回复 发表于 2024-11-11 20:27
 
 
 

回复

553

帖子

3

TA的资源

纯净的硅(初级)

6
 
Jacktang 发表于 2024-11-11 07:31 最后的这个循环神经网络还是比较麻烦的

挺麻烦的,没有那么好懂,理论公式太多了,难以理解。

 
 
 

回复

553

帖子

3

TA的资源

纯净的硅(初级)

7
 
hellokitty_bean 发表于 2024-11-11 09:31 LSTM(Long Short Term Memory)要介绍不?。。。。。。。。

hellokitty_bean兄给我们科普以下?

 
 
 

回复

553

帖子

3

TA的资源

纯净的硅(初级)

8
 
hellokitty_bean 发表于 2024-11-11 09:32 个人感觉,至少要介绍RNN面临的问题,然后针对这些面临的问题又采取了哪些方法来解决

你是想说CNN遇到的问题吧?

 
 
 

回复

553

帖子

3

TA的资源

纯净的硅(初级)

9
 
hellokitty_bean 发表于 2024-11-11 09:33 Wit兄加油哦。。。。。。。。。。。。。。。。。。。。。每天跟着你的步伐进步也蛮好

谦虚了兄弟,您才是大神

点评

Wit兄,咱就是一个小迷弟。。。。。。你这是抬举我呀。。。。  详情 回复 发表于 2024-11-12 12:40
 
 
 

回复

1388

帖子

1

TA的资源

五彩晶圆(初级)

10
 
xinmeng_wit 发表于 2024-11-11 20:27 谦虚了兄弟,您才是大神

Wit兄,咱就是一个小迷弟。。。。。。你这是抬举我呀。。。。

 
 
 

回复
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/7 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表