1.Lenet结构
对于Mnist 28x28的输入数据,Lenet网络的结构为:
2. MNIST数据集
MNIST原始数据集的读取可以看我这篇博客https://blog.csdn.net/SZU_Kwong/article/details/107900629
原始数据一张图像以行向量存储,为了和网络输入格式统一,将MNIST数据集改为张量格式存储
def MNIST_to_Tensor():
"""
将以向量保存的MNIST转换为以张量保存,方便CNN模型读入
"""
data = sio.loadmat('MNIST.mat')
img_train_temp = data['img_train']
img_t10k_temp = data['img_t10k']
label_train = data['label_train']
label_t10k = data['label_t10k']
img_train = []
img_t10k = []
for i in range(60000):
img = img_train_temp[i, :].reshape(28, 28)
img_train.append(img)
for i in range(10000):
img = img_t10k_temp[i, :].reshape(28, 28)
img_t10k.append(img)
img_train = np.asarray(img_train)
img_t10k = np.asarray(img_t10k)
sio.savemat('MNIST_Tensor.mat', {'img_train':img_train, 'img_t10k':img_t10k, 'label_train':label_train, 'label_t10k':label_t10k})
3. Lenet卷积层分析
笔者使用keras来构造模型以及卷积层的分析函数
Lenet的类代码为:
import os
# 初始化tensorflow时不显示日志信息
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '1'
import tensorflow as tf
tf.compat.v1.disable_v2_behavior() # 这个用于滤波器可视化时让tf2与tf1的静态图兼容,训练时需要注释掉!
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras import models
from tensorflow.keras import backend as K
from tensorflow.keras.models import Sequential
from tensorflow.keras.utils import plot_model
import matplotlib.pyplot as plt
import numpy as np
# 控制显存最大用量为40%
from tensorflow.compat.v1 import ConfigProto
from tensorflow.compat.v1 import InteractiveSession
config = ConfigProto()
config.gpu_options.per_process_gpu_memory_fraction = 0.4
config.gpu_options.allow_growth = True
session = InteractiveSession(config=config)
class LeNet():
def __init__(self):
self.model = Sequential([
layers.Conv2D(6, (5, 5), activation='relu', input_shape=(28, 28, 1), name='conv1'),
layers.AveragePooling2D((2, 2), strides=2, name='pool1'),
layers.Conv2D(16, (5, 5), activation='relu', name='conv2'),
layers.AveragePooling2D((2, 2), strides=2, name='pool2'),
layers.Flatten(name='flatten'),
layers.Dense(120, activation='relu', name='fc1'),
layers.Dense(84, activation='relu', name='fc2'),
layers.Dense(10, activation='softmax', name='output')
])
def summary(self):
self.model.summary()
def fit(self, X, y, batch_size=32, epochs=20, validation_split=0.2, show_his=True):
"""
拟合模型
args:
X -- 训练样本 shape=(samples, features)
y -- 标签 shape=(samples) 直接给出类别标号就像,不需要转换成onehot编码
batch_size -- 批量下降一批数量
epochs -- 训练轮次
validation_split -- 验证集占比
show_his -- 是否以图表显示模型训练过程结果
"""
self.model.compile(
optimizer='rmsprop',
loss=keras.losses.SparseCategoricalCrossentropy(),
metrics=['accuracy']
)
history = self.model.fit(X, y, batch_size=batch_size, epochs=epochs, validation_split=validation_split)
if show_his:
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(acc)+1)
plt.plot(epochs, acc, 'bo', label='Training_acc')
plt.plot(epochs, val_acc, 'b', label='Val_acc')
plt.title('Training and Validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training_acc')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()
def evaluate(self, X, y):
"""
评估数据集
args:
X -- 训练样本 shape=(samples, features)
y -- 标签 shape=(samples) 直接给出类别标号就像,不需要转换成onehot编码
returns:
loss -- 数据集损失
acc -- 数据集精度
"""
loss, acc = self.model.evaluate(X, y)
return loss, acc
def predict(self, X):
"""
预测数据
args:
X -- 数据样本 shape=(samples, features)
return:
预测结果
"""
return self.model.predict(X)
def save_weights(self, path):
"""
保存模型权重
args:
path -- 保存路径
"""
if path[-3:] != '.h5':
path = path + '.h5'
self.model.save_weights(path)
def load_weights(self, path):
"""
加载模型权重
path -- 模型存放路径
"""
if path[-3:] != '.h5':
path = path + '.h5'
self.model.load_weights(path)
def plot(self, path):
"""
绘制模型
args:
path -- 模型结果图片保存路径
"""
plot_model(self.model, show_shapes=True, to_file=path)
def activation_visualize(self, img):
"""
可视化输入图像在卷积层的激活
args:
img -- 输入图像
"""
img = img.reshape((1, 28, 28, 1))
layer_outputs = [layer.output for layer in self.model.layers[:3]]
activation_model = models.Model(inputs=self.model.input, outputs=layer_outputs)
activations = activation_model.predict(img)
activation_conv1 = activations[0]
activation_conv2 = activations[2]
_, axs = plt.subplots(2, 3)
for i in range(2):
for j in range(3):
axs[i, j].imshow(activation_conv1[0, :, :, 3*i+j])
plt.figure(1)
_, axs = plt.subplots(4, 4)
for i in range(4):
for j in range(4):
axs[i, j].imshow(activation_conv2[0, :, :, 4*i+j])
plt.show()
def filters_visualize(self):
"""
生成滤波器最大响应并可视化
"""
_, axs = plt.subplots(2, 3)
conv1_output = self.model.get_layer('conv1').output
for i in range(2):
for j in range(3):
idx = 3*i+j
loss = K.mean(conv1_output[:, :, :, idx])
# 计算损失相对于输入图像的梯度
grads = K.gradients(loss, self.model.input)[0]
# 梯度标准化
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)
# 返回给定输入图像的损失和梯度
iterate = K.function([self.model.input], [loss, grads])
# 从带有噪声的灰度图像开始
input_img_data = np.random.random((1, 28, 28, 1)) * 20 + 128.
# 运行40次梯度上升
step = 1
for k in range(40):
loss_value, grads_value = iterate([input_img_data])
input_img_data += grads_value * step
img = input_img_data[0]
# 对张量做标准化,使得均值为0,标准差为0.1
img -= img.mean()
img /= (img.std() + 1e-5)
img *= 0.1
# 将张量裁切到[0,1]
img += 0.5
img = np.clip(img, 0, 1)
img *= 255
img = np.clip(img, 0, 255).astype('uint8')
# 显示滤波器模式
axs[i, j].imshow(img[: ,:, 0])
_, axs = plt.subplots(4, 4)
conv2_output = self.model.get_layer('conv2').output
for i in range(4):
for j in range(4):
idx = 4*i+j
loss = K.mean(conv2_output[:, :, :, idx])
# 计算损失相对于输入图像的梯度
grads = K.gradients(loss, self.model.input)[0]
# 梯度标准化
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)
# 返回给定输入图像的损失和梯度
iterate = K.function([self.model.input], [loss, grads])
# 从带有噪声的灰度图像开始
input_img_data = np.random.random((1, 28, 28, 1)) * 20 + 128.
# 运行40次梯度上升
step = 1
for k in range(40):
loss_value, grads_value = iterate([input_img_data])
input_img_data += grads_value * step
img = input_img_data[0]
# 对张量做标准化,使得均值为0,标准差为0.1
img -= img.mean()
img /= (img.std() + 1e-5)
img *= 0.1
# 将张量裁切到[0,1]
img += 0.5
img = np.clip(img, 0, 1)
img *= 255
img = np.clip(img, 0, 255).astype('uint8')
# 显示滤波器模式
axs[i, j].imshow(img[: ,:, 0])
plt.show()
if __name__ == "__main__":
pass
由于使用的是tensorflow2.3,在使用Keras的微分功能时,它需要在静态图中运行,而tensorflow2.x使用的是动态图,因此在对滤波器可视化时使用tf.compat.v1.disable_v2_behavior()来获得兼容
我们先对模型进行训练并保存模型:
tdata = sio.loadmat('MNIST_Tensor')
x_train = tdata['img_train'].reshape(60000, 28, 28, 1) / 255
x_test = tdata['img_t10k'].reshape(10000, 28, 28, 1) / 255
y_train = tdata['label_train']
y_test = tdata['label_t10k']
model = LeNet()
model.fit(x_train, y_train, validation_split=0, epochs=10, show_his=False)
model.evaluate(x_test, y_test)
model.save_weights('./models/mnist_Lenet')
我们先来看看图像在卷积层的激活,在tensorflow中,模型的每一层都是可以单独输出的,因此我们可以在训练好的模型上,通过输入一张图像进行前向传播,以获得图像在模型各层的激活,我们找来训练集的第一张图像——数字5:
LeNet有两个卷积层,第一个卷积层有6个滤波器,第二个卷积层有16个滤波器,我们来看看上面这张图片经过卷积层的各个滤波器后会被提取出什么特征:
def show_Lenet_conv_activations():
"""
对于一副输入图像,展示其在Lenet两个卷积层的激活
"""
data = sio.loadmat('MNIST_Tensor.mat')['img_train']
img = data[0, ...]
model = LeNet()
model.load_weights('./models/mnist_Lenet')
model.activation_visualize(img)
CNN网络中,低层的卷积核用于提取图像的低级特征,高级的卷积核用于将低级特征结合起来,提取更高级的特征,上面6幅图像是输入图片经过第一层卷积层后的输出,假设左上角为(0,0),右下角为(1,2),则可以发现,滤波器(0,0)似乎并没有过滤出某种特征,滤波器(0,1)像是一个横向线条的提取器,滤波器(0,2)像是一个45°方向线条的检测器,滤波器(1,0)像是一个135°方向线条的检测器,滤波器(1,1)是一个边缘探测器,因为图像经过这个滤波器后输出了图像的大致轮廓,至于最后一个滤波器(1,2),它更像一个角点检测器,因为它将数字的转折点提取了出来。
在经过第二层卷积层后,图像的激活为:
可以发现,经过第二次卷积层后,图像的特征变得更加抽象,我们比较难描述某个滤波器具体提取了什么高级的特征。如果要猜测的话,可以猜滤波器(0,3)在关注斜向下的线条,而滤波器(0,1)和(0,2)在关注斜向上的线条
在看完某张输入图像在各个卷积层的激活后,我们再来看看两个卷积层的各个滤波器对应的最大响应是什么,方法是通过再输入空间中进行梯度上升:从空白输入图像开始,将梯度下降应用于卷积神经网络输入图像的值,其目的是让某个过滤器的响应最大化。得到的输入图像是选定过滤器具有最大响应的图像。要实现这个过程,我们需要构建一个损失函数,其目的是让某个卷积层的某个过滤器的值最大化,我们将损失函数值定义为某个卷积层中某个滤波器参数的平均值,然后,我们使用随机梯度下降来调节输入图像的值,以便让激活值最大化。这个过程可以通过Keras的backend模块中的gradients函数来实现:
def show_Lenet_conv_fileters():
"""
展示Lenet两个卷积层的最大响应
"""
model = LeNet()
model.load_weights('./models/mnist_Lenet')
model.filters_visualize()
最后我们得到两个卷积层各个滤波器的最大响应为:
可以发现,第一卷积层的过滤器可以对于简单的边缘方向,第二个卷积层对应了更精细的纹理。大部分的滤波器响应的是波尔卡点图案