简介:本资源面向机器学习与自然语言处理方向的开发者及学生,提供一套完整的Twitter情感分析预测实战项目,覆盖从数据预处理到多模型对比的全流程。包内共23个文件,含20个Python源代码、2个CSV数据集及1个说明文档,压缩包约2.03MB,解压后数据规模达10MB,代码手工整理、无语法错误且可直接运行。项目涵盖文本清洗、分词、词干提取与词形还原、词云可视化、TF-IDF特征工程,并集成逻辑回归、SVM、KNN、决策树、随机森林、XGBoost、LightGBM、CatBoost等传统模型,以及LSTM、CNN、Transformer、LoRA微调等深度学习方案,还涉及Word2Vec、PCA与t-SNE降维分析。已有99人学习,适合希望系统掌握情感分析建模、模型评估与调参排错的读者参考实践。
1. 从一份 10 MB 的 Twitter 情感数据集说起:20 个脚本能跑出什么
如果你手头正好有一批推文语料,想快速验证「情感三分类到底能做到什么程度」,又不想从零搭预处理管线,这份资源值得先拆开看。它包含 20 个独立 Python 脚本和两个 CSV 数据文件,训练集与验证集合计约 10 MB,覆盖了从 TF-IDF 加逻辑回归的基线方案,到 LSTM、CNN 文本分类,再到 Gemma 7B 微调的完整梯度。换句话说,它不是单一模型的演示,而是一条从传统机器学习到深度学习的对照实验链。
适合谁用?如果你正在做社交媒体舆情监控的原型验证,或者需要一份能直接跑通的情感分析教学案例,这 20 个脚本省去了你反复调试分词、编码、padding 的时间。但要注意,脚本之间的依赖关系并不紧密,每个文件基本独立,你需要自己判断哪个方案匹配当前的数据规模和算力预算。接下来我会按「数据长什么样 → 传统方案怎么跑 → 深度学习怎么搭 → 大模型微调值不值得 → 坑在哪」的顺序,把这份资源拆成可复现的步骤。
2. 数据加载与文本预处理:两个 CSV 怎么喂进模型
2.1 先看清 twitter_training.csv 和 twitter_validation.csv 的结构
拿到数据第一步不是急着写模型,而是确认列名和标签分布。这两个文件通常没有表头,常见做法是用pd.read_csv时手动指定列名。我一般会先跑一段探查代码,确认文本列和标签列的位置,避免后面把标签当特征喂进去。
import pandas as pd # 常见做法:这两个文件没有表头,需要手动指定列名 # 列顺序通常是:id, entity, sentiment, text train_df = pd.read_csv('twitter_training.csv', header=None, names=['id', 'entity', 'sentiment', 'text']) val_df = pd.read_csv('twitter_validation.csv', header=None, names=['id', 'entity', 'sentiment', 'text']) print(train_df.shape, val_df.shape) print(train_df['sentiment'].value_counts()) print(train_df['text'].isnull().sum())这段代码做了三件事:加载两个 CSV 并赋予可读列名、查看标签类别分布、统计文本缺失值。参数上,header=None是关键,因为文件首行就是数据而非表头;names列表的顺序需要根据实际文件调整,如果列数不对,shape会直接报错。标签分布决定了后续要不要做类别加权,缺失值数量决定了要不要先 dropna。
2.2 用 NLTK 做清洗、去停用词和词干提取
推文文本的噪声比新闻语料大得多:URL、@提及、#话题标签、表情符号、重复字符。资源里多个脚本都用了re加nltk的组合,我一般会封装成一个可复用的清洗函数。
import re import nltk from nltk.corpus import stopwords from nltk.stem import PorterStemmer nltk.download('stopwords') stop_words = set(stopwords.words('english')) stemmer = PorterStemmer() def clean_tweet(text): text = str(text).lower() text = re.sub(r'http\S+|www\S+', '', text) # 去 URL text = re.sub(r'@\w+', '', text) # 去 @提及 text = re.sub(r'#', '', text) # 保留话题词,去掉 # text = re.sub(r'[^a-z\s]', '', text) # 只留字母和空格 text = re.sub(r'\s+', ' ', text).strip() # 合并多余空格 tokens = text.split() tokens = [stemmer.stem(w) for w in tokens if w not in stop_words] return ' '.join(tokens) train_df['clean_text'] = train_df['text'].apply(clean_tweet) val_df['clean_text'] = val_df['text'].apply(clean_tweet)清洗顺序有讲究:先去 URL 和提及,再去特殊字符,最后做词干提取。如果把去特殊字符放在前面,@user123会变成user,反而丢失了「这是提及」的信号。PorterStemmer比WordNetLemmatizer快,但会把better变成better而非good,对情感分类影响不大。停用词表用 NLTK 默认的英文表即可,注意not、but这类词在情感任务里其实有信息量,可以考虑从停用词表里剔除。
2.3 标签编码与训练验证集的一致性检查
情感标签通常是positive、negative、neutral、irrelevant四类,但部分脚本只做三分类。用LabelEncoder时,必须保证验证集和训练集用同一套映射,否则预测结果对不上。
from sklearn.preprocessing import LabelEncoder le = LabelEncoder() train_df['label'] = le.fit_transform(train_df['sentiment']) # 关键:验证集用 transform 而不是 fit_transform val_df['label'] = le.transform(val_df['sentiment']) print(dict(zip(le.classes_, le.transform(le.classes_))))fit_transform在训练集上建立映射,transform在验证集上套用同一映射。如果验证集出现训练集没有的标签,transform会直接报错,这其实是好事——说明数据划分有问题,需要回头检查。常见翻车场景是验证集里混入了拼写不一致的标签,比如Positive和positive被当成两类。
3. 传统机器学习方案:从 TF-IDF 到集成模型的全家桶
3.1 TF-IDF 加逻辑回归:三行代码的基线到底靠不靠谱
资源里4-Simple sentiment analysis with XGBoost.py和11-twitter sentiment analysis sentiment polarity.py都走了 TF-IDF 路线。我一般先用逻辑回归跑一个基线,确认数据本身有没有可分性。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.metrics import classification_report pipeline = Pipeline([ ('tfidf', TfidfVectorizer(max_features=5000, ngram_range=(1, 2))), ('clf', LogisticRegression(max_iter=1000, C=1.0)) ]) pipeline.fit(train_df['clean_text'], train_df['label']) pred = pipeline.predict(val_df['clean_text']) print(classification_report(val_df['label'], pred))max_features=5000控制词表规模,推文短文本用 5000 到 10000 足够;ngram_range=(1,2)引入二元词组,能捕捉not good这类否定结构。C=1.0是正则化强度的倒数,值越小正则越强。如果验证集 F1 低于 0.6,说明文本清洗可能过度或者标签噪声太大,需要回头查数据。
3.2 XGBoost、LightGBM、CatBoost 在文本特征上的差异
资源里同时出现了xgboost、lightgbm、catboost三个梯度提升库,这在文本分类任务里不算常见,因为树模型对稀疏高维特征的捕捉能力不如线性模型。但脚本作者显然是想做对照实验。
| 模型 | 优势 | 在文本任务上的注意点 |
|---|---|---|
| XGBoost | 参数成熟,社区案例多 | 对 TF-IDF 稀疏矩阵需要转 dense,内存消耗大 |
| LightGBM | 训练速度快,支持类别特征 | 默认叶子数多,短文本容易过拟合 |
| CatBoost | 类别特征处理强 | 文本特征需要先转数值,优势不明显 |
我实测下来的感受是:如果特征只有 TF-IDF,逻辑回归和线性 SVM 往往比树模型更稳。树模型更适合在 TF-IDF 之外叠加元特征,比如文本长度、感叹号数量、大写字母比例。资源里18-twitter sentiment analysis eda random forest.py就是这种思路,先做 EDA 再喂随机森林。
3.3 朴素贝叶斯和 SVM 的适用边界
6-NLP Twitter With ML.py和2-NLP final project.py里用了MultinomialNB和LinearSVC。朴素贝叶斯假设特征独立,在短文本上反而因为参数少而不容易过拟合,适合数据量小于 5 万条的场景。LinearSVC的决策边界比逻辑回归更「硬」,对类别不平衡更敏感,如果 neutral 类占比过高,需要设class_weight='balanced'。
from sklearn.svm import LinearSVC from sklearn.naive_bayes import MultinomialNB # SVM 版本,注意 class_weight svm_pipe = Pipeline([ ('tfidf', TfidfVectorizer(max_features=8000)), ('clf', LinearSVC(class_weight='balanced', max_iter=2000)) ]) # 朴素贝叶斯版本,适合快速验证 nb_pipe = Pipeline([ ('tfidf', TfidfVectorizer(max_features=8000)), ('clf', MultinomialNB(alpha=0.5)) ])alpha=0.5是拉普拉斯平滑系数,值越大对低频词越宽容。class_weight='balanced'会自动按类别频率反比加权,缓解不平衡问题。这两个模型训练都在秒级,适合在调深度模型之前先摸清数据上限。
4. 深度学习方案:LSTM、CNN 与词向量的组合方式
4.1 Keras Tokenizer 加 padding:序列长度怎么定
1-Twitter Sentiment Analysis using LSTM.py和20-Sentiment Analysis with LSTM Model.py都用了tensorflow.keras.preprocessing.text.Tokenizer。这一步的核心参数是num_words和maxlen。
from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences MAX_WORDS = 10000 MAX_LEN = 50 tokenizer = Tokenizer(num_words=MAX_WORDS, oov_token='<OOV>') tokenizer.fit_on_texts(train_df['clean_text']) train_seq = tokenizer.texts_to_sequences(train_df['clean_text']) val_seq = tokenizer.texts_to_sequences(val_df['clean_text']) train_pad = pad_sequences(train_seq, maxlen=MAX_LEN, padding='post', truncating='post') val_pad = pad_sequences(val_seq, maxlen=MAX_LEN, padding='post', truncating='post')num_words=10000表示只保留频率最高的 1 万个词,其余映射为<OOV>。maxlen=50需要根据清洗后文本的长度分布来定,我一般会先画一个 token 数量直方图,取 95 分位数。padding='post'在末尾补零,truncating='post'从末尾截断,这样保留的是文本开头的信息,对情感分类更有利,因为情感词往往出现在前半句。
4.2 嵌入层加双向 LSTM 的模型结构
资源里13-DL Assignment 02 CNN for Text Classification.py用了 CNN,20号脚本用了 LSTM。我一般会先跑一个 Embedding 加 BiLSTM 的结构,作为深度模型的基线。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, Bidirectional, LSTM, Dense, Dropout model = Sequential([ Embedding(input_dim=MAX_WORDS, output_dim=128, input_length=MAX_LEN), Bidirectional(LSTM(64, return_sequences=False)), Dropout(0.5), Dense(64, activation='relu'), Dropout(0.3), Dense(4, activation='softmax') # 四分类 ]) model.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy']) model.summary()output_dim=128是嵌入维度,数据量小于 10 万条时 64 到 128 足够。Bidirectional(LSTM(64))会输出 128 维向量,因为前向和后向各 64 维。Dropout(0.5)放在 LSTM 之后,Dropout(0.3)放在全连接之后,两层 dropout 是为了对抗过拟合。损失函数用sparse_categorical_crossentropy是因为标签是整数编码,不需要 one-hot。
4.3 训练过程中的早停与学习率调整
深度模型最容易翻车的地方是过拟合和梯度爆炸。资源里部分脚本用了ModelCheckpoint,我一般还会加EarlyStopping和ReduceLROnPlateau。
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau callbacks = [ EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True), ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=2, min_lr=1e-6) ] history = model.fit(train_pad, train_df['label'], validation_data=(val_pad, val_df['label']), epochs=20, batch_size=64, callbacks=callbacks)patience=3表示验证损失连续 3 轮不下降就停止,restore_best_weights=True会回滚到最优轮次的权重。ReduceLROnPlateau在验证损失停滞时把学习率减半,min_lr=1e-6防止学习率降到零。batch_size=64是常见起点,显存不够就降到 32。
5. 大模型微调与避坑:Gemma 7B 和那些血泪教训
5.1 LoRA 微调 Gemma 7B 的最小可行配置
15-Finetuning Gemma 7B it for Sentiment Analysis.py用了peft和trl,这是目前微调大模型的主流组合。LoRA 的核心思想是冻结原模型权重,只训练低秩适配矩阵。
from peft import LoraConfig, get_peft_model from transformers import AutoModelForCausalLM, AutoTokenizer model_name = "google/gemma-7b-it" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto") lora_config = LoraConfig( r=8, # 低秩矩阵的秩 lora_alpha=16, # 缩放系数 target_modules=["q_proj", "v_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM" ) model = get_peft_model(model, lora_config) model.print_trainable_parameters()r=8是秩,值越大可训练参数越多,7B 模型上 8 到 16 是常见范围。lora_alpha=16通常设为r的两倍。target_modules指定适配哪些注意力矩阵,q_proj和v_proj是最小配置。print_trainable_parameters()会显示可训练参数占比,通常在 0.1% 到 1% 之间。
5.2 避坑:数据泄漏、标签噪声与显存溢出
现象一:验证集准确率异常高,但实际预测一塌糊涂。原因通常是清洗时用了全量数据拟合TfidfVectorizer或Tokenizer,导致验证集信息泄漏到训练过程。解决方法是先划分训练验证集,再在训练集上fit,验证集只做transform。
现象二:模型把所有样本预测为多数类。原因是类别不平衡且没有做加权。解决方法是设class_weight='balanced',或者在损失函数里用weight参数。资源里部分脚本没有处理这个问题,需要自己补上。
现象三:训练到一半显存溢出。原因是batch_size太大或序列长度maxlen设得过长。解决方法是把batch_size减半,或者用pad_sequences时把maxlen从 100 降到 50。如果还不行,考虑用梯度累积模拟大 batch。
现象四:微调后模型输出格式混乱。原因是SFTTrainer的formatting_func没有对齐指令模板。解决方法是检查tokenizer.apply_chat_template的输出,确保训练样本和推理时的 prompt 格式一致。
现象五:nltk.download在离线环境失败。原因是资源里脚本默认在线下载停用词和wordnet。解决方法是提前在有网环境下载好,把nltk_data目录打包,离线时设NLTK_DATA环境变量指向该目录。
5.3 传统模型和深度模型的结果对比方法
资源里 20 个脚本跑下来,你需要一个统一的对比框架,否则结果散落在各个文件里没法判断。我一般会建一个结果表,记录每个方案的准确率、F1、训练时间和显存占用。
| 方案 | 准确率区间 | 训练时间 | 适用场景 |
|---|---|---|---|
| TF-IDF + 逻辑回归 | 0.65-0.75 | 秒级 | 快速基线 |
| TF-IDF + LinearSVC | 0.68-0.78 | 秒级 | 类别平衡时 |
| BiLSTM + 嵌入 | 0.72-0.82 | 分钟级 | 中等数据量 |
| CNN 文本分类 | 0.70-0.80 | 分钟级 | 短文本 |
| Gemma 7B + LoRA | 0.80-0.90 | 小时级 | 算力充足 |
准确率区间是经验值,实际取决于数据清洗质量和标签一致性。如果 BiLSTM 跑不过 TF-IDF 加 SVM,优先检查序列长度和嵌入维度,而不是急着换模型。
6. 一个被低估的技巧:用 PCA 和 t-SNE 验证嵌入质量
大部分脚本跑完就结束了,但资源里9-Twitter Sentiment Analysis.py和10-Twitter Sentiment Analysis.py提到了PCA和TSNE。这个技巧我强烈建议加上,因为它能帮你判断嵌入空间到底有没有把不同情感分开。
from sklearn.decomposition import PCA from sklearn.manifold import TSNE import matplotlib.pyplot as plt # 假设 embeddings 是模型倒数第二层的输出 pca = PCA(n_components=50) reduced = pca.fit_transform(embeddings) tsne = TSNE(n_components=2, perplexity=30, random_state=42) vis = tsne.fit_transform(reduced) plt.figure(figsize=(10, 8)) scatter = plt.scatter(vis[:, 0], vis[:, 1], c=train_df['label'], cmap='tab10', alpha=0.6) plt.colorbar(scatter) plt.title('t-SNE of tweet embeddings') plt.show()先用 PCA 把维度降到 50,再用 t-SNE 降到 2 维,这是标准做法,因为 t-SNE 直接处理高维数据计算量太大。perplexity=30控制局部和全局结构的平衡,数据量小于 1000 时调到 5 到 10。如果可视化图上四类标签混在一起,说明嵌入没有学到区分性特征,需要回头检查清洗步骤或增加模型容量。
我自己的习惯是:每次跑完一个深度模型,都强制走一遍 PCA 加 t-SNE,确认嵌入空间是「分开的」而不是「糊成一团」。有一次 BiLSTM 的验证准确率到了 0.78,但 t-SNE 图上 positive 和 neutral 完全重叠,后来发现是清洗时把感叹号全删了,而感叹号恰恰是区分这两类的关键信号。从那以后我每次做文本清洗都会保留标点符号的计数特征,不再无脑re.sub(r'[^a-z\s]', '', text)。希望这个习惯也能帮到你。
本文还有配套的精品资源,点击获取