前言
详细代码见github
问答总结:
torch.argsort
, torch.argmax
, torch.bincount
函数简化了代码,脑海中模拟他们的过程。+
号的传播原理(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} d1∈RM×1,d2∈RM×N,d3∈R1×N,脑海中模拟这个过程。+
号两边是tuple
或者list
时,+
号起拼接作用。Y_predict
和Y_test
都是torch.uint8
的一维tensor向量,需要使用torch.sum(Y_predict == Y_test)
, 直接使用sum(Y_predict == Y_test)
会统计错误!Tensor
的数学计算最好都用·torch.xxx
,比如torch.sum
,torch.sqrt
, torch.max
等等。cifar-10
数据集完成KNN
算法,使用循环
和向量化
两种方式计算距离,直接在训练集和测试集上进行实验,记录结果,并比较两种距离计算方式所花费时间。torch.split
函数划分划分训练集,进行交叉验证
,选取最合适的k
,并作出误差条形图
.cifar-10
是有名的图像分类
数据集,其含有10
个类别,训练集50000
张图片,测试集10000
张图片。
笔者最喜欢的深度学习框架为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
对数据进行封装,然后利用enumerate
和next
函数提取数据。
经过上述步骤,最终我们得到trian_data : [flagTensor:(50000,3,32,32)]
和test_data : [floatTensor:(10000,3,32,32)]
的数据。我们此后所有实验都基于此数据集完成。
KNN算法是一种分类算法, 思想简单,不再赘述。其核心在于(1)选取距离度量。(2)确定k值。在此实验中,对于(1),我们将简单采取L2
距离进行度量。对于(2),我们将使用交叉验证方法确定k值。
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} (x−x_trian)(x−x_train)T=x xT−2x 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} d1∈RM×1: torch.sum(pow(X,2),1).unsqueeze(1)
d 2 ∈ R M × N d2 \in R^{M \times N} d2∈RM×N: -2*X.matmul(X_train.t())
d 3 ∈ R 1 × N d3 \in R^{1\times N} d3∈R1×N: torch.sum(pow(X_train,2),1).unsqueeze(0)
注:这样的写法基于+
操作的传播机制
这样,我们就可以写出向量化代码如下:
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
计算预测正确数量: 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
编写测试程序执行时间计算单元:
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
时函数不用加括号,参数以逗号隔开, 依次写在后面即可。
(1) 方法回顾
(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()
```
(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