1. 说明

之前手搓了一个全连接(FC)神经网络, 现在用 TensorFlow 低阶API 重新实现一遍, 在理解了神经网络的工作原理之后,框架对于学习无疑是最好的选择,熟练使用框架能压缩构架模型的时间以及排除了大量难以发现(或者说,你发现了,但是解决不了)的 Bug 。 由于之前手搓的文章,已经解决一些预处理等问题, 所以现在就不会再次提出, 但是为了保证代码版本(我不确定有没有修改过),一切辅助函数都会在最后再次贴出。

2. 思路

使用 TensorFlow 建立模型的过程, 跟手搓其实差不多。依然是:

  1. 初始化
  2. 前向传播

由于前向传播的过程决定了反向传播的过程,所以 TensorFlow 依据前向传播来推导出了反向传播, 这不仅大大减少了工作量,也在某种角度上来说,使得大多数人都能够使用和学习机器学习。

按得上面的思路, 就开始实现了, 这里说一下数据的 Shape,X_train.shape == [784, 60000], Y_train.shape == [10, 60000], X_test.shape == [784, 10000], Y_test.shape == [10, 10000]

3. 初始化

3.1 Placeholder ——TensorFlow 张量

观察下面代码:

def create_placeholder(nx, ny):
    
    X = tf.placeholder(dtype = tf.float64, shape = [784, None], name = 'X')
    Y = tf.placeholder(dtype = tf.float64, shape = [ 10, None], name = 'Y')
    
    return X, Y

上面的函数输入两个参数, 返回两个在 TensorFlow 中叫做 Placeholder (占位符)的张量(Tensor)。 首先要知道在 TensorFlow 比较重要的概念有 Op 以及 Tensor, 其中 Op 表示操作(各种运算), Tensor 就是我们平时理解的用来运算数据(变量或者常量)。 自然 Placeholder 应该归为 Tensor 一类,可以看下面在IPython中的实验(其结果是我们所得理解和接受的):
在这里插入图片描述
另外,建议使用在IPython环境下实验,例如:输入tf.placeholder?即可以显示关于 placeholder 的用法, 不必死记硬背,自然函数说明是英文。

可以发现上面的 Shape = [784, None] ,784 表示输入的特征个数(图片为 28*28),这个自然是固定的; None表示不确定的值, 这里自然是我们的数据的个数。 以这样的方式创建的占位符会自动检查传入的数据的格式为 [784, m] (m>=1,且m为整数)。 因为输入数据的 Shape 的不确定性, 所以 TensorFlow 提供了 Placeholder 来表示输入。

3.2 参数初始化

参数初始化的基本跟手搓的一样:

def initialize_parameters(layers_dims):
    
    parameters = {}
    
    L = len(layers_dims)
    for l in range(1, L):
        parameters['W' + str(l)] = tf.get_variable(name = 'W' + str(l), shape = [layers_dims[l], layers_dims[l-1]], dtype = tf.float64,
                                                   initializer = tf.contrib.layers.xavier_initializer())
        parameters['b' + str(l)] = tf.get_variable(name = 'b' + str(l), shape = [layers_dims[l], 1], dtype = tf.float64,
                                                   initializer = tf.initializers.zeros())
    
    return parameters

这里仅多了一个知识点就是 tf.get_variable()的用法,自然可以用上面提到的 IPython 的帮助,这里演示一次,下面是部分截图:
在这里插入图片描述
嗯, 参数意外的多, 但是我们在这里能用到却仅有几个。 上面会有很多信息,但是必需要看的是 Docstring(大致告诉了我们,此方法的作用) :用提供的参数返回一个存在的变量或者创建一个新的变量。 那么我们这里便是创建新的变量, 用到的参数还有便是 initializer,这个参数指示了给当前这个变量指定一个初始化器,也就是创建这个变量时,他的值是什么。大多数初识化器在 tf.initializers 中,到官网文档https://tensorflow.google.cn/api_docs/python/tf/initializers ,可以看到各个初始化器的说明。但是上面却用到了 tf.contrib.layers.xavier_initializer(),我想这些不唯一的用法便是自学的难点,如果难以接受,便到 tf.initializers找到自己熟悉的参数初始化方法,然后使用他,就像对偏差的一样。

4. 前向传播及计算损失

参数初始化完毕,然后就可以开始前向传播了,由于 TensorFlow 已经封装好了激活函数,所以不必再将线性传播和激活传播分开实现,而是整合到一起,如下:

def forward_propogation(X, parameters):
    
    L = len(parameters) // 2
    
    A_prev = X
    for l in range(1, L):
        
        '''
            下面的计算可以换成这样,tensorflow 已经实现的运算符重载。 但是部分框架是未实现重载的, 但是又允许这样的运算
            不过可能会报错,而这种错误就很难发现出现。所以,需要记住的是,除非官方文档明确说明已实现重载, 否则还是老老实实这样写。
            就如 Keras 一样。
            tf.matmul(parameters['W' + str(l)], A_prev) + parameters['b' + str(l)]
        '''
        Z = tf.add( tf.matmul( parameters['W' + str(l)], A_prev), parameters['b' + str(l)] ) 
        A_prev = tf.nn.relu(Z)
    
    ZL = tf.add( tf.matmul(parameters['W' + str(L)], A_prev), parameters['b' + str(L)] )
    
    return ZL

矩阵相乘的用法为 tf.matmul(A, B) ,这里返回的是 ZL, 不是之前的 AL,在给出计算损失之后会给出解释。自然的,前向传播完成后,就可以计算损失,如下:

def compute_cost(ZL, Y):
    
    logits = tf.transpose(ZL)
    labels = tf.transpose(Y)
    
    cost = tf.nn.softmax_cross_entropy_with_logits_v2(logits=logits, labels=labels)
    cost = tf.reduce_mean(cost)
    
    return cost

这里在计算前,先进行的矩阵转置,即 Shape 为 [None, 10],这是TensorFlow中的格式,并没有什么好说的。然后就跟我们平时计算损失不太一样,以前计算可能是以下实现比较能理解:

AL = tf.nn.softmax(tf.matmul(x,W) + b)
cross_entropy = -tf.reduce_mean(Y*tf.log(AL ))
'''
 其实
 cost =  tf.nn.softmax_cross_entropy_with_logits_v2(logits=logits, labels=labels)
 就是
 AL = tf.nn.softmax(ZL)
 cost = Y*tf.log(AL)
 把两步整合成为了一步, 最后tf.reduce_mean()就是和然后计算均值。
'''

还有其他损失的计算也都在 tf.nn 中,可以到官网查阅,你会看到熟悉的。

5. 模型

已经说过Tensorflow 不需要实现反向传播, 所以我们已经可以整合一起,然后训练模型了,下面给出实现,由于代码有一定长度,为了阅读方便,就在注释中讲解:

def model(X_train, Y_train, X_test, Y_test, layers_dims, learning_rate, num_epoch = 5, minibatch_size = 128):
    
    # 获得特征的个数、标签类别数以及
    nx = X_train.shape[0]
    ny = Y_train.shape[0]
      
    
    #创建X、Y占位符和参数
    X, Y = create_placeholder(nx, ny)    
    parameters = initialize_parameters(layers_dims)
    seed = 0
    costs = []


    #在这里我们先“完成”了前向传播和损失的计算。
    ZL = forward_propogation(X, parameters)
    cost = compute_cost(ZL, Y)
    
    """
        tf.train 中有各种优化器, 这里选择了 Adam 优化器,仅修改了学习率, 其他参数使用 Adam 的默认值
        用法也是如此, 最后 minimize() 将我们的 cost 传进去, 表示要最小化这个值。 记得我们手搓的时候
        也是通过最小化 cost 来实现优化模型的。
    """
    optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)
    
    
    """
        这个是生成一个初始化器,这里表示的是全局(并不是常识中的全局,而是属于 global 这个集合中的变量,
        TensorFlow 中每个变量都属于一个或者多个集合)变量的初始化器。
        
        为什么要弄个初始化器呢? 这跟 TensorFlow 的机制有关, TensorFlow 先要求我们创建一个计算图,然后
        根据此图完成计算图任务,这样做的目的是为了减少开销,将所有计算任务集中起来一起计算,减少从 Python
        到 C++ 的切换开销。
        
        其实在我们完成初始化和前向传播以及损失的计算和生成 optimizer (上面的优化器)时,我们的计算图就构建
        完成了(后面利用 Tensorboard 可视化我们的计算图)。请注意, 按照 TensorFlow 的机制我们仅是创建了一个
        图, 没有进行任何计算任务, 也就是变量实际上都没有进行初始化。 所以我们需要在运行图前先初始化变量,
        (至于为什么还分集合,等你该懂得的时候,你自然就会懂得了)。
    """
    init = tf.global_variables_initializer()
    
    
    #创建一个会话, 创建了会话之后才会开始调用资源,所以使用后,记得关闭会话以释放资源
    with tf.Session() as sess:
        #先初始化一下变量
        sess.run(init)
        
        for epoch in range(num_epoch):
            seed = seed + 1
            #已实现的随机小批量算法
            minibatches = random_mini_batches(X_train, Y_train, minibatch_size, seed)
            epoch_cost = 0
                        
            for minibatch in minibatches:
                
                (minibatch_X, minibatch_Y) = minibatch
                """
                    这里run时,可以以dict 或者 list 的方式传入需要 run 的 Tensor 或者 Op。
                    由于会对应的返回一个 None 和 数值,我们不需要 None。另外记得 X、Y都为Placeholder
                    并没有真正的值,所以我们在运行时将小批量传过去,格式如下。
                """
                _, minibatch_cost = sess.run([optimizer, cost], feed_dict = {X:minibatch_X, Y:minibatch_Y})
                epoch_cost += minibatch_cost
            
            epoch_cost = epoch_cost / len(minibatches)
            
            print(epoch_cost)
            
            costs.append(epoch_cost)
        
        
        plt.plot(costs)
        plt.xticks([])
        plt.yticks([])
        plt.xlabel('iteration nums')
        plt.show()
                
        """
            这里就是对模型的评估, 代码还是比较好理解的。 先得出真正的label以及预测的label
            然后以element-wise 的方式判断是否相等,其数据类型为 bool
            
            在计算平均值时,先转到 float 型。
            
            这里参与运算的是, Y (占位符), ZL(前向传播结果)。由于前向传播的过程需要用到
            X、Y占位符,所以下面 run() 的时候,传入了数据给X、Y(可以理解成为对X、Y的初始化)。
            
        """
        prediction = tf.equal( tf.argmax(Y), tf.argmax(ZL) )
        accuracy = tf.reduce_mean( tf.cast( prediction, float ) )
        
        #在构建了图后, 传入不同的数据,就会有不同的结果。
        train_acc = sess.run(accuracy, feed_dict = {X:X_train, Y:Y_train})       
        print('Train accuracy:%.2f%%' % (train_acc*100))    
        test_acc = sess.run(accuracy, feed_dict = {X:X_test, Y:Y_test})
        print('Train accuracy:%.2f%%' % (test_acc*100))
        
        

测试及输出:

tf.reset_default_graph()

layers_dims = [784, 128, 10]
learning_rate = 0.0005
model(X_train, Y_train, X_test, Y_test, layers_dims, learning_rate=learning_rate)

在这里插入图片描述
嗯。。可以发现学习的过程居然没有波动呢。。真是可以的。最后给出创建的计算图,有点小。
在这里插入图片描述

计算图的生成代码如下:

writer = tf.summary.FileWriter('.')
writer.add_graph(tf.get_default_graph())

这样会在当前目录下生成一个文件,然后在当前目录下运行CMD,输入tensorboard --logdir . 接着会提醒你在浏览器输入 localhost:6006(不确定你们的是不是这样) 就会打开。还有为了防止,计算图变得乱七八糟, tf.reset_default_graph()就可以重置。

6. 总结

通过上面的例子,要理解的 TensorFlow 概念有:

  1. 计算图的构建与运算之间的关系
  2. Op 及 Tensor
  3. Session

利用 TensorFlow 进行试验的步骤也如下:

  1. 构建计算图
  2. 创建Session
  3. 初始化变量
  4. 执行计算图

其他方法需要自己去探索, TensorFlow 会将一类相关的方法放在一起,例如前面提到的 tf.nn 和 tf.initializers

可能用到的辅助函数:

def load_mnist(path, kind='train'):
    import os
    import gzip
    import numpy as np

    """Load MNIST data from `path`"""
    labels_path = os.path.join(path,
                               '%s-labels-idx1-ubyte.gz'
                               % kind)
    images_path = os.path.join(path,
                               '%s-images-idx3-ubyte.gz'
                               % kind)

    with gzip.open(labels_path, 'rb') as lbpath:
        labels = np.frombuffer(lbpath.read(), dtype=np.uint8,
                               offset=8)

    with gzip.open(images_path, 'rb') as imgpath:
        images = np.frombuffer(imgpath.read(), dtype=np.uint8,
                               offset=16).reshape(len(labels), 784)
        
    images = images / 255.0
    images = images.T
    labels = labels.reshape(labels.shape[0], 1).T
    temp = np.zeros((10, labels.shape[1]))
    temp[labels, np.arange(labels.shape[1])] = 1

    return images, temp
def random_mini_batches(X, Y, mini_batch_size = 512, seed = 1):
    m = X.shape[1]
    mini_batches = []
    np.random.seed(seed)
    
    permutation = np.random.permutation(m)
    
    shuffled_X = X[:, permutation]
    shuffled_Y = Y[:, permutation]
    
    num_complete_minibatches = m // mini_batch_size
    
    for i in range(num_complete_minibatches):
        mini_batch_X = shuffled_X[:, i*mini_batch_size : (i+1)*mini_batch_size]
        mini_batch_Y = shuffled_Y[:, i*mini_batch_size : (i+1)*mini_batch_size]
        
        mini_batch = (mini_batch_X, mini_batch_Y)
        mini_batches.append(mini_batch)
    
    if m % mini_batch_size != 0:
        mini_batch_X = shuffled_X[:, num_complete_minibatches*mini_batch_size:]
        mini_batch_Y = shuffled_Y[:, num_complete_minibatches*mini_batch_size:]
        
        mini_batch = (mini_batch_X, mini_batch_Y)
        mini_batches.append(mini_batch)
        
    return mini_batches

最后, TensorFlow 中已经有其他方法实现了不用手搓一个随机小批量的方法,自己去学习哟,点我传送

Logo

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

更多推荐