前言
完整代码见github
问答总结
- SVM反向传播参数梯度的推导过程?
- 如何使用矩阵乘法的列观点向量化梯度求导过程?
- 布尔索引(
mask[score>0] = 1
)的含义是什么? - 切片索引(
x[[0,1],[1,2]]
)的含义是什么? - 书写完整代码一般分为
main
,train
,evaluate
三个部分。说明他们各自的作用。
一、实验目标
- 使用
多类别SVM
损失函数进行图片分类,推导梯度更新公式,使用随机梯度下降算法
更新梯度。 - 加入正则化项,比较
no-reg
,L1-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∑{0sj−syi+1syi−sj−1≥0else
其中
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} W∈RK×C,b∈RC, 其中 C C C为类别数。对于样本 X i ∈ R 1 × K X_i \in R^{1 \times K} Xi∈R1×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=1∑NLi
我们目标是求 ∂ L ∂ W , ∂ L ∂ b \frac{\partial L}{\partial W}, \frac{\partial L}{\partial b} ∂W∂L,∂b∂L, 稍作变换:
∂
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}
∂W∂L=N1i=1∑N∂W∂Li
∂
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}
∂b∂L=N1i=1∑N∂b∂Li
由于:
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 syi−sj−1<0∑sj−syi+1=j̸=yi and syi−sj−1<0∑XiWj+bj−XiWyi−byi+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
∂Wj∂Li=j̸=yi and syi−sj−1<0∑XiT
∂
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
∂Wyi∂Li=j̸=yi and syi−sj−1<0∑−XiT
∂
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
∂bj∂Li=j̸=yi and syi−sj−1<0∑1
∂
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
∂byi∂Li=j̸=yi and syi−sj−1<0∑−1
这样,我们就能够进行更新了。
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=1∑NLi+21λ∣∣W∣∣2
很容易求得, 加入正则项后,梯度会增加 λ 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 syi−sj−1<0, 改变量为 X i X_i Xi,特别的,对于该样本所属类别 y i y_i yi, 其模板参数梯度改变量为其余改变量之和的相反数。
- 条件二: 回想矩阵乘法最初诞生时的意义。使用列观点看待矩阵乘法:
这样,我们设置 m a s k ∈ R N × C mask \in R^{N \times C} mask∈RN×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} XT∈RK×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))