跳转至

18.1 实现颜色分类

18.1 实现颜色分类⚓︎

18.1.1 提出问题⚓︎

大家知道卷积神经网络可以在图像分类上发挥作用,而一般的图像都是彩色的,也就是说卷积神经网络应该可以判别颜色的。这一节中我们来测试一下颜色分类问题,也就是说,不管几何图形是什么样子的,只针对颜色进行分类。

先看一下样本数据,如图18-14。

图18-14 颜色分类样本数据图

在样本数据中,一共有6种颜色,分别是:

  • 红色 red
  • 绿色 green
  • 蓝色 blue
  • 青色(蓝+绿) cyan
  • 黄色(红+绿) yellow
  • 粉色(红+蓝) pink

而这6种颜色是分布在5种形状之上的:

  • 圆形
  • 菱形
  • 直线
  • 矩形
  • 三角形

我们看看神经网络能否排除形状的干扰,而单独把颜色区分开来。

18.1.2 用前馈神经网络解决问题⚓︎

数据处理⚓︎

由于输入图片是三通道的彩色图片,我们先把它转换成灰度图,

class GeometryDataReader(DataReader_2_0):
    def ConvertToGray(self, data):
        (N,C,H,W) = data.shape
        new_data = np.empty((N,H*W))
        if C == 3: # color
            for i in range(N):
                new_data[i] = np.dot(
                    [0.299,0.587,0.114], 
                    data[i].reshape(3,-1)).reshape(1,784)
        elif C == 1: # gray
            new_data[i] = data[i,0].reshape(1,784)
        #end if
        return new_data

向量[0.299,0.587,0.114]的作用是,把三通道的彩色图片的RGB值与此向量相乘,得到灰度图,三个因子相加等于1,这样如果原来是[255,255,255]的话,最后的灰度图的值还是255。如果是[255,255,0]的话,最后的结果是:

\begin{aligned} Y &= 0.299 \cdot R + 0.586 \cdot G + 0.114 \cdot B \\ &= 0.299 \cdot 255 + 0.586 \cdot 255 + 0.114 \cdot 0 \\ &=225.675 \end{aligned} \tag{1}

也就是说粉色的数值本来是(255,255,0),变成了单一的值225.675。六种颜色中的每一种都会有不同的值,所以即使是在灰度图中,也会保留部分“彩色”信息,当然会丢失一些信息。这从公式1中很容易看出来,假设B=0,不同组合的R、G的值有可能得到相同的最终结果,因此会丢失彩色信息。

在转换成灰度图后,立刻用reshape(1,784)把它转变成矢量,该矢量就是每个样本的784维的特征值。

搭建模型⚓︎

我们搭建的前馈神经网络模型如下:

def dnn_model():
    num_output = 6
    max_epoch = 100
    batch_size = 16
    learning_rate = 0.01
    params = HyperParameters_4_2(
        learning_rate, max_epoch, batch_size,
        net_type=NetType.MultipleClassifier,
        init_method=InitialMethod.MSRA,
        optimizer_name=OptimizerName.SGD)

    net = NeuralNet_4_2(params, "color_dnn")

    f1 = FcLayer_2_0(784, 128, params)
    net.add_layer(f1, "f1")
    r1 = ActivationLayer(Relu())
    net.add_layer(r1, "relu1")

    f2 = FcLayer_2_0(f1.output_size, 64, params)
    net.add_layer(f2, "f2")
    r2 = ActivationLayer(Relu())
    net.add_layer(r2, "relu2")

    f3 = FcLayer_2_0(f2.output_size, num_output, params)
    net.add_layer(f3, "f3")
    s3 = ClassificationLayer(Softmax())
    net.add_layer(s3, "s3")

    return net

这就是一个普通的三层网络,两个隐层,神经元数量分别是128和64,一个输出层,最后接一个6分类Softmax。

运行结果⚓︎

训练100个epoch后,得到如下损失函数图。

图18-15 训练过程中的损失函数值和准确度变化曲线

从损失函数曲线可以看到,此网络已经有些轻微的过拟合了,如果重复多次运行训练过程,会得到75%到85%之间的一个准确度值,并不是非常稳定,但偏差也不会太大,这与样本的噪音有很大关系,比如一条很细的红色直线,可能会给训练带来一些不确定因素。

最后我们考察一下该模型在测试集上的表现:

......
epoch=99, total_iteration=28199
loss_train=0.005832, accuracy_train=1.000000
loss_valid=0.593325, accuracy_valid=0.804000
save parameters
time used: 30.822062015533447
testing...
0.816

在图18-16的可视化结果,一共64张图,是测试集中1000个样本的前64个样本,每张图上方的标签是预测的结果。

图18-16 可视化结果

可以看到有很多直线的颜色被识别错了,比如最后一行的第1、3、5、6列,颜色错误。另外有一些大色块也没有识别对,比如第3行最后一列和第4行的头尾两个,都是大色块识别错误。也就是说,对两类形状上的颜色判断不准:

  • 很细的线
  • 很大的色块

这是什么原因呢?笔者分析:

  1. 针对细直线,由于带颜色的像素点的数量非常少,被拆成向量后,这些像素点就会在1x784的矢量中彼此相距很远,特征不明显,很容易被判别成噪音;
  2. 针对大色块,由于带颜色的像素点的数量非常多,即使被拆成向量,也会占据很大的部分,这样特征点与背景点的比例失衡,导致无法判断出到底哪个是特征点。

笔者认为以上两点是前馈神经网络在训练上的不稳定,以及最后准确度不高的主要原因。

当然有兴趣的读者也可以保留输入样本的三个彩色通道信息,把一个样本数据变成1x3x784=2352的向量进行试验,看看是不是可以提高准确率。

18.1.3 用卷积神经网络解决问题⚓︎

下面我们看看卷积神经网络的表现。我们直接使用三通道的彩色图片,不需要再做数据转换了。

搭建模型⚓︎

def cnn_model():
    num_output = 6
    max_epoch = 20
    batch_size = 16
    learning_rate = 0.1
    params = HyperParameters_4_2(
        learning_rate, max_epoch, batch_size,
        net_type=NetType.MultipleClassifier,
        init_method=InitialMethod.MSRA,
        optimizer_name=OptimizerName.SGD)

    net = NeuralNet_4_2(params, "color_conv")

    c1 = ConvLayer((3,28,28), (2,1,1), (1,0), params)
    net.add_layer(c1, "c1")
    r1 = ActivationLayer(Relu())
    net.add_layer(r1, "relu1")
    p1 = PoolingLayer(c1.output_shape, (2,2), 2, PoolingTypes.MAX)
    net.add_layer(p1, "p1") 

    c2 = ConvLayer(p1.output_shape, (3,3,3), (1,0), params)
    net.add_layer(c2, "c2")
    r2 = ActivationLayer(Relu())
    net.add_layer(r2, "relu2")
    p2 = PoolingLayer(c2.output_shape, (2,2), 2, PoolingTypes.MAX)
    net.add_layer(p2, "p2") 

    params.learning_rate = 0.1

    f3 = FcLayer_2_0(p2.output_size, 32, params)
    net.add_layer(f3, "f3")
    bn3 = BnLayer(f3.output_size)
    net.add_layer(bn3, "bn3")
    r3 = ActivationLayer(Relu())
    net.add_layer(r3, "relu3")

    f4 = FcLayer_2_0(f3.output_size, num_output, params)
    net.add_layer(f4, "f4")
    s4 = ClassificationLayer(Softmax())
    net.add_layer(s4, "s4")

    return net

表18-1展示了在这个模型中各层的作用和参数。

表18-1 模型各层的参数

ID 类型 参数 输入尺寸 输出尺寸
1 卷积 2x1x1, S=1 3x28x28 2x28x28
2 激活 Relu 2x28x28 2x28x28
3 池化 2x2, S=2, Max 2x14x14 2x14x14
4 卷积 3x3x3, S=1 2x14x14 3x12x12
5 激活 Relu 3x12x12 3x12x12
6 池化 2x2, S=2, Max 3x12x12 3x6x6
7 全连接 32 108 32
8 归一化 32 32
9 激活 Relu 32 32
10 全连接 6 32 6
11 分类 Softmax 6 6

为什么第一梯队的卷积用2个卷积核,而第二梯队的卷积核用3个呢?只是经过调参试验的结果,是最小的配置。如果使用更多的卷积核当然可以完成问题,但是如果使用更少的卷积核,网络能力就不够了,不能收敛。

运行结果⚓︎

经过20个epoch的训练后,得到的结果如图18-17。

图18-17 训练过程中的损失函数值和准确度变化曲线

以下是打印输出的最后几行:

......
epoch=19, total_iteration=5639
loss_train=0.005293, accuracy_train=1.000000
loss_valid=0.106723, accuracy_valid=0.968000
save parameters
time used: 17.295073986053467
testing...
0.963

可以看到我们在测试集上得到了96.3%的准确度,比前馈神经网络模型要高出很多,这也证明了卷积神经网络在图像识别上的能力。

图18-18是测试集中前64个测试样本的预测结果。

图18-18 测试结果

在这一批的样本中,只有左下角的一个绿色直线被预测成蓝色了,其它的没发生错误。

18.1.4 1x1卷积⚓︎

读者可能还记得在GoogLeNet的Inception模块中,有1x1的卷积核。这初看起来是一个非常奇怪的做法,因为1x1的卷积核基本上失去了卷积的作用,并没有建立在同一个通道上的相邻像素之间的相关性。

在本例中,为了识别颜色,我们也使用了1x1的卷积核,并且能够完成颜色分类的任务,这是为什么呢?

我们以三通道的数据举例。

图18-19 1x1卷积核的工作原理

假设有一个三通道的1x1的卷积核,其值为[1,2,-1],则相当于把每个通道的同一位置的像素值乘以卷积核,然后把结果相加,作为输出通道的同一位置的像素值。以左上角的像素点为例:

1 \times 1 + 1 \times 2 + 1 \times (-1)=2

相当于把上图拆开成9个样本,其值为:

[1,1,1] # 左上角点
[3,3,0] # 中上点
[0,0,0] # 右上角点
[2,0,0] # 左中点
[0,1,1] # 中点
[4,2,1] # 右中点
[1,1,1] # 左下角点
[2,1,1] # 下中点
[0,0,0] # 右下角点

上述值排成一个9行3列的矩阵,然后与一个3行1列的向量(1,2,-1)^T相乘,得到9行1列的向量,然后再转换成3x3的矩阵。当然在实际过程中,这个1x1的卷积核的数值是学习出来的,而不是人为指定的。

这样做可以达到两个目的:

  1. 跨通道信息整合
  2. 降维以减少学习参数

所以1x1的卷积核关注的是不同通道的相同位置的像素之间的相关性,而不是同一通道内的像素的相关性,在本例中,意味着它关心的彩色通道信息,通过不同的卷积核,把彩色通道信息转变成另外一种表达方式,在保留原始信息的同时,还实现了降维。

在本例中,第一层卷积如果使用3个卷积核,输出尺寸是3x28x28,和输入尺寸一样,达不到降维的作用。所以,一般情况下,会使用小于输入通道数的卷积核数量,比如输入通道为3,则使用2个或1个卷积核。在上例中,如果使用2个卷积核,则输出两张9x9的特征图,这样才能达到降维的目的。如果想升维,那么使用4个以上的卷积核就可以了。

18.1.5 颜色分类可视化解释⚓︎

在这里笔者根据自己的理解,解释一下针对这个颜色分类问题,卷积神经网络是如何工作的。

图18-20 颜色分类问题的可视化解释

如图18-20所示:

  1. 第一行是原始彩色图片,三通道28x28,特意挑出来都是矩形的6种颜色。

  2. 第二行是第一卷积组合梯队的第1个1x1的卷积核在原始图片上的卷积结果。由于是1x1的卷积核,相当于用3个浮点数分别乘以三通道的颜色值所得到和,只要是最后的值不一样就可以了,因为对于神经网络来说,没有颜色这个概念,只有数值。从人的角度来看,6张图的前景颜色是不同的(因为原始图的前景色是6种不同颜色)。

  3. 第三行是第一卷积组合梯队的第2个1x1的卷积核在原始图片上的卷积结果。与2相似,只不过3个浮点数的数值不同而已,也是得到6张前景色不同的图。

  4. 第四行是第二卷积组合梯队的三个卷积核的卷积结果图,把三个特征图当作RGB通道后所生成的彩色图。单独看三个特征图的话,人类是无法理解的,所以我们把三个通道变成假的彩色图,仍然可以做到6个样本不同色,但是出现了一些边框,可以认为是卷积层从颜色上抽取出的“特征”,也就是说卷积网络“看”到了我们人类不能理解的东西。

  5. 第五行是第二卷积组合梯队的激活函数结果,和原始图片相差很大。

如果用人类的视觉神经系统做类比,两个1x1的卷积核可以理解为两只眼睛上的视网膜上的视觉神经细胞,把彩色信息转变成神经电信号传入大脑的过程。最后由全连接层做分类,相当于大脑中的视觉知识体系。

回到神经网络的问题上,只要ReLU的输出结果中仍然含有“颜色”信息(用假彩色图可以证明这一点),并且针对原始图像中的不同的颜色,会生成不同的假彩色图,最后的全连接网络就可以有分辨的能力。

举例来说,从图18-20看,第一行的红色到了第五行变成了黑色,绿色变成了淡绿色,等等,是一一对应的关系。如果红色和绿色都变成了黑色,那么将分辨不出区别来。

代码位置⚓︎

ch18, Level1

思考与练习⚓︎

  1. 从彩色图转换成灰度图会损失一些信息,有可能会导致DNN准确度不高。请尝试用2352(=784x3)的矢量做为样本特征值,送入DNN进行训练。
  2. 从结果上看细线和大色块对DNN的影响较大,请尝试去掉细线样本,看看DNN的准确度是否可以提高。
  3. 读者可以尝试使用4个以上的1x1卷积核,看看是否能提高准确度。
  4. 读者可以尝试不使用第二层卷积,看看是否能完成任务。