卷积神经网络的实战 - 力战手写mnist数据集

写在前面

实战项目全是干货,因此我默认读者掌握了一些神经网络的基本原理,对于过程中的算法推导和一些基本思想我会一笔带过或者以注释的形式体现。希望大家能从这个实战项目上,学习到这类问题的方法和解题思想,按部就班的完成其实这个项目跟下来之后,是可以复用到其他同类型的项目的。LETSGO!


仅以此篇,送给海外留学的刘哥,祝勤勉好学,厚积薄发!

项目实战

运行环境:python3.8.5

调包

import pandas as pd   # pandas数据处理
import numpy as np    # linear algebra 线性代数
import matplotlib.pyplot as plt    # matlab绘图库在python下的应用
import seaborn as sns    # pyplot包的进一步封装
from sklearn.model_selection import train_test_split    # sklearn库中的训练集测试集切分工具
from sklearn.metrics import confusion_matrix    # 引入混淆矩阵
import itertools    # 迭代器

from tensorflow.keras.utils import to_categorical    # 独热编码
from tensorflow.keras.models import Sequential    # Keras的核心数据结构是model类型,Sequential是最简单的顺序模型
from tensorflow.keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPooling2D, BatchNormalization   # 构建卷积神经网络的类和方法
from tensorflow.keras.optimizers import SGD, Adam, RMSprop, Nadam    # SGD优化器,Adam优化器,RMSprop优化算法
from tensorflow.keras.preprocessing.image import ImageDataGenerator  # keras.preprocessing.image中的图片生成器
from tensorflow.keras.callbacks import ReduceLROnPlateau  # Keras回调函数,用于调整学习率参数,以合理速度加速收敛
import warnings   # 设置报警提醒

从kaggle上下载对应的数据集

这一步进行之前请先确认你的python环境里安装了kaggle,如果没有安装,请pip一下。

#kaggle上下载对应的数据集
import kaggle
# jupyter下运行命令行请在代码开头加一个'!'惊叹号
!kaggle competitions download -c digit-recognizer
#应用zipfile对下载的zip文件进行解压
import zipfile
fz = zipfile.ZipFile('digit-recognizer.zip','r')
for file in fz.namelist():
    fz.extract(file)

参数设置

# warning警告信息
warnings.filterwarnings("ignore")
# seaborn主题设置
sns.set(style = 'white',context = 'notebook',palette = 'deep')

定义函数(此处可复用)

  1. binary_pred_stats() 帮忙算几个分类器指标,可自行扩展
    投喂:
    ①训练集真实的y,
    ②训练集属性进入模型后输出的预测的y,
    ③可人为调节的分类器阈值
    吐出:TPR,TNR,Accuarcy
  2. plot_confusion_matrix() 帮忙画混淆矩阵
    投喂:
    ①sklearn.metrics中的confusion_matrix
    ②多分类器的目标分类数量
    ③三个写定的参数,normalize供你改变,缺省false;title供你改变标题内容缺省’confusion matrix’;cmap供你改变主题,缺省blues
    吐出:一张漂亮的混淆矩阵热力图
def binary_pred_stats(ytrue, ypred, threshold=0.5):
    '''
    此函数计算分类器统计量指标
    ytrue:真实的分类标签值 
    ypred:分类器输出的分类标签值 
    threshold:阈值设定参数,缺省值0.5 
    '''
    # 计算全部正例中预测为正例的个数
    one_correct = np.sum((ytrue==1)*(ypred > threshold))
    # 计算全部正例中预测为反例的个数
    zero_correct = np.sum((ytrue==0)*(ypred <= threshold))
    # 计算灵敏度TPR 描述识别出的所有正例占所有正例的比例
    sensitivity = one_correct / np.sum(ytrue==1) 
    # 计算特异度TNR,描述将负例识别为正例的情况占所有负例的比例 
    # 参考资料:https://www.jianshu.com/p/7919ef304b19
    specificity = zero_correct / np.sum(ytrue==0) 
    accuracy = (one_correct + zero_correct) / len(ytrue)
    return sensitivity, specificity, accuracy

def plot_confusion_matrix(cm, classes,  # 绘制混淆矩阵图
                          normalize=False,
                          title='Confusion matrix',
                          cmap=plt.cm.Blues):
    """
    此函数打印并绘制混淆矩阵。可以通过设置“normalize=True”来应用规范化。
    """
    plt.imshow(cm, interpolation='nearest', cmap=cmap)  # heatmap热力图
    plt.title(title) # title默认参数 "Confusion matrix"
    plt.colorbar() 
    tick_marks = np.arange(len(classes))
    plt.xticks(tick_marks, classes, rotation=45)
    plt.yticks(tick_marks, classes)

    if normalize:
        cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]

    thresh = cm.max() / 2.
    for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
        plt.text(j, i, cm[i, j],
                 horizontalalignment="center",
                 color="white" if cm[i, j] > thresh else "black")

    plt.tight_layout()
    plt.ylabel('True label')
    plt.xlabel('Predicted label')

加载数据集

# 加载数据集
df_train = pd.read_csv("train.csv")
df_test = pd.read_csv("test.csv")
# 输入数据需要进行转化,将数据结果转化为一个28*28个像素排布的图片,图片内容为0-9
# 从数据描述中,我们知道dataframe以第一列为目标,随后是784个像素值

数据探索过程

# 展现一部分数据,内容包含前五条记录和后五条记录
df_train.info()
pd.concat([df_train.head(5),df_train.tail(5)])

你可以看到整个数据集共有785个(28*28=784个像素点和1个图案标签,记录了手写数字的真实值)属性描述,包含一个label标签(这种标签是在进行有监督学习中必备的属性信息),以及784个(pixel0-pixel783)像素点属性信息。至于为什么像素点出现了大量的0,像素点数据数值范围0-255,没有手写痕迹的位置就是0.一个手写数字痕迹的方块图,一定是留白的地方多,所以有大量的0存在。

Y_train = df_train['label'] # 分离训练集中的标签(Y)部分
X_train = df_train.drop(labels = ["label"],axis = 1) # 分离训练集中的属性(X)部分
X_test = df_test # 提供的test集可以理解为样本外验证集
# 对训练集中的观测样本进行检查,如果检测到训练样本存在某类样本严重不足的情形下,可能需要过采样、欠采样技术
Y_train.hist(bins=np.arange(-0.5,9.51)) # 频数分布直方图,合理状态应该是服从均匀分布的

# 给图加上数据标签
x_tags = Y_train.value_counts().sort_index().index
y_tags = Y_train.value_counts().sort_index().values
for i,j in zip(x_tags,y_tags):
    plt.annotate(xy = (i-0.4,j),text = '{}'.format(j))

#我们需要整理X_train,然后才能对其使用CNN,需整理为28x28矩阵
#reshape(dim1, dim2, dim3, channels)
# 这里同时对给出的训练集和不含标签的样本外验证集整合为28*28的形式
X_train = X_train.values.reshape(-1,28,28,1)
X_test = X_test.values.reshape(-1,28,28,1)

input_shape = (28,28,1)

# # 从 [0:255] 归一化到 [0:1] 以获得更好的计算性能
X_train = X_train / 255.0
X_test = X_test / 255.0

# 现在我们需要通过独热编码将我们的标签 (0,1,2,...) 转换为类别。
Y_train = to_categorical(Y_train, num_classes = 10)

# 现在我们将我们的训练集分成一个实际的用于模型的训练集和一个测试集,分割比例0.85/0.15
X_train, X_val, Y_train, Y_val = train_test_split(X_train, Y_train, test_size = 0.15, random_state = 3) 

# 检查我们的预处理是否正常
plt.figure()
g = plt.imshow(X_train[2][:,:,0])
print(Y_train[10])

设计卷积神经网络 keras

# 定义CNN模型
model = Sequential()

# 卷积 - 池化 - 输出
# 卷积层设定,感受野filter定义为一个
model.add(Conv2D(filters = 128, kernel_size=(5, 5), activation='relu', padding='same', input_shape = input_shape))
model.add(BatchNormalization())# 层与层之间都要做批标准化处理,保持对激活函数的敏感程度
model.add(Conv2D(filters = 64, kernel_size=(5, 5), activation='relu', padding='same', input_shape = input_shape))
model.add(BatchNormalization()) # 设置多层卷积层捕获更深层次的特征信息
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
          
# 卷积 - 池化 - 输出
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2,2)))
model.add(Dropout(0.25))

model.add(Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(Dropout(0.25))

# 定义全连接层
model.add(Flatten())
model.add(Dense(256, activation='relu'))
model.add(BatchNormalization())
model.add(Dropout(0.25))

# 输出层
model.add(Dense(10, activation='softmax'))

# 设置交叉熵作为损失函数,优化器使用adam或RMSprop
optimizer = RMSprop(lr=0.001, rho=0.9, epsilon=1e-08, decay=0.0) # RMSprop
# optimizer = Adam(lr=0.005)  # ADAM 用的话和RMSprop相互注释即可,改写为class可直接加判断

model.compile(loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy'])
model.summary()

可以通过以下方式作为切入点,更好的优化训练模型的效果:

  1. 根据模拟退火的思想,逐步减少学习率,用于更快地找到局部最小值,从而更快地达到最优。
  2. 设置合理的Epochs和batch_size
  3. 数据增强方法可以增加用于学习训练模型的数据集,增加最后模型的鲁棒性

优化方法

# 1 更新学习率: keras中的回调
learning_rate_reduction = ReduceLROnPlateau(monitor='val_acc',  # 决定是否更新学习率的观测指标,这里选择测试集(忍不住吐槽:validation频繁被译为验证集,但一般只会把不含y标签的部分叫做样本外验证集) validation 准确率 accuracy
                                            patience=3, # 设置多少轮学习无进步后就开始下调学习率
                                            factor=0.5, # 学习率下降比例 新的学习率= 旧学习率 * factor值
                                            min_lr=0.00001 # 最低学习率限度
                                           )

# 2 重设Epochs和batchsize (经验论 + 测试调优)
epochs = 50
batch_size = 128

# 3 人为扩充数据以训练模型,定义一个图像数据的生成器,下面是生成新图像数据的规则
datagen = ImageDataGenerator(
        featurewise_center=False,  # set input mean to 0 over the dataset
        samplewise_center=False,  # set each sample mean to 0
        featurewise_std_normalization=False,  # divide inputs by std of the dataset
        samplewise_std_normalization=False,  # divide each input by its std
        zca_whitening=False,  # apply ZCA whitening
        rotation_range=10,  # randomly rotate images in the range (degrees, 0 to 180)
        zoom_range = 0.10, # Randomly zoom image 
        width_shift_range=0.1,  # randomly shift images horizontally (fraction of total width)
        height_shift_range=0.1,  # randomly shift images vertically (fraction of total height)
        horizontal_flip=False,  # randomly flip images
        vertical_flip=False)  # randomly flip images


datagen.fit(X_train)

人为增添一些数据特性:

  • 将一些训练图像随机旋转 10 度
  • 将一些训练图像随机放大 10%
  • 将图像水平随机移动 10% 的宽度
  • 将图像垂直随机移动 10% 的高度

为什么有这样的自信,人为制造一些数据,施加一些干扰不会对模型性能产生反作用么?
卷积神经网络本身是根据小的感受野提取关键信息,在通过池化层的方法整合过滤得到关键信息,图片具有什么性质呢?图片本身每个像素间都是有关联的,这也是图片能够传递信息的原因。只有图像本身有内涵有意义,图像才有信息,有信息也就有了价值。像素间的颜色差异,对于距离近的部分总是强关联的,而距离越远的两个像素间的关联性越差,反应到图像现实上就表现为强烈的色差。我们对图像中的整体进行旋转,平移,镜像(一般二维图形不用)等操作,就是为了强化图像的整体识别度。

基础数据的像素点位虽然发生了变化,但池化后他们中的绝大多数总是还会映射到原来的水平。经过了调整之后,那些歪歪扭扭的,或者是经过不等比例长宽放缩的图像也更容易被识别出,因为我们在训练时就模拟出了类似的情况,并且让模型对于这种变形后的图像有了认知。(将图片各种变形后,标签值仍然是存在的)

%%time
'''
estimator = model.fit(X_train, Y_train, 
                      validation_data=(X_val, Y_val),
                      epochs=50, 
                      batch_size=50,
                      verbose=0)
'''
# 对于大批量的数据生成,fit函数一次性无法加载全部的数据,
# 结合上面定义的图像生成器,人为构建一些新数据喂入模型用于训练。
# 使用切割出的测试集部分返回验证误差
estimator = model.fit_generator(datagen.flow(X_train,Y_train, batch_size=batch_size),
                              epochs = epochs, validation_data = (X_val,Y_val),
                              verbose = 2, steps_per_epoch=X_train.shape[0] // batch_size
                              , callbacks=[learning_rate_reduction])

监测训练优化过程中的误差变化

# 模型训练过程的误差变化(基于选择的学习率优化器)
plt.plot(estimator.history['loss'])   # 
plt.plot(estimator.history['val_loss'])
plt.title('Model training')
plt.ylabel('training error')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc=0)
plt.show()

模型评价指标

# 基于训练集结果的预测和结果
predtrain = model.predict(X_train)
sensitivity, specificity, accuracy = binary_pred_stats(Y_train, predtrain)
print("train set:", sensitivity, specificity, accuracy)

# 获取测试集结果的预测和结果
predtest = model.predict(X_val)
sensitivity, specificity, accuracy = binary_pred_stats(Y_val, predtest)
print("test set: ", sensitivity, specificity, accuracy)

混淆矩阵输出

# 计算混淆矩阵并输出
Y_pred = model.predict(X_val)
Y_pred_classes = np.argmax(Y_pred,axis = 1) 
Y_true = np.argmax(Y_val,axis = 1) 
confusion_mtx = confusion_matrix(Y_true, Y_pred_classes) 
plot_confusion_matrix(confusion_mtx, classes = range(10)) 

存储验证结果,返回业务数仓用于标签化和BI分析

# 训练好的模型喂入样本外验证集,这个数据集是不包含Y标签的
results = model.predict(X_test)
results = np.argmax(results,axis = 1)
results = pd.Series(results,name="Label")

submission = pd.concat([pd.Series(range(1,28001),name = "ImageId"),results],axis = 1)
submission.to_csv("cnn_mnist_with_datagen.csv",index=False)

你可能感兴趣的:(TD_BrainStrom,cnn,python,人工智能,深度学习,神经网络)