2

从零实现一个简单卷积神经网络

 2 years ago
source link: https://blog.csdn.net/Lucky_Q/article/details/122808493
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

从零实现一个简单卷积神经网络

IsQtion 于 2022-02-07 19:00:56 发布 678

对于卷积公式
在这里插入图片描述
可能有的人知道,可能有的人不知道,或者也仅仅只是知道而不理解。但是不管你知不知道这个公式的意义,都不影响你自己去实现一个卷积。他具体的数学意义,我先不讲,因为有很多人讲的都比我清楚透彻。而我要告诉你的,则是再卷积神经网络里面的卷积操作是如何实现的

提到卷积神经网络,听到的最多的应该就是卷积,激活,池化这三个操作。就拿VGG16这个经典网络模型来说,其实就是通过卷积+激活+池化这三种操作堆叠而成的。

那么他们具体是什么东西,又是如何实现的,这次就来用numpy手撸一下他们的具体实现原理。

在这里插入图片描述

在神经网络中的卷积,就是利用一个卷积核在你的图像矩阵上滑动,上图中,假设灰色矩阵就是我们输入的图像矩阵,里面的数字就代表着图像中的像素值,绿色矩阵就是卷积核,灰色矩阵上面的绿色区域就是当前卷积核所覆盖的区域每次卷积核滑动的距离叫做步长,在上图中步长就是1。具体操作就是,将卷积核与在图像矩阵中所覆盖的区域对应位置相乘再相加,最后的蓝色矩阵就是这次卷积的结果。

Padding

在上面的卷积操作中,原矩阵的大小是4x4,但是卷积完了之后就变成2x2的矩阵了,这是因为边缘上的像素永远不会位于卷积核中心,而卷积核也没法扩展到边缘区域以外
这个结果我们是不能接受的,有时我们还希望输入和输出的大小应该保持一致。为解决这个问题,可以在进行卷积操作前,对原矩阵进行边界填充(Padding),也就是在矩阵的边界上填充一些值,以增加矩阵的大小,通常都用“0”来进行填充的。
在这里插入图片描述
对于卷积核大小为3的卷积,通常只需要在外围填充一圈0就足够了,对于不同大小的卷积核,为了使输出图像的大小一致,在图像外围填充多少圈0是不一样的,填充公式如下:
在这里插入图片描述
P代表因该填充几圈,K代表卷积核的边长,卷积核通常都是奇数

多通道卷积

我们都知道,一张彩色图片是RGB三通道,也就是它的channel有3个,那么在这种多通道的情况下应该如何实现卷积操作呢。很简单,输入的图像有多少个通道,那么我们的卷积核也有多少个通道就可以了
在这里插入图片描述
跟单通道的卷积操作一样,把卷积核按照对应通道放在图片上滑动,对应位置相乘再相加,最后把三个通道得到的卷积结果加起来就行了

#input:输入的数据,input_channel:输入数据的通道数,out_channel:输出的特征图的通道数,kernel_size:卷积核的大小,stride:步长
def convolution(input,input_channel,out_channel,kernel_size,stride):
    kernel = np.random.randn(out_channel,input_channel,kernel_size,kernel_size)     #创建卷积核
    padding = int((kernel_size - 1) / 2)        #计算填充的大小
    padding_input = []
    # 进行对输入矩阵的填充
    for i in range(input_channel):
        padding_input.append(np.pad(input[i], ((padding, padding), (padding, padding)), 'constant', constant_values=(0, 0)))
    padding_input = np.array(padding_input)
    #根据填充后的输入尺寸,卷积核大小,步长,计算输出矩阵的大小
    out_size = int((len(input[0])+2*padding-kernel_size)/stride+1)
    # 创建一个0填充的输出矩阵
    out = np.zeros((out_channel,out_size,out_size))

    for i in range(out_channel):
        out_x = 0
        out_y = 0
        x_end = padding_input.shape[1] - padding - 1  # 卷积边界

        x = padding
        y = padding
        while x<=x_end:
            if y>padding_input.shape[1]-padding-1:      #卷积核超出右侧边界时,向下移动一个步长
                y = padding
                x = x+stride
                out_y = 0
                out_x = out_x + 1
                if x>x_end:
                    break
            #卷积操作
            out[i][out_x][out_y] = np.sum(padding_input[:,x-padding:x+padding+1,y-padding:y+padding+1]*kernel[i])

            y = y+stride
            out_y += 1

    return out

就是对矩阵中的每个值都进行激活函数的运算,拿Relu举例,Relu就是大于0的不动,小于0的让它变为0:
在这里插入图片描述

def ReLu(input):
    out = np.maximum(0,input)
    return out

池化操作跟卷积操作其实很类似,都需要一个核在图片上进行滑动,对核覆盖的区域进行一些操作,只不过区别就在于,卷积的核里面有数字,卷积操作就是覆盖区域跟核做运算。而池化的核是空的。最常见的池化操作有最大池化,平均池化等。

就是选出被核覆盖的区域中的最大值
在这里插入图片描述

就是选出被核覆盖区域中的平均值
在这里插入图片描述

以最大池化为例

#input:输入的数据,pooling_size:卷积核大小,stride:步长
def pooling(input,pooling_size,stride):
    out_size = int((len(input[0])-pooling_size)/stride+1)   #计算池化后的输出矩阵的大小
    out = np.zeros((len(input[0]),out_size,out_size))       #初始化输出矩阵
    # 对每个通道开始池化
    for i in range(input.shape[0]):
        out_x = 0
        out_y = 0
        in_x = 0
        in_y = 0
        #开始滑动
        while True:
            if out_y>=out_size:
                in_y = 0
                in_x+=pooling_size
                out_x+=1
                out_y = 0
                if out_x==out_size:
                    break
            #池化操作
            out[i][out_x][out_y] = np.max(input[i,in_x:in_x+pooling_size,in_y:in_y+pooling_size])
            in_y+=pooling_size
            out_y+=1
    return out

下面这是一次卷积操作输出的三个通道
在这里插入图片描述


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK