Tensorflow函数/类,Tensorflow自定义损失函数,Tensorflow自定义复杂网络结构

Tensorflow基础知识回顾

对于一个基础tensorflow程序来说,需要了解的东西有:

(1)Placeholder,占位符,实际输入值,用它喂数据;

(2)variable,参数变量,weights和biases要用的是它;

(3)reshape,改变shape的值、维数的。

(4)loss,损失函数,预测值与真实值的差别;

(5)optimizer,优化器,已知有误差了,建立一个优化器,准备对损失函数使用,因为我们的训练目标就是降低误差降到越低越好;

(6)train,训练操作,用上面的优化器,减小误差。也就是降低损失函数。

举个最简单的例子,已知loss=……,optimizer=……,则train的一般写法就是:train = optimizer.minimize(loss),优化器要做的就是用它的minimize方法把误差最小化。

(7)session,有上述placeholder,variable,reshape,loss,optimizer,train,还有神经网络结构,程序还是不能运行,因为还没有执行session。

       把一个tensorflow程序比作一堆管道部件的话,只能说有上述的变量以后,拼成了一个完整的管道,但却没有水在里面流动。因为我们还没有用session:

1#第一种 2 3init = tf.global_variables_initializer() 4sess = tf.Session() 5sess.run(init) 6…… 7sess.close() 8 9 10#第二种 11init = tf.global_variables_initializer() 12with tf.Session() as sess: 13 sess.run(init) 14 15#这两种都对,写法很多,越简洁越好 16

       这样整个神经网络的所有参数才能被激活。

(8)上述知识点中需要注意的地方:

  • tf.matmul()是矩阵乘法,tf.multiply()是点乘,用variable进行参数计算的时候不要用混;
  • With tf.Session() as sess: 用了python的with语句之后,with里面的内容运行完会自动close();
  • 在sess.run()的运行阶段,只要用到placeholder,就要用feed_dict给placeholder喂数据;
  • tensorflow的一些函数和numpy的有区别,比如tf.linspace和np.linspace,前者精度是tf.float32,后者精度是np.float64;
  • 练习顺序:(1)MLP(2)CNN;
  • 激活函数activation:作用是给神经元添加非线性因素,让数据能够更好地被分类。因为线性模型的表达能力不够明显。例如,我本人是一个神经元,有一堆狗的照片,我喜欢柯基,当我看到柯基的眼睛的时候,我就非常兴奋(我被柯基的眼睛“激活”了),我的数值就会大幅度提高。因此这只柯基就很好的被分类了。
  • Optimizer都是基于学习率的,这个值不能任意取个0.0001之类的常量就算了。有些函数如SGD,学习率应随着epoch加深而变化,原因可查官网文档。
  • 从头学的话先上tensorflow中文社区参考一下基础知识,上面有tensorflow的函数讲解和代码示例:

http://www.tensorfly.cn/tfdoc/get_started/introduction.html

  • 还有很多,比如激活函数、过拟合、损失函数、优化器、保存、加载,等等都在中文社区里有,有忘记的自己复习巩固就好了。

(9)还有一些bug,需要灵活地解决:

  • 比如写卷积的时候维数不对导致出错,又不熟悉调试,简单粗暴的给每层之后输出一个print(xxx.shape)逐层查看,基本能找到问题所在。
  • 比如数据处理的时候,遇到不懂的维数问题,就可以去求助师门其他人,比起自己啃来,会高效很多。因为视频和书里可能会教你如何赋值x=np.random.rand()这种的,但是不会教你具体数据具体处理。
  • 还有遇到边界处理方式选择不恰当的,padding=same还是valid的问题,可能是卷积核太大,数据维度太小,卷积网络没跑完,神经元的维数就被卷成负数所致报错。
  • 比如OOM……不记得具体报错了,参数过大导致电脑显存不足没办法运行,需要修改batch,那些问题自己遇到时现查尽快解决就好了。
  • 比如多GPU处理问题,keras不好写,涉及内存占用和显存不足的,我也有点头大。类似还有很多很多,如果感觉遇到的bug比较经典,也可以写进来发到师门群里。

一.函数

       在tensorflow中使用函数,只要将参数设置好,就能节省很多代码空间,让程序不至于那么冗余复杂。如果仅仅是简单的一个tensorflow程序,肯定不会有多么复杂;但是很极端的说,假如一个tensorflow程序中,需要进行很多很多相同的循环操作,多到让人看不清,那么函数就十分有必要了。举个例子。。。:

1.为什么用函数

       有一个字母表:alphabet = [A, B, C, [D, E, [F, G, H, [I, [J, K……]]]]],类似一个这样的无穷无尽的列表。现在需要在我们的程序里,把它输出一遍,如果你想硬刚这个列表,那就是得这样写:

1for once in alphabet: 2       if isinstance(once, list): 3              for twice in once: 4                     if isinstance(twice, list): 5                            for third in twice: 6                                   if isinstance(third, list): 7                                          for fourth in third: 8                                                 # 就到这吧…… 9                                                 print(fourth) 10                                   else: 11                                          print(third) 12                     else: 13                            print(twice) 14       else: 15              print(once) 16

       写了四个循环我就受不了了,再多的话肯定这个代码可读性就降为0了,因此,选了这么一个极端的例子说明了函数的重要性(虽然用tensorflow写DNN一般不会用这种数组递归,但是函数还是很有必要的)。

2.函数:加一层卷积

       例如,给一个神经网络写一个CNN的函数:

1def weight_variable(shape): 2    initial = tf.truncated_normal(shape, stddev=0.1) 3    return tf.Variable(initial) 4 5 6def bias_variable(shape): 7    initial = tf.constant(0.1, shape=shape) 8    return tf.Variable(initial) 9 10 11def conv2d(x, W): 12    return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME') 13

       只要想添加一层卷积层,只要写好卷积核的形状和卷积核数,再直接调用就可以。

1W = weight_variable([3, 11, 128, 256]) # patch 3x11, in size 128, out size 256 2b = bias_variable([256]) 3conv = tf.nn.relu(conv2d(x, W) + b) 4

       (默认步长为1,边界处理方式为same的前提下),只要将w的参数设置好(w在此处也就是filter),就能节省很多代码空间。以后每次加一层卷积,就可以简洁的写出。

3.函数:加一层全连接

1def add_layer(inputs, in_size, out_size, activation_function=None,): 2    Weights = tf.Variable(tf.random_normal([in_size, out_size])) 3    biases = tf.Variable(tf.zeros([1, out_size]) + 0.1) 4    Wx_plus_b = tf.matmul(inputs, Weights) + biases 5    if activation_function is None: 6        outputs = Wx_plus_b 7    else: 8        outputs = activation_function(Wx_plus_b) 9return outputs 10

       设置好参数之后,在Weights中,随机生成一个行×列(in_size×out_size)的矩阵;在biases中,生成一个1×out_size的矩阵,再经过一个激活函数,输出的就是经过全连接并激活的神经元的值。(偏移量的默认值不为0,因此用tf.zeros()生成一个全0的矩阵之后,再随意加一个值即可)

4.函数:加几个多测试的数据集

       当模型训练完成后,如果我想用不同几个文件夹里的数据都测试一遍,就不需要重复写好几遍,只改一个参数的路径。只要写一个测试函数即可:

1def compute_accuracy(t_xs, t_ys): 2 global prediction 3 y_pre = sess.run(prediction, feed_dict={xs: t_xs, keep_prob: 1}) 4 correct_prediction = tf.equal(tf.argmax(y_pre,1), tf.argmax(t_ys,1)) 5 accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) 6 result = sess.run(accuracy, feed_dict={xs: t_xs, ys: t_ys, keep_prob: 1}) 7 return result 8 9

       prediction是神经网络输出的已训练好的最后一层,那么只需要修改参数部分的t_xs和t_ys,就可以直接进行预测。要注意一个地方,只要你sess.run()的参数里面用到了placeholder,哪怕外面没显示,也要feed_dict给它喂数据。测试部分不需要dropout,因此保留全部的运行部分,keep_prob=1。

5.总结

       类似的还有很多,不一一赘述。 有了函数,就可以将它转换成模块,共享给他人使用:

  1. 创建一个新文件夹,放入函数所在程序;
  2. 在其中创建一个setup.py文件,内容包含着有关发布的数据;
  3. 然后执行python3 setup.py sdist,以构建一个发布文件;
  4. 再将发布安装到本地,sudo python3 setup.py install。

二.类

1.为什么用类

       用class定义自己的数据结构,可以带来很大的便捷,因为python自己的内置数据结构比较简单,有时候写起来很麻烦。这样的话,自己写自己的class,比较容易对症下药跟自己的数据集匹配得更好。

2.定义类

1class Myclass: 2              def __init__(self): 3                     …… 4              …… 5

       类中每定义一个函数(实际上书上都叫def定义的是类的方法),第一个参数都得是self,因为如果有:

1a=Myclass() 2b=Myclass() 3

       执行这两句话的时候,就是执行的Myclass().init(a)对应着上面写的定义的def init(self):里的self。就是把a赋值给self了。

3.在tensorflow程序中写类

       在tensorflow程序中,在类里每定义一个函数,都要把参数中的第一个参数设置为self,这样才能让数据和它对应的实例联系起来,就不在这再写复杂的代码,只说理论就可以了。这样的好处有很多,很工整是最基础的方面了,还能降低代码复杂性,随着给代码多加功能,这样的做法是最高效的了。

       而且类可以放在python模块中,共享给他人使用。柯老师最近强调过的几次学习到的东西要有效的传递,不能人一毕业了什么都带走了,新同学一点都没留下。这样就有个标准化的规范,有助于新老交替的时候,毕业生能给组里新同学留下一些现成的可以学习的东西,毕竟看代码是学习的一个重要途径,看别人的优秀代码学进自己脑子里,也完全是自己的重大收获。分享是好事。

       如果把类写的足够灵活,将来在其他项目里,总会用到自己的类来完成一些其他用途的。将会大大提升代码规范和节约时间。

三.自定义损失函数

1.传统的损失函数

       传统的损失函数,大家见过最普遍的也就是交叉熵,即cross_entropy,它描述了两个概率分布之间的距离,当交叉熵越小说明二者之间越接近,交叉熵要是为0,他俩基本上就是同一个值了。然后cross_entropy输出的不是概率分布,再一般加一个softmax,把它的值转换为概率。 Tensorflow将cross_entropy与softmax统一封装出来一个函数,实现了softmax后的cross_entropy损失函数:

1cross_entropy = tf.nn.softmax_cross_entropy_with_logits(y, y_) 2

       其中y和y_分别是真实值和预测值。(这也对应了上面第一节的内容,写个函数总能省事一些,减少很多麻烦)

2.自定义损失函数

       自定义损失函数的时候,首先有个关键问题,就是:我宁愿把损失值预估多了,也不愿预估少了。因为少了可能就不准了。损失函数说白了就是真实值和预测值的差值(也可能是大量数据集中所有差值之和,即:∑误差函数=损失函数),因此,损失函数越小,函数预测值就越准,损失函数若为0,预测准确率就是100%。

       先拿网上的一个例子举例:如果我是个卖水果的,想自己动手写一个销量预测的值,我们把预测的销量与实际销量的关系,作为损失函数调整的条件,那么大致得到:

1loss = tf.reduce.sum(tf.select(tf.greater(v, v’), a*(v-v’), b*(v-v’))) 2

       就是比较实际的v和预测出来的v’谁更大。如果v更大,就选择一个常量a,和他们的差相乘a*(v-v’);如果v’更大,就选择一个常量b,和他们的差相乘b*(v-v’);a和b的选择不一定能保证他们选出来的乘积为正数。这每个选择出来的值,就是一个误差函数,再把每一次的选择相加求和,即得损失函数loss。

       其实吴恩达的deep learning的课讲自定义损失函数的时候,用的就是同样的这个例子,只不过他讲的特别高大上,换成了“预测多了,预测少了”的两个值,生成了模拟数据集还增加了随机量作为不可预测的噪音。原理一样,得出的结果也一样。他还将损失函数的两个常量对换了一下,结果有显著差异,因此得出结论:对于相同的神经网络,不同的损失函数会对训练出来的模型产生重要的影响。直接截个吴恩达讲这个例子的图:

 

3.在自己的项目中,定义损失函数

       举几个简单例子。

       在我看来效率最低的损失函数,就是0-1损失。感知机用的就是这个损失函数,感知机具体的没有看,只了解了一点皮毛。 

       0-1损失就是,预测正确输出1,预测错误输出0,简单粗暴。还有对数损失,指数损失,均方误差……等等tensorflow自己提供的。

       如果我想自己改进一个,比如将均方误差改成一个四次方误差的:

1def myloss(y,y’): 2       return sum((y-y’)**4, axis = -1) 3

      这就是一个最简单的改进完成了。。。后面再用优化器训练它缩小误差,让它越准确越好。

      我们要做到的是在句子级别上、帧级别上都设计损失函数,将项目优化到最佳。这个也是我正在不断学习的地方。

四.自定义复杂网络结构

       自定义复杂网络结构想简要的说一下,这个部分不是有很好的讲解思路,怕误导大家。。。网上有很多现有的复杂网络结构,连代码带可视化plt.show()都输出出来写在网页上,可以清晰的看。如ResNet、MobileNet、DenseNet等等多个版本v1v2v3v4……,它们都有分为清晰的模块,每个conv2d、batchnormalization等操作全部打包成函数,可以直接使用。

       涉及到网络多输入或多输出的时候:多输入add时注意维数要匹配,如果维数不匹配就卷积或者别的操作匹配,全部在后面加0凑维数那种操作是不可取的;多输出还好,如果是两个网络的结果合并输出,如将经过神经网络激活后的输入作为输入的一部分和输入一起输进下一层神经网络中,也要对维数有一个对应;同时在sess.run()的时候,多输入多输出也要注意,feed_dict也不能少喂数据。

       自定义复杂的网络结构时,从0开始逐模块构建,从基本网络结构、loss计算、train_optimizer设计、数据集使用上,是一遍系统的学习,多读些现有的成熟代码,能够提高自己的编码能力。自己也在努力提升自己的水平,希望能够不断刷新知识,不断否定错误,持续进步。

 

代码部分的规范,参考:https://morvanzhou.github.io/

 

 

 

 

 

代码交流 2021