cs231n 课程学习 一

cs231n 课程学习 一

cs231n 课程资源:Stanford University CS231n: Convolutional Neural Networks for Visual Recognition

我的 github 作业:FinCreWorld/cs231n: The assigments of cs231n (github.com)

第一章 图像分类——以KNN为例

一 简介

  • 什么是图像分类:给定程序一个图像,程序从预定的标签集合中选出一个标签,赋给该图像,实现图像分类

  • 举例cs231n 课程学习 一

    图像在计算机中以矩阵方式存储,上图猫咪像素点数为 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∑​Bik​Ajk​+k∑​Ajk​2​
其中 Q i j = ∑ k B i k A j k Q_{ij}=\sum_k{B_{ik}A_{jk}} Qij​=∑k​Bik​Ajk​ 为向量乘积的形式,即 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 距离的选取,称这些参数为超参数。这些参数需要我们不断的调试,以达到最佳的性能。

我们的数据集包括训练集与测试集,如果我们在训练集上训练,在测试集上对训练效果进行评价,并以此评价为基础进行调参,那么最后我们的模型将会在测试集上取得非常好的效果,但是该模型往往无法用于实际中,因为我们最后得到的模型仅在测试集上表现很好,出现了过拟合现象,缺乏泛化能力。

cs231n 课程学习 一

因此,我们需要将训练集进行单独划分,再次划分出训练集验证集,我们基于训练集训练,基于验证集调参,最后通过测试集来测试模型的泛化能力,避免模型过拟合验证集。

交叉验证

如果训练集较小,那么我们可能缺乏足够数据来划分出训练集与验证集,这时我们可以采用交叉验证的方式。例如,对于 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 次交叉验证的精度,而曲线表示交叉验证结果的均值。

cs231n 课程学习 一

调参数据集选取的说明

实际中人们较少使用交叉验证,因为其运算量较大。人们常使用 50%-90% 的训练数据用于训练,剩余的进行验证,如果超参数较多,同时训练数据较多,那么我们就可以使用较多一部分的训练数据充当验证集。如果样本数据量较少,我们可以使用交叉验证。

五 近邻分类器的优缺点

  • 优点:
    • 模型简单
    • 训练时间少
  • 缺点:
    • 预测时间长。实际生活中,我们通常较少关心训练时间,而更关心预测时间。我们通常部署训练好的模型,预测时间会影响用户的体验。
    • 无法较好的应对高维数据。因为高维数据的极少量异常点就会带来数据点之间较大的距离变化。
上一篇:loadmat()函数加载.mat数据文件


下一篇:Oracle不走索引的7中场景