1 KNN 算法原理
1.1 算法简介
K近邻算法(英文为K-Nearest Neighbor,因而又简称KNN算法)是非常经典的机器学习算法,用于分类和回归问题。
其基本原理是对于一个新样本,K近邻算法的目的就是在已有数据中寻找与它最相似的K个数据,或者说“离它最近”的K个数据,如果这K个数据大多数属于某个类别,则该样本也属于这个类别。
其中有两个细节:K值的选取和点距离的计算。K值的选取很大程度决定了KNN算法的结果;点距离则一般使用欧氏距离或曼哈顿距离。
欧氏距离:
二维空间:
n维空间:
1.2 算法步骤
1)计算测试数据与各个训练数据之间的距离;
2)按照距离的递增关系进行排序;
3)选取距离最小的K个点;
4)确定前K个点所在类别的出现频率;
5)返回前K个点中出现频率最高的类别作为测试数据的预测分类。
1.3 算法的优缺点
1.3.1 优点
- 简单易理解,易实现。
- 适用于多分类问题。
- 对异常值不敏感。
1.3.2 缺点
- 需要保存全部训练数据,计算复杂度高。
- 对于特征空间维度高的数据集,效果较差。
- 需要选择合适的K值。
1.4 应用场合
- 适用于小型数据集和低维特征空间。
- 可用于分类和回归问题。
- 用于推荐系统、图像识别、模式识别等领域。
2 python代码实现例子:约会网站的配对
1) 准备数据集
大致的格式如上所示,有三个特征值以及一种标签类。
第一列:每年获得的飞行常客里程数
第二列:玩视频游戏所耗时间百分比
第三列:每周消费的冰淇淋公升数
第四列:1-不喜欢、2-有点喜欢、3-非常喜欢
通过输入前三个特征来预测约会的对象是否符合自己的口味。
2) 读取文件
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.lines as mlines
from matplotlib.font_manager import FontProperties
def loadData(filename):
"""
:param filename: 文件路径名
:return: dataMat:数据集 lableMat:标签集
"""
# 读取文件
fr = open(filename)
# 读取文件内容
getFile = fr.readlines()
# 获取文件行数
lines = len(getFile)
# 定义存放标签的列表
lableMat = []
# 定义一个空矩阵,存放特征值的列表
emptyMat = np.zeros((lines,3))
# 初始化索引
index = 0
# 通过迭代读取数据
for line in getFile:
# 将每一行的数据的空格或者换行符去掉
lineArr = line.strip().split()
# 逐行读取前三列数据
#dataMat.append(lineArr[:3])
emptyMat[index,:] = lineArr[:3]
# 读取每一行的标签值(lineArr[-1]代表的是每一行最后一个元素)
lableMat.append(int(lineArr[-1])) # 为什么要转会出问题为int,因为我们的linArr[-1]是str类型,这里不类型转换后续操作
# 索引值自增
index += 1
return emptyMat,lableMat
3)归一化
归一化是为了使数据取值在(0,1)之间 ,公式为
(x表示的是原始数据,min表示的是每一列最小值,max表示的是每一列最大值)
def dataNorm(dataSet):
"""
:param dataSet:
:return: diffs:最大最小之间的差值 normData:归一化的数据集
"""
# 获取数据集中每一列的最大值
maxValue = dataSet.max(0)
# print(maxValue)
# 获取数据集中每一列的最小值
minValue = dataSet.min(0)
# print(minValue)
# 最大值与最小值之间的差值
diffs = maxValue - minValue
# 获取dataSet行数
num = dataSet.shape[0] # num = 1000
# 定义一个和DataSet一样大小的矩阵
normData = np.zeros(dataSet.shape)
# 数据归一化(使数据属于(0,1)之间)
for i in range(num):
# 采用的是最值归一化:x = (x - min) / (max - min)
normData[i,: ] = (dataSet[i,:] - minValue) / diffs
return diffs,normData,minValue
4)分别测试下数据归一化和没有归一化的误差
4.1)未对数据归一化
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.lines as mlines
from matplotlib.font_manager import FontProperties
def loadData(filename):
"""
:param filename: 文件路径名
:return: dataMat:数据集 lableMat:标签集
"""
# 读取文件
fr = open(filename)
# 读取文件内容
getFile = fr.readlines()
# 获取文件行数
lines = len(getFile)
# 定义存放标签的列表
lableMat = []
# 定义一个空矩阵,存放特征值的列表
emptyMat = np.zeros((lines,3))
# 初始化索引
index = 0
# 通过迭代读取数据
for line in getFile:
# 将每一行的数据的空格或者换行符去掉
lineArr = line.strip().split()
# 逐行读取前三列数据
#dataMat.append(lineArr[:3])
emptyMat[index,:] = lineArr[:3]
# 读取每一行的标签值(lineArr[-1]代表的是每一行最后一个元素)
lableMat.append(int(lineArr[-1])) # 为什么要转会出问题为int,因为我们的linArr[-1]是str类型,这里不类型转换后续操作
# 索引值自增
index += 1
return emptyMat,lableMat
def dataNorm(dataSet):
"""
:param dataSet:
:return: diffs:最大最小之间的差值 normData:归一化的数据集
"""
# 获取数据集中每一列的最大值
maxValue = dataSet.max(0)
# print(maxValue)
# 获取数据集中每一列的最小值
minValue = dataSet.min(0)
# print(minValue)
# 最大值与最小值之间的差值
diffs = maxValue - minValue
# 获取dataSet行数
num = dataSet.shape[0] # num = 1000
# 定义一个和DataSet一样大小的矩阵
normData = np.zeros(dataSet.shape)
# 数据归一化(使数据属于(0,1)之间)
for i in range(num):
# 采用的是最值归一化:x = (x - min) / (max - min)
normData[i,: ] = (dataSet[i,:] - minValue) / diffs
return diffs,normData,minValue
def classify(input,data,label,k):
"""
:param input: 输入数据
:param data: 原数据集
:param label: 标签集
:param k: 选取的点的个数
:return: classes:标签类别
"""
# 获取data行数
size = data.shape[0]
# 作差 (其中np.tile(input,(size,1))作用是将input重复一次形成size大小的矩阵)
diff = np.tile(input,(size,1)) - data
# 求平方
sqdiff = diff ** 2
# 求和
squreDiff = np.sum(sqdiff,axis=1)
# 开根号
dist = squreDiff ** 0.5
# 排序 (argsort()根据元素的值从小到大对元素进行排序,返回下标)
# 比如a = [1,5,9,2,3]对应的下标为[0,1,2,3,4] 排序之后[1,2,3,5,9] 返回的结果是[0,3,4,1,2]
sortDist = np.argsort(dist)
# 对前k个最小距离点进行统计
classCount = {}
for i in range(k):
voteLabel = label[sortDist[i]]
# 对选取的前k个最小点的类别进行统计
classCount[voteLabel] = classCount.get(voteLabel,0) + 1
# 统计所选类别出现最多的
maxCount = 0
for key,value in classCount.items():
if value > maxCount:
maxCount = value
classes = key
return classes
def dataTest(filepath):
"""
函数说明:分类器测试函数
Parameters:filepath
Returns:
normDataSet - 归一化后的特征矩阵
ranges - 数据范围
minVals - 数据最小值
"""
# 将返回的特征矩阵和分类向量分别存储到datingDataMat和datingLabels中
datingDataMat, datingLabels = loadData(filepath)
# 取所有数据的百分之十
hoRatio = 0.10
# 数据归一化,返回归一化后的矩阵,数据范围,数据最小值
#ranges,normMat, minVals = dataNorm(datingDataMat)
# 获得normMat的行数
rows = datingDataMat.shape[0]
# 百分之十的测试数据的个数
nums = int(hoRatio * rows)
# 分类错误概率
errorRate = 0
for i in range(nums):
# 前nums个数据作为测试集,后rows - nums个数据作为训练集
classifyResult = classify(datingDataMat[i,:],datingDataMat[nums:rows,:],datingLabels[nums:rows],3)
print("分类结果:%d\t真实类别:%d" % (classifyResult, datingLabels[i]))
if classifyResult != datingLabels[i]:
errorRate += 1
print("错误率:%f%%" % (errorRate / float(nums) * 100))
if __name__ == '__main__':
filepath = "data.txt"
data,label = loadData(filepath)
dataTest(filepath)
运行结果:
错误率为24.000000%
4.2)对数据归一化
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.lines as mlines
from matplotlib.font_manager import FontProperties
def loadData(filename):
"""
:param filename: 文件路径名
:return: dataMat:数据集 lableMat:标签集
"""
# 读取文件
fr = open(filename)
# 读取文件内容
getFile = fr.readlines()
# 获取文件行数
lines = len(getFile)
# 定义存放标签的列表
lableMat = []
# 定义一个空矩阵,存放特征值的列表
emptyMat = np.zeros((lines,3))
# 初始化索引
index = 0
# 通过迭代读取数据
for line in getFile:
# 将每一行的数据的空格或者换行符去掉
lineArr = line.strip().split()
# 逐行读取前三列数据
#dataMat.append(lineArr[:3])
emptyMat[index,:] = lineArr[:3]
# 读取每一行的标签值(lineArr[-1]代表的是每一行最后一个元素)
lableMat.append(int(lineArr[-1])) # 为什么要转会出问题为int,因为我们的linArr[-1]是str类型,这里不类型转换后续操作
# 索引值自增
index += 1
return emptyMat,lableMat
def dataNorm(dataSet):
"""
:param dataSet:
:return: diffs:最大最小之间的差值 normData:归一化的数据集
"""
# 获取数据集中每一列的最大值
maxValue = dataSet.max(0)
# print(maxValue)
# 获取数据集中每一列的最小值
minValue = dataSet.min(0)
# print(minValue)
# 最大值与最小值之间的差值
diffs = maxValue - minValue
# 获取dataSet行数
num = dataSet.shape[0] # num = 1000
# 定义一个和DataSet一样大小的矩阵
normData = np.zeros(dataSet.shape)
# 数据归一化(使数据属于(0,1)之间)
for i in range(num):
# 采用的是最值归一化:x = (x - min) / (max - min)
normData[i,: ] = (dataSet[i,:] - minValue) / diffs
return diffs,normData,minValue
def classify(input,data,label,k):
"""
:param input: 输入数据
:param data: 原数据集
:param label: 标签集
:param k: 选取的点的个数
:return: classes:标签类别
"""
# 获取data行数
size = data.shape[0]
# 作差 (其中np.tile(input,(size,1))作用是将input重复一次形成size大小的矩阵)
diff = np.tile(input,(size,1)) - data
# 求平方
sqdiff = diff ** 2
# 求和
squreDiff = np.sum(sqdiff,axis=1)
# 开根号
dist = squreDiff ** 0.5
# 排序 (argsort()根据元素的值从小到大对元素进行排序,返回下标)
# 比如a = [1,5,9,2,3]对应的下标为[0,1,2,3,4] 排序之后[1,2,3,5,9] 返回的结果是[0,3,4,1,2]
sortDist = np.argsort(dist)
# 对前k个最小距离点进行统计
classCount = {}
for i in range(k):
voteLabel = label[sortDist[i]]
# 对选取的前k个最小点的类别进行统计
classCount[voteLabel] = classCount.get(voteLabel,0) + 1
# 统计所选类别出现最多的
maxCount = 0
for key,value in classCount.items():
if value > maxCount:
maxCount = value
classes = key
return classes
def dataTest(filepath):
"""
函数说明:分类器测试函数
Parameters:filepath
Returns:
normDataSet - 归一化后的特征矩阵
ranges - 数据范围
minVals - 数据最小值
"""
# 将返回的特征矩阵和分类向量分别存储到datingDataMat和datingLabels中
datingDataMat, datingLabels = loadData(filepath)
# 取所有数据的百分之十
hoRatio = 0.10
# 数据归一化,返回归一化后的矩阵,数据范围,数据最小值
ranges,normMat, minVals = dataNorm(datingDataMat)
# 获得normMat的行数
rows = normMat.shape[0]
# 百分之十的测试数据的个数
nums = int(hoRatio * rows)
# 分类错误概率
errorRate = 0
for i in range(nums):
# 前nums个数据作为测试集,后rows - nums个数据作为训练集
classifyResult = classify(normMat[i,:],normMat[nums:rows,:],datingLabels[nums:rows],3)
print("分类结果:%d\t真实类别:%d" % (classifyResult, datingLabels[i]))
if classifyResult != datingLabels[i]:
errorRate += 1
print("错误率:%f%%" % (errorRate / float(nums) * 100))
if __name__ == '__main__':
filepath = "data.txt"
data,label = loadData(filepath)
dataTest(filepath)
运行结果:
错误率为5.000000%
5.完整代码
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.lines as mlines
from matplotlib.font_manager import FontProperties
def loadData(filename):
"""
:param filename: 文件路径名
:return: dataMat:数据集 lableMat:标签集
"""
# 读取文件
fr = open(filename)
# 读取文件内容
getFile = fr.readlines()
# 获取文件行数
lines = len(getFile)
# 定义存放标签的列表
lableMat = []
# 定义一个空矩阵,存放特征值的列表
emptyMat = np.zeros((lines,3))
# 初始化索引
index = 0
# 通过迭代读取数据
for line in getFile:
# 将每一行的数据的空格或者换行符去掉
lineArr = line.strip().split()
# 逐行读取前三列数据
#dataMat.append(lineArr[:3])
emptyMat[index,:] = lineArr[:3]
# 读取每一行的标签值(lineArr[-1]代表的是每一行最后一个元素)
lableMat.append(int(lineArr[-1])) # 为什么要转会出问题为int,因为我们的linArr[-1]是str类型,这里不类型转换后续操作
# 索引值自增
index += 1
return emptyMat,lableMat
def show(dataMat,lableMat):
"""
:param dataMat:
:param lableMat:
"""
# 设置汉字格式
font = FontProperties(fname=r"D:\python-workspace\SimHei.ttf", size=9)
# 设置颜色列表
LabelColros = []
# 给不同的标签添加上不同的颜色
for i in lableMat:
if i == 1:
LabelColros.append('black')
if i == 2:
LabelColros.append('orange')
if i == 3:
LabelColros.append('red')
# 画出散点图,以datas矩阵的第一(飞行常客例程)、第二列(玩游戏)数据画散点数据,散点大小为15,透明度为0.5
plt.scatter(dataMat[:,0].tolist(),dataMat[:,1].tolist(), color=LabelColros,s=15, alpha=.5)
# 设置标题
plt.title('每年获得的飞行常客里程数与玩视频游戏所消耗时间占比', FontProperties=font,color='r',size=10)
# 设置x轴
plt.xlabel('每年获得的飞行常客里程数', FontProperties=font,color='blue')
# 设置y轴
plt.ylabel('玩视频游戏所消耗时间占', FontProperties=font,color='orange')
# 设置图例
didntLike = mlines.Line2D([], [], color='black', marker='.',
markersize=6, label='didntLike')
smallDoses = mlines.Line2D([], [], color='orange', marker='.',
markersize=6, label='smallDoses')
largeDoses = mlines.Line2D([], [], color='red', marker='.',
markersize=6, label='largeDoses')
# 添加图例
plt.legend(handles=[didntLike, smallDoses, largeDoses])
# 显示
plt.show()
def dataNorm(dataSet):
"""
:param dataSet:
:return: diffs:最大最小之间的差值 normData:归一化的数据集
"""
# 获取数据集中每一列的最大值
maxValue = dataSet.max(0)
# print(maxValue)
# 获取数据集中每一列的最小值
minValue = dataSet.min(0)
# print(minValue)
# 最大值与最小值之间的差值
diffs = maxValue - minValue
# 获取dataSet行数
num = dataSet.shape[0] # num = 1000
# 定义一个和DataSet一样大小的矩阵
normData = np.zeros(dataSet.shape)
# 数据归一化(使数据属于(0,1)之间)
for i in range(num):
# 采用的是最值归一化:x = (x - min) / (max - min)
normData[i,: ] = (dataSet[i,:] - minValue) / diffs
return diffs,normData,minValue
def classify(input,data,label,k):
"""
:param input: 输入数据
:param data: 原数据集
:param label: 标签集
:param k: 选取的点的个数
:return: classes:标签类别
"""
# 获取data行数
size = data.shape[0]
# 作差 (其中np.tile(input,(size,1))作用是将input重复一次形成size大小的矩阵)
diff = np.tile(input,(size,1)) - data
# 求平方
sqdiff = diff ** 2
# 求和
squreDiff = np.sum(sqdiff,axis=1)
# 开根号
dist = squreDiff ** 0.5
# 排序 (argsort()根据元素的值从小到大对元素进行排序,返回下标)
# 比如a = [1,5,9,2,3]对应的下标为[0,1,2,3,4] 排序之后[1,2,3,5,9] 返回的结果是[0,3,4,1,2]
sortDist = np.argsort(dist)
# 对前k个最小距离点进行统计
classCount = {}
for i in range(k):
voteLabel = label[sortDist[i]]
# 对选取的前k个最小点的类别进行统计
classCount[voteLabel] = classCount.get(voteLabel,0) + 1
# 统计所选类别出现最多的
maxCount = 0
for key,value in classCount.items():
if value > maxCount:
maxCount = value
classes = key
return classes
def classifyTest(filepath):
"""
:param filepath:
"""
# 设置分类结果
classResult = ['不喜欢','有些喜欢','非常喜欢']
# 用户输入三维特征
miles = float(input("每年获得的飞行常客里程数:"))
precentTats = float(input("玩视频游戏所耗时间百分比:"))
iceCream = float(input("每周消费的冰激淋公升数:"))
# 将输入的特征存入到数组中
inputs = np.array([miles,precentTats,iceCream])
# 处理数据
dataMat,labelMat = loadData(filepath)
# 训练集归一化
diffs,normData,minValue = dataNorm(dataMat)
# 测试集归一化
normInput = (inputs - minValue) / diffs
# 返回分类结果
classify_result = classify(normInput,normData,labelMat,3)
# 输出预测结果
print("你可能%s这个人" % (classResult[classify_result - 1]))
def dataTest(filepath):
"""
函数说明:分类器测试函数
Parameters:filepath
Returns:
normDataSet - 归一化后的特征矩阵
ranges - 数据范围
minVals - 数据最小值
"""
# 将返回的特征矩阵和分类向量分别存储到datingDataMat和datingLabels中
datingDataMat, datingLabels = loadData(filepath)
# 取所有数据的百分之十
hoRatio = 0.10
# 数据归一化,返回归一化后的矩阵,数据范围,数据最小值
ranges,normMat, minVals = dataNorm(datingDataMat)
# 获得normMat的行数
rows = normMat.shape[0]
# 百分之十的测试数据的个数
nums = int(hoRatio * rows)
# 分类错误概率
errorRate = 0
for i in range(nums):
# 前nums个数据作为测试集,后rows - nums个数据作为训练集
classifyResult = classify(normMat[i,:],normMat[nums:rows,:],datingLabels[nums:rows],3)
print("分类结果:%d\t真实类别:%d" % (classifyResult, datingLabels[i]))
if classifyResult != datingLabels[i]:
errorRate += 1
print("错误率:%f%%" % (errorRate / float(nums) * 100))
if __name__ == '__main__':
filepath = "data.txt"
data,label = loadData(filepath)
#datas = np.mat(data)
#show(data,label)
# 这里需要将类型转换为float
# 问题:https://blog.youkuaiyun.com/u012535605/article/details/76675683
#dataNorm(datas.astype(float))
#dataTest(filepath)
classifyTest(filepath)
运行结果:
3 用python绘制ROC曲线
3.1 基本知识
①ROC曲线可以用来评估分类器的输出质量。
②ROC曲线Y轴为真阳性率,X轴为假阳性率。这意味着曲线的左上角是“理想”点——假阳性率为 0,真阳性率为1。
③面积下曲线(AUC)越大通常分类效率越好。
④坡度越大,则越有降低假阳性率,升高真阳性率的趋势。
3.2 用python实现
- 绘制ROC曲线主要基于python 的sklearn库中的两个函数,roc_curv和auc两个函数。
- roc_curv 用于计算出fpr(假阳性率)和tpr(真阳性率)
- auc用于计算曲线下面积,输入为fpr、和tpr
import matplotlib.pyplot as plt
from sklearn.metrics import roc_curve, auc
#根据classifyTest函数中的classify_result来生成分类结果和真实标签
#假设classify_result为0表示“不喜欢”,1表示“有些喜欢”,2表示“非常喜欢”
classify_result = [0, 1, 2]
true_labels = [0, 1, 2]
#计算ROC曲线
fpr, tpr, thresholds = roc_curve(true_labels, classify_result, pos_label=2)
roc_auc = auc(fpr, tpr)
#绘制ROC曲线
plt.figure()
plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC curve (area = %0.2f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='-')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic')
plt.legend(loc="lower right")
plt.show()
运行结果: