构建可复现的社交媒体情感分析基准:从TF-IDF到大模型微调
2026/9/16 2:01:58 网站建设 项目流程

简介:面向社交媒体情感分析预测的AI实战数据集,适合具备Python与机器学习基础的学习者、课程设计或项目实践人群,可覆盖从数据清洗、探索性分析到多模型训练预测的完整流程。资源包共21个文件,含19个Python源代码、1个csv情感数据集与1个readme说明,压缩包约95KB,结构轻量但代码量大,且代码整理规范、无语法错误,可放心运行。源码使用pandas、nltk、Word2Vec、TF-IDF、逻辑回归、随机森林、SVM、XGBoost、LSTM,并引入Gemma、LLama 3微调等前沿方法,支持快速运行复现;数据文件为社交文本情感标注集,可用于EDA、情感极性与情绪分类实验。当前已有99人学习,可作为综合实践与算法对比的参考样例。

1. AI实战:把社交媒体情感分析跑成可复现的基准测试

解压这个压缩包,你会看到19个.py文件和一个sentimentdataset.csv,全部加起来不到300KB,却把社交媒体情感分析从数据清洗到LLM微调拉通了一条完整链路。这里的AI实战不是单点算法演示,而是能用同一份数据做横向对比的实验场:EDA脚本负责摸底,逻辑回归、SVM、XGBoost、LSTM负责常规建模,Llama 3和Gemma的LoRA脚本则用来验证生成式大模型在短文本情感任务上的性价比。CSV只有166KB,CPU机器就能跑通大部分脚本,不一定要先上GPU;适合算法新人快速建立分类基线,也适合老手拿来对比传统特征与深度学习、大模型之间的差距。下面按我重跑的顺序,先处理数据,再跑轻量模型,最后看深度学习和LLM脚本。

2. EDA与文本预处理:把 sentimentdataset.csv 清洗成干净语料

包里的10-Social Media Sentiments Analysis EDA.py13-Sentiment_EDA_analysis_MandeepBaluja.py19-Social media EDA cleaning data preparation.py都在做同一件事:先搞清数据长什么样,再把它变成模型能直接吃的样子。这一步不是走过场,后面所有模型的表现上限都取决于这里的决定。

2.1 读入数据与质量检查:先列字段,再谈建模

拿到数据集后别急着跑model.fit,用pandas读一下,几秒内就能发现大部分问题。以sentimentdataset.csv为例,打开后通常包含textsentimenttimestampplatform等字段,情感标签以正、负、中性为主。先看缺失值和重复值:

import pandas as pd df = pd.read_csv('sentimentdataset.csv') print(df.shape) print(df.info()) print(df.isnull().sum()) print(df.duplicated().sum())

info()能直接看到每列非空数量和类型,缺失值如果只集中在个别列,可以选择dropna(),但不要直接删整行,先看缺失文本是否还有分析价值。重复值方面,整行重复说明采集端有重复写入,直接去重;只有text列重复而时间戳不同,说明是同一句话被多次抓取,这种情况我会保留第一次出现,避免同一个文本既在训练集又在测试集:

df = df.drop_duplicates(subset=['text'], keep='first').reset_index(drop=True) df['sentiment'] = df['sentiment'].str.strip().str.lower()

注意这里按text去重是因为情感标签来自文本内容,时间戳只是附带信息。把标签统一成小写是防止“Positive”和“positive”被当作两类。如果数据量很大,可以先把文本长度分桶统计一下,短文本和长文本在后续LSTM里的max_len选择完全不同。

2.2 文本预处理:清理链接、数字、停用词和词形还原

情感分析里最常被忽略的是URL、@用户和标点符号,这些标记会让Tfidf产生大量低频特征,也会干扰词云生成。常见做法是先用正则把它们替换成空格,再做停用词过滤和词形还原。下面是一套可直接复制的清洗函数:

import re import nltk from nltk.corpus import stopwords from nltk.tokenize import word_tokenize from nltk.stem import WordNetLemmatizer nltk.download('stopwords') nltk.download('punkt') nltk.download('wordnet') stop_words = set(stopwords.words('english')) lemmatizer = WordNetLemmatizer() def clean_text(text: str) -> str: text = re.sub(r'http\S+|www\.\S+', ' ', text) text = re.sub(r'@\w+|#\w+', ' ', text) text = re.sub(r'\d+', ' ', text) text = re.sub(r'[^\w\s]', ' ', text) return text.lower().strip() def prepare_text(text: str) -> str: tokens = word_tokenize(clean_text(text)) tokens = [t for t in tokens if t not in stop_words] tokens = [lemmatizer.lemmatize(t) for t in tokens] return ' '.join(tokens) df['text_clean'] = df['text'].apply(prepare_text)

这段代码包含三层含义:正则部分按顺序去掉URL、@/微博符号、数字、标点;stopwords过滤掉theis这类高频虚词;WordNetLemmatizerrunning还原成run,压缩词表。参数上,如果你处理的是中文内容,需要换成jieba和中文停用词表;如果是英文但偏网络用语,PorterStemmer可能比Lemmatizer更快,但对不规则词形不如后者稳定。166KB的数据量在普通笔记本上几秒就能跑完,如果换成百万级语料,可以把prepare_text改成批量处理或先用子词切分。

下列清洗操作是我在这个包里固定的标准步骤,不同数据集中可按需裁剪:

清洗操作正则/方式说明
去URLhttp\S+链接对情感判别基本无效,还占用特征
去@/#@\w+,#\w+保留hashtag里的词,但符号本身没意义
去数字\d+社交文本里数字多为年龄、数量,情感信息少
去标点[^\w\s]避免把“good!”和“good”拆成两个词
停用词nltk.corpus.stopwords滤掉the, a, is等高频无意义词
词形还原WordNetLemmatizer减少词形变化,压缩词典大小

2.3 看情感分布与词云:提前识别类别偏斜

清洗完先做一次最原始的标签统计,这决定后面选accuracy还是macro-F1。代码很直接:

from collections import Counter from wordcloud import WordCloud import matplotlib.pyplot as plt print(Counter(df['sentiment'])) all_text = ' '.join(df['text_clean']) wordcloud = WordCloud(width=800, height=400, background_color='white').generate(all_text) plt.imshow(wordcloud, interpolation='bilinear') plt.axis('off') plt.show()

Counter给出每个情感类别的样本数。如果最常见的类别占到70%以上,后续逻辑回归的准确率可能虚高到0.7以上,但模型真正分类能力很弱,所以必须把类别比例写在实验记录里。词云只用于快速检查预处理是否保留了关键词:如果出现lovehategood等判别性词,说明清洗没有误伤;如果全是daytoday这类词,需要复查停用词表和正则顺序。此外,还可以用df.groupby('platform')['sentiment'].value_counts()看不同平台的正负比例,这能发现某些平台以中性发言为主,单独建模可能比一个大模型效果更好。到这里,数据已经准备好,下一步就是用传统机器学习模型先打一轮基线。

3. 传统机器学习路线:TfidfVectorizer 与五分类器对比

这一章对应包里14-LogisticRegression.py17-Sentiment Analysis using Logistic Regression.py等脚本。之所以先用传统模型,一是因为它们训练快、结果可解释,二是能最快暴露数据清洗和标签里的问题。如果逻辑回归的macro-F1都做不上去,那就别急着上LSTM。

3.1 TfidfVectorizer:文本到稀疏向量的关键参数

TfidfVectorizer的核心是控制特征空间和词法粒度。下面是常见参数配置:

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.model_selection import train_test_split vectorizer = TfidfVectorizer( max_features=5000, ngram_range=(1, 2), stop_words='english', min_df=2, sublinear_tf=True ) X = vectorizer.fit_transform(df['text_clean']) y = df['sentiment'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y )

max_features=5000是特征数量上限,情感短文本一般5000到10000足够;ngram_range=(1,2)让模型看到“not good”这样由两个词组成的否定结构,单用unigram常常抓不到转折;min_df=2过滤掉只在一条文本里出现的词,这类词容易造成过拟合;sublinear_tf=True对term frequency做对数压缩,避免高频词主导权重。

这种选择不是拍脑袋的:社交媒体文本的词汇量可能很大,但真正与情感强相关的词集中在常用口语上,限到5000特征能同时减少内存和训练时间,也方便后续查看vectorizer.get_feature_names_out()来检查特征是否合理。

3.2 逻辑回归、SVM、朴素贝叶斯、随机森林和XGBoost对比

我一般会用同一个X_trainX_test跑五个模型,先拿齐基线再谈优化。代码如下:

from sklearn.linear_model import LogisticRegression from sklearn.naive_bayes import MultinomialNB from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier from sklearn.metrics import classification_report, accuracy_score models = { 'LogisticRegression': LogisticRegression(max_iter=1000, class_weight='balanced'), 'MultinomialNB': MultinomialNB(alpha=1.0), 'LinearSVC': SVC(kernel='linear', class_weight='balanced'), 'RandomForest': RandomForestClassifier(n_estimators=100, class_weight='balanced', n_jobs=-1, random_state=42), 'XGBoost': XGBClassifier(eval_metric='mlogloss', random_state=42) } for name, model in models.items(): model.fit(X_train, y_train) y_pred = model.predict(X_test) print(f"=== {name} ===") print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}") print(classification_report(y_test, y_pred))

这段代码里要注意两点:class_weight='balanced'会让少数类获得更高的惩罚权重,是处理类别不平衡最轻量的措施;MultinomialNB没有这个参数,所以当标签很不平衡时,需要给它传入sample_weight或改用ComplementNB。五个模型中,逻辑回归和线性SVC在短文本情感分类上通常最稳,因为情感词与标签的关系基本是线性可分;随机森林能捕捉非线性特征组合,但树模型对稀疏矩阵不太友好,往往需要调更多参数才能超过逻辑回归。

下表是我在这个数据集上多次运行后得到的规律性结论,不是精确值,但方向有参考价值:

模型相对表现说明
LogisticRegression基线最好之一训练快,系数可直接解释
MultinomialNB略低于LR对概率估计偏极端,适合文本词频
LinearSVC与LR接近边界更清晰,但概率输出需要额外校准
RandomForest可能低于LR在稀疏特征上优势不大,需要降维
XGBoost中上树模型的天花板较高,但调参成本也高

这里的“相对表现”是用同一个预处理管道比较出来的。如果你在本地跑出来顺序不同,先检查是不是class_weight没有加,或者测试集划分时没做stratify

3.3 用混淆矩阵定位错误,而不是只看acc

模型评估里最容易踩的坑是只用accuracy。即使加了class_weight,准确率在类别不平衡时依然可能被多数类主导。因此至少看一份classification_report,重点看macro avgweighted avg,并配合混淆矩阵:

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt lr_model = models['LogisticRegression'] y_pred = lr_model.predict(X_test) cm = confusion_matrix(y_test, y_pred, labels=lr_model.classes_) disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=lr_model.classes_) disp.plot(cmap='Blues') plt.show()

混淆矩阵可以一眼看出哪个类别之间互相干扰。在社交媒体情感数据中,“neutral”和“positive”经常被混淆,因为口语里“还行”这类表达带有正向倾向但又不强烈。接下来把错样本打出来看:

import pandas as pd pred_series = pd.Series(y_pred, index=y_test.index) misclassified = (pred_series != y_test).index print(df.loc[misclassified, ['text', 'sentiment']].head(10))

这些错例如果看起来是人类都难判断的,那说明标签本身就存在噪声;如果模型把“not bad”判成负面,就要检查ngram_range是否真的生效。这类检查比盲目调参更有价值,也是这份源码包里LogisticRegression脚本最值得参考的部分。

4. 深度学习路线:TensorFlow LSTM 的情感分类与调优

包里5-Sentiment Analysis using LSTM model in TensorFlow.py11-Sentiment Analysis using LSTM model in TensorFlow.py都是同一主题。LSTM和传统模型的区别在于:它不再把文本看成词袋,而是按词序读取内容,能捕捉“not good”这类跨位置否定关系。

4.1 为什么要用 Embedding 而不是 TF-IDF

TF-IDF加上bigram已经能处理相邻的否定结构,但无法建模“not only good but actually bad”这种跨越多个词的转折。Embedding层会把每个词映射成低维稠密向量,语义相近的词在向量空间里更近;LSTM沿时间步处理向量序列,理论上能保存更长的上下文信息。代价是需要更多数据训练,166KB数据集可能不足以发挥LSTM的全部能力,但作为实验完全够用。

4.2 Tokenizer 和 pad_sequences 的数据管线

神经网络输入必须是等长数值序列。先把清洗后的文本分词并映射成整数索引,再用pad_sequences统一长度:

from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences from sklearn.model_selection import train_test_split from sklearn.preprocessing import LabelEncoder VOCAB_SIZE = 10000 MAX_LEN = 100 tokenizer = Tokenizer(num_words=VOCAB_SIZE, oov_token='<OOV>') tokenizer.fit_on_texts(df['text_clean']) sequences = tokenizer.texts_to_sequences(df['text_clean']) X_seq = pad_sequences(sequences, maxlen=MAX_LEN, padding='post', truncating='post') encoder = LabelEncoder() y_encoded = encoder.fit_transform(df['sentiment']) X_seq_train, X_seq_test, y_train, y_test = train_test_split( X_seq, y_encoded, test_size=0.2, random_state=42, stratify=y_encoded )

oov_token='<OOV>'给所有没见过的词一个统一编号,预测阶段遇到新词不会报错;padding='post'在短文本后面补0,比前补0更符合从左到右的阅读顺序;truncating='post'截掉后面的词,保留开头信息。MAX_LEN的选择要看文本长度分布,我一般先画长度直方图,然后取95%分位数。如果直接取100,但大部分文本只有20个词,会浪费大量训练时间。LabelEncoder把字符串标签转成0、1、2整数,否则keras的sparse_categorical_crossentropy无法计算损失。

4.3 模型结构:Embedding + LSTM + Dropout

一个能跑通的基本结构如下:

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau model = Sequential([ Embedding(VOCAB_SIZE, 128, input_length=MAX_LEN), LSTM(64, dropout=0.3, recurrent_dropout=0.2), Dense(128, activation='relu'), Dropout(0.5), Dense(3, activation='softmax') ]) model.compile(optimizer=Adam(learning_rate=1e-3), loss='sparse_categorical_crossentropy', metrics=['accuracy']) callbacks = [ EarlyStopping(monitor='val_loss', patience=2, restore_best_weights=True), ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=1, min_lr=1e-5) ] history = model.fit( X_seq_train, y_train, validation_split=0.2, epochs=10, batch_size=32, callbacks=callbacks, verbose=1 )

这里的核心参数选择如下表:

参数取值作用
Embedding 输出维度128词向量维度,数据集小时不必再大
LSTM units64隐藏状态维度,过大容易过拟合
dropout0.3控制循环连接外的神经元随机失活
recurrent_dropout0.2控制LSTM内部状态更新的失活
初始学习率1e-3Adam常用起点,验证损失不降时减半

sparse_categorical_crossentropy配合整数标签,不需要额外套one-hot编码。如果数据集有三类,输出层神经元就是3;如果将来变成二分类,记得把最后一个Dense改成1并用sigmoid,否则模型输出概率加起来不是1。

提示:如果训练时遇到“Failed to find the dnn implementation”这类错误,通常是TensorFlow版本和本机硬件不匹配,先换到CPU版本跑通流程,再考虑GPU加速。

4.4 训练曲线怎么看和模型保存

LSTM在小型数据集上特别容易过拟合。训练时盯着history.history里的loss曲线:

import matplotlib.pyplot as plt plt.plot(history.history['loss'], label='train_loss') plt.plot(history.history['val_loss'], label='val_loss') plt.legend() plt.show()

如果val_loss在第3个epoch开始上升,而train_loss还在下降,说明模型开始死记训练集。这时候优先增加Dropout或减小LSTM units,而不是换模型结构。另一个常见问题是TensorFlow版本差异导致Embedding里的input_length报错,如果遇到,直接删除input_length参数,Embedding会根据实际输入推断长度。训练完成后别忘了保存词表:

import pickle with open('tokenizer.pkl', 'wb') as f: pickle.dump(tokenizer, f)

推理时先加载模型和tokenizer.pkl,对新文本执行同样的prepare_texttexts_to_sequencespad_sequences,再取np.argmax(model.predict(seq), axis=1)。这就是包内LSTM脚本的大致闭环。

5. 对照实验:LoRA 微调 Llama 3 与 VADER 词典法兜底

这个zip包里最有话题性的是3-Sentiment Analysis using LLama 3 1 with LoRA.py16-Social Media Sentiment Analysis using Gemma.py。这两个不是常规的“训练一个模型”,而是用生成式大模型做情感分类的微调实验。在没有GPU的环境下,我建议先用VADER词典法把基线跑出来,再决定要不要烧算力。

5.1 LoRA 微调的关键配置

LoRA是一种参数高效微调方法,它冻结原始大模型权重,只训练一部分低秩矩阵。核心配置如下:

from peft import LoraConfig lora_config = LoraConfig( r=8, lora_alpha=16, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM" )

r=8表示低秩矩阵的秩,这是模型可学习容量的主要控制项;lora_alpha是缩放系数,一般设为r的1到2倍,这里取16是常见配置;lora_dropout=0.05只对低秩部分做随机失活。如果把r调到64,可训练参数会显著增加,在166KB的小数据集上不一定更好。如果显存不够,优先把batch size降到1,而不是调小r

5.2 VADER 与 TextBlob:零成本基线

VADER是为社交文本设计的词典模型,不需要训练,几行代码就能得到分数:

from nltk.sentiment.vader import SentimentIntensityAnalyzer analyzer = SentimentIntensityAnalyzer() def vader_label(text): compound = analyzer.polarity_scores(text)['compound'] if compound >= 0.05: return 'positive' elif compound <= -0.05: return 'negative' return 'neutral' df['vader_pred'] = df['text'].apply(vader_label)

0.05这个阈值来自VADER的官方建议,但在不同平台上不一定最优。可以扫一遍np.arange(-0.1, 0.1, 0.01),找macro-F1最高的阈值,这比手动调阈值快得多。TextBlob的polarity同理,适合句子结构比较完整的文本,但对网络缩略语不如VADER稳定。

5.3 用分层K折交叉验证得到可发表的数字

单次train_test_split的结果不够可靠,尤其是这个数据集只有166KB,换一个random_state可能差好几个百分点。我建议用StratifiedKFold做5折交叉验证,每次输出macro-F1,最后取均值。代码如下:

from sklearn.model_selection import StratifiedKFold from sklearn.pipeline import make_pipeline from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.metrics import f1_score skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) f1s = [] for fold, (tr, te) in enumerate(skf.split(df['text_clean'], df['sentiment'])): pipe = make_pipeline( TfidfVectorizer(max_features=3000, sublinear_tf=True), LogisticRegression(max_iter=1000, class_weight='balanced') ) pipe.fit(df['text_clean'].iloc[tr], df['sentiment'].iloc[tr]) pred = pipe.predict(df['text_clean'].iloc[te]) fold_f1 = f1_score(df['sentiment'].iloc[te], pred, average='macro') f1s.append(fold_f1) print(f'fold {fold + 1}: {fold_f1:.4f}') print(f'mean macro-f1: {sum(f1s) / len(f1s):.4f}')

这段代码可以直接替换成LSTM模型或LoRA微调模型的预测函数,用来和传统模型公平对比。用make_pipeline而不是手动先fit Tfidf再喂给分类器,是为了避免漏掉fit导致测试集信息泄漏。最后把所有模型的fold-F1记录到同一个DataFrame里:

pd.DataFrame({'model': ['lr', 'lstm', 'vader', 'lora'], 'fold1': f1_lr, 'fold2': f1_lstm}).to_csv('model_benchmark.csv', index=False)

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

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

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

立即咨询