SVM:

W:(D,C)W:(D,C) X:(N,D)X:(N,D) y:(N,)y:(N,)

注:1. (N,)表示一维向量,既不是行向量也不是列向量。如果要显式的表示行向量(1,N)或者列向量(N,1),就要用.view(1,-1)或者.view(-1,1)。

  1. f(xi,W)=WTxif(x_i,W)=W^Tx_i 其中xix_i表示X[i]X[i],即XX的第ii行,是个一维向量(D,)(D,),不是行向量!(即你取行/列都会降维)
  2. 不管是什么乘法(矩阵和矩阵,矩阵和向量,点乘还是矩阵乘法),都用matmul()就好了,免得报错
  3. WW 中行数DD代表特征数,一般为RGB通道+像素值,可以认为这个特征数极大,包含了像素的所有信息;列数CC表示种类数目,如CIFR-10提供了10种图片类别。XX的行数NN表示样本的数量,因数据库的大小不同而不同。

这里直接借用@z_z大佬的图:

image-20250303200719213

LossLoss记得要平均,而且每次都是累加,不是赋值,否则都会被循环最后一次的取值给覆盖。还要记得正则化,一般是L2L_2norm(不用开根号!只需要取每个元素的平方再乘以正则化系数),即R(W)=R×\absW2R(W)=R\times \abs{W^2}

dWdW的计算方法: 注意,求导是对WW中的每个元素而言的。即WW中每个元素变化一点,整个LL损失函数的取值会如何变化。我们关注的是WW而不是XX!因为WW才是我们训练出来的模型。

对于SVM而言,Li=jyimax(0,sjsyi+1)L_i = \sum_{j\neq y_i}\max(0,s_j-s_{y_i}+1)

循环法

计算LidW\frac{\partial L_i}{dW}需要分两种情况,但是总体思想都是用链式法则

LiW=LisisidW\frac{\partial L_i}{\partial W} = \frac{\partial L_i}{\partial s_i}\frac{\partial s_i}{dW} sis_i代表对于第ii个样本所得到的score,使用WTxiW^Tx_i计算得到的,这样的好处是我们很容易知道siW\frac{\partial s_i}{\partial W}等于xix_i

推导:对于某个确定的ii而言,si=WTxis_i=W^T x_i,dsi=d(WT)xi+WTdxi=(dW)Txids_i=d(W^T)x_i+W^Tdx_i=(dW)^Tx_i,两边同时取迹,有si=tr(si)=tr((dW)Txi)=tr(xiTdW)s_i=tr(s_i)=tr((dW)^Tx_i)=tr(x_i^TdW),因此siW=xi\frac{\partial s_i}{\partial W}=x_i。当j=y[i]j=y[i]时,说明是正确的标签,对损失函数贡献为0,自然导数也为0.当jyij\neq y_iLi>0L_i>0时,对于正确类别而言,由Li=jyimax(0,sjsyi+1)L_i = \sum_{j\neq y_i}\max(0,s_j-s_{y_i}+1)不难看出,Lsyi=1\frac{\partial L}{\partial s_{y_i}}=-1,因此有链式法则相乘,梯度减去X[i]-X[i]。同理,对于错误类别,导数为1,那么梯度应该加上X[i]X[i]

代码实现:

def svm_loss_naive(W: torch.Tensor, X: torch.Tensor, y: torch.Tensor, reg: float):
    dW = torch.zeros_like(W)  # initialize the gradient as zero
    num_classes = W.shape[1]
    num_train = X.shape[0]
    loss = 0.0
    for i in range(num_train):
        scores = W.t().mv(X[i])
        correct_class_score = scores[y[i]]
        for j in range(num_classes):
            if j == y[i]:
                continue
            margin = scores[j] - correct_class_score + 1  
            if margin > 0:
                loss += margin
                dW[:,y[i]] -= X[i]
                dW[:,j] += X[i]
    loss /= num_train
    loss += reg * torch.sum(W * W)
    dW_regular = 2 * reg * W
    dW += dW_regular
	return loss, dW

向量化法

特点:直接对整个矩阵进行操作,而不是抽某一行/列。(省时,并发)

为了不使用循环而选出每一个样本正确类别对应的分数,采用一个非常巧妙的办法取出数据,那就是让行数和列数一一对应,形成有序的数对(类似zip),即行和列两个位置不放数字,放两个形状相同的tensor。这里我们希望在第一个样本取出数据后自动到第二个样本,那么我们可以建立自然数(0~N-1)与正确标签y的一一映射,前者可以用torch.arange(N)实现。但是取出来之后是个一维张量,我们需要将其reshape成二维列向量(N,1)。

计算损失矩阵(不妨设为margin)时可以充分利用广播机制代替循环,广播机制会将(N,1)复制成(N,C),每一行都是相同的数字,也就是正确序号对应的分数。我们需要将最小值设成0,小于0的说明不会对损失函数造成影响,而大于0的我们需要注意:有两种情况,一种是本来就是对的,那么经过Li=jyimax(0,sjsyi+1)L_i = \sum_{j\neq y_i}\max(0,s_j-s_{y_i}+1)后会得到1,这种影响我们需要手动排除,因此需要直接赋值为0(过程同上述取数据);如果是错的那就会对损失函数造成影响。最后别忘了损失函数是个标量,需要将矩阵各元素加和求平均再正则化。

而计算dWdW是一个难点,因为我们没办法再用循环的操作了,所以我们只能想办法用矩阵乘法来代替循环(比如说如果我们想要交换矩阵的相邻两列,我们不用循环的话可以用单位矩阵进行相同的变换后右乘得到)

我们先明确:对于margin而言,大于0的说明是错的,梯度应该为1;而对于本身正确的序号,由于一行里有多少次margin>0就有多少次syi-s_{y_i},所以梯度需要减去大于0的元素个数(用sum,且指定dim=1,dim=1代表第二个维度消失,只剩下第一维度(如果用了keepdim=True代表保留求和的那个维度,只是维度变成1))

后面我自己没搞懂,先贴一个支持向量机.pdf by z_z

我自己又想明白了,可能后面得多回顾。SVM&SOFTMAX by 6ch.