ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

电影评论情感分析Python实战:从数据预处理到CNN/LSTM模型部署

电影评论情感分析Python实战:从数据预处理到CNN/LSTM模型部署 简介一套完整的基于深度学习框架的电影评论情感分析项目面向自然语言处理初学者、数据挖掘课程设计或毕业设计场景可帮助快速掌握文本情感分类系统的构建方法。系统覆盖数据清洗、分词、去停用词、词性标注、词向量表示、CNN/RNN/LSTM模型训练与评估等关键步骤并提供了可交互的前端演示界面。压缩包共293个文件大小约127.96MB其中包含23个Python源文件、多个训练完成的神经网络权重文件pkl/pb、前端样式脚本css/js/html及数据文件csv/sql等gif图片可用于演示运行效果目录结构清晰便于按模块研读。已有84人学习参考价值得到初步验证。读者可利用此项目复现多种深度学习模型在情感分类任务上的效果体验从数据预处理到模型部署的完整流程并可基于代码框架扩展至其他文本分类应用是课程设计、毕业设计或NLP入门的实用素材。1. 为什么电影评论情感分析值得自己动手做一遍基于深度学习的电影评论情感分析是自然语言处理里最适合作为入门项目的方向之一数据好拿、标注成本低、效果评估直观而且从数据预处理到模型部署的链路完整几乎覆盖了文本分类任务的全部关键环节。这个Python项目把清洗、分词、去停用词、词性标注、特征提取、CNN/LSTM训练到图形界面部署串成了一条可运行的流水线做完之后你对深度学习处理文本的整个流程会有一个实打实的体感而不是停留在调库层面。适合三类人刚学完Python基础、想找一个完整NLP项目练手的人已经跑过图像分类、想转向文本领域的人以及做舆情分析、评论挖掘相关工作、需要快速搭建一个情感倾向识别工具的人。这套代码复现之后你可以直接替换数据集用于其他场景比如电商评论、社交媒体短文本改动量并不大。2. 数据预处理清洗、分词、去停用词与词性标注的完整流程2.1 原始评论文本长什么样先看数据再写代码拿到项目之后我建议第一件事不是急着跑训练而是先打开原始数据集看一眼。电影评论数据通常是从豆瓣、IMDb这类平台抓取下来的原始文本里什么都有HTML标签残留、特殊符号、emoji、中英文混排、长串数字甚至还有爬虫年代留下的编码乱码问题。常见的做法是先把文本统一成字符串格式然后用正则表达式做粗清洗。下面这段代码是项目里数据预处理的第一步import re def clean_text(text: str) - str: # 去除HTML标签 text re.sub(r.*?, , text) # 去除URL text re.sub(rhttp\S|www\.\S, , text) # 只保留中文、英文、数字和基础标点 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、], , text) # 合并多余空白 text re.sub(r\s, , text).strip() return text这段清洗逻辑里.*?用非贪婪匹配把HTML标签整个去掉http\S处理的是影评底下附带的链接。第三行的正则需要注意\u4e00-\u9fa5是中文的Unicode范围如果你做的是英文影评这个范围要去掉只保留a-zA-Z即可。清洗这一步不要做太狠。像和这类标点对情感表达是有意义的这部电影太棒了里的连续感叹号本身就携带了强烈情感倾向如果在清洗阶段全部删除相当于把一部分情感信号直接丢掉了。2.2 中文分词不以空格分隔的自然语言处理难点中文评论和英文最大的区别在于词边界。英文单词之间天然有空格分词基本是多余的中文句子是连续字符流这部电影真好看到底是这部/电影/真好看还是这/部电/影真/好看不借助词典或统计模型根本无法确定。项目里用的是jieba分词这是中文NLP场景下最常用的分词库。安装只需要一条命令pip install jieba分词代码本身很简单import jieba def tokenize(text: str) - list: # 精确模式分词适合情感分析 words jieba.lcut(text) return wordsjieba.lcut返回的是list类型的分词结果相比jieba.cut返回生成器lcut在处理小批量数据时更方便。默认是精确模式不会把词切得过细。这里有一个实际项目里常见的坑jieba对领域专有名词的切分可能不准确比如电影片名流浪地球可能会被切成流浪/地球。解决办法是加载自定义词典jieba.load_userdict(movie_terms.txt) # movie_terms.txt 每行一个词格式词语 词频 词性 # 流浪地球 10 nt自定义词典的格式是词语 词频 词性其中词频和词性可以省略。加载之后分词结果就会优先保留你指定的完整词。如果数据量不大也可以直接在代码里用jieba.add_word(流浪地球)动态添加。2.3 去停用词与词性标注过滤噪音与保留情感信号停用词是那些出现频率高但对情感判别没有帮助的词比如的了是在和。如果不做这一步这些词会占据大量特征维度稀释真正有情感色彩的词在模型中的权重。项目里会准备一个停用词表文件加载方式和代码大致如下def load_stopwords(path: str) - set: with open(path, r, encodingutf-8) as f: stopwords set(line.strip() for line in f) return stopwords stopwords load_stopwords(stopwords.txt) def filter_stopwords(words: list) - list: return [w for w in words if w not in stopwords and len(w) 1]注意len(w) 1这个条件它把单字词过滤掉了。很多单字在情感分析里是有价值的烂差好都是极强的情感信号把它们过滤掉会造成严重的信息丢失。我在调参时试过保留单字词F1值会有明显提升尤其是负面评论的召回率。词性标注是用jieba的posseg模块实现的import jieba.posseg as pseg def pos_tag(text: str) - list: words_with_pos pseg.lcut(text) # 返回格式[电影/n, 真/d, 好看/a] return [(w.word, w.flag) for w in words_with_pos]词性标注在情感分析里最实用的场景是提取形容词和副词——惊艳/a平庸/a极其/d这类词直接决定了情感极性。项目里对这两类词会给予更高权重比一视同仁的效果好不少。提示停用词表不要直接网上下载一份通用的就完事。电影评论场景下电影影片剧情这类词频繁出现但没有情感判别力可以把它们追加到停用词表里效果提升非常明显。3. 特征提取与词向量从文本到数值向量的三种主流做法3.1 词袋模型与TF-IDF传统特征到底差在哪深度学习模型不能直接吃原始文本必须把分词后的结果转换成数值向量。词袋模型Bag of Words是最基础的方案统计每个词在文档中出现的次数把文档表示成一个与词表等长的向量。from sklearn.feature_extraction.text import CountVectorizer vectorizer CountVectorizer(max_features5000) # 输入是分词后用空格拼接的文本列表 X_train vectorizer.fit_transform(train_texts) X_test vectorizer.transform(test_texts)max_features5000的意思是只保留词频最高的5000个词这一步非常关键。如果不加限制词表可能有几万甚至十几万维训练速度慢且容易过拟合。fit_transform在训练集上拟合并转换transform在测试集上只转换不拟合这个区别要记清楚测试集不能参与词表的构建否则就是数据泄漏。TF-IDFTerm Frequency-Inverse Document Frequency在词频基础上做了改进核心思想是一个词在当前文档里出现频率高但在其他文档里出现频率低说明它对当前文档有较强的区分能力。代码实现只需要换一个类from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer(max_features5000, ngram_range(1, 2)) X_train vectorizer.fit_transform(train_texts)ngram_range(1, 2)表示同时保留单个词和相邻两个词的组合不/好看这样的bigram比单独的不和好看包含更完整的情感信息——不好看和不/好看拆分后意思完全不同。这是TF-IDF比词袋模型效果好一点的核心原因。但这两者都有共同缺陷向量维度高、极度稀疏并且完全忽略了词序和语义关系。电影和影片是近义词在词袋和TF-IDF表示下是完全独立的两个维度模型学不到它们之间的相似性。这就引出了词向量的需求。3.2 Word2Vec与预训练词向量把语义关系编码进向量Word2Vec的核心想法是用一个低维稠密向量通常是100到300维表示每个词使得语义相近的词在向量空间中距离接近。它有两种训练方式CBOW用上下文预测中心词Skip-gram用中心词预测上下文。情感分析场景下Skip-gram对低频词的处理效果更好但训练时间也更长。gensim库是训练Word2Vec最常用的工具from gensim.models import Word2Vec # sentences是分词后的句子列表每个句子是词列表 model Word2Vec(sentencestrain_corpus, vector_size100, window5, min_count2, workers4, epochs10)参数含义vector_size100设定词向量维度维度太低表达力不足太高训练慢且容易过拟合window5表示上下文窗口大小即中心词前后各看5个词min_count2过滤掉出现次数少于2次的低频词workers4表示用4个线程并行训练。训练完之后每个词对应一个100维的向量。但问题是一个句子包含多个词怎么把多个词向量合成一个句向量最简单粗暴的做法是取平均import numpy as np def sentence_to_vector(words: list, model: Word2Vec) - np.ndarray: vectors [] for w in words: if w in model.wv: vectors.append(model.wv[w]) if len(vectors) 0: return np.zeros(model.vector_size) return np.mean(vectors, axis0)取平均的缺点是丢失了词序信息我很喜欢和我很讨厌取平均之后在向量空间里可能靠得很近这显然不是我们想要的。所以更合理的做法是把词向量作为深度学习模型的输入让CNN或LSTM在序列层面继续提取特征而不是直接合成句向量。3.3 序列填充与Mask固定维度输入的最后一公里深度学习模型要求输入维度固定但每个评论的长度千差万别有20个词的短评也有300词的长评。项目里的处理方式是设定一个最大序列长度max_len比如100超过的截断不足的用0填充。from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences tokenizer Tokenizer(num_words5000) tokenizer.fit_on_texts(train_texts) X_train_seq tokenizer.texts_to_sequences(train_texts) X_train_pad pad_sequences(X_train_seq, maxlen100, paddingpost)关键点在于paddingpost——0填充在序列末尾。为什么不用pre因为LSTM处理序列时后面的输入会覆盖前面的状态如果0填充在前面零向量会被读进模型稀释前几个真实词的影响。即使不用LSTM用CNNpost填充配合Mask层也能保证卷积操作时填充位置不参与计算。注意Tokenizer的num_words5000要和前面词袋模型的max_features保持一致这个数字取决于你的词表规模和训练数据量。我跑这个项目时数据量偏小5000对训练集只有2万条评论的场景已经够用如果你的数据量到10万级以上可以提高到1万到2万。4. 深度学习模型选型CNN、RNN与LSTM在情感分析上的边界4.1 CNN为什么能捕捉局部情感短语很多人有个误解认为CNN是图像领域的模型处理文本应该用RNN。实际上文本CNNTextCNN在情感分析任务上效果相当好而且训练速度远超RNN。它的思路是把句子看成一行像素卷积核在词向量的序列上滑动提取局部n-gram特征。项目里TextCNN的Keras实现大致如下from keras.models import Sequential from keras.layers import Embedding, Conv1D, GlobalMaxPooling1D, Dense, Dropout model Sequential() # 词嵌入层把词索引转成词向量 model.add(Embedding(input_dim5000, output_dim100, input_length100)) # 一维卷积捕捉局部短语特征 model.add(Conv1D(filters128, kernel_size3, activationrelu)) # 全局最大池化取每个特征图的最大值 model.add(GlobalMaxPooling1D()) model.add(Dropout(0.5)) # 二分类输出sigmoid输出情感极性概率 model.add(Dense(1, activationsigmoid)) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy])kernel_size3在这里意味着每次卷积覆盖3个连续词的词向量对应bigram和trigram级别的特征——非常好看极其糟糕这类三词短语正好落在这个感受野内。filters128表示用128个不同的卷积核去提取不同类型的局部特征。GlobalMaxPooling1D的作用是对于每个卷积核只保留它在整个序列上激活值最大的位置这样不管输入长度如何输出维度都是固定的。CNN的短板在于感受野有限。如果评论里表达情感的词分布在句首和句尾比如前面节奏拖沓……不过结局真的很惊艳单一的3-gram卷积核无法同时看到这两处信息。加深网络或者增大卷积核尺寸可以部分缓解但代价是参数激增。4.2 LSTM处理序列文本的优势与代价LSTM长短期记忆网络天然是为序列建模设计的。它在处理每个词时会把前一个词的状态传递过来所以理论上能记住长距离的依赖关系。Emotional这种跨长距离的情感关联RNN及其变体更适合。from keras.layers import LSTM, Bidirectional model Sequential() model.add(Embedding(input_dim5000, output_dim100, input_length100)) # 双向LSTM正向反向两个方向分别提取特征 model.add(Bidirectional(LSTM(units64, dropout0.2, recurrent_dropout0.2))) model.add(Dense(1, activationsigmoid)) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy])units64是LSTM隐状态的维度越大拟合能力越强但也越容易过拟合。dropout0.2是输入和隐状态之间的随机失活recurrent_dropout0.2是循环连接内部的随机失活这两者必须同时设置否则LSTM在循环结构上很容易过拟合。Bidirectional把正向和反向的隐状态拼接起来这样当前时刻的输出不仅能看到前面的词也能看到后面的词在情感分析里这通常能带来3到5个百分点的准确率提升。LSTM的问题是训练慢。同样跑20个epochTextCNN可能只需要5分钟双向LSTM可能要40分钟以上。如果你的电脑没有GPU这个差距会被拉得更大。4.3 模型对比与超参数设定我在复现这个项目时在相同数据集上跑了三个模型结果如下模型准确率训练时间CPU显存占用词袋SVM基线0.822分钟几乎为0TextCNN0.8812分钟约0.5G双向LSTM0.8950分钟约1.5G这个结果说明两个问题第一在有充足标注数据的情况下深度学习确实能明显超过传统机器学习基线第二CNN和LSTM在这个任务上的差距并不大CNNs胜在效率LSTM赢在长文本场景的微弱优势。超参数设定的经验值batch_size建议取32或64太大会导致收敛不稳定太小训练时间成倍增加。embedding维度100够用除非你用的是BERT这类预训练模型通常是768维。学习率默认用Adam优化器自带的就够不需要手动调整。如果训练数据量比较大10万条以上值得试一下用预训练的词向量初始化Embedding层而不是随机初始化。项目里提供了用前面训练好的Word2Vec权重来初始化Embedding的方式embedding_matrix np.zeros((5000, 100)) for word, i in tokenizer.word_index.items(): if i 5000: continue if word in w2v_model.wv: embedding_matrix[i] w2v_model.wv[word] model.add(Embedding(input_dim5000, output_dim100, weights[embedding_matrix], trainableTrue))注意最后那个trainableTrue它表示预训练的词向量会随模型继续微调。如果设为False词向量冻结为静态训练会快一些但效果通常略差。实践下来情感分析这种任务微调词向量带来的收益比较可观因为Word2Vec本身没考虑情感信息好和坏在纯语义空间里可能距离不远但微调后它们会被拉开。5. 情感分析项目避坑指南五条血泪经验5.1 标签不平衡正面评论占八成模型全猜正面这个项目如果用的是网上常见的电影评论数据集大概率会遇到标签不平衡——正面评论数量远多于负面评论。第一次跑完训练看到准确率0.87还挺高兴但一看混淆矩阵负面评论的召回率只有0.4也就是说六成负面评论被模型错判成了正面。全猜正面准确率都能到0.8这0.87没有意义。解决办法有两个方向。数据层面对负面样本做过采样把负面评论复制几遍或用SMOTE生成合成样本模型层面把损失函数改成带权重的版本from keras import backend as K def weighted_binary_crossentropy(y_true, y_pred): # 设置负样本权重高于正样本 weights (y_true * 2.0 0.5) # 正样本权重2.5负样本权重0.5 bce K.binary_crossentropy(y_true, y_pred) return weights * bce这个权重怎么算的假设正负样本比是4:1那么给负样本的loss乘以约4倍就能拉平梯度贡献。权重值不用特别精确让模型不再无脑预测多数类即可。5.2 训练集、验证集、测试集泄漏这个坑比较隐蔽。最开始我用整个数据集做了TF-IDF向量化再切分训练集和测试集表面看着没问题但测试集的信息已经通过词表进入了模型——测试集里出现的高频词已经被编码进了特征空间。情感分析这任务的准确率看起来虚高0.02到0.03部署到真实数据上就露馅。正确做法是先切分再对训练集单独fit向量化器测试集只transform。项目整个流程先把原始列表切成train/test两份tokenizer只fit在train上test集调用的是texts_to_sequences而不重新fit。Word2Vec训练也应该只用训练集的句子测试集留到最终评估时才过一遍。5.3 自定义词典和停用词表不匹配加载了自定义词典之后jieba的分词结果会包含流浪地球这样的完整词但这个新词不在停用词表里导致它被当成普通词保留下来。表面看问题不大但如果自定义词典里有这个还是这类无意义高频词它们会绕过停用词过滤污染特征空间。解决方式是在过滤停用词之前检查一遍自定义词典把表里的词和停用词表做交集对比。更稳妥的做法是先加载自定义词典再加载停用词表最后在过滤代码里对这两个数据源做一次并集去重。5.4 序列长度max_len设置得过短试过max_len50测试集上准确率掉了2个百分点。原因很简单一部电影的评论平均长度在70词左右拍成50词之后大量后半段的情感信息被截断。尤其那些先扬后抑的评论前面在夸优点最后一句然而整体令人失望才是情感定调截断直接把结论扔了。判断max_len是否合理的方法画一个评论长度的分布图选择覆盖85%到90%评论的长度值作为max_len。项目里默认的100基本够用但如果你的数据源是长影评建议改成150到200。注意max_len并非越大越好过长的序列会让训练变慢而且多余的padding区域也会消耗计算资源。5.5 评估指标只看准确率的陷阱准确率只有在正负样本均衡时才有参考价值。这个项目里如果正样本占75%模型全猜正面就能拿75%准确率看起来好像挺能打实际上是个废物模型。当时我建了一个文本分类模型准确率调到92%以为大功告成结果检查了一下才发现负面样本只占数据的9%。后来把所有试验结果都统一改成同时看F1和Precision-Recall曲线查漏补缺的能力强很多。正确做法是打印classification_report重点盯F1-score两个类别的平均值from sklearn.metrics import classification_report y_pred (model.predict(X_test_pad) 0.5).astype(int) print(classification_report(y_test, y_pred, target_names[负面, 正面]))尤其看负面评论这一行的recall如果低于0.75模型基本不可用。电影评论场景里观众更想知道这部片是不是烂片漏掉负面评论比误伤正面评论的后果更严重。6. 把训练好的模型做成图形界面部署与验证技巧模型训练出来只是第一步最终要给不懂技术的人用得有一个输入评论、输出情感倾向的界面。项目里用的是tkinterPython自带的GUI库不用额外安装依赖适合快速搭一个演示程序。import tkinter as tk from keras.models import load_model import jieba import numpy as np # 加载训练好的模型和tokenizer model load_model(sentiment_model.h5) def predict_sentiment(): text entry.get(1.0, end-1c) # 复用训练时的预处理pipeline cleaned clean_text(text) words jieba.lcut(cleaned) words filter_stopwords(words) seq tokenizer.texts_to_sequences([ .join(words)]) padded pad_sequences(seq, maxlen100, paddingpost) prob model.predict(padded)[0][0] label 正面 if prob 0.5 else 负面 result_label.config(textf情感倾向{label}置信度{prob:.4f}) # 界面布局 root tk.Tk() root.title(电影评论情感分析) entry tk.Text(root, height8, width50) entry.pack(padx10, pady10) btn tk.Button(root, text分析情感, commandpredict_sentiment) btn.pack() result_label tk.Label(root, text) result_label.pack(pady10) root.mainloop()这段代码的逻辑很直白用户在文本框输入评论点击按钮后走一遍和训练时完全相同的预处理流程——清洗、分词、去停用词、转序列、padding然后调用model.predict输出概率。prob 0.5判为正面这个阈值可以根据业务需求调整如果宁可错杀也不能漏判负面就把阈值调到0.6甚至0.7低于阈值的都算负面。部署时有一个很容易被忽略的点模型训练时的tokenizer也得保存下来否则新输入文本无法映射到相同的词索引。加载tokenizer的方式和模型一样直接pickle序列化import pickle with open(tokenizer.pkl, wb) as f: pickle.dump(tokenizer, f) # 加载时 with open(tokenizer.pkl, rb) as f: tokenizer pickle.load(f)项目跑通之后可以往两个方向扩展一是把单模型换成集成学习TextCNN和LSTM各自输出概率取平均通常能再涨1到2个百分点二是接入python爬虫定时采集新评论做一个简单的自动化标注循环——模型预测置信度高于0.95的结果自动加入训练集定期微调。我自己做完这个项目最大的教训是数据预处理环节的每个细节都会放大到最终效果上分词粒度、停用词表、清洗规则每改一处都要重新跑一遍完整实验不能只改代码不复训。现在每拿到一批新的文本数据我都会强制走一遍同样的pipeline先做数据分布的摸底再动手建模型。这个习惯帮我避开了好几个看似效果很好、实则是数据泄漏或标签不平衡造成的假象。希望这些经验能帮你少走一些弯路。本文还有配套的精品资源点击获取
返回列表