对于一个基础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:
#第一种
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)
……
sess.close()
#第二种
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
#这两种都对,写法很多,越简洁越好
这样整个神经网络的所有参数才能被激活。
(8)上述知识点中需要注意的地方:
(9)还有一些bug,需要灵活地解决:
在tensorflow中使用函数,只要将参数设置好,就能节省很多代码空间,让程序不至于那么冗余复杂。如果仅仅是简单的一个tensorflow程序,肯定不会有多么复杂;但是很极端的说,假如一个tensorflow程序中,需要进行很多很多相同的循环操作,多到让人看不清,那么函数就十分有必要了。举个例子。。。:
有一个字母表:alphabet = [A, B, C, [D, E, [F, G, H, [I, [J, K……]]]]],类似一个这样的无穷无尽的列表。现在需要在我们的程序里,把它输出一遍,如果你想硬刚这个列表,那就是得这样写:
for once in alphabet:
if isinstance(once, list):
for twice in once:
if isinstance(twice, list):
for third in twice:
if isinstance(third, list):
for fourth in third:
# 就到这吧……
print(fourth)
else:
print(third)
else:
print(twice)
else:
print(once)
写了四个循环我就受不了了,再多的话肯定这个代码可读性就降为0了,因此,选了这么一个极端的例子说明了函数的重要性(虽然用tensorflow写DNN一般不会用这种数组递归,但是函数还是很有必要的)。
例如,给一个神经网络写一个CNN的函数:
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial)
def bias_variable(shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial)
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
只要想添加一层卷积层,只要写好卷积核的形状和卷积核数,再直接调用就可以。
W = weight_variable([3, 11, 128, 256]) # patch 3x11, in size 128, out size 256
b = bias_variable([256])
conv = tf.nn.relu(conv2d(x, W) + b)
(默认步长为1,边界处理方式为same的前提下),只要将w的参数设置好(w在此处也就是filter),就能节省很多代码空间。以后每次加一层卷积,就可以简洁的写出。
def add_layer(inputs, in_size, out_size, activation_function=None,):
Weights = tf.Variable(tf.random_normal([in_size, out_size]))
biases = tf.Variable(tf.zeros([1, out_size]) + 0.1)
Wx_plus_b = tf.matmul(inputs, Weights) + biases
if activation_function is None:
outputs = Wx_plus_b
else:
outputs = activation_function(Wx_plus_b)
return outputs
设置好参数之后,在Weights中,随机生成一个行×列(in_size×out_size)的矩阵;在biases中,生成一个1×out_size的矩阵,再经过一个激活函数,输出的就是经过全连接并激活的神经元的值。(偏移量的默认值不为0,因此用tf.zeros()生成一个全0的矩阵之后,再随意加一个值即可)
当模型训练完成后,如果我想用不同几个文件夹里的数据都测试一遍,就不需要重复写好几遍,只改一个参数的路径。只要写一个测试函数即可:
def compute_accuracy(t_xs, t_ys):
global prediction
y_pre = sess.run(prediction, feed_dict={xs: t_xs, keep_prob: 1})
correct_prediction = tf.equal(tf.argmax(y_pre,1), tf.argmax(t_ys,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
result = sess.run(accuracy, feed_dict={xs: t_xs, ys: t_ys, keep_prob: 1})
return result
prediction是神经网络输出的已训练好的最后一层,那么只需要修改参数部分的t_xs和t_ys,就可以直接进行预测。要注意一个地方,只要你sess.run()的参数里面用到了placeholder,哪怕外面没显示,也要feed_dict给它喂数据。测试部分不需要dropout,因此保留全部的运行部分,keep_prob=1。
类似的还有很多,不一一赘述。 有了函数,就可以将它转换成模块,共享给他人使用:
用class定义自己的数据结构,可以带来很大的便捷,因为python自己的内置数据结构比较简单,有时候写起来很麻烦。这样的话,自己写自己的class,比较容易对症下药跟自己的数据集匹配得更好。
class Myclass:
def __init__(self):
……
……
类中每定义一个函数(实际上书上都叫def定义的是类的方法),第一个参数都得是self,因为如果有:
a=Myclass()
b=Myclass()
执行这两句话的时候,就是执行的Myclass().__init__(a)对应着上面写的定义的def __init__(self):里的self。就是把a赋值给self了。
在tensorflow程序中,在类里每定义一个函数,都要把参数中的第一个参数设置为self,这样才能让数据和它对应的实例联系起来,就不在这再写复杂的代码,只说理论就可以了。这样的好处有很多,很工整是最基础的方面了,还能降低代码复杂性,随着给代码多加功能,这样的做法是最高效的了。
而且类可以放在python模块中,共享给他人使用。柯老师最近强调过的几次学习到的东西要有效的传递,不能人一毕业了什么都带走了,新同学一点都没留下。这样就有个标准化的规范,有助于新老交替的时候,毕业生能给组里新同学留下一些现成的可以学习的东西,毕竟看代码是学习的一个重要途径,看别人的优秀代码学进自己脑子里,也完全是自己的重大收获。分享是好事。
如果把类写的足够灵活,将来在其他项目里,总会用到自己的类来完成一些其他用途的。将会大大提升代码规范和节约时间。
传统的损失函数,大家见过最普遍的也就是交叉熵,即cross_entropy,它描述了两个概率分布之间的距离,当交叉熵越小说明二者之间越接近,交叉熵要是为0,他俩基本上就是同一个值了。然后cross_entropy输出的不是概率分布,再一般加一个softmax,把它的值转换为概率。 Tensorflow将cross_entropy与softmax统一封装出来一个函数,实现了softmax后的cross_entropy损失函数:
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(y, y_)
其中y和y_分别是真实值和预测值。(这也对应了上面第一节的内容,写个函数总能省事一些,减少很多麻烦)
自定义损失函数的时候,首先有个关键问题,就是:我宁愿把损失值预估多了,也不愿预估少了。因为少了可能就不准了。损失函数说白了就是真实值和预测值的差值(也可能是大量数据集中所有差值之和,即:∑误差函数=损失函数),因此,损失函数越小,函数预测值就越准,损失函数若为0,预测准确率就是100%。
先拿网上的一个例子举例:如果我是个卖水果的,想自己动手写一个销量预测的值,我们把预测的销量与实际销量的关系,作为损失函数调整的条件,那么大致得到:
loss = tf.reduce.sum(tf.select(tf.greater(v, v’), a*(v-v’), b*(v-v’)))
就是比较实际的v和预测出来的v’谁更大。如果v更大,就选择一个常量a,和他们的差相乘a*(v-v’);如果v’更大,就选择一个常量b,和他们的差相乘b*(v-v’);a和b的选择不一定能保证他们选出来的乘积为正数。这每个选择出来的值,就是一个误差函数,再把每一次的选择相加求和,即得损失函数loss。
其实吴恩达的deep learning的课讲自定义损失函数的时候,用的就是同样的这个例子,只不过他讲的特别高大上,换成了“预测多了,预测少了”的两个值,生成了模拟数据集还增加了随机量作为不可预测的噪音。原理一样,得出的结果也一样。他还将损失函数的两个常量对换了一下,结果有显著差异,因此得出结论:对于相同的神经网络,不同的损失函数会对训练出来的模型产生重要的影响。直接截个吴恩达讲这个例子的图:
举几个简单例子。
在我看来效率最低的损失函数,就是0-1损失。感知机用的就是这个损失函数,感知机具体的没有看,只了解了一点皮毛。
0-1损失就是,预测正确输出1,预测错误输出0,简单粗暴。还有对数损失,指数损失,均方误差……等等tensorflow自己提供的。
如果我想自己改进一个,比如将均方误差改成一个四次方误差的:
def myloss(y,y’):
return sum((y-y’)**4, axis = -1)
这就是一个最简单的改进完成了。。。后面再用优化器训练它缩小误差,让它越准确越好。
我们要做到的是在句子级别上、帧级别上都设计损失函数,将项目优化到最佳。这个也是我正在不断学习的地方。
自定义复杂网络结构想简要的说一下,这个部分不是有很好的讲解思路,怕误导大家。。。网上有很多现有的复杂网络结构,连代码带可视化plt.show()都输出出来写在网页上,可以清晰的看。如ResNet、MobileNet、DenseNet等等多个版本v1v2v3v4……,它们都有分为清晰的模块,每个conv2d、batchnormalization等操作全部打包成函数,可以直接使用。
涉及到网络多输入或多输出的时候:多输入add时注意维数要匹配,如果维数不匹配就卷积或者别的操作匹配,全部在后面加0凑维数那种操作是不可取的;多输出还好,如果是两个网络的结果合并输出,如将经过神经网络激活后的输入作为输入的一部分和输入一起输进下一层神经网络中,也要对维数有一个对应;同时在sess.run()的时候,多输入多输出也要注意,feed_dict也不能少喂数据。
自定义复杂的网络结构时,从0开始逐模块构建,从基本网络结构、loss计算、train_optimizer设计、数据集使用上,是一遍系统的学习,多读些现有的成熟代码,能够提高自己的编码能力。自己也在努力提升自己的水平,希望能够不断刷新知识,不断否定错误,持续进步。
代码部分的规范,参考:https://morvanzhou.github.io/