Day15上 - RNN、LSTM、GRU、情感识别

RNN,即循环神经网络(Recurrent Neural Network),是一种用于处理序列数据的神经网络。与传统的前馈神经网络不同,RNN具有内部的记忆机制,可以记住之前输入的信息,并将其用于当前输入的处理中。这使得RNN非常适合于处理和预测时间序列数据、自然语言处理、语音识别等任务。

整体来看,RNN有被取代的趋势,但RNN是理解大模型的核心,大模型里的很多理念都来源于RNN。学习RNN不是为了怎么使用,而是分析为何消亡,这样才能更深刻的理解 transformer为何活着, transformer是点对点解决RNN的问题。

RNN的基本结构

在RNN中,信息不仅沿着输入向量传播,还从一个步骤传递到下一个步骤。这意味着每个输出都依赖于之前的计算,并且所有计算都涉及相同的参数集。这种结构允许网络学习到序列中的模式,即使这些模式出现在不同的时间点上。

RNN的问题

  1. 梯度消失/爆炸问题:在训练深度RNN时,可能会遇到梯度消失或梯度爆炸的问题。这是因为通过时间的反向传播(BPTT)会将错误信号传回很多时间步长,导致权重更新变得非常小(消失)或非常大(爆炸)。这使得网络难以学习长期依赖关系。

  2. 无法有效捕捉长距离依赖:由于上述梯度问题,标准RNN在处理非常长的序列时可能表现不佳,尤其是在需要记住很久之前的信息时。

解决方案

为了克服这些问题,研究人员开发了多种改进的RNN模型:

  • 长短期记忆网络(LSTM, Long Short-Term Memory):LSTM是一种特殊的RNN类型,它能够学习长期依赖关系。LSTM通过引入“门”的概念来控制信息流,包括遗忘门、输入门和输出门,从而有效地解决了梯度消失问题。

  • 门控循环单元(GRU, Gated Recurrent Unit):GRU是另一种改进型RNN,它简化了LSTM的设计,将遗忘门和输入门合并为更新门,并且没有单独的输出门。尽管结构更简单,但在许多任务中,GRU的表现与LSTM相当,有时甚至更好。

  • 双向RNN(BiRNN, Bidirectional RNN):BiRNN由两个RNN组成,一个处理正向的时间序列,另一个处理反向的时间序列。这允许网络同时考虑过去和未来的信息,对于某些任务来说是非常有用的。

  • 深度RNN:通过堆叠多个RNN层,可以创建深度RNN,以提高模型的能力。然而,这也增加了模型的复杂性和计算需求。

RNN的使用

1. RNN 要解决什么问题?

  • 时序信号的抽取特征
  • 顺序

2. RNN 的解决方法

  • 循环
  • 逐个处理
  • 前后依赖
    • 后一项直接依赖前一项,间接依赖前面所有的项
    • 前后进行【手递手】传递
  • 中间隐藏状态

3. RNN API 如何调用?

  • nn.RNN
    • 自动挡(自动循环)
    • 用于编码器
  • nn.RNNCell
    • 手动挡(手动循环)
    • 用于解码器

时序共享,每一步用的是同一个A;进来多少步就出去多少步,即(X)进来70步,出去的(h)也是70步。hn是最后一步,最后一步很重要,我们经常用最后一步去解决很多问题。因为理论上所有步的信息都聚合到了最后一步上。h0也需要写上,因为解码自回归的时候需要。

所有的网络都是在处理特征,这是人工智能的核心问题,不论是全链接、卷积、RNN循环和transformer,它们都是在处理特征,所以我们只关心特征的维度

import torch
from torch import nn

"""
1. Simple RNN
"""

"""
    时序类数据结构:[seq_len, batch_size, embedding_dim]
    seq_len:序列长度
    batch_size:批量数据有多少个
    embedding_dim:嵌入维度,也就是每个词是多大的向量
"""
# 比如一个短信70个字,3个短信,每个字是256个维度
X = torch.randn(70, 3, 256)
h0 = torch.zeros(1, 3, 512, dtype=torch.float32)
X.shape, h0.shape

# 构建一个循环神经网络,输入层维度256,隐藏层维度512
rnn = nn.RNN(input_size=256, hidden_size=512)

# 调用RNN
out, hn = rnn(X, h0)

# [seq_len, batch_size, hidden_size]
out.shape

# [1, batch_size, hidden_size]
hn.shape

 LSTM 博客

        http://colah.github.io/posts/2015-08-Understanding-LSTMs/

        

当序列很长时,前面的都忘了,所以增加一个能够记忆的东西,这就有了LSTM。

"""
2、 LSTM 长短期记忆网络
    Long 长
    Short 短
    Term 期
    Memory 记忆
"""

"""
    导入必要的模块
    导入 PyTorch 库及其神经网络模块 nn
"""
import torch
from torch import nn

"""
(1)定义 LSTM 层
"""
# Inputs: input, (h_0, c_0)
# Outputs: output, (h_n, c_n)
# 创建一个 LSTM 层,输入维度为 256,隐藏层维度为 512
lstm = nn.LSTM(input_size=256, hidden_size=512)


"""
(2)准备输入数据和初始状态
"""
# 准备输入数据(70个时间步,每个时间步有3个样本,每个样本的特征维度为256)
X = torch.randn(70, 3, 256)
# 初始化隐藏状态h0和细胞状态c0,1个LSTM层,3个样本,每个样本的隐藏状态维度为512
h0 = torch.zeros(1, 3, 512, dtype=torch.float32)
c0 = torch.zeros(1, 3, 512, dtype=torch.float32)

"""
(3)前向传播
"""
#out是LSTM的输出,hn是最后一个时间步的隐藏状态,cn是最后一个时间步的细胞状态
out, (hn, cn) = lstm(X, (h0, c0))

"""
(4)检查输出的形状
"""
# [70, 3, 512]
out.shape

# [1, 3, 512]
hn.shape

# [1, 3, 512]
cn.shape

"""
(1)定义 LSTMCell 层
"""
# Inputs: input, (h_0, c_0)
# Outputs: (h_1, c_1)
#创建一个 LSTMCell 层,输入维度为 256,隐藏层维度为 512
lstm_cell = nn.LSTMCell(input_size=256, hidden_size=512)

"""
(2)准备输入数据和初始状态
"""
# 准备输入数据
X = torch.randn(70, 3, 256)
# 初始化隐藏状态 h0 和细胞状态 c0
h0 = torch.zeros(3, 512, dtype=torch.float32)
c0 = torch.zeros(3, 512, dtype=torch.float32)

"""
(3)处理第一个时间步的数据
"""
X0 = X[0, :, :]
X0.shape

"""
(4)前向传播
"""
hn, cn = lstm_cell(X0, (h0, c0))

"""
(5)检查输出的形状
"""
hn.shape
cn.shape
X.size(0)

"""
(6)循环处理所有时间步的数据
"""
out = []
for x in X:
    h0, c0 = lstm_cell(x, (h0, c0))
    out.append(h0)

"""
(7)拼接所有时间步的输出
"""
# 最终所有步的短期状态
out = torch.stack(tensors=out, dim=0)

"""
(8)获取最后一步的状态
"""
hn = h0
cn = c0

"""
总结:
    LSTM 层:处理整个序列数据,返回所有时间步的输出和最后一个时间步的隐藏状态及细胞状态。
    LSTMCell 层:逐个时间步处理数据,返回每个时间步的隐藏状态和细胞状态。
"""

信息

  • CRUD工程师
    • 增删改查
    • Create(创建):向数据库中添加新的记录或数据项。
    • Read(读取):检索或查询数据库中的现有数据。这可以是获取单个记录、一组记录或者执行复杂的查询以获取特定的数据视图。
    • Update(更新):修改数据库中已有的记录。这可能涉及更改单个字段的值或多个字段的值。
    • Delete(删除):从数据库中移除记录。请注意,删除操作通常是不可逆的,因此在执行删除操作之前应该谨慎考虑,并且在某些情况下,可能会有逻辑删除(例如,设置一个“已删除”标志)而不是物理删除。
  • LSTM
    • 1. 门控思想
    • 2. CRUD思想
      • (1)遗忘门:先丢掉一部分不重要信息
      • (2)输入门:增加重要的信息
  • GRU
    • 核心思想:吃 LSTM 的红利,化简 LSTM
    • 调用层面:跟Simple RNN是一样的

情感识别 sentiment analysis

1. 业务本质:

2. 技术本质:
  • 文本分类
3. 流程:

"""
    需求:酒店评论分析
"""

"""
1. 样本路径和类别读取
"""
"""
    训练数据聚合
"""
import os
train_root = os.path.join("hotel", "train")
train_texts = []
train_labels = []
for label in os.listdir(train_root):
    label_root = os.path.join(train_root, label)
    for file in os.listdir(label_root):
        file_path = os.path.join(label_root, file)
        # 聚合结果
        train_texts.append(file_path)
        train_labels.append(label)
# 打印数据
len(train_texts), len(train_labels)


"""
    测试数据聚合
"""
test_root = os.path.join("hotel","test")
test_texts = []
test_labels = []
for label in os.listdir(test_root):
    label_root = os.path.join(test_root,label)

    if os.path.isdir(label_root):
        for file in os.listdir(label_root):
            file_path = os.path.join(label_root,file)
            test_texts.append(file_path)
            test_labels.append(label)
    else:
        print(f"跳过非目录项:{label_root}")

# 打印数据
len(test_texts),len(test_labels)

"""
2. 构建分词器
    分词,把句子变 token
    把所有不同的token聚在一起
    做 0 ~ N-1 的编码
"""
SEQ_LEN = 30

import jieba
# pip install opencc -U
import opencc

class Tokenizer(object):
    """
        定义一个分词器
    """
    def __init__(self, X, y):
        """
            训练的语料
        """
        self.X = X
        self.y = y
        self.t2s = opencc.OpenCC(config="t2s")
        self._build_dict()

    def _build_dict(self):
        """
            构建字典
        """
        # 1. 获取所有的 token
        words = {"<PAD>", "<UNK>"}
        for file in self.X:
            # 打开文件
            with open(file=file, mode="r", encoding="gbk", errors="ignore") as f:
                text = f.read().replace("\n", "")
                text = self.t2s.convert(text=text)
                words.update(set(jieba.lcut(text)))
        # 2. 构建文本字典
        self.word2idx = {word: idx for idx, word in enumerate(words)}
        self.idx2word = {idx: word for word, idx in self.word2idx.items()}
        # 3. 删掉 数据集
        del self.X
        # 4. 构建标签字典
        labels = set(train_labels)
        self.label2idx = {label: idx for idx, label in enumerate(labels)}
        self.idx2label = {idx: label for label, idx in self.label2idx.items()}
        # 5. 删除 数据集
        del self.y
        

    def encode(self, text, seq_len=SEQ_LEN):
        """
            text --> tokens --> ids

            自我扩展:
                - 右侧截断或填充
                - 左边?
                - 随机?
        """
        # 1. 繁体转简体
        text = text.replace("\n", "")
        text = self.t2s.convert(text=text)
        # 2. 分词
        text = jieba.lcut(text)
        # 3. 统一长度
        text = (text + ["<PAD>"] * seq_len)[:seq_len]
        # 4. 转 id
        ids = [self.word2idx.get(word, self.word2idx.get("<UNK>")) for word in text]
        
        return ids
        
    def decode(self, ids):
        """
            ids --> tokens --> text
        """
        text = "".join([self.idx2word.get(_id, "") for _id in ids])
        return text

    def __str__(self):
        """
            输: 分词器基本信息
        """
        return f"""
        Tokenizer Info: 
            --> Num of Tokens: {len(self.word2idx)}
            --> Num of Labels: {len(self.label2idx)}
        """
    def __repr__(self):
        return self.__str__()

输出结果: 

"""
3. 打包数据
"""
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
import torch

class HotelCommentDataset(Dataset):
    """
        自定义数据集
    """
    def __init__(self, X, y, seq_len=SEQ_LEN):
        """
            初始化
        """
        self.X = X
        self.y = y
        self.seq_len = seq_len

    def __getitem__(self, idx):
        """
            索引操作
                返回第idx个样本
        """
        # 1. 文本
        file = self.X[idx]
        with open(file=file, mode="r", encoding="gbk", errors="ignore") as f:
            text = f.read()
            ids = tokenizer.encode(text=text, seq_len=self.seq_len)
            ids = torch.tensor(data=ids, dtype=torch.long)
                
        # 2. 标签
        label = self.y[idx]
        label = tokenizer.label2idx.get(label)
        label = torch.tensor(data=label, dtype=torch.long)
        
        return ids, label

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

# 1. 定义一个分词器
tokenizer = Tokenizer(X=train_texts, y=train_labels)
tokenizer

# 打包数据
train_dataset = HotelCommentDataset(X=train_texts, y=train_labels)
train_dataloader = DataLoader(dataset=train_dataset, shuffle=True, batch_size=128)
test_dataset = HotelCommentDataset(X=test_texts, y=test_labels)
test_dataloader = DataLoader(dataset=test_dataset, shuffle=False, batch_size=256)

for X, y in test_dataloader:
    print(X.shape)
    print(y.shape)
    break

"""
4. 搭建模型
"""
import torch
from torch import nn

"""
    每句话65个词,分为2类
    
"""
class TextCNN(nn.Module):
    """
        搭建模型
            - 卷积
            # [N, C, L]
            nn.Conv1d()
    """
    def __init__(self, dict_len=len(tokenizer.word2idx), embedding_dim = 256):
        super().__init__()
        # 向量化
        self.embed = nn.Embedding(num_embeddings=dict_len, 
                                  embedding_dim=embedding_dim, 
                                  padding_idx=tokenizer.word2idx.get("<PAD>"))
        # 特征抽取
        self.feature_extractor = nn.Sequential(
            nn.Conv1d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm1d(num_features=512),
            nn.ReLU(),
            nn.MaxPool1d(kernel_size=2, stride=2, padding=0),
            nn.Conv1d(in_channels=512, out_channels=1024, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm1d(num_features=1024),
            nn.ReLU(),
            nn.MaxPool1d(kernel_size=2, stride=2, padding=0)
        )
        # 分类
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(in_features=1024 * (SEQ_LEN // 2 // 2), out_features=128),
            nn.ReLU(),
            nn.Linear(in_features=128, out_features=2)
        )
        
        
    def forward(self, x):
        # 向量化
        x = self.embed(x)
        x = torch.permute(input=x, dims=(0, 2, 1))
        # 抽特征
        x = self.feature_extractor(x)
        # 做分类
        x = self.classifier(x)
        return x

class TextRNN(nn.Module):
    """
        搭建模型
            - 卷积
            # [N, C, L]
            nn.Conv1d()
    """
    def __init__(self, dict_len=len(tokenizer.word2idx), embedding_dim = 256):
        super().__init__()
        # 向量化
        self.embed = nn.Embedding(num_embeddings=dict_len, 
                                  embedding_dim=embedding_dim, 
                                  padding_idx=tokenizer.word2idx.get("<PAD>"))
        # 特征抽取
        self.feature_extractor = nn.RNN(input_size=256, hidden_size=512, num_layers=1, bidirectional=False)
        # 分类
        self.classifier = nn.Sequential(
            nn.Linear(in_features=512, out_features=128),
            nn.ReLU(),
            nn.Linear(in_features=128, out_features=2)
        )
        
    def forward(self, x):
        # 向量化
        x = self.embed(x)
        x = torch.permute(input=x, dims=(1, 0, 2))
        # 特征抽取
        out, hn = self.feature_extractor(x)
        # 分类输出
        x = self.classifier(out.sum(dim=0))
        return x

model= TextRNN()
for X, y in train_dataloader:
    y_pred = model(X)
    print(y_pred.shape)
    break
"""
5. 训练模型
"""
# 检测设备
device = "cuda" if torch.cuda.is_available() else "cpu"
# 实例化模型
model = TextRNN().to(device=device)
# 损失函数
loss_fn = nn.CrossEntropyLoss()
# 优化器
optimizer = torch.optim.Adam(params=model.parameters(), lr=1e-3)
# 轮次
epochs = 20

"""
    6. 评估
"""
import time

def get_acc(dataloader):
    model.eval()
    accs = []
    with torch.no_grad():
        for X, y in dataloader:
            # 0. 数据搬家
            X = X.to(device=device)
            y = y.to(device=device)
            # 1. 正向传播
            y_pred = model(X)
            # 2. 计算结果
            y_pred = y_pred.argmax(dim=1)
            # 3. 计算准确率
            acc = (y_pred == y).to(dtype=torch.float32).mean().item()
            # 4. 保存结果
            accs.append(acc)
    final_acc = round(number=sum(accs) / len(accs), ndigits=6)
    return final_acc

def train():
    train_acc = get_acc(dataloader=train_dataloader)
    test_acc = get_acc(dataloader=test_dataloader)
    print(f"初始 Train_acc: {train_acc}, Test_acc: {test_acc}")
    
    for epoch in range(epochs):
        model.train()
        start_time = time.time()
        for X, y in train_dataloader:
            # 0. 数据搬家
            X = X.to(device=device)
            y = y.to(device=device)
            # 1. 正向传播
            y_pred = model(X)
            # 2. 计算误差
            loss = loss_fn(y_pred, y)
            # 3. 反向传播
            loss.backward()
            # 4. 优化一步
            optimizer.step()
            # 5. 清空梯度
            optimizer.zero_grad()
        
        stop_time = time.time()
        # 每轮结束后测试一下
        train_acc = get_acc(dataloader=train_dataloader)
        test_acc = get_acc(dataloader=test_dataloader)
        
        print(f"Epoch: {epoch + 1}, Train_acc: {train_acc}, Test_acc: {test_acc}, Train_time: {stop_time-start_time}")

train()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值