危机社交媒体分类:一次机器学习实验的完整复盘

· 约 7 分钟 ·
本文目录(17 节)

这个项目要解决的问题是:在突发事件中,从社交媒体的海量信息里快速识别出与危机相关的帖子。这篇复盘记录了我从数据探索到模型上线的完整实验过程——哪些特征有用、哪个模型效果最好、调参踩了哪些坑,以及最终方案是怎么选出来的。


一、项目背景与问题定义

地震、洪水、恐怖袭击这类突发事件发生后,Twitter/微博等社交平台会在几分钟内涌入海量帖子,其中混杂着求助信息、伤亡报告、捐款号召、谣言转发和无关日常内容。应急响应部门需要从中快速筛出与危机相关的信息,用于灾情评估和资源调度。靠人工审核显然跟不上节奏,于是就有了用机器学习做自动分类的需求。

这个项目(crisis-social-media-classification)对应的就是这个场景。我用了两个公开数据集做实验:

  • CrisisLexT26:26 次重大危机事件的标注推文集合,约 5 万条
  • HumAID: humanitarian response 场景的精标注数据,类别更细

合并去重后大约 4.7 万条样本,语言以英文为主。

任务分两层:

  1. 二分类:危机相关 vs 无关
  2. 多分类(仅对危机相关样本):设施损坏 / 人员伤亡 / 捐赠求助 / 信息传播 / 其他

评估指标我选了 Macro F1 而非 Accuracy。原因是类别极度不平衡——信息传播类占 60% 以上,如果模型全猜”信息传播”也能拿到 60% 的 Accuracy,但这对应急响应毫无价值。Macro F1 对每个类别一视同仁地计算 F1 再取平均,能逼模型在小类(捐赠求助)上也下功夫,更贴合实际需求。


二、数据探索与清洗

跑了一下类别分布,结果很扎眼:

类别样本数占比
信息传播18,92040.2%
设施损坏9,84020.9%
人员伤亡7,21015.3%
其他相关6,83014.5%
无关3,2106.8%
捐赠求助1,2902.7%

捐赠求助类只有 2.7%,是典型的长尾。这意味着模型很容易在小类上”摆烂”。

文本清洗策略:

  • 去除:URL、@提及、RT 转发标记、控制字符
  • 保留:话题标签(# 后面的词往往包含语义,如 #Earthquake)、表情符号
  • 标准化:表情符号映射为情感词(如 😢 → sad)、连续标点压缩、小写化
  • 不做:不去停用词——像 not、no 这类否定词对危机语义判断很重要

针对小类数据不足,我试了三种数据增强:

  1. 同义词替换(WordNet):随机替换非停用词,每条增强 2 条
  2. 回译(英→中→英):用 Google Translate API,对捐赠求助类回译一次,类内样本翻倍
  3. EDA(Easy Data Augmentation):随机插入/删除/交换/同义词替换组合

实测回译效果最好,捐赠求助类的召回率提升了约 6 个百分点;EDA 反而引入了较多噪声,对短文本不太友好。


三、特征工程

模型没动之前,特征工程是性价比最高的方向。我对比了三类特征:

1. TF-IDF

from sklearn.feature_extraction.text import TfidfVectorizer

tfidf = TfidfVectorizer(
    ngram_range=(1, 2),
    max_features=10000,
    sublinear_tf=True,
    min_df=3,
    max_df=0.9,
)

ngram_range=(1, 2) 抓 “flood victim” 这类组合语义;sublinear_tf=True 对高频词做对数压缩,避免 “the” 这种词挤占权重。

2. 统计特征

文本长度、感叹号数量、大写字母比例、话题标签数量、疑问句标记数。单独用效果一般,但和 TF-IDF 拼一起能给模型一些”信号强度”提示。

3. 词嵌入均值池化

用 GloVe 50 维预训练向量,对每条文本的所有词取均值。对短文本有一定效果,但长文本会被稀释。

4. 特征选择

把上述特征拼起来维度很高,我用卡方检验选了 top 5000 特征。这一步对小类召回有约 1-2% 的提升——因为卡方会自动剔除那些只在信息传播类高频、对其他类别无区分度的词。


四、模型对比实验

实验设置:5-fold StratifiedKFold(分层抽样,保证每折类别分布一致),统一用相同特征管道。

模型Macro F1训练时间推理延迟备注
Logistic Regression0.72快低基线,正则强
Linear SVM0.73快低与 LR 接近
Random Forest0.68中中高维稀疏特征表现差
XGBoost0.75中中调参后最佳传统模型
BERT-base0.82慢(GPU)高直接微调

几个观察:

  • LR 和 Linear SVM 几乎打平:在 TF-IDF 稀疏特征上,线性模型就是天花板附近,多花力气调非线性模型收益有限
  • Random Forest 翻车:高维稀疏 + 类别不平衡对树模型不友好,特征重要性也很分散
  • XGBoost 优于 RF:树集成模型里 XGBoost 的二阶梯度对不平衡更稳健,且能处理 scale_pos_weight
  • BERT 一骑绝尘:直接微调比所有传统方案高 7 个点以上,但它慢、重、依赖 GPU

五、调参与优化

Optuna 调 XGBoost

import optuna
from xgboost import XGBClassifier

def objective(trial):
    params = {
        "max_depth": trial.suggest_int("max_depth", 3, 9),
        "learning_rate": trial.suggest_float("learning_rate", 0.01, 0.3, log=True),
        "n_estimators": trial.suggest_int("n_estimators", 100, 800),
        "subsample": trial.suggest_float("subsample", 0.6, 1.0),
        "colsample_bytree": trial.suggest_float("colsample_bytree", 0.6, 1.0),
        "min_child_weight": trial.suggest_int("min_child_weight", 1, 10),
        "reg_lambda": trial.suggest_float("reg_lambda", 1e-3, 10.0, log=True),
    }
    model = XGBClassifier(
        **params,
        objective="multi:softprob",
        eval_metric="mlogloss",
        early_stopping_rounds=30,
    )
    # 5-fold CV 返回 mean Macro F1
    return cv_score(model)

study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=50)

最优参数:max_depth=6、learning_rate=0.05、n_estimators=420。max_depth 大于 7 全部过拟合,验证集 F1 直接掉到 0.65 以下。

过拟合处理

  • XGBoost:early_stopping_rounds=30 + L2 正则(reg_lambda=1.0)+ subsample 限制
  • BERT:dropout 0.3、epoch 控制在 3-4、学习率 warmup
  • LR/SVM:本身就是强正则模型,反而不太需要额外处理

集成学习

XGBoost 单模型 F1=0.75,BERT 单模型 F1=0.82。我把两者的概率输出做加权平均:

# 加权融合,BERT 权重更高
final_proba = 0.3 * xgb_proba + 0.7 * bert_proba

融合后 Macro F1 提升到 0.83,比 BERT 单模型还高 1 个点。原因是 BERT 对个别短文本判断失误,XGBoost 的统计特征能修正一些极端错误。这 1 个点的提升在实际部署中是值得的。


六、结果分析与误判案例

危机分类还要区分“同一事件内的新帖子”和“从未见过的新事件”。若转发、近重复文本或同一事件模板跨了数据分区,测试分数可能主要反映记忆。阅读下面的误判时,要连同切分单位、类别定义和每类支持数一起看。

最终模型的混淆矩阵分析(节选关键类别):

真实 \ 预测信息传播捐赠求助人员伤亡
信息传播0.940.010.03
捐赠求助0.210.680.05
人员伤亡0.080.020.85

捐赠求助类召回率最低(0.68),且主要被误判为信息传播类。原因很直接:两类都涉及”传播”动作,区别在于前者是呼吁行动,后者是单纯转发信息。

典型误判:

“Please donate to help flood victims, every dollar counts #floodrelief”

这条被模型分到了”信息传播”。从语义上看它确实是”传播”一个捐款号召,但标注规范里”呼吁捐款”应该归到捐赠求助。这种边界模糊的样本很难靠模型本身解决,需要在 prompt 或后处理规则里加约束。


七、复盘与反思

做得对的决策

  1. 选 Macro F1 而非 Accuracy:从一开始就逼模型关注小类,避免被”刷高 Accuracy”误导
  2. 特征工程优先:花在 TF-IDF + 统计特征上的时间比调 BERT 还多,但回报率最高
  3. 集成而非单一模型:BERT + XGBoost 的融合把性能推到了一个新台阶

如果重来

  1. 先做基线再做深度:第一版应该直接 LR + TF-IDF 跑通端到端流程,再考虑 BERT。我一开始就想上 BERT,结果数据 pipeline 有 bug 拖了一周才暴露
  2. 更早引入回译增强:数据增强是少数能让小类召回稳定提升的手段,应该 Day 1 就做
  3. 更严格的标注规范:边界模糊的样本(如捐款号召 vs 信息传播)应该在标注阶段就明确规则,不然模型再强也只能学到混乱

对后续 NLP 项目的启发

  • 特征工程仍是性价比最高的方向:BERT 不是万能的,传统特征 + 强基线模型往往能跑到 80% 的效果,而成本是 BERT 的 1/10
  • BERT 不是万能的,但确实强:当数据量足够、任务复杂时,BERT 微调的优势无法忽视
  • 数据质量 > 模型复杂度:花一周清洗标注数据,比花一周调 BERT 超参收益高得多

最后附上完整代码示例——TF-IDF + SVM 的简洁 pipeline,作为可复用的基线:

from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.svm import LinearSVC
from sklearn.model_selection import StratifiedKFold, cross_val_score

pipeline = Pipeline([
    ("tfidf", TfidfVectorizer(
        ngram_range=(1, 2),
        max_features=10000,
        sublinear_tf=True,
        min_df=3,
        max_df=0.9,
    )),
    ("clf", LinearSVC(C=1.0, class_weight="balanced")),
])

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(
    pipeline, X_text, y,
    cv=cv, scoring="f1_macro",
    n_jobs=-1,
)
print(f"Macro F1: {scores.mean():.4f} ± {scores.std():.4f}")

这个 pipeline 在我的数据上跑出 Macro F1=0.73,训练时间 30 秒,几乎没什么依赖。任何 NLP 文本分类项目都值得先把它跑通做基线。

💬 评论

这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。

正在连接评论服务,首次访问可能需要几秒…