深度学习系列之cs231n assignment1 KNN(二)

写在前面:久经周折,终于能够将KNN系列给大家继续分享了,这次的内容来源于李飞飞教授团队的cs231n深度学习课程的作业1中的KNN研究,我会在全文我遇到困难的地方进行分享,以及一些想法。

内容安排

深度学习系列依托与cs231n的课程作业,因为只想练习编程,所以不对课程内容进行分享,仅针对编程内容进行分享。那么这一次的分享就是assignment1中K近邻分类器的使用,以及完成其中的四个问题,这四个问题我会在需要时讲解一下。在这里的编程中我们将主要使用的是numpy包的矩阵运算。
在本文中我们将分成1.任务了解、2.数据下载载入问题、3.模型训练细节说明与模型运行三个部分,那么我们首先来对任务进行一下说明。

1.任务了解

在cs231n的assignment1中关于KNN的任务,主要就是对已有的CIFAR10数据集进行预测计算,对于具体的CIFAR10数据集介绍和KNN算法课件cs231n课程以及相应机器学习内容。
在本次作业中我们会遇到这么几个问题,1.数据下载问题;2.数据载入问题;3.任务的完成,将这些问题解决后,还剩下交叉验证的问题,会在以后进行更新。

2.数据下载和载入问题

2.1数据下载
我们所需的数据如果是去官网上下载或许会太慢(真的慢),那么我们可以去
我的百度网盘下载我上传的数据
链接:https://pan.baidu.com/s/19iisthUMAOq07QOWCXf5gg
提取码:pops
这样我们就解决了第一个下载慢的问题,然后呢我们需要到cs231n课程的官网去下载课程作业cs231n课程官网,但好像下载需要翻墙,大家也可以去githubcs231n课程github上下载,如果大家都下载不下来,那就来我的百度网盘下载,
链接:https://pan.baidu.com/s/1ObBA4anAKUkTesYo-ydD7A
提取码:5e1p
好了这样我们就解决了数据下载和作业文档下载了。这个作业需要使用jupyter notebook的哈,然后我们需要编辑的是knn.ipynb和作业文件中cs231n\classifiers中的k_nearest_neighbor.py文件,这两个文件就是我们作业以及编程实现的地方。

2.2数据载入问题
数据载入这里很多朋友遇到了困难(也有可能就我遇到了困难哈哈哈),从我的小白水平出发,首先在打开knn.ipynb,在运行第一段代码时需要删除,

from __future__ import print_function

然后就是第一段代码,

#加载包
import random
import numpy as np
from cs231n.data_utils import load_CIFAR10 #这个是文件夹里带的
import matplotlib.pyplot as plt

#设置绘图参数
%matplotlib inline
plt.rcParams['figure.figsize'] = (10.0, 8.0) # set default size of plots
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'
%load_ext autoreload
%autoreload 2

然后我们就要开始载入数据,下载好的数据解压后放在cs231n/datasets的目录下,当然也可以自行更改,这个地方由于我很小白导致很多地方都搞不懂,我这里遇到了主要问题是,虚拟环境内存太小程序无法运行,

cifar10_dir = 'cs231n/datasets/cifar-10-batches-py' #数据文件夹路径
X_train, y_train, X_test, y_test = load_CIFAR10(cifar10_dir)

print('Training data shape: ', X_train.shape)
print('Training labels shape: ', y_train.shape)
print('Test data shape: ', X_test.shape)
print('Test labels shape: ', y_test.shape)

其报错为,Memory Error然后里面没有任何内容,这种经过我仔细排查应该就是数据太大运行不了导致的,所以大家可以参考这一篇
数据量太大python运行不了怎么办
我使用的是其中的第三个方法,就手动扩展虚拟变量内存,按照步骤来后就解决了数据的读入,正确的输出是以下这样,

Training data shape:  (50000, 32, 32, 3)
Training labels shape:  (50000,)
Test data shape:  (10000, 32, 32, 3)
Test labels shape:  (10000,)

我们将数据分为训练数据以及测试数据两部分,训练数据有50000个32乘32像素的数据,测试集有10000个32乘32像素的数据,还有训练和测试两个标签值,对于CIFAR10数据集,里面有已经分好的10个类别,有什么飞机、汽车、猫等等。至此我们的数据就加载其中了,下面就开始对于完成任务的细节进行说明。

3.模型训练细节说明与模型运行

3.1数据查看和处理
下面我们开始运行文档中已有的程序,首先我们来查看已有数据

classes = ['plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
num_classes = len(classes)
samples_per_class = 7
#enumerate 产生一个可迭代的序列,如0 plane,1 car
for y, cls in enumerate(classes):
    #flatnonzero返回扁平化后矩阵中非零元素的位置,此处表示y_train中等于y的位置,也就是每一类的索引
    idxs = np.flatnonzero(y_train == y) 
    #choice,表示从idxs中随机选取samples_per_class个不重复的样本,这里选取的就是从某一类中抽取7个图片的索引
    idxs = np.random.choice(idxs, samples_per_class, replace=False)
    for i, idx in enumerate(idxs):
        #plt_idx设置subplot的最后一位,因为是横向排列所以先乘以类别数到下一循环中
        plt_idx = i * num_classes + y + 1
        plt.subplot(samples_per_class, num_classes, plt_idx)
        #图片展示
        plt.imshow(X_train[idx].astype('uint8'))
        #关闭坐标轴
        plt.axis('off')
        if i == 0:
            #在一开始生成标签类别
            plt.title(cls)
plt.show()

在这里插入图片描述
下面对训练集和测试集进行处理,

# 缩减数据规模提高运行速度
num_training = 5000 #在做任务实验的时候建议改小数据到50的训练集,5的测试集减少运算时间
mask = list(range(num_training))
X_train = X_train[mask]
y_train = y_train[mask]

num_test = 500
mask = list(range(num_test))
X_test = X_test[mask]
y_test = y_test[mask]
# 将像素矩阵转换为行向量
X_train = np.reshape(X_train, (X_train.shape[0], -1))
X_test = np.reshape(X_test, (X_test.shape[0], -1))
print(X_train.shape, X_test.shape)
(5000, 3072) (500, 3072)

3.2模型训练
在此处我们需要导入课程提供的一个k近邻的包,这个包里面需要我们完成(1.二重循环、2.一重循环、3.无循环、4.测试函数)这四个小函数,那么我们就先对这四个函数进行完成,再来调用这个包进行程序的运行观看效果,这里我们需要打开cs231n/classifier的k_nearest_neighbor.py文件,让我们来看看完整的代码,我将代码的要求按照自己理解翻译成了英文,

import numpy as np
from past.builtins import xrange

class KNearestNeighbor(object):
    """计算欧氏距离"""
    def __init__(self):
        pass

    def train(self, X, y):
        """
        train函数就是用来输入数据的。
    
        输入项:
        X:输入的X是一个行数为num_train,列数为32乘32乘3的numpy array数组
        y:输入的是与X行数相同维度的向量,包括的是X的类别,y[i]是X[i]的标签
        """
        self.X_train = X
        self.y_train = y
    
    def predict(self, X, k=1, num_loops=0):
        """
        使用这个函数进行预测

        输入项:
        X:输入的X是一个行数为num_train,列数为32乘32乘3的numpy array数组
        k:最近的k个相邻标签
        num_loops:决定使用哪种循环计算距离

        返回项:
        y:返回一个num_test维度的numpy array向量,这个向量是关于测试数据的标签
        """
        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)
    
        return self.predict_labels(dists, k=k)

    def compute_distances_two_loops(self, X):
        """
        计算每个测试数据X与每个训练数据self.X_train之间的距离, 使用双循环完成

        输入项;
        X:输入一个行为num_test,列为32乘32乘3的np.array数据

        返回项:
        dists:返回的是测试数据和训练数据两两之间的欧式距离
        """
        num_test = X.shape[0]
        num_train = self.X_train.shape[0]
        dists = np.zeros((num_test, num_train))
        for i in xrange(num_test):
            for j in xrange(num_train):
                """
                ######################################################
                # 任务1目标:                                        
                # 计算第i个数据与第j个数据之间的欧式距离  
                # 然后储存在dists[i,j]
                ######################################################
                """
            pass
        return dists

    def compute_distances_one_loop(self, X):
        """
        使用一个循环来计算数据之间的距离
        输入项和输出项与compute_distances_two_loops相同
        """
        num_test = X.shape[0]
        num_train = self.X_train.shape[0]
        dists = np.zeros((num_test, num_train))
        for i in xrange(num_test):
            """
            #####################################################
            # 任务2目标:                                
            # 使用一重循环计算数据之间的欧式距离            
            # 然后储存在dists[i,:]    
            #####################################################
            """
            pass
        return dists

    def compute_distances_no_loops(self, X):
        """
        不使用循环来计算数据之间的距离
        输入项和输出项与compute_distances_two_loops相同
        """
        num_test = X.shape[0]
        num_train = self.X_train.shape[0]
        dists = np.zeros((num_test, num_train))
        """
        #####################################################
        # 任务3目标:                                
        # 不使用循环计算数据之间的欧式距离           
        # 然后储存在dists,并且不能使用scipy库,主要使用矩阵计算    
        #####################################################
        """
        pass
        return dists

    def predict_labels(self, dists, k=1):
        """
        接受一个测试数据与训练数据的距离矩阵,然后返回测试数据的类别

        输入项:
        dists:测试数据的个数为行数,训练数据的个数为列数的numpy array矩阵,给出第
                i个测试数据与第j个训练数据之间的关系

        返回项:
        y:返回一个测试集的预测类别
        """
        num_test = dists.shape[0]
        y_pred = np.zeros(num_test)
        for i in xrange(num_test):
            closest_y = []
            """
            #####################################################
            # 任务4目标:                                
            # 利用距离矩阵去寻找离i个测试数据最近的k个训练数据
            # 利用y_trian数据去寻找这些邻居的类别				            
            # 将这些类别储存在closet_y中					    
            #####################################################
            """
            pass
            """
            #####################################################
            # 任务5目标:                                
            # 从closet_y中寻找着k个最近的邻居中占比最多的是哪个类别
            # 然后将这个类别储存在y_pred[i]中  
            #####################################################
            """
            pass
        return y_pred

看完了任务要求,我们就要对其中的任务编写代码来完成计算,总的思路就是利用双重循环、单循环和无循环计算测试数据与训练数据之间的距离,然后选择最接近的k个样本的类别,选择最多的类别为最终结果。


3.3任务完成
任务1
任务1需要我们编写一个双循环的欧式距离计算,其代码为,直接将代码放到任务一的pass处,再调整一下缩进就可以了,

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

任务2
任务2则是使用一个循环,计算欧式距离,这里我们需要运用到numpy的一些矩阵运算的概念,在Numpy相关知识我们有所提及,

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

任务3
任务3则不需要使用循环来计算欧式距离,那不需要距离的话,我们需要展开欧氏距离的计算公式,将完全平方项打开,第一堆是测试数据的平方和,第二堆是训练数据的平方和,还有一堆是2倍的交叉项,具体代码如下,这里需要注意的是我们需要使用转置来使得矩阵的维度计算合法,

X_squre = np.sum(np.square(X), axis = 1).reshape([X.shape[0],1])
X_train_squre = np.sum(np.square(self.X_train), axis = 1).reshape([self.X_train.shape[0], 1])
dists = np.array(np.sqrt(X_squre + self.X_train_squre.T - 2*np.dot(X, self.X_train.T)))

任务4
任务4需要计算的是与测试数据最近的K个训练数据点,然后统计他们的类别,储存在closet_y中,

closet_y = self.y_train[np.argpartition(dists[i],-K)[-K:]]
# np.argpartition例子
# a = np.array([1,3,4,5,6])
# np.argpartition(a, -2) 返回一组index
#在第二大的index前是小于第二大的值的index
#在第二大的index之后是大于第二大的值的index

这个地方对于argpartition中的K说一下,K为正数的时候就是选择从小到大排名第K个的数据。K为负的时候,就是选择从大到小排名第K个的数据。
任务5
任务5则需要统计每个样本点中最多的那个训练数据的类别,然后将其视为测试数据的类别,

y_pred[i] = np.argmax(np.bincount(closet_y))
# bincount的统计方法是根据array([0,1,2,3...])分别统计0出现的次数,1出现的次数等
# argmax返回的是一组索引,对于bincount就是由索引赋值,所以就返回了类别

这里需要注意的就是bincount的理解,可以参考这篇文章,bincount理解。好了对于KNN中的5个小任务我们都将核心的代码进行了展示,只需要复制了后放到相应的pass的位置就可以运行了。下面我们继续来展示作业代码中运行的结果。


3.4运行展示
接下来我们开始调用程序,

from cs231n.classifiers import KNearestNeighbor

classifier = KNearestNeighbor()
classifier.train(X_train, y_train)

#输出二重循环欧式距离的维度
dists = classifier.compute_distances_two_loops(X_test)
print(dists.shape)

#可视化欧式距离
plt.imshow(dists, interpolation='none')
plt.show()
(500, 5000)

在这里插入图片描述
下面我们来对KNN的预测精度进行计算,
1)预测精度

#k = 1选择最近的1个邻居计算knn分类精度
y_test_pred = classifier.predict_labels(dists, k=1)

num_correct = np.sum(y_test_pred == y_test)
accuracy = float(num_correct) / num_test
print('Got %d / %d correct => accuracy: %f' % (num_correct, num_test, accuracy))
Got 38 / 500 correct => accuracy: 0.076000

额,预测精度比较差,哈哈哈哈,下面我们来调整一下K来看看运行结果,

#k = 5选择最近的5个邻居,计算knn分类精度
y_test_pred = classifier.predict_labels(dists, k=5)
num_correct = np.sum(y_test_pred == y_test)
accuracy = float(num_correct) / num_test
print('Got %d / %d correct => accuracy: %f' % (num_correct, num_test, accuracy))
out: Got  44/ 500 correct => accuracy: 0.088000

其实也差不多,那么我们再来看看编写的不同循环的欧式距离是否有差异,

2)不同循环距离差异

# 查看一重循环后距离与二重循环距离是否有差异
difference = np.linalg.norm(dists - dists_one, ord='fro')
print('Difference was: %f' % (difference, ))
if difference < 0.001:
    print('Good! The distance matrices are the same')
else:
    print('Uh-oh! The distance matrices are different')
Difference was: 1.483936
Uh-oh! The distance matrices are different

这里为什么无循环的结果不一样了呢?仔细研究是因为这里返回的值是float32位而前面的返回值是float64位,而且保留位数不太一样,我们可以不管。

# 查看无循环后距离与二重循环距离是否有差异
difference = np.linalg.norm(dists - dists_two, ord='fro')
print('Difference was: %f' % (difference, ))
if difference < 0.001:
    print('Good! The distance matrices are the same')
else:
    print('Uh-oh! The distance matrices are different')
Difference was: 0.000000
Good! The distance matrices are the same

很好,一重循环和二重循环计算的欧式距离没有差异。下面再来看看无循环计算的结果,最后来计算一下不同方法计算欧式距离需要的时间差异,
3)不同设计距离计算时间对比

# 定义程序的计算时间函数
def time_function(f, *args):
    """
    计算程序运行时间
    """
    import time
    tic = time.time()
    f(*args)
    toc = time.time()
    return toc - tic

# 展示不同程序的计算时间
two_loop_time = time_function(classifier.compute_distances_two_loops, X_test)
print('Two loop version took %f seconds' % two_loop_time)

one_loop_time = time_function(classifier.compute_distances_one_loop, X_test)
print('One loop version took %f seconds' % one_loop_time)

no_loop_time = time_function(classifier.compute_distances_no_loops, X_test)
print('No loop version took %f seconds' % no_loop_time)
Two loop version took 34.743881 seconds
One loop version took 34.051943 seconds
No loop version took 0.147606 seconds

从图中我们可以看到,采用循环的方法比无循环的numpy矩阵运算的速度要满很多,因此在后续的机器学习中会用到需要矩阵的运算来提高精度。还有之后通过交叉验证来选择最优的K值,我会在后面补上。


交叉验证
最后我们需要进行交叉验证来得到最优的K值,具体代码如下,

num_folds = 5
k_choices = [1, 3, 5, 8, 10, 12, 15, 20, 50, 100]

X_train_folds = []
y_train_folds = []

classifier = KNearestNeighbor()
for k in k_choices:
    acc = []
    for i in range(num_folds):
        X_test_cv = X_train_folds[i]
        y_test_cv = y_train_folds[i]
        ex_num = list(range(num_folds))
        del ex_num[i]
        X_train_cv = np.vstack(X_train_folds[ex_num])
        y_train_cv = np.hstack(y_train_folds[ex_num])        
        
        classifier.train(X_train_cv,y_train_cv)
        dists_cv = classifier.compute_distances_no_loops(X_test_cv)
        y_test_pred = classifier.predict_labels(dists_cv, k)
        num_correct = np.sum(y_test_pred == y_test_cv)
        acc.append(float(num_correct)*num_folds/ num_training)
    k_to_accuracies[k] = acc
    
for k in sorted(k_to_accuracies):
    for accuracy in k_to_accuracies[k]:
        print('k = %d, accuracy = %f' % (k, accuracy))

最后再将结果可视化,

for k in k_choices:
    accuracies = k_to_accuracies[k]
    plt.scatter([k] * len(accuracies), accuracies)

accuracies_mean = np.array([np.mean(v) for k,v in sorted(k_to_accuracies.items())])
accuracies_std = np.array([np.std(v) for k,v in sorted(k_to_accuracies.items())])
plt.errorbar(k_choices, accuracies_mean, yerr=accuracies_std)
plt.title('Cross-validation on k')
plt.xlabel('k')
plt.ylabel('Cross-validation accuracy')
plt.show()

在这里插入图片描述

best_k = 10

classifier = KNearestNeighbor()
classifier.train(X_train, y_train)
y_test_pred = classifier.predict(X_test, k=best_k)

# Compute and display the accuracy
num_correct = np.sum(y_test_pred == y_test)
accuracy = float(num_correct) / num_test
print('Got %d / %d correct => accuracy: %f' % (num_correct, num_test, accuracy))
Got 40 / 500 correct => accuracy: 0.080000

于是得到最优是选择最近的邻居为10个,其预测精度达到8%。


结语
很不容易呀这个章节,因为前期加载数据的繁琐极度不想做这个事了,但是后面研究了好久终于搞懂一点,但是在写文章的时候数据还是载入不了,有点懵,所以交叉验证的也就没有写了,等后面补上去。相应的assignment1还有SVM loss function、softmax、linear classifier和neural_net多个部分的作业也会在后续更新。
谢谢阅读。
更新说明
今天将交叉验证的内容加了上去,但是很奇怪我的预测精度很低,理论上应该和网上的代码产生的结果一样,KNN无非就是通过距离选择最近的,再统计最多的类。所以如果出错的话,就只有两种可能,一种是在距离计算上,一种是在类别匹配或者统计最多的时候上。距离计算应该不存在问题,我觉得有可能是在训练数据与训练数据类别匹配上出错。等改天再来研究吧

发布了20 篇原创文章 · 获赞 79 · 访问量 2758

猜你喜欢

转载自blog.csdn.net/qq_35149632/article/details/104704921