一、整体结构
全连接层(fully-connected layer):相邻层的所有神经元之间都有连接。
基于CNN的网络:靠近输出的层中使用了之前的“Affine - ReLU”组合。此外,最后的输出层中使用了之前的“Affi ne -Softmax”组合。这些都是一般的CNN中比较常见的结构。
二、卷积层
1、全连接层存在的问题
忽视数据的形状:对于3维图像数据(例如,MNIST数据集的例子中,输入图像就是1 通道、高28 像素、长28 像素的(1, 28, 28)形状,但却被排成1 列,以784 个数据的形式输入到最开始的Affine层,无法利用与形状有关的信息),形状中含有重要信息,比如,空间上邻近的像素为相似的值、RBG的各个通道之间分别有密切的关联性、相距较远的像素之间没有什么关联等。
CNN中,有时将卷积层的输入输出数据称为特征图(featuremap)。其中,卷积层的输入数据称为输入特征图(input feature map),输出数据称为输出特征图(output feature map)。
2、卷积运算(卷积层进行的处理,相当于图像处理中的“滤波器运算”)
卷积运算对输入数据应用滤波器(或称“核”),将各个位置上滤波器的元素和输入的对应元素相乘,然后再求和(有时将这个计算称为乘积累加运算)。在全连接的神经网络中,除了权重参数,还存在偏置,CNN 中,滤波器的参数就对应之前的权重,并且CNN 中也存在偏置。
3、填充
在进行卷积层的处理之前,有时要向输入数据的周围填入固定的数据(比如0 等),这称为填充(padding),使用填充主要是为了调整输出的大小,避免在反复进行卷积运算时某个时刻输出大小变为 1 。“幅度为1 的填充”是指用幅度为1 像素的0 填充周围。
4、步幅(stride)
应用滤波器的位置间隔称为步幅(stride)。
5、3维数据的卷积运算
处理多维数据时,滤波器 通道数只能设定为和输入数据的通道数相同的值(本例中为3),每个通道的滤波器大小要全部相同。
6、结合方块思考
当只有一个滤波器时:输出是一个一维的特征图
基于多个滤波器:
输入数据表示顺序:(channel,height,width)可以写成 ( C,H, W)
滤波器的权重数据:( output_channel, input_channel ,height, width)
偏置的形状: (FN, 1, 1)
7、批处理(批处理将N次的处理汇总成了1 次进行)
三、池化层(缩小高、长方向上的空间的运算)
图7-14 的例子是按步幅2 进行2 * 2 的Max池化时的处理顺序,池化时需要确定目标区域的大小和步幅,一般来说,池化的窗口大小会和步幅设定成相同的值。除Max池化外,还有Average池化。
池化层的特征:
1、没有要学习的参数;2、输入数据和输出数据通道数不发生变化;3、对微小的位置变化具有鲁棒性(健壮)
四、卷积层和池化层的实现
1、卷积层的实现
CNN中处理的是4维数据
import numpy as np
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
'''
输入:
input_data: 由(数据量,通道,高,长)的4维数组构成的输入数据
filter_h: 滤波器的高
filter_w: 滤波器的长
stride: 步幅
pad: 填充
输出:
col: 二维矩阵
'''
N, C, H, W = input_data.shape
out_h = (H + 2*pad - filter_h)//stride + 1
out_w = (W + 2*pad - filter_w)//stride + 1
# np.pad 在numpy数组的边缘进行数值填充
# https://www.cnblogs.com/shuaishuaidefeizhu/p/14179038.html
img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))
# 遍历过滤波的每一个位置,记录每一个位置在整个卷积操作中会进行相乘操作的输入数据的元素
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
# 转为二维矩阵(N*out_h*out_w,C*filter_h* ilter_w)
# 每一行代表在进行一次卷积运算时滤波器覆盖的区域,一共进行N*out_h*out_w次卷积操作
col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
return col
def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
N, C, H, W = input_shape
out_h = (H + 2*pad - filter_h)//stride + 1
out_w = (W + 2*pad - filter_w)//stride + 1
col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)
img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]
return img[:, :, pad:H + pad, pad:W + pad]
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
self.x = None
self.col = None
self.col_W = None
self.dW = None
self.db = None
def forward(self, x):
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
out_w = 1 + int((W + 2*self.pad - FW) / self.stride)
col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1).T# 滤波器的展开,每一列代表一个滤波器的所有参数
out = np.dot(col, col_W) + self.b # 输出是二维的(N*out_h*out_w,FN)
# 每一行代表同一个覆盖区域与不同滤波器卷积运算后的结果
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
self.x = x
self.col = col
self.col_W = col_W
return out
def backward(self, dout):
FN, C, FH, FW = self.W.shape
dout = dout.transpose(0,2,3,1).reshape(-1, FN)
self.db = np.sum(dout, axis=0)
self.dW = np.dot(self.col.T, dout)
self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)
dcol = np.dot(dout, self.col_W.T)
dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
return dx
2、池化层的实现
池化层的实现和卷积层相同,都使用im2col展开输入数据。不过,池化的情况下,在通道方向上是独立的,这一点和卷积层不同。池化的应用区域按通道单独展开。
import numpy as np
class Pooling:
def __init__(self, pool_h, pool_w, stride=2, pad=0):
self.pool_h = pool_h
self.pool_w = pool_w
self.stride = stride
self.pad = pad
self.x = None
self.arg_max = None
def forward(self, x):
N, C, H, W = x.shape
out_h = int(1 + (H - self.pool_h) / self.stride)
out_w = int(1 + (W - self.pool_w) / self.stride)
# 展开(1)
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(-1, self.pool_h*self.pool_w)# (N*out_h*out_w*C,pool_h*pool_w)各通道单独计算
# 最大值(2)
arg_max = np.argmax(col, axis=1)# 返回最大值的索引
out = np.max(col, axis=1)# (N*out_h*out_w*C,)
# 转换(3)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
# (N,C, out_h, out_w)
self.x = x
self.arg_max = arg_max
return out
def backward(self, dout):
dout = dout.transpose(0, 2, 3, 1) # (N, out_h, out_w,C)
pool_size = self.pool_h * self.pool_w
dmax = np.zeros((dout.size, pool_size))
dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
dmax = dmax.reshape(dout.shape + (pool_size,))
dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
return dx
五、CNN的实现
class SimpleConvNet:
"""
input_size : 输入数据的维度:(通道,高,长)
hidden_size_list : 隐藏层(全连接)的神经元数量
output_size : 输出层(全连接)的神经元数量
activation : 'relu' or 'sigmoid'
weight_init_std : 初始化时权重的标准差
"""
def __init__(self, input_dim=(1, 28, 28),
conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1},
hidden_size=100, output_size=10, weight_init_std=0.01):
# 初始化卷积层滤波器的尺寸和池化层
filter_num = conv_param['filter_num']
filter_size = conv_param['filter_size']
filter_pad = conv_param['pad']
filter_stride = conv_param['stride']
input_size = input_dim[1]
conv_output_size = (input_size - filter_size + 2*filter_pad) / filter_stride + 1
# 全连接层的输入数据大小,可修改的更通用一些
pool_output_size = int(filter_num * (conv_output_size/2) * (conv_output_size/2))
# 初始化权重参数
self.params = {}
# 卷积
self.params['W1'] = weight_init_std * \
np.random.randn(filter_num, input_dim[0], filter_size, filter_size)
self.params['b1'] = np.zeros(filter_num)
# 隐藏层
self.params['W2'] = weight_init_std * \
np.random.randn(pool_output_size, hidden_size)
self.params['b2'] = np.zeros(hidden_size)
# 输出层
self.params['W3'] = weight_init_std * \
np.random.randn(hidden_size, output_size)
self.params['b3'] = np.zeros(output_size)
# 生成必要的层
self.layers = OrderedDict()
self.layers['Conv1'] = Convolution(self.params['W1'], self.params['b1'],
conv_param['stride'], conv_param['pad'])
self.layers['Relu1'] = Relu()
self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2)
self.layers['Affine1'] = Affine(self.params['W2'], self.params['b2'])
self.layers['Relu2'] = Relu()
self.layers['Affine2'] = Affine(self.params['W3'], self.params['b3'])
self.last_layer = SoftmaxWithLoss()
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
def loss(self, x, t):
y = self.predict(x)
return self.last_layer.forward(y, t)
def accuracy(self, x, t, batch_size=100):
if t.ndim != 1 : t = np.argmax(t, axis=1)
acc = 0.0
for i in range(int(x.shape[0] / batch_size)):
tx = x[i*batch_size:(i+1)*batch_size]
tt = t[i*batch_size:(i+1)*batch_size]
y = self.predict(tx)
y = np.argmax(y, axis=1)
acc += np.sum(y == tt)
return acc / x.shape[0]
def numerical_gradient(self, x, t):
loss_w = lambda w: self.loss(x, t)
grads = {}
for idx in (1, 2, 3):
grads['W' + str(idx)] = numerical_gradient(loss_w, self.params['W' + str(idx)])
grads['b' + str(idx)] = numerical_gradient(loss_w, self.params['b' + str(idx)])
return grads
def gradient(self, x, t):
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.last_layer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
# 設定
grads = {}
grads['W1'], grads['b1'] = self.layers['Conv1'].dW, self.layers['Conv1'].db
grads['W2'], grads['b2'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
grads['W3'], grads['b3'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
return grads
def save_params(self, file_name="params.pkl"):
params = {}
for key, val in self.params.items():
params[key] = val
with open(file_name, 'wb') as f:
pickle.dump(params, f)
def load_params(self, file_name="params.pkl"):
with open(file_name, 'rb') as f:
params = pickle.load(f)
for key, val in params.items():
self.params[key] = val
for i, key in enumerate(['Conv1', 'Affine1', 'Affine2']):
self.layers[key].W = self.params['W' + str(i+1)]
self.layers[key].b = self.params['b' + str(i+1)]