CS231n: 作业1——SVM

前言
完整代码见github


问答总结

  • SVM反向传播参数梯度的推导过程?
  • 如何使用矩阵乘法的列观点向量化梯度求导过程?
  • 布尔索引(mask[score>0] = 1)的含义是什么?
  • 切片索引(x[[0,1],[1,2]])的含义是什么?
  • 书写完整代码一般分为main,train,evaluate三个部分。说明他们各自的作用。

文章目录

  • 一、实验目标
  • 二、数据集
  • 三、实验方法
    • 1、损失函数
    • 2、梯度更新
    • 3、加入正则项
    • 4、代码
  • 四、实验
  • 五、参考

一、实验目标

  • 使用多类别SVM损失函数进行图片分类,推导梯度更新公式,使用随机梯度下降算法更新梯度。
  • 加入正则化项,比较no-regL1-reg, L2-reg结果差异。

二、数据集

数据集依然使用cifar-10数据,其使用方式见此。

三、实验方法

1、损失函数

对样本 i i i, 多类别SVM损失函数定义如下:
L i = ∑ j ≠ y i { 0 s y i − s j − 1 ≥ 0 s j − s y i + 1 e l s e L_i=\sum_{j \ne y_i}\begin{cases} 0 & s_{y_i}-s_j-1 \ge 0 \\ s_j-s_{y_i}+1 & else \end{cases} Li=j̸=yi{0sjsyi+1syisj10else
其中 y i y_i yi是样本 i i i的真实类别, s s s表示其得分,其意义在于:希望使得正确类别的分数比其他类别至少高1,其中由于超平面的特性,阈值1可以为任意正数。

2、梯度更新

首先明确模型为 f ( W , b ) f(W,b) f(W,b), 其中 W ∈ R K × C , b ∈ R C W \in R^{K \times C}, b\in R^{C} WRK×C,bRC, 其中 C C C为类别数。对于样本 X i ∈ R 1 × K X_i \in R^{1 \times K} XiR1×K, 我们计算样本第 c c c类得分: s c = X i W c + b c s_c=X_iW_c+b_c sc=XiWc+bc

我们最终目标是更新参数 W , b W, b W,b。令每一个batch的样本数量为N, 令每个batch的总损失为 L L L, 则有
L = 1 N ∑ i = 1 N L i L = \frac{1}{N}\sum_{i=1}^NL_i L=N1i=1NLi

我们目标是求 ∂ L ∂ W , ∂ L ∂ b \frac{\partial L}{\partial W}, \frac{\partial L}{\partial b} WL,bL, 稍作变换:

∂ L ∂ W = 1 N ∑ i = 1 N ∂ L i ∂ W \frac{\partial L}{\partial W}= \frac{1}{N}\sum_{i=1}^N\frac{\partial L_i}{\partial W} WL=N1i=1NWLi
∂ L ∂ b = 1 N ∑ i = 1 N ∂ L i ∂ b \frac{\partial L}{\partial b}= \frac{1}{N}\sum_{i=1}^N\frac{\partial L_i}{\partial b} bL=N1i=1NbLi

由于:
L i = ∑ j ≠ y i   a n d   s y i − s j − 1 < 0 s j − s y i + 1 = ∑ j ≠ y i   a n d   s y i − s j − 1 < 0 X i W j + b j − X i W y i − b y i + 1 L_i = \sum_{j \ne y_i \ and \ s_{y_i}-s_j-1 < 0} s_j-s_{y_i}+1=\sum_{j \ne y_i \ and \ s_{y_i}-s_j-1 < 0} X_iW_{j}+b_j-X_iW_{y_i}-b_{y_i}+1 Li=j̸=yi and syisj1<0sjsyi+1=j̸=yi and syisj1<0XiWj+bjXiWyibyi+1

故:
∂ L i ∂ W j = ∑ j ≠ y i   a n d   s y i − s j − 1 < 0 X i T \frac{\partial L_i}{\partial W_j}=\sum_{j \ne y_i \ and \ s_{y_i}-s_j-1 < 0}X_i^T WjLi=j̸=yi and syisj1<0XiT
∂ L i ∂ W y i = ∑ j ≠ y i   a n d   s y i − s j − 1 < 0 − X i T \frac{\partial L_i}{\partial W_{y_i}}=\sum_{j \ne y_i \ and \ s_{y_i}-s_j-1 < 0}-X_i^T WyiLi=j̸=yi and syisj1<0XiT
∂ L i ∂ b j = ∑ j ≠ y i   a n d   s y i − s j − 1 < 0 1 \frac{\partial L_i}{\partial b_j}=\sum_{j \ne y_i \ and \ s_{y_i}-s_j-1 < 0}1 bjLi=j̸=yi and syisj1<01
∂ L i ∂ b y i = ∑ j ≠ y i   a n d   s y i − s j − 1 < 0 − 1 \frac{\partial L_i}{\partial b_{y_i}}=\sum_{j \ne y_i \ and \ s_{y_i}-s_j-1 < 0}-1 byiLi=j̸=yi and syisj1<01

这样,我们就能够进行更新了。

3、加入正则项

加入正则项后,以二范数举例,一个batch的损失计算如下:

L = 1 N ∑ i = 1 N L i + 1 2 λ ∣ ∣ W ∣ ∣ 2 L = \frac{1}{N}\sum_{i=1}^NL_i + \frac{1}{2}\lambda||W||^2 L=N1i=1NLi+21λW2

很容易求得, 加入正则项后,梯度会增加 λ W \lambda W λW:

4、代码

(1) 纯循环计算梯度

根据推导公式,我们很容易使用循环计算梯度和损失。

def cal_dw_with_loop(self, X, Y, reg):
    """
    功能: 计算损失和梯度
    输入:
        X(Tensor):(N, K:3*32*32+1)
        Y(Tensor):(N)
        reg(float):                    # 正则化系数
    输出:
        L(int): 1                      # 损失               
        dW(Tensor):(K+1,C)             # 参数梯度       
    """
    L = 0.0
    N = X.size(0)
    F, C = self.W.size()
    dW = torch.zeros(F, C)
    
    # (1) 求解损失
    for idx, Xi in enumerate(X):
        yi = Y[idx]
        scores = Xi.matmul(self.W)
        syi = scores[yi]
        for j in range(self.N):
            if j == yi:
                continue
            sj = scores[j]
            if syi - sj - 1 < 0:
                L += (sj - syi + 1).item()
                dW[:,j] += Xi.t()
                dW[:,yi] -= Xi.t()
    
    # (2) 正则化
    L = L / N +  0.5*reg*torch.sum(torch.pow(self.W, 2)).item()
    dW = dW / N +  reg*self.W
    
    return L, dW

(2) 向量化

  • 条件一:观察计算梯度公式。我们可以发现,对每一个样本,它都可能改变多个类别的模板参数梯度。对某一类别,如果改变,即: s y i − s j − 1 < 0 s_{y_i}-s_j-1<0 syisj1<0, 改变量为 X i X_i Xi,特别的,对于该样本所属类别 y i y_i yi, 其模板参数梯度改变量为其余改变量之和的相反数。
  • 条件二: 回想矩阵乘法最初诞生时的意义。使用列观点看待矩阵乘法:
    CS231n: 作业1——SVM_第1张图片
    这样,我们设置 m a s k ∈ R N × C mask \in R^{N \times C} maskRN×C, 数组, 其中 m a s k [ i ] [ c ] mask[i][c] mask[i][c]处记录样本 i i i对类别 c c c模板参数梯度的影响,那么以列向量观点看,通过 X T m a s k [ : , c ] X^Tmask[:,c] XTmask[:,c], 其中 X T ∈ R K × N X^T \in R^{K \times N} XTRK×N, 其就为所有样本对类别 c c c参数梯度的影响。

基于此,写出向量化代码:

def cal_dw_with_vec(self, X, Y, reg):
     """
     功能: 计算损失和梯度
     输入:
         X(Tensor):(N, K:3*32*32+1)
         Y(Tensor):(N)
         reg(float):                    # 正则化系数
     输出:
         L(int): 1                      # 损失               
         dW(Tensor):(K+1,C)             # 参数梯度       
     """
     
     N = X.size(0)
     F, C = self.W.size()
     
     score = X.matmul(self.W)                                       # (N, C)
     correct = score[range(N), Y.tolist()].unsqueeze(1)             # (N, 1)
     score = torch.relu(score-correct+1)                            # (N, C)
     score[range(N), Y.tolist()] = 0
     
     L = torch.sum(score).item()
     L = L / N +  0.5*reg*torch.sum(torch.pow(self.W, 2)).item()
     
     
     dW = torch.zeros(F, C)
     mask = torch.zeros(N, C)
     mask[score>0] = 1                                              # (N,C)
     mask[range(N), Y.tolist()] = -torch.sum(mask, 1)               # (N,C)
     dW = X.t().matmul(mask)                                        # (F,C)

     dW = dW / N + reg*self.W
     return L, dW
     

这个代码中有很多需要注意的小细节。比如mask[[1,2],[2,1]] = 1的含义。比如mask[score>0] = 1的含义。

四、实验

由于pytorch直接加载的数据集不便划分为训练集、验证集、测试集,我们仅仅使用训练集和测试集。在训练集上训练,在测试集上测试。过程如下:

lrs = [1e-2, 1e-3, 1e-4, 1e-5]
reg_strs = [0, 1, 10, 100, 1000]

result = {}

best_lr = None
best_reg = None
best_svm = None
best_acc = -1

for lr in lrs:
    for reg in reg_strs:
        svmEr = train(lr, reg, 25)
        acc = evaluate(svmEr)
        print("lr:{}; reg:{}; acc:{}".format(lr, reg, acc))
        if acc > best_acc:
            best_lr = lr
            best_reg = reg
            best_svm = svmEr
        result[(lr, reg)] = acc
print("the best: lr:{}; reg:{}; acc:{}".format(best_lr, best_reg, best_acc))

五、参考

  • [1] 红色的石头: 斯坦福CS231n项目实战(二):线性支持向量机SVM

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