CS231n与深度学习入门>>>学习周记1

无人久伴 提交于 2019-12-23 17:55:44

1 感知机

1.1 感知机的基本概念

一个感知机类似于多个神经元构成的一个整体,接收多个信号,输出一个信号。感知机的信号可以形成流,向前方输送信息。而感知机的信号只有"0"和"1"两种取值。
下图即为接收两个输入信号的感知机示意图
两个输入的感知机
其中x1与x2是输入信号,w1与w2是权重,y为输出信号。⚪代表一个“神经元”或“节点”。
每一个输入信号送往神经元时,会被乘上固定的权重w,神经元会计算传入的信号的综合。类似于生物意义上的神经元,只有当传入的信号超 阈值 时,“神经元”才会被 激活 ,输出信号1,这个阈值用 θ 表示。
将上述原理用数学式来表示即为:
在这里插入图片描述
显然,权重说明了各个输入信号的重要性,权重愈大,对应输入信号的重要性越高。而阈值表明了“神经元”激活的难易程度。

1.2 感知机实现的简单逻辑电路

1.2.1 与门(AND gate)

与门对应真值表如下:
在这里插入图片描述
用感知机实现时,由感知机的数学式可以得到权重和阈值满足的关系:
在这里插入图片描述
显然有无穷多个满足上述关系的权重与阈值。

1.2.2与非门与或门(NAND gate,OR gate)

与非门对应真值表:
在这里插入图片描述等价于Not AND。
或门对应真值表:
在这里插入图片描述
同样的,感知机实现上述两个gate也有无穷个权重与阈值满足要求。

1.2.3 感知机的实现

稍加思索,发现感知机的条件部分可以改变一下形式,变为:
在这里插入图片描述
将θ移项至不等式左侧,以b代替-θ,得到了上述式子。其中b称为 偏置(bias)
发现不等式左侧就是一个直线,以实现 与门 为例,作图为:
在这里插入图片描述
实现与门即只需要找到这样的直线,能够将如图所示的两类点“分开”即可。以(w1,w2,b)=(0.5,0.5,0.8)为例,用python实现如下

def AND(x, y):
    w1, w2, theta=0.5, 0.5, -0.7
    tmp=w1*x+w2*y+theta
    return 0 if tmp<0 else 1

同理可以实现与非门与或门。

1.2.4 阶段小结

经过上面的分析,单层感知机的行为可以形象化为一条直线,直线一侧输出0,另一侧输出为1。而对于上述三种逻辑电路的分析与实现,可以看出感知机已经有一点 分类 的味道了,单层感知机将平面一分为二,分别代表两个类别,只要能找到符合条件的权重与阈值,我们就可以利用感知机做一点简单的分类了(非0即1)。然而单层感知机形象化后只是一条直线,自然有较大的局限性。

1.3 感知机的局限性

1.3.1 异或门(XOR gate)

异或门的真值表如下:
在这里插入图片描述
将其形象化:
在这里插入图片描述
就会发现无论如何也不能用一条直线来将两类点分离开,即单层感知机也就无法实现了。
自然而然,就会想到用多层感知机去实现。用python实现为:

def XOR(x, y):
    s1=NAND(x,y)
    s2=OR(x,y)
    tmp=AND(s1,s2)
    return tmp
#输出:
0 0: 0
1 0: 1
0 1: 1
1 1: 0

可以看出异或门的实现是通过与非门,或门,与门的组合来实现的。即实现异或门的感知机是用三个单层感知机“堆积”起来的,因而是一个多层感知机。其示意图为:
在这里插入图片描述

由于拥有权重的层实际上只有两层(第0,1与1,2层之间),因而称为”2层感知机“

1.3.2 阶段小结

从上述分析可以看出,虽然单层感知机是线性的,但是通过多个感知机的叠加,可以得到一个”非线性“的感知机,从而实现更复杂的 分类 问题。从感知机的示意图也可以看出,多层感知机的结构和之前看到的神经网络的结构类似,终于有内味了,而书中写道

感知机是学习神经网络的基础,因此本章的内容非常重要

所以应该说:神经网络的结构与多层感知机的结构类似?
总而言之,他们的思想应该是有类似之处的。

2 基本分类器

从感知机的学习之中,似乎闻到了一丝 分类 的味道。而图像识别领域的一大问题便是图像分类问题。但是上述感知机的分类似乎只能进行两种类别的判断(非0即1),而我们往往需要面对多分类问题。结合CS231n的课程内容,第一个重点便是图像分类器。

2.1 最近邻分类器(Nearest Neighbor Classifier)

This classifier has nothing to do with Convolutional Neural Networks and it is very rarely used in practice, but it will allow us to get an idea about the basic approach to an image classification problem.

可以看到,学习最近邻分类器是为学习其他分类器的一个基础,同时也会加深对图像分类问题的理解。

要想进行分类,首先要知道不同种类之间的差异在何处,自然就要知道如何去度量不同类别之间的差异。
首先考虑两张尺寸相同的图片,如何去判断两张图片之间的差异?
由于我们知道一张图片在计算机内存储为一种矩阵的形式,每个数字代表了一个像素的信息,于是拍脑袋一想,如果我们把两个矩阵相减,再将里面每个元素求和得到的数字作为这两张图片的差别,不就可以度量两张图片之间的差异了吗。公式化成如下:
在这里插入图片描述
其中p代表像素。得到的数值称为 L1距离(L1 distance)
如果两张图片完全一致,那么得到的数值就是0,如果两个图片差异较大,那么这个数值就会比较大。比较符合自己的想法。
看到L1距离之后,自然就会想到 L2距离(L2 distance)
在这里插入图片描述
同样的,两张图片完全一样时,”距离“为0,两张图片差异较大,得到的”距离“也会较大。

当我们有了”差异“的度量之后,便可以考虑分类的实现了。
考虑我们已经拥有的一个数据集,其中的每个图片都已经被正确分类,那么如何去利用这个数据集来判断一张新图片可能的类别呢?
于是又拍脑袋一想,正所谓 ”近朱者赤,近墨者黑”, 如果我把这张图片与数据集中的图片的”距离“计算出来,然后我再选取距离最近的图片所在的类别作为新图片可能的类别,这样就可以判断出一个新图片的可能类别了,于是乎,一个 最近邻分类器 就可以被实现用来分类了。
用python实现为(以L1距离为例):

class NearestNeighbor(object):
    def __init__(self):
        pass

    def train(self, X, Y):
        '''X is N*D and its row is example, Y is 1-dimension with size N'''
        self.Xtr = X
        self.Ytr = Y

    def predict(self, X):
        '''X is N*D where each row is the example we wish to predict'''
        num_test = X.shape[0]
        Ypred = np.zeros(num_test, self.Ytr.dtype)

        for i in range(num_test):
            distances = np.sum(np.abs(self.Xtr, X[i, :]), axis=1)       # L1 measure
            min_index = np.argmin(distances)  			#Find the nearest picture's label
            Ypred[i] = self.Ytr[min_index]

        return Ypred

由于最近邻分类器不需要训练,因此在train函数中只是存储了数据集,然后再predict函数中利用L1距离进行预测。
看到代码,目测predict过程是个挺煎熬的过程,消耗的时间比较多。显然在实践中应用是不合实际的,但是作为入门接触的第一个分类器,思想是比较重要的。

2.2 K近邻分类器(k - Nearest Neighbor Classifier)

通过对最近邻分类器的学习,我们知道了分类的一种思想。但是,在最后分类的时候,只选取距离最近的一张图片便定下结论, 会不会给人一种内定的感觉,给我感觉就是不踏实。
自然而然,就会想到用多张图片来判断新图片的类别。
即:对于每个新的图片,计算与数据集中图片的距离,然后从中选出K个距离最近的图片,组成一个“投票委员会”,这K个图片每张图片都 ”投票“ 给自己所在的类别,最后再根据 ”投票结果“ 判断新图片的可能类别。
于是乎,一个 K近邻分类器 便可以实现用来分类了。
用python实现如下:

class KNN(object):
    def __init__(self):
        pass

    def train(self, X, Y):
        self.Xtr=X
        self.Ytr=Y

    def predict(self, X, k=1, method='L2'):
        num_test=X.shape[0]
        Ypred=np.zeros(num_test)

        for i in range(num_test):
            if method=='L2':			#L2 method
                distances=np.sum((self.Xtr-X[i, :])**2, axis=1)**0.5
            if method=='L1':			#L1 method
                distances=np.sum(np.abs(self.Xtr-X[i, :]), axis=1)
			#Find the k-nearest pictures' labels
            k_labels=[self.Ytr[label_index] for label_index in distances.argsort()[0:k]]
            #Find the most common label 
            Ypred[i]=collections.Counter(k_labels).most_common(1)[0][0]	
            
        return Ypred

这样貌似踏实多了。但随即来了一个问题,由于k的值需要我们手动设定,那么我们如何选取K值来使得分类器的性能最佳。当K太小或太大,都会影响准确度。像k这样的需要我们做出选择的参数,称为 超参数
我们想到的一个思路,便是分别取不同的k值得到不同的 KNN 分类器,然后将每个分类器都用来分类一些图片,将准确率最高的KNN分类器的K值作为最佳的K值即可。
从而引出下面的方法:

2.3 交叉验证(Cross-validation)

我们知道数据集一般分为train data(训练集)和test data (测试集)两个部分,而

Evaluate on the test set only a single time, at the very end.

即不到最后不要去动test data。也就是说,我们要只用train data来验证各个既定超参数的准确性。
转头一想,你能把数据集拆成训练集和测试集两个部分,我卢本伟把训练集也拆成几个部分不是问题。即:

Split your training set into training set and a validation set. Use validation set to tune all hyperparameters. At the end run a single time on the test set and report performance.

在这里插入图片描述
用前四个部分来”训练“模型,然后用第五个fold(称为 校验集?(validation set))来验证模型的准确性,这样就可以在不动test data的情况下判断模型的准确性了,从而我们可以选出表现最好的K值来作为最后的超参数训练模型。最后再用测试集来测试训练出来的模型,计算准确度。

2.4 阶段小结

从上面的分析可以看出,上述两个分类器有着易于实现,易于理解,且训练不怎么消耗时间的优点,我们只是存储了数据集。但是,我们花费了大量的时间在预测分类上,使得在现实中使用时,所消耗的时间太高,因此在实践中一般很少使用。而我们关注的,一般是预测所消耗的时间而不是训练,即相比之下,我们可以忍受训练的时间较长,但不希望预测的时间较长。而且上述两个分类器都是追寻像素级的相似,一旦图片发生了旋转,缩放等等,便会对分类造成影响,导致准确率较低。
但是无论如何,这两种分类器是继续学习的基础,掌握这两种分类器依然是有必要的。

3 线性分类器

有了上述两个分类器,我们可以去实现更为powerful的分类方法。而这类方法主要有两个内容:一个是 计分函数Score function ,用来将图片数据 映射 成一个关于各个类别的分数, 另一个是 损失函数Loss function, 用来判断预测的准确程度。

3.1 计分函数(Score function)

以 CIFAR-10 数据集为例,其训练集有 N=5000 张图片以及 K=10个不同的类别,而每张图片的尺寸都是 D=32x32x3=3072个像素,我们希望找到一个函数f,能够将每个图片的数据映射成该图片关于各个类别的分数。
而能够想到的最简单的函数,便是线性函数了(大概 线性分类器 名称缘由?),定义如下:
在这里插入图片描述
其中W是一个KxD的矩阵,称为 权重b是一个Kx1的矩阵,称为 偏置。**xi**是一个DxN的矩阵,表示整个训练集的图片数据。最终我们得到了一个KxN的矩阵,即各个图片关于每个类别的 分数
以如下例子为例:
在这里插入图片描述
我们最终得到了一个3x1的矩阵,里面有这张图片关于三个类别的分数,显然可以看出,最终得到的分数中 dog类别最高,而cat类别最低,此时分类器便会偏向于把其识别为 dog,这显然是不正确的,因而说明 W并非是一个好的权重,需要继续改进。

实际中,常常会把偏置整合到权重之中,即:
在这里插入图片描述
这样便可以简化部分实现,此时函数变为在这里插入图片描述
看到这里,发现这里与前面的感知机有类似的地方,同样地,如果我们将每个图片视为一个数据点,那么我们就可以将其形象化为:
在这里插入图片描述
我们的目标,便是找到一个好的权重W使得我们的分类尽可能地正确。

3.2 损失函数(Loss function)

既然我们的目标是找到一个好的权重W,那我们首先就需要定义 “好“ 与 ”坏“的度量,因此,引出了 损失函数。当损失函数值较低时,说明比较”好“,相反,则说明比较”差“。

3.2.1 支持向量机损失(Multiclass Support Vector Machine (SVM) )

SVM的主要目的是,使得正确的分类要比错误的分类得到的分数高出一个 边界(Margin),用Δ表示。
因而定义单张图片的损失函数如下:
在这里插入图片描述
其中 yi 表示正确的类别,j代表类别,s代表由计分函数得到的分数。
再将计分函数带入可以得到:
在这里插入图片描述
其中 w 表示权重。而其中 max(0,-) 部分也称为 合页损失(hinge loss)
形象化为:
在这里插入图片描述
然后就迎来了一个新的问题:加入找到了这样的 权重W 使得损失函数为0,那么,2W,3W等等也会使得损失函数为0,那么应该选择哪一个作为最终的损失函数呢?
考虑到计算问题,自然希望在保证性能的情况下,使得 W 尽可能地简单,而如何去度量 W 的“简单”程度,便引进了 正则化(Regularization) 损失的概念。
其中最为常用的是 L2范式(L2 norm) ,定义如下:
在这里插入图片描述
这样就可以度量 W 的复杂情况, R(W) 越小,说明 W 越“简单”。
从而总的损失可以分为两个部分:data lossregularization loss。即:
在这里插入图片描述
从而,我们将问题转化为了:求权重W,使得损失函数值 L 最小。
用python实现为:

def svm_loss_vectorized(W, X, y, reg):
    """
    Structured SVM loss function, vectorized implementation.
    
    """
    loss = 0.0

    scores = X @ W
    correct_class_score = scores[np.arange(X.shape[0]), y].reshape((-1, 1))
    margin = np.clip(scores-correct_class_score+1, 0, None)
    margin[np.arange(X.shape[0]), y] = 0
    loss += np.sum(margin)
    loss /= X.shape[0]
    loss += reg * np.sum(W*W)
    
	return loss

3.2.2 Softmax损失

Softmax分类器的主要思想是,利用Softmax函数将一张图片对于各个类别的分数转换为对各个类别的置信度(概率),主要目的是使得正确的类别所拥有的置信度要高于其他类别。
首先定义如下函数:
在这里插入图片描述
Softmax函数 。其中 zj 表示第j个类别的分数。不难看出该函数值域为 (0,1]
因此定义单张图片的损失函数如下:
在这里插入图片描述
称为 交叉熵损失(cross-entropy loss)。不难看出,当正确类别的置信度是1的时候,损失为0。
在实际过程中,考虑到 fy 可能会比较大而导致溢出,因而会采取一定的技巧,即:
在这里插入图片描述
然后取:在这里插入图片描述
例子:

f = np.array([123, 456, 789]) # example with 3 classes and each having large scores
p = np.exp(f) / np.sum(np.exp(f)) # Bad: Numeric problem, potential blowup

# instead: first shift the values of f so that the highest number is 0:
f -= np.max(f) # f becomes [-666, -333, 0]
p = np.exp(f) / np.sum(np.exp(f)) # safe to do, gives the correct answer

3.3 阶段小结

通过上面的分析,我们知道了一些分类方法的思想,以及如何去判断分类结果“好坏”的一种思想。这些同样是学习神经网络的基础吧,但是到这里,我们仍然处于一个假设之中,即:我们假设我们已经有了相应的权重。自然引出了一个问题:我们应该如何去获得一个权重且去训练它使得其满足我们的要求呢?前半个问题并不难解决,但是后半个问题仍然留有疑问,从而引出了 优化(optimization) 的概念。

4 优化(Optimization)

前面我们将问题转化为了:求权重W 使得损失函数值L 最小。优化解决的正是这个问题。
想像自己正在一片山区中,周围都是由损失函数构成的“山坡”,而我们的目标是尽可能快地前往“山谷”,那么如何前往“山谷”呢,一个自然的方法就是根据自己脚的感觉,沿着“坡度”最大的方向向低处走。
转换为数学语言,即:我们已经知道了损失函数,我们只需求出损失函数的 梯度,然后沿梯度的反方向前进即可(微积分告诉我们,函数沿其梯度方向增长最快)。这便是优化的一种方式。
从而我们将问题转化为:如何求损失函数关于权重的梯度。

4.1 梯度计算

一种是利用定义:
在这里插入图片描述
给定一个较小的h值(一般为1e-5),然后计算梯度。但是由于权重包含了众多数字,显然计算的时间太长,不太可能用于实际使用。
另一种方法是利用微积分(以单张图片的SVM损失为例):
在这里插入图片描述
由微积分知识可以得到:
在这里插入图片描述
在这里插入图片描述
其中 1(X) 为示性函数,当X为真的时候,函数值为1,否则为0。
在实际过程中,一般两种方法都使用,只不过是利用微积分求出公式写入网络,定义法检验求得的梯度是否正确。

4.2 梯度下降法(Gradient Descent)

求得梯度之后,相当于知道了自己所在位置的“山坡方向”,但是我们需要用多大的步伐来“下山"呢?
同样的,在得到梯度之后,我们需要更新数据,也需要设置相应的 步长 ,也称为 学习率(Learning rate)
python实现为:

while True:
  weights_grad = evaluate_gradient(loss_fun, data, weights)
  weights += - step_size * weights_grad # perform parameter update

然而在实际中,数据集的大小通常是很大的,直接计算会消耗大量时间,因而常常取部分的样本出来进行训练与梯度下降,称为Mini-batch gradient descent. python代码如下:

while True:
  data_batch = sample_training_data(data, 256) # sample 256 examples
  weights_grad = evaluate_gradient(loss_fun, data_batch, weights)
  weights += - step_size * weights_grad # perform parameter update

这样便可以开销较小地优化模型了。

4.3 阶段小结

现在我们已经得到了这样一个系统:
在这里插入图片描述
我们已经定义了两种损失函数,定义了计分函数,也知道了如何根据损失函数来优化参数。感觉这就是神经网络的基础思想?当然我们还面临着许多问题,其中一个便是:当定义的损失函数比较复杂时,我们如何更为快速且简捷地求得损失函数的梯度?由此引出 反向传播 的概念。

参考资料:

  1. 《深度学习入门——基于Python的理论与实现》
  2. 斯坦福大学课程CS231n:https://zhuanlan.zhihu.com/p/21930884
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!