1. 开门见山
其实只是刚入门,并不理解博大精深的tensorflow,只是想取个文艺点的名字。写这篇博客主要是记录下学习tensorflow会掌握哪些才够完成一些项目。首先想吐槽一下这个号称最火的深度学习框架,真的太不友好。我见过很多phd大佬吐槽过这个东西,莫名出现问题,而我,当然也是深受其害,作为一个用了挺久无脑Keras的人来说更是如此。因此个人觉得,tensorflow 应该要感受到 PyTorch 和 MXNet 的压力了。我也是跑过 PyTorch的例程,真是比较顺风顺水啊哈哈。但是不踩过tensorflow的坑人生怎么完整呢。好了,接下来我直接用 tf 代替 tensorflow。
2. 正文
接着还是继续吐槽,现在 tf 的网上很多教程,包括官方教程,包括各种博客都是问题百出,可能是版本更新太快了,很多已经不适用了,而且也没人及时更新,有些都被 deprecated 了。另外吐槽下 MNIST 这个数据集,感觉随便就能跑到95+,完全看不出代码哪里有问题。同样的网络模型放到人脸数据库上就完全不行,又比如之前实现过 RBM 在 MNIST上可以但是同样在别的数据集就不行(当然可能得调一下各种参数),我只是想说验证模型是否正确是不能光看 MNIST 上的效果啊。同样,验证你会不会 tf 同样不是只看你跑没跑过教程里面的无脑例子。举个栗子,教程里面说交叉熵损失函数写成:cross_entropy = -tf.reduce_sum(y*tf.log(outputs)),当你满心欢喜的觉得这样写十分简洁并且正确的时候,发现换了一个数据集竟然出问题了。原来是log里面出现0了,才会觉得这是什么鬼教程啊。其他可能存在的问题,就是数据一定要做好预处理(比如tensorflow中输入图像通道数放在了最后一维),权值初始化的方式可能是个要注意的地方,就这样。当然为了多次训练,需要知道怎么保存和加载模型,这就比较的完整了。
说到这里,你可能可以勉强跑一个模型了,但可能会觉得管理起来非常麻烦,也不知道自己写的对不对。这是你可能需要对变量进行管理,和对代码进行封装,接下来 class 和 tensorboard 就要出场了。首先引用下知乎一篇文章(http://www.jianshu.com/p/e112012a4b2d),讲到写 tf 的时候要有良好的代码结构,比如分成操作,模型等几个部分。这个多看看 github 上别人是怎么写的会有一个初步的印象。其实写的跟别人 github 上的 tf 代码差不多也就很可以了,毕竟这就是我的最后最终目的。在这个过程中我发现,在代码结构中,还需要用到面向对象的思想,比如定义网络就写成一个类(class),封装好,调用也方便。这就是我之前说到的代码结构,封装,class。
接下来,就是把可视化,一个看的清楚的可视化,可以看出你的代码是否有问题,这里就是使用我之前说到的 tensorboard。要用 tensorboard,就必须对变量进行管理,使用到的是name_scope和variable_scope,不管理的话你看到的 graph将是密密麻麻的东西。而使用scope,则可以显示出一个个结点,每个结点包含一些变量,如下代码使用name_scope则可以显示出一个简单的两个全连接层(fully-connected layer)的神经网络,代码部分引用自(http://www.jianshu.com/p/e112012a4b2d):
1 #!/usr/bin/python 2 # -*- coding:utf-8 -*- 3 4 import tensorflow as tf 5 from tensorflow.examples.tutorials.mnist import input_data 6 7 def weight_variable(shape): 8 initial = tf.truncated_normal(shape, stddev=0.1) 9 return tf.Variable(initial) 10 11 def bias_variable(shape): 12 initial = tf.constant(0.1, shape=shape) 13 return tf.Variable(initial) 14 15 def conv2d(x, W): 16 return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME') 17 18 def max_pool_2x2(x): 19 return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME') 20 21 def fc_layer(inputs, in_size, out_size, activation_func=None): 22 with tf.name_scope('layer'): # 自动加下标 23 with tf.name_scope('weights'): 24 Weights = tf.Variable(tf.random_normal([in_size, out_size]), name='W') # 不写name就默认是'Variable' 25 with tf.name_scope('biases'): 26 biases = tf.Variable(tf.zeros([1, out_size]) + 0.1, name='b') 27 with tf.name_scope('Wx_plus_b'): 28 Wx_plus_b = tf.matmul(inputs, Weights) + biases 29 if activation_func is None: 30 outputs = Wx_plus_b 31 else: 32 outputs = activation_func(Wx_plus_b) 33 return outputs 34 35 def main(): 36 mnist = input_data.read_data_sets('/tmp/data', one_hot=True, fake_data=False) 37 38 with tf.variable_scope('inputs'): 39 x = tf.placeholder(tf.float32, [None, 784], name='x_input') 40 y = tf.placeholder(tf.float32, [None, 10], name='y_input') 41 42 fc1 = fc_layer(x, 784, 100, activation_func=tf.nn.relu) 43 fc2 = fc_layer(fc1, 100, 100, activation_func=tf.nn.relu) 44 logits = fc_layer(fc2, 100, 10) 45 outputs = tf.nn.softmax(logits) 46 47 with tf.name_scope('loss'): 48 # cross_entropy1 = -tf.reduce_sum(y*tf.log(outputs)) # log里面可能为0 49 cross_entropy2 = tf.reduce_sum(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=y)) 50 51 with tf.name_scope('train'): 52 train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy2) 53 54 sess = tf.Session() 55 sess.run(tf.global_variables_initializer()) 56 57 writer = tf.summary.FileWriter("logs/", sess.graph) 58 59 if __name__ == '__main__': 60 main()
这个代码只有构建网络,没有训练的部分,因为我的小笔记本连训练 MNIST 都吃不消啊。首先说下一些无关紧要却又很紧要的地方:我的环境是win8下的anaconda3,配上tensorflow 1.0,现在sess.run(init)里面的init已经换成上面的tf.global_variables_initializer(),保存graph语句则变成了下面的writer = tf.summary.FileWriter("logs/", sess.graph)。首先python运行代码,然后tensorboard --logdir=logs运行(不用引号),就可以输入网址看可视化了。然后仔细看fc_layer函数会发现,name_scope中的"layer"两次调用重名了,会自动加下标变成layer_1,layer_2这样子,这样全部变量都不会重名了,很神奇。然后cross_entropy2就是我说的一种解决交叉熵log里面为0的问题的方法。
现在你会发现,这完全没有分模块写啊。的确,但是先不急,先再接着看另外一个scope:variable_scope会有什么效果。好吧,其实差不多,也是定义一个scope,但是variable_scope 可以跟 get_variable一起使用,这样来说不同 variable_scope name 下我们使用相同的变量名字(如 'weights')也不会有冲突问题,因为前缀是不同的variable_scope。但是name_scope则没有这样的配合使用效果,因此我发现很多人常用的是 variable_scope 。
----------------------------手动分割----------------------
OK,写到这里,你可以可视化看到漂亮的 graph 显示你的网络是长什么样子的。但是接下来还不急着训练网络,因为我们不希望向上面一样把整个流程(读数据,写网络,训练)写到同一份代码里,因此,这里分3块:网络写成一个类(class),读数据写一个文件,最后在main函数(文件)写完整个流程,这样分开写的重用性比较好,也体现了所谓的面向对象编程,感觉这样写是主流了。
所以,接下来我就用一个fcn网络(Fully Convolutional Network)来举个栗子。所谓的fcn就是通过一堆卷积和转置卷积,把输入image输出二维的feature map,没有fc层,输出的可能是像素级别的分类,blablabla之类的,根据你的需求了。前面的卷积就不说了,后面的转置卷积是如何操作的,这个让我理解了很久。据说这个网站(https://github.com/vdumoulin/conv_arithmetic)的动画很权威,毕竟是来自 Montreal 的。这些操作对应的 stride 是在让我一开始很懵逼,但是后来我理解的是 这些转置卷积的 stride=2 描述的都是其对应卷积操作的 stride=2。这样转置卷积的时候,就相当于中间是间隔着插入0,再进行卷积,这样就把 feature map 增大了一倍,跟卷积的时候用stride=2 使得feature map缩小一倍刚好对应起来了。在 tf 里面实现的时候参数就是根据这样来设置 padding 和 stride 的。接着所谓的 bilinear upsampling 就是转置卷积的时候实现双线性插值一样的效果,即构造一个卷积核,通过 bilinear interpolation 的初始化方式,这样卷积后就是得到双线性插值的效果了。然后在这个基础上,这个卷积核还可以训练的,就设为get_variable,不可训练的就是constant。嗯,感觉没什么问题,直接上代码,一个网络类实现了构造网络,训练,模型保存载入等等功能,有需要的可以看下哈!基本符合我之前所讲的东西,改进后的代码看起来会高端一些了。
1 #!/usr/bin/python 2 # -*- coding:utf-8 -*- 3 4 import inspect 5 import os 6 7 import numpy as np 8 import tensorflow as tf 9 import time 10 from math import ceil 11 import random 12 13 VGG_MEAN = [103.939, 116.779, 123.68] 14 15 16 class FullyConvNet: 17 def __init__(self, batch_size=16, loss_type='mse', optimizer='momentum', vgg16_npy_path=None): 18 # if vgg16_npy_path is None: 19 # path = inspect.getfile(Vgg16) 20 # path = os.path.abspath(os.path.join(path, os.pardir)) 21 # path = os.path.join(path, "vgg16.npy") 22 # vgg16_npy_path = path 23 # print (path) 24 25 # self.data_dict = np.load(vgg16_npy_path, encoding='latin1').item() 26 # print("npy file loaded") 27 self.batch_size = batch_size 28 self.loss_type = loss_type 29 self.optimizer = optimizer 30 self.learning_rate = 0.01 31 32 self.build() 33 self.sess = tf.Session() 34 35 self.sess.run(tf.global_variables_initializer()) 36 writer = tf.summary.FileWriter("logs/", self.sess.graph) 37 self.saver = tf.train.Saver() 38 39 def build(self): 40 41 # start_time = time.time() 42 # print("build model started") 43 # rgb_scaled = rgb * 255.0 44 45 # # Convert RGB to BGR 46 # red, green, blue = tf.split(rgb_scaled, 3, 3) 47 # assert red.get_shape().as_list()[1:] == [224, 224, 1] 48 # assert green.get_shape().as_list()[1:] == [224, 224, 1] 49 # assert blue.get_shape().as_list()[1:] == [224, 224, 1] 50 # bgr = tf.concat([ 51 # blue - VGG_MEAN[0], 52 # green - VGG_MEAN[1], 53 # red - VGG_MEAN[2], 54 # ], 3) 55 # assert bgr.get_shape().as_list()[1:] == [224, 224, 3] 56 57 58 with tf.variable_scope('inputs'): 59 self.x = tf.placeholder(tf.float32, [self.batch_size, 224, 224, 3], name='x_input') 60 self.y = tf.placeholder(tf.float32, [self.batch_size, 224, 224, 5], name='y_input') 61 62 self.conv1_1 = self.conv_layer(self.x, 16, [3, 3], 'conv1') 63 self.pool1 = self.max_pool(self.conv1_1, 'pool1') 64 # self.conv2_1 = self.conv_layer(self.pool1, 64, [3, 3], "conv2") 65 # self.pool2 = self.max_pool(self.conv2_1, 'pool2') 66 self.deconv1 = self.deconv_layer(self.pool1, 5, [3, 3], [self.batch_size, 224, 224, 5], 'deconv1') 67 # self.pool2 = self.max_pool(self.deconv1, 'pool2') 68 69 with tf.variable_scope('loss'): 70 if self.loss_type == 'mse': 71 self.loss = tf.reduce_mean(tf.squared_difference(self.y, self.deconv1, name='mse')) 72 73 else: 74 self.loss = tf.reduce_mean(tf.squared_difference(self.y, self.deconv1, name='mse')) 75 76 with tf.variable_scope('train'): 77 if self.optimizer == 'momentum': 78 self.train_step = tf.train.MomentumOptimizer(self.learning_rate, 0.9).minimize(self.loss) 79 80 else: 81 self.train_step = tf.train.GradientDescentOptimizer(self.learning_rate).minimize(self.loss) 82 83 84 def fit(self, x_train, y_train, x_test, y_test, iterations=2): 85 train_num = x_train.shape[0] 86 # print (sample_index) 87 # print (x_train[sample_index,:].shape) 88 89 # tf.all_variables() 90 old_cost = 100 91 for i in range(iterations): 92 sample_index = random.sample(range(0, train_num), self.batch_size) 93 _, cost = self.sess.run([self.train_step, self.loss], feed_dict={self.x:x_train[sample_index,:], self.y:y_train[sample_index,:]}) 94 test_cost = self.sess.run([self.loss], feed_dict={self.x:x_test, self.y:y_test}) 95 if cost < old_cost: 96 print ('training_cost, testing_cost: ', i, cost, test_cost) 97 old_cost = cost 98 save_path = self.saver.save(self.sess, 'tmp/model.ckpt') 99 # self.saver.restore(self.sess, 'tmp/model.ckpt') 100 cost = self.sess.run([self.loss], feed_dict={self.x:x_train[sample_index,:], self.y:y_train[sample_index,:]}) 101 print (cost) 102 103 def forward(self, x, y): 104 self.saver.restore(self.sess, 'tmp/model.ckpt') 105 cost = self.sess.run([self.loss], feed_dict={self.x:x[0:10], self.y:y[0:10]}) 106 print (cost) 107 output = self.sess.run([self.deconv1], feed_dict={self.x:x[0:10]}) 108 print (output[0].shape) 109 return output[0] 110 111 def max_pool(self, bottom, name): 112 with tf.variable_scope(name): 113 return tf.nn.max_pool(bottom, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME') 114 115 def conv_layer(self, bottom, out_size, kernel_shape, name): 116 with tf.variable_scope(name): 117 in_size = bottom.get_shape().as_list()[-1] 118 # print (bottom.get_shape().as_list()) 119 weights = tf.get_variable('weights', shape=[kernel_shape[0], kernel_shape[1], in_size, out_size], initializer=tf.random_normal_initializer()) 120 # bias = tf.get_variable('bias', [1, n_l1], initializer=b_initializer, collections=c_names) 121 conv = tf.nn.conv2d(bottom, weights, [1, 1, 1, 1], padding='SAME') 122 # conv_biases = tf.nn.bias_add(conv, bias) 123 relu = tf.nn.relu(conv) 124 return relu 125 126 def deconv_layer(self, bottom, out_size, kernel_shape, output_shape, name): 127 with tf.variable_scope(name): 128 in_size = bottom.get_shape().as_list()[-1] 129 # print (in_size) 130 f_shape = [kernel_shape[0], kernel_shape[1], out_size, in_size] 131 weights = self.bilinear_interpolation_init(f_shape) 132 # in_shape = bottom.get_shape().as_list()[0] 133 deconv = tf.nn.conv2d_transpose(bottom, weights, output_shape=output_shape, strides=[1,2,2,1], padding="SAME") 134 return deconv 135 136 137 def bilinear_interpolation_init(self, f_shape): 138 width = f_shape[0] 139 heigh = f_shape[0] 140 f = ceil(width/2.0) 141 c = (2 * f - 1 - f % 2) / (2.0 * f) 142 bilinear = np.zeros([f_shape[0], f_shape[1]]) 143 for x in range(width): 144 for y in range(heigh): 145 value = (1 - abs(x / f - c)) * (1 - abs(y / f - c)) 146 bilinear[x, y] = value 147 weights = np.zeros(f_shape) 148 for i in range(f_shape[2]): 149 weights[:, :, i, i] = bilinear 150 151 init = tf.constant_initializer(value=weights, dtype=tf.float32) 152 var = tf.get_variable(name="up_filter", initializer=init, shape=weights.shape) 153 return var
这就是代码当中的模型部分,可以称为 neural net model,所有训练的步骤都写在那里了,main文件中直接调用这个类的函数就可以了,这样main函数就很简洁了。那么另外可以写一个utils文件进行图像预处理,画图等操作,这样三个文件基本就可以处理一个项目了。
3. 未完待续
先写这么多了,有漏的再补。写了这么多发现自己写的真是不怎么样,还不如会写诗的AI,有时候真的觉得AI做的比人好其实很正常,模型是很多高智商人士设计的,而AI学习过程却仍有黑箱并且总是能产生让我们意想不到的能力(比如AlphaGo),可以说是基于人类又胜于人类,非常可怕。匿了,Auf wiedersehen。