目录

一、背景前言

二、DNN概述

三、手写DNN实现逻辑

四、调用Tensorflow代码构建DNN

五、模型保存和使用

六、提升准确率方案

七、引申和总结--零初始化,梯度消失和反向传播


一、背景前言

我们先看下SoftMax的代码:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
# 文件名: 12_Softmax_regression.py

from examples.tutorials.mnist import input_data
import tensorflow as tf


# mn.SOURCE_URL = "http://yann.lecun.com/exdb/mnist/"
my_mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)#从本地路径加载进来

# The MNIST data is split into three parts:
# 55,000 data points of training data (mnist.train)#训练集图片
# 10,000 points of test data (mnist.test), and#测试集图片
# 5,000 points of validation data (mnist.validation).#验证集图片

# Each image is 28 pixels by 28 pixels

# 输入的是一堆图片,None表示不限输入条数,784表示每张图片都是一个784个像素值的一维向量
# 所以输入的矩阵是None乘以784二维矩阵
x = tf.placeholder(dtype=tf.float32, shape=(None, 784)) #x矩阵是m行*784列
# 初始化都是0,二维矩阵784乘以10个W值 #初始值最好不为0
W = tf.Variable(tf.zeros([784, 10]))#W矩阵是784行*10列
b = tf.Variable(tf.zeros([10]))#bias也必须有10个

y = tf.nn.softmax(tf.matmul(x, W) + b)# x*w 即为m行10列的矩阵就是y #预测值

# 训练
# labels是每张图片都对应一个one-hot的10个值的向量
y_ = tf.placeholder(dtype=tf.float32, shape=(None, 10))#真实值 m行10列

# 定义损失函数,交叉熵损失函数
# 对于多分类问题,通常使用交叉熵损失函数
# reduction_indices等价于axis,指明按照每行加,还是按照每列加
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y),
                                              reduction_indices=[1]))#指明按照列加和 一列是一个类别
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)#将损失函数梯度下降 #0.5是学习率

#参考博客 https://blog.csdn.net/lvchunyang66/article/details/80076959
#交叉熵刻画了两个概率分布之间的距离,p代表正确答案,q代表的是预测值。交叉熵值越小,两个概率分布越接近。

# 初始化变量
sess = tf.InteractiveSession()#初始化Session
tf.global_variables_initializer().run()#初始化所有变量
for _ in range(10000):
    batch_xs, batch_ys = my_mnist.train.next_batch(100)#每次迭代取100行数据
    # y_real=sess.run(y_,feed_dict={y_: batch_ys})
    # print(y_real)
    sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})
#每次迭代内部就是求梯度,然后更新参数
# 评估

# tf.argmax()是一个从tensor中寻找最大值的序号 就是分类号,tf.argmax就是求各个预测的数字中概率最大的那一个
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
#tf.argmax(y_, 1)其实是返回每一行样本中的真实标签 因为是按行找
#关于tf.argmax的blog https://blog.csdn.net/Jiaach/article/details/78874704

# 用tf.cast将之前correct_prediction输出的bool值转换为float32,再求平均
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
#1.0 0.0

# 测试
print(accuracy.eval({x: my_mnist.test.images, y_: my_mnist.test.labels}))

# 总结
# 1,定义算法公式,也就是神经网络forward时的计算
# 2,定义loss,选定优化器,并指定优化器优化loss
# 3,迭代地对数据进行训练
# 4,在测试集或验证集上对准确率进行评测


mnist数据集我们知道,它是0~9的手写数字的这么一个识别。既然是一个10分类的问题,我们就通过softmax这么一个浅层的模型我们来生成一个0~9的分类来进行参数的预测,最后准确率是91%左右。

for _ in range(10000):
    batch_xs, batch_ys = my_mnist.train.next_batch(100)#每次迭代取100行数据
    # y_real=sess.run(y_,feed_dict={y_: batch_ys})
    # print(y_real)
    sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

如果把这个地方的循环次数给增多,那相当于什么意思呢?这是1万乘以100.循环了100万次。我们的训练集总共是5000图片,就相当于5000张图片总共学习了不下于20次。

如果想让你的准确率有所提升的话,要知道我们可以给它更多张图片,但是在如果没有更多张图片的情况下,我们可以让它多学几次。比如这里边把循环次数变成200万,就相当于同样的这些图片让它学40次。

就相当于一本书让它背更多遍,或许有一个更好的一个理解,很多时候我们说如果这个孩子脑子就这样了,他记忆力就这样,那他背20遍跟背40遍也就没什么区别了。当把这个次数给增多的时候。最后的准确率就到92%左右,这个就是我们这个算法的一个瓶颈了。我们可以提高更多的数据量来试一试,但它还是一个瓶颈。实际上这就是我们浅层模型一个弊端,我们在数据给足的情况下,想把模型给变得更好那我们只有选择更好的算法了。这里更好的算法就引出了我们的神经网络DNN。

二、DNN概述

人工神经网络,我们称为ANN。如果是由两个或两个以上的隐藏层称为DNN。

下面就为大家引荐DNN深度神经网络的实现使得我们的模型准通率会有所提升。到DNN这个地方我们就可以称作深度神经网络

这里边我们来看一下相关的实现 ,一般来说实现神经网络的算法有两个阶段,第一个阶段就是要构建一个计算图二个阶段是计算图

我们先看下DNN的代码实现:

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import numpy as np
from tensorflow.contrib.layers import fully_connected


# 构建图阶段
n_inputs = 28*28#输入节点
n_hidden1 = 300#第一个隐藏层300个节点 对第一个隐藏层前面有784*300跟线去算
n_hidden2 = 100#第二个隐藏层100个节点 对第二个隐藏层300*300根线
n_outputs = 10#输出节点

X = tf.placeholder(tf.float32, shape=(None, n_inputs), name='X')
y = tf.placeholder(tf.int64, shape=(None), name='y')

#自己手写的实现逻辑
# 构建神经网络层,我们这里两个隐藏层,基本一样,除了输入inputs到每个神经元的连接不同
# 和神经元个数不同
# 输出层也非常相似,只是激活函数从ReLU变成了Softmax而已
# def neuron_layer(X, n_neurons, name, activation=None):# X是输入,n_neurons是这一层神经元个数,当前隐藏层名称,最后一个参数是加不加激活函数
#     # 包含所有计算节点对于这一层,name_scope可写可不写
#     with tf.name_scope(name):#with让代码看起来更加优雅一些
#         # 取输入矩阵的维度作为层的输入连接个数
#         n_inputs = int(X.get_shape()[1])
#         stddev = 2 / np.sqrt(n_inputs)#求标准方差
#         # 这层里面的w可以看成是二维数组,每个神经元对于一组w参数
#         # truncated normal distribution(调整后的正态分布) 比 regular normal distribution(正态分布)的值小
#         # 不会出现任何大的权重值,确保慢慢的稳健的训练
#         # 使用这种标准方差会让收敛快
#         # w参数需要随机,不能为0,否则输出为0,最后调整都是一个幅度没意义
#         init = tf.truncated_normal((n_inputs, n_neurons), stddev=stddev)#把初始参数随机出来,比较小,不会出现大的权重值
#         w = tf.Variable(init, name='weights')
#         b = tf.Variable(tf.zeros([n_neurons]), name='biases')#b可以全为0
#         # 向量表达的使用比一条一条加和要高效
#         z = tf.matmul(X, w) + b
#         if activation == "relu":
#             return tf.nn.relu(z)
#         else:
#             return z
#自己手写的实现逻辑
'''
with tf.name_scope("dnn"):
    hidden1 = neuron_layer(X, n_hidden1, "hidden1", activation="relu")
    hidden2 = neuron_layer(hidden1, n_hidden2, "hidden2", activation="relu")
    # 进入到softmax之前的结果
    logits = neuron_layer(hidden2, n_outputs, "outputs")
'''
#用Tensorflow封装的函数
with tf.name_scope("dnn"):
    # tensorflow使用这个函数帮助我们使用合适的初始化w和b的策略,默认使用ReLU激活函数
    hidden1 = fully_connected(X, n_hidden1, scope="hidden1")#构建第一层隐藏层 全连接
    hidden2 = fully_connected(hidden1, n_hidden2, scope="hidden2")#构建第二层隐藏层 全连接
    logits = fully_connected(hidden2, n_outputs, scope="outputs", activation_fn=None)#构建输出层 #注意输出层激活函数不需要

with tf.name_scope("loss"):
    # 定义交叉熵损失函数,并且求个样本平均
    # 函数等价于先使用softmax损失函数,再接着计算交叉熵,并且更有效率
    # 类似的softmax_cross_entropy_with_logits只会给one-hot编码,我们使用的会给0-9分类号
    xentropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y, logits=logits)#封装好了损失函数
    #把真实的Y值做onehot编码
    loss = tf.reduce_mean(xentropy, name="loss")#求平均

learning_rate = 0.01

with tf.name_scope("train"):
    optimizer = tf.train.GradientDescentOptimizer(learning_rate)#创建梯度下降的优化器
    training_op = optimizer.minimize(loss)#最小化损失

with tf.name_scope("eval"):#评估
    # 获取logits里面最大的那1位和y比较类别好是否相同,返回True或者False一组值
    correct = tf.nn.in_top_k(logits, y, 1)#logits返回是类别号 y也是类别号
    accuracy = tf.reduce_mean(tf.cast(correct, tf.float32))#转成1.0 0.0

init = tf.global_variables_initializer()
saver = tf.train.Saver()

# 计算图阶段
mnist = input_data.read_data_sets("MNIST_data_bak/")
n_epochs = 400 #运行400次
batch_size = 50 #每一批次运行50个

with tf.Session() as sess:
    init.run()
    for epoch in range(n_epochs):
        for iterationo in range(mnist.train.num_examples//batch_size):#总共多少条/批次大小
            X_batch, y_batch = mnist.train.next_batch(batch_size)#每次传取一小批次数据
            sess.run(training_op, feed_dict={X: X_batch, y: y_batch})#传递参数
        acc_train = accuracy.eval(feed_dict={X: X_batch, y: y_batch})#每运行一次 看训练集准确率
        acc_test = accuracy.eval(feed_dict={X: mnist.test.images,#每运行一次 看测试集准确率
                                            y: mnist.test.labels})
        print(epoch, "Train accuracy:", acc_train, "Test accuracy:", acc_test)

    save_path = saver.save(sess, "./my_dnn_model_final.ckpt")

# 使用模型预测
with tf.Session as sess:
    saver.restore(sess, "./my_dnn_model_final.ckpt")
    X_new_scaled = [...]
    Z = logits.eval(feed_dict={X: X_new_scaled})
    y_pred = np.argmax(Z, axis=1)  # 查看最大的类别是哪个

我们一行行的解释这个代码:

我们通过from tensorflow.examples.tutorials.mnist import input_data来读数据,

from tensorflow.contrib.layers import fully_connected,我们在layers这个函数里面导入fully connected(全连接),用它可以使得代码行更少。

首先我们写几个超参数是去定义这个网络拓扑,具体看下构建图的阶段定义几个超参数:

n_inputs,我们知道数据集minst它其实每一张数据都是一张图片。 图片是正方形的,总共包含784个像素点,即28*28个像素点。

n_hidden1=300,这地方相当于我们在设置第一个隐藏层它里边神经元的个数。

n_hdden2=100,相当于我们在设置第二个隐藏层里边神经元的个数或者是节点的个数。

n_outputs=10, 这个没有什么好讲的。我们是做的0-9的一个10分类。所以我们的输出层里边的节点是10个。

这几个参数就相当于input输入层看一下有多少个节点,隐藏层有多少个节点,然后最后n_outputs输出层有多少个节点。

然后我们如果开始去构建图。

我们首先需要有正向传播的输入。我们需要正向转播的输入的X,跟往常做法一样。我们通过

X = tf.placeholder(tf.float32, shape=(None, n_inputs), name='X')

placeholder进行占位符,然后给数据元素定义一个类型,并强制转换为tf.float32。

 

这个地方的none为什么要记成none呢?我们知道我们手头上有55000张数据图片。但是我们在训练和测试,或者我们在验证的时候,我们是分多个batch和多个epoch的,首先在分多个batch的时候,我们这里边的数据量取决于batchsize。然后测试集用多少的数量取决于测试集有多少的数据。验证集的数据量要取决于验证集具有多少的数据,这个batchsize和测试集的数据还有验证集的数据它们的条数可能是不一样的。所以在这个地方呢我们为了给x传数据的时候,有多少行我们在开始是不清楚的所以写上none,明白这个意思吧?

然后n_inputs就是我要输入多少维度。实际上每一张照片是784,这里这个n_inputs也就是784。这个地方,x是想表达什么呢?就是想表达m行784列,相当于有很多行数据,每一条数据代表一张图片。

接着往下,同样

y = tf.placeholder(tf.int64, shape=(None), name='y')

我们的y也是一样的。y是m行1列,所以这个地方就写了一个维度,这里的int64就是说这个地方的y是一个标签,从0到9的这么一个标签。 所以我们写一个int64就可以了。但是这个地方要去注意一点就是y的shape形状和你读出来的数据是有关系的。如果我们读下来的这个数据是m行10列。那么这个地方的shape就要写成(none,10),如果咱们这个y是m行一列,那就是写一个维度就可以了。

这里延申一点,如果我们来读数据的时候,是如下形式:

mnist = input_data.read_data_sets("MNIST_data/",one_hot=True)

这个里边加了一个参数叫做one-hot=true。它就可以通过这个方法帮助我们进行01编码,比如说把0变成1000000000,9变成000000001。这个函数在读数据的时候就自动帮着我们做了01编码了,在y=tf.placeholder这个地方就必须写成m行10列。即

y = tf.placeholder(tf.int64, shape=(None,10), name='y')

如果一开始看到y就一个维度即m行1列,这就意味着你未来读数据的时候就是给y传的时候,你的y必须是 m行1列,即如下写法:

mnist = input_data.read_data_sets("MNIST_data/"),所以这两个地方你要对应上。

三、手写DNN实现逻辑

def neuron_layer(X, n_neurons, name, activation=None):  # X是输入,n_neurons是这一层神经元个数,当前隐藏层名称,最后一个参数是加不加激活函数
    # 包含所有计算节点对于这一层,name_scope可写可不写
    with tf.name_scope(name):  # with让代码看起来更加优雅一些
        # 取输入矩阵的维度作为层的输入连接个数
        n_inputs = int(X.get_shape()[1])
        stddev = 2 / np.sqrt(n_inputs)  # 求标准方差
        # 这层里面的w可以看成是二维数组,每个神经元对于一组w参数
        # truncated normal distribution(调整后的正态分布) 比 regular normal distribution(正态分布)的值小
        # 不会出现任何大的权重值,确保慢慢的稳健的训练
        # 使用这种标准方差会让收敛快
        # w参数需要随机,不能为0,否则输出为0,最后调整都是一个幅度没意义
        init = tf.truncated_normal((n_inputs, n_neurons), stddev=stddev)  # 把初始参数随机出来,比较小,不会出现大的权重值
        w = tf.Variable(init, name='weights')
        b = tf.Variable(tf.zeros([n_neurons]), name='biases')  # b可以全为0
        # 向量表达的使用比一条一条加和要高效
        z = tf.matmul(X, w) + b
        if activation == "relu":
            return tf.nn.relu(z)
        else:
            return z

如果我们不用TensorFlow,自己手动来构建这个神经网络层,我们可以定义一个neuron-layer这样一个神经网络层的函数。每调动一次神经网络层,这就相当于什么意思呢?这就相当于我们这一层从输入传出的数据再到输出,当然,前边传出的数据有可能是以shuffle的形式传出来的数据。每调用一层神经网络层函数就相当于来走一次这个正向传播,当然,出去的话是往下一层传播这种shuffle的数据。

这个地方我们看下需要哪些参数,

def neuron_layer(X, n_neurons, name, activation=None)

首先需要上一层输出的x。如果要构建某一个神经网络层,我们还需要这一层神经元的个数n_neurons,我们给它名name,写不写其实没有太大的关系。这个到我们后边儿讲tensorboard的时候才会用到这个名称。activation=None,这个超参数是想表达什么意思呢?其实就是表达这个网络神经元有没有激活函数。比如当前我们想构建的neuron_layer叫h1。那这个neuron就是指的当前这个网络里有多少个节点多少个神经元。x是什么呢,这个x就是前边的输入。实际上这个x,它肯定也有很多的维度。来每一个输入都相当于一个节点,x1的输入,它会传到我刚才构建的这每一个神经元里去。x2也是会传到当前构建的每一个神经网络的每一个神经元里去。

那这个x1,x2一直到x多少呢?这个得看你上一层的输出是多少个?比如说咱们举一个例子,比如说h1是第一个隐藏层。如果咱们要是想去构建h1这个隐藏层的话,咱们就得知道h1这个隐藏层跟它前面一层中间的这个w矩阵shape是什么形状。每一个w矩阵的shape形状它取决于什么呢?看下面图1-输入层和隐藏层拓扑示意图我们知道取决于前面神经元的个数跟它当前这一条神经元的个数。因为面前是input输入层。从x1到x784,加入后边h1隐藏层有10个节点的话,那这个地方输入到隐藏层之间的W的形状就是(784,10),这个地方的x是什么东西呢?就是上一层的输出。也就是说上一层这给出去的值。什么叫给出去的值呢?比如说在input这一层输出的,如图标出来的这个地方就叫给出去的值。

                                                                                   图1-输入层和隐藏层拓扑示意图

在这个地方问大家一个问题,输入层这个地方,它里边儿的神经元有加和吗?有激活函数吗?答案是没有的。输入层就是拿过来的数直接往下传递了,输入层在这个地方,就是我前边儿x1这个输入的数是什么?我后边儿就直接给出去了。在这个地方我们要构建h1这个隐藏层的话,这个参数activation我们就必须得传 。

我们来看看这个函数里边儿是怎么来写的。with这一块的代码,实际上它就是一个规范格式的作用。未来在构建一个图的时候。你只要找到这个名称里的节点,然后把这个节点给展开。你就会看到这些代码写的逻辑。所以说这一行

with tf.name_scope(name):

有没有对你这个神经网络的构建没有关系。

然后再往下,

n_inputs = int(X.get_shape()[1])

我们说这个X.get-shape这个拿到的是什么,就是拿到的我们一开始的input层的这个输出的m行的数据的形状,m行n列的数据。那这个地方的1是谁啊?是不是拿的n,因为我们构建输入层和隐藏层之间的W(784,10)矩阵的这个形状的时候我们是不是需要一个784,这个实际上取决于X的列数,即784。

stddev = 2 / np.sqrt(n_inputs)#求标准方差

这个地方把列这个数据拿过来之后求了个方差,我们把这个维度开个根号。算这个时候的stddev是为了下边儿随机的时候用到的。

这层里面的w可以看成是二维数组,每个神经元对于一组w参数。

我们前面的输入的神经元,比如说x1,x2,它都对应W(784,10)这个二维数组里面的每一行w,后边儿h1里边儿的每一个神经元都对应着784行里边的每一列w。这是一种写法的表示。

init = tf.truncated_normal((n_inputs, n_neurons), stddev=stddev)

w参数在一开始是没有的,需要去随机初始化。这里边我们有一个方法,normal-distribution,正态分布,truncated是截断的意思,那加起来就是截断的正态分布。它会比正常的正态分布代表的值要小。比如说普通的正态分布是如图2-正态分布这个样子的,均值是0,方差是1。

                                                                                                图2-正太分布

而截断的正态分布方差我们会自己来定。通过什么来定呢?通过stddev这个超参数赋于截断的正态分布的方差。一般情况下,这个方差,我们会用上一层神经元的个数开根号再分之二。所以主要是看上一层输出的节点有多少个。接下来(n_inputs, n_neurons)这个地方定义的就是w的一个形状。它的形状取决于上一层的输出节点有多少个和当前构建的这一层的神经元的节点有多少个。

truncated normal distribution(调整后的正态分布) 比 regular normal distribution(正态分布)的值小,不会出现任何大的权重值,确保慢慢的稳健的训练 。也就是说,标准的正态分布,或者是某一种正态分布。因为均值是0,所以随机到零的概率比较大,或者是可能随机到零左右的概率比较大。但是也可能会发生偶然情况,负的特别小或者是正的特别大。

而truncated normal distribution给出的情况是它不会让这种负的特别小或者正得特别大的情况出现。它会截断在比较正常的一个区间里和范围内取值,如果我们的w随机初始化,在零附近,就会使得我们的收敛比较快,至于为什么w在零附近的时候收敛得比较快。这个地方大家先有个印象。以后有时间再详说,这就是说我们为什么在每一次用w的时候都要它随机到零附近。

w = tf.Variable(init, name='weights')

我们有了一个init初始值之后我们最终是要把它交给这个tf.Variable然后再去创建一个变量。得到我们的w这样一个Tensor。

b = tf.Variable(tf.zeros([n_neurons]), name='biases')

另外,biases初始化一般情况下全为零,就可以随意初始化了。biases截距的个数取决于什么呢?取决于隐藏层节点的个数,这个b会连接到每一个h1的神经元节点上去,如图3-biases示意图:

                                                                                              图3-biases示意图

b就是一维的向量,既然取决于这个h1的个数,所以咱们构建代码的时候,就传进来h1隐藏层的个数。

我们接下来该干什么呢? 第一步是初始化w,b。第二步该正向传播去求出y-head。

z = tf.matmul(X, w) + b

matmul在这个地方就相当于相乘,正向传播这里边假如说不只有h1一层,是不是应该往前传一层啊,那怎么传呢?x相当于我们的输入。W是当前输入的第一个隐藏层的矩阵。然后再加一个biases截距,就得到我们神经元里边的这个z,当然,这个z它指的不是一个数,它指的是什么呢?它指的是h1里边的每一个神经元加和所得到的结果。比如说对于一条数据来说,h1里边儿有10个神经元,即10个节点。这个z就是10个加和的结果。因为有m行所以结果就是m行个10个加和的结果。它就是个矩阵了。

我们通过矩阵的相乘也能看出来,X是(m,784),W是(784,10)。所以结果Z就是(m,784)*(784,10)=(m,10)

接着再往下:

if activation == "relu":
    return tf.nn.relu(z)
else:
    return z

如果h1的话如果是第一个隐藏层的话。那我们这个地方的activation就需要传入一个激活函数,一般情况下,我们的激活函数用的最多的就是relu。所以在这个地方做了一个判断。如果有activation这个参数,那等于返回就是true,在这个z身上再加一个tf.nn.relu(z)

相当于让它经过加法之后再去经过一个激活函数的变换。这个地方我们把return结果返回。那这一层神经网络的传递就结束了。

什么时候不需要激活函数呢?输出层,如果直接做回归就不要了是吧,如果不作回归需要softmax多分类层。在隐藏层,一定得需要,那三种激活函数的其中一个。

这只是一层神经网络的传递。在这里边我们要构建多少层呢?我前边写超参数的时候,实际上已经表明了。我们实际上需要的是如下输入层,隐藏层。然后在再加一个输出层。

n_inputs = 28*28#输入节点
n_hidden1 = 300#第一个隐藏层300个节点 对第一个隐藏层前面有784*300跟线去算
n_hidden2 = 100#第二个隐藏层100个节点 对第二个隐藏层300*300根线
n_outputs = 10#输出节点

输入层这个地方我写的很清楚,它一共有28*28=784个输入节点,为什么有784个输入节点呢?因为我们的x矩阵是m行784列?所以我在输入层这个地方给它定义的是784。那在h1这个地方,我给它写的是300,相当于在这个地方有300个神经元,对不对?那输入层和隐藏层之间的W矩阵有多少行多少列呢?是不是有784行,300列?那第二个隐藏层,我想要它有100个节点。那这个h1和h2之间的W矩阵,它有多少行多少列呢?是不是300行100列。那我们最后的输出层,这里是有十个神经元。那第二个隐藏层和输出层这个W矩阵是有多少行多少列呢?是100行10列。矩阵形状如图4-DNN网络拓扑图

                                                                                                图4-DNN网络拓扑图

再来回顾下neuron_layer的核心事件:

n_inputs = int(X.get_shape()[1])

这一层就是把当前这一层前边儿的输入拿回来看一下它所在的列是多少个

接下来

init = tf.truncated_normal((n_inputs, n_neurons), stddev=stddev)

构建一下当前这一层和上一层之间的曲线的形状。那我们有了上一层神经元的个数有当前这一层神经元的个数。那我们是不是就可以知道w的形状啦?

w = tf.Variable(init, name='weights')

然后我们通过这个东西再交给tf.Variable我们就可以得到w的一个初始值。

b = tf.Variable(tf.zeros([n_neurons]), name='biases')

 

然后这个biases它取决于当前神经元的个数有多少个,然后得到了w,得到了b,然后我们又有上一层的x即上一层的输出,当做这一层的输入。

z = tf.matmul(X, w) + b

然后再跟当前的w矩阵相乘相加加上这一层的截距,我们就可以得到当前这一层在得到激活函数之前的累加结果。

然后得到这个z之后,我们就把这个z给带到激活函数里边儿去。

if activation == "relu":
    return tf.nn.relu(z)
else:
    return z

得到当前这一层的输出。那这样的话,我们就可以把这个return的结果,也就是这个函数的结果。

这样的话就相当于我们从x1一直到x784。经过这个wb的计算,return的结果就等于h1这里的300个节点,也就是说他300个节点的每一个节点的输出就是这个return的东西,如图5-return示意图。

                                                                                图5-return示意图

接下来我们来看neuron_layer函数应该怎么样调用。

with tf.name_scope("dnn"):

这一行想写的意思就是我们接下来构建一个dnn,给下边儿这些逻辑起个名称叫dnn。

with tf.name_scope("dnn"):
    hidden1 = neuron_layer(X, n_hidden1, "hidden1", activation="relu")
    hidden2 = neuron_layer(hidden1, n_hidden2, "hidden2", activation="relu")
    # 进入到softmax之前的结果
    logits = neuron_layer(hidden2, n_outputs, "outputs")

 

我们调用这个neuron_layer实际上是想构建一个什么层呢?是想构建一个隐藏层。我们就把输入层的这个输入给拿过来。然后呢,再传一下神经元个数。然后,随便起个名称,因为是隐藏层,所以要用到一个激活函数。那这个函数调完了之后它会返回什么呀?那这个地方拿一个变量接一下。比如叫hidden1或者随便一个名称,拿一个变量的名称接一下它就是h1每一个节点的输出值对不对?那么大家知道h1的输出,它会作为什么的输入呢?实际上它会作为h2的输入。所以h1的输出,我们把它交给了hidden1这个变量。

然后我们在调用一下neuron-layer这个函数。

hidden2 = neuron_layer(hidden1, n_hidden2, "hidden2", activation="relu")

hidden1传过来作为h2的输入。那h2这一层有没有自己的个数呢?有。之前定义的个数是100。然后再给当前的隐藏层取一个名字。因为是隐藏层,所以我们要给一个激活函数,即activation="relu",我们用hidden2来接一下第二层的输出结果。

我们再具体聊下第二个隐藏层。第二个隐藏层的输入是谁呢?实际上就是第一个隐藏层刚刚的输出。那第一个隐藏层的输出它的形状是什么?是不是m*300。为什么呢?因为我们刚开始input输入是m行784列,即X,然后h1节点的神经元个数是300,h1节点和input节点之间的W矩阵是(784,300),所以经过z = tf.matmul(X, w) + b这样的操作(m,784)*(784,300)=(m,300)得到的h1的输出的形状是不是就是m行300列呀?

所以h1输出当作h2输入。那么就意味着这个地方的X是m行300列。

因为这个X

def neuron_layer(X, n_neurons, name, activation=None)

它只是一个形参,这个形参X在构建第二个隐藏层时

hidden2 = neuron_layer(hidden1, n_hidden2, "hidden2", activation="relu")

它本质上是传入的是我们的hidden1。这里面的X既然是形参,真正的值取决于真正传入的数据。

比如传hidden1就是hidden1的输出的,千万别和

X = tf.placeholder(tf.float32, shape=(None, n_inputs), name='X')

最开始的输入X弄混了。

那么h2的输出是什么形状呢?我们直接看有多少行数据,然后再看这一层有多少神经元就可以了。即m行100列。它一定是这样的。我们把h2的输出用一个变量来接一下就是n_hidden2。然后n_hidden2再作为输出层的输入,我们再构建下输出层。

logits = neuron_layer(hidden2, n_outputs, "outputs")

输出层和隐层层有什么区别呢?就是激活函数不同。输出层激活函数是SoftMax,隐藏层是Relu。第2点是神经元的个数不同,输出层神经元个数取决于做几分类,如果是10分类,输出层就是10个神经元,即n_outputs是10,以此类推。

而这里参数Inputs就是上一层的输出即hidden2。

那输出层的矩阵是什么形状呢?取决于上一层的输出个数100。所以输出层形状就是100,10。

对于输出层我们直接加和输出,

z = tf.matmul(X, w) + b

但这里的加和的结果是我们的最终的结果吗,不是的。我们可以将输出的结果传给softmax就得到我们预测的结果。

y = tf.nn.softmax(tf.matmul(x, W) + b)

或者我们直接传给tensorflow的封装也可以。

所以再次总结下TensorFlow代码肯定是分为两部分的:

第一部分是构建正向传播的逻辑。在构建图的时候没有传数据,在第二部分才开始建Session来传数据。

然后我们构建图的时候要定义比如有多少层,每层神经元多少神经元,每层神经元里面有什么激活函数。但是图构建好了,里面去传什么数据是没有的。只是把所有格式定义好了。

DNN,如果自己来构建一个正向传播neuron_layer函数,每层构建都调用这个函数,要注意一下隐藏层和输出层的区别,还有隐藏层和输出层节点上的个数的区别,其它的逻辑没有区别,因为这里面用的全部都是全连接。算z就是把所有的x和w相乘相加,再加一个b就可以了。

隐藏层,用的最多的就是relu,经过relu激活函数的非线性变化。如果输出层是做分类,就需要用到softmax;如果是做回归的话,直接通过z输出结果就可以了。

所以笼统的看就是三行代码,第一个隐藏层,第二个隐藏层以及最后的输出层。我们没有构建一个输入层,因为在输入层里面的节点,实际上是进来之后就直接输出了,它没有什么变化,所以输入层就不需要去构建,我们直接把输入的数据当成输入层的输出,交给隐藏层就可以了。所以一般从隐藏层这里开始。

四、调用Tensorflow代码构建DNN

自定义neuron_layer的方法,是用来解释如果做全连接,正向传播,一层层地传递,每一层里面会做怎样的计算。

如果用tensorflow的话,很多东西其实是封装起来了,上面这么多行,下面三行就搞定了。

构建DNN,不再用neuron_layer了,用fully_connected全连接,如下所示:

from tensorflow.contrib.layers import fully_connected
with tf.name_scope("dnn"):
    # tensorflow使用这个函数帮助我们使用合适的初始化w和b的策略,默认使用ReLU激活函数
    hidden1 = fully_connected(X, n_hidden1, scope="hidden1")#构建第一层隐藏层 全连接
    hidden2 = fully_connected(hidden1, n_hidden2, scope="hidden2")#构建第二层隐藏层 全连接
    logits = fully_connected(hidden2, n_outputs, scope="outputs", activation_fn=None)#构建输出层 #注意输出层激活函数不需要

我们在tensorflow.contrib.layers层中导入了fully_connected函数,

通过这个函数,

x就是输入的placeholder数据,

n_hidden1就是当前这一层神经元的个数,

scope=“hidden1”可以理解为名称。

那么只要把这一层的节点数、输入数据告诉我,就可以把这层全连接的结果返回。

这里为什么没有激活函数?

我们看下fully_connected的源码:

@add_arg_scope
def fully_connected(inputs,
                    num_outputs,
                    activation_fn=nn.relu,
                    normalizer_fn=None,
                    normalizer_params=None,
                    weights_initializer=initializers.xavier_initializer(),
                    weights_regularizer=None,
                    biases_initializer=init_ops.zeros_initializer(),
                    biases_regularizer=None,
                    reuse=None,
                    variables_collections=None,
                    outputs_collections=None,
                    trainable=True,
                    scope=None):

解释下:

inputs输入,

num_outputs节点的个数,

activation_fn激活函数的逻辑,默认值是relu,

weights_initializer是w矩阵初始化的方式方法,用的是xavier_initializer,可以理解是类似于正态分布的一种初始化的方法。Xavier定义了一些计算方差的方式。

biases_initializer是zeros_initializer,就是一堆0。

weights_regularizer,biases_regularizer。reg是正则,过拟合提到过L1,L2,这里默认是没有正则,后面可以传东西。

trainable,一般默认True,如果把它改成false,就是不可训练的,意思就是w和b不会更改了。

如图6-DNN简易拓扑图,有输入层,H1,H2,输出层,

 

                                                                                             图6-DNN简易拓扑图

如果构建H1的时候,把Trainable改为false,那么 H1前面的W矩阵就不变了。整个网络在迭代的过程当中,它一定会改H1以后的W矩阵。

如果构建H2的时候,把Trainable改为false, H2前面的W矩阵它也不会变了,那就改其它的矩阵,来最小化损失函数。

什么时候用到这个参数呢? 什么时候希望W固定死了不变?

在已经验证是很好的W的时候。很多情况下,我们踩着别人的肩膀,比如获得第一名,第二名,第三名的这些模型,比如Alexnet,它本身就具备一些图像识别的能力,我们想踩着它的肩膀往上走,我们就把它前面的一些层的W参数固定死不变,只是训练后面层的W参数。这个模型具备一定的图像识别能力了,我们不用从头到尾全部再来训练,因为全部训练,W参数就是上亿个,训练速度很慢。所以我们就留最后一层,或者到倒数第二层让它去更改就行了。

然后第二个隐藏层跟前面是类似的:

hidden2 = fully_connected(hidden1, n_hidden2, scope="hidden2")#构建第二层隐藏层 全连接

第一个隐藏层的输出hidden1作为第二个隐藏层的输入,然后定义一下第二个隐藏层节点个数n_hidden2。

然后构建最后一层:

logits = fully_connected(hidden2, n_outputs, scope="outputs", activation_fn=None)

我们拿第二个隐藏层的输出hidden2作为最后一个输出层的输入,再定一下输出层的节点个数n_outputs,把activation_fn设置None,当然这里也可以去设置activation_fn=tf.nn.softmax,然后计算出最后的结果也是可以的。

我们这里没有这样去用,是因为我们可以接通logits输出层的z,再结合下面的tf.nn.sparse_softmax_cross_entropy_with_logits可以自己计算出交叉熵。

前面就定义好了一个DNN多层的神经网络,接着再往下去定义loss损失。

with tf.name_scope("loss"):
    # 定义交叉熵损失函数,并且求个样本平均
    # 函数等价于先使用softmax损失函数,再接着计算交叉熵,并且更有效率
    # 类似的softmax_cross_entropy_with_logits只会给one-hot编码,我们使用的会给0-9分类号
    xentropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y, logits=logits)#封装好了损失函数
    #把真实的Y值做onehot编码
    loss = tf.reduce_mean(xentropy, name="loss")#求平均

做分类,我们将定义交叉熵损失函数,并且求各样本的平均;

这里介绍一个tensorflow里面的一个很好的封装叫做softmax_cross_entropy_with_logits,另外一个ApI类似的叫做sparse_softmax_cross_entropy_with_logits,with_logits就是用最后输出层的加和结果z来进行计算,然后去算cross_entropy交叉熵,针对softmax。

有没有sparse取决于你给的真实的y是one-hot编码还是0-9的分类号,如果是one-hot编码,就不需要前面加sparse;如果是0-9的分类号,就需要用sparse前缀的ApI。

sparse意思是稀疏,如果分类号是0,那么one-hot编码就是1000000,它就稀疏,加上sparse,它会把这里真实的y,0-9的编号进行one-hot编码,变成稀疏的形式,然后再去算。如果没有用sparse,后面的labels必须是one-hot编码形式的。

xentropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y, logits=logits)#封装好了损失函数

loss = tf.reduce_mean(xentropy, name="loss")#求平均

Tf.nn. sparse_softmax_cross_entropy_with_logits函数可以直接传参真实的y,传参预测出来的z,这里就是我们的logits。通过这个函数直接可以得到所有样本的整体的交叉熵。然后去求一个均值,用tf.reduce_mean,对整体的交叉熵求一下平均值,这样就得到loss损失函数。

之后还需要训练的步骤,用梯度下降optimizer,传学习率learning_rate,然后最小化loss损失,最终得到training_op。

with tf.name_scope("train"):
    optimizer = tf.train.GradientDescentOptimizer(learning_rate)#创建梯度下降的优化器
    training_op = optimizer.minimize(loss)#最小化损失

除了训练,还要评估,要算一下acc,来获取logits里面最大的那1位和y比较类别号是否相同,相同的返回true,不同的返回false。

with tf.name_scope("eval"):#评估
    # 获取logits里面最大的那1位和y比较类别好是否相同,返回True或者False一组值
    correct = tf.nn.in_top_k(logits, y, 1)#logits返回是类别号 y也是类别号
    accuracy = tf.reduce_mean(tf.cast(correct, tf.float32))#转成1.0 0.0

这里的logits每个样本会得到十个值。Logits是输出层得到的结果,没有经过激活函数,相当于最后十个节点的加和的输出z。

拿这个输出来比较最大最小合适吗?不合适,要看激活函数和它正相关还是负相关。

softmax函数是

h(\theta)x=p\left(y^{(i)}=j | x^{(i)} ; \theta\right)=\frac{e^{\theta_{j}^{T} x^{(i)}}}{\sum_{l=1}^{k} e^{\theta_{l}^{T} x^{(i)}}}                                       

它是某一个类别的概率。如果是p1,分子就是e^z1,如果是p2,分子就是e^z2,分母不变,意味着概率的大小取决于分子的大小,ex是单调递增函数,等价于看p1,p2谁大谁小,相当于是看z1,z2谁大谁小。p0到p9是真正的y_pred ,z0到z9没有经过激活函数非线性变化的logits。如图7-概率和加和结果输出示意图

                                                                               图7-概率和加和结果输出示意图

所以这是为什么我们说从p0一直到p9十个概率里面取最大一个,等价于从z0到z9里面取最大一个, 所以从logits里面取最大的一个,和经过非线性变化之后取最大一个,是一样的,等价的。

这就是为什么在评估的时候,我们可以直接拿logits和真实的y来比较的原因。

这里API叫in_top_k,我们看下源码:

This outputs a `batch_size` bool array, an entry `out[i]` is `true` if the
prediction for the target class is finite (not inf, -inf, or nan) and among
the top `k` predictions among all predictions for example `i`. Note that the
behavior of `InTopK` differs from the `TopK` op in its handling of ties; if
multiple classes have the same prediction value and straddle the top-`k`
boundary, all of those classes are considered to be in the top `k`.

如果传的是batch_size个数据,那么predictions就是batch_size个数据,那么targets真实的也是batch_size个数据,真实返回true,错误返回false,所以就返回了batch_size个true和false放到数组里。

这里延伸下:

图像识别每年时间都有大赛,大赛评估两个指标,一个指标叫Top 1 error,一个指标叫Top 5 error,通过这两个指标的排名,给你打分排名。

Top 1 error,如果真实的类别是A这个类别,我给你判成了B这个类别,判错了,而B这个类别是模型怎么给出来的?比如有一百个类别,模型对应100个类别分别给出一个概率值,B就是这个类别概率最大的。Top 1 error就是只取概率最大的那一个去跟正确答案去对比,如果错,就是错了;如果对,就对了,这个就是平时理解的正确率。

Top 5 error,真实的是A就是A改变不了,然后100个类别,模型取前五个概率最大的,比如ABCDE,有可能D是概率最大的,E是排名第二的,C是排名第三的,A排名第4,B是排名第5,只要模型给前5个类别里面有包含,在Top 5 error就算对。如果给出前五个里面都没有按正确的,那说明就是错的。

mnist = input_data.read_data_sets("MNIST_data/")
n_epochs = 400  # 运行400次
batch_size = 50  # 每一批次运行50个

with tf.Session() as sess:
    init.run()
    for epoch in range(n_epochs):
        for iterationo in range(mnist.train.num_examples // batch_size):  # 总共多少条/批次大小
            X_batch, y_batch = mnist.train.next_batch(batch_size)  # 每次传取一小批次数据
            # _, _logits, _y, _correct = sess.run([training_op, logits, y, correct],
            #                                     feed_dict={X: X_batch, y: y_batch})  # 传递参数
            # print("_logiss", _logits)
            # print("_y", _y)
            # print("_correct", _correct)
            sess.run([training_op, logits, y, correct], feed_dict={X: X_batch, y: y_batch})  # 传递参数
        acc_train = accuracy.eval(feed_dict={X: X_batch, y: y_batch})  # 每运行一次 看训练集准确率
        acc_test = accuracy.eval(feed_dict={X: mnist.test.images,  # 每运行一次 看测试集准确率
                                            y: mnist.test.labels})
        print(epoch, "Train accuracy:", acc_train, "Test accuracy:", acc_test)

最后传入批次数据,开始训练我们的模型。

若将最后

sess.run(training_op, feed_dict={X: X_batch, y: y_batch})#传递参数

这一行改为:

_, _logits, _y, _correct = sess.run([training_op, logits, y, correct],
                                    feed_dict={X: X_batch, y: y_batch})  # 传递参数
print("_logiss", _logits)
print("_y", _y)
print("_correct", _correct)

意思是在每一次拿50张图片训练的时候,结果并不重要的用下划线来接,

这样话在每一批次来执行的时候,看一下中间结果的内容,可以通过这种方式来接一下,然后在每一个轮次执行完之后,打印一下测试集的准确率。

运行结果如下:

_y里是真实的类别号如图8-真实的类别号:

                                                                                                图8-真实的类别号

_correct是最后的预测结果如图9-最后的预测结果。相当于一条样本给出的10个概率值,它会取最大的一个,去跟真实的编号比较一下,如果对的话,就是true,如果不对的话就是false。

                                                                                                图9-最后的预测结果

_logits是每一条样本预测10个类别的具体概率,如图10-样本预测10个类别具体概率


                                                                                  图10-样本预测10个类别具体概率

我们去掉具体的细节打印正常运行之后就会发现如图11-DNN准确率:

 

                                                                                            图11-DNN准确率

第一个轮次执行的时候,一起步就比softmax高,所以网络层次越深,准确率就越高,因为DNN第一个隐藏层300个节点,第二个隐藏层100个节点,而softmax根本没有这两个隐藏层。即使再大轮次,softmax也就学到90%这个样子,这是算法本身的决定的。

                                                                                                 图12-DNN135轮次后准确率

到135个轮次的时候,基本上就稳定在97.9%几了,如图12-DNN135轮次后准确率,在这震荡,这就是它的瓶颈了,所以400轮次也没有必要了。

 

五、模型保存和使用

之前把这个模型存了一下,怎么使用模型?只要有saver,就可以用saver.restore重新加载这个模型,如下模型调用方法

# 使用模型预测
with tf.Session as sess:
    saver.restore(sess, "./my_dnn_model_final.ckpt")
    X_new_scaled = [...]
    Z = logits.eval(feed_dict={X: X_new_scaled})
    y_pred = np.argmax(Z, axis=1)  # 查看最大的类别是哪个

然后可以传一些新的数据,把它作为一个字典X_new_scaled里面的一个值传给X,然后用logits.eval ,把新的X传进来,得到结果Z,如果想要看预测哪个类别的话,可以使用np.argmax,传给它Z,告诉它按照哪一列来取最大,就可以得到最后的类别号y_pred。

argmax在TensorFlow里面也有,不管是numpy还是TensorFlow,它们都有相对应的函数的方法,logits.eval最后返回的是最终的结果,它不再是tensor,而sess.run返回的是tensor不是最终的结果。

 

六、提升准确率方案

如果想把准确率再提高,可以增加节点的数量,或者隐藏层的数量,或者加样本。

从特征工程,从数据角度去下手,希望把数据更好的特征提出来,相当于同样的数据站在不同角度去审视同一份数据。找特征,在图像识别这一块,可以通过卷积的一个计算,来找出更多的特征。

改变节点和改变层次相当于从算法本身入手。

增加样本的数量,就让它学更多的东西。从概率的角度上来说,就是尽可能的让模型见多识广,这样未来碰到新的数据的时候,它能找到跟我学过的类似的特点进行判断,以更大的面去覆盖未来的点。

mnist每张图片的特征是像素点,每张图片是784个像素点,第一个隐藏层有300个特征,相当于从784个维度降到了300个维度,只不过它从784个维度提取300个特征是根据H1前面训练出来的w矩阵参数来转换的。

H2有100个节点,也就是说从300个特征再一次提取100个,取决于H2前面训练出来的w矩阵是什么样子。

本质上这张图片784个像素点,如果一开始用softmax回归来做的话,相当于就是784个特征。

如果用DNN来做的话,相当于784个特征,站在各个角度审视这张图片,提取了各种各样的特征,提取300个特征,然后再站在不同角度去审视它,变成了100个特征。然后根据这100个特征再去判别它属于10个类别里的哪一个类别。

七、引申和总结--零初始化,梯度消失和反向传播

我们构建了DNN多层神经网络,有两种方式去构建:

第一种方式,是一层层的把正向传播的计算写出来;

第二种方式是如果你知道它的连接方式是全连接,可以直接调用fully connected的函数,然后去构建每一层。

我们看下图13-DNN拓扑举例,隐藏层有多少个w?

​                                                                                             图13-DNN拓扑举例

这里需要看上一层有多少个输入,和当前一层有多少个神经元的节点,从下往上看分别是输入层,隐藏层,输出层。所以这里W矩阵就是两行四列,bias就是4个值。

隐藏层是4个节点,输出层是3个节点,所以W是四行三列,bias是3个值。

因为输入层是不用设置的,我们直接从隐藏层,输出层去写段落层次就可以了。

Backpropagation反向传播,就是我们不用自己推导每一个维度所对应的公式了,而是在多层神经网络里面,当我们有ŷ 和y的时候,可以得到loss损失,根据loss的逻辑,再根据每个线上的w,来求得每根线上的梯度。如果我们调w1所对应的g1,我们要去找一下从g1到最后loss经历哪些正向传播。

比如我们要求如下图14-反向传播示意图中的粉色线梯度:

                                                                                          图14-反向传播示意图

就要反过去从loss往前推,反向传播,所以第一根线会有上面很多条方向线的贡献,具体的梯度怎么求,看相关联的节点的计算逻辑是什么。

我们用optimizer.minimize,它里面其实有两步:

with tf.name_scope("train"):
    optimizer = tf.train.GradientDescentOptimizer(learning_rate)  # 创建梯度下降的优化器
    training_op = optimizer.minimize(loss)  # 最小化损失

第一步是compute_gradients里面就会做反向传播,根据所有的grads变量或者vars变量求出所有的梯度。然后它会把这些梯度通过apply_gradients应用到每个w上面去,再来更新它。

对于神经网络来说,它会先调更靠近输出层的w矩阵,因为它是反着来的,反过来其实更容易调,所以越靠近前面输入层的w矩阵,调得越费劲。

再来说下梯度消失,所谓梯度消失,就是梯度反下来一层层传的时候,传到前面输入层的时候,梯度就接近于0,梯度接近0意味着wt+1=wt-αg,g越接近零,-αg就越接近零,那w就没有什么可调的了。所以梯度消失这个问题是不希望出现的。

为什么w,b的初始化,都给0或者是接近于0,其实有两点:

第一点站在正则的角度去想,L1,L2的w,b并不期望它的绝对值加和或者平方加和太大,所以在初始化的时候就把它设置小一点,这样在调的过程当中它也不会跑得太大,那L1,L2的值就不会太大。这样可以增加模型的推广能力,泛化能力。

第二点是w的值越接近于零,就越能避免梯度消失的问题。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐