上一节我们完成了 KNN 算法理论的学习,同时还手动写了一个简易版的 KNN 分类器。今天我们来进行 KNN 的实战,看看如何通过 KNN 算法来解决生活中的问题。
在实战之前,我们先来介绍一个概念-超参数。
还记得我们上一节讲到的选择 K 值吗,这里的 K 就是超参。
所谓超参数,就是在机器学习算法模型执行之前需要指定的参数。(调参调的就是超参数) 如KNN 算法中的 K。
与之相对的概念是模型参数,即算法过程中学习的属于这个模型的参数(KNN 中没有模型参数,回归算法有很多模型参数)
如何选择超参数,是机器学习中的永恒问题。调参的方法也很多,例如上节我们提到的交叉验证法。
下面我们就进入实战例子,看看如何用 KNN 算法来解决生活中的问题。
在 sklearn 中使用 KNN
上一节我只是简单的介绍了 sklearn,并创建了一个 KNN 的分类器,今天我们就具体来看看如何使用 sklearn 中的 KNN 分类器。
上节我们已经知道,要想使用 sklearn 的 KNN 算法,我们要先导入相关的包
from sklearn.neighbors import KNeighborsClassifier
然后再实例化该类即可
knn = KNeighborsClassifier()
KNeighborsClassifier 类是有几个构造参数的,我们来逐一看下
- n_neighbors:默认值是5,即为 K 的值
- weights:权重值,默认为uniform,取值可以是 uniform、distance,也可以是用户自己定义的函数。uniform 是均等的权重,就说所有的邻近点的权重都是相等的。distance 是不均等的权重,距离近的点比距离远的点的影响大。
- algorithm:快速 K 近邻搜索算法,默认参数为 auto,可以理解为算法自己决定合适的搜索算法。此外我们也可以自行指定搜索算法 ball_tree、kd_tree、brute 等,一般使用默认即可。
fit 和 predict 函数
fit 函数是用来通过特征矩阵,分类标识,让分类器进行拟合,如:
knn.fit(X_train, y_train)
predict 函数用于返回预测结果,如:
predict_y = knn.predict(X_test)
了解了如何在 sklearn 中使用 KNN 后,我们再通过两个例子,来加深理解。
电影分类
本节所有数据集
先导入数据集,查看数据集整体概况
import pandas as pd df = pd.read_csv("movie_dataset.txt") print(df.info()) print(df.head()) >>> <class 'pandas.core.frame.DataFrame'> RangeIndex: 31 entries, 0 to 30 Data columns (total 4 columns): movie_name 31 non-null object fighting_lens 31 non-null int64 kissing_lens 31 non-null int64 movie_types 31 non-null object dtypes: int64(2), object(2) memory usage: 808.0+ bytes None movie_name fighting_lens kissing_lens movie_types 0 龙争虎斗 101 0 动作 1 蜀山 120 2 动作 2 致青春 3 30 爱情 3 这个杀手不太冷 78 2 动作 4 白蛇 20 50 爱情
数据集中总共有31条数据,且并不存在缺失值。
下面开始对数据集做一些转换
电影类型转换
电影类型是文字描述的,这不利于我们判别分类,可以将“动作”替换为0,“爱情”替换为1。
def trans(x): if x == '动作': x = 0 else: x = 1 return x df['movie_types'] = df['movie_types'].apply(trans) print(df.head()) >>> movie_name fighting_lens kissing_lens movie_types 0 龙争虎斗 101 0 0 1 蜀山 120 2 0 2 致青春 3 30 1 3 这个杀手不太冷 78 2 0 4 白蛇 20 50 1
这里也可以直接使用 lambda 函数做转换,一行代码搞定
df['movie_types'] = df['movie_types'].apply(lambda x: 0 if x == '动作' else 1)
数据向量化
feature = df[['fighting_lens', 'kissing_lens']].values label = df['movie_types'].values
拟合预测
X_train, X_test, y_train, y_test = train_test_split(feature, label, random_state=2002) # 划分训练集和测试集 knn = KNeighborsClassifier() # 创建 KNN 分类器 knn.fit(X_train, y_train) predict_y = knn.predict(X_test) print("KNN 准确率", accuracy_score(y_test, predict_y)) >>> KNN 准确率 0.75
发现准确率并不是很高,说明默认的 K 值取5可能不是最优解
散点图查看数据分布
再通过散点图来直观的查看下,各个类别的分布情况
import seaborn as sns import matplotlib.pyplot as plt sns.scatterplot(x='fighting_lens', y='kissing_lens', hue='movie_types', data=df) plt.show()
原来是有两个并不太正常的点,看起来更加接近动作类别,但是实际上却是爱情类别的电影。
交叉验证
接下来使用交叉验证来寻找最优的 K 值
X_train_new = X_train[:18] X_train_validation = X_train[18:] for k in range(1, 15, 2): knn = KNeighborsClassifier(n_neighbors=k) knn.fit(X_train, y_train) predict_y = knn.predict(X_test) print("K为%s的准确率" % k, accuracy_score(y_test, predict_y)) >>> K为1的准确率 0.75 K为3的准确率 0.75 K为5的准确率 0.75 K为7的准确率 0.875 K为9的准确率 0.875 K为11的准确率 0.875 K为13的准确率 0.875
可以看到,由于本数据集较小,K 取不同值时准确率变化的比较奇怪。不过还是可以得出,当 K 值取7时,基本已经是最优的 K 值了。
手写数字识别分类
使用 sklearn 自带的手写数字数据集,它包括了1797幅数字图像,每幅图像大小是8*8像素。
首先还是导入数据集,并查看数据
from sklearn import datasets import matplotlib.pyplot as plt# 加载数据 digits = datasets.load_digits() data = digits.data # 查看整体数据 print(data.shape) # 查看第一幅图像 print(digits.images[0]) # 第一幅图像代表的数字含义 print(digits.target[0]) # 将第一幅图像显示出来 plt.gray() plt.imshow(digits.images[0]) plt.show() >>> (1797, 64) [[ 0. 0. 5. 13. 9. 1. 0. 0.] [ 0. 0. 13. 15. 10. 15. 5. 0.] [ 0. 3. 15. 2. 0. 11. 8. 0.] [ 0. 4. 12. 0. 0. 8. 8. 0.] [ 0. 5. 8. 0. 0. 9. 8. 0.] [ 0. 4. 11. 0. 1. 12. 7. 0.] [ 0. 2. 14. 5. 10. 12. 0. 0.] [ 0. 0. 6. 13. 10. 0. 0. 0.]] 0
我们把第一幅图像以图片的形式展示了出来,可以依稀的看出是一个0,同时该分类的标注(target)也是0。
数据规范化
在正式处理数据之前,我们先来看一个概念-数据规范化
那么什么是数据规范化呢
数据规范化是数据挖掘的一项基本工作,之所以称之为基本,是因为不同评价指标往往具有不同的量纲,数值间的差别可能很大,不进行处理可能会影响到数据分析的结果。为了消除指标之间的量纲和取值范围差异的影响,需要进行标准化处理,将数据按照比例进行缩放,使之落入一个特定的区域,便于进行综合分析。同时数据规范化对于基于距离的算法尤为重要。
数据规范化又分为如下几种
Min-Mix 规范化
Min-max 规范化方法是将原始数据变换到[0,1]的空间中,也成为离散标准化。其公式为:
新数值 = (原数值 – 极小值)/ (极大值 – 极小值)
离散标准化保留了原来数据中存在的关系,是消除量纲和数据取值范围影响的最简单方法。这种处理方法的缺点是若数值集中且某个数值很大,则规范化后各值接近于0,并且将会相差不大。
Z-Score 规范化
也称标准差标准化,经过处理的数据的均值为0,标准差为1。转化公式为:
新数值 = (原数值 – 均值)/ 标准差
该种标准化方式是当前使用最多的规范化方法。
小数定标规范化
就是通过移动小数点的位置来进行规范化,小数点移动多少位取决于属性 A 的取值中的最大绝对值。
手写数字数据集规范化
由于我们使用的 KNN 算法正是基于距离的,所以要做数值规范化,可以采用 Z-Score 规范化
train_x, test_x, train_y, test_y = train_test_split(data, digits.target, random_state=2002) # 采用 Z-Score 规范化 ss = preprocessing.StandardScaler() train_ss_x = ss.fit_transform(train_x) test_ss_x = ss.transform(test_x)
这里要注意,对于训练数据,是使用 fit_transform 函数来转换,而对于测试数据,则是使用 transform 函数来转换。两者的区别为,fit_transform 相当于是 fit + transform,训练数据作为一个基准数据集,先进行 fit,然后再把 fit (拟合)过的数据分别应用到训练数据和测试数据上,进行 transform 操作。
创建分类器并预测
接下来就是我们已经学习过的知识,创建 KNN 分类器,并验证模型准确率
knn = KNeighborsClassifier() knn.fit(train_ss_x, train_y) predict_y = knn.predict(test_ss_x) print("KNN 准确率: %.4lf" % accuracy_score(test_y, predict_y)) >>> KNN 准确率: 0.9689
可以看到,使用默认的 K 为5的情况下,准确率还是很好的。
分类与回归
在上一节中,我也说过,KNN 不仅可以解决分类问题,同样也可以解决回归问题,下来我们就先来看看什么是分类与回归。
其实回归问题和分类问题的本质一样,都是针对一个输入做出一个输出预测,其区别在于输出变量的类型。
分类:给定一个新的模式,根据训练集推断它所对应的类别(如:+1,-1),是一种定性输出,也叫离散变量预测。
回归:给定一个新的模式,根据训练集推断它所对应的输出值(实数)是多少,是一种定量输出,也叫连续变量预测。
举个例子:预测明天的气温是多少度,这是一个回归任务;预测明天是阴、晴还是雨,就是一个分类任务。
预测二手车价格
预测价格,很明显是一系列连续的变量,所以这个问题就属于回归问题。
先来看下数据
import pandas as pd import matplotlib.pyplot as plt import numpy as np import seaborn as sns df = pd.read_csv('knn-regression.csv') print(df)
Brand:车子的品牌
Type:是车子的类别
Color:车子的颜色
Construction Year:车子生产时间
Odometer:车子行驶的里程
Ask Price:价格,也就是我们需要预测的值
Days Until MOT 和 HP:都是未知的数据列
独热编码处理数据
对于 type 这一列,虽然它是数值型,但是1.0,1.1等都是代表的一种类别,所以我们可以采用独热编码的方式,把该列数据转换一下。如果你不记得独热编码了,可以到前面“数据清洗”一节回顾下。
对于 color 这一列,由于它的数值是 green,red 等字符,也需要采用独热编码,转换成0,1类型数据。
独热编码 Type 和 Color 列
df_new = pd.get_dummies(df, columns=['Type']) df_new = pd.get_dummies(df_new, columns=['Color']) print(df_new) >>> Brand Construction Year Odometer Ask Price Days Until MOT HP \ 0 Peugeot 106 2002 166879 999 138 60 1 Peugeot 106 1998 234484 999 346 60 2 Peugeot 106 1997 219752 500 -5 60 3 Peugeot 106 2001 223692 750 -87 60 4 Peugeot 106 2002 120275 1650 356 59 5 Peugeot 106 2003 131358 1399 266 60 6 Peugeot 106 1999 304277 799 173 57 7 Peugeot 106 1998 93685 1300 0 75 8 Peugeot 106 2002 225935 950 113 60 9 Peugeot 106 1997 252319 650 133 75 10 Peugeot 106 1998 220000 700 82 50 11 Peugeot 106 1997 212000 700 75 60 12 Peugeot 106 2003 255134 799 197 60 Type_1.0 Type_1.1 Type_1.4 Color_black Color_blue Color_green \ 0 1 0 0 0 1 0 1 1 0 0 0 1 0 2 0 1 0 1 0 0 3 0 1 0 0 0 0 4 0 1 0 0 0 0 5 0 1 0 0 0 0 6 0 1 0 0 0 1 7 0 0 1 0 0 1 8 0 1 0 0 0 0 9 0 0 1 0 0 1 10 1 0 0 1 0 0 11 0 1 0 1 0 0 12 0 1 0 1 0 0 Color_grey Color_red Color_white 0 0 0 0 1 0 0 0 2 0 0 0 3 0 1 0 4 1 0 0 5 0 1 0 6 0 0 0 7 0 0 0 8 0 0 1 9 0 0 0 10 0 0 0 11 0 0 0 12 0 0 0
可以看到,原来的 Type 和 Color 两列都没有了,取而代之的是对应 Type_1.0 和 Color_black 等列
查看每列值情况
我们可以通过函数 value_counts() 来查看每一列值的分布情况
for col in df.columns: print(df[col].value_counts()) >>> Peugeot 106 13 Name: Brand, dtype: int64 1.1 8 1.0 3 1.4 2 Name: Type, dtype: int64 black 4 green 3 red 2 blue 2 grey 1 white 1 Name: Color, dtype: int64 1998 3 1997 3 2002 3 2003 2 1999 1 2001 1 Name: Construction Year, dtype: int64 252319 1 131358 1 304277 1 234484 1 120275 1 225935 1 220000 1 93685 1 219752 1 223692 1 166879 1 255134 1 212000 1 Name: Odometer, dtype: int64 799 2 700 2 999 2 750 1 650 1 1300 1 950 1 1399 1 500 1 1650 1 Name: Ask Price, dtype: int64 133 1 266 1 346 1 -87 1 82 1 113 1 173 1 75 1 138 1 197 1 -5 1 356 1 0 1 Name: Days Until MOT, dtype: int64 60 8 75 2 59 1 57 1 50 1 Name: HP, dtype: int64
对于第一列 Brand,它只有一个值,也就是说在所有的数据中,该列都是相同的,即对我们的预测是不会产生任何影响,可以删除
df_new.drop(['Brand'], axis=1, inplace=True)
数据关联性分析
matrix = df_new.corr() plt.subplots(figsize=(8, 6)) sns.heatmap(matrix, square=True) plt.show()
corr 函数是用来计算任意两个变量之间的关联系数的,计算出关联系数后,使用 seaborn 的 heatmap 制作热力图。
在该热力图中,颜色越浅,代表变量之间关联性越强。所以在对角线方向上,由于都是同一个变量之间的关联系数,所以都是1,颜色最浅。
同时还能看出,与 Ask Price 关联性最强的几个变量为 Construction Year,Days Until MOT 和 Color_grey 三个变量,即我们可以选取这三个变量为训练特征,来训练模型。
模型训练
导入相关包
from sklearn.neighbors import KNeighborsRegressor from sklearn.model_selection import train_test_split from sklearn import preprocessing from sklearn.preprocessing import StandardScaler import numpy as np
还记得,在使用 KNN 做分类时,导入的库为 KNeighborsClassifier,现在做回归,使用的库为 KNeighborsRegressor。
选择特征和预测值
X = df_new[['Construction Year', 'Days Until MOT', 'Color_grey']] y = df_new['Ask Price'].values.reshape(-1, 1) X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=33)
只选取了关联性比较强的三个特征
reshape 是用来转换数据为指定行列数矩阵的函数,比如 reshape(2, 3)就是转换为2行3列矩阵。那么(-1, 1)是用来干嘛的呢,就是说,需要把数据转换为1列,但是多少行是不确定的,就用-1来占用。
所以我们看一下 y,应该是一个拥有1列,n 行的矩阵数据
print(y) >>> [[ 999] [ 999] [ 500] [ 750] [1650] [1399] [ 799] [1300] [ 950] [ 650] [ 700] [ 700] [ 799]]
数据规范化
仍然使用 Z-Score 规范化来规范数据
ss = StandardScaler() X_train_ss = ss.fit_transform(X_train) X_test_ss = ss.transform(X_test)
模型训练及预测
knn = KNeighborsRegressor(n_neighbors=2) knn.fit(X_train_ss, y_train) y_pred = knn.predict(X_test_ss)
由于预测的结果是一些连续的数值,我们可以采用可视化的方式来查看预测情况
用散点图展示预测值与实际值,再辅以中心线
plt.scatter(y_pred, y_test) plt.xlabel("Prediction") plt.ylabel("Real Value") diag = np.linspace(500, 1200, 100) plt.plot(diag, diag, '-r') plt.show()
如果我们的预测是完全准确的话,图中的所有点都会落到这条直线上,而点偏离直线的距离越大,说明预测结果与实际值偏差越大。
从预测的结果能够看出,由于我们数据集比较小,且强关联的变量过少,所以导致预测的结果并不是十分理想。如果后期能够增加数据量和关联特征数量,那么预测结果准确率也会随着大大增加。
文中的完整代码,可以在这里下载
总结
今天带你完成了三个项目的实战,分别用 KNN 算法实现了两个分类和一个回归问题的处理。在这个过程中,你应该对数据探索,数据可视化,数据规范化等技能有 了一定的体会。
同时你应该也有注意到,我们在拿到一个问题时,并不要急于训练模型,而是要全面的了解数据,并做好充分的数据处理,这样在后面的模型训练时,才会事半功倍。
练习题
能否使用其他的 K 值,比如100,重新跑下手写数字的案例,看看分类器的准确率是多少?