CS231n:作业1——KNN

前言
详细代码见github


问答总结:

  • 实现knn算法中,我们使用了torch.argsort, torch.argmax, torch.bincount函数简化了代码,脑海中模拟他们的过程。
  • 实现knn算法向量化过程中,我们使用了+号的传播原理(dist=d1+d2+d3),其中 d 1 ∈ R M × 1 , d 2 ∈ R M × N , d 3 ∈ R 1 × N d_1\in R^{M \times 1},d_2 \in R^{M \times N}, d_3 \in R^{1 \times N} d1RM×1,d2RM×N,d3R1×N,脑海中模拟这个过程。
  • +号两边是tuple或者list时,+号起拼接作用。
  • 在统计正确率时,如果Y_predictY_test都是torch.uint8的一维tensor向量,需要使用torch.sum(Y_predict == Y_test), 直接使用sum(Y_predict == Y_test)会统计错误!
  • 基于Tensor的数学计算最好都用·torch.xxx,比如torch.sumtorch.sqrt, torch.max等等。
  • 时间评测单元如何编写?如何调用?
  • 误差条形图如何绘制?

文章目录

  • 一、实验目标
    • 1、实现KNN
  • 一、数据集
    • 1、数据集简介
    • 2、加载数据集
    • 3、加载结果
  • 二、KNN算法
    • 1、模型框架
    • 2、正确率评估
    • 3、两种距离计算方式时间比较
    • 4、交叉验证
  • 参考资料

一、实验目标

1、实现KNN

  • 使用cifar-10数据集完成KNN算法,使用循环向量化两种方式计算距离,直接在训练集和测试集上进行实验,记录结果,并比较两种距离计算方式所花费时间。
  • 使用torch.split函数划分划分训练集,进行交叉验证,选取最合适的k,并作出误差条形图.

一、数据集

1、数据集简介

cifar-10是有名的图像分类数据集,其含有10个类别,训练集50000张图片,测试集10000张图片。
CS231n:作业1——KNN_第1张图片

2、加载数据集

笔者最喜欢的深度学习框架为pytorch, 很幸运,pytorch框架中内置了cifar-10数据集,可是省去自己寻找资源的步骤。具体使用步骤如下:

  • 下载:

    import torchvision.datasets as dset
    dset.CIFAR10(path, train=True, transform=None, target_transform=None, download=True)
    dset.CIFAR10(path, train=False, transform=None, target_transform=None, download=True)
    

    其中,重点的是path参数,这是用来保存下载数据的文件目录。

  • 定义加工函数:

    import torchvision.transforms as transforms
    
    transform = transforms.Compose(
        [
            transforms.ToTensor(),
        ])
    

    此函数可以将原始图片进行转换为tensor裁剪归一化等操作,具体可以参看官方文档. 这里我们仅仅将图片装换为floatTensor, 其值为 [ − 1 , 1 ] [-1,1] [1,1].

  • 得到数据:

     train_set = tv.datasets.CIFAR10(root=path, train=True, download=True, transform=transform)
     test_set = tv.datasets.CIFAR10(root=path, train=False, download=True, transform=transform)
    

    path参数为保存下载数据的文件目录,transform参数为我们定义的加工函数,这样我们就得到了所有图片的tensor数据。

  • 封装(非必要):

     train_loader = torch.utils.data.DataLoader(train_set, batch_size=50000, shuffle=True, num_workers=0)
     test_loader = torch.utils.data.DataLoader(test_set, batch_size=10000, shuffle=False, num_workers=0)
    
     train_data_interator = enumerate(train_loader)
     test_data_interator = enumerate(test_loader)
    
     train_data = next(train_data_interator)
     test_data = next(test_data_interator)
    

    为了和常见的训练过程保持一致,我们使用DataLoader对数据进行封装,然后利用enumeratenext函数提取数据。

3、加载结果

经过上述步骤,最终我们得到trian_data : [flagTensor:(50000,3,32,32)]test_data : [floatTensor:(10000,3,32,32)]的数据。我们此后所有实验都基于此数据集完成。

二、KNN算法

KNN算法是一种分类算法, 思想简单,不再赘述。其核心在于(1)选取距离度量。(2)确定k值。在此实验中,对于(1),我们将简单采取L2距离进行度量。对于(2),我们将使用交叉验证方法确定k值。

1、模型框架

class KNN():
    def __init__(self):
        self.model_name = "KNN"
     
     # ---------------------------------------------------#
    def train(self, train_data, train_labels):
        self.X_trian = train_data
        self.Y_train = train_labels
	def predict(self, X, k, vec=True):
		"""
		功能: 预测输入图片的标签
		输入: 
		    X(tensor), (M, 3*32*32): 输入的图片
		    k(int), (1): 按k个邻居结点判断类别
		    vec(bool), (1): 是否使用向量化距离计算方式
		输出:
		    label(tensor), (M): 所有输入图片的预测类别
		"""
		if vec:
		    dist = self.cal_dist_with_vec(X)
		else:
		    dist = self.cal_dist_with_loop(X)
		    
		topk = self.Y_train[torch.argsort(dist, 1)[:,:k]]
		labels = []
		for each in topk:
		    bin_count = torch.bincount(each)
		    label = torch.argmax(bin_count)
		    labels.append(label.item())
		return torch.LongTensor(labels)
       
    # ------------------------------------------------------#
    def cal_dist_with_vec(self, X):
        """
        功能:使用向量化方法,对于测试数据X, 计算其对于训练数据的L2距离。
        """
    def cal_dist_with_loop(self, X):
	    """
	    功能:使用循环,对于测试数据X, 计算其对于训练数据的L2距离。
	    """

(1) 模型主体

  • train阶段: 即简单记录数据,不在赘述。
  • predict阶段: 根据k值预测输入测试数据的类别。

值得注意的是:这里一系列函数的应用(torch.argsort, torch.bincount, torch.argmax)大大简化了代码。

(2) 距离计算

  • 向量化方法: 对于一个测试样本 x x x和一个训练集样本 x _ t r a i n x\_train x_train, 我们可以这样计算他们的l2距离:

    ( x − x _ t r i a n ) ( x − x _ t r a i n ) T = x   x T − 2 x   x _ t r a i n T + x _ t r a i n   x _ t r a i n T = s u m ( x 2 ) − 2 < x , x _ t r a i n > + s u m ( x _ t r a i n 2 ) \begin{aligned} (x-x\_trian)(x-x\_train)^T &= x\ x^T-2x \ x\_train^T+x\_train \ x\_train^T \\ &= sum(x^2) - 2<x,x\_train>+sum(x\_train^2) \end{aligned} (xx_trian)(xx_train)T=x xT2x x_trainT+x_train x_trainT=sum(x2)2<x,x_train>+sum(x_train2)
    其中, < > <> <>为内积运算,基于此,我们进行向量化:
    d i s t = d 1 + d 2 + d 3 dist = d1+d2+d3 dist=d1+d2+d3
    d 1 ∈ R M × 1 d1\in R^{M \times 1} d1RM×1: torch.sum(pow(X,2),1).unsqueeze(1)
    d 2 ∈ R M × N d2 \in R^{M \times N} d2RM×N: -2*X.matmul(X_train.t())
    d 3 ∈ R 1 × N d3 \in R^{1\times N} d3R1×N: torch.sum(pow(X_train,2),1).unsqueeze(0)
    :这样的写法基于+操作的传播机制
    CS231n:作业1——KNN_第2张图片

    这样,我们就可以写出向量化代码如下:

    def cal_dist_with_vec(self, X):
       """
        功能:对于测试数据X, 计算其对于训练数据的L2距离。
        输入:
            X(tensor), (M,3*32*32): 需要预测的图片。
        输出:
            dist(tensor), (M, N): 每一行为每一个测试用例与所有训练集的L2距离。
        """
        d1 = torch.sum(torch.pow(X,2),1).unsqueeze(1)
        d2 = -2*X.matmul(X, self.X_trian.t())
        d3 = torch.sum(torch.pow(self.X_train, 2),1).unsqueeze(0)
        return torch.sqrt(d1 + d2 + d3)
    
  • 非向量化方法: 写一个二重循环进行计算。

     def cal_dist_with_loop(self, X):
        """
        功能:对于测试数据X, 计算其对于训练数据的L2距离。
        输入:
            X(tensor), (M,3*32*32): 需要预测的图片。
        输出:
            dist(tensor), (M, N): 每一行为每一个测试用例与所有训练集的L2距离。
        """
        M, N = X.size(0), self.X_train.size(0)
        dist = torch.ones(M, N)
        for i in range(M):
            for j in range(N):
                delta_x = X[i] - self.X_train[j]
                dist[i][j] = torch.sqrt(torch.sum(pow(delta_x,2)))
        return dist
    

2、正确率评估

  • 计算预测正确数量: torch.sum(Y_predict == Y_test).item()

  • 计算准确率: Acc = torch.sum(Y_predict == Y_test).item() / len(Y_predict)

    train_data_num = 500
    test_data_num = 100
    X_train = train_data[:train_data_num].view(train_data_num, -1)
    Y_train = train_labels[:train_data_num]
    X_test = test_data[:test_data_num].view(test_data_num,-1)
    Y_test = test_labels[:test_data_num]
    
    Y_predict = knnEr.predict(X_test,k=10,vec=True)
    print("向量化Acc:{}".format(torch.sum(Y_test == Y_predict).item() / len(Y_test)))
    Y_predict = knnEr.predict(X_test,k=10,vec=False)
    print("非向量化Acc:{}".format(torch.sum(Y_test == Y_predict).item() / len(Y_test)))
    
    >> 向量化Acc:0.22
    >> 非向量化Acc:0.22
    

3、两种距离计算方式时间比较

  • 编写测试程序执行时间计算单元:

    def cal_time(f, *args):
        """
        函数功能: 计算函数f执行时间
    
        输入:
            f(function): 所要执行的函数
            *args: 可变长度的函数参数
        输出:
            time(int): 函数执行时间
        """
        import time
    
        t_st = time.time()
        f(*args)
        t_ed = time.time()
    
        return t_ed - t_st
    
  • 调用:

    print("向量化花费时间:{}".format(eva.cal_time(knnEr.predict,X_test,10,True)))
    print("非向量化花费时间:{}".format(eva.cal_time(knnEr.predict,X_test,10,False)))
    
    >> 向量化花费时间:0.008883237838745117
    >> 非向量化花费时间:1.7657294273376465
    

    可以看到,在样本非常小时,时间差距已经非常大了。因此向量化计算能够大大降低运行时间.

    注:调用cal_time时函数不用加括号,参数以逗号隔开, 依次写在后面即可。

4、交叉验证

(1) 方法回顾

  • 将训练集划分为k份,循环k次,每次拿其中1份当做验证集:cv,其余k-1份作为训练集:tr.
  • 每组参数获得k个结果,以结果平均作为标准,比较选取最好的参数。
  • 到测试集上测试最终结果。
    CS231n:作业1——KNN_第3张图片

(2) 编码实现

```
k_fold = 5 # 交叉验证份数
k_classes = [1, 3, 5, 8, 10, 12, 15, 20, 50, 100] # 待选的k值
train_data_num = 5000
test_data_num = 1000
fold_sample_num = int(train_data_num / k_fold)
X_train = train_data[:train_data_num].view(train_data_num, -1)
Y_train = train_labels[:train_data_num]
X_test = test_data[:test_data_num].view(test_data_num,-1)
Y_test = test_labels[:test_data_num]
	   
# ----------------------划分数据集为k_fold 份---------------------------#
X_train_folds = torch.split(X_train, fold_sample_num, 0)
Y_train_folds = torch.split(Y_train, fold_sample_num, 0)
# --------------------------------------------------------------------#

knner = KNN()
k_acc = {} # 记录准确率
for k in k_classes:
    acc_list = []
    for i in range(0, k_fold):
    	# -----------使用+号拼接tuple, 使用torch.cat拼接tuple---------------#
        X_tr = torch.cat(X_train_folds[:i]+X_train_folds[i+1:], 0)
        Y_tr = torch.cat(Y_train_folds[:i]+Y_train_folds[i+1:], 0)
        #----------------------------------------------------------------#
        
        X_cv = X_train_folds[i]
        Y_cv = Y_train_folds[i]
        knner.train(X_tr, Y_tr)
        Y_cv_predict = knner.predict(X_cv, k, True)
        acc = torch.sum(Y_cv_predict == Y_cv).item() / len(Y_cv)
        acc_list.append(acc)
    k_acc[k] = acc_list
```

(3) 可视化
对每个k值,我们都计算了k_fold个准确率,更具这些准确率,我们画出误差条形图

```
# Plot the cross validation
for k in k_classes:
    plt.scatter([k] * k_fold, k_acc[k])
# plot the trend line with error bars that correspond to standard deviation
accuracies_mean = [np.mean(k_acc[k]) for k in k_acc]
accuracies_std = [np.std(k_acc[k]) for k in k_acc]
plt.errorbar(k_classes, accuracies_mean, yerr=accuracies_std)
plt.title('Cross-validation on k')
plt.xlabel('k')
plt.ylabel('Cross-validation accuracy')
plt.show()
```

CS231n:作业1——KNN_第4张图片
(4)选择k值
可以看到,做好的k值为8, 因此我们选择8为最终k值,然后预测测试集:

knner.train(X_train, Y_train)
Y_test_predict = knner.predict(X_test,8,True)
print(torch.sum(Y_test_predict == Y_test).item() / len(Y_test))
>> 0.289

参考资料

  • Pytorch 官方文档: torchvision transform
  • 红色的石头:斯坦福CS231n项目实战(一):k最近邻(kNN)分类算法

你可能感兴趣的:(信息科学)