基于LSTM的影评情感分析项目

前言

本项目基于LSTM (Long Short-Term Memory)也就是常说的长短期记忆神经网络 通过对4万条在IMDb网站上的电影评论进行训练 使其构建成一个能够自动分析英文影评情感的系统

本项目的主要目标:

1. 从数据处理开始 构建并训练一个高效的LSTM模型 使其能够分析该条电影评论的情感倾向。

2. 开发一个基于tkinter的简单GUI界面 如上图所示 可直观的显示输入的评论并获得情感分析的结果

每段代码都附有注释 便于简单的理解代码的逻辑和流程 但并未对底层逻辑进行详细解释 所以如果不知道LSTM相关知识的话 可以先去b站简单补习一下 

文件结构

IMBd analysis/
│
├── data/
│   └── IMDB Dataset.csv
│
├── model/
│   ├── sentiment_model.pt
│   └── training.log
│
├── preprocessed_data/
│   ├── x.pkl
│   ├── y.pkl
│   ├── index_to_word.pkl
│   ├── word_to_index.pkl
│   ├── word_count.pkl
│   ├── corpus_idx.pkl
│   ├── X_test.pkl
│   ├── X_train.pkl
│   ├── y_test.pkl
│   ├── y_train.pkl
│   └── y_converted.pkl 
│
├── utils/
│   ├── __init__.py
│   ├── data_loader.ipynb
│   ├── data_loader.py
│   ├── vocab_builder.ipynb
│   ├── vocab_builder.py
│   ├── data_process.py
│   ├── model.py
│   ├── train.py
│   ├── predict.py
│   ├── predict_review.py
│   └── GUI.py
└── main.py 

包初始化 __init__.py

import nltk

nltk.download('wordnet')
# 下载wordnet数据包 用于词形还原

nltk.download('stopwords')
# 下载停用词数据包 过滤没用的词汇

# 注意 如果下载完之后运行还是提示无法找到这两个数据包的话
# 可以看一下C盘-用户-你的名字-然后打开隐藏文件AppData
# 然后找到Roaming-nltk_data-corpora 找到这两个数据包
# 的压缩zip形式 然后解压到corpora里 就可以了

数据加载 data_loader.py

数据集下载地址:点击跳转

该数据集包含了将近五万条的英文电影评论 所以运行时间可能会有点长 调试的时候建议使用jupyter notebook或者只取1000条

import pickle
import re
import pandas as pd
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from nltk.stem import WordNetLemmatizer

def clean_text(text):
    text = re.sub(r'<.*?>', '', text)
    # 清除内涵的html标签

    text = re.sub(r'[^a-zA-Z]',' ',text)
    # 清楚所有非字母元素

    text = text.lower()
    # 全部转化为小写

    words = word_tokenize(text)
    # 对这一段文本进行分词拆分

    lemmatizer = WordNetLemmatizer()
    # 初始化词形还原器

    words = [lemmatizer.lemmatize(word) for word in words if word not in stopwords.words('english')]
    # 将所有词形进行还原 并同时判断其是否为停用词 不是的话将其储存
    # 停用词为不包含什么实际信息并且出现频率较多的词汇 例如the is a

    text = ' '.join(words)
    # 将处理过的单词进行重新组合

    return text

def dataloader():
    filename = './data/IMDB Dataset.csv'
    data = pd.read_csv(filename)
    # 用pandas取出数据 形成DataFrame形式 便与后续的处理

    x = data['review']
    y = data['sentiment']
    # 取出评论和情感标签

    for i in range(len(x)):
        x[i] = clean_text(x[i])

        if i % 1000 == 0:
            print(i)
        # 便于观测数据处理进度

    with open('preprocessed_data/x.pkl', 'wb') as f:
        pickle.dump(x, f)
    with open('preprocessed_data/y.pkl', 'wb') as f:
        pickle.dump(y, f)
    # 将处理后的数据以二进制形式序列化保存到文件中 便于以后的取用

词汇构建 vocab_builder.py

import pickle
from nltk.tokenize import word_tokenize
from tensorflow import keras
from sklearn.model_selection import train_test_split

def vocab_builder():
    with open('../preprocessed_data/x.pkl', 'rb') as f:
        x = pickle.load(f)
    with open('../preprocessed_data/y.pkl', 'rb') as f:
        y = pickle.load(f)

    index_to_word, all_words = ['PAD',],[]
    # 初始化词汇表和单词表 因为后续要将不满100个单词的句子用0填充
    # 所以词汇表的第一位‘0’ 为填充‘PAD’

    i = 0
    for line in x:
        i+=1
        if i % 1000 == 0:
            print(i)
        # 看看遍历到哪了

        words = word_tokenize(line)
        # 对每一行文本进行分词

        all_words.append(words)
        # 将每一份的文本分词添加到单词表中

        for word in words:
            if word not in index_to_word:
                index_to_word.append(word)
                # 构建唯一单词的词汇表 用于将索引转换为单词

    word_to_index = {word:idx for idx,word in enumerate(index_to_word)}
    # 反过来构建一下词汇对索引的表示 方便将词汇转换为索引

    word_count = len(index_to_word)

    corpus_idx = []
    # 用于存放每句话转换完索引之后的数据

    for line in all_words:
        temp = []
        for word in line:
            temp.append(word_to_index[word])
            # 这是完整的一段文本 也就是一条评论的索引数据

        temp = keras.preprocessing.sequence.pad_sequences([temp], maxlen=100, padding='post')
        # maxlen=100 将超过100个词汇的评论裁剪为100
        # padding='post' 不够100条的在词尾填充‘0’

        corpus_idx.append(temp[0])
        # 因为pad_sequences返回的是二维数组 所以这里要用temp[0]来提取内容
        # 否则corpus_idx 将变成一个三维数组

    label_to_index = {"positive": 1, "negative": 0}
    y_converted = [label_to_index[label] for label in y]
    # 构建一个字典 将目标值转换为1或0

    X_train, X_test, y_train, y_test = train_test_split(corpus_idx,y_converted, test_size=0.2)
    # 划分训练集和测试集 8:2


    with open('../preprocessed_data/index_to_word.pkl', 'wb') as f:
        pickle.dump(index_to_word, f)
    with open('../preprocessed_data/word_to_index.pkl', 'wb') as f:
        pickle.dump(word_to_index, f)
    with open('../preprocessed_data/word_count.pkl', 'wb') as f:
        pickle.dump(word_count, f)
    with open('../preprocessed_data/corpus_idx.pkl', 'wb') as f:
        pickle.dump(corpus_idx, f)
    with open('../preprocessed_data/y_converted.pkl', 'wb') as f:
        pickle.dump(y_converted,f)
    with open('../preprocessed_data/X_train.pkl', 'wb') as f:
        pickle.dump(X_train,f)
    with open('../preprocessed_data/X_test.pkl', 'wb') as f:
        pickle.dump(X_test,f)
    with open('../preprocessed_data/y_train.pkl', 'wb') as f:
        pickle.dump(y_train,f)
    with open('../preprocessed_data/y_test.pkl', 'wb') as f:
        pickle.dump(y_test,f)
    # 保存数据

数据处理 data_process.py

import torch
from torch.utils.data import Dataset

class data_process(Dataset):
    def __init__(self,X,y):
        self.X = X
        self.y = y
    # 初始化 用于接收数据和目标值

    def __len__(self):
        return len(self.X)
    # 返回数据的长度 方便dataloader根据批量大小进行处理

    def __getitem__(self, idx):
        return torch.tensor(self.X[idx], dtype=torch.long), torch.tensor(self.y[idx], dtype=torch.float32)
    # 将数据转化为张量并返回

模型构建 model.py

import torch
import torch.nn as nn
import torch.nn.functional as F

class sentimentmodel(nn.Module):
    def __init__(self,word_count,output):
        super(sentimentmodel, self).__init__()
        self.ebd = nn.Embedding(word_count,256)
        # 定义词嵌入层 将词汇数量映射为256维的向量

        self.lstm = nn.LSTM(256,256,1,batch_first=True)
        # 定义lstm层 进行一次循环 并且将batch_size置为第一维(好习惯)

        self.out = nn.Linear(256,output)
        #定义全连接层

    def forward(self,X,hidden):
        ebd = self.ebd(X)
        # 首先通过嵌入曾 将词汇索引转换为词向量

        ebd = F.dropout(ebd, p=0.5,training=self.training)
        # 使用dropout使每一次训练时神经元失效一部分 防止过拟合
        # 并且只在训练中生效

        ebd = ebd.squeeze(1)
        # 去除维度为1的维度 简化张量形状并使得其贴合方法的处理

        ebd, hidden = self.lstm(ebd,hidden)
        # 进入LSTM层进行运算

        ebd = ebd[:,-1,:]
        # 取出最后一个时间步的输出

        out = self.out(ebd)
        # 将最后一个时间步通过全连接层得到输出

        return torch.sigmoid(out),hidden
        # 将输出的值映射到0-1之间 方便进行0 1判断

    def init_hidden(self, batch_size):
        return (torch.zeros(1, batch_size, 256).to(next(self.parameters()).device)
                    ,torch.zeros(1, batch_size, 256).to(next(self.parameters()).device))
        # 初始化并返回隐藏层和细胞层的全0状态 并且使其在同一GPU或CPU上进行计算

训练脚本 train.py

import pickle
import time
import torch
from torch import nn, optim
from torch.utils.data import DataLoader
from utils.data_process import data_process
from utils.model import sentimentmodel

def train():
    with open('D:/IMBd analysis/preprocessed_data/X_train.pkl', 'rb') as f:
        X_train = pickle.load(f)
    with open('D:/IMBd analysis/preprocessed_data/y_train.pkl', 'rb') as f:
        y_train = pickle.load(f)
    with open('D:/IMBd analysis/preprocessed_data/word_count.pkl', 'rb') as f:
        word_count = pickle.load(f)

    train_process = data_process(X_train, y_train)
    # 创建数据处理的对象

    model = sentimentmodel(word_count, 1)
    # 初始化模型 输出设定为一

    dataloader = DataLoader(train_process, batch_size=32, shuffle=True,drop_last=True)
    # 创建数据加载器 每32个为一组 并且最后不足32个的舍弃

    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model.to(device)
    # 如果GPU可用的话就在GPU上运行 否则CPU

    criterion = nn.BCELoss()
    # 使用二分类交叉熵损失函数 更好的度量与测概率和目标值之间的误差

    optimizer = optim.Adam(model.parameters(), lr=1e-4)
    # 使用Adam自适应学习率来优化参数更新

    epoch = 35
    # 设置训练轮数

    train_log = './model/training.log'
    file = open(train_log, 'w')
    # 保存训练时的日志(好习惯)

    for epoch in range(epoch):
        epoch_idx = 0
        total_loss = 0.0
        start_time = time.time()
        # epoch_idx用于记录处理了多少个batch 方便求平均损失
        # total_loss用于计算每一次训练的总损失
        # 记录时间 计算每一次训练的时长

        for X, y in dataloader:

            X, y = X.to(device), y.to(device)
            # 将数据和目标值移动到可用设备上

            hidden = model.init_hidden(batch_size=X.size(0))
            # 初始化隐藏状态

            output, hidden = model(X,hidden)
            # 向前传播 得到更新后的输出值和隐藏状态

            output = output.squeeze()
            # 去除多余的维度

            optimizer.zero_grad()
            # 梯度清零 防止梯度累加

            loss = criterion(output, y)
            # 计算损失

            total_loss += loss.item()
            # 将损失累加 注意要加item取出标量值 否则就是一个张量

            loss.backward()
            # 反向传播

            optimizer.step()
            # 更新模型参数

            epoch_idx += 1

        message = 'Epoch:{}, Loss:{:.4f}, Time{:.2f}'.format(epoch+1, total_loss / epoch_idx, time.time() - start_time)
        file.write(message + '\n')
        print(message)
        # 将每一轮训练的信息打印并输出 方便发现训练时的问题

    file.close()
    torch.save(model.state_dict(), '../model/sentiment_model.pt')
    # 将日志文件关闭 并且保存训练好的模型


预测脚本1 predict.py

该预测脚本预测的是通过上面划分的测试集 进行准确率的预测

import pickle
import torch
from torch.utils.data import DataLoader
from utils.data_process import data_process
from utils.model import sentimentmodel

def predict():
    word_count, X_test, y_test = load()

    test_process = data_process(X_test, y_test)
    test_loader = DataLoader(test_process, batch_size=32, shuffle=False)
    model = sentimentmodel(word_count, 1)
    model.load_state_dict(torch.load('./model/sentiment_model.pt', map_location=torch.device('cpu')))
    # 加载训练好的模型

    model.eval()
    # 将模型设置为评估模式

    total_correct = 0
    total_count = 0

    with torch.no_grad():
    # 禁用梯度计算 在测试的时候没有必要使用后梯度

        for X, y in test_loader:
            hidden = model.init_hidden(batch_size=X.size(0))
            # 初始化隐藏状态

            outputs, _ = model(X, hidden)
            # 向前传播 且不需要使用隐藏状态 因为不需要向后传播

            outputs = outputs.squeeze()
            # 去除不必要的维度

            y_pred = (outputs >= 0.5).float()
            # 将输出的概率转化为二分类结果 (0或1)

            correct = (y_pred == y).sum().item()
            # 计算预测正确的数量

            total_correct += correct
            total_count += y.size(0)

    accuracy = total_correct / total_count
    print(f'Accuracy: {accuracy*100:.2f}%')
    # 计算并输出准确率

def load():
    with open('./preprocessed_data/word_count.pkl', 'rb') as f:
        word_count = pickle.load(f)
    with open('./preprocessed_data/X_test.pkl', 'rb') as f:
        X_test = pickle.load(f)
    with open('./preprocessed_data/y_test.pkl', 'rb') as f:
        y_test = pickle.load(f)

    return word_count, X_test, y_test

预测脚本2 predict_review.py

该预测脚本预测的是通过用户自行输入的评论

import pickle
import keras
import torch
from nltk import word_tokenize
from utils.data_loader import clean_text
from utils.model import sentimentmodel


def load_model():
    with open('./preprocessed_data/word_to_index.pkl','rb') as f:
        word_to_index = pickle.load(f)


    model = sentimentmodel(len(word_to_index),1)
    model.load_state_dict(torch.load('./model/sentiment_model.pt', map_location=torch.device('cpu')))
    model.eval()
    # 设置模型的嵌入词汇量和输出 并且加载模型 设定为评估模式

    return model, word_to_index

def predict_input(model, word_to_index, review):
    sentence = []
    # 用来储存被转换为索引后的句子

    """review = input("Please enter your review (or type 'exit' to quit): ")
    if review.lower() == 'exit':
        break"""
    # 如果不采用GUI界面输入的话 以上为输入方式

    text = word_tokenize(clean_text(review))
    # 将输入后的句子经过正则处理后分词

    for word in text:
        if word in word_to_index:
            sentence.append(word_to_index[word])
            # 如果这个词在词汇表里的话则添加

        else:
            sentence.append(0)
            # 否则置为0

    sentence = keras.preprocessing.sequence.pad_sequences([sentence], maxlen=100, padding='post')
    # 将这组索引裁剪到固定的100长度 不够的用0填充

    sentence = torch.tensor(sentence, dtype=torch.long).unsqueeze(0)
    # 将这组索引转化为扎改良 并且增加一个维度以匹配模型的输入格式

    hidden = model.init_hidden(1)
    output, _ = model(sentence, hidden)
    output = output.squeeze()
    # 训练模型 并删除多余的维度

    answer = (output>=0.5).float()
    # 将答案转为二分类结果

    if answer :
        print('This is a positive review.')
        return 'This is a positive review.'
    else:
        print('This is a negative review.')
        return 'This is a negative review.'
        # return是为了使GUI界面的弹窗接受到返回的字符串并显示
        # 如果不使用GUI的话则可以删除这两个return


"""def predict_review():
    model, word_to_index = load_model()
    predict_input(model, word_to_index)"""
    # 如果不使用GUI界面的话 则使用这函数进行调用即可

GUI界面 GUI.py

import tkinter as tk
from tkinter import messagebox
from utils.predict_review import *

def GUI():

    def click_submit():
    # 当用户点击Submit按钮时 调用此函数
        review = text.get("1.0", tk.END)
        # 获取用户输入的文本内容 从第一行第0个字符开始到结尾全部捕获

        result = predict_input(model, word_to_index, review)
        # 调用自己设定的那个predict_input获取结果

        messagebox.showinfo("Sentiment Result", result)
        # 将获取的结果显示在弹窗中


    model, word_to_index = load_model()
    # 加载模型和词汇表

    root = tk.Tk()
    root.title("IMDb Analysis")
    root.geometry("300x250+575+300")
    # 创建主窗口 并且设定title、大小和距离左上角的位置

    text_label = tk.Label(root, text="Input your review:")
    text_label.pack(pady=10)
    # 创建一个标签 提示的一段文本内容 父亲为上面设置的主窗口root
    # 并且设置其垂直边距为10个像素
    # 也可以直接在Label(root, text="Input your review:")后面
    # 添加.pack 但是不推荐 因为这样的话会使其返回'None' 这样以后就
    # 无法处理和调用这个部件了 所以像我这样分开处理是非常推荐的
    # 看着也工整

    text = tk.Text(root, width=50, height=10)
    text.pack(padx=20, pady=10)
    # 创建一个多行文本输入框 宽为50 高为10
    # 水平和垂直边距分别为20和10

    submit_button = tk.Button(root, text="Submit", command=click_submit)
    submit_button.pack(pady=10)
    # 创建一个按钮 当点击按钮时调用click_submit函数

    root.mainloop()
    # 启动Tkinter主事件循环 设定的这个窗口开始运行并等待交互

  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值