☰
Twitter情感分析实战:从TF-IDF到LSTM与LoRA微调的完整技术路线
2026/10/5 15:03:01 网站建设 项目流程

简介:这是一份面向机器学习与Python数据挖掘学习者的Twitter情感分析实战套餐,由20个可运行Python脚本构成,覆盖从传统机器学习到深度学习再到LLM微调的完整技术栈。资源共23个文件,包括20个源代码、2个CSV训练与验证数据集和1个说明文档,压缩包约2.03MB,解压后数据规模约10MB;脚本覆盖数据预处理、特征工程、模型训练与评估等完整流程。代码手工整理、可直接运行,依次演示文本清洗、停用词过滤、词干提取、TF-IDF向量化、词云可视化、情感极性判别,并实现逻辑回归、SVM、朴素贝叶斯、决策树、随机森林、XGBoost、LightGBM、CatBoost、LSTM、CNN、BiLSTM等多种模型;第19、20个脚本还演示了基于Transformers和Gemma 7B的微调流程,可对比不同方法的准确率与F1分数。目前已有99人学习下载,适合需要对照完整代码快速理解NLP情感分析建模流程、对比算法效果并开展实验的初中级开发者,也可作为毕业设计或竞赛的参考基线。

1. 为什么 Twitter 情感分析值得当 AI 实战入口:20 份源码、10 MB 数据集和一条完整落地的闭环

Twitter 情感分析大概是 NLP 实战里性价比最高的一道题:数据是公开的推文文本,任务是判断每条推文的情感倾向(正面、负面、中性、无关),既能把 pandas、正则清洗、特征工程这些基本功练扎实,又能从传统机器学习一路跑到 LSTM、TextCNN 和 LLM 微调。这份资源打包了 20 个可运行源码和两个 CSV 数据集(训练集加验证集共 10 MB),从 XGBoost、随机森林到 BiLSTM、TextCNN,再到 Gemma 7B 的 LoRA 微调都覆盖到了。适合两类人:想在自己电脑上跑通完整情感分析流程的 Python 工程师,以及准备面试、需要横向比较多种模型效果的候选人。文章按我拆项目的习惯,从数据读入讲到参数坑,照着改就能复现。

2. 数据读取与清洗:两张 CSV 里藏着全流程的第一个分水岭

2.1 数据集结构:四列格式、标签分布与读取注意

先说数据集本身。twitter_training.csv 是训练集,twitter_validation.csv 是验证集,两者的列结构一致。公开数据集的常见格式是四列:第一列推文 ID,第二列实体或话题名(比如某个品牌、某个赛事),第三列情感标签(Positive、Negative、Neutral、Irrelevant),第四列才是真正的推文文本。这个四列结构我在多个公开数据集里都见过,readme.txt 里一般也会注明;如果拿到手发现列名对不上,第一件事就是打印前五行把列确认清楚。

读取的时候有个小坑:推文文本里经常夹着逗号、引号甚至未闭合的引号,直接让 pd.read_csv 用默认的 C 引擎解析,可能整行错位。常见做法是加 engine='python',虽然慢一点,但对脏 CSV 的容错高很多。我一般还会先把 text 列统一成 str,否则后面正则清洗时遇到 NaN 直接报 TypeError。下面这段是数据读取和标签分布检查的可运行代码:

import pandas as pd df_train = pd.read_csv('twitter_training.csv', encoding='utf-8', engine='python') # 原 CSV 可能没有表头,这里手动指定列名 df_train.columns = ['id', 'entity', 'sentiment', 'text'] print(df_train.head()) print(df_train['sentiment'].value_counts(normalize=True)) print('空值数量:', df_train.isna().sum().sum()) df_train['text'] = df_train['text'].fillna('').astype(str)

代码逻辑:先用 read_csv 的 engine='python' 读入,容忍引号错位;然后手动指定四列列名,避免第一条记录被误当表头;value_counts(normalize=True) 输出的是每类标签的占比而不是条数,方便直接对比训练集和验证集的分布差异;最后把 text 列统一成字符串,NaN 用空串填充,保证后续清洗函数能安全处理。

参数说明:encoding 默认用 utf-8,如果读进来乱码,再试 latin-1 或 ISO-8859-1,公开的老版本推文数据有不少是这种编码。engine='python' 和 'c' 在列数很多时性能差异明显,但这个数据集只有四列,性能损失可以忽略。资源里 6-NLP Twitter With ML.py 和 18-twitter sentiment analysis eda random forest.py 的读取段基本都是这个结构,区别只在列名映射方式上。

标签分布确认完之后,再看一眼文本长度分布。Twitter 单条推文有字符上限,但数据集中经常混着爬取时拼接的长文本,长度方差很大。我习惯加一列 text_len 画个直方图,确认截断位置,这个值直接决定后面深度学习的 maxlen 参数设多少。

2.2 清洗 pipeline:URL、@、#、停用词与词干化的取舍

情感分析的清洗和通用文本分类不完全一样。通用分类可能把 #Apple 当成话题整体保留,但在情感任务里,# 号和 @ 用户名既不携带情感,又会把特征空间撑大,通常要处理。资源里 11-twitter sentiment analysis sentiment polarity.py 用了 TextBlob 那套轻量处理,5-Twitter Sentiment Analysis Using NLP.py 则做了更重的清洗,包括去 HTML 实体和 unicode 符号。

我一般把清洗函数拆成五步,每步目的明确。URL 直接删,链接没有情感倾向;@ 用户名删掉,它是交互信息不是情感信息;# 话题符删掉但保留话题词,比如 #iPhone 变成 iPhone,因为话题词本身可能是情感对象;非字母字符统一清掉;最后转小写加词干化。下面是一版可以直接落地的清洗函数,注意停用词表已经做了情感任务的特殊处理:

import re from nltk.corpus import stopwords from nltk.stem import SnowballStemmer # 情感任务必须保留否定词,否则 not good 会被洗成 good negation_words = {'not', 'no', 'never', 'nor', 'cannot'} stop_words = set(stopwords.words('english')) - negation_words stemmer = SnowballStemmer('english') def clean_tweet(text): text = re.sub(r'http\S+', '', str(text)) # 去链接 text = re.sub(r'@\w+', '', text) # 去 @ 用户名 text = re.sub(r'#(\w+)', r'\1', text) # 去 # 保留词 text = re.sub(r'[^a-zA-Z\s]', '', text) # 去数字和符号 text = text.lower() # 统一小写 words = text.split() words = [w for w in words if w not in stop_words] words = [stemmer.stem(w) for w in words] return ' '.join(words) df_train['clean_text'] = df_train['text'].apply(clean_tweet) print(df_train[['text', 'clean_text']].head())

代码逻辑:五个正则按顺序处理,先删 URL 是因为 URL 里可能带 @ 和 #,顺序反了会残留字符影响后续匹配。stop_words 从 nltk 默认英文停用词表里扣掉了 not、no、never、nor、cannot 这几个否定词,防止清洗把语义反转。SnowballStemmer 把 playing、played、plays 统一成 play,能压特征维度。apply 后生成新列 clean_text,原 text 列保留,方便后面做 EDA 对比。

参数说明:这里最要注意的就是停用词表不能照单全收。nltk 自带的 stopwords 包含 not、no 这类否定词,如果不排除,那听起来是玩笑的 "This is not good" 会被清成 "good",情感直接反掉。项目里 17-NLP Assignment 1.py 就踩过这个坑,改成自定义停用词表后指标才恢复。词干化也要谨慎,PorterStemmer 和 SnowballStemmer 对部分词的处理结果不同,后者对现代英语更稳,资源里两种都有,选一种保持全流程一致就行。

清洗后建议顺手统计一下高频词。项目里 18 号脚本用 Counter 做了词频统计,这一步的意义在于快速验证清洗方向:如果 the、a、is 还占着前排,说明清洗没走对;如果 good、bad、love 进了前五,清洗方向基本正确。

2.3 标签编码与数据集划分:别在两个 CSV 上各玩各的

清洗之后就是标签编码。情感标签是字符串,分类器大多要吃整数。常见做法是用 sklearn 的 LabelEncoder 把四个字符串映射成 0 到 3,这里有个容易翻车的点:必须在训练集上 fit 一次,之后验证集只做 transform,否则两个集合的编码映射可能不一致,模型输出跟着错位。

from sklearn.preprocessing import LabelEncoder encoder = LabelEncoder() y_train = encoder.fit_transform(df_train['sentiment']) # 验证集只用同一映射转换,不重新 fit # y_val = encoder.transform(df_val['sentiment']) print('编码映射:', dict(zip(encoder.classes_, encoder.transform(encoder.classes_))))

代码逻辑:先在训练集标签上 fit,让模型记住字符串到整数的映射,之后验证集用同一映射 transform。打印 classes_ 和对应整数,确认 Positive 编码成 2 还是 0,避免后面读 classification_report 时对错行。

参数说明:老版本 sklearn 的 LabelEncoder 遇到 NaN 会直接报错,所以编码前先处理空值,常见做法是 df['sentiment'].fillna('Neutral')。标签编码看起来简单,但在多分类文本任务里出现频率很高,我见过不止一次因为训练和验证编码不一致导致的“模型崩溃”。

数据集划分按资源本意来即可:两个 CSV 本身就是训练/验证对,不再额外切分。如果要调超参,可以从训练集里切 10% 当开发集,验证集留到最后看真实效果。这样既保证了验证集的纯净,又能拿到 early stopping 的观察依据。

3. 传统机器学习路线:TF-IDF 特征工程与 8 个分类器的横向对比

3.1 TF-IDF 参数:max_features、ngram_range、min_df 怎么配

传统机器学习做情感分类,核心是先把文本变成向量。资源里最集中的就是 TF-IDF 路线:TfidfVectorizer 把每条推文转成稀疏向量,再喂给分类器。TfidfVectorizer 参数看着多,真正影响结果的其实只有三个:max_features、ngram_range、min_df。

max_features 控制特征维度。Twitter 数据集词汇量通常在五万到十万之间,如果全量保留,特征矩阵会非常大,而且低频词基本只出现在一两条推文里,对泛化没有贡献。我一般取 3000 到 10000,10MB 这个规模取 8000 足够。ngram_range 决定是否保留词序信息,单用 (1,1) 会丢掉 "not good" 这种共现组合,设成 (1,2) 能保留两个词的组合,对情感分类收益明显,代价是特征维度翻倍。min_df 设 3,意思是词至少在三条推文里出现才保留,和 max_features 形成双保险。

from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( max_features=8000, ngram_range=(1, 2), min_df=3, sublinear_tf=True, stop_words='english' ) X_train_tfidf = vectorizer.fit_transform(df_train['clean_text']) print('特征矩阵形状:', X_train_tfidf.shape)

代码逻辑:fit_transform 吃进清洗后的文本列,输出文档-词项稀疏矩阵。sublinear_tf=True 把词频换成 1+log(TF),压制高频词的爆发影响;stop_words='english' 是第二道保险,兜住前面清洗可能漏掉的停用词;ngram_range=(1,2) 让特征里同时存在单词和相邻词对。

参数说明:这三个参数是连动的。ngram_range 越大,max_features 越容易被占满;min_df 越大,能进特征空间的词越少。在 Twitter 这种噪声高的短文本上,我建议先固定 ngram_range=(1,2) 和 min_df=3,只调 max_features。验证集分数上不去时,优先动这个参数,而不是急着换模型。

TF-IDF 矩阵出来后,看一眼非零元素占比。如果太稀疏,很多分类器效果会打折扣;如果非零占比过高,说明文本太短、特征区分度不够。推文平均长度六十到一百个 token,稀疏度在 1% 到 3% 属于正常区间,超出这个范围就要回头检查清洗函数。

3.2 一个 Pipeline 串起 6 个 sklearn 分类器:工程收尾的关键写法

资源里 6 号脚本和 18 号脚本把分类器轮着跑了一遍。这里有个工程要点:把向量化和分类器串成 Pipeline,训练和预测只调一次 fit/predict,而且换分类器时不用重新 fit 向量化器,避免数据泄露。

from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC from sklearn.naive_bayes import MultinomialNB from sklearn.ensemble import RandomForestClassifier pipelines = { 'lr': Pipeline([('tfidf', vectorizer), ('clf', LogisticRegression(max_iter=1000, C=1.0))]), 'nb': Pipeline([('tfidf', vectorizer), ('clf', MultinomialNB(alpha=0.1))]), 'svm': Pipeline([('tfidf', vectorizer), ('clf', SVC(kernel='linear', C=1.0))]), 'rf': Pipeline([('tfidf', vectorizer), ('clf', RandomForestClassifier(n_estimators=200, max_depth=20))]), } for name, pipe in pipelines.items(): pipe.fit(df_train['clean_text'], y_train) # y_pred = pipe.predict(df_val['clean_text']) print(f'{name} 完成训练')

代码逻辑:每个 Pipeline 包含同一个 vectorizer 和不同的分类器,训练时 Pipeline 会先 fit 向量化器再训练分类器,预测时同理。换分类器只换 clf 环节,特征工程部分完全复用。LogisticRegression 用 C=1.0 控制正则强度;MultinomialNB 的 alpha 用 0.1 而不是默认 1.0,短文本高噪声场景下降低平滑系数往往更好。

参数说明:SVC 用线性核在稀疏高维特征下比 RBF 核快得多,效果也更好,文本特征本身高维,不需要核函数再映射。RandomForest 必须限制 max_depth,否则容易过拟合,短文本情感分类深度 20 层左右足够。这几个分类器训练速度差异明显:逻辑回归和朴素贝叶斯几十秒,随机森林几分钟,SVC 在一万特征内还算快,再多就吃 CPU 了。

3.3 XGBoost / LightGBM / CatBoost:把文本特征当表格数据打

资源里的 4-Simple sentiment analysis with XGBoost.py 专门试了 XGBoost。这套做法的本质是把 TF-IDF 稀疏矩阵当表格数据喂给树模型。XGBoost、LightGBM、CatBoost 都支持稀疏输入,对特征缩放不敏感,能捕捉特征间的非线性关系。

实际使用中,XGBoost 的经典参数组合是 n_estimators=300、max_depth=6、learning_rate=0.1,配合 subsample 和 colsample_bytree 防过拟合。LightGBM 训练速度更快,适合特征维度更大的场景。CatBoost 擅长类别型特征,但在纯 TF-IDF 场景里优势不明显,所以如果是文本分类,我一般不优先选它。

import xgboost as xgb from sklearn.metrics import accuracy_score xgb_model = xgb.XGBClassifier( n_estimators=300, max_depth=6, learning_rate=0.1, subsample=0.8, colsample_bytree=0.8, eval_metric='mlogloss' ) xgb_model.fit(X_train_tfidf, y_train) # y_pred = xgb_model.predict(X_val_tfidf) # print('XGB accuracy:', accuracy_score(y_val, y_pred))

代码逻辑:subsample=0.8 和 colsample_bytree=0.8 是两列防过拟合的关键参数,前者每棵树只用 80% 样本,后者每棵树只用 80% 特征,对稀疏 TF-IDF 特征尤其有效。eval_metric 设成 mlogloss 适合多分类任务。标签必须是整数,所以前面 2.3 节的 LabelEncoder 是前置条件。

参数说明:n_estimators 不是越大越好,300 配 early stopping 实际效果优于 1000。max_depth=6 是文本任务的常用起点,超过 12 几乎必过拟合。learning_rate 降到 0.05 能再提一点精度,但训练时间翻倍,小数据集没必要。

从这套资源跑下来的经验看,在 10MB 这个数据规模上,逻辑回归和线性 SVC 的分数通常不输给 XGBoost。树模型的优势要在特征工程更丰富的时候才明显。我的建议是:先跑逻辑回归拿 baseline,再用 XGBoost 或 LightGBM 验证是否有提升,不要一上来就上最重的模型。

4. 深度学习路线:Tokenizer、BiLSTM 与 TextCNN 的关键参数和取舍

4.1 Tokenizer 与 pad_sequences:词索引转换的边界条件

深度学习路线和 TF-IDF 路线的分水岭在于:文本不再被转成稀疏特征向量,而是被转成整数索引序列,再映射到 Embedding 向量。资源里 1-Twitter Sentiment Analysis using LSTM.py 和 20-Sentiment Analysis with LSTM Model.py 用的都是 Keras 这套流程。第一步是 Tokenizer,统计词频建立词到索引的字典。

Tokenizer 的关键参数是 num_words、filters、oov_token。num_words 控制词表大小,只保留最高频的词;oov_token 一定要设,否则验证集里没见过的词会被直接丢弃,序列长度和语义都会受影响。

from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences tokenizer = Tokenizer(num_words=20000, oov_token='<OOV>') tokenizer.fit_on_texts(df_train['clean_text']) X_train_seq = tokenizer.texts_to_sequences(df_train['clean_text']) X_train_pad = pad_sequences(X_train_seq, maxlen=120, padding='post', truncating='post') print('序列形状:', X_train_pad.shape)

代码逻辑:fit_on_texts 只在训练集上建字典,texts_to_sequences 把每句话变成索引列表。pad_sequences 把长短不一的序列统一成 maxlen=120,比 120 长的截断,短的补 0。padding='post' 在尾部补零,truncating='post' 优先截断尾部,这样句子开头的信息能保留,对情感分类很重要,因为情感词汇往往出现在句首。

参数说明:maxlen 的选择和 TF-IDF 里的 min_df 一样关键。Twitter 文本平均 40 到 80 个 token,我一般先画出序列长度分布,取 95 分位当 maxlen,避免太长浪费算力、太短丢信息。num_words=20000 对这个规模的数据偏保守,如果发现验证集 OOV 比例高,可以提到 30000。

提示:保存 Keras 模型时,tokenizer 必须一并用 pickle 存下来。model.save 只保存网络权重,不保存词表映射,丢了 tokenizer 就等于丢了解码器,预测阶段会彻底卡住。

4.2 一个可跑的 BiLSTM:Embedding 维度、Dropout 和优化器怎么配

LSTM 在短文本情感分类里是经典主力。资源里 1 号脚本用了单层 LSTM,20 号脚本加了 Dropout,而带 Bidirectional 的变体会在同样的数据上多 2 到 3 个点。双向的意义在于:情感判断往往需要同时看前后文,单向 LSTM 只能从左到右积累信息,双向等于把倒序也扫了一遍。

下面这个模型结构是 LSTM 路线的通用骨架。Embedding 层把 token 索引映射成稠密向量,Bidirectional 包裹 LSTM 层,后面接 Dense 输出层。多分类用 softmax,二分类才用 sigmoid。

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, Bidirectional, LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam model = Sequential([ Embedding(input_dim=20000, output_dim=128, input_length=120), Bidirectional(LSTM(units=64, return_sequences=False, dropout=0.3)), Dense(64, activation='relu'), Dropout(0.3), Dense(4, activation='softmax') ]) model.compile(optimizer=Adam(learning_rate=5e-4), loss='sparse_categorical_crossentropy', metrics=['accuracy'])

代码逻辑:Embedding 的 input_dim 必须和 tokenizer.num_words 一致,否则加载权重时报维度错误;output_dim=128 是经验值,再大收益不增显存却涨。LSTM 里 units=64 是隐层维度,dropout=0.3 是层内 dropout 防过拟合。Dense(64) 加 Dropout 做分类头,最后 Dense(4) 对应四个标签。

参数说明:sparse_categorical_crossentropy 对应整数标签,如果你做了 one-hot 就得换 categorical_crossentropy。有个反直觉的现象:Adam 默认学习率 1e-3 对 LSTM 偏大,训练曲线会先降后震荡,降到 5e-4 后收敛更稳。类别不平衡时还要在 model.fit 里设 class_weight,让少数类获得更高加权,直接改善 F1。

4.3 TextCNN 对比 BiLSTM:训练速度与精度的真实差异

资源里 13-DL Assignment 02 CNN for Text Classification.py 走的是 TextCNN 路线。TextCNN 用多个不同尺寸的卷积核在序列上滑动,本质是提取 n-gram 特征,比 LSTM 快很多,因为卷积可以并行。滤波器尺寸通常取 3、4、5,对应词窗口大小。

典型结构是 Embedding → Conv1D + GlobalMaxPooling1D → Dense。Conv1D 的 filters=128,kernel_size=3 或同时用 3、4、5 三个尺寸,activation='relu',然后 global max pooling。max pooling 保留每个卷积核最强烈的信号,也就是把一句话里最关键的 n-gram 提取出来。这个设计天然适合短文本,因为情感词往往是局部出现的。

对比项TextCNNBiLSTM
训练速度CPU 可跑,快 3 到 5 倍建议 GPU,训练慢
参数敏感点kernel_size、filtersmaxlen、dropout、学习率
长距离依赖弱,只看局部窗口强,能看整句
适用数据规模10MB 以下小数据够用中等数据更能体现优势

实践结论:TextCNN 在 CPU 上跑 10 个 epoch 比 BiLSTM 快 3 到 5 倍,精度低 1 到 2 个点但很稳定。BiLSTM 胜在能建模远距离依赖,但推文长度有限,远距离依赖其实不多。没有 GPU 就先上 TextCNN;追求 F1 上限且有 GPU,就上 BiLSTM。资源里 19 号和 12 号脚本走的是前一条路,1 号和 20 号走后一条。

如果想再提分,可以在 Embedding 之上接预训练词向量。资源里 8-Lightning Sentiment Analysis.py 用 gensim 的 KeyedVectors 加载 Word2Vec 向量替换随机初始化。前提是词表对齐:预训练字典里的词必须有稳定索引映射,缺失的词用随机小值初始化,否则加载时全是 KeyError。

5. 避坑复盘:情感分类项目里反复踩的 5 个坑

5.1 停用词把 not 过滤掉,情感直接翻转

现象:清洗后训练集准确率反而低于不清洗,验证集上 "This is not good" 被预测成 Positive。

原因:nltk 默认 stopwords 包含 not、no、never、nor 这类否定词。清洗函数把它们全删了,"not good" 变成 "good",语义完全反转。这是情感分类里最隐蔽的翻车点,文本看着挺干净,指标却莫名其妙掉 5 到 8 个点。

解决:情感任务前先把否定词从停用词表里排除。我用的写法是 set(stopwords.words('english')) - {'not','no','never','nor','cannot'}。第 2 章的清洗函数已经这么做了。更深一层是把 "not good" 拼成 "not_good" 当一个整体 token,需要配合自定义分词器,适合对 F1 有更高要求的场景。

5.2 训练集和验证集用两套清洗逻辑,分数虚高

现象:训练集做了完整清洗,验证集只简单处理,验证准确率 98%,换到真实数据立刻掉到 70%。

原因:两个 CSV 分别处理时,如果为了快速跑通只清洗了训练集,验证集保留原始文本,分类器学到的是清洗后的特征分布,验证集带着 URL 和符号,两边特征空间不一致,评估结果等于白测。

解决:把清洗函数抽到一个 utils.py 里,训练集和验证集共用;vectorizer 和 tokenizer 只 fit 一边、transform 两边。我现在的习惯是预测前强制校验两边文本长度分布,差得多就回去查清洗链路。

5.3 GPU 显存溢出:batch size 与序列长度没匹配

现象:训练到第二个 epoch 报 CUDA out of memory,有时直接把 Jupyter kernel 挤崩。

原因:Embedding 输出维度乘以序列长度乘以 batch size,就是 LSTM 每一步要占的显存。maxlen=120、词向量 128 维、batch=64,在 4GB 显存上勉强跑;换成双向 LSTM 参数量直接翻倍,加上验证集前向传播,显存瞬间打满。

解决:优先把 batch size 降到 16 或 32;其次把 maxlen 降到 80;最后把 Embedding output_dim 降到 100。显存大户是双向层,如果还超,就砍掉 Bidirectional 换单向。资源里 7-Twitter Sentiment Analysis With LLM on GPU.py 的场景更极端,常见做法是开 gradient checkpointing 和混合精度,用小 batch 撑住大模型微调,LSTM 遇到 OOM 同样按这个顺序排查。

5.4 标签分布失衡:准确率虚高,F1 现原形

现象:准确率 89%,但 classification_report 里 Negative 类的 F1 只有 0.31,验证集里 Negative 样本几乎全被预测成 Neutral。

原因:公开 Twitter 数据集的 Neutral 类占比可能接近 40%,模型把样本全预测成 Neutral 就能拿到高准确率。accuracy 在类别不平衡时被多数类主导,掩盖少数类的失败。

解决:评估指标从 accuracy 换成 macro-F1 或 weighted-F1,同时要在训练阶段处理类别不平衡。Keras 里用 class_weight;sklearn 用 compute_class_weight 算出权重传入模型。XGBoost 可以给每个类别调权重参数,多分类时逐类设置。资源里不少脚本的评估段都打印了 classification_report,就是为了防止被准确率骗。

5.5 词表外 token 与 Embedding 维度对不上

现象:保存模型后重新加载,predict 时报 IndexError,或者结果全部变成同一类。

原因:两个典型来源。一是 Tokenizer 没设 oov_token,验证集新词被跳过,序列参差不齐后全被 pad 成同一个短序列,模型输出自然模板化;二是只保存了 model.h5,丢了 tokenizer 的 word_index,加载后只能用旧索引序列去预测新文本。

解决:Tokenizer 和模型一起保存,pickle 存成 tokenizer.pkl;Embedding 的 input_dim 始终等于 len(tokenizer.word_index) + 1。用了预训练词向量还要额外验证 vocab 里每个词都能在向量表里查到,缺失的用随机小值兜底。从那以后我每次跑完一个模型,都会先确认 tokenizer 文件存在,再确认 input_dim 和词表长度一致,最后才敢删训练时的临时变量。

6. 验证与进阶:混淆矩阵读法、Gemma 微调与可复用的评估模板

6.1 classification_report 和 ConfusionMatrixDisplay 怎么读

模型跑完,真正告诉你模型能不能用的不是 loss 曲线,而是分类报告。我每次只看两件事:每个类别的 F1 有没有低于 0.5 的,以及混淆集中在哪两个相邻类别上。如果 Neutral 和 Positive 大量互混,说明清洗把情感强度词压得太狠;如果 Negative 被分到 Neutral 多,说明否定词处理没干彻底。

from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay print(classification_report(y_val, y_pred, target_names=['Irrelevant', 'Negative', 'Neutral', 'Positive'])) cm = confusion_matrix(y_val, y_pred) disp = ConfusionMatrixDisplay(confusion_matrix=cm) disp.plot()

代码逻辑:target_names 的顺序必须和 LabelEncoder 的 classes_ 对齐,否则报告行名全是错的。ConfusionMatrixDisplay 把矩阵可视化,对角线越大越好,偏离对角线越远说明混淆越严重。判断标准也很简单:macro-F1 如果只比多数类占比高一点点,说明模型没学到真实信号。

6.2 进阶:Gemma 7B 的 LoRA 微调参数与数据量要求

资源里 15-Finetuning Gemma 7B it for Sentiment Analysis.py 是传统路线到 LLM 微调的跳跃。用 LoRA 微调大模型在情感分类上效果好,但门槛主要在显存。7B 模型全量微调至少要 24GB,用 LoRA 或 QLoRA 可以把单卡需求压到 8 到 10GB。LoRA 的核心参数是 rank 和 alpha,rank 控制可训练矩阵的秩,8 起步,16 是上限;alpha 是缩放因子,一般设成 rank 的两倍。

from peft import LoraConfig lora_config = LoraConfig( r=8, lora_alpha=16, target_modules=["q_proj", "v_proj"], lora_dropout=0.1 )

代码逻辑:target_modules 指定对哪些线性层加低秩矩阵,只微调 q_proj 和 v_proj 效果稳定,显存占用小。lora_dropout=0.1 防止低秩适配过拟合。数据量方面,10MB 的训练集微调 7B 模型属于偏少的量,我实际跑下来 2 到 3 个 epoch 就够,每个 epoch 存一次 checkpoint。

这个数据规模下,LLM 微调的最大收益是天然处理了否定句和反讽。TF-IDF 加分类器在 "I love waiting in line" 这种反讽句上基本无解,而 7B 模型微调后能捕捉到语气信号。从那以后,我每次跑情感分析数据集都强制走一遍这个流程:确认标签分布和清洗一致性、设好 oov_token、用 macro-F1 当主指标、tokenizer 和模型一起存,最后再决定要不要上 LLM。这个顺序帮我挡掉了大多数虚高分数和返工,希望帮到你。

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

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

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

立即咨询