1. cnn_operations模块
cnn_operations类
@staticmethod
def calc_loss(Y, tilde_Y):
# 训练样本个数
n_samples = Y.shape[0]
# 网络代价
loss = 0
for i in range(n_samples):
loss += np.sum((Y[i, :] - tilde_Y[i, :])**2)
loss /= (2 * n_samples)
return loss
计算网络代价:
Y: N * M array,各训练样本对应的类别标签,N为训练样本个数,M为类别数
tilde_Y: N * M array,预测出的各训练样本的类别标签
这个函数被定义为静态方法(使用了@staticmethod
装饰器),所以它可以通过类名直接调
用,而不需要创建类的实例。
@staticmethod
def convolute_2d(feature_map, kernel, size_kernel, stride, padding):
# 输入特征图尺寸
size_feature_map = np.asarray(feature_map.shape)
# 输出特征图尺寸
size_output = [int( \
(size_feature_map[0] + 2 * padding - size_kernel[0]) / stride + 1),
int( \
(size_feature_map[1] + 2 * padding - size_kernel[1]) / stride + 1)]
if padding == 0:
feature_map_padded = feature_map
elif padding > 0:
# 补零后的特征图
feature_map_padded = np.zeros(size_feature_map + 2 * padding)
feature_map_padded[padding: -padding, padding: -padding] += \
feature_map
output = np.empty(size_output)
for i in range(0, feature_map_padded.shape[0], stride):
for j in range(0, feature_map_padded.shape[1], stride):
sub_mat = feature_map_padded[i: np.min([i + size_kernel[0],
feature_map_padded.shape[0]]),
j: np.min([j + size_kernel[1],
feature_map_padded.shape[1]])]
shape_sub_mat = sub_mat.shape
if (shape_sub_mat[0] < size_kernel[0]) or \
(shape_sub_mat[1] < size_kernel[1]):
break
output[int(i/stride), int(j/stride)] = \
np.sum(sub_mat * kernel)
return output
二维卷积(卷积核不翻转):
feature_map: 2-d array,输入特征图(也可以被看作是输入的图像或前一层输出的特征图)
kernel: 2-d array,卷积核;
size_kernel: array,卷积核尺寸;
stride: 整数,卷积核步长;
padding: 整数,边缘补零的宽度,常用于保持特征图的尺寸。
output: 二维数组,表示进行卷积操作后输出的特征图。
操作流程:
①计算输入特征图的尺寸size_feature_map。
②计算输出特征图的尺寸size_output。这个尺寸取决于输入特征图的尺寸,卷积核的尺寸,步长和
补零的宽度。
③如果补零的宽度为0,则不进行任何操作;如果补零的宽度大于0,则在输入特征图的边缘添加相
应宽度的零,得到feature_map_padded。
④初始化输出特征图output。
⑤对feature_map_padded进行卷积操作:在每个步长位置,取出与卷积核相同尺寸的子区域
sub_mat,然后将sub_mat与卷积核逐元素相乘并求和,得到的结果存储在output的相应位置。
@staticmethod
def pool(feature_map, type_pooling, size_kernel, stride, padding):
# 输入特征图尺寸
size_feature_map = np.asarray(feature_map.shape)
# 输出特征图尺寸
size_output = [int( \
(size_feature_map[0] + 2 * padding - size_kernel[0]) / stride + 1),
int( \
(size_feature_map[1] + 2 * padding - size_kernel[1]) / stride + 1)]
if padding == 0:
feature_map_padded = feature_map
elif padding > 0:
# 补零后的特征图
feature_map_padded = np.zeros(size_feature_map + 2 * padding)
feature_map_padded[padding: -padding, padding: -padding] += \
feature_map
output = np.empty(size_output)
if type_pooling is "max":
func = np.max
elif type_pooling is "average":
#池化层每个神经元有一个权值,此处只需求和,不需求均值
func = np.sum
for i in range(0, feature_map_padded.shape[0], stride):
for j in range(0, feature_map_padded.shape[1], stride):
sub_mat = feature_map_padded[i: np.min([i + size_kernel[0],
feature_map_padded.shape[0]]),
j: np.min([j + size_kernel[1],
feature_map_padded.shape[1]])]
shape_sub_mat = sub_mat.shape
if (shape_sub_mat[0] < size_kernel[0]) or \
(shape_sub_mat[1] < size_kernel[1]):
break
output[int(i/stride), int(j/stride)] = func(sub_mat)
return output
池化(用于降低特征图的维度,同时保持重要的特征):
feature_map: 2-d array,输入特征图
type_pooling: 池化核类型,{"max", "average"}
size_kernel: array,池化核尺寸
stride: 池化核步长,整数
padding: 边缘补零的宽度,整数
output: 二维数组,表示进行池化操作后输出的特征图。
操作流程:
①计算输入特征图的尺寸size_feature_map。
②计算输出特征图尺寸size_output。取决于输入特征图尺寸,池化核尺寸,步长和补零的宽度。
③如果补零的宽度为0,则不进行任何操作;如果补零的宽度大于0,则在输入特征图的边缘添加相
应宽度的零,得到feature_map_padded。
④初始化输出特征图output。
⑤根据池化类型选择相应的函数func。
⑥对feature_map_padded进行池化操作:在每个步长位置,取出与池化核相同尺寸的子区域
sub_mat,然后将func应用于sub_mat,得到的结果存储在output的相应位置。
@staticmethod
def upsample_conv_2d(delta_next_layer, kernel, size_kernel, stride):
# 下一层(卷积层)中一个神经元的灵敏度map的尺寸
size_delta_next_layer = np.asarray(delta_next_layer.shape)
# 边缘补零后当前层一中一个神经元灵敏度map的尺寸
size_delta_padded = \
[size_delta_next_layer[0] * stride + size_kernel[0] - stride,
size_delta_next_layer[1] * stride + size_kernel[1] - stride]
delta_padded = np.zeros(size_delta_padded)
for i in range(delta_next_layer.shape[0]):
for j in range(delta_next_layer.shape[1]):
# 卷积核不翻转
delta_padded[i * stride: i * stride + size_kernel[0],
j * stride: j * stride + size_kernel[1]] += \
delta_next_layer[i, j] * kernel
return delta_padded
当前层为池化层,下一层为卷积层时的上采样:
delta_next_layer: 2-d array,下一层(卷积层)中一个神经元的灵敏度map
kernel: 2-d array,(下一层中一个神经元的一个)卷积核
size_kernel: array,(下一层中一个神经元的一个)卷积核的尺寸
stride: (下一层中一个神经元的一个)卷积核的步长
delta_padded: 2-d array,当前(池化)层中一个神经元的灵敏度map(边缘补零后)
操作流程:
①计算下一层(卷积层)中一个神经元的灵敏度map的尺寸size_delta_next_layer。
②计算边缘补零后当前层中一个神经元的灵敏度map的尺寸size_delta_padded。这个尺寸取决于
下一层灵敏度map的尺寸,卷积核的尺寸和步长。
③初始化当前层的灵敏度map:delta_padded。
④对下一层的灵敏度map进行遍历,在每个位置,将该位置的灵敏度值乘以卷积核,然后加到当前
层的相应位置。
@classmethod
def upsample_pool(cls, delta_next_layer, type_pooling,
size_kernel, stride):
if type_pooling is "max":
# TODO:
pass
elif type_pooling is "average":
# 池化时实际进行了求和运算,故权值均为1
kernel = np.ones(size_kernel)
delta_padded = cls.upsample_conv_2d(delta_next_layer, kernel,
size_kernel, stride)
return delta_padded
当前层为卷积层,下一层为池化层时的上采样:
delta_next_layer: 2-d array,下一层(池化层)中一个神经元的灵敏度map
type_pooling: (下一层中一个神经元的)池化核类型,{"max", "average"}
size_kernel: array,(下一层中一个神经元的)池化核尺寸
stride: (下一层中一个神经元的)池化核步长
delta_padded: 2-d array,当前(卷积)层中一个神经元的灵敏度map(边缘补零后)
操作流程:
①首先判断池化类型。如果是max,那么暂未实现(标记为TODO)。如果是average,则进行
下一步操作。
②对于average池化,由于池化过程实际上是求和运算,每个元素的贡献相同,所以权值均为1,
构造一个全为1的kernel。
③调用upsample_conv_2d方法,将下一层的灵敏度map扩展到当前层的尺寸。
@staticmethod
def inv_conv_2d(feature_map, size_kernel, stride, padding, delta):
# 输入特征图尺寸
size_feature_map = np.asarray(feature_map.shape)
if padding == 0:
feature_map_padded = feature_map
elif padding > 0:
# 补零后的特征图
feature_map_padded = np.zeros(size_feature_map + 2 * padding)
feature_map_padded[padding: -padding, padding: -padding] += \
feature_map
output = 0
for i in range(0, feature_map_padded.shape[0], stride):
for j in range(0, feature_map_padded.shape[1], stride):
sub_mat = feature_map_padded[i: np.min([i + size_kernel[0],
feature_map_padded.shape[0]]),
j: np.min([j + size_kernel[1],
feature_map_padded.shape[1]])]
shape_sub_mat = sub_mat.shape
if (shape_sub_mat[0] < size_kernel[0]) or \
(shape_sub_mat[1] < size_kernel[1]):
break
output += delta[int(i/stride), int(j/stride)] * sub_mat
return output
计算网络代价对卷积层中一个神经元的一个卷积核的偏导数:
feature_map: 2-d array,输入特征图
size_kernel: array,卷积核尺寸
stride: 卷积核的步长
padding: 边缘补零的宽度
delta: 2-d array,卷积层中一个神经元的灵敏度map
output: 2-d array,尺寸与卷积核尺寸相同
操作流程:
①首先获取输入特征图的尺寸,然后根据边缘补零的宽度padding,对输入特征图进行补零操作。
②初始化输出output为0。
③进行两层循环,以步长`stride`遍历补零后的特征图。
④在每次循环中,取出特征图中与卷积核尺寸相同的一个子区域sub_mat。
⑤检查子区域的尺寸,如果子区域的尺寸小于卷积核尺寸,那么跳过当前循环。
⑥计算子区域与对应的灵敏度值的乘积,并累加到输出值output中。
@staticmethod
def _relu(x):
return np.max([0, x])
@staticmethod
def _sigmoid(x):
return 1 / (1 + np.exp(-x))
@classmethod
def _tanh(cls, x):
return 2 * cls._sigmoid(2 * x) - 1
@classmethod
def activate(cls, feature_map, type_activation):
if type_activation is None:
return feature_map
elif type_activation is "relu":
func = cls._relu
elif type_activation is "sigmoid":
func = cls._sigmoid
elif type_activation is "tanh":
func = cls._tanh
size_feature_map = np.asarray(feature_map.shape)
output = np.empty(size_feature_map)
for i in range(size_feature_map[0]):
for j in range(size_feature_map[1]):
output[i, j] = func(feature_map[i, j])
return output
激活:
feature_map: 2-d array,输入特征图
type_activation: 激活函数类型,{"relu", "sigmoid", "tanh", None}
output: 2-d array,激活后特征图,与输入特征图尺寸相同
根据输入的类型_activation选择对应的激活函数,比如"relu"选择ReLU函数。通过双循环遍历
输入特征图每个元素,将其输入到选择的激活函数中,得到输出特征图每个元素的值。实现了将输入
特征图经过不同激活函数处理,输出经激活后的特征图。
2. test模块
配置和构建一个简单的卷积神经网络模型:
def config_net():
# 输入层
size_input = np.array([8, 8])
args_input = ("input", (size_input,))
# C1卷积层
connecting_matrix_C1 = np.ones([1, 2])
size_conv_kernel_C1 = np.array([5, 5])
stride_conv_kernel_C1 = 1
padding_conv_C1 = 0
type_activation_C1 = "sigmoid"
args_C1 = ("convoluting", (connecting_matrix_C1, size_conv_kernel_C1,
stride_conv_kernel_C1, padding_conv_C1,
type_activation_C1))
# S2池化层
type_pooling_S2 = "average"
size_pool_kernel_S2 = np.array([2, 2])
stride_pool_kernel_S2 = 2
padding_pool_S2 = 0
type_activation_S2 = "sigmoid"
args_S2 = ("pooling", (type_pooling_S2, size_pool_kernel_S2,
stride_pool_kernel_S2, padding_pool_S2,
type_activation_S2))
# C3卷积层
connecting_matrix_C3 = np.ones([2, 2])
size_conv_kernel_C3 = np.array([2, 2])
stride_conv_kernel_C3 = 1
padding_conv_C3 = 0
type_activation_C3 = "sigmoid"
args_C3 = ("convoluting", (connecting_matrix_C3, size_conv_kernel_C3,
stride_conv_kernel_C3, padding_conv_C3,
type_activation_C3))
# 输出层
n_nodes_output = 2
type_output = "softmax"
args_output = ("output", (n_nodes_output, type_output))
args = (args_input,
args_C1,
args_S2,
args_C3,
args_output)
cnn_net = cnn()
cnn_net.config(args)
return cnn_net
定义了网络各层的参数:
输入层大小;
每个卷积层的连接矩阵、卷积核大小、步长、边缘填充、激活函数;
池化层类型、池化核大小、步长、边缘填充、激活函数;
输出层节点数和激活函数。
将这些参数打包成元组,形成每个层的参数元组,如args_C1定义C1层的参数元组。
将所有层的参数元组组成一个总的参数元组args,初始化一个cnn网络对象cnn_net,调用cnn_net
的config方法,传入参数元组args,完成网络结构的配置,返回配置好结构的cnn_net对象。
n_train = 10000
X_train = 0.2 * np.random.randn(8, 8, n_train)
Y_train = np.random.randint(2, size=n_train)
for i in range(Y_train.shape[0]):
if Y_train[i] == 0:
X_train[1, :, i] += np.ones(8)
elif Y_train[i] == 1:
X_train[:, 1, i] += np.ones(8)
size_batch = 50
n_epochs = 500
cnn_net = config_net()
cnn_net.fit(X_train, Y_train, size_batch=size_batch, n_epochs=n_epochs)
n_test = 1000
X_test = 0.2 * np.random.randn(8, 8, n_test)
Y_test = np.random.randint(2, size=n_test)
for i in range(Y_test.shape[0]):
if Y_test[i] == 0:
X_test[1, :, i] += np.ones(8)
elif Y_test[i] == 1:
X_test[:, 1, i] += np.ones(8)
correct_rate = cnn_net.test(X_test, Y_test)
plt.figure()
for i in range(cnn_net.layers[1].n_nodes):
plt.subplot(1, 2, i + 1)
plt.imshow(cnn_net.layers[1].nodes[i].conv_kernels[0], cmap="gray")
plt.show()
生成训练数据和测试数据:
X_train, X_test shape为(8,8,n);Y_train, Y_test为0或1;
根据Y标记,在X的不同位置增加一个单位向量 signal
配置网络结构cnn_net,使用训练数据训练网络cnn_net.fit(),使用测试数据评估网络准确率
cnn_net.test(),可视化第一个卷积层学习得到的卷积核参数。
主要步骤包括:数据生成、网络配置、训练、测试、参数可视化
完整的分类任务流程:生成带有标记的训练和测试数据、搭建并训练卷积神经网络、测试网络效
果、查看卷积层学习到的特征