Skip to main content

聚类算法的奇妙之旅:从K-means到层次聚类

· 7 min read
郭流芳
资深算法工程师

在老虎致远工作的第二年,我遇到了一个有趣的问题:如何将海量的用户数据进行自动分组?这引发了我对聚类算法的深入探索。

故事的开始:为什么需要聚类?

想象一下,你是一家咖啡店的老板,每天都有成百上千的顾客光顾。你发现有些顾客总是点拿铁,有些偏爱美式,还有些钟情于卡布奇诺。如果你能将这些顾客按照喜好自动分组,就能提供更精准的推荐服务。

这就是**聚类(Clustering)**要解决的问题:在没有标签的情况下,自动发现数据中的内在结构和模式。

K-means:简单而优雅的解决方案

算法的诞生背景

K-means算法最早由Stuart Lloyd在1957年提出,但直到1982年才正式发表。当时,贝尔实验室的研究人员面临着如何对大量信号进行量化编码的问题。他们需要一种方法来找到最能代表数据的"中心点"。

核心思想:物以类聚

K-means的思想非常直观:

  1. 选择K个中心点(就像在人群中选择K个队长)
  2. 每个数据点找最近的中心(每个人站到最近的队长身边)
  3. 重新计算中心位置(队长移动到队伍的中心)
  4. 重复步骤2-3直到收敛(直到队形稳定)

极简实现代码

import numpy as np
import matplotlib.pyplot as plt

class SimpleKMeans:
def __init__(self, k=3, max_iters=100):
self.k = k
self.max_iters = max_iters

def fit(self, data):
# 随机初始化K个中心点
self.centroids = data[np.random.choice(data.shape[0], self.k, replace=False)]

for i in range(self.max_iters):
# 计算每个点到各中心的距离
distances = np.sqrt(((data - self.centroids[:, np.newaxis])**2).sum(axis=2))

# 分配每个点到最近的中心
self.labels = np.argmin(distances, axis=0)

# 更新中心点位置
new_centroids = np.array([data[self.labels == j].mean(axis=0) for j in range(self.k)])

# 检查是否收敛
if np.allclose(self.centroids, new_centroids):
break

self.centroids = new_centroids

return self.labels

# 实战演示
if __name__ == "__main__":
# 生成示例数据(咖啡店顾客的消费偏好)
np.random.seed(42)
coffee_lovers = np.random.normal([2, 8], 1, (50, 2)) # 喜欢咖啡的顾客
tea_lovers = np.random.normal([8, 2], 1, (50, 2)) # 喜欢茶的顾客
juice_lovers = np.random.normal([5, 5], 1, (50, 2)) # 喜欢果汁的顾客

data = np.vstack([coffee_lovers, tea_lovers, juice_lovers])

# 应用K-means聚类
kmeans = SimpleKMeans(k=3)
labels = kmeans.fit(data)

# 可视化结果
plt.figure(figsize=(10, 6))
colors = ['red', 'blue', 'green']
for i in range(3):
cluster_data = data[labels == i]
plt.scatter(cluster_data[:, 0], cluster_data[:, 1],
c=colors[i], label=f'群体 {i+1}', alpha=0.6)

plt.scatter(kmeans.centroids[:, 0], kmeans.centroids[:, 1],
c='black', marker='x', s=200, linewidths=3, label='中心点')
plt.xlabel('特征1 (咖啡偏好)')
plt.ylabel('特征2 (甜度偏好)')
plt.title('K-means聚类结果:顾客偏好分组')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

K-means的优势与局限

优势:

  • 简单易懂,计算效率高
  • 对球形分布的数据效果很好
  • 收敛速度快

局限性:

  • 需要预先指定K值
  • 对初始中心点敏感
  • 假设聚类为球形,难以处理复杂形状

层次聚类:更灵活的家族树

为什么需要层次聚类?

回到咖啡店的例子,你可能发现顾客的分组并不是平等的。比如:

  • 咖啡爱好者中,有些偏爱浓缩咖啡,有些喜欢拿铁
  • 茶爱好者中,有红茶派和绿茶派的区别

这种层次化的结构,正是层次聚类的强项。

算法思想:自底向上的合并

层次聚类有两种策略:

  1. 凝聚型(Agglomerative):从每个点开始,逐步合并
  2. 分裂型(Divisive):从整体开始,逐步分割

我们重点讲凝聚型,它就像是构建家族树的过程:

class SimpleHierarchicalClustering:
def __init__(self, linkage='single'):
self.linkage = linkage # 'single', 'complete', 'average'

def fit(self, data):
n = len(data)
# 初始化:每个点都是一个簇
clusters = [[i] for i in range(n)]
distances = []

while len(clusters) > 1:
# 找到最近的两个簇
min_dist = float('inf')
merge_i, merge_j = 0, 0

for i in range(len(clusters)):
for j in range(i + 1, len(clusters)):
dist = self._cluster_distance(data, clusters[i], clusters[j])
if dist < min_dist:
min_dist = dist
merge_i, merge_j = i, j

# 合并最近的两个簇
new_cluster = clusters[merge_i] + clusters[merge_j]
distances.append(min_dist)

# 移除原来的簇,添加新簇
clusters = [c for k, c in enumerate(clusters) if k not in [merge_i, merge_j]]
clusters.append(new_cluster)

self.distances = distances
return clusters[0]

def _cluster_distance(self, data, cluster1, cluster2):
"""计算两个簇之间的距离"""
distances = []
for i in cluster1:
for j in cluster2:
dist = np.linalg.norm(data[i] - data[j])
distances.append(dist)

if self.linkage == 'single':
return min(distances) # 最近距离
elif self.linkage == 'complete':
return max(distances) # 最远距离
else: # average
return np.mean(distances) # 平均距离

# 层次聚类演示
def plot_dendrogram(distances):
"""简单的树状图可视化"""
import matplotlib.pyplot as plt

plt.figure(figsize=(10, 4))
plt.plot(range(len(distances)), distances, 'bo-')
plt.xlabel('合并步骤')
plt.ylabel('合并距离')
plt.title('层次聚类树状图')
plt.grid(True, alpha=0.3)
plt.show()

# 实际应用
hierarchical = SimpleHierarchicalClustering(linkage='average')
result = hierarchical.fit(data)
plot_dendrogram(hierarchical.distances)

两种算法的对比与选择

特性K-means层次聚类
计算复杂度O(nkd·t)O(n³)
结果确定性随机初始化影响确定性结果
簇的形状球形假设任意形状
簇的数量需要预先指定可以灵活选择
可解释性中等高(树状结构)

实际项目经验分享

在老虎致远的项目中,我们遇到过这样的场景:

场景1:用户行为分析

  • 数据量:100万+用户
  • 选择:K-means
  • 原因:数据量大,需要高效算法,且用户行为相对规整

场景2:产品类别归纳

  • 数据量:1000+产品
  • 选择:层次聚类
  • 原因:需要理解产品间的层次关系,为业务人员提供可解释的分类体系

技术演进的思考

从2014年到2017年,我见证了聚类算法在大数据时代的演进:

  1. 分布式聚类:Spark MLlib的出现让K-means可以处理TB级数据
  2. 在线聚类:Mini-batch K-means解决了流式数据的聚类问题
  3. 深度聚类:自编码器+K-means的组合开始崭露头角

延伸阅读

想要深入了解更多聚类技术吗?推荐阅读:

结语

聚类算法教会我的不仅仅是技术,更是一种思维方式:在没有明确答案的情况下,如何发现数据中隐藏的模式。这种"发现未知"的能力,正是数据科学的魅力所在。

在后续的文章中,我会继续分享在老虎致远和广联达期间的其他技术探索,敬请期待!


如果你觉得这篇文章有帮助,欢迎分享给更多对机器学习感兴趣的朋友。有任何问题也欢迎在评论区交流讨论!