☰
IMDB情感分析源码实战:从数据加载到LSTM模型调优
2026/9/28 18:12:11 网站建设 项目流程

简介:这份资源是面向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', encoding='utf-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;第三,encoding='utf-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_count=5通常能把词表压到 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_size=0.2, random_state=42, stratify=df['sentiment'] ) # TF-IDF 向量化:最多保留 50000 个特征,忽略文档频率超过 0.5 的词 vectorizer = TfidfVectorizer( max_features=50000, ngram_range=(1, 2), # 同时用 unigram 和 bigram min_df=3, # 至少出现在 3 篇文档中 max_df=0.5, # 出现在超过 50% 文档中的词忽略 sublinear_tf=True # 用 1+log(tf) 代替原始 tf ) X_train_tfidf = vectorizer.fit_transform(X_train) X_test_tfidf = vectorizer.transform(X_test) # 逻辑回归:C 是正则化强度的倒数,越小正则化越强 clf = LogisticRegression(C=1.0, max_iter=1000, solver='liblinear') 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 数据噪声不小,C=1.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_words=MAX_NB_WORDS, filters='!"#$%&()*+,-./:;<=>?@[\\]^_`{|}~\t\n') tokenizer.fit_on_texts(df['review']) sequences = tokenizer.texts_to_sequences(df['review']) # 填充到固定长度:短的补 0,长的截断 X = pad_sequences(sequences, maxlen=MAX_SEQUENCE_LENGTH, padding='post', truncating='post') 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_size=0.2, random_state=42, stratify=y) # 搭建 LSTM 模型 model = Sequential([ Embedding(MAX_NB_WORDS, EMBEDDING_DIM, input_length=MAX_SEQUENCE_LENGTH), SpatialDropout1D(0.2), # 按维度丢弃整个嵌入向量,防止过拟合 LSTM(128, dropout=0.2, recurrent_dropout=0.2), Dense(64, activation='relu'), Dropout(0.3), Dense(1, activation='sigmoid') # 二分类输出 ]) model.compile( loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'] ) # 早停:验证集 loss 连续 3 轮不下降就停 early_stop = EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True) history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=10, batch_size=128, callbacks=[early_stop] )

参数说明:MAX_SEQUENCE_LENGTH=400是我根据 IMDB 影评长度分布定的,覆盖了大概 95% 的评论,再长收益很小。EMBEDDING_DIM=128是常用起点,词表大可以升到 256,词表小降到 64 也行。SpatialDropout1D和Dropout的区别在于前者按嵌入维度丢弃,后者按神经元丢弃,两个一起用能更有效地抑制过拟合。batch_size=128在 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_dim=100): """加载 GloVe 词向量,构建嵌入矩阵""" embeddings_index = {} with open(glove_path, 'r', encoding='utf-8') as f: for line in f: values = line.split() word = values[0] coefs = np.asarray(values[1:], dtype='float32') 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_length=MAX_SEQUENCE_LENGTH, trainable=False # 冻结嵌入层,只训练上层 ), LSTM(128, dropout=0.2, recurrent_dropout=0.2), Dense(64, activation='relu'), Dropout(0.3), Dense(1, activation='sigmoid') ])

trainable=False表示嵌入层不参与反向传播,这样训练更快,也能防止小数据上过拟合。如果你的数据量足够大(比如 10 万条以上),可以设成True做微调。GloVe 的 100d 版本文件大概 300MB 左右,下载后放在项目目录下就行。注意word_index的索引要和嵌入矩阵行号对齐,len(word_index) + 1是因为 Keras 的索引从 1 开始,0 留给 padding。

4. 避坑与排查:IMDB 情感分析源码跑不通的 5 个常见原因

4.1 现象:准确率卡在 50% 上下,模型像在瞎猜

原因:标签和文本对不上。最常见的情况是pos文件夹被标成了 0,neg标成了 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 can't decode byte

原因:IMDB 原始数据里有个别文件不是 UTF-8 编码,或者文件里混入了非文本内容。Windows 系统默认编码是 GBK,用open()不指定encoding就会报这个错。

解决:在open()里显式写encoding='utf-8',如果还报错就加errors='ignore'跳过无法解码的字符。更稳妥的做法是用chardet检测每个文件的编码,但那样太慢。我一般直接errors='ignore',因为 IMDB 里这种文件极少,忽略几个字符不影响整体效果。

4.3 现象:LSTM 训练时 loss 变成 NaN

原因:学习率太大,或者序列填充的 0 被当成了真实词索引。Keras 的Embedding层默认mask_zero=False,如果 padding 的 0 参与了 LSTM 计算,梯度爆炸就会导致 NaN。

解决:在Embedding层里加mask_zero=True,让 LSTM 自动忽略 padding 位置。同时把optimizer='adam'换成optimizer=Adam(learning_rate=1e-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 的版本对应关系很敏感,tensorflow==2.10对应keras==2.10,装成keras==2.11就可能报错。另外nltk的stopwords需要额外下载语料,不下载就会报LookupError。

解决:先看源码包里的requirements.txt有没有锁版本号,有就严格按它装。没有的话,用pip install tensorflow==2.10.0 keras==2.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_classes=5) # 转成 one-hot model = Sequential([ Embedding(MAX_NB_WORDS, EMBEDDING_DIM, input_length=MAX_SEQUENCE_LENGTH), LSTM(128, dropout=0.2, recurrent_dropout=0.2), Dense(64, activation='relu'), Dense(5, activation='softmax') # 5 分类输出 ]) model.compile(loss='categorical_crossentropy', optimizer='adam', 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, annot=True, fmt='d', cmap='Blues', 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目录别浪费,它是你最后的后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询