|
### KNN 算法思想
> "走像鸭子,叫像鸭子,看起来也像鸭子,很可能就是一只鸭子。"
KNN(K-Nearest Neighbor)是一种经典的**分类算法**,1968 年由 Cover 和 Hart 提出。
**核心思想**:每个样本都可以用它最接近的 **k** 个邻居来代表。在特征空间中的 k 个最相似样本中的大多数属于某一类别,则该样本也属于这个类别。
### 算法流程
1. **计算距离** — 计算已知类别数据集中的点与当前点之间的距离
2. **排序** — 按距离递增次序排序
3. **选近邻** — 选取与当前点距离最小的 k 个点
4. **统计频率** — 统计前 k 个点所在的类别出现的频率
5. **投票决策** — 返回前 k 个点出现频率最高的类别作为预测分类
### sklearn API
```python
from sklearn.neighbors import KNeighborsClassifier
class sklearn.neighbors.KNeighborsClassifier(
n_neighbors=5, # 默认邻居数 k=5
weights='uniform', # 权重:uniform 或 distance
algorithm='auto', # 近邻搜索算法
leaf_size=30,
p=2, # 闵氏距离的幂参数:p=2 为欧氏距离
metric='minkowski', # 距离度量方式
)
```
### 优缺点
**优点**:简单有效、重新训练代价低、无显式训练过程、适合类域交叉样本
**缺点**:
| 缺点 | 说明 | 改进方法 |
|------|------|---------|
| **惰性学习** | 训练过程中不学习判别式函数 | — |
| **不均衡性** | 样本不平衡时大容量类占多数 | 采用权值法 |
| **计算量较大** | 预测时计算量大 | KD-树等数据结构 |
### K 值的选择
- **K 值过小**:模型变复杂,容易过拟合
- **K 值 = N(样本总数)**:无论输入什么都预测为最多类,失去意义
|
|
### 欧氏距离(Euclidean Distance)
二维平面点 a(x₁, y₁) 与 b(x₂, y₂) 间:
```
d = √((x₁ - x₂)² + (y₁ - y₂)²)
```
n 维空间:
```
d = √(Σ(x₁ᵢ - x₂ᵢ)²)
```
### 曼哈顿距离(Manhattan Distance)
城市街区距离:
```
d = |x₁ - x₂| + |y₁ - y₂|
```
### 切比雪夫距离(Chebyshev Distance)
棋盘距离:
```
d = max(|x₁ - x₂|, |y₁ - y₂|)
```
### 闵可夫斯基距离(Minkowski Distance)
一组距离的概括性定义:
```
d = (Σ|x₁ᵢ - x₂ᵢ|ᵖ)^(1/p)
```
- p = 1:曼哈顿距离
- p = 2:欧氏距离
- p → ∞:切比雪夫距离
### 其他距离
| 距离 | 特点 |
|-----|------|
| **马氏距离** | 基于样本分布,量纲无关,排除变量相关性干扰 |
| **标准化欧氏距离** | 各维度标准化后再计算欧氏距离 |
| **汉明距离** | 等长字符串间最小替换次数 |
|
|
计算预测元素与所有训练集样本之间的距离,参考前n个最近距离的样本,预测该样本
# 计算距离,每个样本与所有样本的距离
res = []
for x in X:
# 在axis=1列的方向上相加,每个样本得到一个结果
distance = np.sqrt(((self.X - x)**2).sum(axis=1))
idxes = np.argsort(distance)[:self.n_neighbors]
res.append(np.round(a=self.y[idxes].mean(),decimals=3))
|
|
KNN的核心思想是 环境决定个体
比如,
你是一个Java开发,三年工作经验,有个人想知道你的工资,
他不需要问你,只需要知道这一个类多少工资就可以了
又比如,你去相亲,对方说出她的职业,你就知道他的很多信息...
你周围的人不是在搬砖,就是在拉车,一年365天只有过年休息几天,你估计也是如此
你周围的人活的如牛马,勤勤恳恳地劳动,你估计也是
你周围的人都是坐办公室的,你估计也是
你周围的人非富即贵,你估计也是
你周围的人谈笑有鸿儒,往来无白丁,你估计也是
|
|
环境决定个体,通过分析环境确定个本的性质,这种思想是伟大的
但不是完美的,缺少不维度之间的比较方法
向量内积运算,各个维度上的元素相乘之后,最终相加之后是要化为一个数的
比如,谈对象,有 身高,年龄,相貌,潜力,资产 五项
身高之间的波动一般不超过1米,
资产之间可能有上万的差距,
KNN是计算距离,这种情况下,资产波动几十块钱,就淹没了身高的波动,这不合理
这就是出现了两类解决办法
百分比
不同维度之间使用百分比进行比较,
还是归一化的思想,但百分比是更贴切的描述
所有维度的最大值化为1,这种每个元素值的天花板是一样的,
重在一样/差不多,可以是1.2,1.7...
不要出现一个维度波动可以淹没其他维度变化的情况,
让每个维度的波动维持在一个相对统一的量纲上
此时,针对如何归一的细节,普遍有两个方法:
1. 每个元素除以集合中的最大值,批归一化
2. 一个样本中的元素除以这个样本的最大值,层归一化
3. 标准化,减均值,除以标准差,使得元素之间的平均距离化为1,可以针对全体也可以针对某个样本
4. 概率化,
就是将一个样本看作一个概率,不同的特征就是事件,这通常是分类的最后一步,
比如,n分类问题,最终是分为n个类别,不是这一类就是那一类,必定是一种一个类别,
它们地位同等(因为数据比例等比),概率之和为1,也就是某个样本,必是n分类中有的一类
通常的实现方式是softmax,这也是为什么softmax会放在整个算法的最后一步的原因
这些都是归一化的思路,百分比更合适的一些,
但百分比大家想到的是最后之和是1,就是概率了,
但在AI中都是近似计算,
只要能展现出差异即可,所以最终结果不是1也没关系的,可以是90%,也可以是170%,差不多就行
总体来说,就是将所有元素拉到同一个量纲,通常是1附近,
这样,元素之间的比较,就可以跨维度进行对比,
在同一量纲上,差异越大,越好分类
深度学习之间有多个网络,可以理解为刚开始的时候,
有的元素之间相差0.5%,
到最后一层网络的时候,元素之间出现200%的差异,这就好分类
比如,一个水果,51%像苹果,49%像梨,
另外一种情况是,10%像苹果,120%像梨,显示这种情况更好区分一些
注意,这里再次强调了AI是近似,虽然10%+120% !=1,违反了概率的定义,
但也是有用的,表达了后者更像梨的倾向
还有另外一个思路,加权
|
|
身高,年龄,相貌,潜力,资产,情绪价值 六项进行综合比较,选出一个你满意的
不同维度之间整合,就是各个维度计算之后 相加,没错,就是加法
加法,相加,相融,就代表整合/融合
比如,资产变化会覆盖身高的变化,
那可不可以在资产前面乘一个系数,使资产的变化与身高的变化处于一个量级上?
可以,这就是权加,为每个元素都加一个系数,让所有维度处于一个量级上
接下来的问题是,如何计算这个系数?
人算是不可能了,由计算机算,
可以理解为计算机进行大量的尝试,最终以结果为导向选择了合适的系数
但总体来说,前提让各个维度的量纲处于同一水平上,对后面的计算是有益的
|
|
AI重全局,不重个体
生的是全局的差异,即一个样本在全体数据集中的位置
是相对位置
是百分比
而KNN,强调的是与自己最近的,最相似的样本,取n个
就是要在全体数据集中找到自己的位置
遍历计算距离,计算距离是过程,目的在于找到离自己最近的样本,
那就是自己在整体中的位置...
|
房价:回归问题
|
import numpy as np
class MyKNNRegressor():
def __init__(self,n_neighbors=5):
"""
自定义KNN算法
"""
self.n_neighbors = n_neighbors
def fit(self, X, y):
"""
本质什么也没有做,但可以做一次排序优化工作
"""
self.X = np.array(X)
self.y = np.array(y)
def predict(self, X):
"""
求距离的目的是为了找到距离最近的样本的值
求距离只是一个过程
"""
X = np.array(X)
if X.ndim !=2:
raise Exception("默认是2维结构,批量预测")
# 计算距离,每个样本与所有样本的距离
res = []
for x in X:
# 在axis=1列的方向上相加,每个样本得到一个结果
distance = np.sqrt(((self.X - x)**2).sum(axis=1))
idxes = np.argsort(distance)[:self.n_neighbors]
res.append(np.round(a=self.y[idxes].mean(),decimals=3))
return np.array(res)
knn_my = MyKNNRegressor()
knn_my.fit(X=X_train,y=y_train)
y_test_pred2 = knn_my.predict(X=X_test)
mse = ((y_test_pred2 - y_test)**2).mean()
mse
21.96151578947368
y_test_pred2[:7]
array([23.56, 24.7 , 40.6 , 15.16, 18.04, 19.7 , 18.02])
|
|
def fit(self, X, y):
"""
本质什么也没有做,但可以做一次排序优化工作
"""
self.X = np.array(X)
self.y = np.array(y)
KNN的训练代码没有处理逻辑,直接在处理逻辑在预测中
将预测时,即时计算
如果数据量大,比较慢
-----------------------------------------------------------
|
|
def fit(self, X, y):
这里的X,y就是超参数,参数就参数了,为什么带个超?
这是因为在“参数”一词在AI中特指算法内部的参数,
而X,y是外部输入的,就再起一个名字,叫超参数
当你告诉不懂AI的人,说你要进行超参数优化,
听起来是不是比参数优化高级那么一点点...
----------------------------------------------------------------
|
|
import numpy as np
class KNNCorr():
def __init__(self,n_neighbors=1):
"""相似推荐,从指定数据集寻找最自己最接受的数据
"""
self.n_neighbors = n_neighbors
def fit(self, X=None):
"""目标数据,训练数据
- 以训练
"""
self.X = X
# 在fit方法中预存转置矩阵
# self.X_T = self.X.T.copy(order='C') # C连续内存布局
def predict(self, X):
"""预测
- 预测目标数据中与自己相似度最高的数据的索引
"""
X = np.array(X)
if X.ndim !=2:
raise Exception("默认是2维结构,批量预测")
# 计算距离,每个样本与所有样本的距离
res = []
# 将for循环替换为矢量化计算
# 在数据的第2维,即Index=1的位置上增加一维,index=0上代表批次维度,增加一维后,表示每个元素是(1,3)的矩阵,
# 减X时,会先进行广播,[b,1,n] -- [b,m,n] m为X的元素个数,即self.X中的每个元素会先自复制到与X同元素个数
distance_matrix = np.sqrt(((X[:, np.newaxis] - self.X)**2).sum(axis=2))
index = distance_matrix.argmin(axis=1)
return index
import numpy as np
np.random.seed(42)
X = np.random.rand(7,3)
X_test =X[:3]
y_index = model.predict(X_test)
X[y_index]
array([[0.37454012, 0.95071431, 0.73199394],
[0.59865848, 0.15601864, 0.15599452],
[0.05808361, 0.86617615, 0.60111501]])
X_test
array([[0.37454012, 0.95071431, 0.73199394],
[0.59865848, 0.15601864, 0.15599452],
[0.05808361, 0.86617615, 0.60111501]])
|
|
|
|
from tpf import load_boston
X_train, y_train, X_test, y_test = load_boston(split=True,test_size=0.15)
import numpy as np
class MyKNNRegressor2():
"""改变距离的度量方法
- 没有开平方
"""
def __init__(self,n_neighbors=5):
"""
自定义KNN算法
"""
self.n_neighbors = n_neighbors
def fit(self, X, y):
"""
本质什么也没有做,但可以做一次排序优化工作
"""
self.X = np.array(X)
self.y = np.array(y)
def predict(self, X):
"""
求距离的目的是为了找到距离最近的样本的值
求距离只是一个过程
"""
X = np.array(X)
if X.ndim !=2:
raise Exception("默认是2维结构,批量预测")
# 计算距离,每个样本与所有样本的距离
res = []
for x in X:
# 在axis=1列的方向上相加,每个样本得到一个结果
distance = ((self.X - x)**2).sum(axis=1)
idxes = np.argsort(distance)[:self.n_neighbors]
res.append(np.round(a=self.y[idxes].mean(),decimals=3))
return np.array(res)
knn_my = MyKNNRegressor2()
knn_my.fit(X=X_train,y=y_train)
y_test_pred2 = knn_my.predict(X=X_test)
mse = ((y_test_pred2 - y_test)**2).mean()
mse
21.96151578947368
|
|
class MyKNNRegressor3():
"""改变距离的度量方法
- 没有开平方
"""
def __init__(self,n_neighbors=5):
"""
自定义KNN算法
"""
self.n_neighbors = n_neighbors
def fit(self, X, y):
"""
本质什么也没有做,但可以做一次排序优化工作
"""
self.X = np.array(X)
self.y = np.array(y)
def predict(self, X):
"""
求距离的目的是为了找到距离最近的样本的值
求距离只是一个过程
"""
X = np.array(X)
if X.ndim !=2:
raise Exception("默认是2维结构,批量预测")
# 计算距离,每个样本与所有样本的距离
res = []
for x in X:
# 在axis=1列的方向上相加,每个样本得到一个结果
distance = (np.abs((self.X - x))).sum(axis=1)
idxes = np.argsort(distance)[:self.n_neighbors]
res.append(np.round(a=self.y[idxes].mean(),decimals=3))
return np.array(res)
knn_my = MyKNNRegressor3()
knn_my.fit(X=X_train,y=y_train)
y_test_pred2 = knn_my.predict(X=X_test)
mse = ((y_test_pred2 - y_test)**2).mean()
mse
18.638584210526318
平方与不平方没有任何的区别
而L1距离与L2距离有所区别,从KNN的效果来看,L1距离比L2距离的MSE更小一些
|
|
距离的计算不是目的,是一个过程,
计算距离是为了找出离目标最近的n个样本
找最近的n个样本才是目的
如此,达到同样的目的,就可以有多种方案选择
一段代码要注意分析该代码的目的是什么?
哪些只是计算的一种方式,只是一个过程
要区分过程和目的...
-----------------------------------------------------------------------
|
|
|
|
|