IMDB情感分析源码实战:从数据加载到LSTM模型调优 简介这份资源是面向Python初学者与高校学生的情感分析实战项目源码包围绕IMDB电影评论数据集展开可用于毕业设计、期末大作业或个人练手。项目完整实现了从文本清洗到模型测试的全流程先对评论分词清洗再借助word2vec生成单词特征向量将段落切分为句子列表后计算平均特征向量最终对所有评论向量取平均并采用RandomForest完成测试数据分类帮助读者理解词向量与集成学习在NLP任务中的落地方式。压缩包共9个文件以8个py源码和1个md说明文档为主整体约9KB体积轻量、结构清晰便于快速阅读与二次修改。目前已有497人学习下载说明该方案在同类作业中具有一定参考价值。源码经过严格调试可直接运行配套说明文档有助于梳理各模块职责与调用关系适合希望掌握文本预处理、特征工程与情感分类完整链路的读者参考借鉴。1. 一份 IMDB 情感分析源码包到底能帮你省掉哪几步弯路如果你手上正躺着一份python实现基于IMDB电影评论数据进行情感分析源码说明.zip大概率你面对的不是“要不要学情感分析”而是“怎么在最短时间内把它跑通、看懂、改成自己的东西”。IMDB 电影评论数据集是情感分析领域最经典的二分类基准之一5 万条标注好评/差评的英文影评天然适合拿来验证从词袋模型到 Transformer 微调的完整链路。这份源码包的价值不在于代码有多复杂而在于它把数据加载、文本清洗、特征工程、模型训练、评估指标这几块串成了一条可复现的流水线。适合谁刚配好 vscode python 环境、想找一个完整项目练手的入门者也适合已经会写 python 代码、但没系统做过 NLP 分类任务的开发者。下面我按“先跑通再拆解”的思路把这条链路拆成能直接抄的步骤。2. 先把环境跑通IMDB 数据加载与文本预处理的完整链路2.1 为什么 IMDB 数据集适合做情感分析的第一站IMDB 数据集有两个版本一个是 Keras/TensorFlow 内置的imdb.load_data()返回的是已经编码成整数序列的评论另一个是原始文本版本通常以aclImdb文件夹形式存在包含train/pos、train/neg、test/pos、test/neg四个子目录每个目录下是若干.txt文件。源码包里如果用的是原始文本版本说明作者想让你看到从零清洗的过程如果用的是 Keras 内置版本那重点就在模型结构而不是预处理。我一般建议先用原始文本版本跑一遍因为你能亲眼看到“一条影评长什么样、脏数据长什么样”。IMDB 影评的典型特点是长度差异极大短的几十个词长的上千词包含大量 HTML 标签比如br /有口语化表达和拼写错误。这些特点决定了预处理不能只做简单的lower()和split()。提示如果你用的是 Keras 内置版本load_data()默认只保留出现频率最高的 10000 个词低频词会被替换成oov_char。这个参数直接影响后续词表大小和嵌入层维度别忽略。2.2 用 pandas 加载原始 IMDB 文本并做第一轮清洗假设源码包里的数据目录结构是aclImdb/train/pos、aclImdb/train/neg这种形式下面这段代码可以直接抄。它的作用是遍历所有.txt文件把文本和标签读进 DataFrame同时做基础清洗。import os import re import pandas as pd def load_imdb_data(data_dir): 从 aclImdb 目录加载原始影评文本 data_dir: 包含 train/test 子目录的根路径 返回: 包含 review 和 sentiment 两列的 DataFrame data [] for split in [train, test]: for label in [pos, neg]: folder os.path.join(data_dir, split, label) if not os.path.exists(folder): continue for fname in os.listdir(folder): if not fname.endswith(.txt): continue with open(os.path.join(folder, fname), r, encodingutf-8) as f: text f.read() # 去掉 HTML 标签比如 br / 和 a href... text re.sub(r[^], , text) # 把连续空白字符合并成一个空格 text re.sub(r\s, , text).strip() data.append({ review: text, sentiment: 1 if label pos else 0 }) df pd.DataFrame(data) print(f加载完成共 {len(df)} 条评论正样本 {df[sentiment].sum()} 条) return df df load_imdb_data(aclImdb) print(df.head(3))这段代码的关键点有三个第一re.sub(r[^], , text)把 HTML 标签替换成空格而不是直接删除避免把相邻单词粘在一起第二标签用 1 和 0 表示方便后续直接喂给 sklearn 或 PyTorch第三encodingutf-8必须显式指定IMDB 原始数据里有一些非 ASCII 字符不指定编码在 Windows 上容易报UnicodeDecodeError。参数方面data_dir要指向包含train和test两个子目录的父目录。如果你的目录结构是aclImdb/train/pos这种传aclImdb就对了。如果源码包里已经把数据转成了 CSV那这一步可以跳过直接pd.read_csv()。2.3 分词、去停用词和词表构建的取舍清洗完文本后下一步是分词。英文分词比中文简单但也不是split()就完事。我一般用nltk或spaCy但如果你不想装额外依赖用正则re.findall(r\b[a-z]\b, text.lower())也能凑合。区别在于nltk能处理缩写和标点spaCy还能做词形还原lemmatization把running变成run、better变成good。import re from collections import Counter def tokenize(text): 简单英文分词转小写只保留字母按空白切分 text text.lower() tokens re.findall(r\b[a-z]\b, text) return tokens # 统计词频构建词表 all_tokens [] for review in df[review]: all_tokens.extend(tokenize(review)) word_counts Counter(all_tokens) # 只保留出现次数 5 的词过滤低频噪声 vocab {word: idx 1 for idx, (word, cnt) in enumerate(word_counts.most_common()) if cnt 5} print(f原始词表大小: {len(word_counts)}过滤后: {len(vocab)})这里有个血泪经验词表大小直接决定嵌入层参数量。IMDB 原始词表大概有 10 万 个不同词如果全保留嵌入层就是 10 万 × 128 维参数量上千万小显存机器直接翻车。我一般会把min_count设在 3 到 10 之间具体看数据量。5 万条评论用min_count5通常能把词表压到 2 万到 3 万效果和全量词表差不了多少。停用词要不要去我的建议是做传统机器学习比如 TF-IDF 逻辑回归时去掉因为停用词会稀释特征权重做深度学习比如 LSTM 或 BERT时保留因为模型自己能学到哪些词不重要。源码包里如果用了nltk.corpus.stopwords你可以先按它的来跑通后再对比去掉停用词前后的 F1 变化。3. 从词袋到 LSTM三种情感分析模型的实现与参数调优3.1 用 TF-IDF 逻辑回归搭一个 5 分钟能跑完的基线不管后面用什么深度模型我都会先跑一个 TF-IDF 逻辑回归的基线。原因很简单如果基线能到 88% 准确率你后面上 BERT 只提升到 92%那你要想清楚这 4 个点值不值得多花几十倍算力。而且基线跑得快能帮你快速验证数据加载和预处理有没有 bug。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, accuracy_score # 划分训练集和测试集stratify 保证正负样本比例一致 X_train, X_test, y_train, y_test train_test_split( df[review], df[sentiment], test_size0.2, random_state42, stratifydf[sentiment] ) # TF-IDF 向量化最多保留 50000 个特征忽略文档频率超过 0.5 的词 vectorizer TfidfVectorizer( max_features50000, ngram_range(1, 2), # 同时用 unigram 和 bigram min_df3, # 至少出现在 3 篇文档中 max_df0.5, # 出现在超过 50% 文档中的词忽略 sublinear_tfTrue # 用 1log(tf) 代替原始 tf ) X_train_tfidf vectorizer.fit_transform(X_train) X_test_tfidf vectorizer.transform(X_test) # 逻辑回归C 是正则化强度的倒数越小正则化越强 clf LogisticRegression(C1.0, max_iter1000, solverliblinear) clf.fit(X_train_tfidf, y_train) y_pred clf.predict(X_test_tfidf) print(f准确率: {accuracy_score(y_test, y_pred):.4f}) print(classification_report(y_test, y_pred, target_names[负面, 正面]))这段代码里最值得调的是ngram_range和C。ngram_range(1,2)意味着模型不仅看单个词还看相邻两个词的组合比如not good这种否定短语就能被捕捉到。但 bigram 会让特征数暴增所以max_features要设个上限。C参数控制正则化IMDB 数据噪声不小C1.0通常够用如果过拟合就降到 0.1如果欠拟合就升到 10。我实测下来这个基线在 IMDB 测试集上大概能到 88% 到 89% 的准确率。如果源码包里的深度模型跑出来低于这个数先别怀疑模型结构回头检查数据预处理是不是把标签搞反了。3.2 用 Keras 搭 LSTM 模型嵌入层、序列填充和超参数设置深度模型部分源码包大概率用的是 Keras/TensorFlow 或 PyTorch。这里以 Keras 为例因为它的 API 对新手更友好。核心结构是嵌入层 → LSTM 层 → 全连接层 → sigmoid 输出。import numpy as np from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout, SpatialDropout1D from tensorflow.keras.callbacks import EarlyStopping # 用 Keras Tokenizer 重新构建词表num_words 限制词表大小 MAX_NB_WORDS 50000 MAX_SEQUENCE_LENGTH 400 EMBEDDING_DIM 128 tokenizer Tokenizer(num_wordsMAX_NB_WORDS, filters!#$%()*,-./:;?[\\]^_{|}~\t\n) tokenizer.fit_on_texts(df[review]) sequences tokenizer.texts_to_sequences(df[review]) # 填充到固定长度短的补 0长的截断 X pad_sequences(sequences, maxlenMAX_SEQUENCE_LENGTH, paddingpost, truncatingpost) y df[sentiment].values # 划分训练集和验证集 from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, random_state42, stratifyy) # 搭建 LSTM 模型 model Sequential([ Embedding(MAX_NB_WORDS, EMBEDDING_DIM, input_lengthMAX_SEQUENCE_LENGTH), SpatialDropout1D(0.2), # 按维度丢弃整个嵌入向量防止过拟合 LSTM(128, dropout0.2, recurrent_dropout0.2), Dense(64, activationrelu), Dropout(0.3), Dense(1, activationsigmoid) # 二分类输出 ]) model.compile( lossbinary_crossentropy, optimizeradam, metrics[accuracy] ) # 早停验证集 loss 连续 3 轮不下降就停 early_stop EarlyStopping(monitorval_loss, patience3, restore_best_weightsTrue) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs10, batch_size128, callbacks[early_stop] )参数说明MAX_SEQUENCE_LENGTH400是我根据 IMDB 影评长度分布定的覆盖了大概 95% 的评论再长收益很小。EMBEDDING_DIM128是常用起点词表大可以升到 256词表小降到 64 也行。SpatialDropout1D和Dropout的区别在于前者按嵌入维度丢弃后者按神经元丢弃两个一起用能更有效地抑制过拟合。batch_size128在 8GB 显存上跑 128 维嵌入和 128 单元 LSTM 没问题如果显存不够就降到 64。训练完成后用model.evaluate(X_val, y_val)看验证集准确率。LSTM 在这个任务上通常能到 90% 左右比 TF-IDF 基线高 1 到 2 个点。如果低于 88%检查pad_sequences的padding和truncating参数是不是都是post混用会导致序列对齐错位。3.3 用预训练词向量提升小样本下的泛化能力如果你手上标注数据不多或者想进一步提升效果可以用 GloVe 或 Word2Vec 预训练词向量初始化嵌入层。IMDB 数据集本身有 5 万条不算小但预训练词向量依然能带来 1 到 2 个点的提升因为它引入了外部语料的知识。# 假设你已经下载了 glove.6B.100d.txt def load_glove_embeddings(glove_path, word_index, embedding_dim100): 加载 GloVe 词向量构建嵌入矩阵 embeddings_index {} with open(glove_path, r, encodingutf-8) as f: for line in f: values line.split() word values[0] coefs np.asarray(values[1:], dtypefloat32) embeddings_index[word] coefs # 构建嵌入矩阵第 0 行留给 padding embedding_matrix np.zeros((len(word_index) 1, embedding_dim)) for word, i in word_index.items(): embedding_vector embeddings_index.get(word) if embedding_vector is not None: embedding_matrix[i] embedding_vector return embedding_matrix # 注意这里 word_index 要和 Tokenizer 的 word_index 对应 embedding_matrix load_glove_embeddings(glove.6B.100d.txt, tokenizer.word_index, 100) # 在模型里用预训练权重初始化 Embedding 层并冻结不训练 model Sequential([ Embedding( len(tokenizer.word_index) 1, 100, weights[embedding_matrix], input_lengthMAX_SEQUENCE_LENGTH, trainableFalse # 冻结嵌入层只训练上层 ), LSTM(128, dropout0.2, recurrent_dropout0.2), Dense(64, activationrelu), Dropout(0.3), Dense(1, activationsigmoid) ])trainableFalse表示嵌入层不参与反向传播这样训练更快也能防止小数据上过拟合。如果你的数据量足够大比如 10 万条以上可以设成True做微调。GloVe 的 100d 版本文件大概 300MB 左右下载后放在项目目录下就行。注意word_index的索引要和嵌入矩阵行号对齐len(word_index) 1是因为 Keras 的索引从 1 开始0 留给 padding。4. 避坑与排查IMDB 情感分析源码跑不通的 5 个常见原因4.1 现象准确率卡在 50% 上下模型像在瞎猜原因标签和文本对不上。最常见的情况是pos文件夹被标成了 0neg标成了 1或者 DataFrame 的列顺序在后续处理中被交换了。另一个可能是train_test_split的stratify参数没设导致某一类样本在训练集里几乎不存在。解决在加载数据后立刻打印df[sentiment].value_counts()确认正负样本各占一半。然后在训练前打印y_train[:20]和对应的X_train[:20]的前几个词人工核对一条正面评论的标签是不是 1。如果用的是 Keras 内置load_data()注意它返回的y_train里 0 是负面、1 是正面和原始文件夹的neg/pos顺序一致。4.2 现象UnicodeDecodeError: utf-8 codec cant decode byte原因IMDB 原始数据里有个别文件不是 UTF-8 编码或者文件里混入了非文本内容。Windows 系统默认编码是 GBK用open()不指定encoding就会报这个错。解决在open()里显式写encodingutf-8如果还报错就加errorsignore跳过无法解码的字符。更稳妥的做法是用chardet检测每个文件的编码但那样太慢。我一般直接errorsignore因为 IMDB 里这种文件极少忽略几个字符不影响整体效果。4.3 现象LSTM 训练时 loss 变成 NaN原因学习率太大或者序列填充的 0 被当成了真实词索引。Keras 的Embedding层默认mask_zeroFalse如果 padding 的 0 参与了 LSTM 计算梯度爆炸就会导致 NaN。解决在Embedding层里加mask_zeroTrue让 LSTM 自动忽略 padding 位置。同时把optimizeradam换成optimizerAdam(learning_rate1e-3)或更低观察前几个 batch 的 loss 是否稳定下降。如果还是 NaN检查输入序列里有没有超出词表范围的索引Tokenizer的num_words和Embedding的input_dim要一致。4.4 现象验证集准确率比训练集低 10 个点以上原因过拟合。IMDB 影评里有很多长尾表达模型记住了训练集中的特定短语换到验证集就失效。LSTM 的参数量不小5 万条数据如果不加正则很容易过拟合。解决先加Dropout和SpatialDropout1D再把 LSTM 的recurrent_dropout打开。如果还不行减小 LSTM 单元数从 128 降到 64或者用EarlyStopping在验证集 loss 上升时提前停止。另一个容易被忽略的点是MAX_SEQUENCE_LENGTH设得太大比如设成 1000模型在 padding 上浪费了大量容量降到 400 通常更好。4.5 现象源码包里的requirements.txt装完还是报ModuleNotFoundError原因版本不兼容。TensorFlow 2.x 和 Keras 的版本对应关系很敏感tensorflow2.10对应keras2.10装成keras2.11就可能报错。另外nltk的stopwords需要额外下载语料不下载就会报LookupError。解决先看源码包里的requirements.txt有没有锁版本号有就严格按它装。没有的话用pip install tensorflow2.10.0 keras2.10.0这种明确版本。nltk的问题在代码里加一行nltk.download(stopwords)和nltk.download(punkt)就能解决。如果用的是spaCy还需要python -m spacy download en_core_web_sm。5. 把源码改成自己的东西从 IMDB 迁移到中文影评或电商评论5.1 迁移时最先要改的三个地方IMDB 是英文二分类如果你要迁移到中文影评或电商评论核心改动集中在三处分词器、词表构建、标签映射。中文没有空格分隔re.findall(r\b[a-z]\b)直接失效得换成jieba或pkuseg。词表构建也要从按空格切分改成按分词结果统计。标签映射则取决于你的数据是二分类好评/差评还是多分类1 到 5 星多分类要把最后一层的sigmoid换成softmax损失函数从binary_crossentropy换成categorical_crossentropy。import jieba def tokenize_chinese(text): 中文分词用 jieba 精确模式过滤掉单字和标点 words jieba.lcut(text) # 过滤掉长度小于 2 的词和纯标点 return [w for w in words if len(w) 2 and not re.match(r^[\W_]$, w)] # 多分类示例假设标签是 1 到 5 星 from tensorflow.keras.utils import to_categorical y_multi to_categorical(df[sentiment] - 1, num_classes5) # 转成 one-hot model Sequential([ Embedding(MAX_NB_WORDS, EMBEDDING_DIM, input_lengthMAX_SEQUENCE_LENGTH), LSTM(128, dropout0.2, recurrent_dropout0.2), Dense(64, activationrelu), Dense(5, activationsoftmax) # 5 分类输出 ]) model.compile(losscategorical_crossentropy, optimizeradam, metrics[accuracy])中文分词的坑在于jieba默认词典对影评里的网络用语和新词覆盖不好比如“绝绝子”“yyds”可能被切成单字。解决办法是加载自定义词典把领域-specific 的词加进去。另外中文停用词表比如哈工大停用词表比英文更重要因为中文里的“的”“了”“是”出现频率极高不去掉会严重稀释特征。5.2 用混淆矩阵和错误样本定位模型弱点跑通模型只是第一步真正有价值的是知道模型在哪里犯错。IMDB 二分类任务里我习惯在验证集上画混淆矩阵然后手动看被分错的样本。常见错误模式有两种一种是包含否定词的评论比如 “not bad at all” 被分成负面另一种是混合情感的评论比如 “great acting but boring plot” 被分成正面或负面都有道理。from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt y_pred_prob model.predict(X_val) y_pred (y_pred_prob 0.5).astype(int).flatten() cm confusion_matrix(y_val, y_pred) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabels[负面, 正面], yticklabels[负面, 正面]) plt.xlabel(预测) plt.ylabel(真实) plt.show() # 找出分错的样本 misclassified np.where(y_pred ! y_val)[0] for idx in misclassified[:5]: print(f真实: {y_val[idx]}, 预测: {y_pred[idx]}) print(f文本: {df.iloc[idx][review][:200]}...) print(---)看错误样本时重点关注两类一类是模型置信度很高但分错的y_pred_prob接近 0 或 1这类通常是数据标注有问题或者文本里有反讽另一类是置信度在 0.4 到 0.6 之间的这类是模型真正拿不准的可以考虑加更多训练数据或者引入注意力机制让模型关注关键短语。5.3 一个我常用的验证习惯用留出集做最终检查不管中间怎么调参我都会在最后留一个完全没参与过训练和验证的测试集只跑一次。IMDB 原始数据自带test文件夹但很多人图省事直接用train_test_split把训练集切了又切导致测试集信息泄露。我的习惯是训练集用来训练验证集用来调参和早停原始test文件夹留到最后跑一次看准确率和 F1 是否和验证集接近。如果差太多说明调参过拟合了验证集得回头简化模型。这个习惯帮我避免过好几次“验证集 92%、真实测试 85%”的翻车。源码包里的test目录别浪费它是你最后的后悔药。希望帮到你。本文还有配套的精品资源点击获取