斯坦福CS231n assignment1:SVM图像分类原理及实现

分类
本文Github代码

斯坦福CS231n课程讲解了实现图像分类的方法,从传统的KNN,SVM,到CNN,LSTM模型,讲解的非常专业精准。同时该课程提供了相应的习题来检验和巩固讲授的知识,如果能按部就班的完成,对神经网络将会有深刻的体会和理解。本文将结合代码实现讲解其中的SVM方法实现图像分类的原理和方法,以及需要注意的知识细节。

SVM模型原理

SVM通过平面将空间中的实例划分到不同的类别,从而实现分类。这里的空间包括二维空间,三维空间,一直到高维空间,具体的维数等于实例的特征数量,如果我们待分类的图片是32*32*3(长宽分别是32个像素,RGB3个颜色通道)维的,那么图片所处的空间就是3072维的空间。在这个高维空间,我们通过由权重向量W和偏置项b确定的一个(实际上是一组)超平面来将图片进行分类。为了可视化,我们将多维空间压缩到二维空间,那么就是下面的图像:
通过超平面进行分类

这里每一个平面都将整个高维空间划分成两部分,平面的一侧是某一类图片,另一侧是这个类别之外的其他图片。比如红色的平面一侧是汽车这个类别,另一侧是非汽车类别。每一个类别都对应一个平面,这些平面互相之间不存在关联,利用SVM模型进行分类的目的就是确定这样一组平面,使得同一类尽可能划分在该类对应的平面的一侧,其他类尽可能在另一侧,而且两种类别离平面的距离越大越好(平面尽可能把两类分的更开),这是SVM模型的思路。

所有这些类别对应的平面通过下面的矩阵唯一确定:
线性映射矩阵

其中改变W可以使平面旋转,而改变b使平面平移。如果b为0,此时W*0=0,那么平面会经过原点。

SVM的一种直观解释

SVM模型用于图像分类可以看做给每一种图像的类别生成一个图像模板,然后拿待分类的图像和这个图像模板做内积,计算他们的相似度,相似度最高的类别就是分类类别。根据这个思想,生成的权重向量可视化如下:

图像模板

可以看出,这些图像模板比较能够代表某种类别的共性,比如car类别是一辆红色的车的形象,而horse类型是左右两匹马的形象,这些是集合了所有训练样本得出的模板。从这个角度,SVM可以看做KNN模型的一种简化,KNN模型对一张图片分类时需要和所有训练样本做比较,而SVM只需要和抽象出来的每个类别下的一个图像模板做比较即可,显然更高效。

损失函数

SVM模型有多种不同的实现,区别主要体现在损失函数的定义上,可以根据实现分为:

  1. 经典SVM
  2. Structured SVM

其中经典SVM模型核心思路是找一个超平面将不同类别分开,同时使得离超平面最近的点的距离最大,这样能保证即使是最难区分的点,也有较大的确信度将它们分开,可以通过数学方法证明这样的超平面是唯一存在的(参考《统计学习方法》)。

而Structured SVM可以看做是对经SVM模型的泛化,则是通过事先构造损失函数来求解一个统计上的最优解,该方式避免了经典SVM简单粗暴的惩罚方式(比如分错惩罚1,分对惩罚0),对分类错误程度不同的样本进行程度不同的惩罚,对实际训练数据中噪音数据具有更大的容错性,分类效果也更好。

Structured SVM模型中我们使用折页损失函数(hinge loss function)来计算损失值。折页损失函数可以有不同的表示,我们使用如下的表示:

折页损失

某个样本经过f映射后会得到N个分值,对应为N个类别的得分。这N个类别中只有一个类别是这个样本的实际类别,用yi表示。类别yi的分值用Syi表示,其他不正确的类别得分用Sj表示,Δ是一个距离阈值。从这个折页损失函数的定义可以看出,这个样本在N个类别上的得分,有些会产生损失,有些不会:

  1. 得分比正确类别yi高的类别肯定会产生损失,对于这些类别Sj>Syi,所以Sj-Syi+Δ>0,这说明正确分类的分值应该是最高的。
  2. 得分比正确类别yi低的那些类别也有可能会产生损失,如果Sj<Syi,但是他们的距离太近,小于一个Δ值,也就是Syi-Sj<Δ,此时Sj-Syi+Δ>0,也会贡献损失。这说明我们不仅希望错误类别得分比正确类别低,而且要至少低一个Δ值

损失函数加入正则化项

损失函数数值的大小跟权重参数W成正比,对于同一个样本,成倍的扩大权重向量W会导致损失值成倍增加,这是损失值的变化没有意义的,为了约束这种变化,我们在损失函数中添加一个正则化项,L2正则化项定义如下:

正则化损失项

这样,当权重扩大的时候会导致损失扩大,然后通过反向传递作用回权重矩阵W,从而对过大的权重加以修正,避免了权重向量的单向增大。

在数据损失(data loss)的基础上,添加了L2正则化损失(regularization loss)项的损失函数如下:
加入L2正则项的损失函数

举个例子,假设输入向量x=[1, 1, 1, 1], 权重向量W1=[1, 0, 0, 0],权重向量 W2=[0.25, 0.25, 0.25, 0.25],W1*x = 1,W2*x=1,可以看到两个内积是一样的。但是W1的L2惩罚是1,而W2的L2惩罚是0.25,所以会更倾向于选择W2。直观上解释就是L2惩罚希望各个权重分量是比较均衡的,而不是某些分量权重一支独大,因为这样更能利用起样本中各个特征,而避免过度依赖某几个特征。

展开损失函数:
损失函数

这里Δ和λ是两个超参数,它们的值是如何确定的?
实际上Δ控制的是损失函数中数据损失部分,λ控制正则化损失部分,这两个变量对损失的贡献是正相关的,实际上它们两个共同控制着损失函数中数据损失和正则化损失的权衡,所以同时调整两个值是没意义的。实际使用中,我们一般设置Δ=1为固定值,而通过交叉验证的方式调整λ值。也就是选择不同的λ,通过训练数据进行模型训练,在验证集上验证,然后调整λ,看看结果是不是更优,最终选择一个最优的λ,确定最优的模型。

梯度下降和梯度检验

定义好损失函数后,我们就可以通过梯度下降的方法利用训练集来更新权重参数,也就是模型训练的过程。

SVM模型中有两个参数,权重矩阵W和偏移量b,我们可以将这两个变量合并起来训练,将b作为一列添加在W后面,同时在输入向量X里添加一行,值为1,这样就可以值训练一个合并后的矩阵:

W和b合并技巧

梯度下降的核心就是计算损失函数对各个权重分量的梯度,然后根据梯度更新各个权重。

对SVM损失函数进行梯度计算,根据损失函数定义,一个样本梯度的计算分为正确分类权重对应行和不正确分类权重对应的行两种情况,数据损失项的梯度为下面两个公式:
错误分类项的梯度

正确分类项的梯度

正则化损失项的梯度很简单,2Wij,可以在正则化损失前面添加一个参数0.5,这样可以将正则化损失项的梯度变为Wij,在代码中会讲到这一点。

这样我们就得到了梯度的解析解,在利用梯度进行权重更新之前,我们还需要验证一下这个梯度解析解是不是正确,方法就是利用梯度的定义求解权重向量分量的梯度的数值解,比较数值解和解析解,如果两者的差距非常小,说明解析解是正确的,否则说明有误,需要查找错误。

梯度数值解的计算很简单,根据下面梯度的公式,在给定的样本和权重矩阵下,计算输出,然后让权重矩阵的变量发生微小的变化,再计算输出,两次输出的差值除以变化量就是权重矩阵的梯度:

梯度的公式

检验没问题后,就可以进行训练了。

图像预处理

通常我们会将所有图片,包括训练数据和待分类数据,减去图片每个位置像素的均值,使得数据中心化,这样可以提高模型的效果。同时,也可以对中心化后的数据归一化处理,使其分布在[-1, 1]区间,进一步优化模型效果。

小批量数据梯度下降(Mini-batch gradient descent)

相比于每次拿一个样例数据进行梯度更新,每次使用一个小批量数据进行梯度更新能够更好的避免单个样本数据的扰动,可以显著提高模型训练的效率,损失的变化更加平滑,使得模型更快的收敛。具体操作方法是一次计算一个批量的数据的结果,如256个样本,计算每个结果下权重矩阵每个变量的梯度。对于每个权重分量,累加256个梯度值,求均值作为该分量的梯度,更新该分量。

代码实现

完整的代码在github,包括训练数据,测试数据的获取,图像预处理等,这里只给出计算损失和梯度的关键代码:

def svm_loss_vectorized(W, X, Y, reg):
    """
    :param X: 200 X 3073
    :param Y: 200
    :param W: 3073 X 10
    :return: reg: 正则化损失系数(无法通过拍脑袋设定,需要多试几个值,交叉验证,然后找个最优的)
    """
    delta = 1.0
    num_train = X.shape[0]

    patch_X = X  # 200 X 3073
    patch_Y = Y  # 200

    patch_result = patch_X.dot(W)  # 200 X 3073 3073 X 10 -> 200 X 10

    sample_label_value = patch_result[[xrange(patch_result.shape[0])], patch_Y]  # 1 X 200 切片操作,将得分array中标记位置的得分取出来作为新的array
    loss_array = np.maximum(0, patch_result - sample_label_value.T + delta)  # 200 X 10 计算误差
    loss_array[[xrange(patch_result.shape[0])], patch_Y] = 0  # 200 X 10 将label值所在的位置误差置零

    loss = np.sum(loss_array)

    loss /= num_train  # get mean

    # regularization: 这里给损失函数中正则损失项添加了一个0.5参数,是为了后面在计算损失函数中正则化损失项的梯度时和梯度参数2进行抵消
    loss += 0.5 * reg * np.sum(W * W)

    # 将loss_array大于0的项(有误差的项)置为1,没误差的项为0
    loss_array[loss_array > 0] = 1  # 200 X 10

    # 没误差的项中有一项是标记项,计算标记项的权重分量对误差也有共享,也需要更新对应的权重分量
    # loss_array中这个参数就是当前样本结果错误分类的数量
    loss_array[[xrange(patch_result.shape[0])], patch_Y] = -np.sum(loss_array, 1)

    # patch_X:200X3073  loss_array:200 X 10   -> 10*3072
    dW = np.dot(np.transpose(patch_X), loss_array)  # 3073 X 10
    dW /= num_train  # average out weights
    dW += reg * W  # regularize the weights

    return loss, dW

该模型的准确率在38%左右,所以SVM模型分类效果还是比较差的,通过更复杂的神经网络,或者CNN等模型,可以实现95%以上的准确率。不过SVM是神经网络的基础,理解了SVM的原理,再学习神经网络就比较容易触类旁通。

本文Github代码

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章