机器学习:kNN算法(一)—— 原理与代码实现(不调用库)
一 理论基础
- \(k\)近邻法是一种基本地分类与回归算法,属于判别模型。没有学习策略,不具备显式学习过程。本文主要讨论分类问题。
- 原理:给定一个训练数据集,对于新的输入实例,在训练数据集中找到与该实例最邻近的\(k\)个点,这\(k\)个点的多数属于某个类,就把输入实例归为这个类。
- 三个基本要素:\(k\)值得选择,距离度量以及分类决策规则
1.超参数k:选择较小的k值,对噪声比较敏感。意味着整体模型变得复杂,容易过拟合;选择较大的k值,较远的实例也会对预测起作用,使预测发生错误。意味着模型变得简单。
2.距离度量:距离是空间两个实力点相似程度的反映。\(\mathbf{x}_i=(x_i^{(1)},x_i^{(2)},...,x_i^{(n)})^T,\mathbf{x}_j=(x_j^{(1)},x_j^{(2)},...,x_j^{(n)})^T\),距离定义如下:$$\mathit{L_p}=(\sum_{l=1}{n}|x_i-x_j{(l)}|p)^{\frac{1}{p}}$$
p=1时称为曼哈顿距离,p=2时称为欧氏距离。
3.分类决策规则:一般采用多数表决。
二 代码实现
- 编写的分类器实现功能:超参数k,超参数p,拟合训练集,对新的数据进行分类,对分类结果计算精度。感兴趣的还可以加入超参数距离的权重以及kd树,球树等实现方法。这里不再赘述。
- KNN算法实现代码如下
class KNNClassify():
def __init__(self,k=5, p=2):
self.k = k
self.p = p
self._X_train = None
self._y_train = None
def fit(self, X_train, y_train):
self._X_train = X_train
self._y_train = y_train
return self
def predict_y(self, X_test):
m = self._X_train.shape[0]
y_pre = []
for intX in X_test:
minus_mat = np.fabs(np.tile(intX, (m, 1)) - self._X_train) # 将新的实例复制成m行1列,并进行相减
sq_minus_mat = minus_mat ** self.p
sq_distance = sq_minus_mat.sum(axis=1)
diff_sq_distance = sq_distance ** float(1/self.p)
sorted_distance_index = diff_sq_distance.argsort() # 记录距离最近的k个点的索引
class_count = {}
vola = []
for i in range(self.k):
vola = self._y_train[sorted_distance_index[i]]
class_count[vola] = class_count.get(vola, 0) + 1 # 统计k个点中所属各个类别的实例数目
sorted_class_count = sorted(class_count.items(), key=operator.itemgetter(1), reverse=True) # 返回列表,元素为元组。每个类别以及对应的实例数目
y_pre.append((sorted_class_count[0][0]))
return (np.array(y_pre))
def score(self, X_test, y_test):
j = 0
for i in range(len(self.predict_y(X_test))):
if self.predict_y(X_test)[i] == y_test[i]:
j += 1
return ('accuracy: {:.10%}'.format(j / len(y_test)))
- 下面以鸢尾花数据集来评估编写的分类器效果
import numpy as np
import operator
from sklearn import datasets
from sklearn.model_selection import train_test_split
# 获取数据集,并进行8:2切分
iris = datasets.load_iris()
X = iris.data
y = iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# 定义分类器的实例,并进行拟合预测
f = KNNClassify()
f.fit(X_train, y_train)
y_pre = f.predict_y(X_test)
accuracy = f.score(X_test, y_test)
print(y_test)
print(y_pre)
print(accuracy)
-
输出结果如下,可以看出分类效果还可以。
[1 2 2 2 0 1 2 0 0 0 1 2 0 0 2 1 2 0 2 2 2 2 0 2 1 1 0 2 0 1]
[1 2 2 2 0 1 2 0 0 0 1 2 0 0 2 1 1 0 2 2 2 2 0 2 1 1 0 2 0 1]
accuracy: 96.6666666667% -
需要注意的是KNN由于与距离有关,如果某个特征数值相比其他特征较大大,会对距离产生很大的影响。例如具有实例x1=[1 3 1000 2],x2=[1.5 4 1100 2],计算距离时第三个特征差基本起到了主导作用,弱化了其他特征。为避免这种情况,应先对数据进行预理。
-
预处理方法。关于标准化与归一化,不同的人有不同看法,这里以sklearn为准,标准化对象为特征,归一化对象为样本,当然两者实质都是对训练集数据的改变。可以参考[http://sklearn.apachecn.org/cn/0.19.0/modules/preprocessing.html#preprocessing-scaler]
1.标准化(对特征),也称去均值和方差按比例缩放。数据集的标准化对scikit-learn中实现的大多数机器学习算法来说是常见的要求 。(对象为特征,依赖于训练集中所有样本)- 如果个别特征或多或少看起来不是很像标准正态分布(具有零均值和单位方差),那么它们的表现力可能会较差。在实际情况中,我们经常忽略特征的分布形状,直接减去均值来对某个特征进行中心化,再通过除以非常量特征(non-constant features)的标准差进行缩放。\[x=\frac{x-\bar{x}}{\sigma } \]
- 将特征缩放到给定的区间,通常为[0,1]。或者也可以将每个特征的最大值转换至单位大小,它通过除以每个特征的最大值将训练数据特征缩放至 [-1, 1] 范围内,这就意味着训练数据应该是已经零中心化或者是稀疏数据。两者分别使用MinMaxScaler和MaxAbsScaler实
现。对于两者计算公式分别为$$x=\frac{x-min(x)}{max(x)-min(x)},x=\frac{x}{max(x)}$$
2.归一化(对样本)。归一化 是缩放单个样本以具有单位范数的过程。注意对象为样本而非某个特征!!通常使用 L1 或 L2 范数。例如实例\(\mathbf{x}_1=(1,2,2)^T\),归一化后\(\mathbf{x}_1= (0.33,0.66,0.66)^T\)。不依赖于其他样本特征数值。
3.二值化。
4.非线性变换
5.特征编码L
6.缺失值插补 - 如果个别特征或多或少看起来不是很像标准正态分布(具有零均值和单位方差),那么它们的表现力可能会较差。在实际情况中,我们经常忽略特征的分布形状,直接减去均值来对某个特征进行中心化,再通过除以非常量特征(non-constant features)的标准差进行缩放。
-
由此可以看出,对于kNN而言,当某个特征的差值相比其他特征较大时,可以采用标准化,以避免某个特征对距离影响。如果进行归一化,虽然也是改变了特征取值区间,但是特征间量级关系依然会相差很大。
-
当我们得出一个算法模型后,如何评判模型的好坏呢?包括许多性能度量方法,例如精度与错误率、查准率查全率与F1、ROC与AUC等等。假如我们选择精度作为衡量指标,我们采用多次留出法或者p次k折交叉验证,然后将各个精度取均值作为此模型的评估指标。
1.留出法。将数据集分成互斥的训练集与测试集,可以得出一个精度。因为划分方式存在多种方式,因此使用留出法时,采用若干次随机划分,重复进行实验取平均值作为留出法评估结果。
2.交叉验证。将数据划分成k个互斥大小相似的集合,每次采用k-1个作为训练集,剩下一个作为测试集,总共可进行k次,最终返回k次结果的均值。同样由于划分方式不同,可进行p次划分,每次都进行k折交叉验证,取最后均值作为评估结果,称为p次k折交叉验证。