[深度学习]网络结构,权重初始化,激活函数,fine-tune

时间:2021-09-05 19:50:04

导语:

  • 直接查看一个神经网络的结构;
  • 如何初始化权重,改进准确度;
  • 在Keras上建立现行模型;
  • 激活函数的作用;
  • 如何fine-tune一个预训练的Vgg16网络,来分类猫和狗

七行代码体验深度学习的发现

Epochs

一个eposh就是一遍完整数据集的过程。

  • 运行多个epoch可以提升准确度。
  • 运行多个epoch的时候,分开监测他们的训练结果。
  • 如果你的精确度收拢的并不好,尝试减小“学习率”

在vgg对象中,可以这样改变学习率:

vgg.model.optimizer.lr = 0.01

结果可视化

jupyter进行结果的可视化很方便,举一些例子:

  • 随机一些正确的标签
  • 随机一些错误的标签
  • 每个类中正确率最高的标签
  • 每个类中正确率最低的标签
  • 最不确定的标签(概率接近0.5)
#1. 随机一些正确的标签
correct = np.where(preds==val_labels[:,1])[0]
idx = permutation(correct)[:n_view]
plots_idx(idx, probs[idx])
````



![](http://wx1.sinaimg.cn/mw690/a9c4d5f6gy1fotocxj6wdj20qy0dsn81.jpg)






<div class="se-preview-section-delimiter"></div> ```python #2. 随机一些错误的标签 incorrect = np.where(preds!=val_labels[:,1])[0] idx = permutation(incorrect)[:n_view]
plots_idx(idx, probs[idx])

[深度学习]网络结构,权重初始化,激活函数,fine-tune

#3. 很大概率是猫,并且的确是猫
correct_cats = np.where((preds==0) & (preds==val_labels[:,1]))[0]
most_correct_cats = np.argsort(probs[correct_cats])[::-1][:n_view]
plots_idx(correct_cats[most_correct_cats], probs[correct_cats][most_correct_cats])

[深度学习]网络结构,权重初始化,激活函数,fine-tune

# 很大概率是狗,并且的确是狗
correct_dogs = np.where((preds==1) & (preds==val_labels[:,1]))[0]
most_correct_dogs = np.argsort(probs[correct_dogs])[:n_view]
plots_idx(correct_dogs[most_correct_dogs], 1-probs[correct_dogs][most_correct_dogs])

[深度学习]网络结构,权重初始化,激活函数,fine-tune

#3. 很大概率是猫,但实际是狗
incorrect_cats = np.where((preds==0) & (preds!=val_labels[:,1]))[0]
most_incorrect_cats = np.argsort(probs[incorrect_cats])[::-1][:n_view]
plots_idx(incorrect_cats[most_incorrect_cats], probs[incorrect_cats][most_incorrect_cats])

[深度学习]网络结构,权重初始化,激活函数,fine-tune

#3. 很大概率是狗,但实际是猫
incorrect_dogs = np.where((preds==1) & (preds!=val_labels[:,1]))[0]
most_incorrect_dogs = np.argsort(probs[incorrect_dogs])[:n_view]
plots_idx(incorrect_dogs[most_incorrect_dogs], 1-probs[incorrect_dogs][most_incorrect_dogs])

[深度学习]网络结构,权重初始化,激活函数,fine-tune

#5. 最不确定的标签 (概率最接近0.5).
most_uncertain = np.argsort(np.abs(probs-0.5))
plots_idx(most_uncertain[:n_view], probs[most_uncertain])

[深度学习]网络结构,权重初始化,激活函数,fine-tune

还有一个比较常规的方法,来分析分类模型的结果,就是使用混淆矩阵(confusion matrix),Scikit-learn也有一个函数可以做这件事:

cm = confusion_matrix(val_classes, preds)

打印混淆矩阵,或者将它可视化。

plot_confusion_matrix(cm, val_batches.class_indices)

[深度学习]网络结构,权重初始化,激活函数,fine-tune

混淆矩阵(confusion matrix),又称为可能性表格或是错误矩阵。它是一种特定的矩阵用来呈现算法性能的可视化效果,通常是监督学习(非监督学习,通常用匹配矩阵:matching matrix)。其每一列代表预测值,每一行代表的是实际的类别。这个名字来源于它可以非常容易的表明多个类别是否有混淆(也就是一个class被预测成另一个class)。所有正确的预测结果都在对角线上,所以从混淆矩阵中可以很方便直观的看出哪里有错误,因为他们呈现在对角线外面。

预训练权重

ImageNet 网络以什么开始的?

与finetuning一个被预训练过权重的网络相比,用随机的权重训练一个卷积神经网络要花费非常大的精力。预训练网络很有效,是因为它们已经有了ImageNet的特性,权重中已经encode进特性了。典型的讲,包含线、边缘、曲线以及很多有用的表示特定部分的低级滤波器。比如下面一个layer中可以发现一些条纹和圆形的东西。

[深度学习]网络结构,权重初始化,激活函数,fine-tune

图像识别过程中这些低级的滤波器都是很有用的。预训练的权重已经学习到了这些滤波器,我们只需要简单的”fine-tune”高级的layer,改变我们需要的分类映射。

有篇论文仔细分析了上述问题: https://arxiv.org/abs/1311.290

神经网络基础

标准的全连接神经网络本质上是一系列矩阵的运算。

[深度学习]网络结构,权重初始化,激活函数,fine-tune

上面是原始的表格。

[深度学习]网络结构,权重初始化,激活函数,fine-tune

紫色圆圈是输入向量,黄色圆圈是目标向量y。下面要做一系列矩阵操作来尽可能接近目标向量y。

[深度学习]网络结构,权重初始化,激活函数,fine-tune

输入向量和第一个权值矩阵的第一列相乘,结果是activation向量中第一个值。

[深度学习]网络结构,权重初始化,激活函数,fine-tune

输入向量和第一个权重矩阵的第二列相乘,结果是activation向量中第二个值。同理得到第三个和第四个值。

[深度学习]网络结构,权重初始化,激活函数,fine-tune

第一个activations向量和第二个权重矩阵的第一列相乘,得到第二个activations向量的第一个值。同理得到第二个和第三个值。

[深度学习]网络结构,权重初始化,激活函数,fine-tune

观察每个权值矩阵中第一列的乘法。最后如何得到activation向量和我们的目标向量y一样的值呢?

类似上面,一个神经网络,它的核心就是一系列矩阵,通过矩阵乘法将输入矩阵映射到输出矩阵上。每个矩阵之间的中间矩阵就是activation,矩阵自身就是每一层。学习的过程叫做”fitting”,目标就是调整叫做权重矩阵的值,为了在给神经网络一个输入矩阵的时候,我们有能力产生一个尽可能和真实输出矩阵接近的输出矩阵。是通过传入很多已经标记过得输入矩阵来达到目的。这就是在训练集上做的事情。

根据上面的说法,随机生成矩阵元素的权值。然后执行所有的操作并观察结果,注意activation是如何输出的,和我们的目标矩阵y有多大的差距。使用一些最优化算法,可以使结果尽可能接近目标矩阵y。在这之前,建议以使得激活输出至少相对接近目标矩阵的方式来初始化权值。这种方法叫做权重初始化。

有很多的权重初始化器可以选择。这里,使用了Xavier Initialization(也叫做Glorot Initialization)。值得注意的是,现在很多的深度学习库将会为你处理好权重初始化,不需要你自己进行这一步。

梯度下降法

上面讲到的优化算法,最常见的优化算法,就是在深度学习中无处不在的梯度下降法(Gradient Descent)

标准梯度下降法

标准梯度下降法是一种迭代选择“参数”(称之为深度学习的权重),成功减少了所谓的“损失函数”,损失函数是一些简单的方法,来确定不同的预测输出,由预测”参数”和给定的输入,从用相同的输入来得到相关联的输出。一个常见的损失函数是平方误差之和,它只是预测响应和真实响应之间的差之平方和之和。这种损失函数在线性回归过程中很常见。另一个常见的损失函数是log-loss,上面已经定义过了。通常在神经网络中使用这种损失函数。

由于我们的损失函数本质上是衡量我们的预测与期望值相匹配的程度,我们的优化算法的目标是最小化这个值。我们的预测函数至少有两种数值,即函数作用的输入,以及决定我们对输入做什么的“参数”。因为我们不能改变输入,所以我们必须通过选择能产生接近于期望值的参数来最小化损失函数。

梯度下降是一种迭代的“改进”初始参数值的方法(通过一些进程初始化),以尽量减少损失函数。我们通过计算损失函数关于每个参数的偏导数,并通过在导数方向相反的一步来更新参数。当我们在所有参数中这样做时,我们(希望)以这样一种方式更新我们的参数,这些新参数确定的预测减少损失函数。在以后可能会遇到,使用这种方法可能会出现一些问题,以及我们如何处理它。这个过程中的“梯度”是描述损耗函数是如何在每个参数上变化的向量。我们前面提到的另一个值得注意的是学习率(learning rete)。这个值指示我们在更新参数时采取的步骤有多大,这就是所谓的“超参数(hyper-parameter)”。

线性回归的例子

这听起来可能比实际情况复杂得多。举个例子,我们可以看到线性回归中的梯度下降(拟合一条直线)。

如果直线是ax + b,其中a和b是“参数”,我们的目标求得使损失函数最小化的a和b。 损失函数本质上是一个数学函数,如果猜错了(在我们的例子中是a和b),那么这个函数将会很高,如果猜对了,那么这个函数结果就会比较小。 在线性回归中,我们使用平方差之和作为损失函数。 在每次迭代中,计算这个函数关于a和b的导数。 这告诉我们损失函数如何改变这两个参数。 如果关于a的导数是正值,那么增大a将增大损失函数。 所以就减小a。 如果是负值,增大则会减小损失函数,所以增大a。 无论哪种情况,都朝着与a的导数的正方向相反的方向前进。 一遍一遍的这样做,直到满意。

拓展到神经网络

这种优化技术的强大之处在于我们已经对线性模型的参数进行了随机初始化,并且通过梯度下降迭代,我们可以得到最优解。 这里的关键是,我们用同样的过程来估计像线性函数那样简单的参数,也可以用来估计像神经网络这样复杂的参数,但有一些注意事项。 在线性回归中,我们通常总是能够得到最好的解决方案。 由于具有数百万个参数的神经网络的复杂性,这几乎从未如此。 往往我们永远不会找到所有参数的最佳最小值。 这就是为什么我们不直接在神经网络上运行梯度下降,直到满足一些终止条件,就像我们使用线性回归一样。 相反,我们运行梯度下降,直到我们对结果满意。

随机梯度下降

另一个关键的区别是我们到目前为止只谈到了“标准”梯度下降。 在标准梯度下降中,对所有可用的训练数据评估损失函数。 不幸的是,由于计算限制,这在神经网络中是不可能的。 因此,我们使用所谓的随机梯度下降。 这是通过随机抽样或者我们的数据的“mini-batch”来产生对这个小批量的预测,并用它们的真实值来评估损失函数。 然后我们像往常一样更新权重,然后移动到下一个小批量,重复这个过程。 这个过程的“随机”元素是通过评估不同小批量的损失函数而引入的随机元素,而不是整个训练集。 对于每个小批量,损失函数将略有不同,它也将不同于整个训练集的损失函数。 但事实证明,这并不重要! 随机梯度下降的神奇之处在于,您可以更新随机小批量训练集上的权重,并且您的结果将与您更新整个训练集上的真实损失函数的权重相同。

举例

作为例子,下面展示keras如何实现梯度下降,我们将在线性回归的context中使用它。

x = random((30,2))
y = np.dot(x, [2., 3.]) + 1.
x[:5]

[深度学习]网络结构,权重初始化,激活函数,fine-tune

在这里所做的是创建y值,通过关系 y = 2 * x1 + 3 * x2 + 1与x1 , x2线性相关。 在keras中,一个简单的线性模型被称为Dense layer。 通过我们的输入和期望的输出x和y ,Keras将初始化某种形式的随机权重。 我们会告诉它使用SGD来优化,学习率为0.1,以最小化损失函数均方误差(mse):

lm = Sequential([ Dense(1, input_shape=(2,)) ])
lm.compile(optimizer=SGD(lr=0.1), loss='mse')

通过评估我们的损失函数,可以看到初始权重有多远。

lm.evaluate(x, y, verbose=0)
8.6175813674926758

接下来,我们将运行随机梯度下降。 拟合函数,如下所述,在sgd的每次迭代中,将使用训练集中的一个输入/输出对来评估损失函数并更新参数。 整个训练集中的一次遍历计算被称为epoch。 下面经历5个epoch。

lm.fit(x, y, nb_epoch=5, batch_size=1)

[深度学习]网络结构,权重初始化,激活函数,fine-tune

来看看我们的评估函数:

lm.fit(x, y, nb_epoch=5, batch_size=1)
2.3591119315824471e-05

和期待的一样,小了很多

fitting 后我们也可以看看权重。 我们期望它们非常接近真实参数(2.0,3.0和1.0)。

lm.get_weights()

[深度学习]网络结构,权重初始化,激活函数,fine-tune

的确和预期一致。 如果我们使用的batch size大于1,可以预期我们的权重会更快地收敛到真实权重。

Cats vs Dogs and Finetuning

我们现在已经足够了解如何修改Vgg16来创建一个模型,来输出猫和狗的预测。

添加一个Dense Layer

我们在上一节中使用的Dense Layer将输入向量映射到单个输出。 我们可以很容易地改变这个输出到一个任意长度的向量,注意这个输出的权重结构将只是一个矩阵。

Vgg16的最后一层输出1000个类别的向量,因为这是比赛要求的类别数量。 在这些类别中,其中的一些当然对应于猫和狗,但在更细微的层面(特定品种)。 我们可以手工找出哪些类别是猫,哪些是狗,只需编写一些将imagenet分类转换为猫和狗分类的代码。 但这样做效率不高,我们会错过一些关键信息。

一个更好的方法是简单地在imagenet层的顶部添加Dense Layer,并训练模型,以将猫和狗的输入图像的imagenet分类映射到猫和狗的标签。 为什么这比手动做呢更好? 因为神经网络将利用imagenet分类中的所有可用信息,而不是简单地将cat分类映射到cat和dog分类。 例如,德国牧羊犬与骨头的图片可能在德国牧羊犬类别和骨头类别中具有很强的概率。 如果我们只把狗的类别映射到狗身上,然后把其他的信息丢掉,那么我们就会失去其他对分类有用的信息,比如图像中是否有骨头。

总体方法是:

  • 获取每个图像的真实标签。
  • 获取每个图像的1,000个imagenet类别预测。
  • 将这些预测作为输入提供给简单的线性模型。

需要注意的一点是,我们从批处理中得到的标签需要进行one-hot编码。 one-hot编码只需要分类变量,并将其转换为矩阵,其中每列表示一个类别。 如果图像属于类别A,那么矩阵中该图像的行在A类列中有1,而在其他情况下为0。 我们采取步骤将标签转换为这些向量的一个重要原因,是因为它与dense layer的输出形状相同。 因此,为了训练目的,我们需要对它们进行one-hot。 现在可以预测。

trn_features = model.predict(trn_data, batch_size=batch_size)
val_features = model.predict(val_data, batch_size=batch_size)

如果我们通过预测将我们的猫与狗的训练/验证传递给我们的模型,那么我们将得到1000个imagenet分类概率。 接下来,我们可以做出如下模型:

#1000 输入,为了保留特性,2输出,狗和猫
lm = Sequential([ Dense(2, activation='softmax', input_shape=(1000,)) ])
lm.compile(optimizer=RMSprop(lr=0.1), loss='categorical_crossentropy', metrics=['accuracy'])

需要将1000个 imagenet概率,映射到输出,一个是猫,一个是狗。 初始化后,模型不知道怎么做。 但是,使用one-hot 编码标签,可以使用imagenet预测作为输入来训练此layer。 现在我们所要做的就和以前一样。

batch_size=64
lm.fit(trn_features, trn_labels, nb_epoch=3, batch_size=batch_size, 
       validation_data=(val_features, val_labels))

[深度学习]网络结构,权重初始化,激活函数,fine-tune

这种简单地将dense layers附着到预训练的模型,以对期望的类别进行分类的方法,通常使得深度学习的业余人员获得令人惊讶的结果,诸如分类皮肤损伤。 这真的很简单, 我们真的没有什么神奇的功能,只是通过预先训练好的模型输出来训练线性模型。 通过这样做,我们已经实现了大于97%的分类准确度。

激活层

之前描述一个神经网络时,解释它是一系列的矩阵乘法,称之为层,它转换一个输入向量。 在每个中间步骤中,我们将这些层的输出称为激活层。 然而,经过反思,这似乎很奇怪。 如果一个神经网络只是一个矩阵乘法的序列,那么整个过程就是一个线性过程,可以用一个矩阵表示。

当然不是这种情况。 之前我们关注中间向量的原因是因为在神经网络中,实际上在这些激活层中存在非线性函数,其在前一层的输出上操作。 这个新的向量然后被馈送到下一个矩阵中。 一些常用的激活函数是 tanh ,sigmoid 函数和 relu(rectified linear unit的缩写)。 Relu实际上是最常见的,虽然听起来像一些神秘的功能,但它只是函数max(0,x) 。 现在观察Vgg16中的每个图层都有激活函数,它们只是告诉keras如何转换特定线性图层的输出。

事实证明,这种线性变换和非线性激活的组合能够逼近任何东西。

Finetuning

如果我们观察Vgg16的最后一层,可以看到最后一层只是一个输出1000个元素的dense layer。 因此,将一层意味着要找到猫和狗的dense layer堆叠起来,似乎有些不合理,因为在分类为猫和狗之前,首先强迫神经网络将其分类到imagenet,限制了一些可用的信息。

相反,我们来删除最后一层

model.pop()
for layer in model.layers: layer.trainable=False

然后给猫和狗添加一个新层

model.add(Dense(2, activation='softmax'))

注意 :以上3个步骤都是vgg16的 finetune() 方法!

现在我们可以使用上一层的所有4096个激活(不管它们可能是什么!)来分类为猫和狗。 这隐含地应该为我们提供更丰富的信息来分类。

接下来的步骤是像以前一样简单地训练,如果你这样做,你会发现你的模型会产生比只是在上面增加另一个图层更好的结果。