对于KNN算法,我分成了基础篇,进阶篇,实践篇。本篇是我们的实践篇,对于KNN原理没有过多的讲解,如果是刚接触到K-近邻算法的话,建议可以先从机器学习(一):K-近邻算法(基础篇)学习。欢迎大家学习愉快。
实践基础内容:
- 理解 KNN 算法的思想。
- 使用 Python 实现 KNN 算法 。
- 使用 UCI 上面的 Iris 数据集进行算法测试。
- 记录测试结果
实践进阶内容:
- 自行优化 KNN 算法并进行代码测试。
所用数据集:
- 网址:http://archive.ics.uci.edu/ml/index.php
- Iris数据集
第一步:对数据进行分析:
下面代码画出了iris花的不同属性之间的影响程度:
# -*- coding:utf-8 -*-
import math
import warnings
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.lines as mlines
import matplotlib.patches as mpatchs
warnings.filterwarnings('ignore') #忽略警告
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
def loadDataSet(filename):
"""
函数说明:从文件中下载数据,并将分离除连续型变量和标签变量
:parameter:
data - Iris数据集
attributes - 鸢尾花的属性
type - 鸢尾花的类别
sl-花萼长度 , sw-花萼宽度, pl-花瓣长度, pw-花瓣宽度
:return:
"""
iris_data = pd.read_csv(filename) #打开文件
iris_data = pd.DataFrame(data=np.array(iris_data), columns=['sl', 'sw', 'pl', 'pw', 'type'], index=range(149)) #给数据集添加列名,方便后面的操作
attributes = iris_data[['sl', 'sw', 'pl', 'pw']] #分离出花的属性
iris_data['type'] = iris_data['type'].apply(lambda x: x.split('-')[1]) # 最后类别一列,感觉前面的'Iris-'有点多余即把class这一列的数据按'-'进行切分取切分后的第二个数据
labels = iris_data['type'] #分理出花的类别
return attributes, labels
def showdatas(attributes, datinglabels):
"""
函数说明:画出花的属性两两之间的关系图
:parameter:
attributes - 花的属性
datinglabels - 花的类别
:return:none
"""
fig, axs = plt.subplots(nrows=3, ncols=2, figsize=(20, 8)) #定义一个3行2列的画布
LabelsColors = [] #建立一个颜色标签列表
for i in datinglabels: #遍历花的类型
if i == 'setosa': #setosa类型的花画成黑色的点
LabelsColors.append('black')
if i == 'versicolor': #versicolor类型的花画成橙色的点
LabelsColors.append('orange')
if i == 'virginica': #virginica类型的花画成红色的点
LabelsColors.append('red')
#在画板第一行第一列的位置绘制花萼长度和花萼宽度之间的关系
axs[0][0].scatter(x=attributes['sl'], y=attributes['sw'], color=LabelsColors, s=15, alpha=.5) #x轴为花萼长度,y轴为花萼宽度, 点大小为15, 透明度为0.5
axs0_title_text = axs[0][0].set_title(u'花萼长度和花萼宽度') #设置title
axs0_xlabel_text = axs[0][0].set_xlabel(u'花萼长度') #设置x轴的标签
axs0_ylabel_text = axs[0][0].set_ylabel(u'花萼宽度') #设置y轴的标签
plt.setp(axs0_title_text, size=9, weight='bold', color='red')
plt.setp(axs0_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs0_ylabel_text, size=7, weight='bold', color='black')
#在画板第一行第二列的位置绘制花萼长度和花瓣长度之间的关系
axs[0][1].scatter(x=attributes['sl'], y=attributes['pl'], color=LabelsColors, s=15, alpha=.5) #x轴为花萼长度,y轴为花瓣长度,点的大小为15, 透明度为0.5
axs1_title_text = axs[0][1].set_title(u'花萼长度和花瓣长度') #设立title
axs1_xlabel_text = axs[0][1].set_xlabel(u'花萼长度') #设置x轴标签
axs1_ylabel_text = axs[0][1].set_ylabel(u'花瓣长度') #设置y轴标签
plt.setp(axs1_title_text, size=9, weight='bold', color='red')
plt.setp(axs1_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs1_ylabel_text, size=7, weight='bold', color='black')
#在画板第二行第一列的位置绘制花萼长度与花瓣宽度之间的关系
axs[1][0].scatter(x=attributes['sl'], y=attributes['pw'], color=LabelsColors, s=15, alpha=.5) #x轴为花萼长度,y轴为花瓣长度,点的大小为15, 透明度为0.5
axs2_title_text = axs[1][0].set_title(u'花萼长度和花瓣宽度') #设立title
axs2_xlabel_text = axs[1][0].set_xlabel(u'花萼长度') #设立x轴标签
axs2_ylabel_text = axs[1][0].set_ylabel(u'花瓣宽度') #设立y轴标签
plt.setp(axs2_title_text, size=9, weight='bold', color='red')
plt.setp(axs2_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs2_ylabel_text, size=7, weight='bold', color='black')
#在画板第二行第二列的位置上绘制花萼宽度与花瓣长度之间的关系
axs[1][1].scatter(x=attributes['sw'], y=attributes['pl'], color=LabelsColors, s=15, alpha=.5) #x轴为花萼宽度,y轴为花瓣长度
axs3_title_text = axs[1][1].set_title(u'花萼宽度和花瓣长度') #设立title
axs3_xlabel_text = axs[1][1].set_xlabel(u'花萼宽度') #设立x轴标签
axs3_ylabel_text = axs[1][1].set_ylabel(u'花瓣长度') #设立y轴标签
plt.setp(axs3_title_text, size=9, weight='bold', color='red')
plt.setp(axs3_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs3_ylabel_text, size=7, weight='bold', color='black')
#在画板第三行第一列的位置绘制花萼宽度与花瓣宽度之间的关系
axs[2][0].scatter(x=attributes['sw'], y=attributes['pw'], color=LabelsColors, s=15, alpha=.5) #x轴为花萼宽度,y轴为花瓣宽度
axs4_title_text = axs[2][0].set_title(u'花萼宽度和花瓣宽度') #设立title
axs4_xlabel_text = axs[2][0].set_xlabel(u'花萼宽度') #设立x轴坐标
axs4_ylabel_text = axs[2][0].set_ylabel(u'花瓣宽度') #设立y轴坐标
plt.setp(axs4_title_text, size=9, weight='bold', color='red')
plt.setp(axs4_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs4_ylabel_text, size=7, weight='bold', color='black')
#在画板第三行第二列的位置绘制花瓣长度和花瓣宽度之间的关系
axs[2][1].scatter(x=attributes['pl'], y=attributes['pw'], color=LabelsColors, s=15, alpha=.5) #x轴花瓣长度,y轴为花瓣宽度
axs5_title_text = axs[2][1].set_title(u'花瓣长度和花瓣宽度')
axs5_xlabel_text = axs[2][1].set_xlabel(u'花瓣长度')
axs5_ylabel_text = axs[2][1].set_ylabel(u'花瓣宽度')
plt.setp(axs5_title_text, size=9, weight='bold', color='red')
plt.setp(axs5_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs5_ylabel_text, size=7, weight='bold', color='black')
#设置图例
setosa = mlines.Line2D([], [], color='black', marker='.', markersize=6, label='setosa') #设置setosa的图例为黑色的点,大小为6
versicolor = mlines.Line2D([], [], color='orange', marker='.', markersize=6, label='versicolor') #设置yersicolor的图例为橙色的点,大小为6
virginica = mlines.Line2D([], [], color='red', marker='.', markersize=6, label='virginica') #设置virginica的图例为红色的点,大小为6
axs[0][0].legend(handles=[setosa,versicolor,virginica]) #对每一个图形设置图例
axs[0][1].legend(handles=[setosa,versicolor,virginica])
axs[1][0].legend(handles=[setosa,versicolor,virginica])
axs[1][1].legend(handles=[setosa, versicolor, virginica])
axs[2][0].legend(handles=[setosa, versicolor, virginica])
axs[2][1].legend(handles=[setosa, versicolor, virginica])
#绘制图形
plt.show()
def autoNorm(attributes):
"""
函数说明: 对数据进行归一化
:parameter
attributes - 特征矩阵
:return: nonormAttributes - 归一化后的矩阵
"""
attributes = attributes.values #将DataFrame类型转变为array类型
minVal = attributes.min() #找出数据中的最小值
maxVal = attributes.max() #找出数据中的最大值
range = maxVal - minVal #数据范围
normAttributes = np.zeros(np.shape(attributes)) #初始化归一化数据
m = attributes.shape[0] #获取数据的行数
normAttributes = attributes - np.tile(minVal, (m, 1)) #创建一个全是最小值得数组
normAttributes = normAttributes / np.tile(range, (m, 1)) #创建一个全是范围值得数组
return normAttributes #返回归一化后的数据
def colcPer(attributes):
"""
函数说明:计算每个特征之间的Person相关系数
:parameter:
attributes - 特征值
:return: none
"""
attributes = attributes.astype('float32') #将attributes数据类型更改成float32
colcPerAttributes = np.corrcoef(attributes, rowvar=0) #以每一列计算相关系数
colcPerAttributes = pd.DataFrame(data=colcPerAttributes, columns=['sl', 'sw', 'pl', 'pw'], index=['sl', 'sw', 'pl', 'pw']) # 给数据集添加列名,方便观察
print(colcPerAttributes) #输出相关系数矩阵
if __name__ == '__main__':
df = "iris.data" #文件路径
attributes, labels = loadDataSet(df) #得到特征矩阵和标签变量
showdatas(attributes, labels) #输出相关的散点图
normAttributes = autoNorm(attributes) #进行归一化处理
colcPer(normAttributes) #计算相关系数
看看结果图:
这样看可能不太清楚,如果有信心的同学可以尝试一幅幅地画出来。由图可以看出,黑黄红代表着三种不同的品种,可以看出来,三种颜色的黑点都聚在了一起。说明,不同的花的品种两辆属性之间有着较大的差别,可以很好地使用分类算法。
使用KNN算法
代码如下:
# -*- coding:utf-8 -*-
import warnings
import operator
import numpy as np
import pandas as pd
from time import time
import matplotlib.pyplot as plt
warnings.filterwarnings('ignore') #忽略警告
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
def loadDataSet(filename):
"""
函数说明:从文件中下载数据,并将分离除连续型变量和标签变量
:parameter:
data - Iris数据集
attributes - 鸢尾花的属性
type - 鸢尾花的类别
sl-花萼长度 , sw-花萼宽度, pl-花瓣长度, pw-花瓣宽度
:return:
"""
iris_data = pd.read_csv(filename) #打开文件
iris_data = pd.DataFrame(data=np.array(iris_data), columns=['sl', 'sw', 'pl', 'pw', 'type'], index=range(149)) #给数据集添加列名,方便后面的操作
attributes = iris_data[['sl', 'sw', 'pl', 'pw']] #分离出花的属性
iris_data['type'] = iris_data['type'].apply(lambda x: x.split('-')[1]) # 最后类别一列,感觉前面的'Iris-'有点多余即把class这一列的数据按'-'进行切分取切分后的第二个数据
labels = iris_data['type'] #分理出花的类别
attriLabels = [] #建立一个标签列表
for label in labels: #为了更方便操作,将三中不同的类型分别设为1,2,3
if label == 'setosa': #如果类别为setosa的话,设为1
attriLabels.append(1)
elif label == 'versicolor': #如果是versicolor的时候设为2
attriLabels.append(2)
elif label == 'virginica': #如果是virginica的时候设为3
attriLabels.append(3)
return attributes, attriLabels
def autoNorm(attributes):
"""
函数说明: 对数据进行归一化
:parameter
attributes - 特征矩阵
:return: nonormAttributes - 归一化后的矩阵
"""
attributes = attributes.values #将DataFrame类型转变为array类型
minVal = attributes.min() #找出数据中的最小值
maxVal = attributes.max() #找出数据中的最大值
ranges = maxVal - minVal #数据范围
normAttributes = np.zeros(np.shape(attributes)) #初始化归一化数据
m = attributes.shape[0] #获取数据的行数
normAttributes = attributes - np.tile(minVal, (m, 1)) #创建一个全是最小值得数组
normAttributes = normAttributes / np.tile(ranges, (m, 1)) #创建一个全是范围值得数组
return normAttributes, ranges, minVal #返回归一化后的数据
def classify0(inX, dataSet, labels, k):
"""
函数说明:kNN算法分类器
:param inX: 用于分类的数据(测试集)
:param dataSet: 用于训练的数据(训练集)
:param labels: 花的种类(分类标签)
:param k: 选取距离最小的k个点
:return: sortedClassCount[0][0] - 分类结果
"""
#返回数据的行数
dataSetSize = dataSet.shape[0]
#将输入的测试集在行向方向复制一次,列向方向复制dataSetSize次,再相减
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
#将相减后的数据平方
sqDiffMat = diffMat**2
#将平方后的数据横向相加
sqDistances = sqDiffMat.sum(axis=1)
#开平方,得到测试集到每个训练集的距离
distances = sqDistances**0.5
#返回distances中元素从小到大排序后的索引值
sortedDistIndices = distances.argsort()
#计数类别次数的字典
classCount = {}
for i in range(k): #遍历循环取出k个训练集
#取出前k个标签
voteIlabel = labels[sortedDistIndices[i]]
#计算类别次数
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
#将值按字典的值进行降序排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
#返回次数最多的类别
return sortedClassCount[0][0]
def classifyPerson():
"""
函数说明:通过输入花的四个特征属性,进行分类输出
:return:
"""
#输出结果列表
resultList = ['setosa', 'versicolor', 'virginica']
#输入四个特征
sl = float(input("请输入花萼长度:"))
sw = float(input("请输入花萼宽度:"))
pl = float(input("请输入花瓣长度:"))
pw = float(input("请输入花瓣宽度:"))
filename = "iris.data"
#打开文件,得到特征数组和标签数组
attributes, labels = loadDataSet(filename)
#对特征变量进行归一化处理,得到归一化后的数据,和数据范围和最小值
norAttributes, range, minVal = autoNorm(attributes)
#生成训练集
inArr = np.array([sl, sw, pl, pw])
#测试集归一化
norminArr = (inArr - minVal) / range
#得到分类结果
classifierReslut = classify0(norminArr, norAttributes, labels, 7)
print("该花的类型可能为%s" % (resultList[classifierReslut-1]))
def datingClassTest():
"""
函数说明:分类器测试器
:return: none
"""
#打开文件名
# filename = "iris.data"
filename = "bezdekIris.data"
#打开文件,得到特征数组,和标签数组
attributes, labels = loadDataSet(filename)
#输出每个类别的个数
# for i in set(labels):
# print(i, "的个数为", labels.count(i))
#取数据的程度
hoRatio = 0.3
#数据归一化,得到归一化后的矩阵,数据范围,数据最小值
norAttributes, ranges, minVal = autoNorm(attributes)
#由于attributes里面的数据集中类别排序太正规了,对测试算法具有较大的影响,所以随机打乱数据的下标排序
norAttributes_indexes = np.random.permutation(len(norAttributes))
#得到归一化后数据的行数
m = norAttributes.shape[0]
#取百分之hoRatio的数据
numTestVecs = int(m * hoRatio)
#取norAttributes的前百分之三十的下标
test_indexes = norAttributes_indexes[:numTestVecs]
#取后百分之70的下标
train_indexes = norAttributes_indexes[numTestVecs:]
#得到训练集
x_train = np.array(norAttributes)[train_indexes]
#得到训练集的标签
y_train = np.array(labels)[train_indexes]
#得到测试集
x_test = np.array(norAttributes)[test_indexes]
#得到测试集的标签
y_test = np.array(labels)[test_indexes]
#得到最优参数k
k = adjustK(x_test, x_train, y_test, y_train, numTestVecs)
#计算错误个数
errorCount = 0.0
#遍历numTestVecs次
for i in range(numTestVecs):
#前numTestVecs个数据为测试集,后m-numTestVecs为训练集
classifierResult = classify0(x_test[i,:], x_train, y_train, k)
#输出预测结果和分类结果
print("分类结果:%d \t 真实类别:%d" % (classifierResult, y_test[i]))
#如果不相等,就错误个数加一
if classifierResult != y_test[i]:
errorCount += 1.0
#输出错误率
print("错误率: %f%%" % (errorCount/float(numTestVecs)*100))
def adjustK(x_test, x_train, y_test, y_train, nunTestVecs):
"""
函数说明:获取最优参数k
:param x_test: 测试集的特征值
:param x_train: 训练集的特征值
:param y_test: 测试集的标签值
:param y_train: 训练集的标签值
:param nunTestVecs: 训练集的个数
:return: k - 最优参数k
"""
#定义最优的正确率
best_correctRate = 0.0
#最优的k值
best_k = 0
#让k在1到8取值
for k in range(1,9):
#正确的个数要放在循环内
correctCount = 0.0
#循环多次求最优解
for i in range(nunTestVecs):
#得到测试集的预测值
y_predict = classify0(x_test[i,:], x_train, y_train, k)
#当预测值和真实值相等的时候
if y_predict == y_test[i]:
correctCount += 1.0 #正确个数加一
#求出正确率
correctRate = correctCount / float(nunTestVecs)
print("k=", k, "正确率为:", correctRate)
#判断当前正确率与前面出现的正确率的大小
if correctRate > best_correctRate:
best_correctRate = correctRate #将最大值赋值给最优值
best_k = k
print("正确率最大时k的取值为:", best_k) #输出最优参数k
return best_k
if __name__ == "__main__":
time1 = time()
# datingClassTest() #测试数据集
classifyPerson() # 使用数据集
time2 = time()
time = time2 - time1
print(f"耗时:{time}秒")
在下面,我们使用测试数据集来测试一下算法的错误率:
会发现算法对于这个数据的错误率保持在4%以下。说明这个算法起到了很好的分类效果,代表我们成功了。
ok,再来,我们对这个算法进行使用,将测试数据集注释掉。我们看一下结果:
很漂亮,我们可以成功的使用该算法。到这里,我们的实践算成功了。
在上面的算法中,我进行了k值得调整优化,大家可以看一看adjustk()这个函数,这个函数主要得作用是:让k从0到9取多个值,取出正确率最大的k值,并进行调用,这样能使我们的算法性能达到最优,不过有个很明显的缺点:时间复杂度过大。