《深度学习图解》关于边与角的神经学习


在多个位置复用权重

如果需要在多个位置检测相同的特征,请使用相同的权重。

过拟合的产生通常是由于当前网络参数的数量多于学习特定数据集所需要的参数数量——这种情况下,网络有足够多的参数,以至于它可以记住训练训练集中的每一个细节,而不是对高层次的抽象进行学习。当神经网络有很多参数,但并没有很多训练样例时,过拟合是很难避免的。

之前说过的正则化并不是防止过拟合的唯一技术,甚至不是最理想的技术。

模型中权重的数量与学习这些权重的数据点的数量之比,和过拟合高度相关。因此,有一个更好的防止过拟合的方法——使用松散定义的模型或者说网络结构。

网络结构:在神经网络中,因为我们相信能够在多个位置检测到相同的模式,所以可以有选择地重用针对多个目标的权重。

这可以显著地减少过拟合,并导致模型的精度更高,因为它降低了权重数量与数据量的比例。

尽管删除参数通常来说会降低模型的表达能力(或者说降低对模式的学习能力),但是如果能够巧妙地重用权重,那么模型的表现力可以是相同的,但对过拟合的鲁棒性会更强一些——这种技术也趋向于使模型更小(因为要存储的实际参数更少)。

在神经网络中,最著名且最广泛使用的网络结构叫作卷积,当作为一层使用时叫作卷积层。

卷积层

化整为零,将许多小线性神经元层在各处重用。

卷积层背后的核心思想是,它不是一个大的、稠密的线性神经元层,在其中从每个输入到每个输出都有连接。而是由很多非常小的线性层构成,每个线性层通常拥有小于25个输入和一个输出,我们可以在任意输入位置使用它。

每个小神经元层都被称为卷积核,但它实际上只是一个很小的线性层,接受少量的输入并作为单一输出:

image-20231204172403129

卷积核的具体操作不再赘述,每一个卷积核的输出结果将是一个更小的方形预测矩阵,可以作为下一层的输入。一个卷积层通常包含很多卷积核。

图片底部时4个不同的卷积核,它们处理相同的8×8的数字2的图像。每个卷积核的结果是一个6×6的预测矩阵,对于这个拥有4个3×3的卷积核的卷积层来说,它的结果是4个6×6的预测矩阵:

image-20231204173815116

你可以对这些矩阵求和(求和池化),取它们的均值(平均池化),或按元素计算最大值(最大池化)。

最大池化的使用是最多的:对于每个位置,查看4个卷积核各自的输出,找到最大值,并将它复制到一个6×6的最终矩阵中,当所有运算完成后,这个最终矩阵(且仅有这个最终矩阵)将信号向前传播到下一层。

注意:

  1. 右上角的卷积核只有在发现一条水平线段时才会向前传播1。
  2. 左下角的卷积核仅当它发现指向右上角的对角线时才会向前传播1。
  3. 右下角的卷积核没有识别出它被训练来预测的任何模式。

更重要的是,网络的训练过程允许每个卷积核学习特定的模式,然后在图像的某个地方寻找该模式的存在。一个简单小巧的权重集合可以学习更大的一组训练实例,因为即使数据集没有改变,每个小巧的卷积核也都在多组数据上进行了多次前向传播,从而改变了权重数量与训练这些权重的数据量的比例。

这对网络产生了显著影响,极大地降低了神经网络对训练数据的过拟合现象,提高了网络的泛化能力。

基于Numpy的简单实现

从正向传播开始。下面的代码展示了如何在用NumPy表示的一批图像中选择子区域。注意,它为整批图像选择相同的子区域:

def get_image_section(layer,row_from, row_to, col_from, col_to): # 图像层 起始行 终止行 起始列 终止列
    section = layer[:,row_from:row_to,col_from:col_to] # 在函数内部,使用切片操作符从图像层中提取了一个特定的图像区域
    return section.reshape(-1,1,row_to-row_from, col_to-col_from) # 返回图像的新形状

因为这个方法选择了一批输入图像中的一小部分,所以需要(在图像的每个位置)对它进行多次调用:

layer_0 = images[batch_start:batch_end]
        layer_0 = layer_0.reshape(layer_0.shape[0],28,28)
        layer_0.shape

        sects = list()
        for row_start in range(layer_0.shape[1]-kernel_rows):
            for col_start in range(layer_0.shape[2] - kernel_cols):
                sect = get_image_section(layer_0,
                                         row_start,
                                         row_start+kernel_rows,
                                         col_start,
                                         col_start+kernel_cols)
                sects.append(sect)

        expanded_input = np.concatenate(sects,axis=1)
        es = expanded_input.shape
        flattened_input = expanded_input.reshape(es[0]*es[1],-1)

这段代码中,layer_0是一批大小为28×28的图像。for循环遍历了图像中的每个(kernel_rows×kernel_cols)子区域,将它们放入一个名为sects(切片)的列表中。然后,将sects列表中的部分连接起来并转换为一种特殊形状。

现在,假设每个子区域都被看作它自己的图像。因此,如果批处理大小为8个图像,并且每个图像有100个子区域,那么可以假设它的一批总数为800张的较小图像。

通过线性层的一个输出神经元对它们进行正向传播这一过程,与每批图像在各个子区域上基于线性层进行预测是一样的。

如果使用具有n个输出神经元的线性层进行正向传播,它生成的输出,和用n个线性层(卷积核)在每个输入位置进行预测的输出是一样的。这样做可以使代码更简单,也更快。

代码示例:

import numpy as np, sys
np.random.seed(1) 

from keras.datasets import mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()

images, labels = (x_train[0:1000].reshape(1000,28*28) / 255,
                  y_train[0:1000])


one_hot_labels = np.zeros((len(labels),10))
for i,l in enumerate(labels):
    one_hot_labels[i][l] = 1
labels = one_hot_labels

test_images = x_test.reshape(len(x_test),28*28) / 255
test_labels = np.zeros((len(y_test),10))
for i,l in enumerate(y_test):
    test_labels[i][l] = 1

def tanh(x):
    return np.tanh(x)

def tanh2deriv(output):
    return 1 - (output ** 2)

def softmax(x): # softmax函数的实现方法
    temp = np.exp(x) # 创建幂函数
    return temp / np.sum(temp, axis=1, keepdims=True) # 使用幂函数除以每行元素的和,axis=1表示按行求和,keepdims=true表示计算结果的维度和数组一致

alpha, iterations = (2, 300) # 学习率和轮数
pixels_per_image, num_labels = (784, 10)  # 图片数量和标签数量
batch_size = 128 # 每次训练的样本数量

input_rows = 28 # 输入图像行数
input_cols = 28 # 输入图像列数

kernel_rows = 3 # 卷积核行数
kernel_cols = 3 # 卷积核列数
num_kernels = 16 # 卷积核数量

hidden_size = ((input_rows - kernel_rows) * 
               (input_cols - kernel_cols)) * num_kernels # 计算隐藏层的大小,由公式可以得出

# weights_0_1 = 0.02*np.random.random((pixels_per_image,hidden_size))-0.01
# 没有使用卷积层时,我们可以直接输入图像作为神经网络的输入
# 使用卷积层后,我们需要将输入图像分成多个区域,并对每个区域进行卷积操作
kernels = 0.02*np.random.random((kernel_rows*kernel_cols,
                                 num_kernels))-0.01 # 生成随机数组范围为-0.01到0.01之间

weights_1_2 = 0.2*np.random.random((hidden_size,
                                    num_labels)) - 0.1 # 生成随机数组范围为-0.1到0.1之间



def get_image_section(layer,row_from, row_to, col_from, col_to): # 图像层 起始行 终止行 起始列 终止列
    section = layer[:,row_from:row_to,col_from:col_to] # 在函数内部,使用切片操作符从图像层中提取了一个特定的图像区域
    return section.reshape(-1,1,row_to-row_from, col_to-col_from) # 返回图像的新形状

for j in range(iterations):
    correct_cnt = 0 # 用于记录正确的次数
    for i in range(int(len(images) / batch_size)): # 遍历每个batch的图像数据
        batch_start, batch_end=((i * batch_size),((i+1)*batch_size)) # 根据当前batch的索引计算出batch的起始和结束位置
        layer_0 = images[batch_start:batch_end] # 将图像起始位置到结束位置的部分赋值给layer_0层
        layer_0 = layer_0.reshape(layer_0.shape[0],28,28) # 将layer_0的形状调整为三维
        layer_0.shape

        sects = list() # 创建一个空列表sects
        for row_start in range(layer_0.shape[1] - kernel_rows): # 起始行:layer_0行数减去卷积核的行数
            for col_start in range(layer_0.shape[2] - kernel_cols): # 起始列:layer_0列数减去卷积核的列数
                sect = get_image_section(layer_0,
                                         row_start,
                                         row_start+kernel_rows,
                                         col_start,
                                         col_start+kernel_cols) # 提取图像的部分
                sects.append(sect) # 添加到部分列表中

        expanded_input = np.concatenate(sects,axis=1) # 将列表中多个部分拼接在一起成为一个矩阵
        es = expanded_input.shape # 获取拓展后输入矩阵的形状
        flattened_input = expanded_input.reshape(es[0]*es[1],-1) # 展平成一个二维矩阵,因为ex是一个包含了矩阵形状元组

        kernel_output = flattened_input.dot(kernels) # 将扩展后的输入矩阵与卷积核进行矩阵乘法
        layer_1 = tanh(kernel_output.reshape(es[0],-1)) # 将卷积层的输出矩阵重新变形为原来的行炸后,通过tanh激活函数进行处理,得到第一层的输出
        dropout_mask = np.random.randint(2,size=layer_1.shape) # 使用dropout正则化
        layer_1 *= dropout_mask * 2
        layer_2 = softmax(np.dot(layer_1,weights_1_2))

        for k in range(batch_size):
            labelset = labels[batch_start+k:batch_start+k+1] # 获取当前样本的标签
            _inc = int(np.argmax(layer_2[k:k+1]) == 
                               np.argmax(labelset)) # 如果预测结果与真实标签匹配,则_inc的值为1,否则为0
            correct_cnt += _inc

        layer_2_delta = (labels[batch_start:batch_end]-layer_2)\
                        / (batch_size * layer_2.shape[0]) # 计算输出层的误差项,通过将标签与实际输出大的差值除以批量大小和输出层大小得到
        layer_1_delta = layer_2_delta.dot(weights_1_2.T) * \
                        tanh2deriv(layer_1) # 计算隐藏层的误差项
        layer_1_delta *= dropout_mask # dropout正则化
        weights_1_2 += alpha * layer_1.T.dot(layer_2_delta) # 更新权重矩阵
        l1d_reshape = layer_1_delta.reshape(kernel_output.shape) # 将隐藏层误差项重新调整为与卷积核输出的形状相同
        k_update = flattened_input.T.dot(l1d_reshape) # 计算卷积核的更新量,通过输入矩阵的转置矩阵×调整后的隐藏层误差项
        kernels -= alpha * k_update # 用学习率乘以卷积核的更新量得到更新后的卷积核
    
    test_correct_cnt = 0

    for i in range(len(test_images)):

        layer_0 = test_images[i:i+1] # 将当前图片赋值给变量 layer_0
#         layer_1 = tanh(np.dot(layer_0,weights_0_1)) # 原来是直接通过tanh激活函数
        layer_0 = layer_0.reshape(layer_0.shape[0],28,28) # 调整形状
        layer_0.shape

        sects = list()
        for row_start in range(layer_0.shape[1]-kernel_rows):
            for col_start in range(layer_0.shape[2] - kernel_cols):
                sect = get_image_section(layer_0,
                                         row_start,
                                         row_start+kernel_rows,
                                         col_start,
                                         col_start+kernel_cols)
                sects.append(sect)

        expanded_input = np.concatenate(sects,axis=1)
        es = expanded_input.shape
        flattened_input = expanded_input.reshape(es[0]*es[1],-1)

        kernel_output = flattened_input.dot(kernels)
        layer_1 = tanh(kernel_output.reshape(es[0],-1))
        layer_2 = np.dot(layer_1,weights_1_2)

        test_correct_cnt += int(np.argmax(layer_2) == 
                                np.argmax(test_labels[i:i+1]))
    if(j % 1 == 0):
        sys.stdout.write("\n"+ \
         "I:" + str(j) + \
         " Test-Acc:"+str(test_correct_cnt/float(len(test_images)))+\
         " Train-Acc:" + str(correct_cnt/float(len(images))))

输出为:

I:0 Test-Acc:0.0288 Train-Acc:0.055
I:1 Test-Acc:0.0273 Train-Acc:0.037
I:2 Test-Acc:0.028 Train-Acc:0.037
I:3 Test-Acc:0.0292 Train-Acc:0.04
I:4 Test-Acc:0.0339 Train-Acc:0.046
I:5 Test-Acc:0.0478 Train-Acc:0.068
I:6 Test-Acc:0.076 Train-Acc:0.083
I:7 Test-Acc:0.1316 Train-Acc:0.096
I:8 Test-Acc:0.2137 Train-Acc:0.127
I:9 Test-Acc:0.2941 Train-Acc:0.148
I:10 Test-Acc:0.3563 Train-Acc:0.181
I:11 Test-Acc:0.4023 Train-Acc:0.209
I:12 Test-Acc:0.4358 Train-Acc:0.238
I:13 Test-Acc:0.4473 Train-Acc:0.286
I:14 Test-Acc:0.4389 Train-Acc:0.274
I:15 Test-Acc:0.3951 Train-Acc:0.257
I:16 Test-Acc:0.2222 Train-Acc:0.243
I:17 Test-Acc:0.0613 Train-Acc:0.112
I:18 Test-Acc:0.0266 Train-Acc:0.035
I:19 Test-Acc:0.0127 Train-Acc:0.026
I:20 Test-Acc:0.0133 Train-Acc:0.022
I:21 Test-Acc:0.0185 Train-Acc:0.038
I:22 Test-Acc:0.0363 Train-Acc:0.038
I:23 Test-Acc:0.0928 Train-Acc:0.067
...
I:295 Test-Acc:0.877 Train-Acc:0.817
I:296 Test-Acc:0.8767 Train-Acc:0.826
I:297 Test-Acc:0.8774 Train-Acc:0.816
I:298 Test-Acc:0.8774 Train-Acc:0.804
I:299 Test-Acc:0.8774 Train-Acc:0.814

如上,用卷积层替换掉网络中的第一层,能够减少几个百分点的误差。卷积层的输出本身也是一系列二维图像——也就是每个卷积核在每个输入位置的输出。

卷积层的大多数用法是多层叠加在一起,这样每个卷积层都将前一层的输出作为输入。


文章作者: QT-7274
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 QT-7274 !
评论
  目录