【Python 实战】基于 K 近邻算法实现约会对象分类器
一、场景背景:为什么需要这个分类器?
海伦在使用约会网站时,遇到了一个典型需求:她将过往交往对象分为 3 类,但网站无法自动识别新用户属于哪类 ——
类别 1:不喜欢的人
类别 2:一般喜欢的人
类别 3:非常喜欢的人
而新用户的特征只有 3 个:
每年获得的飞行常客里程数(数值范围大,如 0~13 万)
玩视频游戏所耗时间百分比(数值范围中等,如 0~70%)
每周消费的冰淇淋公升数(数值范围小,如 0~2L)
我们需要做的就是:输入这 3 个特征,让模型自动输出 “该推荐(类别 2/3)” 或 “不推荐(类别 1)”。
二、核心原理:K 近邻算法怎么工作?
K 近邻的核心逻辑可以总结为 “近朱者赤,近墨者黑”—— 未知样本的类别,由它周围 “最近的 K 个邻居” 的类别决定。
整个流程分 5 步,其中数据归一化和K 值选择是关键(后面会重点讲):
准备训练数据:包含已知类别的样本(特征 + 标签);
预处理特征:将不同范围的特征归一化(避免里程数 “碾压” 其他特征);
计算距离:用 “欧几里得距离” 衡量未知样本与所有训练样本的相似度;
找 K 个近邻:按距离从小到大排序,选前 K 个样本;
投票预测:K 个近邻中出现次数最多的类别,就是未知样本的类别。
三、关键问题:为什么要做特征归一化?
先看一组真实训练样本数据,你会发现问题:
| 样本序号 | 玩游戏时间占比 | 飞行里程数 | 冰淇淋消费量 | 类别 |
| 1 | 0.8 | 400 | 0.5 | 1 |
| 2 | 12 | 134000 | 0.9 | 3 |
| 3 | 67 | 32000 | 0.1 | 2 |
如果直接计算距离(比如样本 1 和样本 2):
飞行里程数的差值是 134000-400=133600
玩游戏时间的差值是 12-0.8=11.2
显然,飞行里程数的差值会完全主导距离计算,但海伦认为 3 个特征同等重要 —— 这就是归一化的必要性。
我们用 “0-1 归一化” 将所有特征压缩到 [0,1] 区间,公式如下(修正了原文档的笔误,核心是 “减最小值、除极差”):
比如飞行里程数的范围是 0~134000,那么 134000 归一化后是 1,400 归一化后是 400/134000≈0.003,这样就和其他特征的量级一致了。
四、完整代码实现(Python+NumPy)
1. 依赖库安装
首先确保安装了 NumPy(处理数值计算):
pip install numpy
2. 代码模块拆解
(1)数据预处理:实现归一化函数
import numpy as np
def normalize_data(data_set):
"""
对特征矩阵进行0-1归一化
:param data_set: 训练集特征矩阵(shape:[样本数, 特征数])
:return: normalized_data(归一化后的特征矩阵), min_vals(各特征最小值), ranges(各特征极差)
"""
# 1. 计算每个特征的最小值(按列取min)
min_vals = data_set.min(axis=0)
# 2. 计算每个特征的极差(最大值-最小值,按列取max后减min_vals)
max_vals = data_set.max(axis=0)
ranges = max_vals - min_vals
# 3. 归一化计算(避免除0,若ranges=0则保持原数据)
m = data_set.shape[0] # 样本数量
normalized_data = (data_set - np.tile(min_vals, (m, 1))) / np.tile(ranges, (m, 1))
return normalized_data, min_vals, ranges
(2)K 近邻核心:实现距离计算与预测
def knn_classify(test_x, data_set, labels, k):
"""
K近邻分类核心函数
:param test_x: 单个测试样本(shape:[1, 3])
:param data_set: 归一化后的训练集特征(shape:[样本数, 3])
:param labels: 训练集标签(shape:[样本数, 1],值为1/2/3)
:param k: 近邻数量(建议取奇数,避免投票平局)
:return: predicted_label(预测类别:1/2/3)
"""
# 1. 计算测试样本与所有训练样本的欧几里得距离
m = data_set.shape[0]
# 生成与训练集同形状的测试样本矩阵(便于广播计算)
test_mat = np.tile(test_x, (m, 1))
# 欧几里得距离公式:sqrt(Σ(x1-x2)²)
distances = np.sqrt(np.sum((test_mat - data_set) ** 2, axis=1))
# 2. 按距离从小到大排序,取前K个样本的索引
sorted_dist_indices = distances.argsort()
# 3. 对K个近邻的类别投票(统计出现次数)
class_count = {}
for i in range(k):
# 获取第i个近邻的类别
vote_label = labels[sorted_dist_indices[i]]
# 计数:类别出现一次就加1
class_count[vote_label] = class_count.get(vote_label, 0) + 1
# 4. 按投票次数排序,返回次数最多的类别
# 用sorted排序,key取字典值,逆序(从大到小)
sorted_class_count = sorted(class_count.items(), key=lambda x: x[1], reverse=True)
predicted_label = sorted_class_count[0][0]
return predicted_label
(3)测试函数:模拟真实场景
def test_dating_classifier():
"""
测试函数:生成模拟数据,运行分类器并输出结果
"""
# 1. 1. 模拟训练数据(10个样本,3个特征+1个标签)
# 特征:[飞行里程数, 玩游戏时间%, 冰淇淋消费量]
train_data = np.array([
[400, 0.8, 0.5],
[134000, 12, 0.9],
[20000, 0, 1.1],
[32000, 67, 0.1],
[65000, 20, 0.5],
[54000, 40, 1.0],
[10000, 30, 0.3],
[80000, 15, 0.8],
[75000, 50, 0.2],
[90000, 25, 0.7]
])
# 标签:1=不喜欢,2=一般,3=非常喜欢
train_labels = np.array([1, 3, 2, 2, 3, 2, 1, 3, 2, 3])
# 2. 对训练数据归一化
normalized_train, min_vals, ranges = normalize_data(train_data)
# 3. 模拟测试样本(比如:飞行10000里程,玩游戏10%,吃0.6L冰淇淋)
test_sample = np.array([[10000, 10, 0.6]])
# 4. 测试样本也需要归一化(用训练集的min和range,避免数据泄露)
normalized_test = (test_sample - min_vals) / ranges
# 5. 调用K近邻分类(K取3,奇数避免平局)
k = 3
predicted_label = knn_classify(normalized_test, normalized_train, train_labels, k)
# 6. 输出结果(转换为海伦能理解的语言)
label_map = {1: "不喜欢的人(不推荐)", 2: "一般喜欢的人(可推荐)", 3: "非常喜欢的人(强烈推荐)"}
print(f"测试样本特征:飞行{test_sample[0][0]}里程,玩游戏{test_sample[0][1]}%,冰淇淋{test_sample[0][2]}L")
print(f"分类结果:{label_map[predicted_label]}")
# 运行测试
if __name__ == "__main__":
test_dating_classifier()
六、优化方向:让分类器更实用
K 值选择:目前 K=3 是经验值,可通过 “交叉验证” 选最优 K(比如用 5 折交叉验证,测试 K=1/3/5/7,选准确率最高的 K);
距离度量:除了欧几里得距离,还可尝试 “曼哈顿距离”(适合高维数据);
真实数据接入:将模拟数据换成 UCI 公开的 “约会数据集”(或海伦的真实数据),用pandas读取 CSV 文件,替换train_data和train_labels;
可视化:用matplotlib绘制特征散点图(比如飞行里程 vs 游戏时间,用颜色区分类别),直观展示数据分布。
总结
本文通过 “约会分类” 场景,带你落地了 K 近邻算法的核心流程:数据归一化→距离计算→K 近邻投票→结果输出。关键在于理解 “为什么要归一化” 和 “K 值的影响”—— 这两个点也是面试中常考的基础知识点。
更多推荐

所有评论(0)