LSTM项目实战——情感分析项目

任务:对微博评论信息的情感分析,建立模型,自动识别评论信息的情绪状态。

数据集:simplifyweibo_4_moods.csv

其中label分别为: 0: '喜悦' 1: '愤怒' 2: '厌恶' 3: '低落'

这里加载的是腾讯词向量库: embedding_Tencent.npz(4760 * 200)

对此项目的分析:

1、目标:将评论内容转换为词向量,这里考虑分字,而不是分词,因为数据集共有12w+条数据,分词的话可能会翻倍,按照分字的话最多1w,新华字典中汉字最多9000个。

2、每个词/字转换为词向量长度(维度)200,采用的是腾讯词向量库。

3、每一次传入的词/字的个数是否就是评论的长度? 应该是固定长度,每次传入数据与图像相似。 这里选择长度为70。则传入的数据为70*200,如果想要模型精确度更高,则选择评论长度最长的。

4、一条评论如果超过70个词/字怎么处理? 直接删除后面的内容

5、一条评论如果没有70个词/字怎么处理? 缺少的内容,统一使用一个数字(非词/字的数字)替代。

6、如果语料库中的词/字太多是否可以压缩? 可以,某些词/字出现的频率比较低,可能训练不出特征。因此可以选择频率比较高的词来训练。这里选择4760个。

7、被压缩的词/字如何处理? 可以统一使用一个数字(非词/字的数字)替代。

接下来开始项目:

一、词表onehot建立

由于选择了腾讯词向量库,这里限制了词表长度为4760(0--4759),4760作为<UNK>,4761作为<PAD>,对于整理好的字典数据,这里通过pickle库来保存数据。;

python 复制代码
from tqdm import tqdm    #python第3方,显示进度条
import pickle as pkl     #标准库,打包python数据

MAX_VOCAB_SIZE = 4760   # 词表长度限制
UNK,PAD = '<UNK>','<PAD>'   #未知字,pOdding符号 今天天气真好,我明要去打球->今天天气真好,我<UINK>要去打球PAD><PAD><PAD>3

def build_vocab(file_path,max_size,min_freq):
    '''
    功能:基于文本内容建立词表vocab,vocab中包含语料库中的字
    参数file_path:需要读取的语料库的路径
    max_size:   获取词频最高的前max_size个词
    min_freq:   剔除字频低于min_freq个的词
    '''
    '''
    def tokenizer(x):
        ls = []
        for y in x
            ls.append()
    '''
    tokenizer = lambda x:[y for y in x]
    vocab_dict = {}  #用于保存词的字典,统计每个字出现的次数,语料库中的字1w个,
    with open(file_path,'r',encoding='UTF-8') as f:
        i = 0
        for line in tqdm(f):   #用来显示循环的进度条
            if i ==0:      ##跳过文件种第1行表头内容
                i+=1
                continue
            lin = line[2:].strip()     ##获取评论内容,剔除标签。不用split分割,因为评论内容中可能会存在逗号。
            if not lin:           #如果lin中没有内容则continue
                continue
            for word in tokenizer(lin):     #对每行评论进行分字,并统计每个字出现的次数
                vocab_dict[word] = vocab_dict.get(word,0)+1      #get:查看字典vocab_dic是否存在word键,存在返回字典中键对应的值
        vocab_list = sorted([_ for _ in vocab_dict.items() if _[1] > min_freq],key=lambda x: x[1],reverse=True)[:max_size]
        vocab_dic = {word_count[0]:idx for idx,word_count in enumerate(vocab_list)}
        vocab_dic.update({UNK:len(vocab_list),PAD:len(vocab_dic)+1})
        print(vocab_dic)
        pkl.dump(vocab_dic,open('simplifyweibo_4_mods.pkl','wb'))   #现在我完成:统计了所有的文字,并将每一个独一无二的文字
        print(f"vocab size:{len(vocab_dic)}")       #将评论的内容,根据你现在词表vocab_dic,转换为词向量
    return vocab_dic

if __name__=='__main__':
    vocab = build_vocab('simplifyweibo_4_moods.csv',MAX_VOCAB_SIZE,3)    #如果某个字在整个语料库中一共就出现3次
    print('vocab')

二、评论转换为独热编码+切分数据集+数据打包

首先需要对每条评论数据进行处理,长度超过70的只选取前70个字,未超过70的,补充<PAD>至70,其次通过上一步生成的simplifyweibo_4_mods.pkl文件,查询每个字的独热编码,将每条评论转化为独热编码,直至所有数据。

接下来打包数据(类似Dataloader),当对这个类DatasetIterater实例化的对象进行遍历时,这个类中要有def iter(self),def next(self)这两个方法才可以遍历。

python 复制代码
from tqdm import tqdm
import pickle as pkl#
import random
import torch

UNK, PAD = '<UNK>', '<PAD>'  # 未知字, padding符号
def load_dataset(path, pad_size=70):
    contents = []#用来存储转换为数值标号的句子
    vocab = pkl.load(open('simplifyweibo_4_mods.pkl', 'rb')) #读取vocab文件
    tokenizer = lambda x: [y for y in x]#大模型的课程。
    with open(path, 'r', encoding='UTF-8') as f:
        i = 0
        for line in tqdm(f):
            if i ==0:
                i += 1
                continue
            if not line:    #是不是空行
                continue
            label = int(line[0])
            content = line[2:].strip('\n')
            words_line = []
            token = tokenizer(content)  # 将每一行的内容进行分字
            seq_len = len(token)    #获取一行实际内容的长度
            if pad_size:#判断每条评论是否超过70个字
                if len(token) < pad_size:  # 如果一行的字少于70,则补充<PAD>
                    token.extend([PAD] * (pad_size - len(token)))
                else:   # 如果一行的字大于70,则只取前70个字
                    token = token[:pad_size]    #如果一条评论种的字大于或等于70个字,索引的切分
                    seq_len = pad_size  #当前评论的长度

            # word to id
            for word in token:
                words_line.append(vocab.get(word, vocab.get(UNK)))#把每一条评论转换为独热编码
            contents.append((words_line, int(label), seq_len))
    random.shuffle(contents)#打乱顺序
    train_data = contents[ : int(len(contents)*0.8)]#前80%的评论数据作为训练集
    dev_data = contents[int(len(contents)*0.8) : int(len(contents)*0.9)]#把80%~90%的评论数据作为验证数据
    test_data = contents[int(len(contents)* 0.9):]#90%~最后的数据作为测试数据集
    return vocab, train_data, dev_data, test_data


class DatasetIterater(object):
    '''将数据batches切分为batch_size的包。'''

    def __init__(self, batches, batch_size, device):
        self.batch_size = batch_size
        self.batches = batches
        self.n_batches = len(batches) // batch_size  # 数据划分batch的数量
        self.residue = False  # 记录划分后的数据是否存在剩余的数据
        if len(batches) % self.n_batches != 0:  # 表示有余数
            self.residue = True
        self.index = 0
        self.device = device

    def _to_tensor(self, datas):  # 自己定义的一个函数,并不是内置的函数功能
        x = torch.LongTensor([_[0] for _ in datas]).to(self.device)  # 评论内容
        y = torch.LongTensor([_[1] for _ in datas]).to(self.device)  # 评论情感

        # pad前的长度(超过pad_size的设为pad_size)
        seq_len = torch.LongTensor([_[2] for _ in datas]).to(self.device)
        return (x, seq_len, y)  # (([23,34,...,13],70),2)

    # __getitem__:是通过索引的方式获取数据对象中的内容。__next__  p[3] 是使用 for  i in train_iter:  for  i  in p:

    def __next__(self):  # 用于定义迭代器对象的下一个元素。当一个对象实现了__next__方法时,它可以被用于创建迭代器对象。
        if self.residue and self.index == self.n_batches:  # 当读取到数据的最后一个batch:
            batches = self.batches[self.index * self.batch_size: len(self.batches)]
            self.index += 1
            batches = self._to_tensor(batches)  # 转换数据类型tensor
            return batches

        elif self.index > self.n_batches:  # 当读取完最后一个batch时:
            self.index = 0
            raise StopIteration  # 为了防止迭代永远进行,我们可以使用StopIteration(停止迭代)语句
        else:  # 当没有读取到最后一个batch时:
            batches = self.batches[self.index * self.batch_size: (self.index + 1) * self.batch_size]  # 提取当前bat
            self.index += 1
            batches = self._to_tensor(batches)
            return batches

    def __iter__(self):
        return self

    def __len__(self):
        if self.residue:
            return self.n_batches + 1
        else:
            return self.n_batches  # dataload

if __name__=='__main__':    #数据的预处理:1、将所有的评论都转换为独热编码,2、提取出 训练集、验证集、测试集。
    vocab, train_data, dev_data,test_data = load_dataset('simplifyweibo_4_moods.csv')
    print(train_data, dev_data,test_data )
    print('结束')

三、LSTM网络模型的构建

python 复制代码
import torch.nn as nn


class Model(nn.Module):  # 名字随意定义
    '''
    nn.LSTM 参数拆解
embed:输入特征维度(词向量维度,你这里是 200)
128:每一层 LSTM 的隐藏单元数(隐状态维度)
3:LSTM 的堆叠层数(3 层深层 LSTM)
bidirectional=True:双向 LSTM,最终隐状态维度会翻倍(128×2)
batch_first=True:输入张量形状为 (batch_size, seq_len, embed_dim),而非默认的 (seq_len, batch_size, embed_dim)
dropout=0.3:层间 Dropout,仅在多层 LSTM 的非最后一层生效
nn.Linear(128 * 2, num_classes) 为什么乘 2因为开启了双向 LSTM,前向和后向的隐状态会拼接在一起,所以输入全连接层的维度是 128×2,对应 4 分类任务的输出维度 num_classes=4
    '''
    def __init__(self, embedding_pretrained, n_vocab, embed, num_classes):
        super(Model, self).__init__()

        if embedding_pretrained is not None:  # 4761 pad  告诉模型padding_idx是4761,如果没有预训练的词向量模型,那么就
            self.embedding = nn.Embedding.from_pretrained(embedding_pretrained, padding_idx = n_vocab-1, freeze=False)
            # freeze:指定是否冻结embeding层的权重。True则embedding层的权重在训练过程中
        else:
            self.embedding = nn.Embedding(n_vocab, embed, padding_idx = n_vocab - 1)  # 如果新训练embedding使用本行
        # padding_idx默认为None,如果指定,则padding_idx对应的参数PAD不会对梯度产生影响,因此在padding_idx处词嵌入向量在训
        self.lstm = nn.LSTM(embed, 128, 3, bidirectional=True, batch_first=True, dropout=0.3)
        # 128为每一层中每个隐状态中的U、W、V的神经元个数,resnet()
        # 3为隐藏层 层的个数,batch_first=True表示输入和输出张量将以(batch, seq, feature) 而不是(seq, batch, feature)
        # bidirectional = True:指定LSTM是双向的。网络会同时从前向后和从后向前处理输入序列,两个方向的输出结合起来。通常可以
        # dropout = 0.3:这指定了在LSTM层中使用的dropout比例。Dropout是一种正则化技术,用于防止网络在训练过程中过拟合。在每个LSTM的输出上,都有
        self.fc = nn.Linear(128 * 2, num_classes)

    def forward(self,x):
        x,_ = x         #就是只提取评论的独热编码
        out = self.embedding(x)
        out,_ = self.lstm(out)      
        out = self.fc(out[:,-1,:])
        return out

四、训练+测试+验证

python 复制代码
import torch
import torch.nn.functional as F
import numpy as np
from sklearn import metrics

def evaluate(class_list, model, data_iter, test=False):
    # 验证集的处理
    # class_list: ['喜悦', '愤怒', '厌恶', '低落']
    model.eval()  # 进入测试模型,将model的w设置为只读模式,w中的值都没被修改的权限,保护模型不被修改。
    loss_total = 0
    predict_all = np.array([], dtype=int)
    labels_all = np.array([], dtype=int)

    with torch.no_grad():  # 一个上下文管理器,关闭梯度计算。当你确认不会调用Tensor.backward()的时候。这可以减少计算所用内存消耗。
        for texts, labels in data_iter:
            outputs = model(texts)  # 它就是输出。
            loss = F.cross_entropy(outputs, labels)
            loss_total += loss
            labels = labels.data.cpu().numpy()
            # torch.max(outputs.data, 1)[1].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)


def train(model, train_iter, dev_iter, test_iter, class_list):
    # 进入训练模式,启用 dropout、batch_norm 等训练特定层
    model.train()

    # 初始化优化器(Adam 优化器,学习率 1e-3)
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

    total_batch = 0  # 记录累计进行的 batch 数量
    dev_best_loss = float('inf')  # 初始化验证集最佳损失为无穷大
    last_improve = 0  # 记录上次验证集 loss 下降的 batch 数
    flag = False  # 标记是否长时间未优化
    epochs = 20  # 训练轮数

    for epoch in range(epochs):
        # 打印当前训练轮次
        print('Epoch [{}/{}]'.format(epoch + 1, epochs))

        # 遍历训练数据迭代器
        for i, (trains,labels) in enumerate(train_iter):
            # 经过DatasetIterater中的_to_tensor返回的数据格式为:(x,seq_len),y
            # 模型前向传播,获取输出
            outputs = model(trains)
            # 计算交叉熵损失
            loss = F.cross_entropy(outputs, labels)
            # 梯度清零(避免梯度累积)
            model.zero_grad()
            # 反向传播,计算梯度
            loss.backward()
            # 更新模型参数
            optimizer.step()
            if total_batch % 100 == 0:  # 每多少轮输出在训练集和验证集上的效果
                predic = torch.max(outputs.data, 1)[1].cpu()
                train_acc = metrics.accuracy_score(labels.data.cpu(), predic)
                dev_acc, dev_loss = evaluate(class_list, model, dev_iter)  # 将验证数据集传入模型
                if dev_loss < dev_best_loss:
                    dev_best_loss = dev_loss
                    # 保存最优模型
                    torch.save(model.state_dict(), 'TextRNN.ckpt')
                    last_improve = total_batch  # 保存最优模型的batch值

                msg = 'Iter: {0:>6}, Train Loss: {1:>5.2}, Train Acc: {2:>6.2%}, Val Loss: {3:>5.2}, Val Acc: {4:>6.2%}'
                print(msg.format(total_batch, loss.item(), train_acc, dev_loss, dev_acc))

            model.train()
            total_batch += 1
            if total_batch - last_improve > 10000:
                # 验证集loss超过10000 batch没下降,结束训练
                print("No optimization for a long time, auto-stopping...")
                flag = True

        # 若触发早停,跳出外层训练循环
        if flag:
            break

    test(model, test_iter, class_list)

import time

def test(model, test_iter, class_list):
    # model.load_state_dict(torch.load('TextRNN.ckpt'))
    model.eval()  # 进入测试模式,w只有读的权限
    start_time = time.time()  # 当前的时间

    test_acc, test_loss, test_report = evaluate(class_list, model, test_iter, test=True)
    msg = 'Test Loss: {0:>5.2},  Test Acc: {1:>6.2%}'
    print(msg.format(test_loss, test_acc))
    print(test_report)

五、主程序

python 复制代码
import torch
import numpy as np
import load_dataset, TextRNN
from train_eval_test import train


device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
np.random.seed(1)  # np.random.seed:用于设置随机数生成器的种子。
torch.manual_seed(1)  # torch.manual_seed是用于设置PyTorch中随机数生成器的种子的函数。它将为当前进程设置一个随机种子。
torch.cuda.manual_seed_all(1)  # 是用于设置所有CUDA设备的随机种子的函数。它将为每个CUDA设备设置相同的随机种子,以确保在不同的运行中获
torch.backends.cudnn.deterministic = True
# torch.backends.cudnn.deterministic:用于控制使用cudnn库时的算法是否确定。如果将这个标志设置为True,每次返回的卷积算法将是确定的,
# 如果配合上设置Torch的随机种子为固定值的话,可以保证每次运行网络的时候输入是固定的。


vocab, train_data, dev_data, test_data = load_dataset.load_dataset('simplifyweibo_4_moods.csv')

train_iter = load_dataset.DatasetIterater(train_data, 128, device)  # 128条数据打包。
dev_iter = load_dataset.DatasetIterater(dev_data, 128, device)
test_iter = load_dataset.DatasetIterater(test_data, 128, device)

embedding_pretrained = torch.tensor(np.load('embedding_Tencent.npz')["embeddings"].astype('float32'))
# embedding_pretrained = None    # 不使用外部训练的词向量。
embed = embedding_pretrained.size(1) if embedding_pretrained is not None else 200  # 词向量200
class_list = ['喜悦', '愤怒', '厌恶', '低落']
num_classes = len(class_list)
model = TextRNN.Model(embedding_pretrained, len(vocab), embed, num_classes).to(device)
train(model, train_iter, dev_iter, test_iter, class_list)
相关推荐
byte轻骑兵2 小时前
从收音机到蓝牙:LE Audio核心BASS服务解析与实战
人工智能·音视频·语音识别·le audio·低功耗音频
饭后一颗花生米2 小时前
2026 AI加持下前端学习路线:从入门到进阶,高效突破核心竞争力
前端·人工智能·学习
默 语2 小时前
“我跑不过我的代码“:今天北京半马,程序员追机器人追到开电瓶车
人工智能·机器人·openclaw
AC赳赳老秦2 小时前
HR必备:OpenClaw批量筛选简历、发送面试通知,优化招聘流程
运维·人工智能·python·eclipse·github·deepseek·openclaw
GreenTea3 小时前
Deep Dive into Claude Code:源码泄漏引发的AI Agent架构全解析
前端·人工智能·后端
圊妖3 小时前
Claude Code 一些进阶用法
人工智能·ai编程·claude
颜酱3 小时前
从零实现「拍照记单词」小应用(可复刻版)
前端·javascript·人工智能