不怕学不会 使用TensorFlow从零开始构建卷积神经网络

人们可以使用TensorFlow的所有高级工具如tf.contrib.learn和Keras,能够用少量代码轻易的建立一个卷积神经网络。但是通常在这种高级应用中,你不能访问代码中的部分内容,对深层次的原理缺乏理解。

在本教程中,我将介绍如何从零开始使用底层的TensorFlow构建卷积神经网络,并使用TensorBoard可视化我们的函数图像和网络性能。本教程需要你了解神经网络的一些基础知识。在整篇文章中,我还将把卷积神经网络的每一步都分解为绝对的基础知识,以便你可以充分理解图中每一步发生的情况。通过从头开始构建这个模型,你可以轻松将图形的不同方面可视化,这样你就可以看到每个卷积层并使用它们进行自己的推论。我只会着重讲代码的重要的部分,想获取要详细代码和注释,请访问下方链接。

https://github.com/wagonhelm/Visualizing-Convnets/blob/master/visualizingConvnets. ipynb

收集数据集

首先,我必须决定使用哪个图像数据集。我决定使用牛津大学Visual Geometry Group的宠物数据集。我选择这个数据集有几个原因:这个数据集比较简单,并且标记得很好,它有适当数量的训练数据,如果我接下来想训练一个检测模型,它也有可用包围盒。或者你也可以使用我在Kaggle上找到的Simpsons数据集,它包含大量可用来训练的简单数据。

选择一个模型

接下来,我要选择卷积神经网络模型。比较流行的模型是GoogLeNet或VGG16,它们都具有多重卷积,可用于用于检测ImageNet中1000种数据集的图像。我决定建立一个简单的四层卷积网络:

让我们详细讲解这个模型的构成,根据前三个通道被卷积到32个特征映射。我们接下来卷积这组32个特征映射组成另外32个特征。然后将其池化为一个112x112x32的图像,然后我们卷积64特征映射两次之后,最终池化为56x56x64。这个最后的池化层的每个单元然后被全连接到512个神经元,然后根据类的数量接通softmax层。

处理和建立一个数据集

首先,让我们开始加载我们的依赖项,其中包括用于处理图像数据的函数imFunctions。

import imFunctions as imf
import tensorflow as tf
import scipy.ndimage
from scipy.misc import imsave
import matplotlib.pyplot as plt
import numpy as np

使用imFunctions下载和提取图片。

imf.downloadImages('annotations.tar.gz', 19173078)
imf.downloadImages('images.tar.gz', 791918971)
imf.maybeExtract('annotations.tar.gz')
imf.maybeExtract('images.tar.gz')

然后,我们可以将图像分类到单独的文件夹中,包括训练和测试文件夹。sortImages函数中的数字表示你想从训练数据中分离出测试数据的百分比。

imf.sortImages(0.15)

然后,我们可以将数据集构建为一个numpy数组,其中对应的独热向量表示我们的类。当构建convnet时,这也将减少所有的训练和测试图像中的图像均值。这个函数会问你想要包含哪些类,由于我的GPU内存有限(3GB),我选择了一个非常小的数据集,试图区分两种狗:柴犬和萨摩耶。

train_x, train_y, test_x, test_y, classes, classLabels = imf.buildDataset()

卷积和汇集如何工作

现在我们有了一套数据集,让我们稍微回顾一下,看看卷积的工作原理。在学习彩色卷积滤波器之前,让我们看一下灰度图。我们来做一个7×7的滤波器,应用四个不同的特征映射。TensorFlow的conv2d功能相当简单,并有四个变量:input,filter,strides和padding。在TensorFlow网站上,他们描述的conv2d功能如下:

计算给定四维输入和滤波张量的二维卷积。 给定形状的输入张量[batch,in_height,in_width,in_channels]和形状为[filter_height,filter_width,in_channels,out_channels]的滤波器/内核张量。

由于我们正在处理的是灰度图,因此in_channels是1,因为我们应用了四个滤波器,所以out_channels是4.我们将以下四个滤波器/内核应用于我们的一个图像(或者说batch为1):

让我们看看滤波器如何影响我们的灰度图像输入。

gray = np.mean(image,-1)
X = tf.placeholder(tf.float32, shape=(None, 224, 224, 1))
conv = tf.nn.conv2d(X, filters, [1,1,1,1], padding="SAME")
test = tf.Session()
test.run(tf.global_variables_initializer())
filteredImage = test.run(conv, feed_dict={X: gray.reshape(1,224,224,1)})
tf.reset_default_graph()

这将返回一个(1,224,224,4)的4维张量,我们可以用它来可视化四个滤波器:

很明显看到滤波器内核的卷积非常强大。为了将其分解,我们的7×7内核每次每步跨越图像49个像素,然后将每个像素的值乘以每个内核值,然后将所有49个值加在一起以构成一个像素。(如果你仍然不理解图像滤波的核心,访问链接http://setosa.io/ev/image-kernels/)

本质上,大多数卷积神经网络都是由卷积和池化构成的。被用于卷积最常见的是3×3内核滤波器。特别是,带有2×2的步幅和2×2的池化核大小的最大池化是一种非常激进的方法,可以根据在内核中的最大像素值缩小图片尺寸。以下为它的示例。

现在,对于两个conv2d和最大池化,有两个选项可以选择填充:“VALID”,这将缩小输入和“SAME”,通过在输入边缘周围添加零来保持输入大小。这里是一个3×3内核的最大池化的示例,使用1×1的步幅来比较填充选项:

创建ConvNet

现在我们已经介绍了所有的基础知识,可以开始构建自己的卷积神经网络模型了。我们可以从占位符开始。X将是我们的输入占位符,我们将把我们的图像馈入,Y_是一组图像的真实类别。

X = tf.placeholder(tf.float32, shape=(None, 224, 224, 3))
Y_ = tf.placeholder(tf.float32, [None, classes])
keepRate1 = tf.placeholder(tf.float32)
keepRate2 = tf.placeholder(tf.float32)

我们将在一个范围内为每个过程创建所有部分。Scope对于在TensorBoard中可视化图形是非常有用的,因为它们将所有东西都组合成一个可扩展的对象。我们创建了第一组内核大小为3×3的滤波器,这个滤波器需要三个通道并输出32个滤波器。这意味着对于32个滤波器中的每一个,R,G和B通道将会有3×3的内核权重。我们的滤波器的权重值使用截尾正态分布初始化非常重要,所以我们有多个随机滤波器,使TensorFlow适应我们的模型。

# CONVOLUTION 1 - 1
with tf.name_scope('conv1_1'):
    filter1_1 = tf.Variable(tf.truncated_normal([3, 3, 3, 32], dtype=tf.float32,
                            stddev=1e-1), name='weights1_1')
    stride = [1,1,1,1]
    conv = tf.nn.conv2d(X, filter1_1, stride, padding='SAME')
    biases = tf.Variable(tf.constant(0.0, shape=[32], dtype=tf.float32),
                         trainable=True, name='biases1_1')
    out = tf.nn.bias_add(conv, biases)
    conv1_1 = tf.nn.relu(out)

在第一次卷积conv1_1结束时,我们使用relu进行处理,它将每个负数归零。然后,我们将这32个特征卷积到另外的32个特征中。可以看到,conv2d将输入赋值为第一个卷积层的输出。

# CONVOLUTION 1 - 2
with tf.name_scope('conv1_2'):
    filter1_2 = tf.Variable(tf.truncated_normal([3, 3, 32, 32], dtype=tf.float32,
                                                stddev=1e-1), name='weights1_2')
    conv = tf.nn.conv2d(conv1_1, filter1_2, [1,1,1,1], padding='SAME')
    biases = tf.Variable(tf.constant(0.0, shape=[32], dtype=tf.float32),
                         trainable=True, name='biases1_2')
    out = tf.nn.bias_add(conv, biases)
    conv1_2 = tf.nn.relu(out)

然后,我们把图像缩小一半。

# POOL 1
with tf.name_scope('pool1'):
    pool1_1 = tf.nn.max_pool(conv1_2,
                             ksize=[1, 2, 2, 1],
                             strides=[1, 2, 2, 1],
                             padding='SAME',
                             name='pool1_1')
    pool1_1_drop = tf.nn.dropout(pool1_1, keepRate1)

最后一部分涉及在池层上使用dropout(我们稍后详细介绍)。然后我们再来两次卷积,这样就有64个特征和另一个池。请注意,第一个卷积必须将先前的32个特征通道转换为64。

# CONVOLUTION 2 - 1
with tf.name_scope('conv2_1'):
    filter2_1 = tf.Variable(tf.truncated_normal([3, 3, 32, 64], dtype=tf.float32,
                                                stddev=1e-1), name='weights2_1')
    conv = tf.nn.conv2d(pool1_1_drop, filter2_1, [1, 1, 1, 1], padding='SAME')
    biases = tf.Variable(tf.constant(0.0, shape=[64], dtype=tf.float32),
                         trainable=True, name='biases2_1')
    out = tf.nn.bias_add(conv, biases)
    conv2_1 = tf.nn.relu(out)
    
# CONVOLUTION 2 - 2
with tf.name_scope('conv2_2'):
    filter2_2 = tf.Variable(tf.truncated_normal([3, 3, 64, 64], dtype=tf.float32,
                                                stddev=1e-1), name='weights2_2')
    conv = tf.nn.conv2d(conv2_1, filter2_2, [1, 1, 1, 1], padding='SAME')
    biases = tf.Variable(tf.constant(0.0, shape=[64], dtype=tf.float32),
                         trainable=True, name='biases2_2')
    out = tf.nn.bias_add(conv, biases)
    conv2_2 = tf.nn.relu(out)
    
# POOL 2
with tf.name_scope('pool2'):
    pool2_1 = tf.nn.max_pool(conv2_2,
                             ksize=[1, 2, 2, 1],
                             strides=[1, 2, 2, 1],
                             padding='SAME',
                             name='pool2_1')
    pool2_1_drop = tf.nn.dropout(pool2_1, keepRate1)

接下来,我们创建一个512个神经元的完全连接层,它将为我们的56x56x64的pool2_1层的每个像素建立一个权重连接。这是超过1亿不同的权重值!为了计算我们完全连接网络,我们必须把输入降至一维,然后乘以权重,再加上偏置。

#FULLY CONNECTED 1
with tf.name_scope('fc1') as scope:
    shape = int(np.prod(pool2_1_drop.get_shape()[1:]))
    fc1w = tf.Variable(tf.truncated_normal([shape, 512], dtype=tf.float32,
                                           stddev=1e-1), name='weights3_1')
    fc1b = tf.Variable(tf.constant(1.0, shape=[512], dtype=tf.float32),
                       trainable=True, name='biases3_1')
    pool2_flat = tf.reshape(pool2_1_drop, [-1, shape])
    out = tf.nn.bias_add(tf.matmul(pool2_flat, fc1w), fc1b)
    fc1 = tf.nn.relu(out)
    fc1_drop = tf.nn.dropout(fc1, keepRate2)

最后,我们得到带有关联权重和偏置的softmax,最后输出Y。

#FULLY CONNECTED 3 & SOFTMAX OUTPUT
with tf.name_scope('softmax') as scope:
    fc2w = tf.Variable(tf.truncated_normal([512, classes], dtype=tf.float32,
                                           stddev=1e-1), name='weights3_2')
    fc2b = tf.Variable(tf.constant(1.0, shape=[classes], dtype=tf.float32),
                       trainable=True, name='biases3_2')
    Ylogits = tf.nn.bias_add(tf.matmul(fc1_drop, fc2w), fc2b)
    Y = tf.nn.softmax(Ylogits)

创建损失和优化

现在,我们可以开始开发我们模型的训练方面。首先,我们必须决定批处理大小;我不能使用超过10个,因为GPU内存不足。然后,我们必须决定训练次数,即算法循环遍历所有训练数据的次数,最后决定我们的学习速率alpha。

numEpochs = 400
batchSize = 10
alpha = 1e-5

然后,我们为交叉熵,准确性检查和反向传播优化创建范围。

with tf.name_scope('cross_entropy'):
    cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=Ylogits, labels=Y_)
    loss = tf.reduce_mean(cross_entropy)

with tf.name_scope('accuracy'):
    correct_prediction = tf.equal(tf.argmax(Y, 1), tf.argmax(Y_, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

with tf.name_scope('train'):
    train_step = tf.train.AdamOptimizer(learning_rate=alpha).minimize(loss)

然后,我们可以创建我们的会话并初始化所有的变量。

sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init)

为TensorBoard创建摘要

现在,我们也要使用TensorBoard,这样我们可以看到我们的分类器的工作效果。我们将创建两个图:一个用于我们的训练集,一个用于我们的测试集。我们可以通过使用add_graph函数来显示我们的图形网络。我们将使用摘要标量来衡量我们的总体损失和准确性,将我们的摘要合并到一起,这样我们只需要调用write_op记录我们的标量。

writer_1 = tf.summary.FileWriter("/tmp/cnn/train")
writer_2 = tf.summary.FileWriter("/tmp/cnn/test")
writer_1.add_graph(sess.graph)
tf.summary.scalar('Loss', loss)
tf.summary.scalar('Accuracy', accuracy)
tf.summary.histogram("weights1_1", filter1_1)
write_op = tf.summary.merge_all()

训练模型

然后我们可以编码进行评估和训练。我不希望每步都记录损失和准确性,因为这会大大减慢分类器的速度。所以,我们每五步记录一次。

steps = int(train_x.shape[0]/batchSize)

for i in range(numEpochs):
    accHist = []
    accHist2 = []
    train_x, train_y = imf.shuffle(train_x, train_y)
    for ii in range(steps):
        #Calculate our current step
        step = i * steps + ii
        #Feed forward batch of train images into graph and log accuracy
        acc = sess.run([accuracy], feed_dict={X: train_x[(ii*batchSize):((ii+1)*batchSize),:,:,:], Y_: train_y[(ii*batchSize):((ii+1)*batchSize)], keepRate1: 1, keepRate2: 1})
        accHist.append(acc)
        
        if step % 5 == 0:
            # Get Train Summary for one batch and add summary to TensorBoard
            summary = sess.run(write_op, feed_dict={X: train_x[(ii*batchSize):((ii+1)*batchSize),:,:,:], Y_: train_y[(ii*batchSize):((ii+1)*batchSize)], keepRate1: 1, keepRate2: 1})
            writer_1.add_summary(summary, step)
            writer_1.flush()
            
            # Get Test Summary on random 10 test images and add summary to TensorBoard
            test_x, test_y = imf.shuffle(test_x, test_y)
            summary = sess.run(write_op, feed_dict={X: test_x[0:10,:,:,:], Y_: test_y[0:10], keepRate1: 1, keepRate2: 1})
            writer_2.add_summary(summary, step)
            writer_2.flush()

        #Back propigate using adam optimizer to update weights and biases.
        sess.run(train_step, feed_dict={X: train_x[(ii*batchSize):((ii+1)*batchSize),:,:,:], Y_: train_y[(ii*batchSize):((ii+1)*batchSize)], keepRate1: 0.2, keepRate2: 0.5})
    
    print('Epoch number {} Training Accuracy: {}'.format(i+1, np.mean(accHist)))
    
    #Feed forward all test images into graph and log accuracy
    for iii in range(int(test_x.shape[0]/batchSize)):
        acc = sess.run(accuracy, feed_dict={X: test_x[(iii*batchSize):((iii+1)*batchSize),:,:,:], Y_: test_y[(iii*batchSize):((iii+1)*batchSize)], keepRate1: 1, keepRate2: 1})
        accHist2.append(acc)
    print("Test Set Accuracy: {}".format(np.mean(accHist2)))

可视化图表

在训练中,通过在终端中激活TensorBoard来检查TensorBoard结果。

tensorboard --logdir="/tmp/cnn/"

然后,我们可以将我们的Web浏览器指向默认的TensorBoard地址http://0.0.0.0/6006。我们先来看看我们的图表模型。

正如你所看到的,通过使用范围,我们得到了完美的可视化的图形。

测试性能

让我们来看看准确性和损失的标量历史记录。

你可能发现,这个图反应了一个很大的问题。我们的训练数据,分类器获得了100%的准确性和0损失,但是我们的测试数据最多只能达到80%的准确性,损失也很大。这是典型的过拟合现象,可能的原因包括没有足够的训练数据或神经元过多。

我们可以通过调整,缩放和旋转我们的训练数据来创建更多的训练数据,但是更简单的方法是添加dropout到池化和完全连接的层的输出中。这将使每个训练步骤完全切割,在层中随机丢弃一部分神经元。这将迫使我们的分类器每次只训练一小组神经元,而非整个集合。这让神经元专注于特定的任务。剔除80%的卷积层和50%完全连接层效果非常好。

通过减少神经元,我们能够达到90%的测试准确性,几乎是10%的性能增长!但缺点是分类器花了大约6倍的时间来训练。

可视化进化滤波器

为了好玩,每50个训练步骤,我通过滤波器传递一个图像,把滤波器的权重进化做成一个gif。它的效果很酷,并我们可以通过它对卷积网络的工作方式有更好的认识。以下是两个滤波器来自conv1_2:

你可以看到最初的权重初始化显示了图像很多细节,但随着时间的推移权重更新,他们变得更加专注于检测某些边缘。令我吃惊的是,我发现第一个卷积核filter1_1几乎没有变化。似乎初始权重初始化本身已经足够好了。我们继续深入,在conv2_2中你可以看到它开始检测更抽象和普遍的特征。

总而言之,使用少于400个训练图像进行训练,训练后准确性几乎可以达到90%,这给我留下了深刻的印象。我相信,如果有更多的训练数据,更多的超参数调整,我可以取得更好的结果。

这篇文章总结了如何使用TensorFlow从零开始创建卷积神经网络,以及如何从TensorBoard获取推论以及如何使我们的滤波器可视化。重点记住,使用少量数据制作分类器时,更容易的方法取一个已经使用多个GPU进行训练的大型数据集的模型和权重(如GoogLeNet或VGG16),并截断最后一层并用他们自己的分类器替换它们。然后,分类器所要做的就是学习最后一层的权重,并使用预先存在的训练过的已有的滤波器权重。

原文发布于微信公众号 - ATYUN订阅号(atyun_com)

原文发表时间:2017-12-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏人工智能头条

只知道GAN你就OUT了——VAE背后的哲学思想及数学原理

1113
来自专栏WD学习记录

机器学习深度学习 笔试面试题目整理(1)

梯度消失:这本质上是由于激活函数的选择导致的, 最简单的sigmoid函数为例,在函数的两端梯度求导结果非常小(饱和区),导致后向传播过程中由于多次用到激活函数...

663
来自专栏Ldpe2G的个人博客

PCANet --- 用于图像分类的深度学习基准

1272
来自专栏量化投资与机器学习

【Python机器学习】系列之线性回归篇【深度详细】

谢谢大家的支持!现在该公众号开通了评论留言功能,你们对每篇推文的留言与问题,可以通过【写评论】给圈主留言,圈主会及时回复您的留言。 本次推文介绍用线性模型处理回...

5059
来自专栏大数据文摘

能模仿韩寒小四写作的神奇递归神经网络(附代码)

1815
来自专栏机器之心

入门 | 深度学习模型的简单优化技巧

以下是我与同事和学生就如何优化深度模型进行的对话、消息和辩论的摘要。如果你发现了有影响力的技巧,请分享。

1130
来自专栏PPV课数据科学社区

【学习】详解数据挖掘十大经典算法!

国际权威的学术组织the IEEE International Conference on Data Mining (ICDM) 2006年12月评选出了数据挖...

3277
来自专栏机器之心

教程 | 如何用30行JavaScript代码编写神经网络异或运算器

选自Medium 机器之心编译 参与:Panda 配置环境、安装合适的库、下载数据集……有时候学习深度学习的前期工作很让人沮丧,如果只是为了试试现在人人都谈的深...

2559
来自专栏AI科技评论

开发 | CNN中的maxpool到底是什么原理?

AI科技评论按:本文整理自知乎问题“请问 CNN 中的 maxpool 到底是什么原理,为什么要取最大值,取最大值的原理是什么?谢谢。”的下Yjango和小白菜...

3487
来自专栏机器之心

资源 | 从最小二乘到DNN:六段代码了解深度学习简史

选自floydhub 机器之心编译 参与:路雪、刘晓坤、黄小天 六段代码使深度学习发展成为今天的模样。本文介绍它们的发明者和背景。每个故事包括简单的代码示例,均...

3299

扫描关注云+社区