文章目录

  • 第4章 卷积神经网络的结构
    • 4.1 概述
      • 4.1.1 局部连接
      • 4.1.2 参数共享
      • 4.1.3 3D特征图
    • 4.2 卷积层
      • 4.2.1 卷积运算及代码实现
      • 4.2.2 卷积层及代码初级实现
      • 4.2.3 卷积层参数总结
      • 4.2.4 用连接的观点看卷积层
      • 4.2.5 使用矩阵乘法实现卷积层运算
      • 4.2.6 批量数据的卷积层矩阵乘法的代码实现
    • 4.3 池化层
      • 4.3.1 概述
      • 4.3.2 池化层代码实现
    • 4.4 全连接层
      • 4.4.1 全连接层转化成卷积层
      • 4.4.2 全连接层代码实现
    • 4.5 卷积网络的结构
      • 4.5.1 层的组合模式
      • 4.5.2 表示学习
    • 4.6 卷积网络的神经科学基础

第4章 卷积神经网络的结构

上一章介绍的神经网络采用分层结构,层与层之间的神经元进行全连接,本章将其称为常规神经网络。同样,卷积神经网络也是多层网络结构,层与层之间的神经元进行连接,层内神经元之间无连接。对神经元的操作也是输入和权重向量进行内积后进行非线性激活。网络最后会输出分值向量,定义损失函数,并采用梯度下降法进行优化。那么,卷积神经网络和常规神经网络有什么不同呢?

4.1 概述

发展卷积神经网络的初衷是进行图像分类。图像主要有如下3 个特性。

  • 多层次结构:如边缘组成眼睛,眼睛和鼻子等组成脸,脸和身体等组成人。
  • 特征局部性:如眼睛就局限在一个小区域,提取眼睛特征时,只需根据这个小区域的像素提取即可。
  • 平移不变性:如不管眼睛在图像哪个位置,特征提取器都需提取眼睛特征。

根据图像的3 个特性,卷积神经网络引人特有的先验知识一 深度网络局部连接参数共享

虽然卷积网络是为图像分类而发展起来的,但现在已经被用在各种任务中,如语音识别和机器翻译等。只要信号满足多层次结构、特征局部性和平移不变性3 个特性,都可以使用卷积网络。在本章中,我们只针对图像分类任务来讲解。

4.1.1 局部连接

在常规神经网络中,每个神经元都与前一层中的所有神经元连接(全连接)。

这个很没有必要,全连接方式下,神经元权重十分多,大量的参数会导致网络过拟合,而存储大量权重还需要超大内存,这一点也会限制其应用。

根据特征局部性,如果某个神经元需要提取眼睛特征,则只需针对眼睛所在的局部区域内的像素进行特征提取,不需要提取眼睛区域外的信息,所以该神经元只需与眼睛区域进行局部连接,其他区域是不需要连接的。

4.1.2 参数共享

在图像分类中,同一物体可能会在图像的不同位置出现,例如人脸会出现在图像的任意位置,神经元必须对人脸的位置不敏感。而识别不同位置人脸的不同神经元,采用的权重应该是相同的。因为神经元学习是先通过权重和像素进行内积,再进行非线性激活实现的(同一人脸的像素相同)。这些神经元共享相同的参数,这就是参数共享

如上所述仅仅是平移不变性,如果对人脸进行了旋转,特别是旋转角度比较大时,此时检测人脸所用的参数和检测未旋转的人脸一般是不同的。
这和人类相似,一张照片被旋转了很大角度,人可能就认不出了。所以,人类视觉系统具有良好的平移不变性,但旋转不变性要差很多。

4.1.3 3D特征图

图像是二维结构,为了识别人脸, 需要大量的神经元协同工作,这些神经元都与该人脸连接。要提取人脸不同的特征,必须有大量的神经元,它们从观察输入的同一局部区域提取不同特征。这样神经元的组织方式必然是三维:高度、宽度和深度。高度和宽度决定神经元的空间尺寸,深度决定了对输入区域提取特征的维度(每个神经元提取一个特征)。
例如,将CIFAR- 10 中的图像作为输入,该输入的维度是 32 × 32 × 3, 32 × 32 是空间尺寸, 3 是深度,表示同一位置有3 个特征(即红、绿和蓝这3 种颜色特征);
再如,卷积网络中某一层是7× 7 × 128 , 7 × 7 是空间尺寸, 128 表示同一位置有128 个特征。
我们把神经元的3D 排列称为3D 特征图。3D 特征图表示为 [H×W×D][H \times W \times D][H×W×D] , 其中 HHH 是高度, WWW 是宽度, DDD 是深度,宽度和高度称为空间维度。3D 特征图可以看作 DDD 个2D 数据。每个2D 数据的尺寸均是 [H×W][H \times W][H×W],称为特征图, 3D 特征图总共有D 个特征图。

常规神经网络的向量可以看作3D 特征图的特例( 1×1×D1 \times 1 \times D1×1×D ),即空间尺寸为 1 的 3D 特征图。卷积神经网络和常规神经网络一样,它们由层组成,每层使用可微函数将输入的 3D 特征图(向量)变换为输出3D 特征图。

在卷积神经网络中,主要包含3 种基本模块: 卷积层、池化层和全连接层,下面将分别介绍它们。

4.2 卷积层

卷积网络采用卷积层来实现上述的局部连接和参数共享,所以卷积层是卷积网络的核心,而卷积层的核心是卷积运算。请读者思考为什么卷积运算能实现局部连接和参数共享。

4.2.1 卷积运算及代码实现

卷积网络的卷积运算和信号处理中的卷积运算不太一样,把它理解为向量内积更合适(神经元的工作机制)。在图像处理中,边缘检测算法就是利用卷积运算实现的。卷积运算是线性滤波,对于图像中的每个像素,计算以该像素为中心的局部窗口内的像素和卷积核的内积,并将其作为该像素的新值。遍历图像中的每个像素,进行上述内积操作,就完成了一次滤波,得到一个和原图像尺寸一样的“新图像” 。局部窗口和卷积核的大小一样,卷积核是一个小矩阵( 3 x 3 或 5 × 5 ), 卷积运算公式为:

其中 (i,j)(i,j)(i,j) 是中心像素的坐标,i=1,2,…,h,j=1,2,…,wi= 1, 2,…, h, \ j=1,2 , …,wi=1,2,…,h, j=1,2,…,w,这里 hhh 是图像高度, www 是图像宽度。卷积需遍历整个图像。fff 是原图像(注意它是二维矩阵), ggg 是“新图像”, hhh 是卷积核(这里卷积核的大小是 3 × 3 ), ∗*∗ 是卷积运算符。可以看出,卷积就是滤波,所以卷积核也被称为滤波器。

当对图像边界进行卷积时,卷积核的一部分位于图像外面,无像素与之相乘, 此时有两种策略:一种是舍弃图像边缘,这样会使“新图像”尺寸减小(对于3 × 3 卷积核,每边会减小1 );另一种是采用像素填充技巧,人为指定位于图像外面的像素值,使卷积核能与之相乘。像素填充主要有两种方式: 0 填充复制边缘像素。在卷积神经网络中,普遍采用0填充方式。

在卷积运算中,卷积核的取值是核心,取值不同时,“新图像” 的效果差别很大。通过卷积运算可以获得原图像的边缘、模糊图像和锐化图像等。这些著名的卷积核如下所示。

图4.2 展示了图像边缘检测的效果,为什么Sobel 等卷积核能检测出图像的边缘呢?这是因为在图像的边缘区域,像素值的变化剧烈,而在平滑区域,像素值基本一致。计算局部窗口的像素差能区分边缘和平滑区域,像素差大的为边缘,像素差接近0 的为平滑区域。边缘检测的卷积核计算了窗口内像素差,其他两种卷积核请读者自行分析。

下面通过代码来更清楚地了解卷积运算的细节:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import numpy as nph = 32 # 输入数据的高度
w = 48 # 输入数据的宽度
input_2Ddata = np.random.randn(h, w)
output_2Ddata = np.zeros(shape=(h, w)) # 卷积输出尺寸与输入一样kern = np.random.randn(3, 3) # 3×3卷积核
# kern = np.array([ [-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float64) # sobel卷积核padding = np.zeros(shape=(h+2, w+2)) # 0填充
padding[1:-1, 1:-1] = input_2Ddata
for i in range(h):for j in range(w):window = padding[i:i+3, j:j+3] # 中心像素(i,j)的局部窗口output_2Ddata[i, j] = np.sum(kern*window) # 卷积运算即内积

4.2.2 卷积层及代码初级实现

上面介绍的卷积运算的输入和输出数据都是2D 特征图,而卷积网络都是3D 特征图, 如何对卷积运算进行升级,使之能处理3D 特征图呢?

重复一遍:3D 特征图表示为 [H×W×D][H \times W \times D][H×W×D] , 其中 HHH 是高度, WWW 是宽度, DDD 是深度,宽度和高度称为空间维度。3D 特征图可以看作 DDD 个2D 数据。每个2D 数据的尺寸均是 [H×W][H \times W][H×W],称为特征图3D 特征图总共有 D 个特征图

升级做法如下:每个特征图都分别与
一个卷积核进行卷积运算,这样就得到D 个特征图,这 D 个特征图先进行矩阵相加,得到一个特征图,再给该特征图的每个元素再加一个相同的偏置,最终得到一个新的特征图。因为最终需要得到 3D 特征图,所以上述过程需进行多次,这就是一个完整的卷积层操作。从上面的过程可以看出,为了获得每个输出特征图,需要 D 个卷积核,我们把这D 个卷积核称为一个卷积核组,它是一个3D 矩阵。为了获得D 个特征图,则需 D 个卷积核组

图4.3 演示了卷积层操作示意图。其中,输入特征图是 3 × 3 × 3 ,无 0 填充。卷积核组的尺寸为 2 × 2 × 3 ,则输出特征图的尺寸为 2 × 2 × 2 。每个卷积核组有3 个卷积核(与输入特征图数量一致)有 2 组卷积核,故输出 2 个特征图。每个输入特征图与对应的卷积核进行卷积运算,所得值相加,得到输出特征图的元素值。比如,第一个输出特征图的第一个元素 0.2 ,它是第一组卷积核与输入特征图的第一个局部窗口2 × 2 × 2 进行内积的结果。

程序能代替千言万语,直观地展示卷积层运算过程的细节。下面的代码是完全按照上述描述编写的。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import numpy as npdef conv2D(input_2Ddata, kern, in_size, out_size, kern_size=3, stride=1):(h1, w1) = in_size # 输入数据尺寸(h2, w2) = out_size # 输出数据尺寸output_2Ddata = np.zeros(shape=out_size)for i2,i1 in zip(range(h2), range(0, h1, stride)): # 输入数据进行步长for j2,j1 in zip(range(w2), range(0, w1, stride)):window = input_2Ddata[i1:i1+kern_size, j1:j1+kern_size] # 局部窗口output_2Ddata[i2, j2] = np.sum(kern*window) # 内积return output_2Ddata
###################################################################h1 = 32 # 输入数据高度
w1 = 48 # 输入数据宽度
d1 = 12 # 输入数据深度
input_3Ddata = np.random.randn(h1, w1, d1) # 超参数
S = 2 # 步长
F = 3 # 卷积核尺寸
d2 = 24 # 输出数据深度
####P = (F-1)//2 # 填充尺寸
h2 = (h1-F+2*P)//S + 1 # 输出数据高度
w2 = (w1-F+2*P)//S + 1 # 输出数据宽度padding = np.zeros(shape=(h1+2*P, w1+2*P, d1)) # 0填充
padding[P:-P, P:-P, :] = input_3Ddataoutput_3Ddata = np.zeros(shape=(h2, w2, d2))kerns = np.random.randn(d2, F, F, d1) # 4D卷积核
bias = np.random.randn(d2) # 1D偏置for m in range(d2): # 每个输出2D数据for k in range(d1): # 每个输入2D数据input_2Ddata = padding[:,:, k] # 第k个输入2D数据kern = kerns[m, :,:, k] # 卷积核output_3Ddata[:,:, m] += conv2D(input_2Ddata, kern, in_size=(h1, w1), out_size=(h2, w2), kern_size=F, stride=S) # 加上每个卷积结果output_3Ddata[:,:, m] += bias[m] # 每个输出2D数据只有一个偏置

首先定义conv2D 函数实现常规卷积运算,注意卷积核kern 的尺寸为奇数,一般是正方形。注意padding 的实现细节,即0 填充的数量。

然后定义输入和输出的3D 特征图。注意卷积运算没有改变特征图的空间尺寸,但深度维度可能会增加。本例中,输入深度 ind=12in_d = 12ind​=12 维,输出深度 outd=24out_d = 24outd​=24 维。每个输出特征图需要累加输入3D 特征图的每个2D 特征图的卷积结果,最后加一个偏置。注意卷积核是四维矩阵,共有 outd=24out_d = 24outd​=24 个卷积核组,每个卷积核组的尺寸是 [kernh×kernw×ind][kern_h \times kern_w \times in_d][kernh​×kernw​×ind​],每次和输入2D 特征图进行卷积运算的二维卷积核的取值都不相同。四维卷积核和一维偏置,就是卷积层需要学习的参数

上面的程序有利于理解卷积层的运算,但实际的运行效率非常低,后面会实现一个高效版本的程序。

总结一下,卷积层运算需要的参数量如下。

  • 卷积核四维矩阵:[outd×kernh×kernw×ind][out_d \times kern_h \times kern_w \times in_d][outd​×kernh​×kernw​×ind​]
  • 偏置向量:outdout_doutd​

其中参数数量与输入和输出3D 特征图的深度成正比,与卷积核的面积成正比。需要特别注意的是,它与特征图的空间尺寸无关。

进行上面的卷积层运算后,输出特征图的空间尺寸和输入特征图一致。这是卷积网络中最常用的卷积类型。但有时需要缩小输入特征图的空间尺寸, 一般缩小为原来的四分之一,这样就不需要对输入特征图的每个元素都进行卷积运算,而是进行下采样,下采样的间隔称为步长S。上面的代码中,步长 S=1S = 1S=1,故程序不需指明步长。包含步长 SSS 的程序如下:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Thu Aug 16 14:17:42 2018@author: apple
"""import numpy as npdef conv2D(input_2Ddata, kern):(h, w) = input_2Ddata.shape # 输入数据的高度和宽度(kern_h, kern_w) = kern.shape # 卷积核的高度和宽度padding_h = (kern_h-1)//2padding_w = (kern_w-1)//2padding = np.zeros(shape=(h+2*padding_h, w+2*padding_w)) # 0填充padding[padding_h:-padding_h, padding_w:-padding_w] = input_2Ddata    output_2Ddata = np.zeros(shape=(h, w)) # 输出数据的尺寸和输入数据一样for i in range(h):for j in range(w):window = padding[i:i+kern_h, j:j+kern_w] # 局部窗口output_2Ddata[i,j] = np.sum(kern*window) # 内积return output_2Ddata
###################################################################h = 32 # 输入数据的高度
w = 48 # 输入数据的宽度
in_d = 12 # 输入数据的深度
out_d = 24 # 输出数据的深度
input_3Ddata = np.random.randn(h, w, in_d)
output_3Ddata = np.zeros(shape=(h, w, out_d))(kern_h, kern_w) = (3, 3) # 或者(5, 5)
kerns = np.random.randn(out_d, kern_h, kern_w, in_d) # 4D卷积核
bias = np.random.randn(out_d) # 1D偏置for m in range(out_d): # 每一个输出2D数据for k in range(in_d): # 每一个输入2D数据input_2Ddata = input_3Ddata[:,:, k] # 第k个输入2D数据kern = kerns[m, :,:, k]output_3Ddata[:,:, m] += conv2D(input_2Ddata, kern) # 加上每个卷积结果output_3Ddata[:,:, m] += bias[m] # 每个输出2D数据只有一个偏置


算法的超参数是卷积核尺寸 FFF,步长 SSS 和输出特征图的深度 d2d_2d2​ 。FFF 不需太大,为 333 最为常见,因为卷积核参数数量与 FFF 的平方成正比,FFF 太大会导致参数数量急剧增加,运算量也急剧增加。SSS 最常用 111 ,偶尔用 222 ,虽然增大 SSS 能减小运算量,但输出特征图的空间尺寸会急剧减小,这会丢失很多信息,导致学习效果降低,因此在实践中, SSS 不会超过 222。深度 d2d_2d2​ 和常规神经网络的隐含层的宽度超参数类似,增大 d2d_2d2​ ,学习容量增大,但运算量也增加。确定最优 d2d_2d2​ 没有理论方法,实践中采用试错法输出特征图的深度一般大于等于输入特征图的深度,因为输出特征图的空间尺寸可能会变小,这样单个神经元观察到的局部区域会变大,所以需要提取更多的特征。

4.2.3 卷积层参数总结

4.2.4 用连接的观点看卷积层

4.1 节指出局部连接和参数共享是卷积网络的核心概念。这显著区别于常规神经网络,因为常规神经网络采用全连接并且参数各不相同。现在看看卷积运算如何实现局部连接和参数共享。

输入和输出3D 特征图中的每个元素称为神经元。根据上节内容,仔细分析输出神经元的激活值是怎么计算出来的?你会发现:输出神经元只观察输入神经元中的一小部分,即空间尺度上只观察卷积核内的神经元,这就是局部连接,卷积核的空间大小也叫感受野。同一特征图的所有神经元使用相同的卷积核扫描输入3D 特征图, 即参数共享

  1. 局部连接

  空间维度(高度和宽度)与深度维度的连接方式是不同的:前者是局部的,后者是全连接。深度上为什么是全连接?这是因为深度方向的神经元可以看作在空间位置处提取的特征,往往需要利用所有特征进行信息加工。这种全连接方式是目前的主流做法。

  CIFAR-10 图像的输入特征图的尺寸为 32 × 32 × 3 ,如果卷积核尺寸是 5 × 5 ,那么卷积层中每个神经元连接输入特征图中5 × 5 × 3 的局部区域,共 5 × 5 x 3 = 75 个权重。

  输入特征图的尺寸是14 × 14 × 128 ,如果卷积核尺寸是 1 × 1 ,那么卷积层中每个神经元和输入特征图有 1 × 1 x128 = 128 个连接。

  输入特征图的尺寸是 7 × 7 × 256 ,卷积核尺寸是 7 × 7 ,那么卷积层中每个神经元和输入特征图有 7 × 7 × 256 = 12 544 个连接。注意,此时卷积核尺寸和输入特征图空间尺寸一致,所以此局部连接就是全连接



卷积网络这种局部连接方式,从特征提取的角度来看,就是输出神经元只根据局部窗口内的数据进行特征提取,同时利用深度维度上所有的数据,与窗口外的数据无关。需强调的是,输出3D 特征图中同一空间位置处的深度维度上的所有神经元,所观察的局部数据是一样的,所以采用的卷积核必须不同,否则提取的将是相同的特征。这也可以看作局部窗口的数据通过卷积运算和非线性激活提取了多个特征。所以3D 特征图 [H×W×D][H × W × D][H×W×D] 可以看作每个空间位置的元素有D 个特征。比如彩色图像每个空间位置像素有R、G 和B 这3 个特征。这种观点是后面高效程序实现的基础。

  1. 参数共享


4.2.5 使用矩阵乘法实现卷积层运算

卷积层的基本运算是卷积核组输入特征图的局部区域做内积,即把卷积核组和输入特征图的局部区域均拉伸为向量,然后对这两个向量做内积运算。矩阵乘法也是两个向量做内积,因此,如果把输入特征图和所有卷积核组分别转化为矩阵,则卷积层的运算就变成两个巨大矩阵的乘法

(1) 将输入 3D 特征图转化为矩阵 XXX:

(2)卷积核组转化为矩阵 WWW:

(3)点乘

(4)reshape

这个方法的缺点是占用内存较多,因为输入特征图中的元素在X 中会被复制很多次,优点是矩阵乘法实现起来非常高效(常用的BLAS API )。

4.2.6 批量数据的卷积层矩阵乘法的代码实现

为了高效利用矩阵乘法, 一般会批量进行图像的卷积运算。令批量为B ,输入数据就是4D 数据 [B×H×W×D][B × H× W × D][B×H×W×D] 。每个3D 特征图都转变为二维大矩阵,然后按行堆叠在一起,形成一个超级大矩阵。本例中,如果有 B=10B = 10B=10 个输入特征图,则超级大矩阵 XXX 的尺寸是 30×250×36330 × 250 × 36330×250×363 。第(2)步和第(3 ) 步不变,第(4)步要变为4D 输出数据 10×55×55×9610 × 55 × 55 × 9610×55×55×96 。

算法的核心是实现4 .2.5 节所述方法的第(1)步,把局部窗口数据拉伸为行向量,然后遍历每个特征图的每一个局部窗口,使这些行向量按行堆叠在一起即可,代码如下:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import numpy as npfilter_size = 3
filter_size2 = filter_size*filter_size
stride = 1
padding = (filter_size - 1)//2 # 1(batch, in_height, in_width, in_depth) = (8, 32, 48, 16)
in_data = np.random.randn(batch, in_height, in_width, in_depth) # 2out_height = (in_height - filter_size + 2*padding)//stride + 1 # 3
out_width = (in_width - filter_size + 2*padding)//stride + 1 # 4
out_size = out_height*out_widthmatric_data = np.zeros( (out_size*batch, filter_size2*in_depth) ) # 5padding_data = np.zeros((batch, in_height + 2*padding, in_width + 2*padding, in_depth) ) # 6
padding_data[:, padding : -padding, padding : -padding, :] = in_dataheight_ef = padding_data.shape[1] - filter_size + 1 # 7
width_ef = padding_data.shape[2] - filter_size + 1 # 8for i_batch in range(batch): # 9i_batch_size = i_batch*out_size # 10for i_h, i_height in zip(range(out_height), range(0, height_ef, stride)): # 11i_height_size = i_batch_size + i_h*out_width # 12for i_w, i_width in zip(range(out_width), range(0, width_ef, stride)): # 13matric_data[i_height_size + i_w, :] = padding_data[i_batch,i_height : i_height + filter_size,i_width  : i_width  + filter_size,:].ravel() # 14
  • 先设置算法超参数: 卷积核尺寸 filter_sizefilter\_sizefilter_size 和步长 stridestridestride
  • paddingpaddingpadding 计算0 填充
  • in_datain\_datain_data 随机生成4D 的输入特征图
  • out_heightout \_ heightout_height 和 out_widthout\_widthout_width 计算输出特征图的高度和宽度
  • matric_datamatric\_datamatric_data 是分配输出大矩阵的存储空间,注意行数量为 out_size∗batchout \_size *ba tchout_size∗batch
  • 接下来 进行 0 填充
  • height_efheight\_efheight_ef 和 width_efwidth\_efwidth_ef 是 计算卷积运算以步长 stridestr idestride 滑动时,在输入数据体上最大能滑动到的位置
  • 接下来:遍历每个输入3D 特征图,计算第 i_batchi\_batchi_batch 个3D 特征图的首个局部窗口数据的行位置;
    遍历每一行,计算第 i_hi\_hi_h 行首个局部窗口数据的行位置;
    遍历每一列,获取局部窗口数据,并使用 ravelravelravel 方法将其拉伸为 1D 向量,赋值给对应的行。

第(2 )步,卷积核组拉伸为列向量。实际上并不需要事先生成四维的卷积核,直接生成二维卷积核矩阵即可,代码如下:

out_depth = 32
weights = 0.01 * np.random.randn(filter_size2*in_depth, out_depth)
bias = np.zeros((1, out_depth))

这里设置超参数 out_depthout\_depthout_depth ,生成小的随机数初始化权重矩阵。注意矩阵有 out_depthout\_depthout_depth 列。偏置初始化为 000 。

第( 3 )步,矩阵相乘和ReLU 非线性激活,代码如下:

filter_data = np.dot(matric_data, weights) + bias
filter_data = np.maximum(0, filter_data)

第(4)步, 把 filter_datafilter \_datafilter_data 的每一行数据转变为输出4D 特征图对应位置的深度维度的数据。代码如下:

# note, ReLU activation
out_data = np.zeros((batch, out_height, out_width, out_depth)) # 1for i_batch in range(batch): # 2i_batch_size = i_batch*out_size # 3for i_height in range(out_height): # 4i_height_size = i_batch_size + i_height*out_width # 5for i_width in range(out_width): # 6out_data[i_batch, i_height, i_width, :] = filter_data[i_height_size + i_width, :] # 7


需要特别强调的是,第( 3 )步运算和常规的神经网络是一致的,先进行矩阵相乘,然后进行非线性激活,只是由于输入和输出是3 D 特征图,需要对特征图进行形状的转换。

本书后面解释CNN 结构时,把非线性激活层ReLU 包含在卷积层里面,不单独作为一层。整个程序结构清晰,逻辑简单,可读性很强,程序运行效率高,希望读者仔细研读。我建议读者先理解NumPy 中 array 多维数组的存储模式。array 中元素存储模式是先存储最后维度的数据,然后依次存储前一维度的数据,最后存储第一个维度数据。对于读者熟悉的2D 数据data=np.random.randn(h,w)data=np.random.randn( h , w )data=np.random.randn(h,w) ,先存储第二维度的数据,也就是先存储行数据(每行数据有 www 个元素),一行一行地依次存储。对于本程序中的4D 特征图,读者很少用,但其存储模式和2D 类似, data=np.random.randn(b,h,w,d)data =np . random.randn(b, h, w , d )data=np.random.randn(b,h,w,d) 是以第四维度的数据(有 ddd 个元素)为存储单位的,依次存储这 ddd 个元素。如果这 ddd 个元素存储了 www 次,就相当于存储完第三维数据。如果这 ddd 个元素存储了 w×hw × hw×h 次,就相当于存储完第二维数据,等等。为了快速读取数据,数据存储地址最好连在一起,所以array 数组最好是依次读取最后维度的数据,不要依次读取其他维度的数据。例如:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-import numpy as np
import timeh = 1000
w = 1000
weights = np.random.randn(h, w)data = np.random.randn(w)
# faster 2.4ms
t1 = time.clock()
for i in range(h):data += weights[i,:]
t2 = time.clock()
print('run time(ms):', 1000*(t2 - t1) )data = np.random.randn(h)
#slow 6.0ms
t1 = time.clock()
for i in range(w):data += weights[:,i]
t2 = time.clock()
print('run time(ms):', 1000*(t2 - t1) )

可以看出,两个程序块的数据吞吐量一样,速度却相差两倍以上,每次读取行的程序比读取列的程序快很多,因为行数据块的存储地址是连续的,而列数据块的地址是断续的。根据这个原理和卷积网络的局部连接性质, 4D 数据的维度需定义为:data=np.random.randn(batch,height,width,depth)data = np.random.randn(batch, height , width , depth)data=np.random.randn(batch,height,width,depth)
需要注意的是,最后维度是深度,第一维度是批量。程序依次读取深度维数据,即最后一个维度的数据,以达到加速目的。

4.3 池化层

通常,卷积层的超参数设置为:输出特征图的空间尺寸等于输入特征图的空间尺寸。这样如果卷积网络里面只有卷积层,特征图空间尺寸就永远不变。虽然卷积层的超参数数量与特征图空间尺寸无关,但这样会带来一些缺点。

(1 ) 空间尺寸不变,卷积层的运算量会一直很大,非常消耗资源

(2)卷积网络结构最后是通过全连接层输出分值向量的,如果空间尺寸一直不变, 则全连接层的权重数量会非常巨大,导致过拟合

(3)前面几层的卷积层的输出存在大量冗余,如果空间尺寸不变,则冗余会一直存在,因此需要一种技巧来减小空间尺寸。

4.3.1 概述

池化是一种最常用的减小空间尺寸的技巧,它可以对输入的每一个特征图独立地降低其空间尺寸,而保持深度维度不变。首先对特征图的每个局部窗口数据进行融合,得到一个输出数据,然后采用大于1 的步长扫描特征图。最常见的局部窗口尺寸是 2×22 × 22×2 ,有时也会采用 3×33 × 33×3 , 步长是 222 会去除 75%75\%75% 的神经元,步长如果采用 333 , 则会去除 88.89%88. 89 \%88.89% 的神经元,这过于剧烈,实践中不会采用。由于池化操作会去除大量的神经元,所以可以看作一种提纯操作。对局部窗口数据进行融合,最常使用的是MAX 操作,即选取局部窗口数据的最大值。当然,也可以采用取平均值操作,但不常用。图4.5 展示了池化层操作示意图,输入特征图尺寸 4 × 4 × 3 被降采样到了 2 × 2 × 3 ,采取的滤波器尺寸是 2 ,步长为 2 。采用最大值池化, 2x2 的局部区域选取最大值。

为什么采用最大值进行池化操作?这是因为卷积层后接 ReLU 激活, ReLU 激活函数把负值都变为 0 ,正值不变,所以神经元的激活值越大,说明该神经元对输人局部窗口数据的反应越激烈,提取的特征越好。用最大值代表局部窗口的所有神经元,是很合理的。最大值操作还能保持图像的平移不变性,同时适应图像的微小变形和小角度旋转。

最后强调,池化只减小空间维度尺寸,深度维度的尺寸保持不变。如前所述,深度维度的尺寸可以看作空间位置处神经元提取的特征数量。随着空间尺寸的减小,神经元的感受野越来越大,即神经元观察到的局部区域越来越大,所以需要提取更多的特征,故深度维度一般会随着空间尺寸的减小而增大。

下面简要介绍池化层的一些参数。

对输入特征图进行固定的操作,所以没有可学习的参数;池化层中很少使用 0 填充。

不使用池化层:如前所述,油化层的目的是减小特征图的空间尺寸,卷积层也可以减小空间尺寸即采用步长S=2 来降低特征图的空间尺寸,卷积层与步长为2 的池化操作一,会去除75% 的神经元。

4.3.2 池化层代码实现

池化层将每个局部窗口的数据转化为小矩阵,按行堆叠成大矩阵,然后每行取最大值得到大的列向量,最后转化为3D 特征图。

(1)输入 3D 特征图转化为矩阵 X局部区域转化为小矩阵。比如,输入是 56 × 56 × 96 ,局部窗口尺寸为 2 × 2 ,步长为 2 进行池化,取输入中的 2 × 2 × 96 局部数据块,将其转化为尺寸为 96 × 4 的小矩阵,注意是将 96 维的深度向量拉伸为一个列向量,共有 4 个深度向量。以步长为 2 扫描每一个局部窗口, 所以输出的宽高均为(56-2 )// 2+ 1 =28, 共有 28 × 28 = 784 个局部窗口, 784 × 96=75264 个行向量,输出矩阵 X 的尺寸是 75 × 264 × 4 。因为局部窗口之间没有重叠,所以输入特征图中的元素在不同的行中没有重复。

(2)最大值池化: 提取大矩阵每行的最大值,matric_data.max(axis=1,keepdims=True)matric\_data.max (axis = 1 , keepdims = True )matric_data.max(axis=1,keepdims=True),即得到每个局部窗口的最大值。在本例中,这个操作的输出是大列向量 [75264×1][ 75 264 × 1 ][75264×1] 。

(3)输出新3D 特征图28 × 28 × 96 ,大列向量的每96 个元素构成输出3D 特征图的一个深度向量。

由于池化操作简单直观,读者也有卷积层程序的基础,因此直接给出批量数据池化层的代码:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import numpy as npfilter_size = 2
filter_size2 = filter_size*filter_size
stride = 2(batch, in_height, in_width, in_depth) = (8, 32, 48, 16)
in_data = np.random.randn(batch, in_height, in_width, in_depth)out_height = (in_height - filter_size)//stride + 1
out_width = (in_width - filter_size)//stride + 1
out_size = out_height*out_width
out_depth = in_depth
out_data = np.zeros((batch, out_height, out_width, out_depth))matric_data = np.zeros( (out_size*in_depth*batch, filter_size2) ) # 1height_ef = in_height - filter_size + 1
width_ef = in_width - filter_size + 1for i_batch in range(batch):i_batch_size = i_batch*out_size*in_depth # 2for i_h, i_height in zip(range(out_height), range(0, height_ef, stride)):i_height_size = i_batch_size + i_h*out_width*in_depthfor i_w, i_width in zip(range(0, out_width*in_depth, in_depth),range(0, width_ef, stride)): # 3md = matric_data[i_height_size + i_w : i_height_size + i_w + in_depth, : ] # 4src = in_data[i_batch, i_height : i_height + filter_size,i_width  : i_width  + filter_size, :] # 5for i in range(filter_size):for j in range(filter_size):md[:, i*filter_size + j] = src[i, j, :] # 6matric_data_max_value = matric_data.max(axis = 1, keepdims = True) # 7
matric_data_max_pos = matric_data == matric_data_max_value # 8for i_batch in range(batch):i_batch_size = i_batch*out_size*out_depthfor i_height in range(out_height):i_height_size = i_batch_size + i_height*out_width*out_depthfor i_width in range(out_width):out_data[i_batch, i_height, i_width, :] = matric_data_max_value[i_height_size + i_width*out_depth :i_height_size + i_width*out_depth + out_depth].ravel() # 9

4.4 全连接层

如果卷积网络输入是 224 × 224 × 3 的图像,经过一系列的卷积层和池化层(因为卷积层增加深度维度,池化层减小空间尺寸), 尺寸变为 7 × 7 × 512 ,之后需要输出类别分值向量, 计算损失函数。

假设类别数量是1000 (ImageNet 是1000 类), 则分值向量可表示为特征图 1 × 1 × 1000 。如何将 7 × 7 × 512 的特征图转化为1 × 1 × 1000 的特征图呢?最常用的技巧是全连接方式,即输出 1 × 1 × 1000 特征图的每个神经元(共1000 个神经元)与输入的所有神经元连接,而不是局部连接。每个神经元需要权重的数量为 7 × 7 × 512 = 25 088 ,共有1000 个神经元,所以全连接层的权重总数为: 25 088 × 1000 = 25 088 000 , 参数如此之多,很容易造成过拟合,这是全连接方式的主要缺点

全连接层的实现方式有两种。一种方式是把输入3D 特征图拉伸为 1D 向量,然后采用常规神经网络的方法进行矩阵乘法;另一种方式是把全连接层转化成卷积层,这种方法更常用,尤其是在物体检测中。

4.4.1 全连接层转化成卷积层

全连接层和卷积层中的神经元都是计算点积和非线性激活,函数形式是一样的,唯→的差别在于卷积层中的神经元只与输入数据中的一个局部区域连接,并且采用参数共享; 而全连接层中的神经元与输入数据中的全部区域都连接,并且参数各不相同。因此,两者是可能相互转化的。

  • 卷积层转换为全连接层:任意一个卷积层都能转换为等价的全连接层。此时连接整个输入空间的权重矩阵是一个巨大的矩阵除了某些特定区域(局部连接)外,其余都是。不同神经元的矩阵,其非零区域的元素都是相等的(参数共享),详见4 . 2.4 节。
  • 全连接层转化为卷积层:比如,一个全连接层,输入特征图是 7 × 7 × 512 ,输出特征图是 1 × 1 × 1000 ,这个全连接层可以等效为一个卷积层: F= 7, P = 0, S = 1, K = 1000 。即将卷积核的尺寸设置为和输入特征图的空间尺寸一致,不需要0 填充,也不需要滑动卷积窗口,所以输出空间尺寸为 1 ,只有一个单独的深度向量,所以输出变成 1 ×1 × 1000。全连接层转化为卷积层操作,还会带来额外的好处:可以在一次前向传播中,让卷积网络在一幅更大的输入图像中的不同位置进行卷积。


全连接层转换为卷积层后的卷积网络只需进行一次前向传播,就和 36 次卷积的效果是一样的。相比之下, 一次前向传播计算要高效得多,因为共享了计算资源。这一技巧在实践中经常被使用,特别是在物体检测领域。通常,输入一张尺寸大的图像,使用变换后的卷积网络对空间上很多不同位置的子图像进行评估,得到分值向量,然后求这些分值向量的平均值。如上,得到 6 × 6 × 1000 特征图后,再对每个 6 × 6 特征图进行平均,得到最终特征图 1 × 1 × 10 00 ,效果一般会更好。

上述操作只能得到步长为 32 的卷积效果,如果想用步长小于 32 ,如16 ,最终会得到 1 1 × 11 × 1000 的输出,因为( 384-224)// 6+ 1 = 11 。这一问题可以采用两次前向传播解决,第一次在原始图像进行卷积,得到 6 × 6 × 1000 的输出;第二次分别沿宽度和高度平移 16 个像素,得到“新图像” 368 × 368 × 3 ,然后在“新图像”上进行卷积,得到 5 ×5 × 1000 的输出,因为(368-224)// 32+1=5 。两次结果合井,得到 11 × 11 × 1000 。

4.4.2 全连接层代码实现

代码可以直接采用4.2.6 节的卷积代码,只是超参数固定为S=1, P=0 ,卷积核尺寸 F 一般等于输入特征图的空间尺寸,这样输出特征图空间尺寸为 1 × 1 。如果卷积核尺寸小于空间尺寸,则输出特征图空间尺寸将大于 1 × 1 。当卷积核尺寸 F 等于输入特征图的空间尺寸时,可以采用把输入 3D 特征图拉伸为 1D 数据的方式进行计算,此时代码
如下:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import numpy as nplast = 0
(batch, in_height, in_width, in_depth) = (8, 32, 48, 16)
in_data = np.random.randn(batch, in_height, in_width, in_depth)size = in_height * in_width * in_depth
matric_data = np.zeros( (batch, size) )for i_batch in range(batch):matric_data[i_batch] = in_data[i_batch].ravel() # 1out_depth = 32  weights = 0.01 * np.random.randn(size, out_depth)
bias = np.zeros((1, out_depth))filter_data = np.dot(matric_data, weights) + biasif not last:out_data = np.maximum(0, filter_data) # 2 RELU激活

语句①把3D 特征图拉伸为 1D 向量;语句②进行非线性激活。注意,最后一层全连接层输出分值,不需要非线性激活。该程序除了多了拉伸语句,其他代码和常规神经网络代码一模一样。

4.5 卷积网络的结构

前面介绍了卷积网络的3 种基本模块:卷积层( CONV )、池化层(POOL ,默认最大值池化)和全连接层( FC )。卷积层和全连接层后面都需紧接ReLU 激活层,为了简化书写,省略此层。但必须注意的是,最后一个全连接层后面不需接ReLU 激活层。那么,如何通过这些模块组织成卷积网络呢?

4.5.1 层的组合模式

卷积网络最基本的结构是:先堆叠一个或多个卷积层进行特征提取,然后接一个池化层进行空间尺寸缩小,之后重复此模式,直到空间尺寸足够小(如 7 × 7 和 5 × 5 ),最后接多个全连接层,其中最后一个全连接层输出类别分值

例如,下面是一些常见的网络结构。

  • INPUT →[CONV → POOL]× 3 → FC → FC:每个卷积层之后紧跟一个池化层,重复了3 次。
  • INPUT →[CONV × 2 → POOL]× 3 → FC × 2 → FC:每2 个卷积层之后有一个池化层,堆叠多个卷积层可以学习到更丰富的特征。
  • INPUT →[CONV → POOL]→[CONV × 2 → POOL]× 2 → [CONV × 5 → POOL]× 2 → FC × 2 → FC::堆叠卷积层的数量随着网络加深而变大,如开始时候,一个卷积层后立即池化,最后是5 个卷积层才池化一次。因为随着特征图空间尺寸的减小,神经元的感受野越来越大,提取的特征更具有全局性,需要提取更复杂的关系, 所以需要更多的卷积层。这种结构现在十分流行。

输入图像的空间尺寸常用的是32 ( CIFAR-10 ), 64 、96 ( STL-10 ),224 ( ImageNet )、384 和512 ,这些尺寸能被2 整除很多次

卷积层使用小尺寸卷积核( 3 × 3 ),步长S=1 ,如果必须使用的卷积核( 5 × 5 或者7 × 7 ),通常只用在第一个卷积层中,其输入是原始图像,且仅使用一次。

池化层对特征图进行空间降采样,最常用的是 2 × 2 感受野的最大值池化,步长为 2 ,另一个不常用的是 3 × 3 感受野,步长为2 。

为什么使用步长 S = 1 的卷积层?实践表明小步长的效果更好。步长为 1 的卷积层不改变输入特征图的空间维度,只对深度维度进行变换。

为何卷积层使用零填充?因为对特征图进行零填充,不会改变输入特征图的空间尺寸, P = (F - 1)//2 。如果不进行零填充,每次卷积后特征图的尺寸就会减小2 ,那么特征图边缘的信息就会过快地损失掉,特别是堆叠很多个卷积层的时候。

为什么要堆叠多个卷积层,而不直接用一个卷积核尺寸大的卷积层?假设一层一层地堆叠了3 个 3 × 3 的卷积层,第一个卷积层中的每个神经元对输入特征图有 3 × 3 的感受野,第二个卷积层上的神经元对第一个卷积层有 3 × 3 的感受野,即对输入特征图有 5 × 5 的感受野,同样,第三个卷积层上的神经元对第二个卷积层有3 × 3 的感受野,即对输入特征图有 7 × 7 的感受野。假设不采用堆叠 3 个 3 × 3 的卷积层,而是使用一个单独的 7 × 7 的感受野的卷积层,那么所有神经元的感受野也是 7 × 7 ,但这样有一些缺点

  • 首先,多个卷积层与非线性激活层的交替结构, 比单一的卷积层结构提取的特征更富有表现力;
  • 其次,假设输入输出的特征图的深度维度都是 C, 那么单独的 7 × 7 卷积层包含 C×(7×7)×C=49C2C ×(7 × 7) × C = 49C^2C×(7×7)×C=49C2 个权重,而 3 个 3 × 3 的卷积层只有 3×(C×(3×3)×C)=27C23 ×(C × (3 × 3)× C) =27C^23×(C×(3×3)×C)=27C2 个权重。

堆叠多个 3 × 3 的卷积层缺点是:在进行误差反向传播时,需要存储中间每个卷积层的激活值,这会占用更多的内存。

为什么卷积网络称为深度网络?这里的深度是指网络的深度,一般来说,网络隐含层大于两层,就能称为深度网络,也就是深度学习,卷积网络的深度一般都大于5 层,甚至达到上千层!传统的机器学习方法可以看作浅层网络,如SVM 可以看作一个隐含层的网络,深度学习之前的神经网络隐含层一般不超过两层。卷积网络的深度主要是由图像的多层次结构决定的: 网络的前层学习图像的低层特征,如边缘和纹理模式;中间层学习图像的中层视觉特征,如眼睛、腿等物体部件;后层网络学习物体整体概念,最后的全连接层得到物体类别。

卷积网络最基本的结构是将卷积层、池化层和全连接层这3 层进行简单的串联( VGG 为其代表),虽然结构清晰,容易掌握, 但网络学习效率不高。谷歌的Inception 结掏和微软亚洲研究院的残差(Residual Net )结构,虽然连接模式复杂,但网络参数更少, 学习效率更高。这三种最经典的结构,本书后面都有论述。

4.5.2 表示学习

可以从另一个角度来理解上述卷积神经网络。卷积网络的输入是图像,如224 × 224 × 3 ,经过多个卷积层和池化层后,假设输出为7 × 7 × 512 ,最后接多个全连接层输出分值向量。多个全连接层可以看作常规神经网络,该网络的输入是7 × 7 × 512 。这样可以把卷积网络全连接层前的多个卷积层和池化层看作特征提取器: 每层对上一层的输出进行特征变换,把与类别无关的低层表示 224 × 224 × 3 (如图像)变换为与类别密切相关的高层表示 7 × 7 × 5 1 2 ,使得原来基于全连接层难以完成的任务成为可能。每层一般是对 3D 特征图的空间尺寸进行减小,深度尺寸进行增加,即从 224 × 224 × 3 最终变换为 7 × 7 × 512 。所以卷积网络也称为“特征学习”或“表示学习”

深度学习之前,机器学习进行模式分类,这个过程需要专家提取特征,这称为“特征工程”,见第 1 章内容。特征的好坏对模型的泛化性能至关重要,专家设计出好特征十分困难,特别是在图像和声音领域,图像领域著名的人工特征有 SIFT 、HOG 和LBP 等。卷积网络通过机器学习技术,自己从数据中产生好特征,把人类从特征工程中解放出来,大大扩展了机器学习的适用领域。

机器学习技术自身产生的好特征是指针对特定的训练集来说的,对其他训练集可能不是。如果该特征对很多不同的训练集都是好特征,就说明该模型具有很强的迁移学习能力,是十分理想的模型。举个例子, 训练卷积网络对于文字的识别能力,如果训练集里面的字体都是宋体,网络对训练集取得很好的分类效果,这时就可以认为模型学习出了好特征。但是该模型对于行书字体的识别效果很差,说明该模型学习到的特征对于行书字体而言,不是好特征。

4.6 卷积网络的神经科学基础

生物学启发人工智能最为成功的案例可能就是卷积网络,卷积网络的一些关键设计原则均来自神经科学。神经生理学家 David Hubel 和 Torsten Wiesel 通过对猫的视觉系统的多年研究发现:初级视觉皮层 V1 细胞具有强烈的方向选择性,即对视野中特定方向的条纹反应强烈,而对其他方向的条纹几乎没有反应。初级视觉皮层 V1 是大脑对视觉输入开始执行显著高级处理的第一个区域,卷积网络层的设计借鉴了 V1 的3 个性质。V1 用二维结构来反映视网膜的图像结构。卷积网络通过用二维映射定义特征的方式来描述该特征,如3D 特征图。V1 包含许多简单细胞这些细胞的活动在某种程度上可以概括为:在一个小的空间位置接受域内的图像的线性函数。卷积网络的神经元设计为局部连接的卷积运算。V1 还包含许多复杂细胞,这些细胞响应类似于简单细胞检测的那些特征,但是复杂细胞对于特定的位置的微小偏移具有不变性,这启发了卷积网络的池化单元。

大多数的V1 细胞具有由 Gabor 函数所描述的权重,而Gabor 函数是高斯函数和余弦函数的乘积。卷积网络第一层卷积层学习到的特征和Gabor 函数非常类似。

可惜的是,神经科学很难告诉我们该如何训练卷积网络。

CNN的Python实现——第四章:卷积神经网络的结构相关推荐

  1. 第五章 卷积神经网络(CNN)

    文章目录 5.1 卷积神经网络的组成层 5.2 卷积如何检测边缘信息? 5.3 卷积层中的几个基本参数? 5.3.1 卷积核大小 5.3.2 卷积核的步长 5.3.3 边缘填充 5.3.4 输入和输出 ...

  2. 花书+吴恩达深度学习(十四)卷积神经网络 CNN 之经典案例(LetNet-5, AlexNet, VGG-16, ResNet, Inception Network)

    目录 0. 前言 1. LeNet-5 2. AlexNet 3. VGG-16 4. ResNet 残差网络 5. Inception Network 如果这篇文章对你有一点小小的帮助,请给个关注, ...

  3. 【李刚-21天通关Python】第四章:函数

    [李刚-21天通关Python]第四章:函数 第四章:函数 函数入门与定义函数 多返回值函数与递归函数 关键字参数与参数默认值 参数收集和逆向参数收集 变量作用域 局部函数 实操:定义计算N的阶乘的函 ...

  4. 第3.1章 卷积神经网络(CNN)——Conv、Pool、FC、Activation Function、BN各个层的作用及原理

    第3.1章 卷积神经网络CNN-不同层的作用 一.Convolution(CONV) 二.Pooling(POOL) 三.Fully Connected(FC) 四.Activation Functi ...

  5. 日月光华深度学习(四)-计算机视觉-卷积神经网络

    日月光华深度学习-计算机视觉-卷积神经网络 计算机视觉-卷积神经网络 [4.1]--认识卷积神经网络(一) [4.2]--认识卷积神经网络-卷积层和池化层 [4.3]--卷积神经网络整体架构 [4.4 ...

  6. 第11章 卷积神经网络(CNNs)

    第11章 卷积神经网络(CNNs) 我们回顾了整个机器学习和深度学习知识,现在我们学习CNNs(Convolutional Neural Networks)以及它在深度学习中的作用.在传统的前馈神经网 ...

  7. 【深度学习】基于Torch的Python开源机器学习库PyTorch卷积神经网络

    [深度学习]基于Torch的Python开源机器学习库PyTorch卷积神经网络 文章目录 1 CNN概述 2 PyTorch实现步骤2.1 加载数据2.2 CNN模型2.3 训练2.4 可视化训练 ...

  8. 《Scikit-Learn与TensorFlow机器学习实用指南》第13章 卷积神经网络

    第13章 卷积神经网络 来源:ApacheCN<Sklearn 与 TensorFlow 机器学习实用指南>翻译项目 译者:@akonwang @WilsonQu 校对: @飞龙 ​尽管 ...

  9. 第四章 前馈神经网络

    第四章 前馈神经网络 第四章 前馈神经网络 神经元 Sigmoid 型函数 Logistic函数 Tanh函数 Hard-Logistic 函数和 Hard-Tanh 函数 ReLU 函数 带泄露的 ...

最新文章

  1. 深入理解 KVC\KVO 实现机制 — KVC
  2. web功底之强,实属罕见。。
  3. Identity Server 4 - Hybrid Flow - Claims
  4. react常用知识点总结
  5. 将String转换成InputStream
  6. link标签引入.css文件(目的):适配不同屏幕
  7. ubuntu linux桌面快捷方式,Ubuntu下生成桌面快捷方式
  8. jquery核心的学习进程一
  9. 数据库不存在 php报错,在php中需要用到的mysql数据库的简单操作,phpmysql
  10. 遗传算法(GA)实例介绍(JAVA)
  11. 知道对方IP,你该这样入侵(附:如何隐藏IP地址)
  12. Java疯狂讲义读书笔记第一章
  13. Unity3D 快捷键技巧
  14. 百度2005年面试题
  15. 服务器开机硬盘raid连接错误,服务器磁盘阵列常见问题及解决方法
  16. IceSword V1.22 Final 冰刃
  17. javaScript中原型和原型链的分析深究 —————— 开开开山怪
  18. 大厂面试中HR可能会问到的问题
  19. ng-alain php,Angular 中后台前端解决方案 - Ng Alain 介绍
  20. 计算机计算建筑结构的方法有哪些,建筑结构设计包括哪些内容呢?

热门文章

  1. Feburary——766.托普利茨矩阵
  2. Lazada对卖家履约及时率规则进行更新调整-新增OVL限单
  3. 30分钟java桌球小游戏_Java桌球小游戏(兴趣制作)
  4. 接口自动化测试(1)
  5. 在一个笼子里同事养着一些鸡和兔子,你想了解有多少只鸡和兔,主任对你说:我只告诉你鸡和兔的总头数是16和总脚数是40,你能不能自己计算有多少只鸡和多少只兔?
  6. 中文文字检测及识别(ORC)
  7. Mac下安装SecureCRT并激活 阿星小栈
  8. 谈谈引用和Threadlocal的那些事
  9. 【原创】石灰水滴在汽车上自己清洗的方法
  10. Robot fish: bio-inspired fishlike underwater robots 阅读笔记 1