cs231n 课程学习 一
cs231n 课程资源:Stanford University CS231n: Convolutional Neural Networks for Visual Recognition
我的 github 作业:FinCreWorld/cs231n: The assigments of cs231n (github.com)
第一章 图像分类——以KNN为例
一 简介
-
什么是图像分类:给定程序一个图像,程序从预定的标签集合中选出一个标签,赋给该图像,实现图像分类
-
举例:
图像在计算机中以矩阵方式存储,上图猫咪像素点数为 248 × 400 248\times400 248×400,由于使用三通道RGB的表示形式,总数据点数为 248 × 400 × 3 248\times400\times3 248×400×3。标签集有 {cat、dot、hat、mug},使用分类器预测该图像,为不同标签赋予值,表示该图像属于对应标签的概率。
-
困难:
- 角度变化:图像中对于事物的观察角度不同
- 大小变化:同一类物体可能拥有不同的大小。如姚明和潘长江
- 扭曲:物体的形状不是固定的,比如做瑜伽的人
- 遮盖:物体可能被遮盖
- 亮度:如黑底白猫与黑猫白底,只是亮度有区别
- 背景相似度:常见于生物拟态
- 类别内部差别:同一类物体可能形状千差万别,比如衣服
-
数据驱动方式:对于一些问题,我们可以简单的通过算法解决,比如排序问题。但是图像分类很难通过简单的算法解决,而是需要使用大量的数据来供程序进行学习。这种方式称为数据驱动。
-
分类流程:
- 输入:包括 N N N 个图像,每个图像都拥有 K K K 类标签中的一个,称所有这样图像的集合为训练集。
- 学习:使用训练集训练一个分类器或模型,来学习每个标签所对应的图像基本形状
- 评价:通过验证集评价学习器学习的质量(分类效果)
二 最近邻分类器(Nearest Neighbor Classifier)
最近邻分类器会将测试图像与每一个训练图像进行比较,选出与测试图像距离最小的训练图像,该训练图像的标签即为预测的测试图像的标签。
-
关于图像向量化
由于图像大小一般为 width × height × channels \text{width}\times\text{height}\times\text{channels} width×height×channels,运算不方便,我们可以将其转化为 D × 1 D\times1 D×1 的向量,其中 D = width × height × channels D=\text{width}\times\text{height}\times\text{channels} D=width×height×channels
-
关于距离
考虑有两个相同维度的向量, v 1 ⃗ D × 1 \vec{v_1}_{D\times1} v1 D×1 和 v 2 ⃗ D × 1 \vec{v_2}_{D\times1} v2 D×1,这两个向量之间的距离有
- L1 距离: d ( v 1 ⃗ , v 2 ⃗ ) = ∑ k ∣ v 1 k ⃗ − v 2 k ⃗ ∣ d(\vec{v_1},\vec{v_2})=\sum_k|\vec{v_{1k}}-\vec{v_{2k}}| d(v1 ,v2 )=∑k∣v1k −v2k ∣
- L2 距离: d ( v 1 ⃗ , v 2 ⃗ ) = ∑ k ( v 1 k ⃗ − v 2 k ⃗ ) 2 d(\vec{v_1},\vec{v_2})=\sqrt{\sum_k(\vec{v_{1k}}-\vec{v_{2k}})^2} d(v1 ,v2 )=∑k(v1k −v2k )2
三 k-近邻分类器(k-Nearest Neighbor Classfier)
k-近邻分类器是最近邻分类器的升级版,给定测试图像,我们从训练集中找出与测试图像距离最小的前 k k k 个图像,其中出现次数最多的标签就是我们的预测值。 k = 1 k=1 k=1 时,k-近邻分类器等价于最近邻分类器。k-近邻分类器的分类效果更为平滑,可以减小异常值带来的影响。下面给出一个k-近邻分类器的简单实现
k-近邻分类器的实现实例
class KNearestNeighbor(object):
""" a kNN classifier with L2 distance """
def __init__(self):
pass
def train(self, X, y):
"""
Train the classifier. For k-nearest neighbors this is just
memorizing the training data.
Inputs:
- X: A numpy array of shape (num_train, D) containing the training data
consisting of num_train samples each of dimension D.
- y: A numpy array of shape (N,) containing the training labels, where
y[i] is the label for X[i].
"""
self.X_train = X
self.y_train = y
def predict(self, X, k=1, num_loops=0):
"""
Compute the distance between each test point in X and each training point
in self.X_train using a nested loop over both the training data and the
test data.
Inputs:
- X: A numpy array of shape (num_test, D) containing test data.
Returns:
- dists: A numpy array of shape (num_test, num_train) where dists[i, j]
is the Euclidean distance between the ith test point and the jth training
point.
"""
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):
for j in range(num_train):
dists[i, j] = np.sqrt(np.sum(np.square(X[i] - self.X_train[j])))
return self.predict_labels(dists, k=k)
def predict_labels(self, dists, k=1):
"""
Given a matrix of distances between test points and training points,
predict a label for each test point.
Inputs:
- dists: A numpy array of shape (num_test, num_train) where dists[i, j]
gives the distance betwen the ith test point and the jth training point.
Returns:
- y: A numpy array of shape (num_test,) containing predicted labels for the
test data, where y[i] is the predicted label for the test point X[i].
"""
num_test = dists.shape[0]
y_pred = np.zeros(num_test)
for i in range(num_test):
# A list of length k storing the labels of the k nearest neighbors to
# the ith test point.
closest_y = []
closest_y = np.argsort(dists[i, :], kind='quicksort')[:k]
closest_y = self.y_train[closest_y]
y_pred[i] = np.argmax(np.bincount(closest_y))
return y_pred
k-近邻分类器的向量化实现
该分类器的主要运算量在于 predict
函数中的两层循环,用于计算所有训练样例与所有测试样例之间的距离,我们可以使用 numpy
包提供的向量化计算进行加速,我们需要将原本的求和运算(循环实现)推导至矩阵运算(向量操作实现)
我们假设每一个图像都被压缩成
1
×
D
1\times D
1×D 的向量,训练集
A
N
×
D
A_{N\times D}
AN×D 包含
N
N
N 个训练样例,测试集
B
M
×
D
B_{M\times D}
BM×D 包含
M
M
M 个测试样例,距离矩阵
P
M
×
N
P_{M\times N}
PM×N,
P
i
j
P_{ij}
Pij 表示第
i
i
i 个测试样例到第
j
j
j 个训练样例的距离。有
P
i
j
2
=
∑
k
(
B
i
k
−
A
j
k
)
2
=
∑
k
B
i
k
2
−
2
∑
k
B
i
k
A
j
k
+
∑
k
A
j
k
2
\begin{aligned} P_{ij}^2&=\sum_k(B_{ik}-A_{jk})^2\\ &= \sum_k{B_{ik}^2}-2\sum_k{B_{ik}A_{jk}}+\sum_k{A_{jk}}^2\\ \end{aligned}
Pij2=k∑(Bik−Ajk)2=k∑Bik2−2k∑BikAjk+k∑Ajk2
其中
Q
i
j
=
∑
k
B
i
k
A
j
k
Q_{ij}=\sum_k{B_{ik}A_{jk}}
Qij=∑kBikAjk 为向量乘积的形式,即
Q
=
B
⋅
A
T
Q=B\cdot{A^T}
Q=B⋅AT,而左右两个求和式可以使用 np.sum
函数以及广播机制解决。
最后,我们可以简单的通过 numpy
来实现向量运算,有
sum_B2 = np.sum(np.square(X), axis=1).reshape(-1, 1)
sum_A2 = np.sum(np.square(self.X_train), axis=1).reshape(1, -1)
dists = np.sqrt(sum_B2 - 2 * B.dot(A.T) + sum_A2)
四 超参数调参(Hyperparameter tuning)
以上分类器实现的过程中,我们需要对一些参数进行选择,比如 k k k 值的选取以及 L1 和 L2 距离的选取,称这些参数为超参数。这些参数需要我们不断的调试,以达到最佳的性能。
我们的数据集包括训练集与测试集,如果我们在训练集上训练,在测试集上对训练效果进行评价,并以此评价为基础进行调参,那么最后我们的模型将会在测试集上取得非常好的效果,但是该模型往往无法用于实际中,因为我们最后得到的模型仅在测试集上表现很好,出现了过拟合现象,缺乏泛化能力。
因此,我们需要将训练集进行单独划分,再次划分出训练集与验证集,我们基于训练集训练,基于验证集调参,最后通过测试集来测试模型的泛化能力,避免模型过拟合验证集。
交叉验证
如果训练集较小,那么我们可能缺乏足够数据来划分出训练集与验证集,这时我们可以采用交叉验证的方式。例如,对于 5 划分交叉验证(5-fold-cross-validation),我们将训练集等分为 5 份,使用其中的 4 份进行训练,最后一份进行验证,如此训练-验证 5 次,对最后的预测精度取平均即可。代码实现如下
def accuracy(y_test_pred, y_test):
return float(np.sum(y_test_pred==y_test))/y_test_pred.shape[0]
X_train_folds = np.array_split(X_train, num_folds)
y_train_folds = np.array_split(y_train, num_folds)
classifier = KNearestNeighbor()
for k in k_choices:
k_to_accuracies[k] = []
for i in range(num_folds):
# test data
X_te = X_train_folds[i]
y_te = y_train_folds[i]
# validation data
X_tr = np.vstack(X_train_folds[0:i] + X_train_folds[i+1:])
y_tr = np.hstack(y_train_folds[0:i] + y_train_folds[i+1:])
classifier.train(X_tr, y_tr)
y_pre = classifier.predict(X_te, k, 0)
k_to_accuracies[k].append(accuracy(y_pre, y_te))
上述代码用于调试 k
值(k-近邻)。第一层循环确定不同的 k
值,第二层循环进行交叉验证,训练集一共被划分为了 num_folds
份,遍历其中的 1
份数据进行验证,num_folds-1
份数据进行训练,将结果保存到 k_to_accuracies[k]
中。 k_to_accuracies[k]
是一个数组,保存了当前 k
值下,使用不同训练集与验证集之后的训练精度。
最后交叉验证结果如下,可以发现当 k
在 10 左右时训练精度最佳,每一列有 5 个点,表示 5 次交叉验证的精度,而曲线表示交叉验证结果的均值。
调参数据集选取的说明
实际中人们较少使用交叉验证,因为其运算量较大。人们常使用 50%-90% 的训练数据用于训练,剩余的进行验证,如果超参数较多,同时训练数据较多,那么我们就可以使用较多一部分的训练数据充当验证集。如果样本数据量较少,我们可以使用交叉验证。
五 近邻分类器的优缺点
- 优点:
- 模型简单
- 训练时间少
- 缺点:
- 预测时间长。实际生活中,我们通常较少关心训练时间,而更关心预测时间。我们通常部署训练好的模型,预测时间会影响用户的体验。
- 无法较好的应对高维数据。因为高维数据的极少量异常点就会带来数据点之间较大的距离变化。