K-Means · 让数据自己 "抱团"


当你不确定恶意行为 "长什么样" 的时候

南京大学计算机学院    杨已彪

计算机软件新技术全国重点实验室

yangyibiao@nju.edu.cn

2026 年 10 月

从决策树到 K-Means · 一个自然的追问

决策树帮我们解决了 但还有一个问题
已知恶意样本 → 提取判据规则 如果不知道恶意行为长什么样呢?
有标注数据 → 监督学习 没有标注数据怎么办?
能回答 "是/否" 能发现 "未知的未知" 吗?

核心转折: 决策树需要 "有答案" 的数据。但网空场景中,大量数据是没有标签的。

🎯 K-Means 就是解决这类问题的算法 —— 聚类(Clustering)· 无监督学习: 让数据自己 "抱团",而不是由人告诉它 "正确答案"。

一个网空场景:未知威胁的发现

某部内网,数千台主机产生海量流量日志。 已知: 部分主机已被标记为恶意(已知家族) 未知: 是否存在从未被发现的异常行为模式?

✅ 有标注 → 用决策树

  • 已知恶意样本

  • 提取判据规则

  • 检测已知威胁

❓ 无标注 → 用 K-Means

  • 不知道恶意什么样

  • 让数据自己 "抱团"

  • 发现异常群体


一句话理解聚类: 分类是 "对答案",聚类是 "找朋友"。

本部分目标: 没有 "标准答案" 时,让算法自动发现数据中的结构。

什么是聚类? · 物以类聚,人以群分

有监督学习(决策树) 无监督学习(K-Means)
数据有标签(正常/恶意) 数据无标签
学习 "输入 → 输出" 的映射 发现数据内在的结构
目标是预测 目标是发现

🌍 生活中的聚类

  • 超市按消费行为给会员分群

  • 城市按出行模式划分功能区

  • 新闻网站按主题聚合文章

🎯 共同点

  • 事先不知道分几组、每组叫什么

  • 先分组,再命名

  • 这正是 "无监督" 的含义

聚类 vs 分类 · 一个直观对比

✅ 分类(决策树)

已知:

  • ✅ 正常流量(已标注)

  • ✅ 恶意流量(已标注) 目标: 新来的流量属于哪一类?

⬅ 有参考答案

❓ 聚类(K-Means)

已知:

  • ❌ 不知道谁正常

  • ❌ 不知道谁恶意 目标: 流量天然分成几组?每组有什么特点?

⬅ 没有参考答案

网空含义: 分类是 "已知威胁的检测",聚类是 "未知威胁的发现"。

K-Means 的核心思想

一句话:让相似的数据聚在一起,形成 "团伙"。

%%{init: { 'theme': 'base', 'themeVariables': { 'fontSize': '20px', 'fontFamily': 'LXGWWenKai, \"Noto Sans SC\", sans-serif' }, 'flowchart': { 'nodeSpacing': 16, 'rankSpacing': 20 } } }%%
flowchart TB
    A["📡 内网主机行为数据 <br/> (无标签)"]
    --> B["🔍 K-Means 自动聚类"]
    --> C["📊 发现 3 个行为群体"]
    C --> D["🟢 簇 A<br/> 正常业务行为"]
    C --> E["🟢 簇 B<br/> 正常业务行为"]
    C --> F["🔴 簇 C<br/> ⚠ 行为异常 <br/> 与任何已知模式不同"]
    F --> G["🎯 重点排查 <br/>→ 未知威胁线索"]
    style A fill:#1a2a3a,stroke:#268bd2,stroke-width:2px,color:#fff
    style B fill:#1a2a3a,stroke:#f39c12,stroke-width:2px,color:#fff
    style C fill:#1a2a3a,stroke:#268bd2,stroke-width:2px,color:#fff
    style D fill:#1a2d1a,stroke:#27ae60,stroke-width:2px,color:#27ae60
    style E fill:#1a2d1a,stroke:#27ae60,stroke-width:2px,color:#27ae60
    style F fill:#2d1b1b,stroke:#e74c3c,stroke-width:2px,color:#e74c3c
    style G fill:#1a2a3a,stroke:#e74c3c,stroke-width:2px,color:#e74c3c

K-Means 的 "K" 是什么?

K = 你想要分成几组(簇) 以网空场景为例:

K 分组 适用
K=2 正常 / 异常 初步筛查
K=3 正常 / 可疑 / 异常 分层研判

❓ K 怎么定? 两条路:① 业务经验 —— 安全场景常用 K=2(正常/异常)或 K=3(分层研判);② 数据驱动 —— 肘部法(后文细讲)。

K-Means 的 "Means" 是什么?

Means = 每个簇的 "中心点" (均值)

每个簇用平均特征代表 —— 群体的 "典型画像"。

🟢 簇 A · 中心点

特征 1 平均值 = 0.82
特征 2 平均值 = -1.34
特征 3 平均值 = 2.15

🟢 簇 B · 中心点

特征 1 平均值 = -2.31
特征 2 平均值 = 0.57
特征 3 平均值 = -0.98

🔴 簇 C · 中心点

特征 1 平均值 = 3.47
特征 2 平均值 = 2.18
特征 3 平均值 = -3.02

📌 每个中心点就是该簇所有点的 "平均画像" —— Means 就是这些中心点的统称

K-Means 工作流程 · 网空版

%%{init: { 'theme': 'base', 'themeVariables': { 'fontSize': '18px', 'fontFamily': 'LXGWWenKai, \"Noto Sans SC\", sans-serif' }, 'flowchart': { 'nodeSpacing': 16, 'rankSpacing': 24 } } }%%
flowchart LR
    A["📡 无标注数据"]
    --> B["🎯 随机选 K 个<br/>初始中心"]
    --> C["📏 计算每个点<br/>到各中心距离"]
    --> D["📦 分配到<br/>最近簇"]
    --> E["🔄 更新中心<br/>(重算均值)"]
    --> F{"收敛?"}
    F -->|否| C
    F -->|是| G["✅ 输出 K 个群体<br/>(供研判)"]

核心逻辑: 分配 → 更新 → 再分配 → 再更新 ... 直到中心点不再移动。 收敛条件: 所有 K 个中心点移动距离的最大值 < 预设阈值(如 0.0001) 即最 "活跃" 的中心点也几乎不动了 → 算法停止

第一步:随机选 K 个初始中心

📊 10 台主机数据

主机 外连频率 数据外传量
H1 2 50
H2 3 45
H3 2 55
H4 8 150
H5 9 140
H6 7 160
H7 20 30
H8 22 25
H9 18 35
H10 25 20

🎯 随机初始中心(K=3)

在数据范围内完全随机选 3 个点:

中心 坐标 说明
✦ 中心 1 (2, 80) 频率低 · 流量中
✦ 中心 2 (15, 180) 频率中 · 流量高
✦ 中心 3 (12, 10) 频率中 · 流量低

⚡ 初始位置是完全随机的猜测,算法会逐步迭代优化

第二步:分配每个点到最近的簇

计算每个点到 3 个中心的距离(欧氏距离,后文详解),加入最近的那个:

🟢 簇 A · 中心 (2,80)
主机 坐标 距离
H1 (2,50) 30.0
H2 (3,45) 35.0
H3 (2,55) 25.0
🟡 簇 B · 中心 (15,180)
主机 坐标 距离
H4 (8,150) 30.8
H5 (9,140) 40.4
H6 (7,160) 21.5
🔴 簇 C · 中心 (12,10)
主机 坐标 距离
H7 (20,30) 21.5
H8 (22,25) 18.0
H9 (18,35) 25.7
H10 (25,20) 16.4

📌 初步形成 3 个群体,但中心位置仍不准确(中心点尚未稳定)

第三步:更新中心点(向重心移动)

每个簇重新计算中心 = 该簇所有点的平均值:

🟢 簇 A

H1 (2,50) + H2 (3,45) + H3 (2,55) 旧中心: (2, 80) 新中心: (2.3, 50) ⬇ 向下移动 30

🟡 簇 B

H4 (8,150) + H5 (9,140) + H6 (7,160) 旧中心: (15, 180) 新中心: (8, 150) ⬇ 向左 7 · 向下 30

🔴 簇 C

H7 (20,30) + H8 (22,25) + H9 (18,35) + H10 (25,20) 旧中心: (12, 10) 新中心: (21.3, 27.5) ↗ 向右 9.3 · 向上 17.5

🔄 三个中心都向数据密集区域移动了 → 重复 "分配 → 更新" 直到收敛

第四步:收敛!最终聚类结果

经过 2 轮迭代,中心点不再移动:

🟢 簇 A

H1 (2,50)
H2 (3,45)
H3 (2,55)
中心: (2.3, 50)
✅ 低频率 · 低流量
→ 正常业务

🟡 簇 B

H4 (8,150)
H5 (9,140)
H6 (7,160)
中心: (8, 150)
⚠ 中频率 · 高流量
→ 疑似数据外泄

🔴 簇 C

H7 (20,30)
H8 (22,25)
H9 (18,35)
H10 (25,20)
中心: (21.3, 27.5)
⚠ 高频率 · 低流量
→ 疑似 C2 心跳

迭代过程追踪:

轮次 中心 1 中心 2 中心 3 状态
初始 (2, 80) (15, 180) (12, 10) —
第 1 轮 (2.3, 50) (8, 150) (21.3, 27.5) 更新
第 2 轮 (2.3, 50) (8, 150) (21.3, 27.5) ✓ 收敛

🎯 网空发现: 簇 B 和簇 C 行为异常,建议立即深入排查!

距离如何计算? —— 欧氏距离

\[ d(x,y)=\sqrt{\sum_{i=1}^{n}(x_i-y_i)^2} \]

网空场景示例: 两台主机的行为特征:

主机 外连频率 数据外传量
H1 2 50
H4 8 150

\[ d(H1, H4) = \sqrt{(2-8)^2 + (50-150)^2} = \sqrt{36 + 10000} \approx 100.2 \]

距离越大 → 两台主机行为差异越大

K-Means 的优化目标

⚠ 初始中心是随机选的 → 每次运行可能收敛到不同的结果(局部最优)。目标函数就是一把 "尺子": 用来判断该不该停、哪种结果更好。

为什么需要优化目标?

作用 说明
判断收敛 目标函数值不再下降 → 算法停止
比较结果 多次运行,选目标函数最低的那个

目标函数

\[ \min_{\mu_1,\dots,\mu_K} \sum_{k=1}^{K}\sum_{x \in C_k} \|x - \mu_k\|^2 \]

人话: 让每个点都离自己的中心点尽可能近。

实际做法:多次运行,选最优

第 1 次
随机 → 收敛 → 目标值 1500 ✓ 最优
第 2 次
随机 → 收敛 → 目标值 3500 ✗ 较差
第 3 次
随机 → 收敛 → 目标值 2800 △ 一般
...
...
第 10 次
随机 → 收敛 → 目标值 1600 ✓ 次优

💡 Sklearn 中 n_init=10 默认运行 10 次,返回目标函数最低的结果

K-Means 为什么有效?

算法不断重复两步:

%%{init: { 'theme': 'base', 'themeVariables': { 'fontSize': '17px', 'fontFamily': 'LXGWWenKai, \"Noto Sans SC\", sans-serif' } } }%%
flowchart LR
    A["📦 分配<br/>每个点到最近簇"]
    --> B["📊 更新<br/>重新计算中心"]
    B -->|重复| A
    B --> C["✅ 收敛"]
    style A fill:#1a2a3a,stroke:#268bd2,stroke-width:2px,color:#fff
    style B fill:#1a2a3a,stroke:#f39c12,stroke-width:2px,color:#fff
    style C fill:#1a2d1a,stroke:#27ae60,stroke-width:2px,color:#27ae60

数学保证: 每一步都会使目标函数下降或不变

收敛性质: ✅ 保证收敛到局部最优解 | ⚠ 不能保证全局最优解

💡 这就是为什么需要 多次运行(n_init) 或使用 k-means++ 来获得更好的初始中心

如何选择 K ? —— 肘部法

绘制: K 值 → 误差平方和(簇内距离平方和)

误差平方和 K 1 2 3 4 5 6 拐点("肘部"): K=3 下降快 · 加簇划算 下降慢 · 加簇不划算

随着 K 增加,误差持续下降。

出现明显拐点 → 即较优 K 值。

拐点之前,增加 K 收益大;拐点之后,收益递减。

肘部法回答 "选几个 K";下一个问题:分出来的簇,质量到底好不好?

如何衡量聚类质量?

簇内紧密度 (a)

同一个簇内的点彼此距离有多近

\[ a = \frac{1}{n}\sum_{x \in C} \|x - \mu\|^2 \]

理想状态:越小越好(紧凑)

簇间分离度 (b)

不同簇之间的距离有多远

\[ b = \min_{j \neq k} \|\mu_k - \mu_j\|^2 \]

理想状态:越大越好(分离)

💡 好的聚类 = 监狱管理:牢房内囚犯行为相似(簇内紧密),牢房之间界限分明(簇间分离)

轮廓系数 · 综合评分

将 "紧密度(a)" 和 "分离度(b)" 合并为 一个数值: \[ s = \frac{b - a}{\max(a, b)} \]

🧭 直觉: 每个点问自己 —— "我在自己簇里挨得紧吗(a 小)? 我离最近的隔壁簇够远吗(b 大)?" \(s\) 把答案合并成一个 [-1, 1] 的分数。

取值范围: [-1, 1]

取值 含义
接近 1 ✅ a 很小, b 很大 → 聚类质量好
接近 0 ⚠ a ≈ b → 簇之间有重叠
接近 -1 ❌ a 很大, b 很小 → 样本分错了簇

💡 整体轮廓系数 = 所有样本轮廓系数的平均值,越高越好(通常 > 0.5 为良好)。

Sklearn 实践 · 生成数据 + 训练模型

from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
# 生成 1000 个样本, 4 个 "团伙"
X, y_true = make_blobs(n_samples=1000, centers=4,
                       cluster_std=1.0, random_state=42)
# 训练 K-Means (K=4)
model = KMeans(n_clusters=4, random_state=42)
model.fit(X)
# 查看每个点的簇标签
labels = model.labels_
print(labels[:20])  # 输出: [0 0 0 1 1 2 ...]

Sklearn 实践 · 聚类中心与可视化

# 查看聚类中心
print(model.cluster_centers_)

输出:

[[-1.2  7.8]
 [ 5.3  0.9]
 [-7.1 -6.4]
 [ 3.5 -4.2]]
import matplotlib.pyplot as plt
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis')
plt.scatter(model.cluster_centers_[:, 0],
            model.cluster_centers_[:, 1],
            marker='X', s=200, color='red')
plt.title('K-Means Clustering (K=4)')
plt.show()

Sklearn 实践 · 肘部法选 K

inertia = []
for k in range(1, 11):
    model = KMeans(n_clusters=k, random_state=42)
    model.fit(X)
    inertia.append(model.inertia_)
plt.plot(range(1, 11), inertia, 'bo-')
plt.xlabel('K')
plt.ylabel('误差平方和')
plt.show()

K-Means 优点 · 网空视角

✅ 通用优点

  • 简单、直观、易实现

  • 计算高效,适合大规模数据

  • 可解释性强

🎯 网空场景价值

优势 价值
快速 海量日志分钟级完成粗聚类
无需标注 不需要预先知道恶意样本
可解释 每个簇中心可被 "画像"
可扩展 数千台主机数万特征可处理

💡 通用优点让 K-Means "好用",网空价值让它 "有用"

K-Means 固有局限 · 网空视角

⚠ 固有局限

  • 需要提前指定 K

  • 对异常值敏感

  • 只能发现球状簇

  • 结果不稳定(受初始中心影响)

🎯 网空场景下的应对

局限 应对策略
K 值难选 肘部法 + 领域知识结合
异常值敏感 预处理阶段剔除极端离群点
只能发现球状簇 配合 DBSCAN/谱聚类
不稳定 多次运行取稳定结果(k-means++)

💡 了解局限是为了在实战中规避它们 —— 没有完美的算法,只有合适的用法

K-Means 在网空领域的典型应用

场景 K-Means 做什么 实战价值
🕵 主机行为画像 按流量模式聚类,发现 "行为异常" 的主机组 定位潜在失陷主机
🔗 僵尸网络发现 聚类出通信模式相似的主机群 识别受控主机
📊 日志压缩 将海量日志聚类为有限几类模式 辅助人工筛选
🚨 告警关联 聚合同类告警,减少重复 告警降噪
👤 用户行为基线 按访问模式聚类,发现异常访问者 内部威胁发现

随堂思考题

某部内网采集了主机行为数据: K-Means 聚类后发现一个簇:

  • 外连频率:极高

  • 数据外传量:极低

  • 工作时间占比:几乎为 0

  • 通信目标数:极少 问题: 这个簇可能代表什么?如果你是分析人员,下一步怎么做?

参考答案:疑似 C2 受控主机

🚨 综合研判:高度疑似 C2 受控主机(被攻击者远程控制的 "肉鸡")

📊 行为解读
行为模式 可能的含义
外连频率 极高 维持心跳连接
数据外传量 极低 传递控制指令,非数据泄露
工作时间占比 为 0 自动化脚本,非人工操作
通信目标数 极少 只与固定 C2 服务器通信
🚨 处置流程
🔍 深入排查 — 提取进程日志
⬇
📡 溯源分析 — IP 归属 + 威胁情报
⬇
🔗 态势关联 — 是否存在横向移动
⬇
⚡ 应急响应 — 确认后启动处置

🎯 K-Means 的价值:从海量主机中缩小目标范围 — 让安全专家从 1000 台聚焦到 10 台

本部分总结 · K-Means

💡 核心思想
让数据自己 "抱团" —— 物以类聚

🔑 关键区别
无监督学习 — 不需要标注,发现未知结构

❓ 两大关键问题
距离怎么算? K 怎么选?

🎯 网空定位
从 "已知威胁检测" 到 "未知威胁发现" 的认知跃迁

K-Means 不做 "判断",只做 "发现" —— 判断留给专家。

下一部分预告

🎲 朴素贝叶斯 · 概率的推断 当判定需要 "可信度" 而非 "是/否" 的时候