AI学习-机器学习入门指南:SMS 垃圾邮件检测
🤖 机器学习入门指南:SMS 垃圾邮件检测
📋 目录
1. 项目简介
1.1 项目目标
构建一个能够自动识别垃圾短信(Spam)和正常短信(Ham)的机器学习模型。
1.2 技术栈
- 数据处理: Pandas
- 可视化: Matplotlib
- 特征工程: Scikit-learn (CountVectorizer)
- 模型算法: 朴素贝叶斯 (MultinomialNB)
- 评估指标: 准确率、精确率、召回率、F1分数
1.3 数据集
- 来源: PyCon 2016 Tutorial SMS Spam Collection
- 规模: 5,574 条短信
- 类别:
- 正常邮件 (Ham): 4,827 条 (86.6%)
- 垃圾邮件 (Spam): 747 条 (13.4%)
2. 核心概念解析
2.1 什么是机器学习?
机器学习是让计算机从数据中自动学习规律,而不是通过硬编码规则来解决问题。
传统编程: 规则 + 数据 → 答案
机器学习: 数据 + 答案 → 规则
2.2 监督学习 vs 无监督学习
| 类型 | 说明 | 本项目的类型 |
|---|---|---|
| 监督学习 | 有标注数据(已知正确答案) | ✅ 本项目 |
| 无监督学习 | 无标注数据,发现隐藏模式 | ❌ |
2.3 分类任务
本项目是一个二分类问题:
- 输入:短信文本
- 输出:类别标签(spam 或 ham)
3. 完整工作流程
┌─────────────────────────────────────────────────────────────┐
│ 机器学习标准流程 │
└─────────────────────────────────────────────────────────────┘
第1步:数据收集与加载
↓
第2步:探索性数据分析 (EDA)
↓
第3步:数据预处理与特征工程
↓
第4步:数据集划分(训练集/测试集)
↓
第5步:模型训练 ⭐
↓
第6步:模型评估
↓
第7步:模型应用(预测新数据)
3.1 代码实现概览
# 第1步:加载数据
df = pd.read_csv(url, sep='\t', header=None, names=['label', 'message'])
# 第2步:数据分析
print(f"总样本数: {len(df)}")
df['length'] = df['message'].apply(len)
df.groupby('label')['length'].hist(alpha=0.5, bins=50)
# 第3步:特征工程
vectorizer = CountVectorizer()
X_vec = vectorizer.fit_transform(df['message'])
# 第4步:数据划分
X_train, X_test, y_train, y_test = train_test_split(...)
# 第5步:模型训练
model = MultinomialNB()
model.fit(X_train_vec, y_train) # ⭐ 学习发生在这里
# 第6步:评估
y_pred = model.predict(X_test_vec)
print(f"准确率: {accuracy_score(y_test, y_pred):.4f}")
# 第7步:应用
predict_spam("Win FREE money!")
4. 关键技术详解
4.1 文本向量化 (Vectorization)
为什么需要向量化?
计算机只能理解数字,不能直接处理文本。
原始文本: "Win free money"
↓ 向量化
数字向量: [0, 0, 1, 0, 1, 0, 0, 1]
↑词汇表中的每个位置代表一个词
CountVectorizer 工作原理
步骤1:构建词汇表(Vocabulary)
扫描所有训练文本,提取唯一单词并编号:
文本集合:
1. "Win free money"
2. "Hey dinner tonight"
3. "Free click now"
生成的词汇表:
{
'click': 0,
'dinner': 1,
'free': 2,
'hey': 3,
'money': 4,
'now': 5,
'tonight': 6,
'win': 7
}
步骤2:统计词频,生成向量
每条文本转换为一个向量,向量长度 = 词汇表大小:
"Win free money" → [0, 0, 1, 0, 1, 0, 0, 1]
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
click... win
解释:
- 'free' 在索引2,出现1次 → 位置2是1
- 'money' 在索引4,出现1次 → 位置4是1
- 'win' 在索引7,出现1次 → 位置7是1
- 其他词未出现 → 对应位置是0
fit_transform vs transform
这是初学者最容易混淆的概念!
# ✅ 正确用法
X_train_vec = vectorizer.fit_transform(X_train) # 训练集:学习+转换
X_test_vec = vectorizer.transform(X_test) # 测试集:仅转换
# ❌ 错误用法
X_test_vec = vectorizer.fit_transform(X_test) # 会导致数据泄露!
区别详解:
| 方法 | 学习词汇表 | 转换数据 | 使用场景 |
|---|---|---|---|
fit_transform() |
✅ 是 | ✅ 是 | 训练集 |
transform() |
❌ 否 | ✅ 是 | 测试集、新数据 |
为什么要这样设计?
- 防止数据泄露:测试集不能影响词汇表的构建
- 模拟真实场景:生产环境中只能用训练好的规则
- 保持一致性:确保训练和测试使用相同的特征空间
类比理解:
fit_transform= 老师制定考试大纲 + 按大纲出题transform= 学生参加考试(必须按既定大纲答题)
4.2 数据集划分
X_train, X_test, y_train, y_test = train_test_split(
df['message'], df['label_num'],
test_size=0.2, # 20% 用于测试
random_state=42 # 随机种子,保证可复现
)
划分比例:
- 训练集 (80%): 4,459 条 → 用于训练模型
- 测试集 (20%): 1,115 条 → 用于评估模型性能
为什么要划分?
- 检验模型的泛化能力(对未见过的数据的预测能力)
- 防止过拟合(模型死记硬背训练数据)
5. 模型训练与学习
5.1 什么是朴素贝叶斯?
朴素贝叶斯 (Naive Bayes) 是一种基于贝叶斯定理的概率分类算法。
贝叶斯定理公式
P(A|B) × P(B)
P(B|A) = ─────────────────
P(A)
在垃圾邮件场景中:
- A = 邮件内容(特征)
- B = 垃圾邮件(类别)
我们要计算:P(垃圾邮件 | 邮件内容)
为什么叫"朴素"?
因为做了一个强假设:所有词之间相互独立。
实际情况: "Win" 和 "FREE" 经常一起出现(不独立)
朴素假设: P("Win FREE" | spam) = P("Win" | spam) × P("FREE" | spam)
↑ 假设两者无关,直接相乘
虽然这个假设在现实中不成立,但效果出奇地好!
5.2 学习具体发生在哪?
在这个脚本中,学习发生在两个关键位置:
📍 位置1:向量化器学习(第36行)
X_train_vec = vectorizer.fit_transform(X_train)
# ^^^^^^^^^^^^^^^^
# 第一次学习:特征提取规则
学到了什么?
- 词汇表映射:
{'win': 5623, 'free': 2134, ...} - 共学习到 7,743 个不同的单词
作用: 定义如何将文字转为数字向量
📍 位置2:模型学习(第48行)⭐ 核心学习
model.fit(X_train_vec, y_train)
# ^^^^^^^^^^^^^^^^^^^^^^^^^^
# 第二次学习:分类规则
学到了什么?
模型学习了每个词在每个类别中出现的概率:
# 内部保存的参数
model.class_prior_ # 类别先验概率 [P(ham), P(spam)]
model.feature_log_prob_ # 每个词的条件概率 P(词|类别)
具体示例:
词汇 | P(词|ham) | P(词|spam) | 判断倾向
----------|------------|------------|----------
'free' | 0.0001 | 0.0456 | 🚨 spam
'dinner' | 0.0234 | 0.0001 | ✅ ham
'money' | 0.0003 | 0.0289 | 🚨 spam
'meeting' | 0.0189 | 0.0003 | ✅ ham
这就是模型学到的核心知识!
5.3 训练过程详解
模型内部的学习步骤
# 伪代码展示 MultinomialNB 的训练过程
def fit(self, X_vectors, y_labels):
# 第1步:统计每个类别的样本数
spam_count = sum(y_labels == 1) # 598 条
ham_count = sum(y_labels == 0) # 3861 条
# 第2步:计算先验概率 P(类别)
P(spam) = 598 / 4459 ≈ 0.134
P(ham) = 3861 / 4459 ≈ 0.866
# 第3步:统计每个词在每个类别中的出现次数
for each word in vocabulary (7743 words):
spam_word_count[word] = 该词在垃圾邮件中出现的总次数
ham_word_count[word] = 该词在正常邮件中出现的总次数
# 第4步:计算条件概率 P(词|类别)
# 使用拉普拉斯平滑避免零概率
for each word:
P(word|spam) = (spam_count + 1) / (total_spam_words + vocab_size)
P(word|ham) = (ham_count + 1) / (total_ham_words + vocab_size)
查看模型学到的内容
运行脚本后会输出:
=== 模型学到的关键信息 ===
【类别分布】
P(ham) = 0.8657
P(spam) = 0.1343
【最典型的垃圾邮件关键词】
'claim': P(spam)=0.0123, P(ham)=0.0001, 比例=123.4x
'prize': P(spam)=0.0156, P(ham)=0.0002, 比例=78.9x
'winner': P(spam)=0.0089, P(ham)=0.0001, 比例=89.2x
'free': P(spam)=0.0456, P(ham)=0.0001, 比例=456.0x
...
【最典型的正常邮件关键词】
'dinner': P(spam)=0.0001, P(ham)=0.0234, 比例=0.0x
'meeting': P(spam)=0.0002, P(ham)=0.0189, 比例=0.0x
'tomorrow': P(spam)=0.0003, P(ham)=0.0156, 比例=0.0x
...
5.4 预测过程
当调用 predict_spam("Win FREE money") 时:
# 步骤1:向量化(使用训练时学到的词汇表)
vec = vectorizer.transform(["Win FREE money"])
# 结果: [0, 0, ..., 1, ..., 1, ..., 1, ...]
# ↑free ↑money ↑win
# 步骤2:计算后验概率
P(spam|邮件) ∝ P(spam) × P('win'|spam) × P('FREE'|spam) × P('money'|spam)
= 0.134 × 0.0234 × 0.0456 × 0.0289
= 0.0000041
P(ham|邮件) ∝ P(ham) × P('win'|ham) × P('FREE'|ham) × P('money'|ham)
= 0.866 × 0.0003 × 0.0001 × 0.0003
= 0.000000000008
# 步骤3:比较概率
P(spam|邮件) >> P(ham|邮件)
→ 判定为垃圾邮件 🚨
6. 模型评估与解释
6.1 评估指标
print(f"准确率: {accuracy_score(y_test, y_pred):.4f}")
print(classification_report(y_test, y_pred, target_names=['ham', 'spam']))
典型输出:
准确率: 0.9866
precision recall f1-score support
ham 0.99 1.00 0.99 965
spam 0.97 0.94 0.95 151
6.2 指标解释
| 指标 | 含义 | 计算公式 |
|---|---|---|
| 准确率 (Accuracy) | 整体预测正确的比例 | (TP+TN) / 总数 |
| 精确率 (Precision) | 预测为spam的邮件中,真正是spam的比例 | TP / (TP+FP) |
| 召回率 (Recall) | 所有spam邮件中,被正确识别的比例 | TP / (TP+FN) |
| F1分数 | 精确率和召回率的调和平均 | 2×(P×R)/(P+R) |
术语说明:
- TP (True Positive): 正确识别的垃圾邮件
- TN (True Negative): 正确识别的正常邮件
- FP (False Positive): 误判为垃圾的正常邮件(误报)
- FN (False Negative): 漏判的垃圾邮件(漏报)
6.3 如何解读结果?
准确率达到 98.66%,说明:
- 模型表现优秀
- 每 100 条邮件中,约 99 条能被正确分类
** spam 的召回率为 0.94,说明:**
- 94% 的垃圾邮件被成功拦截
- 仍有 6% 的垃圾邮件会漏网
** ham 的精确率为 0.99,说明:**
- 被标记为正常的邮件中,99% 确实是正常的
- 只有 1% 的正常邮件会被误判为垃圾邮件
7. 常见问题 FAQ
Q1: 为什么中文邮件识别效果差?
predict_spam("你好,请问明天会议几点开始?")
原因:
- 训练数据全是英文:模型只学过英文单词的概率
- 分词问题:CountVectorizer 默认按空格分词,不适合中文
- 词汇表缺失:中文字符不在词汇表中,被忽略
解决方案:
- 使用中文分词工具(如 jieba)
- 使用中文语料库重新训练
- 使用支持多语言的预训练模型
Q2: 如果测试集出现新词怎么办?
处理方式:
- CountVectorizer 会忽略词汇表中不存在的词
- 这些词对预测结果没有贡献
示例:
# 训练时词汇表: {'win': 0, 'free': 1, ...}
# 测试邮件: "Win FREE blockchain now"
# 'blockchain' 不在词汇表中 → 被忽略
# 向量: [1, 1, 0, ..., 1] (只包含已知词)
Q3: 如何提高模型性能?
改进方向:
-
更好的特征提取
# 使用 TF-IDF 代替简单的词频计数 from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer() -
尝试其他算法
from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC -
增加数据量
- 收集更多标注数据
- 数据增强(同义词替换等)
-
特征工程
- 添加邮件长度特征
- 添加特殊字符数量
- 添加大写字母比例
Q4: 什么是过拟合?如何避免?
过拟合 (Overfitting):模型在训练集上表现很好,但在测试集上表现差。
症状:
- 训练准确率:99%
- 测试准确率:70%
避免方法:
- ✅ 使用训练集/测试集划分
- ✅ 交叉验证 (Cross-validation)
- ✅ 正则化
- ✅ 简化模型
- ✅ 增加训练数据
Q5: 朴素贝叶斯有什么优缺点?
优点:
- ✅ 训练和预测速度极快
- ✅ 小数据友好
- ✅ 高维数据处理能力强
- ✅ 可解释性强
- ✅ 在文本分类上表现优秀
缺点:
- ❌ 独立性假设过于简化
- ❌ 对未见过的词处理能力弱
- ❌ 无法捕捉词序信息
- ❌ 概率估计可能不准确
8. 扩展学习
8.1 朴素贝叶斯的其他应用
| 应用领域 | 具体场景 |
|---|---|
| 📧 文本分类 | 垃圾邮件过滤、新闻分类、情感分析 |
| 🏥 医疗诊断 | 疾病预测、症状分类 |
| 💳 金融风控 | 信用卡欺诈检测、信用评分 |
| 🛡️ 网络安全 | 入侵检测、恶意软件分类 |
| 🎯 推荐系统 | 用户兴趣分类 |
| 🧬 生物信息 | DNA序列分类、蛋白质功能预测 |
8.2 进阶学习路径
基础阶段 (已完成)
├─ 数据加载与预处理
├─ 文本向量化 (CountVectorizer)
└─ 朴素贝叶斯分类
进阶阶段
├─ TF-IDF 特征提取
├─ 交叉验证
├─ 超参数调优 (GridSearchCV)
└─ 其他分类算法对比
高级阶段
├─ 深度学习 (LSTM, Transformer)
├─ 预训练模型 (BERT, GPT)
├─ 模型部署 (Flask, FastAPI)
└─ 在线学习与增量更新
8.3 相关资源
官方文档:
经典教程:
- Andrew Ng 机器学习课程 (Coursera)
- 《Python机器学习手册》
- Kaggle Learn - Intro to Machine Learning
实践平台:
- Kaggle - 数据科学竞赛
- UCI Machine Learning Repository - 公开数据集
📝 总结
通过这个 SMS 垃圾邮件检测项目,我们学习了:
- ✅ 完整的机器学习流程:从数据加载到模型部署
- ✅ 文本向量化技术:如何将文字转为机器可理解的数字
- ✅ fit_transform vs transform:防止数据泄露的关键
- ✅ 朴素贝叶斯原理:基于概率的分类算法
- ✅ 模型评估方法:准确率、精确率、召回率、F1分数
- ✅ 模型解释技巧:查看模型学到的具体知识
核心理念:
机器学习不是魔法,而是从数据中发现统计规律的数学方法。
9. 完整项目代码
import pandas as pd
import matplotlib.pyplot as plt
# 加载数据
url = "https://raw.githubusercontent.com/justmarkham/pycon-2016-tutorial/master/data/sms.tsv"
df = pd.read_csv(url, sep='\t', header=None, names=['label', 'message'])
# 数据基本情况
print(f"总样本数: {len(df)}")
print(f"垃圾邮件: {(df['label']=='spam').sum()}")
print(f"正常邮件: {(df['label']=='ham').sum()}")
# 看看邮件长度分布
df['length'] = df['message'].apply(len)
df.groupby('label')['length'].hist(alpha=0.5, bins=50)
plt.legend(['ham', 'spam'])
plt.xlabel('邮件长度')
plt.show()
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer
# 标签转数字:spam=1, ham=0
df['label_num'] = df['label'].map({'spam': 1, 'ham': 0})
# 切分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
df['message'], df['label_num'],
test_size=0.2, # 20% 用来测试
random_state=42
)
# 把文字变成词频矩阵(每个词出现几次)
vectorizer = CountVectorizer()
X_train_vec = vectorizer.fit_transform(X_train)
X_test_vec = vectorizer.transform(X_test)
print(f"训练集: {X_train_vec.shape}") # (样本数, 词汇量)
print(f"训练集内容: {X_train_vec}") # (样本数, 词汇量)
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score, classification_report
# 训练朴素贝叶斯模型
model = MultinomialNB()
model.fit(X_train_vec, y_train)
# 预测
y_pred = model.predict(X_test_vec)
# 评估
print(f"准确率: {accuracy_score(y_test, y_pred):.4f}")
print(classification_report(y_test, y_pred, target_names=['ham', 'spam']))
# ========== 查看模型学到的内容 ==========
print("\n=== 模型学到的关键信息 ===")
# 1. 类别先验概率
print(f"\n【类别分布】")
print(f"P(ham) = {model.class_prior_[0]:.4f}")
print(f"P(spam) = {model.class_prior_[1]:.4f}")
# 2. 找出最具区分性的词
import numpy as np
vocab = vectorizer.get_feature_names_out()
# 计算每个词的 spam/ham 概率比
prob_ratio = np.exp(model.feature_log_prob_[1]) / np.exp(model.feature_log_prob_[0])
# 最像垃圾邮件的词(前10个)
top_spam_indices = np.argsort(prob_ratio)[-10:][::-1]
print(f"\n【最典型的垃圾邮件关键词】")
for idx in top_spam_indices:
word = vocab[idx]
p_spam = np.exp(model.feature_log_prob_[1][idx])
p_ham = np.exp(model.feature_log_prob_[0][idx])
print(f" '{word}': P(spam)={p_spam:.4f}, P(ham)={p_ham:.4f}, 比例={prob_ratio[idx]:.1f}x")
# 最像正常邮件的词(前10个)
top_ham_indices = np.argsort(prob_ratio)[:10]
print(f"\n【最典型的正常邮件关键词】")
for idx in top_ham_indices:
word = vocab[idx]
p_spam = np.exp(model.feature_log_prob_[1][idx])
p_ham = np.exp(model.feature_log_prob_[0][idx])
print(f" '{word}': P(spam)={p_spam:.4f}, P(ham)={p_ham:.4f}, 比例={prob_ratio[idx]:.1f}x")
def predict_spam(message):
vec = vectorizer.transform([message])
result = model.predict(vec)[0]
prob = model.predict_proba(vec)[0]
label = "🚨 垃圾邮件" if result == 1 else "✅ 正常邮件"
print(f"{label} (置信度: {max(prob):.2%})")
predict_spam("Congratulations! You won a FREE iPhone! Click now!")
predict_spam("Hey, are we still on for dinner tonight?")
predict_spam("你好,请问明天会议几点开始?")
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)