RNN从理论到实战【实战篇】

news2024/12/25 23:50:53

来源:投稿 作者:175
编辑:学姐

昨天的文章中,我们学习了RNN的理论部分,本文来看如何实现它,包括堆叠RNN和双向RNN。从而理解它们的原理。最后看一个应用到词性标注任务的实战。

RNNCell

首先实现单时间步RNN计算类,这是一个公共类:

class RNNCell(Module):
    def __init__(self, input_size, hidden_size: int, bias: bool = True, nonlinearity: str = 'tanh') -> None:
        '''
        RNN单时间步的抽象

        :param input_size: 输入x的特征数
        :param hidden_size: 隐藏状态的特征数
        :param bias: 线性层是否包含偏置
        :param nonlinearity: 非线性激活函数 tanh | relu
        '''
        super(RNNCell, self).__init__()
        # 输入x的线性变换
        self.input_trans = Linear(input_size, hidden_size, bias=bias)
        # 隐藏状态的线性变换
        self.hidden_trans = Linear(hidden_size, hidden_size, bias=bias)

        if nonlinearity == 'tanh':
            self.activation = F.tanh
        else:
            self.activation = F.relu

    def forward(self, x: Tensor, h: Tensor) -> Tensor:
        '''
        单个RNN的前向传播
        :param x:  形状 [batch_size, input_size]
        :param h:  形状 [batch_size, hidden_size]
        :return:
        '''
        # [batch_size, input_size] x [input_size, hidden_size] + [batch_size, hidden_size] x [hidden_size, hidden_size]
        # = [batch_size, hidden_size]
        h_next = self.activation(self.input_trans(x) + self.hidden_trans(h))
        return h_next

激活函数支持tanh和relu,这只是单时间步的RNN计算,RNN模型就是基于它来实现的。

RNN

下面来实现简单RNN。

class RNN(Module):
    def __init__(self, input_size: int, hidden_size: int, batch_first: bool = False, num_layers: int = 1,
                 nonlinearity: str = 'tanh',
                 bias: bool = True, dropout: float = 0) -> None:
        '''
        :param input_size:  输入x的特征数
        :param hidden_size: 隐藏状态的特征数
        :param batch_first:
        :param num_layers: 层数
        :param nonlinearity: 非线性激活函数 tanh | relu
        :param bias: 线性层是否包含偏置
        :param dropout: 用于多层堆叠RNN,默认为0代表不使用dropout
        '''
        super(RNN, self).__init__()
        self.num_layers = num_layers
        self.hidden_size = hidden_size
        self.batch_first = batch_first
        # 支持多层
        self.cells = ModuleList([RNNCell(input_size, hidden_size, bias, nonlinearity)] +
                                [RNNCell(hidden_size, hidden_size, bias, nonlinearity) for _ in range(num_layers - 1)])
        self.dropout = dropout
        if dropout:
            # Dropout层
            self.dropout_layer = Dropout(dropout)

从参数可以看到,我们支持多层RNN,同时在多层RNN之间经过了一层Dropout。

  def forward(self, input: Tensor, h_0: Tensor) -> Tuple[Tensor, Tensor]:
        '''
        RNN的前向传播
        :param input: 形状 [n_steps, batch_size, input_size] 若batch_first=False
        :param h_0: 形状 [num_layers, batch_size, hidden_size]
        :return:
            output: (n_steps, batch_size, hidden_size)若batch_first=False 或
                    (batch_size, n_steps, hidden_size)若batch_first=True
            h_n: (num_layers, batch_size, hidden_size)
        '''

        is_batched = input.ndim == 3
        batch_dim = 0 if self.batch_first else 1
        if not is_batched:
            # 转换为批大小为1的输入
            input = input.unsqueeze(batch_dim)
            if h_0 is not None:
                h_0 = h_0.unsqueeze(1)
        if self.batch_first:
            batch_size, n_steps, _ = input.shape
            input = input.transpose((1, 0, 2))  # 将batch放到中间维度
        else:
            n_steps, batch_size, _ = input.shape

        if h_0 is None:
            h = [Tensor.zeros((batch_size, self.hidden_size), device=input.device) for _ in range(self.num_layers)]
        else:
            h = h_0
            h = list(F.unbind(h))  # 按层数拆分h

        output = []
        for t in range(n_steps):
            inp = input[t]

            for layer in range(self.num_layers):
                h[layer] = self.cells[layer](inp, h[layer])
                inp = h[layer]
                if self.dropout and layer != self.num_layers - 1:
                    inp = self.dropout_layer(inp)

            # 收集最终层的输出
            output.append(h[-1])

        output = F.stack(output)
        if self.batch_first:
            output = output.transpose((1, 0, 2))
        h_n = F.stack(h)

        return output, h_n

为了简化实现,将batch维度放到维度1。

由于包含多层,每层含有不同的隐藏状态,所以需要按层数来拆分h。

多层的情况下,需要在合适的位置增加Dropout。比如上图的例子中,在RNN1和RNN2以及RNN2和RNN3的连接处增加Dropout。

双向RNN

双向RNN其实就是多了另一个反方向处理的RNN,因此我们首先增加新的用于处理反序输入的RNN:

# 支持多层
self.cells = ModuleList([RNNCell(input_size, hidden_size, bias, nonlinearity)] +
[RNNCell(hidden_size, hidden_size, bias, nonlinearity) for _ in range(num_layers - 1)])
if self.bidirectional:
 # 支持双向
 self.back_cells = copy.deepcopy(self.cells)

最简单的方法,就是将输入逆序,然后依照正向过程重新,重新跑一遍反向RNN过程。但这样会有重复代码,因此我们把RNN沿着某个方向的运算过程抽成一个函数。

    def forward(self, input: Tensor, h_0: Tensor) -> Tuple[Tensor, Tensor]:
        '''
        RNN的前向传播
        :param input: 形状 [n_steps, batch_size, input_size] 若batch_first=False
        :param h_0: 形状 [num_layers, batch_size, hidden_size]
        :return:
            num_directions = 2 if self.bidirectional else 1

            output: (n_steps, batch_size, num_directions * hidden_size)若batch_first=False 或
                    (batch_size, n_steps, num_directions * hidden_size)若batch_first=True
                    包含每个时间步最后一层(多层RNN)的输出h_t
            h_n: (num_directions * num_layers, batch_size, hidden_size) 包含最终隐藏状态
        '''

        is_batched = input.ndim == 3
        batch_dim = 0 if self.batch_first else 1
        if not is_batched:
            # 转换为批大小为1的输入
            input = input.unsqueeze(batch_dim)
            if h_0 is not None:
                h_0 = h_0.unsqueeze(1)
        if self.batch_first:
            batch_size, n_steps, _ = input.shape
            input = input.transpose((1, 0, 2))  # 将batch放到中间维度
        else:
            n_steps, batch_size, _ = input.shape

        if h_0 is None:
            num_directions = 2 if self.bidirectional else 1
            h = Tensor.zeros((self.num_layers * num_directions, batch_size, self.hidden_size), dtype=input.dtype,
                             device=input.device)
        else:
            h = h_0

        hs = list(F.unbind(h))  # 按层数拆分h

        if not self.bidirectional:
            # 如果是单向的
            output, h_n = one_directional_op(input, self.cells, n_steps, hs, self.num_layers, self.dropout_layer,
                                             self.batch_first)
        else:
            output_f, h_n_f = one_directional_op(input, self.cells, n_steps, hs[:self.num_layers], self.num_layers,
                                                 self.dropout_layer, self.batch_first)

        output_b, h_n_b = one_directional_op(F.flip(input, 0), self.back_cells, n_steps, hs[self.num_layers:],self.num_layers, self.dropout_layer, self.batch_first, reverse=True)


            output = F.cat([output_f, output_b], 2)
            h_n = F.cat([h_n_f, h_n_b], 0)

        return output, h_n

我们这里输出的维度和PyTorch保持一致。那么其中的one_directional_op是怎么实现的呢?

def one_directional_op(input, cells, n_steps, hs, num_layers, dropout, batch_first, reverse=False):
    '''
    单向RNN运算
    Args:
        input:  [n_steps, batch_size, input_size]
        cells:
        n_steps:
        hs:
        num_layers:
        dropout:
        batch_first:
        reverse:

    Returns:

    '''
    output = []
    for t in range(n_steps):
        inp = input[t]

        for layer in range(num_layers):
            hs[layer] = cells[layer](inp, hs[layer])
            inp = hs[layer]
            if dropout and layer != num_layers - 1:
                inp = dropout(inp)

        # 收集最终层的输出
        output.append(hs[-1])

    output = F.stack(output)

    if reverse:
        output = F.flip(output, 0) # 

    if batch_first:
        output = output.transpose((1, 0, 2))

    h_n = F.stack(hs)

    return output, h_n

这里要注意的是output = F.flip(output, 0)将输出按时间步维度逆序,使得时间步t=0上,是看了整个序列的结果。

最后我们通过词性标注任务实战来应用我们的RNN。

词性标注实战

词性标注任务可以看成是多类别文本分类问题,我们使用NLTK提供的宾州树库(Penn Treebank)样例数据,首先加载词性标注语料库:

def load_treebank():
    from nltk.corpus import treebank
    sents, postags = zip(*(zip(*sent) for sent in treebank.tagged_sents()))

    vocab = Vocabulary.build(sents, reserved_tokens=["<pad>"])

    tag_vocab = Vocabulary.build(postags)

    train_data = [(vocab.to_ids(sentence), tag_vocab.to_ids(tags)) for sentence, tags in
                  zip(sents[:3000], postags[:3000])]
    test_data = [(vocab.to_ids(sentence), tag_vocab.to_ids(tags)) for sentence, tags in
                 zip(sents[3000:], postags[3000:])]

    return train_data, test_data, vocab, tag_vocab

我们采用前3000句作为训练数据,其余的作为测试数据。然后实现我们的数据集类:

class RNNDataset(Dataset):
    def __init__(self, data):
        self.data = np.asarray(data)

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

    def __getitem__(self, i):
        return self.data[i]

    @staticmethod
    def collate_fn(examples):
        inputs = [Tensor(ex[0]) for ex in examples]
        targets = [Tensor(ex[1]) for ex in examples]
        inputs = pad_sequence(inputs)
        targets = pad_sequence(targets)
        mask = inputs.data != 0
        return inputs, targets, Tensor(mask)

为了对齐批次内数据的长度,需要对输入序列和输出序列进行补齐,同时用mask记录了哪些是经过补齐的标记。

然后基于我们上面实现的RNN来实现该词性标注分类模型,这里同样也叫RNN:

class RNN(nn.Module):
    def __init__(self, vocab_size: int, embedding_dim: int, hidden_dim: int, output_dim: int, n_layers: int,
                 dropout: float, bidirectional: bool = False):
        super(RNN, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        # 调用我们模型库中的RNN
        self.rnn = nn.RNN(embedding_dim, hidden_dim, batch_first=True, num_layers=n_layers, dropout=dropout, bidirectional=bidirectional)

        num_directions = 2 if bidirectional else 1
        self.output = nn.Linear(num_directions * hidden_dim, output_dim)

    def forward(self, input: Tensor, hidden: Tensor = None) -> Tensor:
        embeded = self.embedding(input)
        output, _ = self.rnn(embeded, hidden)  # pos tag任务利用的是包含所有时间步的output
        outputs = self.output(output)
        log_probs = F.log_softmax(outputs, axis=-1)
        return log_probs

这里在序列标注任务中,需要使用序列全部状态的隐藏层,存储在变量output中。

最后,在训练和预测阶段,需要使用mask来保证仅对有效标记求损失、对正确预测结果以及总的标记计数。

训练代码如下:

embedding_dim = 128
hidden_dim = 128
batch_size = 32
num_epoch = 10
n_layers = 2
dropout = 0.2

# 加载数据
train_data, test_data, vocab, pos_vocab = load_treebank()
train_dataset = RNNDataset(train_data)
test_dataset = RNNDataset(test_data)
train_data_loader = DataLoader(train_dataset, batch_size=batch_size, collate_fn=train_dataset.collate_fn, shuffle=True)
test_data_loader = DataLoader(test_dataset, batch_size=batch_size, collate_fn=test_dataset.collate_fn, shuffle=False)

num_class = len(pos_vocab)

# 加载模型
device = cuda.get_device("cuda:0" if cuda.is_available() else "cpu")
model = RNN(len(vocab), embedding_dim, hidden_dim, num_class, n_layers, dropout, bidirectional=True)
model.to(device)

# 训练过程
nll_loss = NLLLoss()
optimizer = SGD(model.parameters(), lr=0.1)

model.train()  # 确保应用了dropout
for epoch in range(num_epoch):
    total_loss = 0
    for batch in tqdm(train_data_loader, desc=f"Training Epoch {epoch}"):
        inputs, targets, mask = [x.to(device) for x in batch]
        log_probs = model(inputs)
        loss = nll_loss(log_probs[mask], targets[mask])  # 通过bool选择,mask部分不需要计算
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    print(f"Loss: {total_loss:.2f}")

# 测试过程
acc = 0
total = 0
model.eval()  # 不需要dropout
for batch in tqdm(test_data_loader, desc=f"Testing"):
    inputs, targets, mask = [x.to(device) for x in batch]
    with no_grad():
        output = model(inputs)
        acc += (output.argmax(axis=-1).data == targets.data)[mask.data].sum().item()
        total += mask.sum().item()

# 输出在测试集上的准确率
print(f"Acc: {acc / total:.2f}")

我们通过model.train()来model.eval()来控制需不需要进行Dropout。最终,在双向RNN中训练了10个批次,结果为:

Training Epoch 9: 94it [02:00,  1.29s/it]
Loss: 103.25
Testing: 29it [00:05,  5.02it/s]
Acc: 0.70

由于电脑上没有GPU,因此速度较慢,就只训练了10个批次,看起来效果还不错,测试集上的准确率达到了70%。

完整代码

https://github.com/nlp-greyfoss/metagrad

参考

Speech and Language Processing

自然语言处理:基于预训练模型的方法

https://nn.labml.ai/lstm/index.html

关注下方《学姐带你玩AI》🚀🚀🚀

220+篇AI必读论文免费领取

码字不易,欢迎大家点赞评论收藏!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/164757.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

iMX6ULL —按键输入捕获与GPIO输入配置与高低电平读取

硬件介绍1.1 板子上按键原理图先来看原理图&#xff0c;我板子上有4个按键sw1~sw4:1.1.1 SW1SW1是板子的系统复位按键&#xff0c;不可编程使用1.1.2 SW2、SW3SW2&#xff1a;SNVS_TAMPER1&#xff0c;GPIO5_1平时是低电平&#xff0c;按下去是高电平。SW3&#xff1a;ONOFF它也…

2023年java面试题之zookeeper基础2

一、请描述一下 Zookeeper 的通知机制是什么&#xff1f;Zookeeper 允许客户端向服务端的某个 znode 注册一个 Watcher 监听&#xff0c;当服务端的一些指定事件触发了这个 Watcher &#xff0c;服务端会向指定客户端发送一个事件通知来实现分布式的通知功能&#xff0c;然后客…

echarts基本用法

目录 tooltip:{ // 设置提示框信息 图表的提示框组件 legend:{ // 图例组件 toolbox : { //工具箱组件 可以另存为图片等功能 grid{ //网格配置 grid可以控制线型图 柱状图 图表大小 xAxs: { // 设置x轴的相关配置 y轴同理 series:[ // 系列图表 它决定着显示那种…

Spring MVC 详解 (Spring Boot)

Spring MVC 详解 - Spring Boot一、什么是 Spring MVC1.1 MVC 定义1.2 MVC 和 Spring MVC 的关系1.3 学习目的二、Spring MVC 创建和连接2.1 创建 Spring MVC 项目2.2 相关注解三、获取参数3.1 使用 Servlet API3.2 通过方法参数直接拿到3.2.1 传递单个参数3.2.2 传递多个参数3…

【Acwing 周赛复盘】第86场周赛复盘(2023.1.14)

【Acwing 周赛复盘】第86场周赛复盘 周赛复盘 ✍️ 本周个人排名&#xff1a;678/2358 AC情况&#xff1a;2/3 这是博主参加的第一次周赛&#xff0c;深刻体会到了世界的参差 &#x1f602; 看到排名 TOP3 的大佬都是不到 5 分钟内就 AK 了&#xff0c;真是恐怖如斯&#xff0…

29.动态内存申请

1.动态内存分配的概念 在数组一章中&#xff0c;介绍过数组的长度是预先定义好的&#xff0c;在整个程序中固定不变&#xff0c;但是在实际的编程中&#xff0c;往往所需的内存空间取决于实际输入的数据&#xff0c;而无法预先确定。为了解决上述问题&#xff0c;C语言提供了一…

Linux 发布 JavaWeb 项目

Linux 发布 JavaWeb 项目 安装 mysql 使用 yum search mysql-community 查看是否安装下载地址&#xff1a;https://dev.mysql.com/downloads/repo/yum/ 选择自己虚拟机的版本 在此处&#xff0c;复制 链接地址&#xff0c; 然后使用命令 wget 链接地址 来进行 下载rpm 安装 …

Python解题 - CSDN周赛第23期 - 树形背包与优化

以问哥目前的水平来看&#xff0c;本期的四道题的整体难度还是中等偏上的&#xff0c;而且从结果上来看&#xff0c; 也达到了竞赛的标准&#xff08;只有三名选手拿到满分&#xff09;。也许在某些大佬看来还是太简单了&#xff0c;毕竟都是模板题&#xff0c;直接套模板就能过…

基于深度学习人脸性别识别项目

项目概述要求针对提供的人脸数据集&#xff0c;根据人脸图像预测人脸性别。本次将提供 20000 多张已经分割的人脸图像&#xff0c;要求基于人脸图像自动识别该人性别。数据集的年龄从 1 岁覆盖到 100 多岁&#xff0c;包括了白种人、黄种人、黑种人等多种种族数据。数据集存在人…

2022年“网络安全”赛项海南省赛选拔赛 任务书

2022年“网络安全”赛项海南省赛选拔赛 任务书 一、竞赛时间 共计6小时。 &#xff08;二&#xff09;A模块基础设施设置/安全加固&#xff08;350分&#xff09; 一、项目和任务描述&#xff1a; 假定你是某企业的网络安全工程师&#xff0c;对于企业的服务器系统&#xff0c…

【数据结构】二叉搜索树

一、概念二叉搜索树也叫二叉排序树。在一颗二叉搜索树中&#xff0c;他的左子树二点节点值一定比根节点的值小&#xff0c;他的右子树节点的值一定比根节点的值大。二、特点他的左子树节点的值一定比根节点的值小他的右子树节点的值一定比根节点的值大他的每一颗子树都是一颗二…

java+springboot笔记2023002

java的注解机制&#xff1a; Java主要提供了5个基础注解&#xff0c;分别是&#xff1a; Override Deprecated SuppressWarnings SafeVarargs FunctionalInterface Java元注解&#xff1a; Retention&#xff0c; Target&#xff0c; Inherited&#xff0c; Documented&#x…

算法刷题打卡第66天:极大极小游戏

极大极小游戏 难度&#xff1a;简单 给你一个下标从 0 开始的整数数组 nums &#xff0c;其长度是 2 的幂。 对 nums 执行下述算法&#xff1a; 设 n 等于 nums 的长度&#xff0c;如果 n 1 &#xff0c;终止 算法过程。否则&#xff0c;创建 一个新的整数数组 newNums &a…

MySQL索引命中与失效

目录创建表MySQL执行优化器索引的命中与失效情况总结讨论MySQL索引命中与失效&#xff0c;我们得先来创建表 创建表 SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS 0;-- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABL…

java动态规划算法

使用场景 动态规划最重要的是转移方程&#xff0c;而转移方程需要递归和记忆化搜索产生的表&#xff0c;因此直接贴出转移方程是没什么用的&#xff0c;不探究如何从递归到记忆化搜索再到转移方程&#xff0c;还是很难想到怎么去得到转移方程。下面我们将从例子中探寻如何三步走…

四、Gradle项目的生命周期

文章目录四、Gradle项目的生命周期【尚硅谷】Gradle教程-讲师&#xff1a;刘辉 生活明朗&#xff0c;万物可爱&#xff0c;人间值得&#xff0c;未来可期 四、Gradle项目的生命周期 Gradle 项目的生命周期分为三大阶段&#xff1a;Initialization -> Configuration -> E…

Maestro 薛定谔软件简单分子对接案例

##参考&#xff1a; Maestro 薛定谔软件使用&#xff1a; https://www.bilibili.com/video/BV1RN411X7Te https://www.youtube.com/watch?vNkM8jjHr7f4&listPL3dxdlKx_PcfuvHwJ0RjpZFt4HjwyTr7f Maestro 薛定谔对接&#xff1a; https://www.bilibili.com/video/BV17p…

【Java多线程】线程的常用方法

测试Thread中的常用方法1.start():启动当前线程&#xff1b;调用当前线程的run()2.run():通常需要重写Thread类中的此方法&#xff0c;将创建的线程要执行的3.currentThread():静态方法&#xff0c;返回当前代码的线程4.getName():获取当前线程的名字5.setName():设置当前线程的…

MySQL逻辑删除+Mybatis-Plus = 墙裂推荐

目录前言逻辑删除使用Mybatis-Plus逻辑删除它做了什么注意写在后面的一些话前言 一般情况下&#xff0c;我们要删除一条数据&#xff0c;直接使用 delete 即可&#xff0c;就像这样&#xff1a;delete from user where id 1&#xff0c;这样做的好处是&#xff1a; 符合我们…

C进阶_字符串库函数

目录 求字符串长度 strlen 常规实现 递归实现 指针-指针实现 长度不受限制的字符串函数 strcpy 模拟实现strcpy strcat 模拟实现strcat strcmp 模拟实现strcmp 长度受限制的字符串函数 strncpy strncat strncmp 求字符串长度 strlen size_t strlen ( const c…