cs231n-2022-assignment1#Q3:Implementing a softmax classifier

本文介绍了softmax分类器的原理,包括softmax函数、概率解释、交叉熵损失函数及其实现中的数值稳定性处理。重点展示了代码实现,从naive版本到向量化优化,并探讨了实验性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1. 前言

2. SoftMax分类器

2.1 softmax函数

2.2 Probabilistic Interpretation

2.3 cross-entropy  loss function

2.4 Practical issues: Numeric stability

2.5 向量微积分

2.5 梯度计算

3. 代码实现

3.1 softmax_loss_naive

3.2 softmax_loss_vectorized

4. softmax分类的实验性能


1. 前言

        本文是李飞飞cs231n-2022的第一次作业的第3个问题(Implement a Softmax classifier)。 前两个问题分别参见:

        cs231n-2022-assignment1#Q1:kNN图像分类器实验

        cs231n-2022-assignment1#Q2:训练一个支持向量机(SVM)       

        本次作业相关的课程内容参见:CS231n Convolutional Neural Networks for Visual Recognition

        建议有兴趣的伙伴读原文,过于精彩,不敢搬运。本文可以作为补充阅读材料,主要介绍作业完成过程所涉及一些要点以及关键代码解读。作业的原始starter code可以从该课程网站下载。本文仅涉及完成作业所需要修改的代码,修改的文件涉及以下几个文件:

  • cs231n/classifiers/softmax.py
  • root\softmax.ipynb

2. SoftMax分类器

        SoftMax分类器与SVM分类器的主要区别在于对数据样本经过f(x_i,W) = x_i^T\cdot W_{D,C}(其中D表示数据维度数,或者说所包含元素个数;C表示分类数)映射的结果数据的(物理意义的)解释,以及由此而衍生的损失函数、梯度计算等。

2.1 softmax函数

        softmax函数定义如下:

                s_j(z) = \frac{e^{z_j}}{\sum\limits_{j}{e^{z_j}}}

        softmax函数是sigmoid函数从2维向量向多维向量的推广。

        由以上定义可知,\sum\limits_{j}s_j(z) = 1,因此s_j(z)可以看作是一种概率度量。 

2.2 Probabilistic Interpretation

        借助softmax函数可以将f(x_i,W) = x_i^T\cdot W_{D,C}的各分量变换为针对数据样本x_i在系数W的条件下被分类为类j的概率: 

                P(y_i = j | x_i ; W) = s_j(f(x_i,W)) = \frac{e^{f_j}}{\sum\limits_{k}{e^{f_k}}}

        换句话说,Softmax分类器将原始变换 f(x_i,W) = x_i^T\cdot W_{D,C}的结果解释为样本被识别为各类别的非归一化的对数概率(unnormalized log probabilities)。

2.3 cross-entropy  loss function

        在信息论中,用交叉熵来衡量两个概率分布时间的距离,如下所示:

                 H(p,q)= - \sum\limits_{x} p(x) log(q(x))

        在SoftMax分类器中,则基于交叉熵来计算损失函数。

        一方面是通过对f(x_i,W) = x_i^T\cdot W_{D,C}进行softmax映射得到样本归类为各类的概率,这个构成一个概率分布。另一方面是根据标签得到的真实概率分布P_{gt} = [P_0,P_1,...,P_{C-1}],其中,显而易见的是,如果样本x_i的真实类别为y_i,则有P_{gt}[j] = 0, \ j\neq y_i; \ 1, \ j= y_i;,因此可以得到交叉熵损失函数如下:

                \begin{align} L_i &= - \sum\limits_{j} P_{i,gt}[j] \cdot log(P_i) \\&= - log P(y_i | x_i; W) \\&= - log s_j(f_{y_i}) \\&= - log\bigg[\frac{e^{f_{y_i}}}{\sum\limits_{j}{e^{f_j}}}\bigg] \end{align}

        以上,为了描述的简化,用f_j表示f_j(x_i, W) = (x_i^T W)_j.

        对所有样本的损失函数求和然后再加上正则损失(与SVM分类相同),就得到SoftMax分类器的完整的损失函数:L = \sum\limits_{i}L_i + \frac{1}{2} \cdot \lambda \sum\limits_{i,j}w_{i,j}^2

2.4 Practical issues: Numeric stability

        由于以上计算涉及指数计算,因此很容易出现由overflow导致数值稳定性问题。

        通常的做法是在进入指数运算处理之前,先对f(x_i)进行“归一化”处理,如下所示:

                f(x_i) = f(x_i) - max(f(x_i))

         这样做有效地压缩了数据范围,但是并不影响softmax变换输出结果。

2.5 向量微积分

         向量微积分(vector calculus)也称矩阵微积分(matrix calculus),是标量微积分的扩展。这里只介绍接下来梯度计算所需要的东西。更全面的相关只是请参考相关正式文献。由矩阵微积分法则:

                \frac{\partial{(x^T \cdot w)}}{\partial{x}} = w^T \quad \Rightarrow \frac{\partial{(x^T \cdot w)}}{\partial{w}} = x^T         

                其中x和w表示两个向量(1阶张量),具体过程如下:

 

                g(W) = \sum\limits_{i,j}w_{ij}^2 \quad \Rightarrow \frac{\partial{g(W)}}{\partial{W}} = \frac{1}{2}W^T         

                其中W表示一个矩阵(2阶张量)。

        注意,这里采用的是所谓的numerator layout notation。与之相对的还有另一种标记法叫denominator layout notation。两者之间的差别,在于微分后所得的张量的shape如何安排(layout)。理论上来说,两者都是可以的,只要前后连贯一致即可。但是实际上可能numerator layout notation更为常见一些。在numerator layout notation中,对列向量的求导得到的是行向量,对矩阵的求导得到的张量的layout是按该矩阵的转置方式来进行安排的。

2.5 梯度计算

        Softmax损失函数相比SVM损失函数的一个优势在于softmax函数本身是可微的,因此其梯度计算相对来说更简洁易懂。

        以下来看看如何计算梯度\frac{\partial{L_i}}{\partial{W}}(为简洁起见,暂时只考虑单个样本的损失函数的梯度,且不考虑正则项)。由上一节我们得到:

        ​​​​​​​        L_i = - log\bigg[\frac{e^{f_{y_i}}}{\sum\limits_{j}{e^{f_j}}}\bigg] = - log(s_{i,y_i})

        这里s_{i,j} = P(y_i = j | x_i ; W) = [softmax(f(x_i,W))]_j = \frac{e^{f_j}}{\sum\limits_{k}{e^{f_k}}}

        根据链式法则:

        ​​​​​​​        \frac{\partial{L_i}}{\partial{W}} = \frac{\partial{L_i}}{\partial{s_{i,y_i}}} \cdot \frac{\partial{s_{i,y_i}}}{\partial{W}} = - \frac{1}{s_{i,y_i}} \cdot \frac{\partial{s_{i,y_i}}}{\partial{W}} = - \frac{1}{s_{i,y_i}} \cdot \left [ \begin{matrix} \frac{\partial{s_{i,y_i}}}{\partial{w_1}} \\ \frac{\partial{s_{i,y_i}}}{\partial{w_2}} \\ \cdots \\ \frac{\partial{s_{i,y_i}}}{\partial{w_C}} \end{matrix} \right ] 

        For j=y_i-\frac{1}{s_{i,y_i}} \frac{\partial{s_{i,y_i}}}{\partial{w_j}} = (s_{i,y_i} - 1) x_i^T

        For j\neq y_i-\frac{1}{s_{i,y_i}} \frac{\partial{s_{i,y_i}}}{\partial{w_j}} = s_{i,j} \cdot x_i^T

        推导过程如下(for j=y_ij \neq y_i​​​​​​​的推到大抵相同,略):

 

3. 代码实现

3.1 softmax_loss_naive

def softmax_loss_naive(W, X, y, reg):
    """
    Softmax loss function, naive implementation (with loops)

    Inputs have dimension D, there are C classes, and we operate on minibatches
    of N examples.

    Inputs:
    - W: A numpy array of shape (D, C) containing weights.
    - X: A numpy array of shape (N, D) containing a minibatch of data.
    - y: A numpy array of shape (N,) containing training labels; y[i] = c means
      that X[i] has label c, where 0 <= c < C.
    - reg: (float) regularization strength

    Returns a tuple of:
    - loss as single float
    - gradient with respect to weights W; an array of same shape as W
    """
    # Initialize the loss and gradient to zero.
    loss = 0.0
    dW = np.zeros_like(W)

    #############################################################################
    # TODO: Compute the softmax loss and its gradient using explicit loops.     #
    # Store the loss in loss and the gradient in dW. If you are not careful     #
    # here, it is easy to run into numeric instability. Don't forget the        #
    # regularization!                                                           #
    #############################################################################
    # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
    num_train = X.shape[0]
    num_class = W.shape[1]
    for i in range(num_train):
        score = X[i].dot(W) # (1,D) x (D,C) --> (1,C)
        score = score - np.max(score) # Normalization to avoid numeric stability problem
        exp_score = np.exp(score)
        prob  = exp_score / np.sum(exp_score)
        loss -= np.log(prob[y[i]])
        
        prob[y[i]] = prob[y[i]] - 1
        dW   += np.expand_dims(X[i].T,axis=1) * np.expand_dims(prob,axis=0)
    
    loss  = loss / num_train + 0.5 * reg * np.sum(W*W)
    dW    = dW / num_train + reg * W
    # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

    return loss, dW

        几个要点如下: 

score = score - np.max(score) 

        这条语句对应于2.4所描述的防止由于指数运算带来的numeric stability问题。

        exp_score = np.exp(score)
        prob  = exp_score / np.sum(exp_score)

        这两条语句对应于softmax()映射。其结果就是每个样本对应于每个分类的概率,即P(y_i = j | x_i ; W),也即2.5节中的s_{i,j}。而当前样本的loss就是该概率的对数的负值。

        

        prob[y[i]] = prob[y[i]] - 1

        这条语句对应于2.5中所述的针对对应正确标签那一项的调整。 

 

        dW   += np.expand_dims(X[i].T,axis=1) * np.expand_dims(prob,axis=0)

       如前所述,在numerator layout notation中,对列向量的求导得到的是行向量,对矩阵的求导得到的张量的layout是按该矩阵的转置方式来进行安排的。所以\triangledown _W L_i的形状本应是(C,D),但是,一方面这个只是一个convention而已;另一方面,\triangledown _W L_i是要用于W的更新的,需要转置变换回到和W相同的形状。所以以下是直接计算得到\triangledown _W L_i的转置。

         np.expand_dims(X[i].T,axis=1)的形状是(D,1),np.expand_dims(prob,axis=0)的形状是(1,C),两者直接相乘由于numpy的broadcasting作用会得到形状(D,C)的张量。注意,要用np.expand_dims()进行维度扩充才能进行正确的broadcasting,相关细节可以查阅numpy文档关于broadcasting的描述。

        其余关于正则化的处理上一篇SVM中的相同,此处不再赘述。

 

3.2 softmax_loss_vectorized

        softmax_loss_naive中的关于样本的显式循环消除掉,实现完全向量化的处理得到如下代码:

def softmax_loss_vectorized(W, X, y, reg):
    """
    Softmax loss function, vectorized version.

    Inputs and outputs are the same as softmax_loss_naive.
    """
    # Initialize the loss and gradient to zero.
    loss = 0.0
    dW = np.zeros_like(W)

    #############################################################################
    # TODO: Compute the softmax loss and its gradient using no explicit loops.  #
    # Store the loss in loss and the gradient in dW. If you are not careful     #
    # here, it is easy to run into numeric instability. Don't forget the        #
    # regularization!                                                           #
    #############################################################################
    # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
    num_train = X.shape[0]
    num_class = W.shape[1]
    scores = X @ W  # (N,D) x (D,C) --> (N,C)
    scores = scores - np.max(scores,axis=1,keepdims=True)
    exp_scores = np.exp(scores)
    probs  = exp_scores / np.sum(exp_scores,axis=1,keepdims=True)
    loss   = -np.sum(np.log(probs[range(num_train),y]))
    
    probs[range(num_train),y] -= 1
        
    dW     = X.T.dot(probs)
    
    loss   = loss / num_train + 0.5 * reg * np.sum(W*W)
    dW     = dW / num_train + reg * W    

    # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****

    return loss, dW

         一些要点分别说明如下。

        scores的向量化非常直观:从单个样本的“score = X[i].dot(W)” 到批量处理的“scores = X @ W”。虽然运算符号变了,但是其实用"X[i] @ W"和"X.dot(W)"应该也是可以?需要实验确认一下。

        

    scores = scores - np.max(scores,axis=1,keepdims=True)
    exp_scores = np.exp(scores)
    probs  = exp_scores / np.sum(exp_scores,axis=1,keepdims=True)

        以上语句完成了完全向量化的score归一化、softmax映射以及概率的计算。需要注意的一点,为了能够进行正确的broadcasting处理,在np.max()和np.sum()处理中使用了keepdims选项用于保持被运算消除了的一个维度。 对于shape=(N,C)的scores,调用"np.max(scores,axis=1)"是做按行累加会得到shape=(N,),但是这样的话再执行与scores的相减运算无法进行正确的broadcasting。因此用keepdims=True将由于求和运算被压缩掉的维度仍然保留下来,“np.max(scores,axis=1,keepdims=True)”生成的结果为shape=(N,1)。后面np.sum()的调用也同理。

        

    loss   = -np.sum(np.log(probs[range(num_train),y]))

        loss的计算只取probs的每一行中对应样本的对应于类别真值的概率再取对数。 np.log执行的是element-wise运算,不需要考虑shape-matching的问题。

        “probs[range(num_train),y]”的索引方式很优雅,相当于是取:

[ p[0,y[0]], p[1,y[1]], ..., p[num_train-1,y[num_train-1]] ] 

        "probs[range(num_train),y] -= 1" 的处理方式同理。

# Both are equivalent
dW     = X.T.dot(probs)
# dW     = X.T @ probs 

         dW的向量化是这里最不直观的,可能需要好好咀嚼一下才能理解。但是在向量化处理中有一个非常nice的事情就是,当shape匹配上了,基本上就对了。这就跟两个接口对接时一样,由于接口形状匹配的要求,使得你想出错都不是那么容易。X.T.shape=(D,N), probs.shape=(N,C),所以两者相乘(矩阵乘法)得到(D,C),perfect,恰是所想要的W的形状。

 

4. softmax分类的实验性能

        对softmax.ipnyb做一些修改(其实与上一题svm.ipnyb中的修改基本相同),然后运行代码检查结果是否正确。。。此处过程不再赘述。最终softmax分类器的结果如下所示:

lr 1.000000e-07 reg 2.500000e+04 train accuracy: 0.349612 val accuracy: 0.364000
lr 1.000000e-07 reg 5.000000e+04 train accuracy: 0.330020 val accuracy: 0.347000
lr 5.000000e-07 reg 2.500000e+04 train accuracy: 0.340796 val accuracy: 0.345000
lr 5.000000e-07 reg 5.000000e+04 train accuracy: 0.332857 val accuracy: 0.349000

        这个结果居然比svm分类器要差一些。。。有点出乎我的意料。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笨牛慢耕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值