LSTM自然语言处理情感分析项目(三)定义模型结构与模型训练评估测试

目录

一.构建双向LSTM网络

[1. 词嵌入层(Embedding Layer)](#1. 词嵌入层(Embedding Layer))

[2. LSTM 层详解](#2. LSTM 层详解)

[3. 全连接层](#3. 全连接层)

4.前向传播过程

二.模型的训练,评估和测试

1.评估函数

核心功能

2.训练函数

训练前准备

训练循环

性能监控与模型保存

早停机制

3.测试函数

4.单个样本测试

单样本测试函数的作用

[1. 模型加载与准备](#1. 模型加载与准备)

[2. 词汇表加载](#2. 词汇表加载)

[3. 文本预处理](#3. 文本预处理)

[4. 文本向量化](#4. 文本向量化)

[5. 张量准备](#5. 张量准备)

[6. 模型预测](#6. 模型预测)

[7. 结果解析与展示](#7. 结果解析与展示)


一.构建双向LSTM网络

我们的模型主要包含三个核心组件:

  • 词嵌入层(Embedding Layer)
  • 双向 LSTM 层
  • 全连接输出层

1. 词嵌入层(Embedding Layer)

python 复制代码
import torch.nn as nn
class Model(nn.Module):
    def __init__(self,embedding_pretrained,n_vocal,embed,num_classes):
        super(Model,self).__init__()
        if embedding_pretrained is not None:
            self.embedding=nn.Embedding.from_pretrained(embedding_pretrained,padding_idx=n_vocal-1,freeze=False)
        else:
            self.embedding=nn.Embedding(n_vocal,embed,padding_idx=n_vocal-1)
  • 这里我们提供了两种词嵌入方式:使用预训练词向量(如 Word2Vec、GloVe)或随机初始化
  • padding_idx参数指定了填充符号的索引,在训练过程中不会更新其嵌入值
  • freeze=False表示预训练词向量在训练过程中可以微调,有助于适应特定任务

2. LSTM 层详解

LSTM 层是模型的核心,负责捕捉文本序列中的上下文信息:

python 复制代码
self.lstm = nn.LSTM(
    input_size=embed,        # 输入特征维度(词向量维度)
    hidden_size=128,         # 隐藏层维度
    num_layers=3,            # LSTM层数
    bidirectional=True,      # 双向LSTM
    batch_first=True,        # 输入输出格式为(batch, seq, feature)
    dropout=0.3              # Dropout比率,防止过拟合
)
复制代码
dropout=0.3,训练的参数比例为0.7,舍弃一些极端的参数如0.0001等,防止过拟合
复制代码
bidirectional=True,双向LSTM,网络会同时从前向后和从后向前处理序列,两个方向的输出结合起来
复制代码
128为每一层中每个隐状态中的U,W,V的神经元个数
复制代码
3为隐藏层 层的个数,batch_first=True表示输入和输出张量以(batch,seq,feature)提供
  • 双向 LSTM :模型会同时从左到右和从右到左处理序列,捕捉更全面的上下文信息
  • 多层结构:3 层 LSTM 可以学习更复杂的特征表示,每一层的输出作为下一层的输入
  • Batch First:设置为 True 时,输入输出的形状为 (batch_size, sequence_length, features),更符合我们的使用习惯

3. 全连接层

python 复制代码
self.fc = nn.Linear(128*2, num_classes)
  • 由于使用了双向 LSTM,每个时间步的输出是两个方向的拼接,所以输入维度是 128×2
  • 输出维度为类别数量,最终通过 softmax(通常在损失函数中集成)得到各类别的概率

4.前向传播过程

python 复制代码
 def forward(self,x):#([23,34,.13],69)
        x,_=x#就是只提取评论的独热编码
        out=self.embedding(x)
        out,_=self.lstm(out)#调试模型,你来观察lstm输出结果是什么样的数据类型?为什么有一个下划线
        out=self.fc(out[:,-1,:]) # 句子最后时刻的 hidden state
        return out

关于代码中**out, _ = self.lstm(out)**的下划线:LSTM 的输出包含两部分,第一部分是所有时间步的隐藏状态,第二部分是最后一个时间步的隐藏状态和细胞状态。在这里我们用下划线忽略了第二部分,因为后续计算只需要所有时间步的输出。

forward 方法定义了数据在模型中的流动过程

  1. 首先从输入中提取文本序列部分
  2. 将序列通过嵌入层转换为词向量序列
  3. 将词向量序列输入 LSTM 层,得到所有时间步的输出
  4. 取最后一个时间步的输出(out[:, -1, :])作为整个句子的表示
  5. 通过全连接层得到最终的分类结果

这种取最后一个时间步输出 的做法适用于文本分类任务,假设最后一个时间步的状态已经捕捉了整个序列的信息。

二.模型的训练,评估和测试

我们的代码包含三个主要函数:

  • evaluate():评估函数,用于在验证集上评估模型性能
  • test():测试函数,用于在测试集上获取最终评估结果
  • train():训练函数,实现模型的完整训练流程

1.评估函数

python 复制代码
def evaluate(class_list, model, data_iter,test=False):
    model.eval()
    loss_total=0
    predict_all=np.array([],dtype=int)
    labels_all=np.array([],dtype=int)
    with torch.no_grad():
        for texts,labels in data_iter:
            outputs=model(texts)
            loss=F.cross_entropy(outputs,labels)
            loss_total+=loss
            labels=labels.data.cpu().numpy()
            predic=torch.max(outputs.data,1)[1].cpu().numpy()
            labels_all=np.append(labels_all,labels)
            predict_all=np.append(predict_all,predic)
    acc=metrics.accuracy_score(labels_all,predict_all)
    if test:
        report=metrics.classification_report(labels_all,predict_all,target_names=class_list,digits=4)
        return acc,loss_total/len(data_iter),report
    return acc,loss_total/len(data_iter)
核心功能
  • 将模型设置为评估模式(model.eval()
  • 禁用梯度计算(torch.no_grad())以提高效率
  • 计算整体损失和准确率
  • 生成详细的分类报告(针对测试模式)

评估模式与训练模式的主要区别在于:评估模式会关闭 dropout 层,固定批量归一化层的统计量,确保评估结果的一致性

2.训练函数

训练前准备
python 复制代码
def train(model,train_iter,dev_iter,test_iter,class_list):
    model.train()  # 设置为训练模式
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)  # 初始化优化器

这里使用了 Adam 优化器,这是一种在实践中表现优异的自适应学习率优化器,适合大多数深度学习任务。

训练循环

训练过程采用双层循环结构:

  • 外层循环控制训练轮数(epochs)
  • 内层循环迭代处理每个批次的数据
python 复制代码
total_batch=0# 记录进行到多少batch
    dev_best_loss=float('inf')#表示无穷大
    last_improve=0#记录上次验证集loss下降的batch数
    flag=False#记录是否很久没有效果提升
    epochs=2#设置训练次数
    for epoch in range(epochs):
        print('Epoch [{}/{}]'.format(epoch+1,epochs))
        for i,(trains,labels) in enumerate(train_iter):
            # 经过DatasetIterater中的_to_tensor返回的数据格式为:(x,seg_len),y
            outputs=model(trains)
            loss=F.cross_entropy(outputs,labels)
            model.zero_grad()
            loss.backward()
            optimizer.step()

每处理一个批次,都会:

  1. 执行前向传播计算输出
  2. 计算损失(使用交叉熵损失函数)
  3. 反向传播计算梯度
  4. 更新模型参数
性能监控与模型保存
python 复制代码
        if total_batch % 100 == 0:
        # 计算训练集准确率
        # 在验证集上评估
            if dev_loss < dev_best_loss:
                dev_best_loss = dev_loss
                torch.save(model.state_dict(), 'TextRNN.ckpt')  # 保存最佳模型
                last_improve = total_batch

每 100 个批次,我们会评估模型在训练集和验证集上的性能,并保存验证集损失最小的模型(最佳模型)。

早停机制

为了防止过拟合和节省训练时间,代码实现了早停机制:

python 复制代码
            if total_batch - last_improve > 10000:
                print('No optimization for a long time, auto-stopping...')
                flag = True
                break
    acc,loss_avg,report=test(model,test_iter,class_list)
    print('Test Acc:{} Loss_avg:{}'.format(acc,loss_avg))
    print('Test report:{}'.format(report))

如果连续 10000 个批次验证集性能没有提升,训练会自动停止。

3.测试函数

测试函数与评估函数功能相似,主要区别在于参数默认值的设置。这种设计允许我们在调用时使用更简洁的语法:

  • 调用测试时:test(class_list, model, test_iter)
  • 调用评估时:evaluate(class_list, model, dev_iter)

在实际应用中,这两个函数可以合并为一个,通过参数来控制行为,减少代码冗余。

python 复制代码
def test(class_list, model, data_iter,test=True):
    model.eval()
    loss_total=0
    predict_all=np.array([],dtype=int)
    labels_all=np.array([],dtype=int)
    with torch.no_grad():
        for texts,labels in data_iter:
            outputs=model(texts)
            loss=F.cross_entropy(outputs,labels)
            loss_total+=loss
            labels=labels.data.cpu().numpy()
            predic=torch.max(outputs.data,1)[1].cpu().numpy()
            labels_all=np.append(labels_all,labels)
            predict_all=np.append(predict_all,predic)
    acc=metrics.accuracy_score(labels_all,predict_all)
    if test:
        report=metrics.classification_report(labels_all,predict_all,target_names=class_list,digits=4)
        return acc,loss_total/len(data_iter),report
    return acc,loss_total/len(data_iter)

4.单个样本测试

单样本测试函数的作用

单样本测试函数是连接模型与实际应用的重要桥梁,它实现了从原始文本到情感类别的完整转换流程。这个函数通常包含以下步骤:

  • 加载训练好的模型参数
  • 对输入文本进行预处理
  • 执行模型预测
  • 返回并展示预测结果
1. 模型加载与准备
python 复制代码
def test_sample(sample, model):
    # 加载训练好的模型参数
    model.load_state_dict(torch.load('TextRNN.ckpt', map_location=device))
    # 设置模型为评估模式
    model.eval()

这部分代码负责加载训练好的模型权重map_location=device参数确保模型可以正确加载到指定的设备(CPU 或 GPU)上。model.eval()将模型设置为评估模式,这会关闭 dropout 等只在训练时使用的功能,确保预测结果的一致性

2. 词汇表加载
python 复制代码
# 加载词汇表
    vocab = pkl.load(open('simplifyweibo_4_moods.pkl', 'rb'))

词汇表(vocabulary)是训练过程中创建的,用于将文本中的词语(或字符)映射到数字索引。这里加载的词汇表必须与训练时使用的保持一致,否则会导致索引不匹配的错误。

3. 文本预处理

文本预处理是确保模型能够正确处理输入的关键步骤,需要与训练数据的预处理方式完全一致:

python 复制代码
# 将文本分割为字符
    token = [x for x in sample]
# 填充或截断文本至固定长度(70)
    token.extend(['<PAD>'] * (70 - len(token)))

这段代码首先将输入文本分割为字符序列(这里使用的是字符级模型),然后将序列长度统一调整为 70------ 短于 70 的文本用<PAD>(填充符)补足,长于 70 的文本会被截断(在这段代码中只展示了填充部分)。

统一序列长度是因为神经网络通常要求输入具有固定的维度。

4. 文本向量化
python 复制代码
# 将文本转换为数字索引
    word_line = []
    for word in token:
    # 查找词汇表,未知词用<UNK>的索引代替
        word_line.append((vocab.get(word, vocab.get('<UNK>'))))

这一步将字符序列转换为数字索引序列。对于词汇表中不存在的字符(未知字符),使用<UNK>(未知符)的索引代替,这与训练过程中的处理方式一致。

5. 张量准备
python 复制代码
# 转换为PyTorch张量并添加批次维度
    x = torch.LongTensor(word_line).unsqueeze(0).to(device)
# 构造模型所需的输入格式
    x = (x, torch.tensor([0]).to(device))

PyTorch 模型要求输入为张量(Tensor)格式,因此需要进行以下转换:

  • 将列表转换为LongTensor(因为是整数索引)
  • 使用unsqueeze(0)添加批次维度(模型通常处理批次数据,即使批次大小为 1)
  • 将张量移动到适当的设备(CPU 或 GPU)
  • 构造与训练时一致的输入格式(这里是包含两个元素的元组)
6. 模型预测
python 复制代码
# 执行预测
    with torch.no_grad():  # 关闭梯度计算,节省内存
        outputs = model(x)

with torch.no_grad():上下文管理器用于禁用梯度计算,这在纯预测阶段可以显著节省内存并提高计算速度。模型的输出outputs通常是每个类别的得分(logits)。

7. 结果解析与展示
python 复制代码
# 获取预测结果
    predic = torch.max(outputs.data, 1)[1].cpu().numpy()[0]

# 定义情感类别列表
    class_list = ['喜悦', '愤怒', '厌恶', '低落']

# 输出预测结果
    print(f"{sample}的情绪是: {class_list[predic]}")
  • torch.max(outputs.data, 1)返回指定维度(这里是类别维度)的最大值和对应的索引
  • [1]取索引部分,即预测的类别索引
  • cpu().numpy()[0]将结果从张量转换为 numpy 数组,并取出唯一的元素(因为批次大小为 1)
  • 最后将数字索引转换为对应的情感类别名称并打印
相关推荐
三年呀4 小时前
深度剖析Mixture of Experts(MoE)架构:从原理到实践的全面指南
人工智能·深度学习·架构·模型优化·大规模模型
BJ_Bonree4 小时前
博睿数据首次亮相全球顶级科技盛会Tech Week Singapore 2025,与全球共享可观测性前沿成果!
人工智能
新智元4 小时前
OpenAI 官宣自研首颗芯片,AI 界「M1 时刻」九个月杀到!联手博通三年 10GW
人工智能·openai
新智元5 小时前
Karpathy「疯狂之作」:100 美元、4 小时,就能训练你自己的「小型 GPT」
人工智能·openai
喜欢吃豆5 小时前
一份面向研究人员的强化学习对齐指南:为自定义语言模型实施与评估 PPO 和 DPO
人工智能·语言模型·自然语言处理·架构·大模型
用户68545375977695 小时前
🚀 Transformer:让AI变聪明的"读心术大师" | 从小白到入门的爆笑之旅
人工智能·后端
金井PRATHAMA5 小时前
语义与认知中的循环解释悖论及其对人工智能自然语言处理深层语义分析的影响与启示
人工智能·自然语言处理·知识图谱
berryyan5 小时前
Windows WSL 环境下配置 Claude Code 非官方账号2233.ai完整教程
人工智能·python