前言
认识kNN
kNN,k个最近的邻居。顾名思义,对于每一张预测图像(X_test),kNN 把它与训练集(X_train)中的每一张图像计算距离,找出距离最近的k张图像。这k张图像就是这张我们预测图像的“邻居”了,而邻居里占多数的标签类别(y_test)就是预测图像的类别。
数据的形状含义:
• X_train[5000×Dimensions]:行数(5000)代表训练图集一共有5000张图;列数(Dimensions)代表每一张图具体的像素信息32*32*RGB=3072,以下我会将这个“每张图具体的像素信息”称为 dimensions 或 D 。
• X_test[500×Dimensions]:行数(500)代表我们待测试的图像一共有500张;列数代表 D 。
• y_train[5000] / y_test [500]:y 为一个一维数组,代表每张图像所对应的标签。
TODO1:计算距离
由于并没有哪个方向上的坐标是值得带权重的,这里选择采用L2距离公式 (Euclidean distance) 。
【思路】将每行 X_test 的图像与 X_train 库的图像的 D 相减,之后将两者 D 的差求平方、求总和、然后把得到的那个和做开根,最后将结果存进 [500*5000] 大的数组dist里。
再重复一遍就是四步走:相减、平方、求和、开根。
按照思路,刚开始写出来的代码是
dist[i,j] = np.sqrt(np.sums(np.square(X_test[i] - X_train[j])))
【开始 Debug】通过报错,可以看到错的都是些拼写上的“瑕疵”,以后一定要多注意这些细节并加以训练啊:dists, np.sum, X[i], self.X_train[j]...
不过这里值得一提的是,开方也可以通过 (X[i] - self.X_train[j])**2 的形式实现。
【修改代码】
dists[i,j] = np.sqrt(np.sum(np.square(X[i] - self.X_train[j])))
成功通过。
【思考提升】有两重循环的代码往往是不能被容忍的,接下来一定要优化这个算法。
TODO2:开始预测
当我们把 dists 都算出来后,可以开始挑选每张测试图像的标签最靠近哪个训练库里的图像标签了。
【思路】直观地来思考,上面算的 dists 值越小,某两张图就越相似。但是我们要用的是 kNN 算法,所以必然要建一个数组来存 kNN 的值,之后再在数组里挑出那个占大多数的标签。
重复一遍:循环地取每行最小的k个值,放进数组里,然后竞选大多数的k标签。
提示:先将值的数组排好序会比所谓的“循环地取每行最小的k个值”容易操作得多。
closest_y = self.y_train[np.argsort(dists[i])[:k]]
y_pred[i] = np.argmax(closest_y)
【开始 Debug】准确率对不上,发现argmax 函数是返回数组里值最大的数,我们需要的是出现次数最“多”的的值,继续找 API ,再加入bincount 函数。
【修改代码】
y_pred[i] = np.argmax(np.bincount(closest_y))
结果正确。
【思考提升】当我们需要某个具体的操作时,应如何借助 API 达到我们的目的?这是我需要提升的一个技能点。
TODO3:优化距离算法
【思路】之前是将每一张 test和训练集里的每一张 train 做比较,不妨利用广播直接把 test直接“播”进整个训练集里,这样就可以少一层循环。
重复一遍:把 test[i] 一整行的数据,播进train 里,再对整个 train 集做 L2 距离计算。
dists[i,:] = np.sqrt(np.sum(np.square(self.X_train – X[i])))
【开始 Debug】距离矩阵与结果对不上,重新脑补我的 L2 的计算过程,发现 sum 用在二维数组时,是默认按行加而不是在一行里取和的。
【修改代码】
dists[i,:] = np.sqrt(np.sum(np.square(self.X_train – X[i]), axis = 1))
结果正确。
【思考提升】关于无循环的算法,我是没有想到。通过提示,我才知道原来要把“相减再平方”的公式展开变成平方和公式,真心佩服这种敢于直接对公式大动干戈的行为!虽然这一层面的想法我不是自己想出来的,不过还是请让我自己写一遍代码吧:
dists = np.dot(X,self.X_train.T) * -2 #500*5000
dists +=np.sum(np.square(X), axis = 1, keepdims = True) #test 竖着播
dists +=np.sum(np.square(self.X_train), axis = 1)
dists = np.sqrt(dists)
TODO4:调整超参数k
k 究竟取多少才合适呢?我们可以利用交叉验证(Cross Validation) 来得到最优的k。
【思路】对每一份小集都取不同的 k进行预测,用一个字典存不同 k 所带来的准确率。
for k in k_choices:
for i in num_folds:
y_pred =classifier.predict(X_test, k = k)
accuracies[i] = np.sum(y_pred == y_test) / float(num_test)
k_to_accuracies[k] = accuracies
【开始 Debug】首先出现了num_folds 是一个整数不让我在 for 里用;其次 'accuracies' is not defined ,让我很惊讶要用一个数组原来是需要先定义的?最后还是通过不了,再回去重读一般题目的描述,发现是每一次执行中,选择一个fold 为验证集,其它的则为训练集。那怎么保证同一个 fold 被多次使用然后值又不改变呢?
【修改代码】
for k in k_choices:
accuracies = np.zeros(num_folds)
for i in range(num_folds): #对 k 值的第 i 个主 fold 做以下验证
X_new_train = X_train_folds[:]
y_new_train = y_train_folds[:]
X_val =X_new_train.pop(i)
y_val =y_new_train.pop(i)
X_new_train =np.array([ele for x in X_new_train for ele in x])
y_new_train =np.array([ele for y in y_new_train for ele in y])
classifier =KNearestNeighbor()
classifier.train(X_new_train, y_new_train)
y_pred =classifier.predict(X_val, k = k)
accuracies[i] =np.sum(y_pred == y_val) / float(num_test)
k_to_accuracies[k] = accuracies
【思考提升】X_new_train 是一个含有4个 folds 的三维数组,要想当真正能用的训练集,得把里面的元素拿出来重排;y 同理。
通过该份作业我所收获的技巧
• reshape函数的 “-1” 用法可快速得到我们想要的数组形状:
>>> a = np.arange(6)
>>> np.reshape(a, (3,-1)) #3行2列
• NumPy的广播和 dot() 特别好用,当我们的算法有多重循环时,应考虑使用向量运算提升效率。但是在用 sum 时,应特别注意是要往哪个 axis 上做运算!
• 对于超参数的调参,往往用的是Cross validation 的方法,因此要建立一个数组对超参进行收集存放。
• 对于API 的查找,我们可以输入关键字去搜索(比如上面的 count );或者先在百度搜索下,然后再在API文档中搜索。