目录
一、KNN算法基本思想
给定一个训练数据集,对新的输入实例,在训练数据集中找到与该实例最邻近的 k 个实例,这个实例的多数属于某个类,就把该输入实例分为这个类(概括为近朱者赤近墨者黑)
二、影片分类基本思想
我们利用KNN算法可实现对影片的简单分类(动作片、爱情片)
(一)分类规则的设定
利用KNN算法进行分类前,我们需先选取合适的分类特征对影片进行区分,这里我们选取影片的打斗次数和亲吻次数作为分类的特征标准,即
爱情片:亲吻次数较多,打斗次数较少
动作片:打斗次数较多,亲吻次数较少
(二)k值的选择
KNN算法中k的选择影响着分类结果的精确度,需选取合适的k值,由于本次训练集中的数据较少,k值选定为3
(三)距离度量
KNN算法的分类依据是样本点与数据集中的点的距离,因此距离度量至关重要。若分类规则中的特征数量级差距较大,则需采取归一化处理,一个简单的归一化操作如:
( test - min) / ( max - min )
其中test表示该点在此特征上的值,max和min分别表示该特征取值范围的最大值和最小值
三、利用KNN算法实现简单影片分类的实现
(一)本次分类中所应用到的库
import numpy as np
import operator
import matplotlib.pyplot as plt
(二)创建数据集并为其打上标签
def createDateSet():
# 四组二维特征
group = np.array([[1, 101], [5, 89], [108, 5], [115, 8]])
# 四组特征的标签
labels = ['爱情片', '爱情片', '动作片', '动作片']
return group, labels
group所代表的是本次数据集中存储特征向量的列表,[1,101]表示某一影片亲吻次数为1,打斗次数为101。labels则是对group中每组特征向量打上的标签,如labels中第一个元素为'爱情片',则代表group中的第一个特征向量[1,101]所代表的影片为爱情片
(三)归一化数据
def autoNorm(dataSet):
# 计算每种属性的最大值、最小值、范围
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
ranges = maxVals - minVals
# 创建一个与dataSet同shape的零矩阵
normDataSet = np.zeros(np.shape(dataSet))
# 返回dataSet的行数
m = dataSet.shape[0]
# 原始值减去最小值
normDataSet = dataSet - np.tile(minVals, (m, 1))
# 再除以最大和最小值的差,得到归一化数据
normDataSet = normDataSet / np.tile(ranges, (m, 1))
# 返回归一化数据结果,数据范围,最小值
return normDataSet, ranges, minVals
最后返回的是数据集,特征值的取值范围,特征值的最小值
(四)分类器的实现
def classify0(inX, dataSet, labels, k):
#获取归一化数据
normDataSet, ranges, minVals = autoNorm(dataSet)
# 获取数据集大小
dataSetSize = normDataSet.shape[0]
# 计算欧氏距离
diffMat = np.tile(inX, (dataSetSize, 1)) - normDataSet
sqDiffMat = diffMat ** 2
sqDistances = sqDiffMat.sum(axis=1)
distances = sqDistances ** 0.5
# 根据距离排序
sortedDistIndicies = distances.argsort()
# 统计前k个样本的类别
classCount = {}
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
# 排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
# 返回出现次数最多的类别
return sortedClassCount[0][0]
主要思路为获取数据后,计算其欧氏距离,并根据每个点的距离进行排序,取出前k(前文选取k值为3)个点的类别(标签),对这k个点中,不同标签出现的次数进行排序,返回出现次数最多的标签,用该标签表示测试点的类别
(四)绘制散点图
if __name__ == '__main__':
# 创建数据集
group, labels = createDateSet()
# 测试集
test = [101, 20]
# kNN分类
test_class = classify0(test, group, labels, 3)
# 打印分类结果
print("测试数据所属类别:", test_class)
# 绘制数据集的散点图
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(group[:, 0], group[:, 1], c=[labels.index(l) for l in labels], cmap='viridis', label=labels)
ax.scatter(test[0], test[1], c='red', marker='x', label='Test Data')
ax.legend()
ax.set_xlabel('Number of fights')
ax.set_ylabel('Number of kisses')
ax.set_title('Scatter Plot of Data Set')
plt.show()
(五)运行结果如下
(此处为简单运行结果,下文在模型评估处将增加数据集)
源代码如下:
import numpy as np
import operator
import matplotlib.pyplot as plt
def createDateSet():
# 四组二维特征
group = np.array([[1, 101], [5, 89], [108, 5], [115, 8]])
# 四组特征的标签
labels = ['爱情片', '爱情片', '动作片', '动作片']
return group, labels
def autoNorm(dataSet):
# 计算每种属性的最大值、最小值、范围
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
ranges = maxVals - minVals
# 创建一个与dataSet同shape的零矩阵
normDataSet = np.zeros(np.shape(dataSet))
# 返回dataSet的行数
m = dataSet.shape[0]
# 原始值减去最小值
normDataSet = dataSet - np.tile(minVals, (m, 1))
# 再除以最大和最小值的差,得到归一化数据
normDataSet = normDataSet / np.tile(ranges, (m, 1))
# 返回归一化数据结果,数据范围,最小值
return normDataSet, ranges, minVals
def classify0(inX, dataSet, labels, k):
#获取归一化数据
normDataSet, ranges, minVals = autoNorm(dataSet)
# 获取数据集大小
dataSetSize = normDataSet.shape[0]
# 计算欧氏距离
diffMat = np.tile(inX, (dataSetSize, 1)) - normDataSet
sqDiffMat = diffMat ** 2
sqDistances = sqDiffMat.sum(axis=1)
distances = sqDistances ** 0.5
# 根据距离排序
sortedDistIndicies = distances.argsort()
# 统计前k个样本的类别
classCount = {}
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
# 排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
# 返回出现次数最多的类别
return sortedClassCount[0][0]
if __name__ == '__main__':
# 创建数据集
group, labels = createDateSet()
# 测试集
test = [101, 20]
# kNN分类
test_class = classify0(test, group, labels, 3)
# 打印分类结果
print("测试数据所属类别:", test_class)
# 绘制数据集的散点图
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(group[:, 0], group[:, 1], c=[labels.index(l) for l in labels], cmap='viridis', label=labels)
ax.scatter(test[0], test[1], c='red', marker='x', label='Test Data')
ax.legend()
ax.set_xlabel('Number of fights')
ax.set_ylabel('Number of kisses')
ax.set_title('Scatter Plot of Data Set')
plt.show()
补充:
本次利用knn算法实现影片简单分类中,选取的两个特征分别为亲吻次数和打斗次数,其数量级几乎相同,个人认为不需要归一化处理数据对结果也没有影响(源代码中有归一化处理)
四、对该KNN算法进行模型评估
(一)常见的分类模型评估指标
准确率: 准确率是分类器正确预测的样本数占总样本数的比例。 公式:准确率 = (TP + TN) / (TP + TN + FP + FN)
精确率: 精确率衡量的是被分类器预测为正类的样本中有多少是真正的正类样本。 公式:精确率 = TP / (TP + FP)
召回率: 召回率衡量的是真正的正类样本中有多少被分类器预测为正类。 公式:召回率 = TP / (TP + FN)
F1 值: F1 值是精确率和召回率的调和平均值,综合考虑了精确率和召回率。 公式:F1 值 = 2 * (精确率 * 召回率) / (精确率 + 召回率)
在上述公式中,TP、TN、FP、FN分别代表真正例数、真负例数、假正例数和假负例数。
(二)了解ROC曲线和PR曲线
(1)ROC曲线
ROC(Receiver Operating Characteristic)曲线是用于评估二分类器性能的一种常用工具。它展示了在不同阈值下,真阳率(True Positive Rate,又称为召回率)与假阳率(False Positive Rate)之间的关系。
-
真阳率(True Positive Rate,TPR):真正例被分类器判定为正例的比例
-
假阳率(False Positive Rate,FPR):真负例被分类器误判为正例的比例
ROC曲线的横轴是FPR,纵轴是TPR。它的图形直观地展示了在不同阈值下,分类器的真阳率和假阳率之间的权衡关系。理想情况下,ROC曲线越靠近左上角(0,1)点,表示分类器性能越好,因为此时真阳率高而假阳率低,即分类器能够同时实现高召回率和低误报率。
另外,ROC曲线下的面积也是评估分类器性能的重要指标之一。AUC值越接近1,表示分类器性能越好;AUC值为0.5时,表示分类器的预测结果等同于随机猜测。
(2)PR曲线
PR(Precision-Recall)曲线是评估二分类器性能的一种常用工具,特别适用于不平衡数据集。PR曲线显示的是模型在不同阈值下的精度(Precision)和召回率(Recall)之间的关系。
1.精度(Precision): 精度是指分类器预测为正类别的样本中,实际为正类别的比例。精度衡量了分类器在预测为正类别时的准确性。
2.召回率(Recall): 召回率是指实际为正类别的样本中,被分类器预测为正类别的比例召回率衡量了分类器对正类别样本的识别能力。
PR曲线是以召回率为横轴,精度为纵轴,绘制的曲线。PR曲线的一端通常代表着低阈值(高召回率、低精度),另一端代表着高阈值(低召回率、高精度)。理想情况下,PR曲线应该尽可能地靠近右上角,即高精度和高召回率都很高,表示分类器在各个阈值下都表现良好。
PR曲线下的面积越大,说明分类器的性能越好。和ROC曲线不同,PR曲线不受不平衡数据集的影响,因此更适用于评估在样本不平衡情况下的分类器性能。
(三)不同k值下roc曲线的绘制
(1)代码部分
def plotROC(labels, probs, title='ROC Curve'):
# 绘制ROC曲线
thresholds = np.unique(probs)
thresholds = np.append(thresholds, max(probs) + 1)
tpr_values = [0] * len(thresholds)
fpr_values = [0] * len(thresholds)
for i, threshold in enumerate(thresholds):
preds = [1 if prob >= threshold else 0 for prob in probs]
tp = sum([1 for p, l in zip(preds, labels) if p == 1 and l == 1])
fp = sum([1 for p, l in zip(preds, labels) if p == 1 and l == 0])
tn = sum([1 for p, l in zip(preds, labels) if p == 0 and l == 0])
fn = sum([1 for p, l in zip(preds, labels) if p == 0 and l == 1])
tpr_values[i] = tp / (tp + fn)
fpr_values[i] = fp / (fp + tn)
# 计算AUC
auc = -np.trapz(tpr_values, fpr_values)
(2)运行结果
总体代码如下:
import numpy as np
import matplotlib.pyplot as plt
def createDateSet():
# 创建数据集
group = np.array([[40, 101], [52, 89], [108, 25], [115, 38],[20, 95], [65, 88], [110, 10], [112, 12],
[3, 105], [57, 93], [105, 125], [113, 10],[15, 100], [10, 85], [107, 85], [115, 63],
[18, 90], [22, 87], [111, 29], [114, 51],[45, 103], [39, 91], [106, 42], [114, 37]])
labels = ['爱情片', '爱情片', '动作片', '动作片','爱情片', '爱情片', '动作片', '动作片',
'爱情片', '爱情片', '动作片', '动作片','爱情片', '爱情片', '动作片', '动作片',
'爱情片', '爱情片', '动作片', '动作片','爱情片', '爱情片', '动作片', '动作片']
return group, labels
def autoNorm(dataSet):
# 归一化数据
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
ranges = maxVals - minVals
normDataSet = (dataSet - minVals) / ranges
return normDataSet
def classify0(inX, dataSet, labels, k):
# kNN分类器
normDataSet = autoNorm(dataSet)
distances = np.sqrt(np.sum((normDataSet - inX) ** 2, axis=1))
sortedDistIndicies = distances.argsort()
classCount = {}
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
sortedClassCount = sorted(classCount.items(), key=lambda x: x[1], reverse=True)
return sortedClassCount[0][0]
def plotROC(labels, probs, title='ROC Curve'):
# 绘制ROC曲线
thresholds = np.unique(probs)
thresholds = np.append(thresholds, max(probs) + 1)
tpr_values = [0] * len(thresholds)
fpr_values = [0] * len(thresholds)
for i, threshold in enumerate(thresholds):
preds = [1 if prob >= threshold else 0 for prob in probs]
tp = sum([1 for p, l in zip(preds, labels) if p == 1 and l == 1])
fp = sum([1 for p, l in zip(preds, labels) if p == 1 and l == 0])
tn = sum([1 for p, l in zip(preds, labels) if p == 0 and l == 0])
fn = sum([1 for p, l in zip(preds, labels) if p == 0 and l == 1])
tpr_values[i] = tp / (tp + fn)
fpr_values[i] = fp / (fp + tn)
# 计算AUC
auc = -np.trapz(tpr_values, fpr_values)
# 绘图
plt.plot(fpr_values, tpr_values, label=f'ROC curve (AUC = {auc:.2f})')
plt.plot([0, 1], [0, 1], linestyle='--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title(title)
plt.legend()
plt.show()
if __name__ == '__main__':
# 创建数据集
group, labels = createDateSet()
# 测试数据
test = np.array([101, 39])
# 绘制数据集的散点图
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(group[:, 0], group[:, 1], c=[labels.index(l) for l in labels], cmap='viridis', label=labels)
ax.scatter(test[0], test[1], c='red', marker='x', label='Test Data')
# ax.legend()
ax.set_xlabel('Number of fights')
ax.set_ylabel('Number of kisses')
ax.set_title('Scatter Plot of Data Set')
plt.show()
# 不同k值下的ROC曲线
for k in [1, 3, 5, 7]:
# 计算测试数据点的概率
probs = []
for point in group:
dist = np.sqrt(np.sum((point - test) ** 2))
probs.append(dist)
# 绘制ROC曲线
plotROC([1 if label == '爱情片' else 0 for label in labels], probs, title=f'ROC Curve (k={k})')
五、总结
K最近邻(KNN)算法是一种简单且直观的监督学习算法,它的优势和劣势如下:
优势:
-
简单易理解: KNN 是一种直观的算法,易于理解和实现。它不需要对数据进行假设,也不需要进行模型训练,因此适用于初学者和快速原型开发。
-
无参数: KNN 是一种无参数的算法,因此不需要对数据进行假设,适用于各种类型的数据,包括线性和非线性数据。
-
适用于多类别问题: KNN 可以直接用于多类别分类问题,而不需要额外的修改或调整。
-
适用于非线性数据: KNN 可以很好地处理非线性数据,因为它不对数据做出任何假设。
-
适用于小数据集: 对于小型数据集,KNN 可以表现出良好的性能,因为它可以利用整个训练集进行预测。
劣势:
-
计算复杂度高: 随着数据量的增加,KNN 的计算复杂度会显著增加,因为它需要在预测时计算所有训练样本与测试样本之间的距离。
-
对异常值敏感: KNN 对异常值敏感,因为它是基于距离的算法,异常值可能会影响最终的预测结果。
-
需要选择合适的 K 值: KNN 需要选择合适的 K 值,即近邻的数量。选择不合适的 K 值可能导致过拟合或欠拟合。
-
不适用于高维数据: 随着特征维度的增加,KNN 的性能会下降,这是因为在高维空间中,距离度量变得不可靠,且需要更多的计算资源。
-
预测速度慢: 由于在预测时需要计算与所有训练样本的距离,因此 KNN 的预测速度较慢,特别是对于大型数据集。
总的来说,KNN 算法在简单性和直观性方面具有优势,适用于小型数据集和多类别问题。但是,它也存在一些限制,特别是在大型数据集和高维空间中的应用上。