线性分类器
在线性网络中,以一次线性函数作为计算的网络,SVM,softmax,以及浅层神经网络中,关于基本实现上,最重要的就是loss和gradient的实现,train的过程,其实最终都是求loss和gradient,所以,其他部分我不会记录太多。
SVM
在cs231n的作业1中,每个分类器都用loop和非loop方法,或是否直接处理矩阵方法,其中留给我们写的大多都是其中核心内容,所以额外部分我也不多说。
现在基本上都会直接使用非loop方法或者直接操作矩阵。所以我也只说这部分。
线性函数计算无外乎:
其中,W权重和b偏置是我们需要进行训练的参数,就直接以CIFAR-10的数据集来说,我们知道得到的结果有10类,所以有10个不同类别的得分情况
上面的矩阵代表了某张图片的每个类别的得分情况,其中有一个代表了正确的得分,correct_scores,其余代表了其他几个类别的得分,那么SVM的loss是怎么计算的?这里要引入一个新的参数,边界值,delta
其他的九个类别分别减去正确类别的得分,只有当其他类的得分比正确类得分小于边界值时,loss为0.上式常被称为折叶损失(hinge loss)。还有一种平方折叶损失,使用的是 ,将更强烈地惩罚过界的边界值。标准版是折叶损失,但是在某些数据集中,平方折叶损失可能会更好。
正则化(Regularization): 假设我们目前的loss为0,在这个前提下,权重W并不是唯一的:可能有很多相似的W都能正确地分类所有的数据。一个简单的例子:如果W能够正确分类所有数据,即对于每个数据,损失值都是0。那么当x > 1时,任何数乘都能使得损失值为0,因为这个变化将所有分值的大小都均等地扩大了,所以它们之间的绝对差值也扩大了。 最常用的正则化惩罚是L2范式,L2范式通过对所有参数进行逐元素的平方惩罚来抑制大数值的权重。
loss= 0.0
num_train= X.shape[0]
delta= 1.0
scores= X.dot(W)
correct_scores= scores[np.arange(num_train), y]
# correct_class_score[:,np.newaxis] 把correct_class_score.shape = (N,)
#turn to correct_class_score[:,np.newaxis].shape = (N,1)
margins= np.maximum(0, scores-correct_scores[:,np.newaxis] +delta)
margins[np.arange(num_train), y] = 0.0
loss+= np.sum(margins) /num_train
loss+= 0.5*reg*np.sum(W*W)
因为是以整个矩阵来进行计算,所以为了除去loop,我们会将10个类都计算一次,包括正确分类,但是第9行代码会在最后将正确分类的损失设为0.第11行就是正则化惩罚,来选择一个分布相对均匀的。举个例子:下面分别是W1,W2,X
如果偏置bias是固定的。那么scores = wx + b W1和W2得到的结果是一样的。所以loss也是一样的,但是他们两个中谁会更好一点,L2正则化惩罚就是来选取在结果相同的情况下选取更优的权重。我们可以看出W2 每个元素是0.25的会更好一点。因此,我们在相同结果下,最优的权重更加均匀分布。这也是cs231n视频中所提到的问题。
我们现在来看看gradient,最后的激活函数,我们可以这么理解。np.maxmium(0, scores - correct_scores) 跟Relu acitivity function 有点类似。等于0的梯度不会进行BP,大于0的梯度为1,以下为代码部分
margins[margins>0] = 1.0
row_sum= np.sum(margins, axis=1)
margins[np.arange(num_train), y] = -row_sum
dW+= np.dot(X.T, margins) /num_train+reg*W
计算loss和gradient是每个神经网络中最重要的部分,而train的部分,大多都用mini_batch SGD方法来训练,batch_size为每批的训练量,FP(forward propagation)得到loss,BP(backwork propagation)得到gradient。然后用gradient和learning_rate 的乘积来更新参数。batch = np.random.choice(num_train, batch_size, replace = True) replace参数代表取出是否放回,True放回反之不放回。
predict:计算出每个类的得分scores,然后用np.argsort(scores, axis = -1)[:,-1]得到每一行的最大值的下标。
accuracy:np.mean(y_pred == y) 得到精确率。
Softmax
前面所有的计算与SVM的过程是一致的,最后的激活函数不同,softmax是以百分比来描述每个类别的可能性,对scores进行指数操作np.exp(scores)。
求和得到分母,然后每个类别的可能性
loss: Softmax的loss跟SVM不一样,将SVM的折叶损失替换为交叉熵损失(cross-entropy loss)。公式如下:
或等价于
下面引用知乎上的一段话。
在上式中,使用 来表示分类评分向量f 中的第j个元素。和之前一样,整个数据集的损失值是数据集中所有样本数据的损失值 的均值于正则化损失之和。其中函数
被称为softmax函数:其输入值是一个向量,向量中元素为任意实数的评分值(z 中的),函数对其进行压缩,输出一个向量,其中每个元素值在0到1之间,且所有元素之和为1.所以,包含softmax函数的完整交叉熵损失看起来唬人,实际上还是比较容易理解的。
loss= 0.0
# 样本数量
num_train= X.shape[0]
# 类别数
C= W.shape[1]
# 在某些情况下,当scores的数值过大,计算exp指数时会导致数值爆炸
# 防止数值爆炸,一般会用减去最大值或者均值
scores= np.dot(X,W)
scores= scores-np.max(scores,axis= 1,keepdims= True)
exp_scores= np.exp(scores)
margins= np.sum(exp_scores,axis=1,keepdims=True)
# 第一种表示方式
margins_log= np.log(exp_scores/margins)
loss= -(loss+np.sum(margins_log)) /num_train+np.sum(W*W) *reg
# 第二种表示方式
loss= -scores[np.arange(num_train),y].sum() +np.log(np.sum(exp_scores,axis=1)).sum()
loss/= num_train
loss+= reg*np.sum(W*W)
gradient:我们对上式 进行求导。
这部分取自知乎我也是看知乎的回答之后,才慢慢弄懂得。
直译就是对于某个样本,其对正确分类的贡献比错误分类的贡献小,计算好右边那一大堆后,直接对正确分类减1就行了
margins_grad= exp_scores/margins
margins_grad[np.arange(num_train),y] += -1
dW+= np.dot(X.T, margins_grad) /num_train+reg*W
trian&predict:基本上和SVM没有区别,他们之间主要的区别还是loss function。
SVM和Softmax的比较
两个分类器都计算了同样的分值向量f(本节中是通过矩阵乘来实现)。不同之处在于对f中分值的解释:SVM分类器将它们看做是分类评分,它的损失函数鼓励正确的分类的分值比其他分类的分值高出至少一个边界值。Softmax分类器将这些数值看做是每个分类没有归一化的对数概率,鼓励正确分类的归一化的对数概率变高,其余的变低。SVM的最终的损失值是1.58,Softmax的最终的损失值是0.452,但要注意这两个数值没有可比性。只在给定同样数据,在同样的分类器的损失值计算中,它们才有意义。
Softmax分类器为每个分类提供了“可能性”:SVM的计算是无标定的,而且难以针对所有分类的评分值给出直观解释。Softmax分类器则不同,它允许我们计算出对于所有分类标签的可能性。举个例子,针对给出的图像,SVM分类器可能给你的是一个[12.5, 0.6, -23.0]对应分类“猫”,“狗”,“船”。而softmax分类器可以计算出这三个标签的”可能性“是[0.9, 0.09, 0.01],这就让你能看出对于不同分类准确性的把握。为什么我们要在”可能性“上面打引号呢?这是因为可能性分布的集中或离散程度是由正则化参数λ直接决定的,λ是你能直接控制的一个输入参数。举个例子,假设3个分类的原始分数是[1, -2, 0],那么softmax函数就会计算:
现在,如果正则化参数λ更大,那么权重W就会被惩罚的更多,然后他的权重数值就会更小。这样算出来的分数也会更小,假设小了一半吧[0.5, -1, 0],那么softmax函数的计算就是:
现在看起来,概率的分布就更加分散了。还有,随着正则化参数λ不断增强,权重数值会越来越小,最后输出的概率会接近于均匀分布。这就是说,softmax分类器算出来的概率最好是看成一种对于分类正确性的自信。和SVM一样,数字间相互比较得出的大小顺序是可以解释的,但其绝对值则难以直观解释
双层神经网络
继续从loss和gradient来开始说,我们先了解两层神经网络的结构。
数据集还是CIFAR-10,因此我们的数据是图像
def __init__(self, input_size, hidden_size, output_size, std=1e-4):
"""
Initialize the model. Weights are initialized to small random values and
biases are initialized to zero. Weights and biases are stored in the
variable self.params, which is a dictionary with the following keys:
W1: First layer weights; has shape (D, H)
b1: First layer biases; has shape (H,)
W2: Second layer weights; has shape (H, C)
b2: Second layer biases; has shape (C,)
Inputs:
- input_size: The dimension D of the input data.
- hidden_size: The number of neurons H in the hidden layer.
- output_size: The number of classes C.
"""
self.params = {}
self.params['W1'] = std * np.random.randn(input_size, hidden_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['W2'] = std * np.random.randn(hidden_size, output_size)
self.params['b2'] = np.zeros(output_size)
其中input_size,hidden_size,output_size 是初始化权重与偏置的参数。
input - fully connected layer - ReLU - fully connected layer - softmax
这是双层神经网络的结构,输入经过全连层,经过Relu 激活函数,在经过一层全连层,经过softmax激活函数,这是FP过程得到loss。
W1,b1 = self.params['W1'],self.params['b1']
W2,b2 = self.params['W2'],self.params['b2']
N, D = X.shape
full_con1 = np.dot(X, W1) + b1
relu1 = np.maximum(full_con1,0)
scores = np.dot(relu1, W2) + b2
# Compute the loss
loss = 0.0
# 防止指数计算数值爆炸
scores = scores - np.max(scores,axis=1,keepdims=True)
exp_scores = np.exp(scores) # N,c
margins = np.sum(exp_scores, axis=1,keepdims=True)
loss = -scores[np.arange(N),y].sum() + np.log(np.sum(exp_scores,axis=1)).sum()
loss /= N
loss += 0.5*reg*(np.sum(W1*W1) + np.sum(W2*W2))
backword propagation:
其中最重要的就是把每一层的shape给记住,在进行矩阵点乘的时候就不会出错。双层神经网络求导还没有那么困难,所以把求导过程写在纸上,然后边看边去用代码实现,过程中注意矩阵点乘时的shape是否符合要求,是否需要进行转置就行了。
grads = {}
grads['W1'] = np.zeros_like(W1)
grads['b1'] = np.zeros_like(b1)
grads['W2'] = np.zeros_like(W2)
grads['b2'] = np.zeros_like(b2)
margins_grad = exp_scores / margins
margins_grad[np.arange(N),y] += -1
dL = margins_grad #shape N,C
dL /= N
grads['b2'] = np.sum(dL,axis=0) #shape (C,)
grads['W2'] = relu1.T.dot(dL) + reg * W2 # shape H,C
dReluf1[full_con1 <= 0] = 0
grads['b1'] = np.sum(dReluf1,axis=0) #shap H,
grads['W1'] = X.T.dot(dReluf1) + reg * W1 #shape D,H
train&predict:和SVM、Softmax没什么区别。求导的时候先在草稿纸上写出过程,会比空头想会好很多。dL /= N 这个一定要记得加,我之前就忘记加了。其实我也不是很清楚为什么要加这个,哦,可能是我们通过计算margins_grad,其实跟loss有点像,因为我们的导数dL其实就是根据loss求导来的,因为loss /= N 除以 数据量N,但是margins_grad 的结果还没有除以数据量N,所以要加上dL /= N。写博客的好处就是将之前的知识总结并且有时候能把其中没弄懂得弄懂。哈哈