首页 > 其他分享 >[博客入坑]CS231N assignment 1 _ KNN 知识 & 详细解析

[博客入坑]CS231N assignment 1 _ KNN 知识 & 详细解析

时间:2023-04-07 17:55:39浏览次数:48  
标签:KNN CS231N 训练 assignment 维度 train dists folds np

从零开始的全新博客


我之前一直在语雀上更新文章, 但是一会不更发现居然需要VIP才能发博客了:

sss

不过考虑到自己确实有一会没写博客了, 之前对神经网络在课上学过, 也鼓捣过pytorch, 但是深感自己没有系统学习过. 第一次接触这种公开课, 希望也能有种从零开始的感觉, 让自己面对这门课程能够更加专注.

那废话不多说, 下面我们就来做内容解析. 

环境配置(本地搭建windows)

请注意,下面是我整个过程的记录,如果你觉得太长不看, 可以直接转到最后一段.

首先我们从课程官网CS231n Convolutional Neural Networks for Visual Recognition(2019)上获取zip文件包. 为了方便, 我们需要在anaconda上搭建jupyter notebook的环境. 我本以为按照教程操作就可以了,网上博客也不少了, 没想到还是踩了许多坑呜呜呜

首先是教程上这句话:

 这里的"anaconda内置了许多科学用的python包"我没有太理解,感觉它只是一个python虚拟环境构建器而已, 确实用anaconda新建的环境是没有什么包的,但是我认真探索了一番, 才知道其base环境的确自带了很多有用的包,所以一些基础性的工程一般用base就没问题. (看下面就知道它自带的包有多丰富)

因此我们只需要在base环境输入jupyter notebook即可. 但是这里我又遇到了打不开的问题, 原谅我忘了截图, 但是原因和这个博客说的一摸一样: (2条消息) 启动Jupyter报错:ImportError: cannot import name ‘soft_unicode‘ from ‘markupsafe‘_ Laurence的博客-CSDN博客 按照这个做了之后, 虽然会报一些error, 但是不影响正常打开.

另外, 如果你是照着2016版本学习的, 会发现其残留了大量的python2时期的代码, 且本身代码bug较多, 比较搞人心态, 所以我建议用相对较新课程版本的代码.(我用的是2019年,之后的版本无法本地搭环境). 但是,又出问题了, 因为我的anaconda版本比较新,所以会出现各种功能(典型的是scipy的imread方法)被废弃的问题.因此最终我决定还是自己新建一个python3.7的环境, 按照pip install -r requirements.txt来确保需要的包版本完全一致. 这样之后,终于解决了环境问题, 按照.sh的内容下载数据集(也可以像我直接用wsl执行.sh文件), 最终完全配成功环境.

解说&作业: KNN和基本处理逻辑

上来我们就感受到了其和国内作业的不同, 作业是真的在指导你一步一步完成, 因此直接贴答案自然是不合适的, 我们还要学会阅读代码, 学习其中的思路.  

KNN本质上的思想极其简单,就是看它的旁边几个训练集的向量是什么类型, 少数服从多数决定输入位置向量的类别. 图像本质上也是向量, 但是其被折叠成了矩阵. 

jupyter的技巧

在jupyter, 可以用一些以%打头的内容来增强展示效果:

# 为了让matplotlib图像直接显示在jupyter页面做的操作
%matplotlib inline 
plt.rcParams['figure.figsize'] = (10.0, 8.0) # 设置画布尺寸
plt.rcParams['image.interpolation'] = 'nearest' # 插值方式
plt.rcParams['image.cmap'] = 'gray' # 颜色
# 随着内容改变, jupyter内容自动重载
%load_ext autoreload
%autoreload 2

不过需要注意,这些语句之后不能有空格和注释, 否则会出现错误, 这点和bash里面的export的"="左右不能有空格很像.

回到文档

随后在使用模型之前一般需要检查数据类型来确保模型的可用性,因为基础阶段的多数模型是不能自适应尺寸的,与此同时我们看到了图片本质是numpy的数组,图片为32*32的RGB图像:

随后大体是图像展示,缩减训练集和测试集,将图像转化为向量(注意这里调用的函数为  X_train = np.reshape(X_train, (X_train.shape[0], -1)   ,也就是除了第一维度剩下的flatten, 换言之输入的图片应当是行向量)等步骤, 训练过程只需要单纯将所有训练集记下来就可以. 只要照着代码思路阅读一般不会有问题.

为了不致博客冗长, 接下来开始只会摘录一些重要的内容继续解说.

当我们完成训练之后, 它要求我们用多种方式计算每个测试集和每个训练集的距离. 这里课程将测试集作为行, 训练集作为列, 希望得到的距离是一个矩阵. 能够直接用矩阵表达最后的结果是简洁的, 所以我们不该局限于用两层循环写出这个结果.

对于两层循环,答案是简单的, 即

dists[i,j] = np.sqrt(np.sum(np.square(X[i]-self.X_train[j]),axis=None))

 缩减到一层循环,即只循环测试集, 为此我们需要对单个测试集行向量和每个训练集行向量计算结果. 这里就不得不说到numpy的广播机制了. 

Numpy广播(Broadcasting)

网上对这个都有点不明不白, 我参考了官方文档说明: Broadcasting — NumPy v1.24 Manual 其广播机制大概用人话说就是: 我们列举两个矩阵的shape, 将其shape向右对齐之后, 每个部分维度必须满足: 相等/其中一个为1/不存在. 我认为这个例子最能说明:

而至于较小维度的那个就会直接将值不断复制,大概如这张图所示:

我们将A和B的维度向右对齐, 会发现第一维度下一个为8,一个不存在(其实元素数量和这个维度为1是一样的), 满足条件, 剩下的三个维度均满足其中一个是1的条件, 而这会导致这个维度的shape被推广到较大一者. 一个图像处理的例子如下:

我们将scale的维度同样处理,就知道scale会被复制256*256次, 也就是对256*256应当对应一个值. 如果你不能理解,建议你画一下图:

我们就知道, 上面的矩阵相乘(是元素相乘,不是矩阵乘法,和matlab的 .* 类似) 表达的意义是对RGB三通道进行不同的比例增强. 由此,我们就能体会到利用好这个原则可以简化代码书写, 避免走向一涉及多元素就想到循环的怪圈.

回到上面

于是, 我们按照同样的思路去思考现在一层循环的情况:

这样我们就知道了该怎么做. 结果呼之欲出. 需要注意, 我们的目标结果是500*5000的矩阵, 所以结果还需要转置一次,不过因为numpy中sum得到的是行(低维度),所以代码不用写出.

dists[i,:] = np.sqrt(np.sum(np.square(X[i,:] - X.train),axis=1))

 (在matlab中,因为没有广播机制,所以需要手动用repmat函数广播)

再来看看没有循环的结果.这也是最困难的, 为此我们需要做适当的变换,手动触发广播机制. 这个思路参考了网上的答案, 感觉真的巧妙,记录一下. 首先根据完全平方公式, (a-b)^2 = a^2 + b^2 - 2ab, 我们按照示意绘制出这样的图: 

根据这个图片的思路, 我们也可以写出结果:

dists += np.sum(X ** 2, axis=1,keepidms=True)
dists += np.sum(self.X_train ** 2, axis=1,keepidms=True).T
dists -= 2 * np.dot(X, self.X_train.T)
dists = np.sqrt(dists) # 开方

需要注意的是, 这里广播考虑到np.sum降低了维度, 根据广播机制的右对齐原则, 就不能再使用, 所以必须开启keepdims=True, 此时相加将保留维度特性,得到的应当是列向量, 所以train的dist需要转置为行,如上图.

下面我们写出KNN代码. 因为已经得到了距离矩阵,我们只需要得到距离最小训练集的index, 反查对应的类别即可. 其实这里自己写肯定是for循环检测, 不过多用Numpy的库也能充分应用到其优化, 也深感工具的实用性.

min_idx = np.argsort(dists[i,:])[0:k] # argsoft获得训练集idx索引
closest_y = self.y_train[min_idx] # 反向获取idx对应的类别
y_pred[i] = np.argmax(np.bincount(closest_y)) # bincount返回各个类别的出现次数(下标和数值对应), argmax返回最大的下标

k超参数的结果我们会在后面再讨论. 我们来看看计算距离的时间统计:

这个时间结果的原因: 首先不用loop的时候, 基本运算都是numpy库内进行, 而numpy是C底层,会快很多, 且其用到了很多加速和并行运算的技巧, 我自己也明显发现没有循环的CPU占用率明显高于剩下二者. 

最后是超参数k对于结果的影响. 这里用了一个方法来准确衡量准确度:

K折交叉验证 (k-fold cross validation)

这是实际上为探索超参数的最常用方法了,大概意思就是数据分成K份, K-1份训练,1份验证,且我们随机选取数次训练(当然这里直接就训练5次), 取平均得到准确率结果. 引用别的博客的话, 所有数据都会参与到训练和预测中,有效避免过拟合,充分体现了交叉的思想. 代码如下:

x_train_folds = np.array_split(X_train,num_folds)
y_train_folds = np.array_split(y_train,num_folds) # 分成5份,在竖直方向上分隔(默认,axis=0可以省略)


for k in k_choices:
    k_to_accuracies[k] = np.zeros(num_folds) # 预申请内存
    for i in range(num_folds):
        x_tr = x_train_folds[0:i] + x_train_folds[i+1:] # 因为array_split得到了列表,所以可以直接相加得到训练集
        y_tr = y_train_folds[0:i] + y_train_folds[i+1:] 
        x_tr = np.concatenate(x_tr,axis=0)# 上下叠放,axis意义同理
        y_tr = np.concatenate(y_tr,axis=0)
        # 测试集
        x_test = x_train_folds[i]
        y_test = y_train_folds[i]
        # 训练
        model = KNearestNeighbor()
        model.train(x_tr,y_tr)
        # 计算距离
        dists = model.compute_distances_no_loops(x_test)
        # 预测
        y_pred = model.predict_labels(dists,k=k)
        # 计算准确率
        acc = np.sum(y_pred == y_test)
        acc = float(acc) / (num_training / num_folds)
        k_to_accuracies[k][i] = acc
pass

# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

下面是结果:

可以看出, 随着K上升, 预测结果先增大后减小,在10-12准确度最好.当然实际上的准确率确实可以用菜鸡互啄来形容了, 而且KNN本身的复杂度很高(因为要对周围的所有点进行扫描),所以一方面用矩阵很有必要,另一方面KNN对所有像素点都考虑,完全没考虑特征, 所以受背景影响严重.

下面是练习题我的回答:

题目1

首先因为显示这个图是直接调用plt.imshow函数, 所以很显然, 白色就是dist小,黑色就是dist大. 产生bright row/column自然就是这个样本过于特殊, 导致这个测试/训练样本和所有的训练/测试样本都差异过大, 考虑到KNN是对整个图像亮度考虑, 只能说明图像亮度差异太大或色差太大(也就是RGB通道亮度差异), 或者图像损坏了.

题目2

1. 因为L1定义就是像素差的绝对值之和, 所有图像亮度减去同一个值, 相互之间的差不变,所以L1距离不变.

2. 但是如果每个图像亮度减的不同,那么相互之间的差会多出h*w*(μ之差), 会变化

3. 4. 同理分别是不会和会.

5. 旋转之后, 我个人感觉所有数据旋转的规则保证一致的条件下,flatten之后的xi和yi就应当一一对应, 求距离之后二者对应不变. 不过,如果仅仅一张图旋转而其他数据集不变的话, 确实会产生很大的影响, 因为这种对应关系就会被破坏. 

题目3

1. 肯定不是,因为本来就是选最近的,决策边界不可能不变.

2.3. 只能说未必, 超参数只是经验值, 其适用于大多数场合, 但不是所有场合都可以的.

4. 正确, 本来就必须一个个比较

标签:KNN,CS231N,训练,assignment,维度,train,dists,folds,np
From: https://www.cnblogs.com/360MEMZ/p/17294324.html

相关文章