RNN模型

问题与思考

1、为什么LSTM 与 GRU 的门 使用sigmoid做激活函数?

核心原因在于其输出值域为 (0, 1)。这个特性完美契合了门控机制在神经网络中需要扮演的"开关"或"阀门"角色。

为什么不使用softmax?

它有一个致命的特性:所有元素的输出之和必须等于 1。这就产生了一种"跷跷板效应"------如果遗忘门的值变大了,输入门或输出门的值就必然被挤压变小。

Sigmoid 是对输入向量中的每一个元素独立进行计算的。这意味着在 LSTM 中,遗忘门 ftft​ 、输入门 itit​ 和输出门 otot​ 之间是完全独立的。

2、LSTM 与 GRU 如何缓解梯度消失?

两者都是通过将传统的矩阵乘法状态转移替换为带有门控的加法操作,从而在反向传播时创造出一条梯度可以畅通无阻的"捷径",从根本上缓解了梯度消失问题。

特性 LSTM GRU
线性传播载体 独立的细胞状态 CtCt​ 隐藏状态 htht​
核心公式特征 Ct=ft⊙Ct−1+...Ct​=ft​⊙Ct−1​+... ht=(1−zt)⊙ht−1+...ht​=(1−zt​)⊙ht−1​+...
梯度传递条件 遗忘门 ft≈1ft​≈1 更新门 zt≈0zt​≈0
设计哲学 显式地维护一条独立的"长程记忆高速公路" 将长程记忆与短期输出合并,通过门控动态调节信息流

一、RNN模型简介

1. 什么是RNN模型?

RNN(Recurrent Neural Network),中文称作循环神经网络,是一种专门用于处理序列数据的神经网络架构。一般以序列数据为输入,通过网络内部的结构设计有效捕捉序列之间的关系特征,一般也是以序列形式进行输出。它的特点是能够捕捉序列数据中的时间依赖关系,广泛应用于自然语言处理(NLP)、时间序列预测、语音识别等领域。

2. RNN模型的作用

因为RNN结构能够很好利用序列之间的关系, 因此针对自然界具有连续性的输入序列, 如人类的语言等进行很好的处理, 广泛应用于NLP领域的各项任务, 如文本分类, 情感分析, 意图识别, 机器翻译等.

工作原理

  • 输入: RNN接收一个序列数据作为输入,每次输入序列中的一个元素 (时间步)。
  • 隐藏状态更新: 对于每个时间步,RNN根据当前的输入和上一个时间步的隐藏状态计算出新的隐藏状态。这个计算通常使用激活函数(如tanh)。
  • 输出: RNN根据当前的隐藏状态计算出当前的输出。
  • 循环: 上述过程会在序列的每个时间步重复进行,直到序列结束。

神经网络结构

3. 数据的处理过程

RNN的循环机制使模型隐层上一时间步产生的结果, 能够作为当下时间步输入的一部分(当下时间步的输入除了正常的输入外还包括上一步的隐层输出)对当下时间步的输出产生影响

4. RNN模型的分类

类型 输入结构 输出结构 应用场景
N vs N 等长序列输入 等长序列输出 词性标注、逐点预测
N vs 1 序列输入 单个时间步输出 文本分类、情感分析
1 vs N 单个时间步输入 序列输出 文本/音乐生成、图像描述生成
N vs M 不等长序列输入 不等长序列输出 机器翻译、文本摘要

二、传统的RNN模型

1. 什么时候使用?

适用场景:短序列任务、计算资源有限的场景、作为学习RNN的基础

**不适用:**长序列任务、需要长期依赖的任务(需要记住长期信息的任务)、对训练稳定性要求较高的任务

2. 传统RNN的内部结构及计算公式

实现步骤:

  1. 序列数据、时间步

  2. 输入:Xt(当前步的输入数据),ht-1(上一步的隐藏层输出)

  3. 拼接:将Xt、ht-1进行拼接,拼接为一个新的数组

  4. 加权、加激活函数(tanh(wt(Xt,ht-1) + b))

  5. 生成当前时刻的ht

  6. 之后的时间步,重复上边2---5步

3. 使用Pytorch构建传统RNN网络

python 复制代码
import torch

# 建立网络层
rnn = torch.nn.RNN(input_size=5, hidden_size=4, num_layers=1, batch_first=False)

# 输入数据 [batch_size,seq_len,input_size]
x = torch.randn([4, 3, 5])

# 初始化h0 [num_layer,batch_size,hidden_size]
h0 = torch.zeros([1, 4, 4])
# 输出结果
output, hn = rnn(x.permute(1, 0, 2), h0)
print(output.shape)  # [seq_len,batch_size,hidden_size]
print(hn.shape)  # [num_layer,batch_size,hidden_size]

'''
输出:
torch.Size([3, 4, 4])
torch.Size([1, 4, 4])
'''

4. 传统RNN的优势与缺点

优点:由于内部结构简单,对计算资源要求低,相比之后我们要学习的RNN变体(LSTM和GRU模型)参数总量少了很多,在短序列任务上性能和效果都表现优异。

缺点:传统RNN在解决长序列之间的关联时,通过实践证明经典RNN表现很差。原因是在进行反向传播的时候,过长的序列导致梯度的计算异常,发生梯度消失或爆炸。

三、LSTM模型

1. 什么是LSTM?

LSTM(Long Short-Term Memory)也称为长短期记忆网络,是一种改进的循环神经网络(RNN),专门设计用于解决传统RNN的梯度消失问题和长程依赖问题

它的核心思想通过引入门机制(输入门、遗忘门、输出门)和细胞状态(Cell State)来控制信息的流动,从而决定哪些信息需要保留、哪些信息需要丢弃,进而能够更好地捕捉长序列数据中的长期依赖关系。

2. LSTM内部结构及计算公式

2.1 遗忘门:

作用:细胞状态(前边的信息)中需要忘记那些,去除不重要的信息。

遗忘门值:

当前时间步输入xt与上一个时间步隐藏状态ht−1拼接,得到xt,ht−1,然后通过一个全连接层做变换,最后通过sigmoid函数进行激活得到ft

2.2 输入门:

**作用:**当前时刻的信息要记住多少,更新细胞状态的记忆。

输入门值:

它和遗忘门值计算方式相同,但权重不同

2.3 输出门:

作用:决定当前的隐藏状态有多少细胞状态的内容,用来生成当前时刻隐藏状态

输出门值:

它和遗忘门值计算方式相同,但权重不同

2.4 细胞状态:

作用:贯穿整个网络、携带长期记忆;信息通过细胞状态传递,并由各个门控机制选择性地修改。

计算:

2.5 实现步骤:

① 遗忘门值:

当前时间步输入xt与上一个时间步隐藏状态ht−1拼接,得到xt,ht−1,然后通过一个全连接层做变换,最后通过sigmoid函数进行激活得到ft

② 去除上一步细胞状态不重要信息 =

遗忘门值 * 上一层细胞状态

③ 计算当前时刻候选细胞状态:

与传统RNN的内部结构计算相同

④ 输入门门值:

⑤ 更新细胞状态:

遗忘门门值与上一个时间步得到的Ct−1相乘,再加上输入门门值与当前时间步得到的未更新Ct相乘的结果

⑥ 输出门的门值:

⑦ 计算当前时刻隐藏状态:

输出门值作用在更新后的细胞状态Ct上,并做tanh激活

3. BI-LSTM

3.1 介绍:
3.2 内部结构:
优缺点:

优点:捕捉上下文信息数据更准确;适用于需要全局信息的任务

缺点:参数量大;计算复杂度高;难以并行化(与LSTM类似,Bi-LSTM需要按时间步依次计算)

4. Pytorch构建LSTM模型

4.1 LSTM函数

pyTorch中的LSTM实现通过torch.nn.LSTM类提供,如下:

lstm = nn.LSTM(input_size, hidden_size, num_layers, bidirectional, batch_first, dropout)

主要参数介绍如下:

  • input_size: 输入特征维度,词嵌入维度
  • hidden_size: 隐藏层特征维度(即每个时间步输出的h_t的维度)
  • num_layers: LSTM堆叠的层数(默认1层)
  • bias: 是否使用偏置项(默认True)
  • batch_first: 输入/输出张量的第一个维度是否为batch(默认False,即seq_len在前)
  • dropout: 非最后一层的LSTM层输出上应用的dropout概率(默认0,即无dropout)
  • bidirectional: 是否使用双向LSTM(默认False)
4.2 输入的表示

LSTM的输入包含三个关键部分:输入序列x、初始隐藏状态h0和初始细胞状态c0,如果不提供h0和c0,PyTorch会自动将h0和c0初始化为全零张量。

输入序列x的形状为:(seq_len, batch, input_size),具体如下所示:

  • seq_len: 序列长度(时间步数)
  • batch: 批量大小
  • input_size: 输入特征维度

初始隐藏状态h0和初始细胞状态c0的形状必须是:(num_layers * num_directions, batch_size, hidden_size),具体含义如下所示:

  • num_layers:LSTM的层数
  • num_directions:1(单向LSTM)或2(双向LSTM)
  • batch_size:输入数据的批量大小
  • hidden_size:LSTM的隐藏层大小
4.3 输出的表示

LSTM的输出是一个元组(output, (h_n, c_n)):

1.output:

包含LSTM最后一层在所有时间步的隐藏状态

  • 单向LSTM: (seq_len, batch, hidden_size)
  • 双向LSTM: (seq_len, batch, hidden_size * 2)

2.h_n (隐藏状态):

包含所有层在最后一个时间步的隐藏状态

  • 单向LSTM: (num_layers, batch, hidden_size)
  • 双向LSTM: (num_layers * 2, batch, hidden_size)

3.c_n (细胞状态):

  • 包含所有层在最后一个时间步的细胞状态
  • 单向LSTM:(num_layers, batch, hidden_size)
  • 双向LSTM:(num_layers * 2, batch, hidden_size)
4.4 LSTM实践
python 复制代码
import torch

# 构建网络层
layer = torch.nn.LSTM(input_size=5, hidden_size=3, num_layers=2, batch_first=False, dropout=0.1)

# x为LSTM所需参数,而不是上边嵌入层的输出
# [seq_len,batch_size,input_size] 
x = torch.randn(3, 3, 5)
# 初始化h0、c0 [num_layers,batch_size,hidden_size]
h0 = torch.zeros([2, 3, 3])
c0 = torch.zeros([2, 3, 3])

# 网络处理
y, (hn, cn) = layer(x, (h0, c0))
print(y.shape)  # [seq_len,batch_size,hidden_size]
print(hn.shape)  # [num_layers,batch_size,hidden_size]
print(cn.shape)  # [num_layers,batch_size,hidden_size]


'''
输出:
torch.Size([3, 3, 3])
torch.Size([2, 3, 3])
torch.Size([2, 3, 3])
'''

双向LSTM

python 复制代码
import torch

# 构建网络层
layer = torch.nn.LSTM(input_size=5, hidden_size=3, num_layers=2, batch_first=False, dropout=0.1,bidirectional=True)

# x为LSTM所需参数,而不是上边嵌入层的输出
# [seq_len,batch_size,input_size]
x = torch.randn(3, 3, 5)
# 初始化h0、c0 [num_layers,batch_size,hidden_size]
h0 = torch.zeros([2*2, 3, 3])
c0 = torch.zeros([2*2, 3, 3])

# 网络处理
y, (hn, cn) = layer(x, (h0, c0))
print(y.shape)  # [seq_len,batch_size,hidden_size*2]
print(hn.shape)  # [num_layers,batch_size,hidden_size]
print(cn.shape)  # [num_layers,batch_size,hidden_size]


'''
torch.Size([3, 3, 6])
torch.Size([4, 3, 3])
torch.Size([4, 3, 3])
'''

5. LSTM的优势与缺点

优点:

能够处理长序列数据

缓解梯度消失: 通过细胞状态, 上一时刻的细胞状态线性 的传到下一时刻**,**避免了梯度在时间步之间的连乘,从而缓解了梯度消失问题

缺点:① 计算开销较大,由于包含多个门的计算 ② LSTM较为复杂,调参时需要更多的时间和精力

四、GRU模型

1. GRU介绍

GRU(Gated Recurrent Unit)也称为门控循环单元,是一种改进版的RNN。

能够有效捕捉长序列之间的语义关联,避免了传统RNN中的梯度消失问题,并减少了LSTM模型中的复杂性。

GRU对比RNN、LSTM做了那些优化?

① 对比传统 RNN:引入"门控机制"解决梯度问题------梯度消失/爆炸长期依赖

对比 LSTM:结构精简与参数优化:

LSTM结构复杂,包含 3 个门(输入门、遗忘门、输出门)和 2 个状态(细胞状态 CtCt​ 、隐藏状态 htht​ ),参数量大且计算慢。

GRU 对此做了以下"减法"优化:将"输入门"与"遗忘门"合二为一;状态的融合:取消独立的"细胞状态";重置门(Reset Gate)的引入与位置调整

2. GRU的结构图

重置门:

**作用:**重置上一时刻的隐藏状态,要忽略多少先前的隐藏状态

重置门值:

更新门:

**作用:**决定在多大程度上保留先前的隐藏状态,以及在多大程度上更新候选隐藏状态为新的隐藏状态。

更新门值

实现步骤:

① 重置门值:

上一时刻隐藏状态 与 当前时刻的输入值 进行拼接,计算拼接后的加权值,并添加sigmoid激活函数 ------ sigmoid(wtht-1,xt+b)

② 忽略多少先前的隐藏状态 =

重置门值*上一时刻隐藏状态

③ 更新门值:

上一时刻隐藏状态 与 当前时刻的输入值 进行拼接,计算拼接后的加权值,并添加sigmoid激活函数

④ 候选隐藏状态:

上一时刻隐藏状态 与 当前时刻的输入值 进行拼接,计算拼接后的加权值,并添加tanh激活函数

当前时刻的隐藏状态:

3. Bi-GRU介绍

Bi-GRU(Bidirectional Gated Recurrent Unit)是 GRU的改进,它通过将正向和反向GRU结合在一起,能够同时利用输入序列的过去和未来信息。双向GRU是一种在序列建模中常见的结构,尤其在自然语言处理(NLP)和时间序列分析中具有很大的优势。

Bi-GRU的核心思想是同时利用序列的正向和反向信息:

  • 正向 GRU:从序列的起始位置到结束位置处理数据,捕捉过去的信息。
  • 反向 GRU:从序列的结束位置到起始位置处理数据,捕捉未来的信息。

将正向和反向GRU的隐藏状态结合起来,得到更全面的序列表示。

3.1 内部结构

输入层:将输入序列传递给两个GRU网络(正向和反向)。

正向GRU:按照时间顺序处理输入序列(从第一个时间步到最后一个时间步)。

反向GRU:逆序处理输入序列(从最后一个时间步到第一个时间步)。

合并层:正向和反向GRU的输出通常被拼接在一起,形成一个包含更多上下文信息的表示。

输出层:将合并后的表示传递到下游任务,进行分类、回归或者其他任务的预测。

3.2 BI-GRU的优缺点

优点:

  • 捕捉上下文信息:通过结合正向和反向的信息,Bi-GRU能够更好地捕捉序列的上下文依赖关系
  • 适用于需要全局信息的任务:在自然语言处理(NLP)等任务中,Bi-GRU能够同时考虑过去和未来的信息
  • 性能优于单向GRU:在许多任务中,Bi-GRU的表现优于单向GRU

缺点:

计算复杂度高:Bi-GRU需要同时计算正向和反向的GRU,计算量是单向GRU的两倍

参数量大:Bi-GRU的参数比单向GRU多,训练时间较长

难以并行化:与GRU类似,Bi-GRU需要按时间步依次计算

4. Pytorch构建GRU模型

PyTorch通过torch.nn.GRU类提供GRU实现:

layer = torch.nn.GRU(input_size=5, hidden_size=3, num_layers=2, batch_first=False, dropout=0.1, bidirectional=True)

具体参数说明如下:

  • input_size:输入特征的维度
  • hidden_size:隐藏状态的维度
  • num_layers:GRU的层数(默认值为1)
  • batch_first:如果为True,输入和输出的形状为 (batch_size, seq_len, input_size);否则为 (seq_len, batch_size, input_size)
  • bidirectional:如果为True,使用双向GRU;否则为单向GRU(默认False)
  • dropout:在多层GRU中,是否在层之间应用dropout(默认值为0)

GRU的输入表示为:

  • 输入数据x: (seq_len, batch, input_size)
  • 初始隐藏状态h_0: (num_layers * num_directions, batch, hidden_size)

GRU的输出是一个元组(output, h_n):

  1. output:包含最后一层在所有时间步的隐藏状态
  2. 单向: (seq_len, batch, hidden_size)(默认)
  3. 双向: (seq_len, batch, hidden_size * 2)
  4. h_n (最终隐藏状态):所有层的最后一个时间步的隐藏状态
  5. 单向: (num_layers, batch, hidden_size)
  6. 双向: (num_layers * 2, batch, hidden_size)

代码示例:

python 复制代码
import torch

# 构建网络层
layer = torch.nn.GRU(input_size=5, hidden_size=3, num_layers=2, batch_first=False, dropout=0.1, bidirectional=True)

# x为GRU所需参数,而不是上边嵌入层的输出
# [seq_len,batch_size,input_size]
x = torch.randn(3, 3, 5)
# 初始化h0、c0 [num_layers,batch_size,hidden_size]
h0 = torch.zeros([2*2, 3, 3])

# 网络处理
y, hn = layer(x, h0)
print(y.shape)  # [seq_len,batch_size,hidden_size*bidirectional]
print(hn.shape)  # [num_layers*bidirectional,batch_size,hidden_size]


'''
输出:
torch.Size([3, 3, 6])
torch.Size([4, 3, 3])
'''

5. GRU的优缺点

优点

  • 比LSTM更简单:GRU只有两个门(与LSTM的三个门相比),因此它的计算复杂度更低,训练和推理速度较快。
  • 线性路径: ,可缓解梯度消失

缺点

  • 对超长序列的捕捉能力弱于LSTM:LSTM 通过独立的细胞状态(Cell State)显式存储长期信息,而 GRU 直接更新隐藏状态,可能导致长程依赖信息逐渐稀释。
  • 门控机制的非线性限制:更新门和重置门的 Sigmoid 激活函数可能导致梯度饱和(接近 0 或 1),影响参数更新。LSTM 的细胞状态更新包含线性操作,梯度传播更稳定。
  • 不可并行计算:时间步之间的依赖关系仍然限制了其并行化程度。

五、案例-酒店评论情感分析

目标:基于酒店评论数据,训练一个二分类模型,判断评论是"好评"(1)还是"差评"(0)。共2096条数据数据

python 复制代码
import numpy as np
import pandas as pd
import jieba, torch
from sklearn.metrics import accuracy_score
from torch.utils.data import Dataset, DataLoader
from sklearn.model_selection import train_test_split

# 1.定义变量
MAX_VOCAB_SIZE = 10000  # 词表最大容量: 保留出现概率最高的10000个词
MAX_LEN = 50  # 句子的最大长度: 所有句子统一成50个词, 不足不0, 超长阶段.
EMBED_DIM = 64  # 词向量维度: 词向量维度为64
HIDDEN_DIM = 64  # 隐藏层维度: 隐藏层维度为64
BATCH_SIZE = 64  # 批次大小: 批次大小为64
EPOCHS = 10  # 训练轮数: 训练轮数设置为10轮.
LR = 0.001  # 学习率: 0.001


# 2.读取、处理数据
def load_csv(path):
    df = pd.read_csv(path, sep="\t", encoding="utf-8")
    # print(df.head())
    # 删除评论内容为空的行.
    df.dropna(subset=["sentence", "label"])
    # 将sentence 转化为列表
    comment = df["sentence"].astype(str).tolist()
    label = df["label"].values
    # print(comment)
    return comment, label


def deal_data(texts):
    '''

    :param texts: 语料
    :return: seqs:转换后的语料, vocab:词表
    '''
    # 分词
    tokens = [jieba.lcut(i) for i in comment]
    word_count = {'<PAD>': 0, '<UNK>': 1}
    # 统计每次词语出现的频次
    for sent in tokens:
        for word in sent:
            word_count[word] = word_count.get(word, 0) + 1

    # 选取词频前10000的
    # word_to_id
    # top_words = sorted(word_count, key=lambda x:word_count[x], reverse=True)
    top_words = sorted(word_count, key=word_count.get, reverse=True)[:MAX_VOCAB_SIZE - 2]
    # print(top_words)

    # 生成词表
    vocab = {i: top_words.index(i) + 2 for i in top_words}
    vocab['<PAD>'] = 0
    vocab['<UNK>'] = 1
    # print(vocab)
    seqs = []
    # 填充
    for sen in tokens:
        temp = []
        for word in sen:
            temp.append(vocab.get(word, 1))
        if len(temp) > MAX_LEN:
            temp = temp[:MAX_LEN]
        if len(temp) < MAX_LEN:
            temp = temp + [0] * (MAX_LEN - len(temp))
        # print(temp)
        seqs.append(temp)
    return np.array(seqs), vocab


class CommentDataset(Dataset):
    def __init__(self, seqs, labels):
        self.seqs = seqs
        self.labels = labels

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

    def __getitem__(self, idx):
        x = self.seqs[idx]
        y = self.labels[idx]
        return torch.LongTensor(x), torch.FloatTensor([y])


# dataset、dataloader
def build_dataloader(seqs, labels):
    # 划分数据集
    train_seq, test_seq, train_label, test_label = train_test_split(seqs, labels, test_size=0.2, random_state=22)
    # 创建dataset
    train_dataset = CommentDataset(train_seq, train_label)
    test_dataset = CommentDataset(test_seq, test_label)
    # 创建dataloader
    train_dataloader = DataLoader(dataset=train_dataset, batch_size=BATCH_SIZE, shuffle=True)
    test_dataloader = DataLoader(dataset=test_dataset, batch_size=BATCH_SIZE, shuffle=False)
    return train_dataloader, test_dataloader


# 3.模型构建
class LATMModel(torch.nn.Module):
    def __init__(self, vocab):
        super().__init__()
        # 词嵌入层
        self.emb = torch.nn.Embedding(num_embeddings=len(vocab), embedding_dim=EMBED_DIM)
        # lstm层,添加batch_first=True可以不用更改嵌入层传入的值
        self.lstm = torch.nn.LSTM(input_size=EMBED_DIM, hidden_size=HIDDEN_DIM, num_layers=1, batch_first=True)
        # 输出层
        self.out = torch.nn.Linear(in_features=HIDDEN_DIM, out_features=1)

    def forward(self, x):
        x = self.emb(x)
        output, (h, c) = self.lstm(x)
        out = self.out(h[-1])  # h[-1] 取最后一层 隐藏层
        out = torch.sigmoid(out)
        return out


# 4.模型训练
def train(model, loader, criterion, optimizer):
    # 1. 设置模型为训练模式.
    model.train()
    # 2. 定义变量, 记录: 本轮总损失.
    total_loss = 0
    # 3. 定义变量, 记录: 预测正确的样本个数.
    correct = 0
    # 4. 定义变量, 记录: 总样本数.
    total = 0

    # 5. 从数据加载器中获取每批次数据.
    for seq, label in loader:  # seq: 数字序列张量(x特征) label: 标签张量(y标签)
        # 5.1 清空梯度.
        optimizer.zero_grad()
        # 5.2 前向传播, 得到输出.
        output = model(seq)
        # 5.3 计算损失.
        loss = criterion(output, label)  # output: 预测值, label: 真实标签
        # 5.4 反向传播, 计算梯度.
        loss.backward()
        # 5.5 优化器更新参数.
        optimizer.step()

        # 6. 统计训练指标.
        # 6.1 累加当前批次损失.
        total_loss += loss.item()
        # 6.2 输出 > 0.5判为好评, 否则差评.
        pred = (output > 0.5).float()
        # 6.3 统计预测正确的样本个数.
        correct += (pred == label).sum().item()
        # 6.4 统计总样本数.
        total += label.size(0)
    # 7. 返回(本轮的)平均损失 和 训练集准确率
    return total_loss / len(loader), correct / total


# 5.模型评估
def evaluate(model, loader):
    weight = torch.load(r'./data/model.pth', weights_only=False)
    model.load_state_dict(weight)
    # 1. 模型设置为评估模式.
    model.eval()
    # 2. 定义变量, 存储所有预测结果.
    preds = []
    # 3. 定义变量, 存储所有真实标签.
    trues = []
    # 4. 评估阶段 不计算梯度, 节省显存.
    with torch.no_grad():
        # 4.1 遍历测试集的每一批数据
        for seq, label in loader:
            # 4.2 模型的预测.
            output = model(seq)
            # 4.3 转为0/1的预测结果.
            pred = (torch.sigmoid(output) > 0.5).numpy()
            # 4.4 保存预测结果.
            preds.extend(pred)
            # 4.5 保存真实标签.
            trues.extend(label.numpy())

    # 5. 计算并返回测试集的准确率.
    return accuracy_score(trues, preds)


# 6. 模型预测
def predict(model, text, vocab):
    # 模型加载
    weight = torch.load(r'./data/model.pth', weights_only=False)
    model.load_state_dict(weight)
    # 1.对输入的评论文本分词.
    words = jieba.lcut(text)
    # 2. 分词转数字序列, 未知词用1替代.
    seq = [vocab.get(w, 1) for w in words]
    # 3. 统一序列长度为50.
    seq = seq[:MAX_LEN] + [0] * (MAX_LEN - len(seq))
    # 4. 转为张量, 并增加1个批次维度.
    seq = torch.LongTensor([seq])

    # 5. 模型预测.
    # 5.1 开启评估模式.
    model.eval()
    # 5.2 不计算梯度.
    with torch.no_grad():
        # 5.3 前向传播, 获取模型输出.
        output = model(seq)
    # 6. 返回预测结果.
    # 参1: 预测结果, 参2: 模型认为这句话是好评的概率(置信率)
    return '好评' if output > 0.5 else '差评', round(output.item(), 4)
    # print(f'评论: {comment}\n结果: {res}, 好评概率: {prob:.3f}\n')


if __name__ == '__main__':
    path = r'./data/train.tsv'
    comment, label = load_csv(path)
    seqs, vocab = deal_data(comment)
    # dataloader
    train_dataloader, test_dataloader = build_dataloader(seqs, label)
    model = LATMModel(vocab)

    optimizer = torch.optim.Adam(model.parameters(), lr=LR)
    error = torch.nn.BCELoss()

    # # 模型训练
    # for epoch in range(EPOCHS):
    #     loss, accuracy = train(model=model, loader=train_dataloader, optimizer=optimizer, criterion=error)
    #     print(f"loss:{loss}, accuracy:{accuracy}")
    # # 保存模型
    # torch.save(model.state_dict(), r'./data/model.pth')

    # 模型评估 -> 在测试集上评估模型.
    # test_acc = evaluate(model, test_dataloader)
    # print(f'\n 测试集准确率: {test_acc:.3f}')

    # 模型预测
    test_list = [
        '房间干净, 服务很好, 下次还来住',
        '环境差, 隔音不好, 非常不满意'
    ]
    for sen in test_list:
        result = predict(model=model, text=sen, vocab=vocab)
        print(f"{sen}:{result}")

'''
预测结果输出:
    房间干净, 服务很好, 下次还来住:('好评', 0.6892)
    环境差, 隔音不好, 非常不满意:('差评', 0.1319)
'''