简述

CNN,全称 convolution neural network,卷积神经网络。

使用范围:

  • 图片或者视频中的物体识别(Object recognition)
  • 自然语言处理
  • 玩游戏,最著名的就是围棋的 阿尔法go
  • 医疗创新,从药物发现到疾病预测

原因1:图像很大
现在用于计算机视觉问题的图像通常是224×224或更大。想象一下,构建一个神经网络来处理224×224彩色图像:包括图像中的3个彩色通道(RGB),得到224×224×3 = 150,528个输入特征!在这样的网络中,一个典型的隐含层可能有1024个节点,因此我们必须为第一层单独训练150,528 x 1024 = 1.5 +亿个权重。我们的网络将是巨大的,几乎不可能训练。

原因2:Position可以改变
如果你训练一个网络来检测狗,你希望它能够检测狗,不管它出现在图像的什么地方。

结构

CNN基本结构:INPUT -> 卷积->激活 -> 池化 -> flatten->全连接 ->softmax->OUTPUT

img

输入层 input layer

卷积层 (convolutional layer)

将输入通过多个卷积核(kernel/filter),生成输出。

卷积核每次只关心和它同样大小的输入,然后矩阵内积后,产生出一个输出。接着,移动卷积核观察的区域,依次产生所有输出。最后,我们就得到一个新的矩阵。

每个卷积核都会生成一个矩阵作为输出。一个卷积层可能有多个卷积核。

我们可以使用一个输入图像和一个滤波器通过将滤波器与输入图像进行卷积来生成一个输出图像。这包括

  • 将过滤器覆盖在图像的某个位置上。
  • 在过滤器中的值与其在图像中的对应值之间执行元素级乘法。
  • 总结所有元素产品。此总和是输出图像中目标像素的输出值。
  • 重复所有位置。
img

填充

先用3×3过滤器对4×4输入图像进行卷积,以产生2×2输出图像吗?通常,我们希望输出图像的大小与输入图像的大小相同。为此,我们在图像周围添加零,以便我们可以在更多位置叠加过滤器。

卷积神经网络(CNN)简介
  • conv层的主要参数是它拥有的过滤器的数量。
import numpy as np

class Conv3x3:
  # A Convolution layer using 3x3 filters.

  def __init__(self, num_filters):
    self.num_filters = num_filters

    # filters is a 3d array with dimensions (num_filters, 3, 3)
    # We divide by 9 to reduce the variance of our initial values
    self.filters = np.random.randn(num_filters, 3, 3) / 9

这段代码实现了一个使用3x3过滤器的卷积层。该卷积层使用随机初始化的权重矩阵作为过滤器,每个过滤器的大小为3x3,总共有num_filters个过滤器。对于初始权重矩阵,使用随机数生成器生成随机值,并将其除以9以减少方差。这个类没有实现前向传播或反向传播的方法,因此只能作为一个构建卷积神经网络的基本模块。

class Conv3x3:
  # ...

  def iterate_regions(self, image):
    '''
    Generates all possible 3x3 image regions using valid padding.
    - image is a 2d numpy array
    '''
    h, w = image.shape

    for i in range(h - 2):
      for j in range(w - 2):
        im_region = image[i:(i + 3), j:(j + 3)]
        yield im_region, i, j

  def forward(self, input):
    '''
    Performs a forward pass of the conv layer using the given input.
    Returns a 3d numpy array with dimensions (h, w, num_filters).
    - input is a 2d numpy array
    '''
    h, w = input.shape
    output = np.zeros((h - 2, w - 2, self.num_filters))

    for im_region, i, j in self.iterate_regions(input):
      output[i, j] = np.sum(im_region * self.filters, axis=(1, 2))

    return output

这段代码实现了一个3x3卷积层(Conv3x3)的前向传播,包括迭代图像区域和卷积操作。

在迭代图像区域的函数(iterate_regions)中,给定输入图像,函数通过一个嵌套的循环遍历每个3x3像素区域,并返回该区域以及其在原始图像中的位置(i, j)。这个函数会在卷积操作中被调用。

在前向传播函数(forward)中,给定输入图像,函数首先获取其维度(h, w),并根据卷积层中滤波器的数量(self.num_filters)创建一个大小为(h - 2, w - 2, self.num_filters)的零数组作为输出。

然后,通过调用 iterate_regions 函数迭代图像中的所有3x3区域。针对每个区域,函数使用卷积层中的滤波器(self.filters)进行卷积运算,并将结果计入输出数组中的对应位置(i, j)。

最终,函数返回输出数组作为卷积层的输出。

import mnist
from conv import Conv3x3

# The mnist package handles the MNIST dataset for us!
# Learn more at https://github.com/datapythonista/mnist
train_images = mnist.train_images()
train_labels = mnist.train_labels()

conv = Conv3x3(8)
output = conv.forward(train_images[0])
print(output.shape) # (26, 26, 8)

导入了mnist和Conv3x3两个模块。

调用mnist.train_images()和mnist.train_labels()分别获取MNIST训练集中的图像和标签数据。

创建了一个Conv3x3的实例对象conv,卷积核数量为8。

调用conv.forward()方法对train_images[0]进行卷积运算,得到输出output。

打印输出output的形状,结果为(26, 26, 8)。其中,26表示输出的特征图尺寸为26x26,8表示共有8个卷积核

几个参数:

1、深度(depth):卷积核个数,也称神经元个数,决定输出的特征图的数量。

2、步长(stride):卷积核滑动一次的大小,决定滑动多少步可以到达边缘。

3、填充值(padding):在外围边缘补充0的层数

激活层

因为神经网络中每一层的输入输出都是一个线性求和的过程,下一层的输出只是承接了上一层输入函数的线性变换,所以如果没有激活函数,那么无论你构造的神经网络多么复杂,有多少层,最后的输出都是输入的线性组合,纯粹的线性组合并不能够解决更为复杂的问题。而引入激活函数之后,我们会发现常见的激活函数都是非线性的,因此也会给神经元引入非线性元素,使得神经网络可以逼近其他的任何非线性函数,这样可以使得神经网络应用到更多非线性模型中。

  • sigmoid

    Sigmoid函数也叫Logistic函数,用于隐层神经元输出,取值范围为(0,1),它可以将一个实数映射到(0,1)的区间,可以用来做二分类。在特征相差比较复杂或是相差不是特别大时效果比较好。sigmoid是一个十分常见的激活函数,函数的表达式如下:

    f(x)=11+exf(x)=\frac{1}{1+e^{-x}}

  • tanh

    Tanh 激活函数又叫作双曲正切激活函数(hyperbolic tangent activation function)。与 Sigmoid 函数类似,Tanh 函数也使用真值,但 Tanh 函数将其压缩至-1 到 1 的区间内。与 Sigmoid 不同,Tanh 函数的输出以零为中心,因为区间在-1 到 1 之间。

    f(x)=tanh(x)=exexex+ex=21+e2x1f(x)=tanh(x)=\frac{e^x-e^{-x}}{e^x+e{-x}}=\frac{2}{1+e^{-2x}}-1

    tanh(x)=2sigmoid(2x)1tanh(x)=2sigmoid(2x)-1

  • relu

    ReLU函数又称为修正线性单元(Rectified Linear Unit),是一种分段线性函数,其弥补了sigmoid函数以及tanh函数的梯度消失问题,在目前的深度神经网络中被广泛使用。

    f(x)= \begin{equation} \left\{ \begin{array}{lr} x & x>=0\\ 0 & x<0 \end{array} \right. \end{equation}

池化层(pooling layer)

conv层输出中包含的大部分信息都是冗余的。它们所做的就是减小输入的大小(您猜对了)。池通常由一个简单的操作完成,比如max、min或average。

  • 最大池化(Max Pooling)。取4个点的最大值。这是最常用的池化方法。
  • 均值池化(Mean Pooling)。取4个点的均值。
img
import numpy as np

class MaxPool2:
  # A Max Pooling layer using a pool size of 2.

  def iterate_regions(self, image):
    '''
    Generates non-overlapping 2x2 image regions to pool over.
    - image is a 2d numpy array
    '''
    h, w, _ = image.shape
    new_h = h // 2
    new_w = w // 2

    for i in range(new_h):
      for j in range(new_w):
        im_region = image[(i * 2):(i * 2 + 2), (j * 2):(j * 2 + 2)]
        yield im_region, i, j
		'''
		这段代码是一个生成器函数,用于将图像分成2x2个小块,并且返回每个小块的图像数据(im_region)
		和在原始图像中的位置坐标(i,j)。每次调用函数时,会返回一个新的小块和它的位置坐标,
		直到所有的小块都被返回完毕。
		'''

  def forward(self, input):
    '''
    Performs a forward pass of the maxpool layer using the given input.
    Returns a 3d numpy array with dimensions (h / 2, w / 2, num_filters).
    - input is a 3d numpy array with dimensions (h, w, num_filters)
    '''
    h, w, num_filters = input.shape
    output = np.zeros((h // 2, w // 2, num_filters))

    for im_region, i, j in self.iterate_regions(input):
      output[i, j] = np.amax(im_region, axis=(0, 1))

    return output

这个MaxPool2类中有两个方法:iterate_regions和forward。iterate_regions方法用于生成非重叠的2x2图像区域,用于接下来的池化操作。forward方法用于执行maxpool层的前向传递,返回一个大小为(h/2, w/2, num_filters)的3D张量,其中h、w是输入张量的高度和宽度,num_filters是输入张量的过滤器数量。

在forward方法中,使用iterate_regions方法遍历输入张量的每个2x2区域,并在这些区域上执行最大池化操作,将结果存储在输出张量中。最大池化操作获取每个区域中最大的像素值作为该区域的输出。最终,输出张量中的每个值都是该区域内的最大像素值。

import mnist
from conv import Conv3x3
from maxpool import MaxPool2

# The mnist package handles the MNIST dataset for us!
# Learn more at https://github.com/datapythonista/mnist
train_images = mnist.train_images()
train_labels = mnist.train_labels()

conv = Conv3x3(8)
pool = MaxPool2()

output = conv.forward(train_images[0])
output = pool.forward(output)
print(output.shape) # (13, 13, 8)

这段代码用于从MNIST数据集中提取一个图像,然后对其进行3x3卷积操作和2x2最大池化操作,并输出结果的形状。

Flatten 层

把一个矩阵拉平,变为一个高维向量,作为全连接层的输入。

全连接层(fully-connected layer)

softmax层

Softmax 是用于多类分类问题的激活函数,在多类分类问题中,超过两个类标签则需要类成员关系。对于长度为 K 的任意实向量,Softmax 可以将其压缩为长度为 K,值在(0,1)范围内,并且向量中元素的总和为 1 的实向量。

Si=eijejS_i=\frac{e^i}{\sum_{j}e^j}

import numpy as np

class Softmax:
  # A standard fully-connected layer with softmax activation.

  def __init__(self, input_len, nodes):
    # We divide by input_len to reduce the variance of our initial values
    self.weights = np.random.randn(input_len, nodes) / input_len
    self.biases = np.zeros(nodes)

  def forward(self, input):
    '''
    Performs a forward pass of the softmax layer using the given input.
    Returns a 1d numpy array containing the respective probability values.
    - input can be any array with any dimensions.
    '''
    input = input.flatten()

    input_len, nodes = self.weights.shape

    totals = np.dot(input, self.weights) + self.biases
    exp = np.exp(totals)
    return exp / np.sum(exp, axis=0)

  • flatten()输入以使其更容易使用,因为我们不再需要它的形状。
  • np.dot()input与self.weights元素相乘,然后对结果求和。
  • np.exp()计算用于Softmax的指数。

交叉熵损失

softmax真正做的是帮助我们量化我们对预测的确信程度,这在训练和评估CNN时非常有用。更具体地说,使用softmax允许我们使用交叉熵损失,它考虑到我们对每个预测的确定程度。下面是我们计算交叉熵损失的方法::

L=ln(Pc)L=-ln(P_c)

其中c是正确的类(在本例中是指正确的数字)
PcP_c是对C的预测概率,ln是自然对数。一如既往,损失越小越好。例如,在最好的情况下,我们有Pc=1,L=ln(1)=0P_c=1,L=-ln(1)=0

在更现实的情况下,我们可能会有Pc=0.8,L=ln(0.8)=0.223P_c=0.8,L=-ln(0.8)=0.223

输出层

import mnist
import numpy as np
from conv import Conv3x3
from maxpool import MaxPool2
from softmax import Softmax

# The mnist package takes care of handling the MNIST dataset for us!
# Learn more at https://github.com/datapythonista/mnist
# We only use the first 1k testing examples (out of 10k total) in the interest of time.
# Feel free to change this if you want.
test_images = mnist.test_images()[:1000]
test_labels = mnist.test_labels()[:1000]

conv = Conv3x3(8)                  # 28x28x1 -> 26x26x8
pool = MaxPool2()                  # 26x26x8 -> 13x13x8
softmax = Softmax(13 * 13 * 8, 10) # 13x13x8 -> 10

def forward(image, label):
  '''
  Completes a forward pass of the CNN and calculates the accuracy and
  cross-entropy loss.
  - image is a 2d numpy array
  - label is a digit
  '''
  # We transform the image from [0, 255] to [-0.5, 0.5] to make it easier
  # to work with. This is standard practice.
  out = conv.forward((image / 255) - 0.5)
  out = pool.forward(out)
  out = softmax.forward(out)

  # Calculate cross-entropy loss and accuracy. np.log() is the natural log.
  loss = -np.log(out[label])
  acc = 1 if np.argmax(out) == label else 0

  return out, loss, acc

print('MNIST CNN initialized!')

loss = 0
num_correct = 0
for i, (im, label) in enumerate(zip(test_images, test_labels)):
  # Do a forward pass.
  _, l, acc = forward(im, label)
  loss += l
  num_correct += acc

  # Print stats every 100 steps.
  if i % 100 == 99:
    print(
      '[Step %d] Past 100 steps: Average Loss %.3f | Accuracy: %d%%' %
      (i + 1, loss / 100, num_correct)
    )
    loss = 0
    num_correct = 0

这段代码实现了一个简单的卷积神经网络,用于对 MNIST 手写数字进行分类。具体来说,它定义了一个包含一个卷积层、一个池化层和一个 Softmax 层的网络。然后,它在测试集的前 1000 个样本上进行了测试,并输出了损失和准确率的统计信息。

该网络的前向传递过程包括以下步骤:

  1. 将输入图像从 [0, 255] 的像素值范围转换为 [-0.5, 0.5] 的范围。
  2. 将输入图像传递给卷积层,并取得输出。
  3. 将卷积层的输出传递给池化层,并取得输出。
  4. 将池化层的输出传递给 Softmax 层,并取得输出。
  5. 计算交叉熵损失和准确率。

该网络的测试结果表明,在测试集的前 1000 个样本上,平均损失为 0.278,准确率为 92%。

这段代码是在测试模型时使用的。首先,对于每个测试样本(test_images和test_labels),我们调用forward函数进行前向传递,并获取模型的输出(_),损失值(l)和正确预测的数量(acc)。然后,我们将损失值累加到loss变量中,并将正确预测的数量累加到num_correct变量中。

每100个测试步骤,我们打印过去100个测试步骤的平均损失和准确率。最后,我们将loss和num_correct重置为0,以进行下一批测试。

pytorch当中的函数和命名

卷积层

torch.nn.Conv2d()
参数说明
in_channels:输入通道数(深度)
out_channels:输出通道数(深度)
kernel_size:滤波器(卷积核)大小
stride:表示滤波器滑动的步长
padding:是否进行零填充
bias:默认为 True,表示使用偏置
groups:控制分组卷积,默认不分组,为1组。
dilation:卷积对输入的空间间隔,默认为 True

激活层(一般用ReLU)

torch.nn.ReLU()
参数说明
inplace:是否在原数据进行操作,默认是False

池化层

torch.nn.MaxPool2d()
torch.nn.AvgPool2d()
参数说明
kernel_size :表示做最大池化的窗口大小
stride:步长
padding:是否进行零填充
dilation:卷积对输入的空间间隔,默认为 True

全连接层

torch.nn.Linear()
参数说明
in_features :输入特征数;
out_features:输出特征数;
bias:默认为 True,表示使用偏置

前馈过程

图片预处理

图片先是从各种格式,JPG,PNG,BMP转换成为一个张量(Tensor)。张量的维度一般是(图片高,图片宽,层数),层数一般为(R,G,B)三层。

卷积运算

padding是填充的空白,stride是步长。

激活函数

Relu对正数值维持一个线性的操作,负数值会被置零,即不会被更新。

池化层池化

减少参数量,以减少下一层的计算量

flatten层

使用Flatten将它们reshape成一维数据,以便作为后面全链接层的输入。

全连接层

一般是普通的全链接神经网络,输入为Flatten之后的一维的图片特征,输出层一般是Sigmoid层。

softmax层

将全链接层的输出,进行softmax运算,得到每个类的概率。

f(x)=exiexf(x)=\frac{e^x}{\sum_{i}e^x}

由这个等式可以看出,在二分类的时候,softmax函数其实就是sigmoid。

import numpy as np

z = [2.0, 1.0, 0.1]
print(np.exp(z))  # [7.3890561  2.71828183 1.10517092]
softmax = np.exp(z)/np.sum(np.exp(z))
print(softmax)    # [0.65900114 0.24243297 0.09856589]

计算loss值

交叉熵损失函数的计算公式为:J=i=1nyilog(yi^)J=-\sum_{i=1}^{n}y_i\log(\hat{y_i}),其中 nn 为类别数,yiy_i 为第 ii 个类别的真实标签(0 或 1),yi^\hat{y_i} 为第 ii 个类别的预测概率。

import numpy as np

Y = np.array([1, 0, 0]) # 真实类别为第一类

Y_pred1 = np.array([0.7, 0.2, 0.1]) # 预测1
Y_pred2 = np.array([0.1, 0.3, 0.6]) # 预测2

print('预测1 loss = ', np.sum(-Y * np.log(Y_pred1)))  # 预测1 loss = 0.35
print('预测2 loss = ', np.sum(-Y * np.log(Y_pred2)))  # 预测2 loss = 2.30

对于本题的情况,真实类别为第一类(即 y1=1y_1=1y2=0y_2=0y3=0y_3=0),预测1的结果为 0.70.70.20.20.10.1,预测2的结果为 0.10.10.30.30.60.6

代入公式计算得到:

J1=[(1×log(0.7))+(0×log(0.2))+(0×log(0.1))]=log(0.7)0.3567J_1 = -[(1\times\log(0.7))+(0\times\log(0.2))+(0\times\log(0.1))] = -\log(0.7) \approx 0.3567

J2=[(1×log(0.1))+(0×log(0.3))+(0×log(0.6))]=log(0.1)2.3026J_2 = -[(1\times\log(0.1))+(0\times\log(0.3))+(0\times\log(0.6))] = -\log(0.1) \approx 2.3026

保留小数点后两位,预测1的损失为0.36,预测2的损失为2.30。

后馈过程

优化网络,改变Weights,让Loss变小

改变神经网络中的权重weights,来优化我们的Loss值,使它变小。 那么怎样优化呢 ?
让我们把整个卷积神经网络,从头到尾整个前馈过程,看成一个数学公式。变量为权重Weights,结果为Loss值。我们想要得到最小的Loss值,我们可以对这个数学公式进行求导,可是这个优化问题是个np hard问题,想简单的来个导数为零…这个等式解不出来,所以我们采用了梯度下降法。
简单的讲,对每一个变量(权重Weights)求导,然后我们让这个权重按照这个值的相反方向运动,以达到减小Loss的效果。