- 机器学习
- 教程
- 人工智能
- 数据分析
【免费下载链接】mlcourse.ai
Open Machine Learning Course
本篇技术指南围绕 mlcourse.ai(Open Machine Learning Course)Demo 作业四展开:利用超过 100 万条 Reddit 评论构建"讽刺(sarcasm)二分类器"。文章以 assignment04_sarcasm_detection_with_logit.md 为主骨架,结合仓库中对应的 任务 Notebook 与 完整参考答案 Notebook,完整复现数据下载、EDA、Tf-Idf 特征化、逻辑回归训练、混淆矩阵评估、ELI5 解释,以及用 subreddit 元特征提升精度的全过程。读者学完后,将掌握一套"词向量(Tf-Idf)+线性分类器+可解释性工具"的标准 NLP 文本分类实战方案。
任务背景:为什么讽刺检测是一个有挑战的分类问题
讽刺(sarcasm)是人类语言中最难被机器识别的修辞手法之一——字面含义与实际意图相反。本作业使用论文《A Large Self-Annotated Corpus for Sarcasm》公开的自标注语料:超过 100 万条来自 Reddit 的评论,每条被标注为"讽刺"或"非讽刺"。这是一个天然的文本二分类问题,也是逻辑回归(Logistic Regression)在自然语言处理领域的典型应用场景。
本文档在 mlcourse.ai 中对应 Demo 作业四,仓库中同时提供了三份可对照学习的材料:
- 作业正文:assignment04_sarcasm_detection_with_logit.md(MyST Markdown 格式)
- 待完成版 Notebook:jupyter_english/assignments_demo/assignment04_sarcasm_detection_with_logit.ipynb
- 完整解答版 Notebook:jupyter_english/assignments_demo/assignment04_sarcasm_detection_with_logit_solution.ipynb(法文版同款作业位于 jupyter_french/assignments_demo/a4-demo-sarcasm-detection-with-logit-fr_def.ipynb)
:width: 400px 图 1:作业正文中自带的一处"冷幽默"配图,暗示讽刺检测并非表面看起来那么容易环境准备与依赖库
作业开头给出了全部必要的导入语句,它们是后续所有步骤的基础:
# some necessary imports import os from pathlib import Path import numpy as np import pandas as pd import seaborn as sns from matplotlib import pyplot as plt from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, confusion_matrix from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline依赖清单可归纳为四类:
| 类别 | 库 | 用途 |
|---|---|---|
| 数据科学基础 | numpy、pandas | 数组运算与表格数据处理 |
| 可视化 | seaborn、matplotlib | EDA 绘图 |
| 机器学习 | scikit-learn | Tf-Idf 向量化、逻辑回归、评估、Pipeline |
| 系统工具 | os、pathlib | 文件路径与下载管理 |
数据集下载与加载
原始语料超过 100 万条评论,体积较大,不适合直接存放在仓库中。作业正文提供了一个 Google Drive 下载辅助函数,通过gdown命令行工具完成下载:
def download_file_from_gdrive(file_url, filename, out_path='../../_static', overwrite=False): """ Downloads a file from GDrive given an URL :param file_url: a string formated as https://drive.google.com/uc?id=<file_id> :param: the desired file name :param: the desired folder where the file will be downloaded to :param overwrite: whether to overwrite the file if it already exists """ file_exists = os.path.exists(f'{out_path}/{filename}') if (file_exists and overwrite) or (not file_exists): os.system(f'gdown {file_url} -O {out_path}/{filename}')该函数的三个关键点:
- 幂等性:只有文件不存在、或显式设置
overwrite=True时才触发下载,避免重复下载浪费流量; - 参数语义:
file_url必须是形如https://drive.google.com/uc?id=<file_id>的分享链接;filename是保存的文件名;out_path指定下载目录; - 可移植性:内部调用
os.system执行gdown,要求环境中已安装 gdown(pip install gdown)。
随后设定常量并读取数据:
FILE_URL = 'https://drive.google.com/uc?id=1KbBdJaEY8RF4GXzoihWgH0RdqoVBZ_oi' FILE_NAME = 'train-balanced-sarcasm.csv.zip' DATA_PATH = '../../_static/data/' download_file_from_gdrive(file_url=FILE_URL, filename=FILE_NAME, out_path=DATA_PATH) train_df = pd.read_csv(DATA_PATH + "train-balanced-sarcasm.csv.zip")如果下载不便,也可从 Kaggle 的 Sarcasm 数据集下载train-balanced-sarcasm.csv.zip,放入本地任意目录后修改DATA_PATH指向即可。注意:仓库中的 Jupyter Notebook 版本将DATA_PATH指向../input/sarcasm/train-balanced-sarcasm.csv,这是 Kaggle 环境的默认输入路径;在本地运行时请按实际目录调整。
数据初探:head / info / 缺失值处理 / 类别平衡
加载后首先用train_df.head()查看前几行,确认列结构;再用train_df.info()检查每一列的类型与非空情况:
- 核心特征列:
comment(评论文本)、subreddit(所在子版块)、author(作者)、score(评分)等; - 目标列:
label,取值 0(非讽刺)或 1(讽刺)。
由于部分评论文本缺失,直接删除对应行:
train_df.dropna(subset=["comment"], inplace=True)随后统计标签分布,验证数据集确实平衡(这正是 "train-balanced" 的含义):
train_df["label"].value_counts()讽刺与非讽刺样本数大致各占一半,这使accuracy(准确率)成为合理的主要评估指标,不需要额外处理类别不平衡问题。
划分训练集与验证集
使用train_test_split以固定随机种子划分,保证结果可复现:
train_texts, valid_texts, y_train, y_valid = train_test_split( train_df["comment"], train_df["label"], random_state=17 )random_state=17是整个 mlcourse.ai 课程中统一采用的随机种子,保证作业、答案与读者自跑的结果可以横向比较。默认test_size=0.25,即 75% 用于训练、25% 用于验证。
作业任务清单(原文)
作业正文给出了明确的四项任务,后续章节逐一展开:
- EDA:分析数据集并绘制可视化图表(长度分布、词云、子版块统计等);
- 建模:构建 Tf-Idf + 逻辑回归 Pipeline,基于评论
comment预测label; - 解释:用 ELI5 绘制对讽刺最具判别力的词/二元词组;
- 进阶(可选):把
subreddit当作 Bag of Words 特征加入模型,观察精度提升。
Part 1:探索性数据分析(EDA)
解答版 Notebook 给出了一套完整的 EDA 流程,这里逐条解读其业务洞察。
评论长度分布
分别对讽刺与非讽刺评论的字符长度取log1p(对数压缩,缓解长尾)后绘制直方图:
train_df.loc[train_df["label"] == 1, "comment"].str.len().apply(np.log1p).hist( label="sarcastic", alpha=0.5 ) train_df.loc[train_df["label"] == 0, "comment"].str.len().apply(np.log1p).hist( label="normal", alpha=0.5 ) plt.legend()结论是:两类评论的长度分布几乎一致——长度本身不是有效的判别信号,必须依赖词汇内容。
词云
使用wordcloud库分别对两类评论生成词云,直观感受高频词的差异:
from wordcloud import STOPWORDS, WordCloud wordcloud = WordCloud( background_color="black", stopwords=STOPWORDS, max_words=200, max_font_size=100, random_state=17, width=800, height=400, ) plt.figure(figsize=(16, 12)) wordcloud.generate(str(train_df.loc[train_df["label"] == 1, "comment"])) plt.imshow(wordcloud)词云能给出直观印象(例如讽刺评论中常见 "sure"、"right"、"yeah" 等反讽高频词),但如作业所评价:"词云好看,但对建模帮助有限"——它只是词频的粗略可视化,真正的判别力要靠后面的带权重线性模型。
子版块 / 作者 / 评分的讽刺倾向
对subreddit分组聚合,观察哪些板块平均讽刺率更高:
sub_df = train_df.groupby("subreddit")["label"].agg([np.size, np.mean, np.sum]) sub_df.sort_values(by="sum", ascending=False).head(10) sub_df[sub_df["size"] > 1000].sort_values(by="mean", ascending=False).head(10)聚合结果同时给出样本数(size)、平均讽刺率(mean)与讽刺样本总数(sum),限定size > 1000是为了过滤掉样本过少、统计噪声大的子版块。同样的思路用于author(作者层面几乎无区分度,仅显示采样规律)和score(正负评分分层统计)——这些分析为 Part 4 的"子版块特征工程"埋下伏笔。
Part 2:构建 Tf-Idf + 逻辑回归 Pipeline
这是本作业的核心建模环节。解答版给出了完整参数:
# build bigrams, put a limit on maximal number of features # and minimal word frequency tf_idf = TfidfVectorizer(ngram_range=(1, 2), max_features=50000, min_df=2) # multinomial logistic regression a.k.a softmax classifier logit = LogisticRegression(C=1, n_jobs=4, solver="lbfgs", random_state=17, verbose=1) # sklearn's pipeline tfidf_logit_pipeline = Pipeline([("tf_idf", tf_idf), ("logit", logit)])三个组件的设计意图逐条说明:
TfidfVectorizer(ngram_range=(1, 2), max_features=50000, min_df=2):ngram_range=(1, 2):同时生成单词与相邻二元词组(bigram),捕获 "yes sure"、"oh really" 这类讽刺性搭配;max_features=50000:只保留 TF-IDF 权重最高的 5 万个特征,控制稀疏矩阵规模与训练开销;min_df=2:出现次数少于 2 的词直接丢弃,过滤拼写错误与一次性词汇,兼具降噪与降维作用;
LogisticRegression(C=1, n_jobs=4, solver="lbfgs", random_state=17, verbose=1):C=1:正则化强度的倒数,C 越小正则越强,这里保持默认以留出调节空间;solver="lbfgs":适合中小规模稠密/稀疏特征的拟牛顿求解器,对多分类同样适用;n_jobs=4:并行计算,加速大规模稀疏矩阵上的训练;random_state=17:保证可复现;
Pipeline([("tf_idf", tf_idf), ("logit", logit)]):把"特征化 + 建模"串成一体,fit时先学习词典再训练模型,predict时自动沿用同一套词汇表,避免验证集信息泄漏。
训练与预测:
%%time tfidf_logit_pipeline.fit(train_texts, y_train) %%time valid_pred = tfidf_logit_pipeline.predict(valid_texts)评估:
accuracy_score(y_valid, valid_pred)Part 3:混淆矩阵与 ELI5 模型解释
混淆矩阵
解答版自定义了一个可复用的混淆矩阵绘图函数(支持normalize归一化与path_to_save_fig保存),核心部分如下:
cm = confusion_matrix(actual, predicted).T if normalize: cm = cm.astype("float") / cm.sum(axis=1)[:, np.newaxis] plt.imshow(cm, interpolation="nearest", cmap=cmap) plt.colorbar() # ... 在每个格子中标注数值,阈值 cm.max()/2 决定文字黑/白两色 plt.ylabel("Predicted label") plt.xlabel("True label")调用时直接传入验证集真实标签、预测结果以及模型类别:
plot_confusion_matrix( y_valid, valid_pred, tfidf_logit_pipeline.named_steps["logit"].classes_, figsize=(8, 8), )这里展示了Pipeline.named_steps["logit"]的用法——通过名称索引取出流水线中的子估计器,是 sklearn Pipeline 的常用调试技巧。作业结论:混淆矩阵相当均衡,两类错误率接近,模型没有明显偏向某一类。
用 ELI5 揭示"讽刺关键词"
作业推荐用 ELI5 库可视化逻辑回归权重,找到对讽刺最具判别力的词/词组:
import eli5 eli5.show_weights( estimator=tfidf_logit_pipeline.named_steps["logit"], vec=tfidf_logit_pipeline.named_steps["tf_idf"], )show_weights需要同时传入估计器与对应的向量化器(vec),以便把逻辑回归系数从"特征索引"还原为"人类可读的词条"。输出结果中,正向权重最大的特征即最强烈的讽刺信号,例如 "yes sure"、"oh yeah" 等明显反讽用语——这正是上文中ngram_range=(1, 2)保留二元词组的意义所在。
Part 4(进阶):把 subreddit 作为 Bag of Words 特征
作业的加分项是把元数据subreddit特征化后与文本特征拼接。解答版的具体做法:
准备与双向量化器
subreddits = train_df["subreddit"] train_subreddits, valid_subreddits = train_test_split(subreddits, random_state=17)文本与子版块各用一个独立的 Tf-Idf 向量化器:
tf_idf_texts = TfidfVectorizer(ngram_range=(1, 2), max_features=50000, min_df=2) tf_idf_subreddits = TfidfVectorizer(ngram_range=(1, 1))注意tf_idf_subreddits只用ngram_range=(1, 1)(子版块名本身就是单一 token,无需组合词)。
分别变换后拼接
X_train_texts = tf_idf_texts.fit_transform(train_texts) X_valid_texts = tf_idf_texts.transform(valid_texts) X_train_subreddits = tf_idf_subreddits.fit_transform(train_subreddits) X_valid_subreddits = tf_idf_subreddits.transform(valid_subreddits)用scipy.sparse.hstack将两个稀疏矩阵水平拼接为一个特征矩阵:
from scipy.sparse import hstack X_train = hstack([X_train_texts, X_train_subreddits]) X_valid = hstack([X_valid_texts, X_valid_subreddits])复用同一个逻辑回归训练与评估:
logit.fit(X_train, y_train) valid_pred = logit.predict(X_valid) accuracy_score(y_valid, valid_pred)作业结论:加入子版块特征后精度略有提升。原因不难理解:某些子版块(如特定幽默、吐槽类板块)本身具有更高的讽刺倾向,相当于给模型注入了"对话场景"这一先验信息,而 Tf-Idf 子版块特征本质上就是作业要求的 Bag of Words 思路。
关键经验与可迁移要点
- Tf-Idf + 线性分类器是文本分类的高性价比基线:在词序重要性有限的短文本场景(如评论、推文)上,其效果常常不逊于复杂深度模型,且训练成本极低;
- 二元词组(bigram)对讽刺检测价值显著:讽刺信号多来自固定搭配而非单个词,"yes sure" 这类特征只有通过
ngram_range=(1, 2)才能被模型捕捉; - Pipeline 保证一致性与复现性:统一
random_state,并在 Pipeline 内完成 fit/transform,可有效防止验证阶段的信息泄漏; - 可解释性是工程落地的重要一环:ELI5 权重可视化让黑盒分类器变得可审计,便于发现数据偏差或部署前的规则化替换;
- 元数据特征工程简单有效:把
subreddit、author等结构化字段向量化后与文本特征拼接(scipy.sparse.hstack),是"多源特征融合"的低成本范式。
延伸学习路径(仓库内资料)
- 逻辑回归理论与文本分类应用,参见仓库英文课程专题四:jupyter_english/topic04_linear_models(法文版见 jupyter_french/topic04_linear_models);
- 特征工程与特征选择专题:jupyter_english/topic06_features_regression;
- 本作业其余兄弟作业(作业一到十的 Demo 与解答)集中在 jupyter_english/assignments_demo 目录,适合作为完整 ML 实战路线逐题攻克。
- 机器学习
- 教程
- 人工智能
- 数据分析
【免费下载链接】mlcourse.ai
Open Machine Learning Course
相关推荐
Reddit 讽刺评论检测实战:用 Tf-Idf 与逻辑回归构建文本分类器(mlcourse.ai Assignment 4 题解全解析)
Reddit 讽刺评论检测实战:用 Tf Idf 与逻辑回归构建文本分类器(mlcourse.ai Assignment 4 题解全解析) 导读:本文基于 ml
机器学习教程人工智能数据分析Qwen3-Coder 评测仓库视角:用 aider code editing benchmark 追踪 LLM 代码编辑性能稳定性——Claude 3.5 Sonnet 实测数据复盘
Qwen3 Coder 评测仓库视角:用 aider code editing benchmark 追踪 LLM 代码编辑性能稳定性——Claude 3.5 S
机器学习教程人工智能数据分析TradingAgents-CN 上游同步策略:分叉仓库的人工选择性同步实战指南
TradingAgents CN 上游同步策略:分叉仓库的人工选择性同步实战指南 本文档是 TradingAgents CN 仓库中 docs/maintena
机器学习教程人工智能数据分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考