• 第5章 MNIST数字识别问题

    • 5.1 MNIST数据处理
    • 5.2 神经网络的训练以及不同模型结果的对比
      • 5.2.1 TensorFlow训练神经网络
      • 5.2.2 使用验证数据集判断模型的效果
      • 5.2.3 不同模型效果比较
    • 5.3 变量管理
    • 5.4 TensorFlow模型持久化
      • 5.4.1 持久化代码实现

        • 1. ckpt文件的保存
        • 2. 加载已经保存的TensorFlow模型
      • 5.4.2 持久化原理及数据格式
    • 5.5 最佳实践样例程序
      • 1. 定义神经网络的前向传播过程
      • 2. 训练程序
      • 3. 代码
    • 5.6 补充
      • 5.6.1 单层神经网络
      • 5.6.2 梯度下降
      • 5.6.3 学习速率衰减
      • 5.6.4 过拟合和dropout

第5章 MNIST数字识别问题

5.1 MNIST数据处理

MNIST数据集是NIST数据集的一个子集,包含60000张作为训练数据,10000张作为测试数据,图片大小为28*28。

  • TensorFlow提供了一个类来处理MINIST数据,会自动下载并转化MINIST数据的格式,解析成训练和测试神经网络时的使用格式
from tensorflow.examples.tutorials.mnist import input_data#载入MNIST数据集,如果指定地址没有下载好的数据,则会自动下载
mnist = input_data.read_data_sets("path/to/mnist data/",one_hot=True)
# 该函数会自动将MNIST数据集划分为train/test/validation(验证)三个数据集

读取mnist数据集的程序如下

# 在Yann LeCun教授的网站中(http://yann.lecun.com/exdb/mnist ) 对MNIST数据集做出了详细的介绍。
# 1. 读取数据集,第一次TensorFlow会自动下载数据集到下面的路径中
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)# 2. 数据集会自动被分成3个子集,train、validation和test。以下代码会显示数据集的大小。
print("Training data size: ", mnist.train.num_examples)
print("Validating data size: ", mnist.validation.num_examples)
print("Testing data size: ", mnist.test.num_examples)# 3. 查看training数据集中某个成员的像素矩阵生成的一维数组和其属于的数字标签。
print("Example training data: ", mnist.train.images[0])
print("Example training data label: ", mnist.train.labels[0])# 4. 使用mnist.train.next_batch来实现随机梯度下降。
batch_size = 100
xs, ys = mnist.train.next_batch(batch_size)    # 从train的集合中选取batch_size个训练数据。
print("X shape:", xs.shape)
print("Y shape:", ys.shape)

输出结果

Extracting MNIST_data/train-images-idx3-ubyte.gz
Extracting MNIST_data/train-labels-idx1-ubyte.gz
Extracting MNIST_data/t10k-images-idx3-ubyte.gz
Extracting MNIST_data/t10k-labels-idx1-ubyte.gzTraining data size:  55000
Validating data size:  5000
Testing data size:  10000
Example training data:[太多了,不列举了]
Example training data label:  [ 0.  0.  0.  0.  0.  0.  0.  1.  0.  0.]
X shape: (100, 784)
Y shape: (100, 10)

5.2 神经网络的训练以及不同模型结果的对比

  神经网络在验证数据集上的表现可以近似的作为评价不同神经网络模型的标准或决定迭代次数的依据。

  为了评测神经网络模型在不同参数下的效果,一般会从训练数据中抽取一部分作为验证数据。使用验证数据就可以评判不同参数取值下模型的表现。除了使用验证数据集,还可以采用交叉验证(cross validation)的方式来验证模型效果,但因为神经网络训练实践本身就比较长,采用cross validation会花费大量时间。所以在海量数据的情况下,一般会更多地采用验证数据集的形式来评测模型的效果。

  为了说明验证数据在一定程度上可以作为模型效果的评判标准,我们将对比在不同迭代轮数的情况下,模型在验证数据和测试数据上的正确率。为了同时得到同一个模型在验证数据和测试数据上的正确率,可以在每1000轮的输出中加入在测试数据集上的正确率。

  在神经网络结构的设计上,需要使用激活函数和多层隐藏层。在神经网络优化时,可以使用指数衰减的学习率,加入正则化的损失函数以及滑动平均模型。
  

5.2.1 TensorFlow训练神经网络

# 《TensorFlow实战Google深度学习框架》05 minist数字识别问题
# filename:ts05.02.py # TensorFlow训练神经网络--全模型import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data# 1.设置输入和输出节点的个数,配置神经网络的参数
INPUT_NODE = 784      # 输入节点个数,对于该数据集就是图片像素总数
OUTPUT_NODE = 10      # 输出节点,等同于类别个数
LAYER1_NODE = 500     # 隐藏层数,此处使用仅有一个隐藏层,500个节点BATCH_SIZE = 100      # 一个batch中样本个数,数字越小,越接近随机梯度下降,数字越大,越接近梯度下降# 模型相关的参数
LEARNING_RATE_BASE = 0.8      # 基础学习率
LEARNING_RATE_DECAY = 0.99    # 学习率的衰减率
REGULARAZTION_RATE = 0.0001   # 描述模型复杂度的正则化项在损失函数中的系数
TRAINING_STEPS = 30000        # 训练次数
MOVING_AVERAGE_DECAY = 0.99   # 滑动平均衰减率# 2. 定义辅助函数来计算前向传播结果,使用ReLU做为激活函数。
def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2):# 不使用滑动平均类if avg_class == None:layer1 = tf.nn.relu(tf.matmul(input_tensor, weights1) + biases1)return tf.matmul(layer1, weights2) + biases2else:# 使用滑动平均类layer1 = tf.nn.relu(tf.matmul(input_tensor, avg_class.average(weights1)) + avg_class.average(biases1))return tf.matmul(layer1, avg_class.average(weights2)) + avg_class.average(biases2)# 3. 定义训练过程
def train(mnist):x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')y_ = tf.placeholder(tf.float32, [None, OUTPUT_NODE], name='y-input')# 生成隐藏层的参数。weights1 = tf.Variable(tf.truncated_normal([INPUT_NODE, LAYER1_NODE], stddev=0.1))biases1 = tf.Variable(tf.constant(0.1, shape=[LAYER1_NODE]))# 生成输出层的参数。weights2 = tf.Variable(tf.truncated_normal([LAYER1_NODE, OUTPUT_NODE], stddev=0.1))biases2 = tf.Variable(tf.constant(0.1, shape=[OUTPUT_NODE]))# 计算不含滑动平均类的前向传播结果y = inference(x, None, weights1, biases1, weights2, biases2)# 定义训练轮数及相关的滑动平均类global_step = tf.Variable(0, trainable=False)variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)variables_averages_op = variable_averages.apply(tf.trainable_variables())average_y = inference(x, variable_averages, weights1, biases1, weights2, biases2)# 计算交叉熵及其平均值cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=tf.argmax(y_, 1))cross_entropy_mean = tf.reduce_mean(cross_entropy)# 损失函数的计算regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)regularaztion = regularizer(weights1) + regularizer(weights2)loss = cross_entropy_mean + regularaztion# 设置指数衰减的学习率。learning_rate = tf.train.exponential_decay(LEARNING_RATE_BASE,global_step,mnist.train.num_examples / BATCH_SIZE,LEARNING_RATE_DECAY,staircase=True)# 优化损失函数train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)# 反向传播更新参数和更新每一个参数的滑动平均值with tf.control_dependencies([train_step, variables_averages_op]):train_op = tf.no_op(name='train')# 计算正确率correct_prediction = tf.equal(tf.argmax(average_y, 1), tf.argmax(y_, 1))accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))# 初始化回话并开始训练过程。with tf.Session() as sess:tf.global_variables_initializer().run()validate_feed = {x: mnist.validation.images, y_: mnist.validation.labels}test_feed = {x: mnist.test.images, y_: mnist.test.labels}# 循环的训练神经网络。for i in range(TRAINING_STEPS):if i % 1000 == 0:validate_acc = sess.run(accuracy, feed_dict=validate_feed)print("After %d training step(s), validation accuracy using average model is %g " % (i, validate_acc))xs, ys = mnist.train.next_batch(BATCH_SIZE)sess.run(train_op, feed_dict={x: xs, y_: ys})test_acc = sess.run(accuracy, feed_dict=test_feed)print(("After %d training step(s), test accuracy using average model is %g" % (TRAINING_STEPS, test_acc)))# 4. 主程序入口,这里设定模型训练次数为5000次。
def main(argv=None):mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)train(mnist)if __name__=='__main__':tf.app.run()

结果:

After 0 training step(s), validation accuracy using average model is 0.111
After 1000 training step(s), validation accuracy using average model is 0.977
After 2000 training step(s), validation accuracy using average model is 0.9824
After 3000 training step(s), validation accuracy using average model is 0.9832
After 4000 training step(s), validation accuracy using average model is 0.9848
After 5000 training step(s), test accuracy using average model is 0.9822
...

由以上结果可以看出,在训练初期,随着训练的进行,模型在验证数据集上的表现越来越好,但从4000轮开始,模型在验证数据集上的表现开始波动,这说明模型已经接近极小值了,所以迭代已经可以结束了。

5.2.2 使用验证数据集判断模型的效果

  • 所需初始参数:上述程序的开始,设置了初始学习率、学习率衰减率、隐藏层节点数、迭代次数、batch_size、正则项系数、滑动平均衰减数等7个不同的参数。

  • 如何设置初始参数:一般情况需要实验来调整

  • 设置初始参数难点:虽然模型的最终效果是在测试数据上进行判定的,但不能直接使用测试数据,否则会过拟合,从而丢失对未知数据的判断能力,所以要保证测试数据在训练过程中是不可见的。

  • 解决方法:

    1. 从训练数据中抽取一部分作为验证数据,来评判不同参数取值下模型的表现。
    2. 使用“交叉验证(cross validation)”,但是神经网络的训练时间本身就比较长,所以采用该方法会花费大量的时间,故一般不会选用。

下面使用代码来对比模型在不同迭代次数情况下,在验证数据和测试数据上的正确率。

# 为了同时得到模型在测试数据和验证数据上的正确率,可以在每1000轮的输出中加入在测试数据集上的正确率for i in range(TRAINING_STEPS):if i % 1000 == 0:# 计算滑动平均模型在测试数据集和验证数据集上的正确率validate_acc = sess.run(accuracy, feed_dict=validate_feed)test_acc = sess.run(accuracy, feed_dict=test_feed)print("After %d training step(s), validation accuracy using average model is %g ,test accuracy using average model is %g" % (i, validate_acc,test_acc))xs, ys = mnist.train.next_batch(BATCH_SIZE)sess.run(train_op, feed_dict={x: xs, y_: ys})

结果:

After 0 training step(s), validation accuracy using average model is 0.1114 ,
test accuracy using average model is 0.109
After 1000 training step(s), validation accuracy using average model is 0.9778 ,
test accuracy using average model is 0.9741
After 2000 training step(s), validation accuracy using average model is 0.9822 ,
test accuracy using average model is 0.9798
After 3000 training step(s), validation accuracy using average model is 0.9838 ,
test accuracy using average model is 0.9816
After 4000 training step(s), validation accuracy using average model is 0.9846 ,
test accuracy using average model is 0.9824
After 5000 training step(s), validation accuracy using average model is 0.9852 ,
test accuracy using average model is 0.9823
After 6000 training step(s), validation accuracy using average model is 0.9858 ,
test accuracy using average model is 0.9829
After 7000 training step(s), validation accuracy using average model is 0.985 ,
test accuracy using average model is 0.983
After 8000 training step(s), validation accuracy using average model is 0.9854 ,
test accuracy using average model is 0.9831

可以看出,验证集和测试集虽然不完全一样,但是其趋势相同,且它们的相关系数为0.9999,意味着在MNIST数据集上完全可以通过模型在验证数据集上的表现来判断一个模型的优劣。

5.2.3 不同模型效果比较

在神经网络结构的设计上,需要使用激活函数和多层隐藏层。在神经网络优化时,可以使用指数衰减的学习率,加入正则化的损失函数以及滑动平均模型。
1. 本质影响:神经网络的结构
2. 滑动平均模型、指数衰减的学习率、正则化对MNIST数据集影响看起来不是很大的原因:

  • 因为滑动平均模型、指数衰减的学习率都在限制神经网络的参数的更新速度,而该数据库模型收敛的速度很快,所以影响不大。
  • 但是当问题更复杂时,迭代不会很快收敛,所以滑动平均模型、指数衰减的学习率可以发挥更大的作用。
  • 正则化带来的效果更为显著

总结:优化方法可以对模型带来更好的效果(模型越复杂,效果越明显)

5.3 变量管理

当神经网络的结构复杂、参数很多时,需要更好的方式来传递和管理神经网络的参数。
TensorFlow提供了通过变量名称来创建或获取一个变量的机制,不同函数可以直接通过变量的名字来使用变量,不需要通过参数的形式到处传递。

  • 通过变量名称来获取变量:
# 1.
tf.get_variable# 除了tf.Variable函数,Tensorflow还提供了tf.get_variable函数来创建或者获取变量。当tf.get_variable用于创建变量时,它和tf.Variable的功能是基本等价的。
# 下面这两个定义是等价的。
v = tf.get_variable("v",shape=[1],initializer=tf.constant_initilizer(1.0))
v = tf.Variable(tf.constant(1.0,shape=[1]),name="v")# 2.
tf.variable_scope
  • TensorFlow提供了7种不同的初始化函数
初始化函数 功能 主要参数
tf.constant_initializer 将变量初始化为给定常量 常量的取值
tf.random_normal_initializer 将变量初始化为满足正太分布的随机值 正太分布的均值和标准差
tf.truncated_normal_initializer 将变量初始化为满足正太分布的随机值,但若随机出来的值偏离平均值超过两个标准差,那么这个数将会被重新随机 正太分布的均值和标准差
tf.random_uniform_initializer 将变量初始化为满足平均分布的随机值 最大,最小值
tf.uniform_unit_scaling_initializer 将变量初始化为满足平均分布但不影响输出数量级的随机值 factor(产生随机值时乘以的系数)
tf.zeros_initializer 将变量设置为全为0 变量维度
tf.ones_initializer 将变量设置为全为1 变量维度
  • tf.get_variable:

    1. tf.get_variable变量名是一个必填参数,其首先会试图去创建一个名字为v的参数,如果创建失败(比如已经有同名的参数),那么这个程序就会报错。这是为了避免无意识的变量复用造成的错误。比如在定义神经网络参数时,第一层网络的权重已经叫weights了,那么在创建第二层神经网络时,如果参数名仍然叫weights,就会触发变量重用的错误。否则两层神经网络共用一个权重会出现一些比较难以发现的错误。
    2. 如果需要通过tf.get_variable获取一个已经创建的变量,需要通过tf.variable_scope函数来生成一个上下文管理器,并明确指定在这个上下文管理器中,tf.get_variable将直接获取已经生成的变量。
    3. 下面给出一段代码说明如何通过tf.variable_scope函数来控制tf.get_variable函数获取已经创建过的变量。
#在名字为foo的命名空间内创建名字为v的变量
with tf.variable_scope("foo"):v = tf.get_variable("v",[1],initializer=tf.constant_initializer(1.0))#因为在命名空间foo已经存在名字为v的变量,所有下面的代码将会报错:
with tf.variable_scope("foo"):v = tf.get_variable("v",[1])#在生成上下文管理器时,将参数reuse设置为True。这样tf.get_variable函数将直接获取已经生成的变量
with tf.variable_scope("foo",reuse=True):v1 = tf.get_variable("v",[1])print v == v1    #输出为True,代表v,v1是相同的Tensorflow中的变量#将参数reuse设置为True时,tf.variable_scope将只能获取已经创建的变量,因为在命名空间bar中还没有创建变量v,所以下面的代码将会报错:
with tf.variable_scope("bar",reuse=True):v = tf.get_variable("v",[1])
  • 通过 tf.variable_scope 控制 tf.get_variable 的语义:

    1. 如果tf.variable_scope函数使用参数 reuse=None 或者reuse=False创建上下文管理器,tf.get_variable操作将创建新的变量,如果同名的变量已经存在,则tf.get_variable函数将报错。另外,Tensorflow中tf.variable_scope函数是可以嵌套的。
    2. 如果tf.variable_scope函数使用参数 reuse=True 生成上下文管理器时,该上下文管理器中的所有 tf.get_variable 函数会直接获取已经创建的变量,如果变量不存在,将会报错。

    3. 使用变量管理后,就不再需要将所有变量都作为参数传递到不同的函数中了,当神经网络结构更加复杂,参数更多时,使用这种变量管理的方式将大大提高程序的可读性。

    示例代码:

with tf.variable_scope("root"):# 可以通过 tf.get_variable_scope().reuse函数来获取当前上下文管理器中的reuse参数的取值print(tf.get_variable_scope().reuse)            # 若输出False则最外层reuse是Falsewith tf.variable_scope("foo",reuse=True):       # 新建一个嵌套的上下文管理器,并指定reuse=Trueprint(tf.get_variable_scope().reuse)        # 输出Truewith tf.variable_scope(""bar)               # 新建一个嵌套的上下文管理器,不指定reuse,# 此时reuse回合外面一层保持一致print(tf.get_variable_scope().reuse)    # 输出Trueprint(tf.get_variable_scope().reuse)            # 输出False# 退出reuse设置为True设置的上      # 下文之后,# reuse的值又回到了False
  • 通过 tf.variable_scope 来管理变量命名空间:
v1 = tf.get_variable("v", [1])
print(v1.name)                 # 输出v:0,v是变量名称,“:0”表示这个变量是生成变量这个运算的第一个结果with tf.variable_scope("foo"):v2 = tf.get_variable("v", [1])
print(v2.name)                 # 输出foo/v:0,在tf.variable_scope中创建的变量,名称前面会加入命名空间的名称,# 并通过/来分割命名空间的名称和变量的名称with tf.variable_scope("foo"):with tf.variable_scope("bar"):v3 = tf.get_variable("v", [1])print(v3.name)         # 输出foo/bar/v:0,命名空间可以嵌套,同时变量的名称也会加入所以命名空间的名称作为前缀。v4 = tf.get_variable("v1", [1])
print(v4.name) # v1:0          # 输出foo/v1:0,当命名空间退出后,变量名称也就不会再被加入其前缀了。# 创建一个名称为空的命名空间,并设置reuse=True,我们可以通过变量的名称来获取变量
with tf.variable_scope("",reuse=True):v5 = tf.get_variable("foo/bar/v", [1])# 可以直接通过带命名空间名称的变量名来获取其他命名空间下的变量# 这里是通过指定名称foo/bar/v来获取在命名空间foo/bar/中创建的变量print(v5 == v3)            # 输出:Truev6 = tf.get_variable("v1", [1])print(v6 == v4)            # 输出:True

利用上述两个函数对5.2.1小节定义的计算前向传播结果做了改进,就不需要将所有变量都作为参数传递到不同函数中了。

import tensorflow as tf
def inference(input_tensor, reuse=False):# 定义第一层神经网络的变量和前向传播的过程with tf.variable_scope('layer', reuse=reuse):# 根据传进来的reuse来判断是创建新变量还是使用已经创建好的,# 第一次构造网络时需要创建新的变量,# 之后每次调用该函数都直接使用reuse=True就不需要每次将变量传进来了weights = tf.get_variable("weights", [INPUT_NODE, LAYER1_NODE],initializer=tf.truncated_normal_initializer(stddev=0.1))biases = tf.get_variable("biases", [LAYER1_NODE],initializer=tf.constant_initializer(0.0))layer1 = tf.nn.relu(tf.matmul(input_tensor, weights) + biases)# 类似的定义第二层神经网络的变量和前向传播过程with tf.variable_scope('layer2', reuse=reuse):weights = tf.get_variable("weights", [LAYER1_NODE, OUTPUT_NODE],initializer=tf.truncated_normal_initializer(stddev=0.1))biases = tf.get_variable("biases", [OUTPUT_NODE],initializer=tf.constant_initializer(0.0))layer2 = tf.matmul(layer1, weights) + biases# 返回最后的前向传播结果return layer2x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')
y = inference(x)# 在程序中如果需要使用训练好的神经网络进行推导时,可以直接调用inference(new_x,True)
new_x = ...
new_y = inference(new_x, True)

5.4 TensorFlow模型持久化

为了将训练得到的模型保存下来方便下次使用,即结果可以复用,需要将神经网络模型持久化。

5.4.1 持久化代码实现

TensorFlow提供了一个非常简单的API来保存和还原神经网络模型,该API就是tf.train.Saver类。

1. ckpt文件的保存

import tensorflow as tf
# 声明两个变量,并计算其和
v1=tf.Variable(tf.constant(1.0,shape=[1]),name="v1")
v2=tf.Variable(tf.constant(2.0,shape=[1]),name="v2")init_op=tf.global_variables_initializer()
#声明tf.train.Saver类用于保存模型
saver=tf.train.Saver()with tf.Session() as sess:sess.run(init_op)#将模型保存到文件saver.save(sess,"E:\pycharm\TensorFlow chap5\model.ckpt")

Tensorflow模型一般会存在后缀为.ckpt文件中,虽然上面的程序只指定了一个文件路径,但是在这个文件目录下会出现三个文件,这是因为Tensorflow会将计算图的结构和涂上的参数取值分来保存。

  1. 第一个文件为model.ckpt.meta,它保存了Tensorflow计算图的结构,
  2. 第二个文件为model.ckpt,这个文件保存了Tensorflow程序中每一个变量的取值,
  3. 最后一个文件为checkpoint文件,这个文件保存了一个目录下所有的模型文件列表。

2. 加载已经保存的TensorFlow模型

# 2. 加载已经创建的模型
import tensorflow as tf# 使用和保存模型代码中一样的方式来声明变量
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name="v1")
v2 = tf.Variable(tf.constant(2.0, shape=[1]), name="v2")
result = v1 + v2saver = tf.train.Saver()with tf.Session() as sess:# 加载已经保存的模型,并通过已经保存的模型中的变量的值来计算加法saver.restore(sess, "E:\pycharm\TensorFlow chap5\model\model.ckpt")输出:[ 3.]

加载模型的代码中,没有运行变量的初始化过程,而是将变量的值通过已经保存的模型加载进来

  • 如果不希望重复定义图上的运算,也可以直接加载已经持久化的图
# 3. 直接加载已经持久化的图
import tensorflow as tf
# 直接加载
saver=tf.train.import_meta_graph("E:\pycharm\TensorFlow chap5\model\model.ckpt.meta")
with tf.Session() as sess:saver.restore(sess,"E:\pycharm\TensorFlow chap5\model\model.ckpt")# 通过张量名称来获取张量print(sess.run(tf.get_default_graph().get_tensor_by_name("add:0")))#输出:[ 3. ]
  • 如何加载部分变量
    上面给出的程序中,默认保存和加载了Tensorflow计算图上定义的全部变量。但有时候可能只需要保存或者加载部分变量,比如,可能有一个之前训练好的五层神经网络模型,但现在想尝试一个六层的神经网络,那么可以将前面五层神经网络中的参数直接加载到新的模型,而仅仅将最后一层神经网络重新训练。为了保存或者加载部分变量,在声明tf.train.Saver类时可以提供一个列表来指定需要保存或者加载的变量。比如在加载模型的代码中使用saver=tf.train.Saver([v1])命令来构建tf.train.Saver类,那么只有变量v1会被加载进来,如果运行修改后之家在v1的代码会得到变量未初始化的错误:

      tensorflow.python.framework.errors.FailedPreconditionError:Attempting to use uninitialized value v2

      因为v2没有被加载,所以v2在运行初始化之前是没有值的。除了可以选取需要被加载的变量,tf.train.Saver类也支持在保存或者加载时给变量重命名。

      下面给出一个简单的程序来说明重命名时如何被调用的

#这里声明的变量名称和已经保存的模型中变量的名称不同v1 = tf.Variable(tf.constant(1.0,shape=[1]),name="other-v1")v2 = tf.Variable(tf.constant(2.0,shape=[1]),name="other-v2")#如果直接使用tf.train.Saver类来加载模型会报变量找不到的错误。#使用一个字典(dictionary)来重命名变量就可以加载原来的模型了。这个字典指定了原来名称为v1的变量现在加载到变量v1中(名称为other-v1),#名称为v2的变量加载到变量v2中(名称为other-v2)saver = tf.train.Saver({"v1":v1, "v2":v2})

  在这个程序中,对变量v1和v2的名称进行了修改,如果直接通过tf.train.Saver默认的构造函数来加载保存的模型,那么程序会报变量找不到的错误,因为保存时候变量的名称和加载时变量的名称不一致。为了解决这个问题,Tensorflow可以通过字典(dictionary)将模型保存时的变量名和需要加载的变量联系起来。这样做主要目的之一时方便使用变量的滑动平均值,在Tensorflow中,每一个变量的滑动平均值是通过影子变量维护的,所以要获取变量的滑动平均值实际上就是获取这个影子变量的取值。如果在加载模型时直接将影子变量映射到变量自身,那么在使用训练好的模型时就不需要再调用函数来获取变量的滑动平均值了。下面的代码给出了一个保存滑动平均模型的样例:

# 《TensorFlow实战Google深度学习框架》05 minist数字识别问题
# win10 Tensorflow1.0.1 python3.5.3
# CUDA v8.0 cudnn-8.0-windows10-x64-v5.1
# filename:ts05.10.py # 滑动平均类的保存import tensorflow as tf# 1. 使用滑动平均
v = tf.Variable(0, dtype=tf.float32, name="v")
for variables in tf.global_variables():print(variables.name)
'''
v:0
'''
ema = tf.train.ExponentialMovingAverage(0.99)
maintain_averages_op = ema.apply(tf.global_variables())
for variables in tf.global_variables():print(variables.name)
'''
v:0
v/ExponentialMovingAverage:0
'''
# 2. 保存滑动平均模型
saver = tf.train.Saver()
with tf.Session() as sess:init_op = tf.global_variables_initializer()sess.run(init_op)sess.run(tf.assign(v, 10))sess.run(maintain_averages_op)# 保存的时候会将v:0  v/ExponentialMovingAverage:0这两个变量都存下来。saver.save(sess, "Saved_model/model2.ckpt")print(sess.run([v, ema.average(v)]))
'''
[10.0, 0.099999905]
'''
# 3. 加载滑动平均模型
v = tf.Variable(0, dtype=tf.float32, name="v")# 通过变量重命名将原来变量v的滑动平均值直接赋值给v。
saver = tf.train.Saver({"v/ExponentialMovingAverage": v})
with tf.Session() as sess:saver.restore(sess, "Saved_model/model2.ckpt")print(sess.run(v))# 输出:0.0999999
  • 使用tf.train.Saver会保存运行Tensorflow程序所需要的全部信息,然后有时并不需要某些信息。比如在测试或者离线预测时,只需要知道如何从神经网络的输入层经过前向传播计算得到输出层即可,而不需要类似于变量初始化,模型保存等辅助节点的信息。而且,将变量取值和计算图结构分成不同的文件存储有时候也不方便,于是Tensorflow提供了convert_variables_to_constants函数,通过这个函数可以将计算图中的变量及其取值通过常量的方式保存,这样整个Tensorflow计算图可以统一存放在一个文件中。如下:

 import tensorflow as tffrom tensorflow.python.framework import graph_utilv1 = tf.Variable(tf.constant(1.0,shape=[1]),name="v1")v2 = tf.Variable(tf.constant(2.0,shape=[1]),name="v2")result = v1 + v2init_op = tf.global_variables_initializer()with tf.Session() as sess:sess.run(init_op)#导出当前计算图的GraphDef部分,只需要这部分就可以完成从输入层到输出层的计算过程graph_def = tf.get_default_graph().as_graph_def()#将图中的变量及其取值转化为常量,同时将图中不必要的节点去掉output_graph_def = graph_util.convert_variables_to_constants(sess,graph_def,['add'])with tf.gfile.GFile("/path/to/model/combined_model.pb","wb") as f:f.write(output_graph_def.SerializeToString())

  通过下面的程序可以直接计算定义的加法运算的结果,当只需要得到计算图中某个节点的取值时,该方法更为简便。

import tensorflow as tffrom tensorflow.python.platform import gfilewith tf.Session() as sess:model_filename = "/path/to/model/combined_model.pb"#读取保存的模型文件,并将文件解析成对应的GraphDef Protocol Bufferwith gfile.FastGFile(model_filename,'rb') as f:graph_def = tf.GraphDef()graph_def.ParseFromString(f.read())#将graph_def中保存的图加载到当前的图中.return_elements=["add:0"]给出了返回的张量的名称。#在保存能的时候给出的时计算节点的名称,所以为"add",在加载的时候给出的是张量的名称,所以时add:0result = tf.import_graph_def(graph_def,return_elements=["add:0"])print(sess.run(result))

5.4.2 持久化原理及数据格式

  • Tensorflow是一个通过图的形式来表达计算的编程系统,Tensorflow程序中的所有计算都会表达为计算图上的节点。
  • Tensorflow通过元图(MetGraph)来记录计算图中节点的信息以及运行计算图中节点所需要的元数据。
  • Tensorflow中元图是由MetaGraphDef Protocol BUffer定义的,MetaGraphDef中的内容就构成了Tensorflow持久化时的第一个文件。

元图(MetGraph)主要记录5类信息

保存MetGraph信息的文件默认以.meta为后缀名,是一个二进制文件,无法直接查看,TensorFlow提供export_meta_graph函数来以json格式导出MetaGraphDef Protocol BUffer。

  • 下面分别介绍元图存储的信息

    1. meta_info_def 属性:记录了计算图中的元数据(计算图版本号、标签等)及程序中所有用到的运算方法信息。
    2. graph_def 属性:记录了计算图上的节点信息,因为在meta_info_def属性已经包含了所有运算的信息,所以graph_def只关注运算的连接结构。
    3. saver_def 属性:记录了持久化模型时需要使用的一些参数,如保存到文件的文件名、保存操作和加载操作的名称,以及保存频率等。
    4. collection_def 属性:计算图中维护集合的底层实现,该属性是一个从集合名称到集合内容的映射。

5.5 最佳实践样例程序

  1. 将不同的功能模块分开:将训练和测试分成两个独立的程序,这可以使得每一个组件更加灵活。比如训练神经网络的程序可以持续输出训练好的模型,而测试程序可以每隔一段实践检验最新模型的正确率,如果模型效果更好,则将这个模型提供给产品使用。除了将不同的功能模块分开;
  2. 本节还将前向传播的过程抽象成一个单独的库函数。因为神经网络的前向传播过程在训练和测试的过程中都会用到,所以通过库函数的方式使用起来既方便又可以保证训练和测试过程中使用的前向传播方法是一致的。

1. 定义神经网络的前向传播过程

# 《TensorFlow实战Google深度学习框架》05 minist数字识别问题
# win10 Tensorflow1.0.1 python3.5.3
# CUDA v8.0 cudnn-8.0-windows10-x64-v5.1
# filename:mnist_inference.py # 定义神经网络的前向传播过程import tensorflow as tf# 1. 定义神经网络结构相关的参数
INPUT_NODE = 784
OUTPUT_NODE = 10
LAYER1_NODE = 500# 2. 通过tf.get_variable函数来获取变量
def get_weight_variable(shape, regularizer):weights = tf.get_variable("weights", shape, initializer=tf.truncated_normal_initializer(stddev=0.1))if regularizer != None:tf.add_to_collection('losses', regularizer(weights))return weights# 3. 定义神经网络的前向传播过程
def inference(input_tensor, regularizer):with tf.variable_scope('layer1'):weights = get_weight_variable([INPUT_NODE, LAYER1_NODE], regularizer)biases = tf.get_variable("biases", [LAYER1_NODE], initializer=tf.constant_initializer(0.0))layer1 = tf.nn.relu(tf.matmul(input_tensor, weights) + biases)with tf.variable_scope('layer2'):weights = get_weight_variable([LAYER1_NODE, OUTPUT_NODE], regularizer)biases = tf.get_variable("biases", [OUTPUT_NODE], initializer=tf.constant_initializer(0.0))layer2 = tf.matmul(layer1, weights) + biasesreturn layer2

2. 训练程序

# 《TensorFlow实战Google深度学习框架》05 minist数字识别问题
# win10 Tensorflow1.0.1 python3.5.3
# CUDA v8.0 cudnn-8.0-windows10-x64-v5.1
# filename:mnist_train.py # 训练程序import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import mnist_inference
import os# 1. 定义神经网络结构相关的参数
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.8
LEARNING_RATE_DECAY = 0.99
REGULARIZATION_RATE = 0.0001
TRAINING_STEPS = 30000
MOVING_AVERAGE_DECAY = 0.99
MODEL_SAVE_PATH = "MNIST_model/" # 在当前目录下存在MNIST_model子文件夹
MODEL_NAME = "mnist_model"# 2. 定义训练过程
def train(mnist):# 定义输入输出placeholder。x = tf.placeholder(tf.float32, [None, mnist_inference.INPUT_NODE], name='x-input')y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input')regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE)y = mnist_inference.inference(x, regularizer)global_step = tf.Variable(0, trainable=False)# 定义损失函数、学习率、滑动平均操作以及训练过程。variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)variables_averages_op = variable_averages.apply(tf.trainable_variables())cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=tf.argmax(y_, 1))cross_entropy_mean = tf.reduce_mean(cross_entropy)loss = cross_entropy_mean + tf.add_n(tf.get_collection('losses'))learning_rate = tf.train.exponential_decay(LEARNING_RATE_BASE,global_step,mnist.train.num_examples / BATCH_SIZE, LEARNING_RATE_DECAY,staircase=True)train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)with tf.control_dependencies([train_step, variables_averages_op]):train_op = tf.no_op(name='train')# 初始化TensorFlow持久化类。saver = tf.train.Saver()with tf.Session() as sess:tf.global_variables_initializer().run()for i in range(TRAINING_STEPS):xs, ys = mnist.train.next_batch(BATCH_SIZE)_, loss_value, step = sess.run([train_op, loss, global_step], feed_dict={x: xs, y_: ys})if i % 1000 == 0:print("After %d training step(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)# 3. 主程序入口
def main(argv=None):mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)train(mnist)if __name__ == '__main__':tf.app.run()

输出:

Extracting MNIST_data/train-images-idx3-ubyte.gz
Extracting MNIST_data/train-labels-idx1-ubyte.gz
Extracting MNIST_data/t10k-images-idx3-ubyte.gz
Extracting MNIST_data/t10k-labels-idx1-ubyte.gzAfter 1 training step(s), loss on training batch is 3.4301.
After 1001 training step(s), loss on training batch is 0.245217.
After 2001 training step(s), loss on training batch is 0.177494.
After 3001 training step(s), loss on training batch is 0.137083.
After 4001 training step(s), loss on training batch is 0.123793.
After 5001 training step(s), loss on training batch is 0.102773.
After 6001 training step(s), loss on training batch is 0.0973897.
After 7001 training step(s), loss on training batch is 0.0889735.
...

3. 代码

# 《TensorFlow实战Google深度学习框架》05 minist数字识别问题
# win10 Tensorflow1.0.1 python3.5.3
# CUDA v8.0 cudnn-8.0-windows10-x64-v5.1
# filename:mnist_eval.py # 测试程序
import time
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import mnist_inference
import mnist_train# 1. 每10秒加载一次最新的模型
# 加载的时间间隔。
EVAL_INTERVAL_SECS = 10def evaluate(mnist):with tf.Graph().as_default() as g:x = tf.placeholder(tf.float32, [None, mnist_inference.INPUT_NODE], name='x-input')y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input')validate_feed = {x: mnist.validation.images, y_: mnist.validation.labels}y = mnist_inference.inference(x, None)correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))variable_averages = tf.train.ExponentialMovingAverage(mnist_train.MOVING_AVERAGE_DECAY)variables_to_restore = variable_averages.variables_to_restore()saver = tf.train.Saver(variables_to_restore)while True:with tf.Session() as sess:ckpt = tf.train.get_checkpoint_state(mnist_train.MODEL_SAVE_PATH)if ckpt and ckpt.model_checkpoint_path:saver.restore(sess, ckpt.model_checkpoint_path)global_step = ckpt.model_checkpoint_path.split('/')[-1].split('-')[-1]accuracy_score = sess.run(accuracy, feed_dict=validate_feed)print("After %s training step(s), validation accuracy = %g" % (global_step, accuracy_score))else:print('No checkpoint file found')returntime.sleep(EVAL_INTERVAL_SECS)# 主程序
def main(argv=None):mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)evaluate(mnist)if __name__ == '__main__':tf.app.run()

5.6 补充

5.6.1 单层神经网络

MNIST数据集,图像大小为28x28,将其进行分类的最简单的方法就是使用784个像素作为单层神经网络的输入。

神经网络中的每个神经元对其所有的输入进行加权求和,并添加一个被称为偏置的常数,之后通过一些非线性激活函数来反馈结果。

为了将数字分为10类,我们设计了一个具有10个输出神经元的单层神经网络,对于分类问题,常用的激活函数是softmax函数,该函数通过取每个元素的指数,然后归一化向量,从而将softmax应用于向量。

softmax(Ln)=eLn||eL||softmax(Ln)=eLn||eL||

softmax(L_n)=\frac{e^{L_n}}{||e^L||}

其中,LnLnL_n为所有权重的和+偏置

softmax名称的来源:

指数是骤增的函数,这将加大向量中每个元素的差异,也会迅速的产生一个巨大的值,然后当进行向量标准化时,支配范数(norm)的最大的元素将会被标准化为一个接近于1的数字,其他的元素将会被一个较大的值分割并标准化为一个接近于0的数字,所得到的向量清楚的显示了哪个是其最大值,即max,但是却又保留了其值的原始的相对排列顺序,因此即为soft

假设我们有一个数组,V,Vi表示V中的第i个元素,那么这个元素的Softmax值就是

Si=eVi∑jeViSi=eVi∑jeVi

S_i=\frac{e^{V_i}}{\sum_je^{V_i}}

也就是说,是该元素的指数,与所有元素指数和的比值

这个定义可以说非常的直观,当然除了直观朴素好理解以外,它还有更多的优点,比如可以计算样本和标注之间的差距,并且计算上非常方便。

假设输入样本为3,-1,3,则softmax直观来说就是将输入映射为(0,1)的值,且这些值的累加和为1(满足概率性质),那么我们就可以将其理解为概率,在最后选择输出的时候,就可以选择概率最大(也就是值对应最大的)结点作为预测目标。

使用矩阵乘法进行前向传播:

我们现在将使用矩阵乘法将这个单层的神经元的行为总结进一个简单的公式当中。直接这样做:将包含100 个图像的「mini-batch」作为输入,产生 100 个预测(10 元素向量)作为输出。

权值矩阵是784x10的大小,784对应784个像素,10对应10个神经元的输出。

第一个神经元对应的是图像和第一列权值相乘之和,第二个神经元是图像和第二列权值相乘之和,以此类推,并且每个神经元都要添加一个偏置bias,

5.6.2 梯度下降

神经网络从输入图像中产生预测,但是我们需要知道预测结果和真实值之间的差距,任何一种定义的距离都可以这样操作,但是对于分类问题,一般使用交叉熵距离来度量:

crossentropy=−∑Y′i⋅log(Yi)crossentropy=−∑Yi′⋅log(Yi)

cross entropy=-\sum Y_i' \cdot log(Y_i)

其中,Y′iYi′Y_i'是真实值,YiYiY_i是

独热码(one-hot):编码意味着你使用一个 10 个值的向量,其中除了第 6 个值为 1 以外的所有值都是 0。这非常方便,因为这样的格式和我们神经网络预测输出的格式非常相似,同时它也作为一个 10 值的向量。

训练:使用训练图像和标签来调整权值和偏置,以使交叉熵损失函数最小。

梯度的数学意义在于它指向「上(up)」。因为我们想要到达一个交叉熵低的地方,那么我们就去向相反的方向。我们用一小部分的梯度更新权重和偏置并且使用下一批训练图像再次做同样的事情。我们希望的是,这可以使我们到达交叉熵最小的凹点的低部。

交叉熵是一个关于权重、偏置、训练图像的像素和其已知标签的函数。

如果我们相对于所有的权重和所有的偏置计算交叉熵的偏导数,我们就得到一个对于给定图像、标签和当前权重和偏置的「梯度」。请记住,我们有 7850 个权重和偏置,所以计算梯度需要大量的工作。

梯度下降算法遵循着一个最陡的坡度下降到局部最小值的路径。训练图像在每一次迭代中同样会被改变,这使得我们向着一个适用于所有图像的局部最小值收敛。

学习率:在整个梯度的长度上,你不能在每一次迭代的时候都对权重和偏置进行更新。这就会像是你穿着七里靴却试图到达一个山谷的底部。你会直接从山谷的一边到达另一边。为了到达底部,你需要一些更小的步伐,即只使用梯度的一部分,通常在 1/1000 区域中。我们称这个部分为「学习率(Learning rate)」。

神经网络学习过程:

初始化偏置和权值→→\to定义损失函数→→\to梯度下降法更新参数→→\to使用下一个mini-batch优化参数

为什么使用mini-batch:

当只用一个图像来进行参数更新的过程叫做随机梯度下降,在 100 个样本上都这样做可以得到一个更好地表示由不同样本图像施加约束的梯度并且可能更快地朝着解决方案收敛。mini-batch 的大小是可调整的参数。还有一个更加技术化的原因:使用批处理也意味着使用较大的矩阵,而这些通常更容易在 GPU 上优化。

增加层

为了提高识别的准确度,可以将神经网络增加更多的层,第二层神经元将计算前一层神经元输出的加权和,而非计算像素的加权和,下面是5层全连接的神经网络:

最后一层的激活函数仍然使用softmax,中间层使用sigmoid函数。

随着层数的增加,神经网络的收敛越来越难,因为梯度消失,如何来改进呢?
——修正线性单元ReLU激活函数

sigmoid函数的问题:

该函数将所有的值都归到了(0,1)之间,是饱和函数,当层数越来越多的时候,神经元的输入和梯度已经基本归零了,因为变化非常小。出于历史原因,一些现代神经网络使用了 ReLU(修正线性单元)。

升级:

① 用ReLU替换之后,收敛更快准确度更高。

② 将你的 tf.train.GradientDescentOptimiser 替换为 tf.train.AdamOptimizer。

随机初始化:

如果准确性一直卡在0.1,可以看看有没有将权值初始化为随机值了,对于偏置值,如何使用ReLU的话,最好的办法就是将它们都初始化为小的正值,这样神经元一开始就会工作在ReLU的非零区域内。

W = tf.Variable(tf.truncated_normal([K, L] ,stddev=0.1))B = tf.Variable(tf.ones([L])/10)

③ 不定值NaN

如果准确率曲线陡然下滑,并输出的交叉熵为NaN,则是正在计算log(0),而这个值正是NaN,

5.6.3 学习速率衰减

通过四五个中间层之后,准确度可以提升至98%,但是当迭代次数5000以上的时候,你会发现结果并非如此,而是跳动很大的曲线,也就是在全百分比范围内跳动,这意味着学习率太大了,一个好的解决方案是开始很快随后将学习速率指数级衰减至很小,比如0.0001。

5.6.4 过拟合和dropout

在数千次迭代之后,测试和训练数据的交叉熵曲线开始不相连。学习算法只是在训练数据上做工作并相应地优化训练的交叉熵。它再也看不到测试数据了,所以这一点也不奇怪:过了一会儿它的工作不再对测试交叉熵产生任何影响,交叉熵停止了下降,有时甚至反弹回来。

它不会立刻影响你模型对于真实世界的识别能力,但是它会使你运行的众多迭代毫无用处,而且这基本上是一个信号——告诉我们训练已经不能再为模型提供进一步改进了。这种无法连接通常会被标明「过拟合(overfitting)」,而且当你看到这个的时候,你可以尝试采用一种规范化(regularization)技术,称之为「dropout」。

在 dropout 里,在每一次训练迭代的时候,你可以从网络中随机地放弃一些神经元。你可以选择一个使神经元继续保留的概率 pkeep,通常是 50% 到 75% 之间,然后在每一次训练的迭代时,随机地把一些神经元连同它们的权重和偏置一起去掉。在一次迭代里,不同的神经元可以被一起去掉(而且你也同样需要等比例地促进剩余神经元的输出,以确保下一层的激活不会移动)。当测试你神经网络性能的时候,你再把所有的神经元都装回来 (pkeep=1)。

TensorFlow 提供一个 dropout 函数可以用在一层神经网络的输出上。它随机地清零一些输出并且把剩下的提升 1/pkeep。

TensorFlow:实战Google深度学习框架(四)MNIST数据集识别问题相关推荐

  1. 06.图像识别与卷积神经网络------《Tensorflow实战Google深度学习框架》笔记

    一.图像识别问题简介及经典数据集 图像识别问题希望借助计算机程序来处理.分析和理解图片中的内容,使得计算机可以从图片中自动识别各种不同模式的目标和对象.图像识别问题作为人工智能的一个重要领域,在最近几 ...

  2. (转)Tensorflow 实战Google深度学习框架 读书笔记

    本文大致脉络: 读书笔记的自我说明 对读书笔记的摘要 具体章节的摘要: 第一章 深度学习简介 第二章 TensorFlow环境搭建 第三章 TensorFlow入门 第四章 深层神经网络 第五章 MN ...

  3. 免费教材丨第55期:Python机器学习实践指南、Tensorflow 实战Google深度学习框架

    小编说  时间过的好快啊,小伙伴们是不是都快进入寒假啦?但是学习可不要落下哦!  本期教材  本期为大家发放的教材为:<Python机器学习实践指南>.<Tensorflow 实战G ...

  4. 《Tensorflow 实战google深度学习框架》第二版源代码

    <<Tensorflow 实战google深度学习框架–第二版>> 完整资料github地址: https://github.com/caicloud/tensorflow-t ...

  5. 学习《TensorFlow实战Google深度学习框架 (第2版) 》中文PDF和代码

    TensorFlow是谷歌2015年开源的主流深度学习框架,目前已得到广泛应用.<TensorFlow:实战Google深度学习框架(第2版)>为TensorFlow入门参考书,帮助快速. ...

  6. 说说TensorFlow实战Google深度学习框架

    说说TensorFlow实战Google深度学习框架 事情是这样的,博主买了这本书,但是碍于想在电脑上边看边码,想找找PDF版本,然后各种百度,Google,百度网盘,最后找到的都是很多200M的,百 ...

  7. TensorFlow实战Google深度学习框架

    TensorFlow是谷歌2015年开源的主流深度学习框架.科技届的聚光灯已经从"互联网+"转到了"AI+": 掌握深度学习需要较强的理论功底,用好Tensor ...

  8. TensorFlow实战Google深度学习框架5-7章学习笔记

    目录 第5章 MNIST数字识别问题 第6章 图像识别与卷积神经网络 第7章 图像数据处理 第5章 MNIST数字识别问题 MNIST是一个非常有名的手写体数字识别数据集,在很多资料中,这个数据集都会 ...

  9. Tensorflow 实战 Google 深度学习框架(第2版)---- 10.2.2节 P274 代码

    #-*-coding:utf-8-*- import keras from tflearn.layers.core import fully_connected from keras.datasets ...

  10. Tensorflow 实战 Google 深度学习框架(第2版)---- 10.2.2节 P272 代码

    #-*-coding:utf-8-*- import keras from keras.datasets import mnist from keras.layers import Input,Den ...

最新文章

  1. iOS正则匹配手机号
  2. 微软将Bing变开放平台 同谷歌争夺开发者
  3. Python基础(1)
  4. 也说说“从Adapter模式到Decorator模式”
  5. MySql 内连接,外连接查询方式区别
  6. 用于Spring应用程序的Gradle原型
  7. 【c++ Primer 】 4.10复习题 12题(int)、(int)和(int*)
  8. 【DP】【Uva437】UVA437 The Tower of Babylon
  9. seaborn_Seaborn Distplot:综合指南
  10. mysql游标嵌套游标_mysql游标嵌套循环
  11. 关闭TOMCAT日志的三个方法
  12. MYSQL-DBA书籍推荐
  13. 廊坊交警利用智能交通管控平台助力平安建设工作
  14. Android 深入理解AIL语言与init.rc文件
  15. 从零建造一个基于深度强化学习的期货日内高频交易模型(一)下载数据
  16. 计算机大赛搞笑队名,电子设计大赛队名
  17. java mp4 合并_使用 Python 把多个 MP4 合成一个视频
  18. c语言中的除号什么作用,c语言中除号用什么表示
  19. 第四套人民币荧光“四大天王”收藏价值分析
  20. VisualDrag低代码拖拽模板

热门文章

  1. java MD5 并发
  2. Windows 10封装中出现“无法验证你的Windows安装”错误解决方法
  3. 常用的数据统计Sql 总结
  4. 判断.java文件中getConnection与cleanUp数量是否匹配
  5. 移动互联网时代 浏览器不可能干掉App
  6. Android多媒体学习三:实现自己的Camera
  7. winform打开cad图纸_为什么CAD图纸打开后会显示很多问号“???”,该怎么解决...
  8. Activiti源码 之Command与 CommandInterceptor
  9. 配置tomcat支持http delete和put的方法
  10. c php数据,C 数据类型