【17】RNN循环神经网络基础

发布于:2025-03-15 ⋅ 阅读:(10) ⋅ 点赞:(0)

目录

128-RNN网络

什么是RNN?

134-RNN实战模型


128-RNN网络

【文本分类模型:1-没有关注文本顺序,2-全连接层导致训练参数过多,导致过拟合】

什么是RNN?

RNN的主要用来处理序列数据。

在传统的神经网络模型中,是从输入层到隐含层再到输出层层与层之间是全连接的,每层之间的节点是无连接的。但是这种普通的神经网络对于很多问题却无能无力。例如,你要预测句子的下一个单词是什么,一般需要用到前面的单词,因为一个句子中前后单词并不是独立的。

RNN之所以称为循环神经网路,即一个序列当前的输出与前面的输出也有关。

具体的表现形式为网络会对前面的信息进行记忆并应用于当前输出的计算中,即隐藏层之间的节点不再无连接而是有连接的,并且隐藏层的输入不仅包括输入层的输出还包括上一时刻隐藏层的输出。

 一个RNN单元能够逐一处理句子中的所有单词。

将句子中的第一个单词传递给RNN单元,RNN单元生成输出和中间状态。该状态是序列的连续含义,由于在完成对整个序列的处理之前不会输出此状态,所以将其称为隐藏状态

RNN单元在处理第一个单词之后,生成了输出和隐藏状态。输出和隐藏状态都有各自的用途输出可以被训练以预测句子中的下一个字符或单词。这是大多数语言模型任务的工作方式。

在文本分类例子中,我们只考虑句子的整体含义,因此我们可以忽略每个单元生成的输出,而将重点放在隐藏状态上。隐藏状态的目的是保持句子的连续含义。我们可以用最后的隐藏状态作为分类特征。

因为每个单词使用相同的RNN单元,所以大大减少了神经网络所需的参数量,这使我们能够处理较大规模的小批次数据。网络参数进行学习的方式是处理序列的顺序,这是RNN的核心原则。【顺序不同,学习不同。问题需要处理顺序,使用RNN】

134-RNN实战模型

【参考文件】17-1RNN循环网络.ipynb

【导出代码】

# %% [markdown]
# # 129-RNNCell实例化——数据读取

# %%
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'True'

import glob
import numpy as np


import torch
from torch import nn
from torch.utils import data
from torch.utils.data import DataLoader
import torchvision
from torchvision import transforms
import torch.nn.functional as F
import torchtext
from torchtext.data.utils import get_tokenizer



from PIL import Image
import matplotlib.pyplot as plt
%matplotlib inline
from lxml import etree   
from matplotlib.patches import Rectangle       #绘制矩形框

# %%
torch.__version__

# %%
torchtext.__version__

# %% [markdown]
# ### 【目的】通过RNN循环网络结构,将一句话的单词依次输入到网络结构中,得到最后的隐藏状态输出,进行文本分类。

# %% [markdown]
# ##### 加载文本数据集

# %%
train_iter, test_iter = torchtext.datasets.IMDB()

# %% [markdown]
# ##### 分词工具

# %%
tokenizer = get_tokenizer('basic_english')
print(tokenizer('This is a book about Pytorch.'))

# %% [markdown]
# 加载数据集,获取文本列表

# %%
train_iter, test_iter = torchtext.datasets.IMDB()
train_data, test_data = list(train_iter), list(test_iter)

# %% [markdown]
# 获取分类

# %%
all_classes = set([label for (label, text) in train_data])
num_class = len(all_classes)

# %%
num_class

# %% [markdown]
# ## 处理文本思路
# * 分词
# * 生成词表   he-->30, her-->31
# * 词嵌入   30--->(0.2, 0.4, 0.2, 0.9, 2.1)   独热编码

# %%
from torchtext.data.utils import get_tokenizer   #【分词工具】
from torchtext.vocab import build_vocab_from_iterator #【创建词表工具】

# %% [markdown]
# ##### 分词工具 

# %%
tokenizer = get_tokenizer('basic_english')

# %% [markdown]
# ##### 词表生成器

# %%
def yield_tokens(data):
    for _, text in data:
        yield tokenizer(text)
        
vocab = build_vocab_from_iterator(yield_tokens(train_data), specials=["<pad>", "<unk>"], min_freq=3)

# %% [markdown]
# ##### 设置词表默认值

# %%
vocab["<pad>"], vocab["<unk>"]

# %%
vocab.set_default_index(vocab["<unk>"])     #查不到单词时的默认值

# %%
#【默认分词演示】
tokenizer('this is a RYGH lessons')


# %%
#【词表映射】
vocab(['this', 'is', 'a', 'rygh', 'lessons'])

# %% [markdown]
# ##### 创建函数
# * 第一个返回评论的索引
# * 返回类别索引

# %%
text_pipeline = lambda x: vocab(tokenizer(x))       #1-分词、2-词表映射
label_pipeline = lambda x: int(x == 1)

# %%
text_pipeline('this is a book about pytorch')

# %%
label_pipeline('2')

# %% [markdown]
# # 130-批次处理函数

# %%
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# %% [markdown]
# 上一章:将十条文本连接到一起,并且记录偏移量offset。
# 
# RNNCell:不需要连接,单独创建列表。文本列表、批次列表。

# %%
def cllote_batch(batch):
    label_list, text_list = [], []
    for (_label, _text) in batch:   #【batch返回一个元组:标签,文本同时返回】
        label_list.append(label_pipeline(_label))   #【每条评论的标签】
        #【创建切片:缩短评论长度,rnn不能创建长时依赖】
        pre_text = torch.tensor(text_pipeline(_text), dtype=torch.int64)[:100]      #【将每条文本评论转换成索引,再转换成张量类型。】
        text_list.append(pre_text)   #【每条评论的int类型的索引值的tensor】
    label_list = torch.tensor(label_list)
    #【每条评论长度不太,对应的tensor长度不同?】    【评论疮长度不同,批次训练要求长度相同】
    text_list = torch.nn.utils.rnn.pad_sequence(text_list)   #【按照最长评论进行填充】
    return label_list.to(device), text_list.to(device)   

# %%
#【将原始数据,每次处理512条,交给cllote_batch进行处理】
BATCHSIZE = 512
train_dl = DataLoader(dataset=train_data,
                      batch_size=BATCHSIZE,
                      shuffle=True,
                      collate_fn=cllote_batch)   #【批次处理函数】
test_dl = DataLoader(dataset=test_data,
                      batch_size=BATCHSIZE,
                      collate_fn=cllote_batch)

# %%
for i, (la, tex) in enumerate(train_dl):
    print(la.size(), tex.size())
    if i == 10:
        #【进行一次中断】
        break

# %% [markdown]
# label:批次的长度, text:第i批次的批次长度   第i批次(batch_size)

# %% [markdown]
# # 131-RNNCell在序列上的展开

# %% [markdown]
# ![Clip_2025-03-14_14-41-00.png](attachment:Clip_2025-03-14_14-41-00.png)

# %% [markdown]
# # 创建模型

# %%
#【词表大小:单词的数量】
vocab_size = len(vocab)
print(vocab_size)

# %%
#【将每个单词映射到多长的张量上】  输入大小
embeding_dim = 300  
#【RNNCell要输出多长的计算单元】  输出大小
hidden_unit =  128


# %% [markdown]
# 序列如何在RNN上展开

# %%
class RNN_encode_Demo(nn.Module):
    def __init__(self, embeding_dim, hidden_unit):      
        super(RNN_encode_Demo, self).__init__()
        self.rnn_cell = nn.RNNCell(input_size=embeding_dim, hidden_size=hidden_unit)
    def forward(self, inputs):
        ht = None
        #【将每个单词的tensor进行展开】
        for word in inputs:
            ht = self.rnn_cell(word, ht)
        return ht
        

# %% [markdown]
# # 132-RNNCell初始化hiddenstat

# %%
class RNN_encode(nn.Module):
    def __init__(self, embeding_dim, hidden_unit):
        super(RNN_encode, self).__init__()
        self.rnn_cell = nn.RNNCell(input_size=embeding_dim, hidden_size=hidden_unit)
    def forward(self, inputs):
        #【初始化hidenstat:0tensor//自行创建的tensor也要放到device】
        batch_size = inputs.shape[1]
        ht = torch.zeros(batch_size, hidden_unit).to(device)   #【长度为rnncell的输出】
        #【将每个单词的tensor进行展开,依次在rnncell上进行调用】
        for word in inputs:
            ht = self.rnn_cell(word, ht)
        return ht
            
        

# %% [markdown]
# # 133-创建文本分类模型
# * embeding层:将输入的单词映射成张量。
# [23, 34, 45, 56, 78]--->[[long=300], [①long=300], [②long=300],...]的tensor
# * RNNCell层:将映射成张量的评论交给RNN_encode, 返回一个hiddenstat
# 将[①]在rnncell上进行调用,得到一个hiddenstat(文本的总结特征)
# * Linear层:将ht交给Linear

# %%
class RNN_Net(nn.Module):
    def __init__(self, vocab_size, embeding_dim, hidden_unit):
        super(RNN_Net, self).__init__()
        #【初始化词嵌入层】
        self.embedding = nn.Embedding(num_embeddings=vocab_size, embedding_dim=embeding_dim)        #【0-输入的文本(词表)大小、1-输出的大小(输入rnn_encode大小)】  
        self.rnnencode = RNN_encode(embeding_dim, hidden_unit)      #【初始化rnn_encode】
        #【Linear输出层】
        self.fc1 = nn.Linear(in_features=hidden_unit, out_features=2)
        
    def forward(self, inputs):
        x = self.embedding(inputs)
        x = self.rnnencode(x)
        x = self.fc1(x)
        return x

# %% [markdown]
# # 134-模型训练

# %%
model = RNN_Net(vocab_size, embeding_dim, hidden_unit).to(device)

# %%
model

# %%
loss_fn = nn.CrossEntropyLoss()     #【输出层为2】
from torch.optim import lr_scheduler
optimizer = torch.optim.Adam(model.parameters(), betas=(0.5, 0.5), lr=0.001)
exp_lr_scheduler = lr_scheduler.StepLR(optimizer, step_size=15, gamma=0.1)

# %%
def train(dataloader):
    total_acc, total_count, total_loss = 0, 0, 0
    model.train()
    for label, text in dataloader:
        label, text= label.to(device), text.to(device)
        predited_label = model(text)  
        loss = loss_fn(predited_label, label)
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        with torch.no_grad():
            total_acc += (predited_label.argmax(1) == label).sum().item()
            total_count += label.size(0)
            total_loss += loss.item()*label.size(0)
            
    return total_loss/total_count, total_acc/total_count

# %%
def test(dataloader):
    total_acc, total_count, total_loss = 0, 0, 0
    model.eval()
    with torch.no_grad():
        for label, text in dataloader:
            predited_label = model(text)
            loss = loss_fn(predited_label, label)            
            total_acc += (predited_label.argmax(1) == label).sum().item()
            total_count += label.size(0)
            total_loss += loss.item()*label.size(0)
            
    return total_loss/total_count, total_acc/total_count

# %%
def fit(epochs, train_dl, test_dl):
    train_loss =[]
    train_acc = []
    test_loss = []
    test_acc = []
    
    for epoch in range(epochs):
        epoch_loss, epoch_acc = train(train_dl)
        epoch_test_loss, epoch_test_acc = test(test_dl)
        train_loss.append(epoch_loss)
        train_acc.append(epoch_acc)
        test_loss.append(epoch_test_loss)
        test_acc.append(epoch_test_acc)
        exp_lr_scheduler.step()
        template = ("epoch: {:2d}, train_loss: {:.5f}, train_acc: {:.1f}%," "test_loss: {:.5f}, test_acc: {:.1f}%")
        print(template.format(epoch, epoch_loss, epoch_acc*100, epoch_test_loss, 100*epoch_test_acc))
    print("Done!")
    
    return train_loss, train_acc, test_loss, test_acc

# %%
EPOCHS = 10

# %%
train_loss, train_acc, test_loss, test_acc = fit(epochs=EPOCHS, train_dl=train_dl, test_dl=test_dl)


# %% [markdown]
# # 正确率一直为什么50%???

# %% [markdown]
# * RNN不能记住长时的依赖。
# * 当较长的序列在RNNCell上进行展开,RNN不能记住长时的依赖(之前的hiddenstat已经忘记)

# %% [markdown]
# # 解决

# %% [markdown]
# 将每条评论进行切片

# %% [markdown]
#