作者注:本文所用数据集和部分资料来自于《机器学习实战》。
《机器学习实战》是一本被很多人推荐过的书,但是查看书评也有一些反对的声音。作者在学习完吴恩达的ML课程之后,想要通过一定的实践对机器学习知识进行巩固,因此选择这本书想把书里面的例子都实践一遍。
KNN算法是机器学习当中最简单的算法,这个算法甚至在Ng的课程中都没有被提到。也有人认为KNN不能算是一个机器学习算法,因为这个算法在执行时没有训练的过程。下面先简要地介绍一下这个算法。
KNN算法的核心思想就是比较距离。在给定的训练数据集中,对每一个测试数据求其对所有训练数据样本的距离。一般情况下这里的距离是指欧氏距离。即:
在书中是这么描述算法流程的:
- 收集数据:提供文本数据
- 准备数据:使用python解析文本数据
- 分析数据:使用Matplotlib画二维扩散图(散点图)
- 训练算法:无
- 测试算法:使用部分数据作为测试样本。如果预测分类与实际类别不同,则标记一个错误。
- 使用算法:自行输入一些特征来判断对方是否为自己喜欢的类型。
接下来分步讲述如何实现这个算法。这本书已经提供了一个文本文件作为数据,所以并不需要自己收集数据。所以程序从算法的第二步开始实践。
这个读入文本文件的过程基本上与书上没有太大差别,就是逐行读取文本文件中的数据并且写入一个矩阵当中。
def file2matrix(filename): #load data set
fr = open(filename)
numberOfLines = len(fr.readlines()) #get the number of lines in the file
returnMat = np.zeros((numberOfLines,3)) #prepare matrix to return
classLabelVector = [] #prepare labels return
fr = open(filename)
index = 0
for line in fr.readlines():
line = line.strip()
listFromLine = line.split('\t')
returnMat[index,:] = listFromLine[0:3]
classLabelVector.append((listFromLine[-1]))
index += 1
return returnMat,classLabelVector
在完成准备数据之后,作者发现数据的标签是字符形式,而不是数字类型。(看网上说第二个数据集的标签是数字,不是字符)所以这里还要继续做一步预处理,即将字符型标签转换为数字类型的标签。做法简答粗暴:就是遍历每一组数据的标签并覆盖掉原来的标签。这里两类标签的对应关系如下表所示:
字符标签 | 数字标签 |
---|---|
didntLike | 1 |
smallDoses | 2 |
largeDoses | 3 |
代码如下:
def y_classify(y): # convert the string-type label to int
for i in range(y.shape[0]):
if y[i] == "didntLike":
y[i] = 1
elif y[i] == "smallDoses":
y[i] = 2
else:
y[i] = 3
return y
在数据可视化的过程中需要用到matplotlib.pyplot库。由于在画图中想把三类不同的标签的点画成不同的颜色,所以必须首先把有相同标签的样本数据写到一个数组里,然后对三个类别的样本分别绘图。这里需要注意的是,在pycharm中必须加入这两行语句,才能在运行时正常显示。
def plot_data(x,y):
type1_x = [] # define three pairs of array to store the classification data
type1_y = []
type2_x = []
type2_y = []
type3_x = []
type3_y = []
for i in range(len(y)): # to classify the data
if y[i] == '1':
type1_x.append(x[i][0])
type1_y.append(x[i][1])
if y[i] == '2':
type2_x.append(x[i][0])
type2_y.append(x[i][1])
if y[i] == '3':
type3_x.append(x[i][0])
type3_y.append(x[i][1])
plt.scatter(type1_x, type1_y, s=20, c='r', label='didntLike') # plot data
plt.scatter(type2_x, type2_y, s=40, c='b', label='smallDoses')
plt.scatter(type3_x, type3_y, s=60, c='k', label='largeDoses')
plt.legend() # show plot
plt.show()
绘图的结果如下图所示:
在这张散点图中我们发现样本的三个特征的数存在明显的倾斜(skew)现象,即特征向量的数据差数量级。在这种情况下,我们应该对数据进行归一化, 即每一个样本数据都减去其平均值再除以极差。
其运行的代码如下:
def normalize(matrix):
matrix_mean = sum(matrix)/len(matrix)
min_value = matrix.min(0)
max_value = matrix.max(0)
ranges = max_value-min_value
norm_matrix = (matrix-matrix_mean)/ranges
return norm_matrix
接下来要对数据集进行分割,即分出来训练集和测试集。由于这个数据本身就是随机的,没有固定的数据,我们可以通过生成随机数来选取90%的数据为训练样本,余下的10%为测试样本。然后通过计算测试样本与每一个训练样本的距离来预测测试样本的标签。
def classify(x_train, y_train, x_test):
y_label = np.zeros(x_test.shape[0])
for i in range(x_test.shape[0]):
dis = 100
y_label[i] = 1
for j in range(y_train.shape[0]):
dis_temp = sum((x_test[i,:]-x_train[j,:])**2)
if dis_temp < dis:
dis = dis_temp
y_label[i] = y_train[j]
return y_label
最后通过统计错误分类的样本数计算分类器的错误率。
x,y=file2matrix("datingTestSet.txt")
x = np.array(x) # vectorized x and y
y = np.array(y)
y = y_classify(y)
fig = plt.figure() #creat a empty plot
ax = fig.add_subplot(111) #choose the location of the plot
plot_data(x,y) # data visualization with the first two features
for i in range(x.shape[1]): # normalize training data
x[:,i] = normalize(x[:,i])
rand_arr = np.arange(x.shape[0])
np.random.shuffle(rand_arr)
y_result = classify(x[rand_arr[0:900],:],y[rand_arr[0:900]],x[rand_arr[900:1001],:])
y_result = y_result.astype(int)
yy = y[rand_arr[900:1001]].astype(int)
error_rate = len(np.flatnonzero(y_result-yy))/100
print(error_rate)
由于之前一直使用MATLAB写程序实现算法,刚上手python还不太适应。在调试程序的过程中出现了一些错误,这里也记录下来。
- 在矩阵索引中,python的下表是从0开始的,而且并不包括索引下标的右边界。所以右边界要比实际上需要的索引数字多1。
- 在计算出预测分类值之后,使用正确标签的矩阵y[rand_arr[900:1001]]与预测矩阵y_result相减时总是报错,说二者类型不一致。经过查看dtype发现:一个是 float64 而一个是 <U10 。通过上网查阅发现矩阵的dtype并不能直接更改,只能通过astype函数进行更改。
- 在对矩阵进行切片时并不创建新的矩阵,如果要对矩阵切片操作又不希望改变原来的矩阵,最好创建新的矩阵。
完整程序和元数据集见GitHub_OrangeCat95_ML-in-Action_KNN1_date_website。