最近因为学校里的课程需求,开始学习cs231n,没有自己完整的写代码,主要参考了知乎上一个专栏中一位大神的作业(结尾处有链接),并理解了每一行代码的作用,此篇博客主要记录了在学习别人的完整的代码的过程中遇到的一些问题,代码在python3.6.4,pycharm上测试成功。pycharm中为主要代码,其余部分为学习过程中遇到的一些生疏知识点,和对代码中一些难以理解的部分的说明。
数据载入部分:
完整代码如下:
# encoding: utf-8 import pickle import numpy as np import os def load_cifar_batch(filename): with open(filename, 'rb') as f: datadict = pickle.load(f, encoding='bytes') x = datadict[b'data'] y = datadict[b'labels'] x = x.reshape(10000, 3, 32, 32).transpose(0, 2, 3, 1).astype('float') y = np.array(y) return x, y def load_cifar10(root): xs = [] ys = [] for b in range(1, 6): # 路径拼接,跟下载好的文件名有关 f = os.path.join(root, 'data_batch_%d' % (b,)) # 将拼接好的文件名传入上面定义的load_cifar_batch函数中,得到数据x和标签y x, y = load_cifar_batch(f) xs.append(x) ys.append(y) # 因为共有五个训练集,所以将得到的结果分别拼接到Xtrain和Ytrain中 Xtrain = np.concatenate(xs) Ytrain = np.concatenate(ys) # del删除变量,而该变量对应的值在内存中不会删除,因此当把值赋给xs,ys后,虽然删除x和y,但xs和ys中的值不会消失 del x, y Xtest, Ytest = load_cifar_batch(os.path.join(root, 'test_batch')) return Xtrain, Ytrain, Xtest, Ytest一些之前不熟悉的函数:
# x格式为array,reshape重新调整x的维数
# transpose对array进行转置,例如a.transpose().
x = x.reshape(10000, 3, 32, 32).transpose(0, 2, 3, 1).astype('float')
#在reshape括号内的参数中,可以利用-1来自动计算相应维度
a = np.arange(8)
# 此时-1即代表2,因为此array中有8个元素,第一维设置成了4,第二维只能为2
a = a.reshape(4, -2)
# 因此此时的-1即代表4
a = a.reshape(-1,2)
# 此外在在numpy中reshape有两种写法,下面的写法的作用同上
a = np.reshape(a, (-1, 2))
一开始不太理解transpose括号内有参数的用法,例如上例中的transpose(0, 2, 3, 1)。可以结合索引来理解,例如有一个三维矩阵a,其中元素11的索引值为a[1, 0 ,2], 经过a = np.transpose(a, (0, 2, 1)) 后,元素11的索引值也就变成了a[1, 2, 0],因为transpose是将括号内的参数中代表的第二维和第一维索引调换了顺序,因此该转置本质上只对第一维和第二维元素实现了转置操作,同理可将括号内参数扩大到4位到多位。
a = np.arange(27)
a = a.reshape(3, 3, 3)
print(a[1, 0, 2])
a = np.transpose(a, (0, 2, 1))
print(a[1, 2, 0])
# 对多个矩阵或序列进行合并。可在括号内加上参数axis,取值可为0或1,取值为1时沿水平方向合并,取值为0时延垂直方向合并,
# axis的默认值为1,即按水平方向合并。
np.concatenate()
knn计算距离部分代码:首先定义了类KNearestNeighbor,然后定义了三种计算欧式距离的方法,其中最后一种的方法最优。
# encoding: utf-8 # knn算法定义距离的核心代码 import numpy as np class KNearestNeighbor: def __init__(self): pass def train(self, X, y): self.X_train = X self.y_train = y # num_loops表示采用哪种方式计算欧氏距离(默认采用第三种) def predict(self, X, k=1, num_loops=0): if num_loops == 0: dists = self.compute_distances_no_loops(X) elif num_loops == 1: dists = self.compute_distances_one_loop(X) elif num_loops == 2: dists = self.compute_distances_two_loops(X) else: raise ValueError('Invalid value %d for num_loops' % num_loops) # 通过compute_distances_no_loops计算距离,得到dists,再把dists传入预测函数predict_labels中 return self.predict_labels(dists, k=k) def compute_distances_two_loops(self, X): num_test = X.shape[0] num_train = self.X_train.shape[0] # 初始化存放距离的矩阵,先都赋初值0 dists = np.zeros((num_test, num_train)) print(X.shape, self.X_train.shape) for i in range(num_test): for j in range(num_train): dists[i, j] = np.sqrt(np.sum((X[i, :]-self.X_train[j, :])**2)) return dists def compute_distances_one_loop(self, X): num_test = X.shape[0] num_train = self.X_train.shape[0] dists = np.zeros((num_test, num_train)) for i in range(num_test): dists[i, :] = np.sqrt(np.sum(np.square(self.X_train-X[i, :]), axis=1)) return dists # 该方法在时间上明显优于前两种方法(速度一两秒,而前两种方法需要几十秒) def compute_distances_no_loops(self, X): num_test = X.shape[0] num_train = self.X_train.shape[0] dists = np.zeros((num_test, num_train)) test_sum = np.sum(np.square(X), axis=1) train_sum = np.sum(np.square(self.X_train), axis=1) inner_product = np.dot(X, self.X_train.T) dists = np.sqrt(-2*inner_product+test_sum.reshape(-1, 1)+train_sum) return dists def predict_labels(self, dists, k=1): num_test = dists.shape[0] y_pred = np.zeros(num_test) for i in range(num_test): closest_y = [] # 将距离按照从小到大的顺序排列,输出对应的索引序号;dists[i, :]代表第i行的所有列 y_indicies = np.argsort(dists[i, :], axis=0) # 输出前k个图像的类别,k为1的时候该语句没多大作用,k>1是,将前k个 类别值 存入closet_y中 # 注意y_indicies[: k] 此处的切片对第一维限定为从第0到第k-1行,没有对第二维做出限定,即前k-1行的所有列 closest_y = self.y_train[y_indicies[: k]] # 对得到的k个数进行投票,选取出现次数最多的类别作为最后的预测类别,这一句代码很精妙!!! # bincount的作用:返回closet_y中每个数字出现的次数(以列表或者所是array的形式),列表的索引即对应closet_y中标签中的数值,索引对应的值为该索引对应的值出现的次数。 # argmax的作用:返回最大值的索引,因为先使用了bincount,最大值的索引即为closet_y中出现次数最多的标签值!!! y_pred[i] = np.argmax(np.bincount(closest_y)) return y_pred
生疏的函数如下:
# numpy切片问题:
# 一维numpy的切片类似于python中列表的切片方式
# 对于多为numpy切片问题,见如下几个例子,将多维拆解成几个一维并且主要记住冒号:在不同位置的用法即可
# 产生一个4*4的矩阵a
a = np.arange(16).reshape(4, 4)
# 将输出第二行第二列对应的数值5
print(a[1, 1])
# 将输出第一行到第三行中第二列的值。
print(a[:3, 1])
# 也可以省略某一维的参数,这意味着省略的那一维的数据全部输出
# 输出第一行到第三行中所有列的值
print(a[:3]
# 其他一些用法
# a为一个四行一列的矩阵
a = np.array([[0], [1], [2], [3]])
# 按矩阵的维度抽取相应元素组成一行元素的用法。将a中所有行的第一列元素,元素组合成b,b为一个列表,只有一维,b的shape为[4,]
# b = a.flatten()函数也可以实现同样的功能
b = a[:, 0]
# 这种定义方法a的shape为(2, 2, 1),多了一维
a = np.array([[[0], [1]], [2], [3]]])
# 下面语句实现把三维的a转化为一个行向量,维数必须对应上,同理flatten可实现同样功能
# 建议使用flatten来实现,就不用先观察矩阵的维度了
b = a[:, 0, 0]
predict_labels函数中倒数第二行y_pred[i] = np.argmax(np.bincount(closest_y))的用法说明
# bincount函数的用法
x = np.array([0, 1, 1, 3, 3, 3, 3, 5])
# bincount函数的返回结果为一个列表,其中列表的索引于x中的值对应,
# 例如此例中x中的最大值为5,则bincount函数返回的列表的索引为0到5,每个索引对应的值为该索引对应的数字出现的次数(有点绕,看输出结果理解一下)
y = np.bincount(x)
print(y)
输出结果-》 [1 2 0 4 0 1]
# numpy里的argmax函数返回括号内参数中最大值对应的索引值,y的最大值为4,对应的索引值为3,因此返回结果为3
# 这两个函数的结合因此实现了对多个类别中出现次数最多的类别进行统计并输出的功能!!!
z = np.argmax(y)
输出结果为 3
这两个函数的结合因此实现了对多个类别中出现次数最多的类别进行统计并输出的功能。因为在这个作业中图片的类别是以数字来表示的,因此等价于实现了计算一堆数字中出现次数最多的那个数字出现的次数。
数据测试部分函数:测试部分取交叉验证方式
# encoding: utf-8 import numpy as np from assignment1.data_utils import load_cifar10 import matplotlib.pyplot as plt from assignment1.knn import KNearestNeighbor # 地址为我本机电脑保存图片数据集的位置,根据个人图片实际存放位置进行修改! x_train, y_train, x_test, y_test = load_cifar10('/Users/231HomeWorks/assignment1/cs231n/datasets/cifar-10-batches-py/') # 交叉测试,将训练集分成份,4份作为训练集,1份作为测试集,当选取当前份作为测试集时,其他份作为训练集!如此循环 num_training = 5000 mask = range(num_training) x_train = x_train[mask] y_train = y_train[mask] print(y_train.shape) # 此处的y_train为标准格式,行向量格式!! print(y_train) print('正在计算:') # 将数据拉长成长向量,便于计算欧式距离,注意此处并非一维向量,将每个图片的像素值拉长成一维向量,但是因为有多张图片,故此处reshape后变成二维向量 # 注意此处reshape的用法,第二种用法为x_train = x_train(x_train.shape[0], -1) 。 # shape[0]代表第一维的个数,此处即为图片数,而-1位自动匹配,总元素数除以shape[0]的值即为-1代表的值 x_train = np.reshape(x_train, (x_train.shape[0], -1)) x_test = np.reshape(x_test, (x_test.shape[0], -1)) # 交叉验证,分成五份 num_folds = 5 # k值得取值范围列表 k_choices = [1, 3, 5, 8, 10, 12, 15, 20, 50, 100] x_train_folds = [] y_train_folds = [] # y_train本身为行向量的形式,该条语句将y_train转化为列向量的形式 # 如果不用此语句进行转化,那么在此后的vs.stack()操作中会把行向量转化为多行的形式(矩阵形式), # 那么之后需要利用flatten()从新把多行矩阵的形式平铺为放为行向量形式。 y_train=y_train.reshape(-1,1) x_train_folds=np.array_split(x_train,num_folds) y_train_folds=np.array_split(y_train,num_folds) k_to_accuracies={} for k in k_choices: k_to_accuracies.setdefault(k,[]) for i in range(num_folds): classifier=KNearestNeighbor() # 递归取五份中的第i份作为测试集,其余四份进行拼接作为训练集 # x_train_folds为一个列表,但是列表中的元素为numpy。因此下面的语句会先进行 + 操作,对应的是列表拼接操作,之后在进行vstack操作。 x_val_train=np.vstack(x_train_folds[0:i]+x_train_folds[i+1:]) # y_val_train = np.vstack(y_train_folds[0:i] + y_train_folds[i + 1:]) y_val_train=y_val_train[:,0] # y_val_train = y_val_train.reshape(1, -1),这种方式不行,虽然转化了一行的形式,但最终该变量仍为矩阵形式,而不是行向量形式 # flatten操作可以完美替代上面的[:, 0]操作 # y_val_train = y_val_train.flatten() print(y_val_train.shape) classifier.train(x_val_train,y_val_train) for k in k_choices: # 调用predict函数进行预测 y_val_pred=classifier.predict(x_train_folds[i],k=k) # 测试集部分仍然需要通过[:, 0]转换成行向量的形式,如果前面不将y_train转化为一列的形式,此处不需要[:, 0]操作 num_correct=np.sum(y_val_pred==y_train_folds[i][:,0]) # 测试集部分 accuracy=float(num_correct)/len(y_val_pred) k_to_accuracies[k]=k_to_accuracies[k]+[accuracy] # 计算准确度的函数 for k in sorted(k_to_accuracies): sum_accuracy=0 for accuracy in k_to_accuracies[k]: print('k=%d, accuracy=%f' % (k,accuracy)) sum_accuracy+=accuracy print('the average accuracy is :%f' % (sum_accuracy/5))
该部分函数在当时写作业的时候很不明白的就是为什么需要先把y_train通过reshape.(-1, 1)转化为多行一列的形式,关于这一点,已在上面的pycharm语句中的相关注释部分进行了详尽的说明。建议不进行转化,而在之后利用flatten函数进行展开。
在做这个作业的时候,最大的障碍不是算法的理解,因为knn这个算法原理非常简单,相反,最困难的部分是对于python中很多的函数都不熟悉,之前几乎没怎么用过python,numpy模块也是做作业前临时学了一两个小时,对于numpy中维度的变换经常转不过来,经验就是多用print来输出当前变量的状态,再就是新建一个专门用来测试的py文件,用同类型的元素少的变量来进行实验。
代码部分参考了知乎上WILL的专栏,https://zhuanlan.zhihu.com/p/28204173 ,建议结合两篇一块参考。如果对您有帮助,请点个赞,欢迎留言一块讨论。