全连接神经网络的问题:
- 全连接层的参数太多。(一张图片的大小为32323,假设第一层隐藏层的节点数为500个,那么一个全连接层的神经网络将有32 * 32 * 3 * 500 + 500=150多万个参数)
- 参数增多导致计算速度减慢,还很容易导致过拟合问题。
卷积神经网络的组成:
- 输入层:一张图片的像素矩阵
- 卷积层:
卷积层试图将神经网络中的每一小块更加深入地分析从而得到抽象程度更高的特征。
一般来说,通过卷积层处理过的节点矩阵会变得更深。
filter可以将当前层神经网络上的一个子节点矩阵转化为下一层神经网络上的一个单位节点矩阵。(单位节点矩阵指的是一个长和宽都为1,但深度不限的节点矩阵。)
常用的过滤器尺寸为3 * 3或者5 * 5。因为过滤器处理的矩阵深度和当前层神经网络节点矩阵的深度是一致的,所以虽然节点矩阵是三维的,但过滤器的尺寸只需指定两个维度。过滤器的深度(即处理得到的单位节点矩阵的深度)需要人工指定。过滤器的尺寸是指一个过滤器输入节点矩阵的大小,而深度是指输出单位节点矩阵的深度。
通过过滤器将一个2 * 2 * 3的节点矩阵变化为一个1 * 1 * 5的单位节点矩阵。总共需要2 * 2 * 3 * 5 + 5=65个参数。
在卷积神经网络中,每一个卷积层中使用的过滤器中的参数都是一样的。共享过滤器的参数可以使得图像上的内容不受位置的影响。比如,无论数字1出现在左上角还是右下角,图片的种类都是不变的。因为在左上角和右下角使用的过滤器中的参数相同,所以通过卷积层后,无论数字在哪个位置,得到的结果都是一样的。共享每一个卷积层中过滤器中的参数可以巨幅减少神经网络的参数。假设第一层卷积层使用尺寸为5 * 5,深度为16的过滤器,那么这个卷积层的参数个数为5 * 5 * 3 * 16 + 16=1216个。使用500个隐藏节点的全连接层将有1.5百万个参数。卷积层的参数个数和图片的大小无关,它只和过滤器的尺寸、深度以及当前层节点矩阵的深度有关
# input:当前层的节点矩阵。四维矩阵,第一维为一个输入batch,后三个维度为一个节点矩阵
# filter_weight:卷积层的权重
# strides:不同维度上的步长,**第一维和最后一维的数字要求一定是1,因为卷积层的步长只对矩阵的长和宽有效**
conv = tf.nn.conv2d(input, filter_weight, strides=[1, 1, 1, 1], padding='SAME')
- 池化层:不改变三维矩阵的深度,但是会缩小矩阵的大小。池化操作可以认为是将一张高分辨率的图片转化为一张低分辨率的图片。并且可以进一步缩小最后全连接层中的节点的个数,从而减少整个神经网络的参数。
池化层可以非常有效的缩小矩阵的尺寸,从而减少最后全连接层中的参数。使用池化层既可以加快计算速度也有防止过拟合问题的作用。
卷积层使用的过滤器是跨整个深度的,而池化层的过滤器除了在长和宽的维度上移动,它还需要在深度的维度上移动。
# ksize:第一个和最后一个必须为1,因为池化层的过滤器是不能跨不同样例或者节点矩阵深度的
# strides:第一维和最后一维也只能是1,因为池化层不能减少节点矩阵的深度或者输入样例的个数
pool = tf.nn.max_pool(actived_conv, ksize=[1, 3, 3, 1],
strides=[1, 2, 2, 1], padding='SAME')
- 全连接层:完成分类任务。前面几层可以认为是自动图片特征提取的过程
- Softmax层:主要完成分类任务。
经典卷积网络模型
LeNet-5模型
- 一般卷积层的过滤器边长不会超过5.
- 在过滤器深度上,大部分卷积神经网络都采用逐层递增的方式。
- 卷积层的步长一般为1
- 池化层的过滤器边长一般为2或者3,步长也一般为2或者3
# 前向传播函数
# -*- coding: utf-8 -*-
import tensorflow as tf
# 配置神经网络的参数
INPUT_NODE = 784
OUTPUT_NODE = 10
IMAGE_SIZE = 28
NUM_CHANNELS = 1
NUM_LABELS = 10
# 第一层卷积层的尺寸和深度
CONV1_DEEP = 32
CONV1_SIZE = 5
# 第二层卷积层的尺寸和深度
CONV2_DEEP = 64
CONV2_SIZE = 5
# 全连接层的节点个数
FC_SIZE = 512
def inference(input_tensor, train, regularizer):
# 通过使用不同的命名空间来隔离不同层的变量,这可以让每一层中的变量命名
# 只需要考虑当前层的作用,而不用担心重名的问题
with tf.variable_scope('layer1-conv1'):
conv1_weights = tf.get_variable(
"weights", [CONV1_SIZE, CONV1_SIZE, NUM_CHANNELS, CONV1_DEEP],
initializer=tf.truncated_normal_initializer(stddev=0.1))
conv1_biases = tf.get_variable(
"biases", [CONV1_DEEP], initializer=tf.constant_initializer(0.0))
# 使用边长为5,深度为32的过滤器,过滤器移动的步长为1,且使用全零填充
conv1 = tf.nn.conv2d(
input_tensor, conv1_weights, strides=[1, 1, 1, 1], padding='SAME')
relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_biases))
# 第二层池化层,边长为2,全零填充且步长为2。这一层的输入是上一层的输出
# 也就是28*28*32的矩阵,输出为14*14*32的矩阵
with tf.name_scope('layer2-pool1'):
pool1 = tf.nn.max_pool(
relu1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# 声明第三层卷积层的变量,这一层的输入为14*14*32的矩阵
with tf.variable_scope('layer3-conv2'):
conv2_weights = tf.get_variable(
"weight", [CONV2_SIZE, CONV2_SIZE, CONV1_DEEP, CONV2_DEEP],
initializer=tf.truncated_normal_initializer(stddev=0.1))
conv2_biases = tf.get_variable(
"biases", [CONV2_DEEP],
initializer=tf.constant_initializer(0.0) )
# 使用边长为5,深度为64的过滤器,步长为1且使用全零填充
conv2 = tf.nn.conv2d(
pool1, conv2_weights, strides=[1, 1, 1, 1], padding='SAME')
relu2 = tf.nn.relu(tf.nn.bias_add(conv2, conv2_biases))
# 实现第四层池化层的前向传播过程输入为14*14*64的矩阵,输出为7*7*64的矩阵
with tf.name_scope('layer4-pool2'):
pool2 = tf.nn.max_pool(
relu2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# 第四层的输出为一个矩阵,第五层全连接层的输入为一个向量
# 将第四层的输出转化为第五层全连接层的输入格式,第四层输出为7*7*64的矩阵,将其拉伸为一个向量
# pool2.get_shape函数可以得到第四层输出矩阵的维度,
# 因为每一层神经网络的输入输出都为一个batch的矩阵,所以这里得到的维度也包含
# 了一个batch中数据的个数
pool_shape = pool2.get_shape().as_list()
# 将矩阵拉成向量后的长度为矩阵的长宽及深度的乘积。
# pool_shape[0]为一个batch中数据的个数
nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]
# 通过tf.reshape函数将第四层的输出变为一个batch向量
reshaped = tf.reshape(pool2, [pool_shape[0], nodes])
# 声明第五层的全连接层,输入为拉直后的一组向量,向量长度为3136,输出为
# 一组长度为512的向量。dropout在训练时会将部分节点的输出改为0,避免过拟合
# 从而使模型在测试数据上的效果更好。只用于全连接层而不用于其他层。
with tf.variable_scope('layer5-fc1'):
fc1_weights = tf.get_variable(
"weights", [nodes, FC_SIZE],
initializer=tf.truncated_normal_initializer(stddev=0.1))
# 只有全连接层的权重需要加入正则化
if regularizer is not None:
tf.add_to_collection('losses', regularizer(fc1_weights))
fc1_biases = tf.get_variable(
"biases", [FC_SIZE], initializer=tf.constant_initializer(0.1))
fc1 = tf.nn.relu(tf.matmul(reshaped, fc1_weights) + fc1_biases)
if train:
fc1 = tf.nn.dropout(fc1, 0.5)
# 声明第六层全连接网络,输入为512的向量,输出为10的向量,通过softmax得到最后的分类结果
with tf.variable_scope('layer6-fc2'):
fc2_weights = tf.get_variable(
"weigth", [FC_SIZE, NUM_LABELS],
initializer=tf.truncated_normal_initializer(stddev=0.1))
if regularizer is not None:
tf.add_to_collection('losses', regularizer(fc2_weights))
fc2_biases = tf.get_variable(
"bias", [NUM_LABELS],
initializer=tf.constant_initializer(0.1))
logit = tf.matmul(fc1, fc2_weights) + fc2_biases
return logit
# 训练函数
# -*- coding: utf-8 -*-
import os
import numpy as np
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import tensorflow.contrib as contrib
# 加载mnist_inference.py中定义的常量和前向传播的函数
import mnist_inference
# 配置神经网络参数
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.8
LEARNING_RATE_DECAY = 0.99
REGULARAZTION_RATE = 0.0001
TRAINING_STEPS = 30000
MOVING_AVERAGE_DECAY = 0.99
# 模型保存的路径和文件名
MODEL_SAVE_PATH = "cnn/model/"
MODEL_NAME = "model.ckpt"
def train(mnist):
# 定义输出输入palceholder
x = tf.placeholder(tf.float32, [
BATCH_SIZE, # 第一维表示一个batch中样例的个数
mnist_inference.IMAGE_SIZE, # 第二维和第三维表示图片的尺寸
mnist_inference.IMAGE_SIZE,
mnist_inference.NUM_CHANNELS], # 第四维表示图片的深度
name='x-input')
y_ = tf.placeholder(
tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input')
regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)
# 直接使用mnist_inference.py中定义的前向传播过程
y = mnist_inference.inference(x, train, regularizer)
global_step = tf.Variable(0, trainable=False)
Variable_averages = tf.train.ExponentialMovingAverage(
MOVING_AVERAGE_DECAY, global_step)
Variable_averages_op = Variable_averages.apply(
tf.trainable_variables())
cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(
labels=tf.argmax(y_, 1), logits=y)
cross_entropy_mean = tf.reduce_mean(cross_entropy)
loss = cross_entropy_mean + tf.add_n(tf.get_collection('losses'))
learing_rate = tf.train.exponential_decay(
LEARNING_RATE_BASE,
global_step,
mnist.train.num_examples / BATCH_SIZE,
LEARNING_RATE_DECAY)
train_step = tf.train.ProximalGradientDescentOptimizer(learing_rate)\
.minimize(loss, global_step=global_step)
with tf.control_dependencies([train_step, Variable_averages_op]):
train_op = tf.no_op(name='train')
# TensorFlow持久化
saver = tf.train.Saver()
with tf.Session() as sess:
tf.initialize_all_variables().run()
for i in range(TRAINING_STEPS):
xs, ys = mnist.train.next_batch(BATCH_SIZE)
reshaped_xs = np.reshape(xs, (BATCH_SIZE,
mnist_inference.IMAGE_SIZE,
mnist_inference.IMAGE_SIZE,
mnist_inference.NUM_CHANNELS))
_, loss_value, step = sess.run([train_op, loss, global_step],
feed_dict={x: reshaped_xs, y_: ys})
# 每1000轮保存一次模型
if i % 1000 == 0:
# 输出当前的训练情况
print("After %d training steps(s), loss on training batch is %g."
% (step, loss_value))
# 保存当前的模型
saver.save(
sess, os.path.join(MODEL_SAVE_PATH, MODEL_NAME),
global_step=global_step)
def main(argv=None):
mnist = input_data.read_data_sets("cnn/data", one_hot=True)
train(mnist)
if __name__ == '__main__':
main()
Inception-v3模型(46层,11个模块)
LeNet-5模型中,不同卷积层通过串联的方式连接在一起,而Inception-v3模型中的inception结构是将不同的卷积层通过并联的方式结合在一起。
一个卷积层可以使用边长为1、3、5的过滤器,同时使用所有尺寸的过滤器,然后将得到的矩阵拼接起来。
Inception模块会首先使用不同尺寸的过滤器处理输入矩阵。虽然过滤器大小不同,但使用全0填充且步长为1,那么前向传播得到的结果矩阵的长和宽都与输入矩阵一致,这样经过不同过滤器处理的结果矩阵可以拼接为一个更深的矩阵(将它们在深度这个维度上组合起来)
# -*- coding: utf-8 -*-
import tensorflow as tf
# 使用Tensorflow-Slim可以在一行中实现一个卷积层的前向传播算法。
# slim.conv2d函数有3个函数是必填的。
# 第一个参数:输入节点矩阵
# 第二个参数:当前卷积层过滤器的深度
# 第三个参数:过滤器的尺寸
# 可选参数:过滤器的移动步长,填充,激活函数,变量的命名空间等
# net = slim.conv2d(input, 32, [3, 3])
# 加载slim库
slim = tf.contrib.slim
# slim.arg_scope函数可以用于设置默认的参数取值。
# 第一个参数:一个函数列表,在这个列表中的函数将使用默认的参数取值
# 比如调用slim.conv2d(net, 320, [1, 1])函数时,会自动加上stride=1和padding='SAME'参数
with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d],
stride=1, padding='VALID'):
...
net = 上一层的输出节点矩阵
# 为一个Inception模块声明一个统一的变量命名空间
with tf.variable_scope('Mixed_7c'):
# 给Inception模块中每一条路径声明一个命名空间
with tf.variable_scope('Branch_0'):
# 实现一个过滤器边长为1,深度为320的卷积层
branch_0 = slim.conv2d(net, 320, [1, 1], scope='Conv2d_0a_1x1')
with tf.variable_scope('Branch_1'):
branch_1 = slim.conv2d(net, 384, [1, 1], scope='Conv2d_0a_1x1')
# tf.concat函数可以将多个矩阵拼接起来。
# 第一个参数:拼接的维度,这里给定3,代表矩阵是在深度这个维度上进行拼接
branch_1 = tf.concat(3, [
slim.conv2d(branch_1, 384, [1, 3], scope='Conv2d_0b_1x3')
slim.conv2d(branch_1, 384, [3, 1], scope='Conv2d_0c_3x1')])
...
...
net = tf.concat(3, [branch_0, branch_1, branch_2, branch_3])
迁移学习
- 将一个问题上训练好的模型通过简单的调整使其适用于一个新的问题
- 一般来说,在数据量足够的情况下,迁移学习的效果不如完全重新训练
- 训练时间和训练样本远远小于训练完整的模型