sanji DIY AI & ML:K-Means 算法

发布日期:2025-06-29 00:58    点击次数:158

sanji DIY AI & ML:K-Means 算法

作家:Jacob Inglesanji

图片由 DALL-E 生成

前言: 本文使用的数据基于学问分享许可条约提供。天然我们依然尽最大勤劳确保信息的准确性和圆善性,但我们并不保证其圆善性或可靠性。数据的使用苦守各自许可条约的条件和条件,任何第三方使用也应该苦守原许可要求。提议读者详备稽查与数据集连接的具体学问分享许可条约,以了解允许的使用口头、修改功令以及签字要求。

本文的技俩是一个随着作念的技俩,演示如安在不依赖机器学习库的情况下,用 Python 构建你我方的 K-Means 对象

迎接来到我 DIY AI & ML 系列的第三篇著作!此次,我们将用 Python 搭建一个 K-Means 对象。K-Means 是我最可爱的机器学习算法之一,因为它能帮我们发现数据中那些看不见的模式。履行得好的话,它能通过严实的数学逻辑,展示出数据中很特酷好酷好的分组或聚类。这在履行天下中能带来好多很蛮横的应用。比如说,假定你被要求分析一个电商网站的点击流数据。你就可以哄骗 K-Means 把客户左证他们点击的东西、加到购物车的东西、或者购买的东西,辞别红特定的群体。这可以匡助制定个性化政策,让你左证客户所属的群体,量身定制网站体验。好啦,我们目下证实插足算法的内容。

K-Means 算法起首我得提一下,K-Means 算法是个无监督的机器学习算法。无监督机器学习模子的症结特色是:数据中莫得指标值或者标签。换句话说,我们不是要斟酌什么,而是念念给数据打上标签。在 K-Means 算法里,我们的指标是把数据分红不同的组或者簇。那这是怎么作念到的呢?起首,用户有“解放”去指定要把数据分红若干个簇。我这里加上引号,是因为信托簇的数目时,有些最好实践要洽商。太少可能会遗漏好多对你的用例有价值的信息;太多的话,可能就变得冗余了。至于怎么分派簇,每个数据点会被分到离它最近的阿谁簇。底下还有更多细节,我们接着讲。

症结想法:欧几里得距离

图片来自 Pythagorean Theorem

念念念念你早期数学课的内容吧。你可能还牢记毕达哥拉斯定理:a² + b² = c²我们用这个公式来算直角三角形斜边(也即是我那时叫的“最长边”)的长度。要获取 c 的准确数值,得对 a² + b² 开方。也许你没坚韧到,其实贯通了这个公式,你也就懂了怎么在坐标平面上研究两点之间的距离!这亦然线性代数中的一个想法,叫 L2 范数。比如说,假定你有一个二维坐标平面,x 是横轴,y 是纵轴。给定两点 (x1,y1) 和 (x2,y2),我们可以用这个公式研究它们之间的欧几里得距离:

图片由作家提供sanji

我们也可以在多于二维的空间里研究欧几里得距离。比如说,在三维空间中找两点之间距离的公式是:

图片由作家提供

那这和 K-Means 有什么相干?我们立地就讲。

质心:运行化、迭代、敛迹这部分,我们先拿个毛糙例子来显露 K-Means 怎么运作。底下这张图,是我纰谬念念的一个二维数据集。如若把这些数据点画出来,八成能一眼看出分组,不太需要用 K-Means 来分。不外为了演示,我照旧来走一遍经由。

图片由作家提供

像我前边说的,我们可以指定分红若干个簇。这个例子里,我们分红 3 个簇。起首,偶而生成三个点,手脚率先的质心。质心即是代表一个簇中心的坐标。我们用星形标记这些偶而质心:

图片由作家提供

接着,我们要给每个数据点分派质心,也即是看它离哪个质心最近,用欧几里得距离来研究。换句话说,每个数据点被分派到离它最近的质心。看到欧几里得距离在 K-Means 里起到的症结作用了吗?

图片由作家提供

运行化之后,我们要研究每个簇里统统点的均值,获取新的质心坐标。比如说,在一个簇里,把统统点的 x 和 y 坐标分别取平均数,这个新的坐标就成了新的质心或者簇中心。遵守八成是这样(看重,红色簇的质心恰好落在一个数据点上,因为阿谁簇唯独这一个点):

图片由作家提供

这些簇看着是不是不太对?不首要,我们还没齐全。要一直相通这个过程,直到算法舒服特定条件敛迹。待会儿详备说。再来一次迭代吧。起首,偶而运行化三个质心:

图片由作家提供

然后,按照欧几里得距离,把数据点分派给最近的质心。

终末,再行研究每个簇的新质心:

图片由作家提供

肉眼看,此次 K-Means 算法依然差未几把数据分好了。不外它我方可不知谈我方依然敛迹了。是以我们还得继续作念雷同的法子,何况查抄新旧质心的变化。如若新旧质心之间的欧几里得距离互异低于设定的容差(比如 0.0001 这样小的值),我们就可以住手算法,说它敛迹了。是不是嗅觉为什么我一脱手要先讲欧几里得距离了?来看敛迹的过程吧。照老功令,先偶而生成质心:

图片由作家提供

给数据点分派最近的质心:

终末,再行研究每个簇的新质心,同期查抄新旧质心的距离是否低于容差。此次,距离是 0,显露确乎达到了我们的法式,算法敛迹了:

图片由作家提供

找到最好簇数目目下,给定 k 个簇,我们依然知谈怎么把数据正确分红 k 个簇了。不外,还有个问题:最好的 k 是若干呢?有几种步调可以用,在我们要构建的 KMeans 对象里,我们会找出能获取最高综合统统(silhouette score)的 k。

图片由作家提供

a 是每个数据点到它处所簇质心的平均距离,b 是每个簇质心到最隔邻簇质心的平均距离。更庸碌点说,综合统统越高,显露一个簇里面的数据点互相之间越围聚、越长入,同期不同簇之间又分得越开。

KMeans 类目下我们依然讲收场贯通 KMeans 算法所需要的统统想法!是时刻来构建我们的对象了。先从运行化函数脱手。可以看到,我们只会用到 numpy、pandas、tqdm(用来暴露进程条),以及 scikit-learn 里的 silhouette_score(省心,就只用 sklearn 里的这一个功能,我们确实会从零构建算法)。底下说说各个属性:

import numpy as np

import pandas as pd

from sklearn.metrics import silhouette_score

from tqdm import tqdm

class DIYKMeans:

def __init__(self, max_k=10, max_iters=100, tol=1e-4, random_state=None):

self.max_k = max_k

self.max_iters = max_iters

self.tol = tol

self.random_state = random_state

self.k = None

self.centroids = None

self.labels_ = None

self.X_scaled = None

self.feature_means = None

self.feature_stds = None

self.stable_iterations = 0

• max_k:模子要尝试拟合的最大簇数目。• max_iters:拟合模子时,偶而运行化质心的最大次数。• tol:研究新旧质心距离时的容差阈值。• random_state:偶而数种子,保证可复现。• k、centroids、labels_:找到最好 k 后,会赋值这些属性。k 是最好簇数目,centroids 是最好 k 的质心坐标,labels_ 是每个数据点的簇标签。• X_scaled、feature_means、feature_stds:数据的法式化值,以及各特征的均值和法式差。• stable_iterations:纪录在容差畛域内平稳迭代次数的占位符。默许要相接 3 次平稳后,才认为算法敛迹。

质心操作 & 法式化接下来这一组步调,即是 K-Means 算法的基本操作啦。看起来应该很眼熟吧?毕竟我们之前依然讲过了。看重,我们还加了一个数据法式化的步调。为什么迫切呢?因为,数据得在合并表率上,否则表率大的特征会让算法偏向它们。比如说,我们念念按卧室数、浴室数和房价来聚类房屋。卧室和浴室可能就在 1-5 个房间之间,但价钱可能是几十万。如若不法式化,距离研究时价钱特征就会皆备主导聚类。为了幸免这种问题,我们要用法式化,这里用的是均值归一化。另外,看重我的敛迹步调也作念了改造。平时算法只消在一次迭代中舒服容差就算敛迹了,我以为不太稳,是以开采成相接 3 次舒服容差才算确实敛迹。

• standardize_data:把每个特征法式化到合并表率。• initialize_centroids:给定数据集特征数和 k,偶而生成 k 个质心。• compute_distances:用 numpy 的线性代数库研究欧几里得距离,也即是两个坐标之间酿成向量的模。• assign_clusters:给定一个数据点和质心,复返最近的质心索引。• compute_new_centroids:左证每个簇的点,分别取各特征的均值,复返新的质心坐标。• has_converged:给定容差值,查抄新旧质心是否在容差畛域内。相接 3 次舒服,才判定敛迹。

def standardize_data(self, X):

self.feature_means = np.mean(X, axis=0)

self.feature_stds = np.std(X, axis=0)

self.feature_stds[self.feature_stds == 0] = 1

return (X - self.feature_means) / self.feature_stds

def initialize_centroids(self, X, k):

np.random.seed(self.random_state)

return X[np.random.choice(X.shape[0], k, replace=False)]

def compute_distances(self, X, centroids):

return np.linalg.norm(X[:, np.newaxis] - centroids, axis=2)

def assign_clusters(self, distances):

return np.argmin(distances, axis=1)

def compute_new_centroids(self, X, labels, k):

return np.array([X[labels == i].mean(axis=0) for i in range(k)])

def has_converged(self, old_centroids, new_centroids, stable_threshold=3):

if np.linalg.norm(new_centroids - old_centroids) < self.tol:

self.stable_iterations += 1

else:

self.stable_iterations = 0

return self.stable_iterations >= stable_threshold

Fit 步调这是统统法子确切和会到沿路的步调。给定我们开采的 max_k 属性,我们会尝试统统在这个畛域内的 k 值。关于每一个 k,我们都会跑一遍迭代经由:偶而运行化质心、分派数据点、再行研究质心,一直到敛迹为止。同期,我们也司帐算每一组聚类的综合统统(silhouette score)。哪个 k 的综合统统最高,就选哪个手脚最终的簇数目。之后,我们就把 k、centroids 和 labels_ 属性赋值成最好的遵守啦。

def fit(self, X):

X = np.array(X)

X = self.standardize_data(X)

self.X_scaled = X

best_k = None

best_score = -1

best_labels = None

best_centroids = None

print("Fitting KMeans across different k values:")

for k in tqdm(range(2, self.max_k + 1), desc="Searching for optimal k"):

centroids = self.initialize_centroids(X, k)

self.stable_iterations = 0

for _ in range(self.max_iters):

distances = self.compute_distances(X, centroids)

labels = self.assign_clusters(distances)

new_centroids = self.compute_new_centroids(X, labels, k)

if self.has_converged(centroids, new_centroids):

break

centroids = new_centroids

score = silhouette_score(X, labels)

if score > best_score:

best_k = k

best_score = score

best_labels = labels

best_centroids = centroids

self.k = best_k

self.centroids = best_centroids

self.labels_ = best_labels

用信用卡数据举个例子

像片来自 Avery Evans,发布在 Unsplash

让我们用 Kaggle 上的一个信用卡数据集来测试我们的 DIY KMeans 对象。假定你在一乡信用卡公司当数据科学家,被分派到一个任务:要识别客户群体。之前,业务团队平时是左证客户的蹂躏类别和平均月余额来辞别客户群的。目下相易念念要膨大这套逻辑,但又不知谈怎么作念到范畴化。这时刻,我们我方造的 KMeans 对象就派上用场啦。先导入数据:

data = pd.read_csv('CC GENERAL.csv')

品色堂免费论坛

我们有好多特征可以用。不外,为了让业务部门能闲散妥贴新的步调,我们只保留以下这些列。看重,这些界说都是凯旋从 Kaggle 页面拿下来的:

BALANCE_FREQUENCY:账户余额更新的频率,分数在 0 到 1 之间(1 = 平时更新,0 = 很少更新)

PURCHASES_FREQUENCY:购物的频率,分数在 0 到 1 之间(1 = 平时购物,0 = 很少购物)

CREDIT_LIMIT:用户的信用卡额度

PAYMENTS:用户的支付金额

PRCFULLPAYMENT:用户支付全额的百分比

TENURE:用户握卡时刻

cols_to_keep = ['BALANCE_FREQUENCY','PURCHASES_FREQUENCY','CREDIT_LIMIT','PAYMENTS','PRC_FULL_PAYMENT','TENURE']

data_for_kmeans = data[cols_to_keep]

data_for_kmeans = data_for_kmeans.dropna()

快速作念个探索性分析,望望这些特征的基本统计信息。这里有几个症结事实:

我们有八成 8600 个圆善数据的客户样本

购物频繁与不频繁的客户八成是 50/50 各占一半

大大批客户的信用额度低于 6500 好意思元

大大批客户支付金额在 2000 好意思元以内,但最高支付到了 50000 好意思元

大大批客户只支付了最多 15% 的账单

大大批客户的握卡时刻是 12 年,险些没什么波动

data_for_kmeans.describe()

图片由作家提供

拟合我们好处的 KMeans 模子目下对数据有了个大要了解,我们来拟合我们的模子吧!

km = DIYKMeans(max_k=20)

km.fit(X=data_for_kmeans)

图片由作家提供

print('optimal number of clusters: ', km.k)

图片由作家提供

贯通聚类的含义

像片来自 Shlomo Shalev,发布在 Unsplash

太好了!我们好处的 KMeans 对象把数据分红了 7 个簇。但职责远没齐全。既然我们依然通过数学步调把客户群分开了,接下来就要探索每个群体里荫藏的症结模式了。为了作念这件事,我我方又写了一个小用具函数,它能输出每个簇的东谈主数、每个特征的均值,还配上热力争,肤浅对比分析。我在数据长入加了一个叫 'Cluster' 的列,来源是我们 KMeans 对象的 labels_ 属性。以下是我不雅察到的一些症结模式:

除了第 3 组,其他每一组的客户握卡时刻基本都是 12 年。

第 2 组是独逐一个高购物频率、高支付比例的群体,信用额度略高于平均水平。

第 0、4、5 组的支付比例都很低;但唯独第 4 组的购物频率也很低。

第 6 组是高蹂躏群体,信用额度最高,支付金额最大。

第 5 组在蹂躏水平上和第 6 组接近,但支付金额彰着低好多。

左证这些模式,有哪些内容的政策或者决策可以推出来呢?记着,信用卡公司收获,靠的是收手续费和利息。是以,从公司的角度,最好是客户不要一次性全额付清账单,这样能赚点利息,但又不可付太少,否则风险就大了。这里有几个我的小念念法:

念念目标让第 5 组的客户支付比例向第 6 组围聚;

洽商给第 2 组客户齐全普及信用额度,因为他们支付才略可以;

对第 0、4、5 组客户,洽商合适裁汰信用额度,直到他们的支付比例普及。

data_for_kmeans['Cluster'] = km.labels_

def summarize_by_cluster_with_heatmap(df, cluster_col='Cluster'):

if cluster_col not in df.columns:

raise ValueError(f"Column '{cluster_col}' not found in DataFrame.")

value_cols = [col for col in df.select_dtypes(include='number').columns if col != cluster_col]

if not value_cols:

raise ValueError("No numeric columns to summarize (other than the cluster column).")

grouped = df.groupby(cluster_col)

means = grouped[value_cols].mean().round(2).add_suffix('_mean')

counts = grouped.size().to_frame('count')

summary = pd.concat([counts, means], axis=1).reset_index()

return summary.style.background_gradient(cmap='YlGnBu', subset=summary.columns[1:]).format("{:.2f}")

summarize_by_cluster_with_heatmap(df=data_for_kmeans)

图片由作家提供





Powered by 丁香成人网 @2013-2022 RSS地图 HTML地图

Copyright Powered by站群 © 2013-2024 版权所有