🤖 机器学习入门指南: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() ❌ 否 ✅ 是 测试集、新数据

为什么要这样设计?

  1. 防止数据泄露:测试集不能影响词汇表的构建
  2. 模拟真实场景:生产环境中只能用训练好的规则
  3. 保持一致性:确保训练和测试使用相同的特征空间

类比理解:

  • 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 / 44590.134
    P(ham) = 3861 / 44590.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("你好,请问明天会议几点开始?")

原因:

  1. 训练数据全是英文:模型只学过英文单词的概率
  2. 分词问题:CountVectorizer 默认按空格分词,不适合中文
  3. 词汇表缺失:中文字符不在词汇表中,被忽略

解决方案:

  • 使用中文分词工具(如 jieba)
  • 使用中文语料库重新训练
  • 使用支持多语言的预训练模型

Q2: 如果测试集出现新词怎么办?

处理方式:

  • CountVectorizer 会忽略词汇表中不存在的词
  • 这些词对预测结果没有贡献

示例:

# 训练时词汇表: {'win': 0, 'free': 1, ...}
# 测试邮件: "Win FREE blockchain now"
# 'blockchain' 不在词汇表中 → 被忽略
# 向量: [1, 1, 0, ..., 1]  (只包含已知词)

Q3: 如何提高模型性能?

改进方向:

  1. 更好的特征提取

    # 使用 TF-IDF 代替简单的词频计数
    from sklearn.feature_extraction.text import TfidfVectorizer
    vectorizer = TfidfVectorizer()
    
  2. 尝试其他算法

    from sklearn.linear_model import LogisticRegression
    from sklearn.svm import SVC
    
  3. 增加数据量

    • 收集更多标注数据
    • 数据增强(同义词替换等)
  4. 特征工程

    • 添加邮件长度特征
    • 添加特殊字符数量
    • 添加大写字母比例

Q4: 什么是过拟合?如何避免?

过拟合 (Overfitting):模型在训练集上表现很好,但在测试集上表现差。

症状:

  • 训练准确率:99%
  • 测试准确率:70%

避免方法:

  1. ✅ 使用训练集/测试集划分
  2. ✅ 交叉验证 (Cross-validation)
  3. ✅ 正则化
  4. ✅ 简化模型
  5. ✅ 增加训练数据

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

实践平台:


📝 总结

通过这个 SMS 垃圾邮件检测项目,我们学习了:

  1. 完整的机器学习流程:从数据加载到模型部署
  2. 文本向量化技术:如何将文字转为机器可理解的数字
  3. fit_transform vs transform:防止数据泄露的关键
  4. 朴素贝叶斯原理:基于概率的分类算法
  5. 模型评估方法:准确率、精确率、召回率、F1分数
  6. 模型解释技巧:查看模型学到的具体知识

核心理念:

机器学习不是魔法,而是从数据中发现统计规律的数学方法。

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("你好,请问明天会议几点开始?")
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐