机器学习课程学习——K近邻算法实现
一、关于KNN算法
KNN(K Near Neighbor):k个最近的邻居,即每个样本都可以用它最接近的k个邻居来代表。KNN算法属于监督学习方式的分类算法,我的理解就是计算某给点到每个点的距离作为相似度的反馈。
KNN就是“近朱者赤,近墨者黑”的一种分类算法。
KNN是一种基于实例的学习,属于懒惰学习,即没有显式学习过程。
KNN 原理
- 假设有一个带有标签的样本数据集(训练样本集),其中包含每条数据与所属分类的对应关系。
- 输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较。
- 计算新数据与样本数据集中每条数据的距离。
- 对求得的所有距离进行排序(从小到大,越小表示越相似)。
- 取前 k (k 一般小于等于 20 )个样本数据对应的分类标签。
- 求 k 个数据中出现次数最多的分类标签作为新数据的分类。
简单来说 K-近邻算法采用测量不同特征值之间的距离方法进行分类。
距离计算
在KNN中,通过计算对象间距离来作为各个对象之间的非相似性指标,避免了对象之间的匹配问题,在这里距离一般使用欧式距离或者曼哈顿距离:


二、问题引入
海伦一直使用在线约会网站寻找适合自己的约会对象。她曾交往过三种类型的人:
- 不喜欢的人
- 一般喜欢的人
- 非常喜欢的人
这些人包含以下三种特征:
1. 每年获得的飞行的客里程数
2. 玩视频游戏所耗时间百分比
3. 每周消费的冰淇淋公升数
该网站现在需要尽可能向海伦推荐她喜欢的人,需要我们设计一个分类器,根据用户的以上三种特征,识别出是否该向海伦推荐。
三、需求概要分析
根据问题,我们可知,样本特征个数为3,样本标签为三类。现需要实现将一个待分类样本的三个特征值输入程序后,能够识别该样本的类别,并且将该类别输出。
四、程序设计和使用工具导入
1.程序流程设计思路
(1)实现K近邻分类器并训练模型存储数据
(2)生成模拟的约会数据集
(3)对生成的数据集进行可视化处理
(4)计算最佳K值和准确率
(5)对K值和准确率的关系进行可视化处理
(6)设计几个样本测试
(7)对用户输入进行预测
2.使用工具包导入及其作用
(1)numpy:负责底层的数据存储和数学计算
import numpy as np
(2) matplotlib相关包:负责结果的可视化展示
import matplotlib.pyplot as plt
(3)operator和collections:负责算法中的排序和统计功能
import operator
from collections import Counter
3.在设计过程中可能遇到的问题
可视化处理后的数据图像不能正确显示中文
显示文字为方格乱码,推测为字体问题
解决方式:导入 platform 包(负责系统兼容性处理)并设置中文字体
import platform
def setup_chinese_font():
"""设置中文字体"""
try:
# 根据操作系统设置字体
system = platform.system()
if system == "Windows":
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
elif system == "Darwin": # macOS
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Heiti TC']
else: # Linux
plt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
return True
except:
return False
五、部分功能展示
生成模拟的约会数据集
数据生成
模拟真实数据分布,为算法提供训练和测试数据
通过设置不同的均值和方差,创建可区分的类别特征
为后续的特征归一化和距离计算奠定基础
def load_dating_data():
"""生成模拟的约会数据集"""
np.random.seed(42)
n_samples = 300
# 不喜欢的人(类别1)
X1 = np.column_stack([
np.random.normal(10000, 3000, n_samples // 3), # 飞行里程较少
np.random.normal(15, 5, n_samples // 3), # 游戏时间中等偏多
np.random.normal(0.3, 0.1, n_samples // 3) # 冰淇淋消费较少
])
y1 = np.ones(n_samples // 3) # 类别1
# 一般喜欢的人(类别2)
X2 = np.column_stack([
np.random.normal(50000, 15000, n_samples // 3), # 飞行里程中等
np.random.normal(8, 3, n_samples // 3), # 游戏时间中等
np.random.normal(0.6, 0.2, n_samples // 3) # 冰淇淋消费中等
])
y2 = np.ones(n_samples // 3) * 2 # 类别2
# 非常喜欢的人(类别3)
X3 = np.column_stack([
np.random.normal(120000, 40000, n_samples // 3), # 飞行里程较多
np.random.normal(3, 2, n_samples // 3), # 游戏时间较少
np.random.normal(1.0, 0.3, n_samples // 3) # 冰淇淋消费较多
])
y3 = np.ones(n_samples // 3) * 3 # 类别3
# 合并数据
X = np.vstack([X1, X2, X3])
y = np.hstack([y1, y2, y3])
# 打乱数据
indices = np.random.permutation(len(X))
X = X[indices]
y = y[indices]
return X, y
K近邻分类器
KNN分类器类
包含初始化,特征归一化,距离计算,单样本预测
归一化处理:确保特征尺度一致
距离计算:找到最相似的样本
邻居选择:选取距离最小的K个样本
多数投票:根据邻居的类别决定预测结果
class KNNClassifier:
"""K近邻分类器实现"""
def __init__(self, k=3):
"""
初始化KNN分类器
参数:
k: 最近邻居的数量,默认为3
"""
self.k = k
self.X_train = None
self.y_train = None
self.feature_ranges = None
def fit(self, X, y):
"""
训练模型(KNN没有显式的训练过程,只是存储数据)
参数:
X: 训练特征,形状为(n_samples, n_features)
y: 训练标签,形状为(n_samples,)
"""
self.X_train = np.array(X)
self.y_train = np.array(y)
# 计算特征范围用于归一化
self.feature_ranges = {
'min': np.min(self.X_train, axis=0),
'max': np.max(self.X_train, axis=0)
}
def normalize_features(self, X):
"""
归一化特征值到0-1范围
参数:
X: 需要归一化的特征数据
返回:
归一化后的特征数据
"""
X_normalized = np.zeros(X.shape)
for i in range(X.shape[1]):
min_val = self.feature_ranges['min'][i]
max_val = self.feature_ranges['max'][i]
if max_val - min_val == 0: # 避免除零
X_normalized[:, i] = 0
else:
X_normalized[:, i] = (X[:, i] - min_val) / (max_val - min_val)
return X_normalized
def euclidean_distance(self, x1, x2):
"""
计算两个样本之间的欧几里得距离
参数:
x1, x2: 两个样本的特征向量
返回:
欧几里得距离
"""
return np.sqrt(np.sum((x1 - x2) ** 2))
def predict_single(self, x):
"""
预测单个样本的类别
参数:
x: 单个样本的特征向量
返回:
预测的类别
"""
# 归一化输入样本
x_normalized = self.normalize_features(np.array([x]))[0]
# 计算与所有训练样本的距离
distances = []
for i, train_sample in enumerate(self.X_train):
train_sample_normalized = self.normalize_features(np.array([train_sample]))[0]
dist = self.euclidean_distance(x_normalized, train_sample_normalized)
distances.append((dist, self.y_train[i]))
# 按距离排序并选择K个最近邻居
distances.sort(key=operator.itemgetter(0))
k_nearest = distances[:self.k]
# 统计K个最近邻居的类别
k_labels = [label for (_, label) in k_nearest]
most_common = Counter(k_labels).most_common(1)
return most_common[0][0]
def predict(self, X):
"""
预测多个样本的类别
参数:
X: 需要预测的样本特征,形状为(n_samples, n_features)
返回:
预测的类别数组
"""
predictions = []
for sample in X:
predictions.append(self.predict_single(sample))
return np.array(predictions)
def accuracy(self, X_test, y_test):
"""
计算模型在测试集上的准确率
参数:
X_test: 测试特征
y_test: 测试标签
返回:
准确率
"""
predictions = self.predict(X_test)
accuracy = np.sum(predictions == y_test) / len(y_test)
return accuracy
关于归一化
归一化特征值,消除特征之间量级不同导致的影响。
在统计学中,归一化的具体作用是归纳统一样本的统计分布性。归一化在0-1之间是统计的概率分布,归一化在-1–+1之间是统计的坐标分布。
这里使用的是最小-最大归一化,即 v_new = (v_old - min) / (max - min)
关于K值设定:
K值设定为多大?
K太小,分类结果易受噪声点影响;K太大,近邻中又可能包含太多的其他类别的点。(对距离加权,可以降低K值设定的影响)
K值通常是采用交叉检验来确定,平衡偏差和方差
def find_best_k(X_train, y_train, X_test, y_test, k_range=range(1, 21)):
"""寻找最佳的K值"""
best_k = 1
best_accuracy = 0
accuracies = []
for k in k_range:
knn = KNNClassifier(k=k)
knn.fit(X_train, y_train)
accuracy = knn.accuracy(X_test, y_test)
accuracies.append(accuracy)
if accuracy > best_accuracy:
best_accuracy = accuracy
best_k = k
return best_k, best_accuracy
可视化处理
数据分布可视化
直观展示数据的可分性
帮助理解特征与类别之间的关系
验证特征工程的有效性
def visualize_data(X, y):
"""可视化数据集"""
plt.figure(figsize=(15, 5))
# 类别标签映射
labels = {1: '不喜欢', 2: '一般喜欢', 3: '非常喜欢'}
colors = {1: 'red', 2: 'orange', 3: 'green'}
# 特征1 vs 特征2
plt.subplot(1, 3, 1)
for label in [1, 2, 3]:
mask = y == label
plt.scatter(X[mask, 0], X[mask, 1],
c=colors[label], label=labels[label], alpha=0.6)
plt.xlabel('飞行里程数')
plt.ylabel('游戏时间百分比')
plt.legend()
plt.title('飞行里程 vs 游戏时间')
# 特征1 vs 特征3
plt.subplot(1, 3, 2)
for label in [1, 2, 3]:
mask = y == label
plt.scatter(X[mask, 0], X[mask, 2],
c=colors[label], label=labels[label], alpha=0.6)
plt.xlabel('飞行里程数')
plt.ylabel('冰淇淋消费量')
plt.legend()
plt.title('飞行里程 vs 冰淇淋消费')
# 特征2 vs 特征3
plt.subplot(1, 3, 3)
for label in [1, 2, 3]:
mask = y == label
plt.scatter(X[mask, 1], X[mask, 2],
c=colors[label], label=labels[label], alpha=0.6)
plt.xlabel('游戏时间百分比')
plt.ylabel('冰淇淋消费量')
plt.legend()
plt.title('游戏时间 vs 冰淇淋消费')
plt.tight_layout()
plt.show()

包括在最佳K值中的关系图实现
# 绘制K值与准确率的关系图
plt.figure(figsize=(10, 6))
plt.plot(k_range, accuracies, 'bo-', linewidth=2, markersize=8)
plt.xlabel('K值')
plt.ylabel('准确率')
plt.title('K值与分类准确率的关系')
plt.grid(True, alpha=0.3)
plt.xticks(k_range)
plt.show()

六、KNN优缺点及适用场景
优点
简单直观:易于理解和实现
无需训练:直接存储数据,新数据可随时加入
适应性强:适用于多分类和复杂决策边界
可解释性好:预测结果可通过最近邻居解释
缺点
计算量大:预测时需要计算所有训练样本距离
内存消耗高:需要存储全部训练数据
对特征尺度敏感:必须进行特征归一化
K值选择困难:需要交叉验证确定最佳K值
适用场景
多分类问题
稀有事件分类问题
文本分类问题
模式识别
聚类分析
样本数量较少的分类问题
关键要点
-
必须特征归一化 - 避免某些特征主导距离计算
-
K值需要调优 - 平衡过拟合和欠拟合
-
适合中等规模数据 - 在数据量适中时效果最佳
-
重视可解释性 - 在需要透明决策的场景有优势
站内参考链接
更多推荐
所有评论(0)