【深度学习框架体系的学习】tensorflow

参考了github上开源源码eat_tensorflow2_in_30_days以及tensorflow的官网

结构化流程

1.数据处理

图片数据

在tensorflow中准备图片数据的常用方案有两种:
第一种是使用tf.keras中的ImageDataGenerator工具构建图片数据生成器。
第二种是使用tf.data.Dataset搭配tf.image中的一些图片处理方法构建数据管道。
第一种非常简单,主要用第二种TensorFlow原生方法:先定义加载图片的函数,使用并行化预处理num_parallel_calls 和预存数据prefetch来提升性能,还能查看部分样本,查看数据形状。

文本数据

文本数据预处理较为繁琐,包括中文切词(本示例不涉及),构建词典,编码转换,序列填充,构建数据管道等等。
在tensorflow中完成文本数据预处理的常用方案有两种:
第一种是利用tf.keras.preprocessing中的Tokenizer词典构建工具和tf.keras.utils.Sequence构建文本数据生成器管道。
第二种是使用tf.data.Dataset搭配.keras.layers.experimental.preprocessing.TextVectorization预处理层。
第一种非常复杂,要使用第二种,所以要先定义一些词表大小,单词最大长度这些,还需要写很多辅助函数,拿到数据之后还要向量化。
tf.data.TextLineDataset

1.trip

函数传函数用到的就是map函数。
读取test数据集不需要做shuffle
num_parallel_calls函数是为了提高数据map的处理速度, 并行处理,
tf.data.experimental.AUTOTUNE可以让程序自动的选择最优的线程并行个数
prefetch可以让数据队列预先缓存一定个数的batch, 提高对GPU的利用率.

2.定义模型

使用Keras接口有以下3种方式构建模型:使用Sequential按层顺序构建模型,使用函数式API构建任意结构模型,继承Model基类构建自定义模型。
1.如果用tf.keras.model,就必须传入input和output
2.如果要继承model类自定义模型的话,就得写一个类,并且这个类必须要有以下这些函数:__int__函数,build函数和call函数。
__int__函数做的是模型的初始化。
build函数,就是定义一些层。
call函数就相当于对定义的层走前向传播。
3.还可以定义Block类继承layer.Layer,也就是定义了一个非线性函数。

2.trip

tf.keras.backend.clear_session() #清空会话
keras和pytorch一样简单,只需要定义输入和输出,中间的都不用管。
浓缩成一个维度 x = layers.Flatten()(x)
只有卷积层和全连接层才会有参数的存在。
model.summary()
return_sequences=True是LSTM的一个参数,意思就是这一时刻LSTM的输出会是下一时刻LSTM的输入。

3.训练模型

训练模型通常有3种方法,内置fit方法,内置train_on_batch方法,以及自定义训练循环。此处我们选择最常用也最简单的内置fit方法。
别忘了用TensorBoard方法保存参数。
如果要用自定义的方法训练模型

3.trip

tf.GradientTape()梯度带用来记录模型的梯度
gradents = tape.gradient(loss, model.trainable_variables)
有时候不会去训练整个参数,有时候会用梯度带把某一层的参数固定住,亦或者这个参数是从某个大模型拿过来的,已经不需要对他调参了,会对某些variables进行固定,这时候不是用model.train(),而是自己设计一个variable列表,也就是只对特定的参数进行梯度更新。
.reset_states()梯度置零
tf.keras.callbacks.可以设置学习率减半或者早停相关的类,由此可见callbacks的作用就是对训练过程进行一个操作。

4.评估模型

4.trip

#在tensorboard中查看模型
notebook.start("–logdir ./data/keras_model")
还能画一些图什么的

5.使用模型

可以使用model.predict(ds_test)进行预测。
也可以使用model.predict_on_batch(x_test)对一个批量进行预测。
model(x_test)
model.call(x_test)都可以

5.trip

6.保存模型

可以使用Keras方式保存模型,也可以使用TensorFlow原生方式保存。

前者仅仅适合使用Python环境恢复模型;

1.保存模型结构和权重(后缀名h5):model.save(’./data/keras_model.h5’)
2.保存模型结构(to_json方法):json_str = model.to_json()
恢复模型结构(model_from_json方法):model_json = models.model_from_json(json_str)
3.保存模型权重(save_weights(’./data/keras_model_weight.h5’))
加载权重(.load_weights(’./data/keras_model_weight.h5’)),这个方法只能用模型结构来调用
后者则可以跨平台进行模型部署。

推荐使用后一种方式进行保存。

如何保存权重,此时权重文件为ckpt后缀名,以及如何保存模型结构于模型参数到文件,该方式保存的模型具有跨平台性便于部署。

6.trip

TensorFlow核心概念

张量数据结构

程序 = 数据结构+算法。
TensorFlow程序 = 张量数据结构 + 计算图算法语言
张量和计算图是 TensorFlow的核心概念。
Tensorflow的基本数据结构是张量Tensor。张量即多维数组。Tensorflow的张量和numpy中的array很类似。
从行为特性来看,有两种类型的张量,常量constant和变量Variable.
常量的值在计算图中不可以被重新赋值,变量可以在计算图中用assign等算子重新赋值。
tf.constant(数据)
可以简单地总结为:有几层中括号,就是多少维的张量
变量的值可以改变,可以通过assign, assign_add等方法给变量重新赋值
v = tf.Variable([1.0, 2.0], name=“V”)
v.assign_add([1.0, 1.0])

三种计算图

有三种计算图的构建方式:静态计算图,动态计算图,以及Autograph.
在TensorFlow1.0时代,采用的是静态计算图,需要先使用TensorFlow的各种算子创建计算图,然后再开启一个会话Session,显式执行计算图。
而在TensorFlow2.0时代,采用的是动态计算图,即每使用一个算子后,该算子会被动态加入到隐含的默认计算图中立即执行得到结果,而无需开启Session。
使用动态计算图即Eager Excution的好处是方便调试程序,它会让TensorFlow代码的表现和Python原生代码的表现一样,写起来就像写numpy一样,各种日志打印,控制流全部都是可以使用的。
使用动态计算图的缺点是运行效率相对会低一些。因为使用动态图会有许多次Python进程和TensorFlow的C++进程之间的通信。而静态计算图构建完成之后几乎全部在TensorFlow内核上使用C++代码执行,效率更高。此外静态图会对计算步骤进行一定的优化,剪去和结果无关的计算步骤。
如果需要在TensorFlow2.0中使用静态图,可以使用@tf.function装饰器将普通Python函数转换成对应的TensorFlow计算图构建代码。运行该函数就相当于在TensorFlow1.0中用Session执行代码。
使用tf.function构建静态图的方式叫做 Autograph.
一般情况下推荐使用静态图的方式或者Autograph,调试的时候可以用动态图。

计算图介绍

计算图由节点(nodes)和线(edges)组成。
节点表示操作符Operator,或者称之为算子,线表示计算间的依赖。
实线表示有数据传递依赖,传递的数据即张量。
虚线通常可以表示控制依赖,即执行先后顺序。

静态计算图

在TensorFlow1.0中,使用静态计算图分两步,第一步定义计算图,第二步在会话中执行计算图。
先构建一个图,其实不构建也会默认构建,然后通过tf.Session函数开启会话去run算子。
TensorFlow2.0 怀旧版静态计算图
TensorFlow2.0为了确保对老版本tensorflow项目的兼容性,在tf.compat.v1子模块中保留了对TensorFlow1.0那种静态计算图构建风格的支持。可称之为怀旧版静态计算图,已经不推荐使用了。
所以假如有一段现成的代码是1.0,想要在2.0运行,只能通过怀旧版。

动态计算图

在TensorFlow2.0中,使用的是动态计算图和Autograph.
在TensorFlow1.0中,使用静态计算图分两步,第一步定义计算图,第二步在会话中执行计算图。
动态计算图已经不区分计算图的定义和执行了,而是定义后立即执行。因此称之为 Eager Excution.
Eager这个英文单词的原意是"迫不及待的",也就是立即执行的意思。

Tensorflow2.0的 Autograph

动态计算图运行效率相对较低。
可以用@tf.function装饰器将普通Python函数转换成和TensorFlow1.0对应的静态计算图构建代码。
在TensorFlow1.0中,使用计算图分两步,第一步定义计算图,第二步在会话中执行计算图。
在TensorFlow2.0中,如果采用Autograph的方式使用计算图,第一步定义计算图变成了定义函数,第二步执行计算图变成了调用函数。 不需要使用会话了,一些都像原始的Python语法一样自然。
实践中,我们一般会先用动态计算图调试代码,然后在需要提高性能的的地方利用@tf.function切换成Autograph获得更高的效率。
当然,@tf.function的使用需要遵循一定的规范,我们后面章节将重点介绍。
@tf.function这个注解或者装饰器的方法就是把装饰的函数由动态执行转换为静态执行。

自动微分机制

神经网络通常依赖反向传播求梯度来更新网络参数,求梯度过程通常是一件非常复杂而容易出错的事情。
而深度学习框架可以帮助我们自动地完成这种求梯度运算。
Tensorflow一般使用梯度磁带tf.GradientTape来记录正向运算过程,然后反播磁带自动得到梯度值。
这种利用tf.GradientTape求微分的方法叫做Tensorflow的自动微分机制。
如何用磁带的方法求一阶微分,二阶微分,已经最小化。
首先通过with关键字和 GradientTapeAPI创建上下文:
with tf.GradientTape() as tape:
tape.watch([a, b, c])
watch函数就是用户来跟踪常量的。
还能反播梯度:dy_dx, dy_da, dy_db, dy_dc = tape.gradient(y, [x, a, b, c])。
如果要算二阶导数,就得用两层Tape了。
这里的代码就是在autograph中完成最小值求解,其实就是不管多复杂的神经网络,我们都可以通过定义磁带来记录那些我们想记录的量。
optimizer.minimize(f,[x])调用minimize这个方法甚至可以不用定义磁带,但是这里f传入的是目标函数的函数。

TensorFlow的层次结构

低阶API示范

这里有打印时间分割线的代码,再训练每个epoch的时候会有用,也就是printbar()函数

中阶API示范

TensorFlow的中阶API主要包括各种模型层,损失函数,优化器,数据管道,特征列等等。

高阶API示范

TensorFlow的高阶API主要为tf.keras.models提供的模型的类接口。
使用Keras接口有以下3种方式构建模型:使用Sequential按层顺序构建模型,使用函数式API构建任意结构模型,继承Model基类构建自定义模型。
此处分别演示使用Sequential按层顺序构建模型以及继承Model基类构建自定义模型。
中阶和低阶好像没有什么意义,平时也不用,但是遇到了能看懂就行。

TensorFlow低阶API

张量结构操作诸如:张量创建,索引切片,维度变换,合并分割。
张量数学运算主要有:标量运算,向量运算,矩阵运算。另外我们会介绍张量运算的广播机制。
其实这些都很简单了,和pytorch差不多,官方文档有更详细的介绍。
太多啦自己上官网慢慢看吧起码得熟悉几个。

张量的结构操作

张量创建

tf.fill([3,2], 5) 创建一个全为5的32形状的矩阵,其实fill这个API完全可以用zeros或者ones来实现,ones5不就行了。
tf.random.set_seed() 设置随机种子
tf.random.uniform(形状,最小值,最大值) 均匀随机分布
tf.random.normal([3, 3], mean=0, stddev=1.0) 正态分布随机
tf.random.truncated_normal() 正态分布随机,剔除2倍方差以外数据重新生成
tf.eye(3, 3) 单位矩阵
tf.linalg.diag([1, 2, 3]) 对角阵

索引切片

这一部分其实还是用pandas比较方便。
张量的索引切片方式和numpy几乎是一样的。切片时支持缺省参数和省略号。
对于tf.Variable,可以通过索引和切片对部分元素进行修改。
对于提取张量的连续子区域,也可以使用tf.slice.
此外,对于不规则的切片提取,可以使用tf.gather, tf.gather_nd, tf.boolean_mask。
tf.boolean_mask功能最为强大,它可以实现tf.gather, tf.gather_nd的功能,并且tf.boolean_mask还可以实现布尔索引。
如果要通过修改张量的某些元素得到新的张量,可以使用tf.where, tf.scatter_nd。

维度变换

维度变换相关函数主要有 tf.reshape, tf.squeeze, tf.expand_dims, tf.transpose.
tf.reshape 可以改变张量的形状。
tf.squeeze 可以减少维度。
tf.expand_dims 可以增加维度,这个API在pytorch用的是unsqueeze
tf.transpose 可以交换维度

合并分割

和numpy类似,可以用:
tf.concat和tf.stack方法对多个张量进行合并;
tf.split方法把一个张量分割成多个张量。
tf.concat和tf.stack有略微的区别:
tf.concat是连接,不会增加维度;
tf.stack是堆叠,会增加维度

张量的数学运算

标量运算

加减乘除乘方,以及三角函数,指数,对数等常见函数,逻辑比较运算符等都是标量运算符。
标量运算符的特点是对张量实施逐元素运算。
有些标量运算符对常用的数学运算符进行了重载。并且支持类似numpy的广播特性。
许多标量运算符都在 tf.math模块下。
详细见该代码

向量运算

向量运算符只在一个特定轴上运算,将一个向量映射到一个标量或者另外一个向量。 许多向量运算符都以reduce开头

矩阵运算

矩阵必须是二维的。类似tf.constant([1,2,3])这样的不是矩阵。
矩阵运算包括:矩阵乘法,矩阵转置,矩阵逆,矩阵求迹,矩阵范数,矩阵行列式,矩阵求特征值,矩阵分解等运算。
除了一些常用的运算外,大部分和矩阵有关的运算都在tf.linalg子包中。

广播机制

广播其实就是个维度扩展机制。
TensorFlow的广播规则和numpy是一样的:
1、如果张量的维度不同,将维度较小的张量进行扩展,直到两个张量的维度都一样。
2、如果两个张量在某个维度上的长度是相同的,或者其中一个张量在该维度上的长度为1,那么我们就说这两个张量在该维度上是相容的。
3、如果两个张量在所有维度上都是相容的,它们就能使用广播。
4、广播之后,每个维度的长度将取两个张量在该维度长度的较大值。
5、在任何一个维度上,如果一个张量的长度为1,另一个张量长度大于1,那么在该维度上,就好像是对第一个张量进行了复制。
tf.broadcast_to 以显式的方式按照广播机制扩展张量的维度。
tf.broadcast_static_shape(a.shape,b.shape) 计算广播后计算结果的形状,静态形状,TensorShape类型参数
tf.broadcast_dynamic_shape(tf.shape©, tf.shape(d)) 和上面的API差不多,都是计算两个张量广播之后的维度形状

AutoGraph的使用规范

有三种计算图的构建方式:静态计算图,动态计算图,以及Autograph。
TensorFlow 2.0主要使用的是动态计算图和Autograph。
动态计算图易于调试,编码效率较高,但执行效率偏低。
静态计算图执行效率很高,但较难调试。
而Autograph机制可以将动态图转换成静态计算图,兼收执行效率和编码效率之利。
当然Autograph机制能够转换的代码并不是没有任何约束的,有一些编码规范需要遵循,否则可能会转换失败或者不符合预期。
我们将着重介绍Autograph的编码规范和Autograph转换成静态图的原理。
并介绍使用tf.Module来更好地构建Autograph。
本篇我们介绍使用Autograph的编码规范。

Autograph编码规范总结

1、被@tf.function修饰的函数应尽可能使用TensorFlow中的函数而不是Python中的其他函数。例如使用tf.print而不是print,使用tf.range而不是range,使用tf.constant(True)而不是True.
2、避免在@tf.function修饰的函数内部定义tf.Variable.
3、被@tf.function修饰的函数不可修改该函数外部的Python列表或字典等数据结构变量。

AutoGraph的机制原理

1.控制流转换成tensorflow的代码
第一件事情是创建计算图。
即创建一个静态计算图,跟踪执行一遍函数体中的Python代码,确定各个变量的Tensor类型,并根据执行顺序将算子添加到计算图中。 在这个过程中,如果开启了autograph=True(默认开启),会将Python控制流转换成TensorFlow图内控制流。 主要是将if语句转换成 tf.cond算子表达,将while和for循环语句转换成tf.while_loop算子表达,并在必要的时候添加 tf.control_dependencies 指定执行顺序依赖关系。
2.第二件事情是执行计算图。

了解了以上Autograph的机制原理,我们也就能够理解Autograph编码规范的3条建议了。

1、被@tf.function修饰的函数应尽量使用TensorFlow中的函数而不是Python中的其他函数。例如使用tf.print而不是print.
解释:Python中的函数仅仅会在跟踪执行函数以创建静态图的阶段使用,普通Python函数是无法嵌入到静态计算图中的,所以在计算图构建好之后再次调用时,这些Python函数并没有被计算,而TensorFlow中的函数则可以嵌入到计算图中。使用普通的Python函数会导致 被@tf.function修饰前【eager执行】和被@tf.function修饰后【静态图执行】的输出不一致。
2、避免在@tf.function修饰的函数内部定义tf.Variable.
解释:如果函数内部定义了tf.Variable,那么在【eager执行】时,这种创建tf.Variable的行为在每次函数调用时候都会发生。但是在【静态图执行】时,这种创建tf.Variable的行为只会发生在第一步跟踪Python代码逻辑创建计算图时,这会导致被@tf.function修饰前【eager执行】和被@tf.function修饰后【静态图执行】的输出不一致。实际上,TensorFlow在这种情况下一般会报错。
3、被@tf.function修饰的函数不可修改该函数外部的Python列表或字典等数据结构变量。
解释:静态计算图是被编译成C++代码在TensorFlow内核中执行的。Python中的列表和字典等数据结构变量是无法嵌入到计算图中,它们仅仅能够在创建计算图时被读取,在执行计算图时是无法修改Python中的列表或字典这样的数据结构变量的。

AutoGraph和tf.Module

前面在介绍Autograph的编码规范时提到构建Autograph时应该避免在@tf.function修饰的函数内部定义tf.Variable.
但是如果在函数外部定义tf.Variable的话,又会显得这个函数有外部变量依赖,封装不够完美。
一种简单的思路是定义一个类,并将相关的tf.Variable创建放在类的初始化方法中。而将函数的逻辑放在其他方法中。
这样一顿猛如虎的操作之后,我们会觉得一切都如同人法地地法天天法道道法自然般的自然。
惊喜的是,TensorFlow提供了一个基类tf.Module,通过继承它构建子类,我们不仅可以获得以上的自然而然,而且可以非常方便地管理变量,还可以非常方便地管理它引用的其它Module,最重要的是,我们能够利用tf.saved_model保存模型并实现跨平台部署使用。
实际上,tf.keras.models.Model,tf.keras.layers.Layer 都是继承自tf.Module的,提供了方便的变量管理和所引用的子模块管理的功能。
因此,利用tf.Module提供的封装,再结合TensoFlow丰富的低阶API,实际上我们能够基于TensorFlow开发任意机器学习模型(而非仅仅是神经网络模型),并实现跨平台部署使用。

如何在tf.Modeule的基础上更好的构建graph

通俗点来讲就是不要直接写这个方法,要把这个方法封装到类中,在从这个类的实例化对象调用这个方法就行了。这样就十分方便,详细内容看github上的代码和deep_though的讲解。还有一种就是单独定义方法,然后像添加属性一样添加方法,这个方法添加到类当中,之后就一样了。
class DemoModule(tf.Module):
def init(self, init_value=tf.constant(0.0), name=None):
super(DemoModule, self).init(name=name)
with self.name_scope: # 相当于with tf.name_scope(“demo_module”)
self.x = tf.Variable(init_value, dtype=tf.float32, trainable=True)

@tf.function(input_signature=[tf.TensorSpec(shape=[], dtype=tf.float32)])
def addprint(self, a):
    with self.name_scope:
        self.x.assign_add(a)
        tf.print(self.x)
        return self.x

其实就是把成员遍历的定义放在__init__函数中,计算定义在其他函数中,也就是把变量的定义和计算区别开来,这样就可以对该类的实例化对象做各种操作了。
除了利用tf.Module的子类化实现封装,我们也可以通过给tf.Module添加属性的方法进行封装。也即是给实例化对象增加成员方法,也就是将成员变量和成员方法单独定义,然后再通过添加属性的方法也就是可以的。
tf.keras中的模型和层都是继承tf.Module实现的,也具有变量管理和子模块管理功能。
print(issubclass(tf.keras.Model, tf.Module))
如何判断一个类是另一个类的子类,就可以用python原生的函数issubclass()来去验证。

在tensorflow中如何冻结一层的操作

model.layers[0].trainable = False

TensorFlow中阶API

数据管道Dataset(tf.data)

如果需要训练的数据大小不大,例如不到1G,那么可以直接全部读入内存中进行训练,这样一般效率最高。 但如果需要训练的数据很大,例如超过10G,无法一次载入内存,那么通常需要在训练的过程中分批逐渐读入。
使用 tf.data API 可以构建数据输入管道,轻松处理大量的数据,不同的数据格式,以及不同的数据转换。

构建数据管道

可以从 Numpy array, Pandas DataFrame, Python generator, csv文件, 文本文件, 文件路径, tfrecords文件等方式构建数据管道。
其中通过Numpy array, Pandas DataFrame, 文件路径构建数据管道是最常用的方法。
通过tfrecords文件方式构建数据管道较为复杂,需要对样本构建tf.Example后压缩成字符串写到tfrecoreds文件,读取后再解析成tf.Example。
但tfrecoreds文件的优点是压缩后文件较小,便于网络传播,加载速度较快。

应用数据转换

Dataset数据结构应用非常灵活,因为它本质上是一个Sequece序列,其每个元素可以是各种类型,例如可以是张量,列表,字典,也可以是Dataset。
Dataset包含了非常丰富的数据转换功能。
map: 将转换函数映射到数据集每一个元素。
flat_map: 将转换函数映射到数据集的每一个元素,并将嵌套的Dataset压平。
interleave: 效果类似flat_map,但可以将不同来源的数据夹在一起。
filter: 过滤掉某些元素。
zip: 将两个长度相同的Dataset横向铰合。
concatenate: 将两个Dataset纵向连接。
reduce: 执行归并操作。
batch : 构建批次,每次放一个批次。比原始数据增加一个维度。 其逆操作为unbatch。
padded_batch: 构建批次,类似batch, 但可以填充到相同的形状。
window :构建滑动窗口,返回Dataset of Dataset.
shuffle: 数据顺序洗牌。
repeat: 重复数据若干次,不带参数时,重复无数次。
shard: 采样,从某个位置开始隔固定距离采样一个元素。
take: 采样,从开始位置取前几个元素。

提升管道性能

训练深度学习模型常常会非常耗时。
模型训练的耗时主要来自于两个部分,一部分来自数据准备,另一部分来自参数迭代。
参数迭代过程的耗时通常依赖于GPU来提升。
而数据准备过程的耗时则可以通过构建高效的数据管道进行提升。
以下是一些构建高效数据管道的建议。
1、使用 prefetch 方法让数据准备和参数迭代两个过程相互并行。
2、使用 interleave 方法可以让数据读取过程多进程执行,并将不同来源数据夹在一起。
3、使用 map 时设置num_parallel_calls 让数据转换过程多进行执行。
4、使用 cache 方法让数据在第一个epoch后缓存到内存中,仅限于数据集不大情形。
5、使用 map 转换时,先batch, 然后采用向量化的转换方法对每个batch进行转换。

tips

不太推荐tf.data.TFRecordDataset的方法构建dataset,因为流比较多比较复杂,不利于debug。
迭代器和next()函数一般情况下配合使用:it = iter(dataset) print(next(it).numpy())
dataset.element_spec,element_spec可以获取形状和类型
.from_tensor_slices(传入两个张量),并且对这两个张量做降维,然后拼成一个元组。
drop_remainder=True可以将最后一个batch忽略掉
Dataset.padded_batch
Dataset.repeat()
tf.data.TextLineDataset(内存中的文件对象),生成以行为单位的数据集
os.sep表示对应操作系统的分割符

激活函数activation(tf.keras.activations)

tf.keras.activations.serialize 序列化,就是返回激活函数的名字
tf.keras.activations.deserialize 反序列化,传入字符串返回激活函数fuction
tf.keras.activations.get 上两个的集合,用get多一些

模型层layers(tf.keras.layers)

深度学习模型一般由各种模型层组合而成。
tf.keras.layers内置了非常丰富的各种功能的模型层。例如,
layers.Dense,layers.Flatten,layers.Input,layers.DenseFeature,layers.Dropout
layers.Conv2D,layers.MaxPooling2D,layers.Conv1D
layers.Embedding,layers.GRU,layers.LSTM,layers.Bidirectional等等。
如果这些内置模型层不能够满足需求,我们也可以通过编写tf.keras.Lambda匿名模型层或继承tf.keras.layers.Layer基类构建自定义的模型层。
其中tf.keras.Lambda匿名模型层只适用于构造没有学习参数的模型层。

内置模型层

一些常用的内置模型层简单介绍如下。
基础层
Dense:密集连接层。参数个数 = 输入层特征数× 输出层特征数(weight)+ 输出层特征数(bias)
Activation:激活函数层。一般放在Dense层后面,等价于在Dense层中指定activation。
Dropout:随机置零层。训练期间以一定几率将输入置0,一种正则化手段。
BatchNormalization:批标准化层。通过线性变换将输入批次缩放平移到稳定的均值和标准差。可以增强模型对输入不同分布的适应性,加快模型训练速度,有轻微正则化效果。一般在激活函数之前使用。
SpatialDropout2D:空间随机置零层。训练期间以一定几率将整个特征图置0,一种正则化手段,有利于避免特征图之间过高的相关性。
Input:输入层。通常使用Functional API方式构建模型时作为第一层。
DenseFeature:特征列接入层,用于接收一个特征列列表并产生一个密集连接层。
Flatten:压平层,用于将多维张量压成一维。
Reshape:形状重塑层,改变输入张量的形状。
Concatenate:拼接层,将多个张量在某个维度上拼接。
Add:加法层。
Subtract: 减法层。
Maximum:取最大值层。
Minimum:取最小值层。
卷积网络相关层
Conv1D:普通一维卷积,常用于文本。参数个数 = 输入通道数×卷积核尺寸(如3)×卷积核个数
Conv2D:普通二维卷积,常用于图像。参数个数 = 输入通道数×卷积核尺寸(如3乘3)×卷积核个数
Conv3D:普通三维卷积,常用于视频。参数个数 = 输入通道数×卷积核尺寸(如3乘3乘3)×卷积核个数
SeparableConv2D:二维深度可分离卷积层。不同于普通卷积同时对区域和通道操作,深度可分离卷积先操作区域,再操作通道。即先对每个通道做独立卷积操作区域,再用1乘1卷积跨通道组合操作通道。参数个数 = 输入通道数×卷积核尺寸 + 输入通道数×1×1×输出通道数。深度可分离卷积的参数数量一般远小于普通卷积,效果一般也更好。
DepthwiseConv2D:二维深度卷积层。仅有SeparableConv2D前半部分操作,即只操作区域,不操作通道,一般输出通道数和输入通道数相同,但也可以通过设置depth_multiplier让输出通道为输入通道的若干倍数。输出通道数 = 输入通道数 × depth_multiplier。参数个数 = 输入通道数×卷积核尺寸× depth_multiplier。
Conv2DTranspose:二维卷积转置层,俗称反卷积层。并非卷积的逆操作,但在卷积核相同的情况下,当其输入尺寸是卷积操作输出尺寸的情况下,卷积转置的输出尺寸恰好是卷积操作的输入尺寸。
LocallyConnected2D: 二维局部连接层。类似Conv2D,唯一的差别是没有空间上的权值共享,所以其参数个数远高于二维卷积。
MaxPool2D: 二维最大池化层。也称作下采样层。池化层无可训练参数,主要作用是降维。
AveragePooling2D: 二维平均池化层。
GlobalMaxPool2D: 全局最大池化层。每个通道仅保留一个值。一般从卷积层过渡到全连接层时使用,是Flatten的替代方案。
GlobalAvgPool2D: 全局平均池化层。每个通道仅保留一个值。
循环网络相关层
Embedding:嵌入层。一种比Onehot更加有效的对离散特征进行编码的方法。一般用于将输入中的单词映射为稠密向量。嵌入层的参数需要学习。
LSTM:长短记忆循环网络层。最普遍使用的循环网络层。具有携带轨道,遗忘门,更新门,输出门。可以较为有效地缓解梯度消失问题,从而能够适用长期依赖问题。设置return_sequences = True时可以返回各个中间步骤输出,否则只返回最终输出。
GRU:门控循环网络层。LSTM的低配版,不具有携带轨道,参数数量少于LSTM,训练速度更快。
SimpleRNN:简单循环网络层。容易存在梯度消失,不能够适用长期依赖问题。一般较少使用。
ConvLSTM2D:卷积长短记忆循环网络层。结构上类似LSTM,但对输入的转换操作和对状态的转换操作都是卷积运算。
Bidirectional:双向循环网络包装器。可以将LSTM,GRU等层包装成双向循环网络。从而增强特征提取能力。
RNN:RNN基本层。接受一个循环网络单元或一个循环单元列表,通过调用tf.keras.backend.rnn函数在序列上进行迭代从而转换成循环网络层。
LSTMCell:LSTM单元。和LSTM在整个序列上迭代相比,它仅在序列上迭代一步。可以简单理解LSTM即RNN基本层包裹LSTMCell。
GRUCell:GRU单元。和GRU在整个序列上迭代相比,它仅在序列上迭代一步。
SimpleRNNCell:SimpleRNN单元。和SimpleRNN在整个序列上迭代相比,它仅在序列上迭代一步。
AbstractRNNCell:抽象RNN单元。通过对它的子类化用户可以自定义RNN单元,再通过RNN基本层的包裹实现用户自定义循环网络层。
Attention:Dot-product类型注意力机制层。可以用于构建注意力模型。
AdditiveAttention:Additive类型注意力机制层。可以用于构建注意力模型。
TimeDistributed:时间分布包装器。包装后可以将Dense、Conv2D等作用到每一个时间片段上。

自定义模型层

如果自定义模型层没有需要被训练的参数,一般推荐使用Lamda层实现。
如果自定义模型层有需要被训练的参数,则可以通过对Layer基类子类化实现。
Lambda层由于没有需要被训练的参数,只需要定义正向传播逻辑即可,使用比Layer基类子类化更加简单。
Lambda层的正向逻辑可以使用Python的lambda函数来表达,也可以用def关键字定义函数来表达。

损失函数losses

一般来说,监督学习的目标函数由损失函数和正则化项组成。(Objective = Loss + Regularization)
对于keras模型,目标函数中的正则化项一般在各层中指定,例如使用Dense的 kernel_regularizer 和 bias_regularizer等参数指定权重使用l1或者l2正则化项,此外还可以用kernel_constraint 和 bias_constraint等参数约束权重的取值范围,这也是一种正则化手段。
损失函数在模型编译时候指定。对于回归模型,通常使用的损失函数是平方损失函数 mean_squared_error。
对于二分类模型,通常使用的是二元交叉熵损失函数 binary_crossentropy。
对于多分类模型,如果label是one-hot编码的,则使用类别交叉熵损失函数 categorical_crossentropy。如果label是类别序号编码的,则需要使用稀疏类别交叉熵损失函数 sparse_categorical_crossentropy。
如果有需要,也可以自定义损失函数,自定义损失函数需要接收两个张量y_true,y_pred作为输入参数,并输出一个标量作为损失函数值。

内置损失函数

内置的损失函数一般有类的实现和函数的实现两种形式。
如:CategoricalCrossentropy 和 categorical_crossentropy 都是类别交叉熵损失函数,前者是类的实现形式,后者是函数的实现形式。
常用的一些内置损失函数说明如下。
mean_squared_error(平方差误差损失,用于回归,简写为 mse, 类实现形式为 MeanSquaredError 和 MSE)
mean_absolute_error (绝对值误差损失,用于回归,简写为 mae, 类实现形式为 MeanAbsoluteError 和 MAE)
mean_absolute_percentage_error (平均百分比误差损失,用于回归,简写为 mape, 类实现形式为 MeanAbsolutePercentageError 和 MAPE)
Huber(Huber损失,只有类实现形式,用于回归,介于mse和mae之间,对异常值比较鲁棒,相对mse有一定的优势)
binary_crossentropy(二元交叉熵,用于二分类,类实现形式为 BinaryCrossentropy)
categorical_crossentropy(类别交叉熵,用于多分类,要求label为onehot编码,类实现形式为 CategoricalCrossentropy)
sparse_categorical_crossentropy(稀疏类别交叉熵,用于多分类,要求label为序号编码形式,类实现形式为 SparseCategoricalCrossentropy)
hinge(合页损失函数,用于二分类,最著名的应用是作为支持向量机SVM的损失函数,类实现形式为 Hinge)
kld(相对熵损失,也叫KL散度,常用于最大期望算法EM的损失函数,两个概率分布差异的一种信息度量。类实现形式为 KLDivergence 或 KLD)
cosine_similarity(余弦相似度,可用于多分类,类实现形式为 CosineSimilarity)

自定义损失函数

评估指标metrics(tf.keras.metrics)

失函数除了作为模型训练时候的优化目标,也能够作为模型好坏的一种评价指标。但通常人们还会从其它角度评估模型的好坏。
这就是评估指标。通常损失函数都可以作为评估指标,如MAE, MSE, CategoricalCrossentropy等也是常用的评估指标。
但评估指标不一定可以作为损失函数,例如AUC, Accuracy, Precision。因为评估指标不要求连续可导,而损失函数通常要求连续可导。
编译模型时,可以通过列表形式指定多个评估指标。
如果有需要,也可以自定义评估指标。
自定义评估指标需要接收两个张量y_true, y_pred作为输入参数,并输出一个标量作为评估值。
也可以对tf.keras.metrics.Metric进行子类化,重写初始化方法, update_state方法, result方法实现评估指标的计算逻辑,从而得到评估指标的类的实现形式。
由于训练的过程通常是分批次训练的,而评估指标要跑完一个epoch才能够得到整体的指标结果。因此,类形式的评估指标更为常见。即需要编写初始化方法以创建与计算指标结果相关的一些中间变量,编写update_state方法在每个batch后更新相关中间变量的状态,编写result方法输出最终指标结果。
如果编写函数形式的评估指标,则只能取epoch中各个batch计算的评估指标结果的平均值作为整个epoch上的评估指标结果,这个结果通常会偏离拿整个epoch数据一次计算的结果。
评估指标的使用也很简单,线实例化出来,然后不断更新状态就行了。

常用的内置评估指标

MeanSquaredError(平方差误差,用于回归,可以简写为MSE,函数形式为mse)
MeanAbsoluteError (绝对值误差,用于回归,可以简写为MAE,函数形式为mae)
MeanAbsolutePercentageError (平均百分比误差,用于回归,可以简写为MAPE,函数形式为mape)
RootMeanSquaredError (均方根误差,用于回归)
Accuracy (准确率,用于分类,可以用字符串"Accuracy"表示,Accuracy=(TP+TN)/(TP+TN+FP+FN),要求y_true和y_pred都为类别序号编码)
Precision (精确率,用于二分类,Precision = TP/(TP+FP))
Recall (召回率,用于二分类,Recall = TP/(TP+FN))
TruePositives (真正例,用于二分类)
TrueNegatives (真负例,用于二分类)
FalsePositives (假正例,用于二分类)
FalseNegatives (假负例,用于二分类)
AUC(ROC曲线(TPR vs FPR)下的面积,用于二分类,直观解释为随机抽取一个正样本和一个负样本,正样本的预测值大于负样本的概率)
CategoricalAccuracy(分类准确率,与Accuracy含义相同,要求y_true(label)为onehot编码形式)
SparseCategoricalAccuracy (稀疏分类准确率,与Accuracy含义相同,要求y_true(label)为序号编码形式)
MeanIoU (Intersection-Over-Union,常用于图像分割)
TopKCategoricalAccuracy (多分类TopK准确率,要求y_true(label)为onehot编码形式)
SparseTopKCategoricalAccuracy (稀疏多分类TopK准确率,要求y_true(label)为序号编码形式)
Mean (平均值)
Sum (求和)
KL散度

自定义评估指标

优化器optimizers

机器学习界有一群炼丹师,他们每天的日常是:
拿来药材(数据),架起八卦炉(模型),点着六味真火(优化算法),就摇着蒲扇等着丹药出炉了。
不过,当过厨子的都知道,同样的食材,同样的菜谱,但火候不一样了,这出来的口味可是千差万别。火小了夹生,火大了易糊,火不匀则半生半糊。
机器学习也是一样,模型优化算法的选择直接关系到最终模型的性能。有时候效果不好,未必是特征的问题或者模型设计的问题,很可能就是优化算法的问题。
深度学习优化算法大概经历了 SGD -> SGDM -> NAG ->Adagrad -> Adadelta(RMSprop) -> Adam -> Nadam 这样的发展历程。
对于一般新手炼丹师,优化器直接使用Adam,并使用其默认参数就OK了。
一些爱写论文的炼丹师由于追求评估指标效果,可能会偏爱前期使用Adam优化器快速下降,后期使用SGD并精调优化器参数得到更好的结果。
此外目前也有一些前沿的优化算法,据称效果比Adam更好,例如LazyAdam, Look-ahead, RAdam, Ranger等.

优化器的使用

优化器主要使用apply_gradients方法传入变量和对应梯度从而来对给定变量进行迭代,或者直接使用minimize方法对目标函数进行迭代优化。
当然,更常见的使用是在编译时将优化器传入keras的Model,通过调用model.fit实现对Loss的的迭代优化。
初始化优化器时会创建一个变量optimier.iterations用于记录迭代的次数。因此优化器和tf.Variable一样,一般需要在@tf.function外创建。

内置优化器

深度学习优化算法大概经历了 SGD -> SGDM -> NAG ->Adagrad -> Adadelta(RMSprop) -> Adam -> Nadam 这样的发展历程。
在keras.optimizers子模块中,它们基本上都有对应的类的实现。
SGD, 默认参数为纯SGD, 设置momentum参数不为0实际上变成SGDM, 考虑了一阶动量, 设置 nesterov为True后变成NAG,即 Nesterov Acceleration Gradient,在计算梯度时计算的是向前走一步所在位置的梯度。
Adagrad, 考虑了二阶动量,对于不同的参数有不同的学习率,即自适应学习率。缺点是学习率单调下降,可能后期学习速率过慢乃至提前停止学习。
RMSprop, 考虑了二阶动量,对于不同的参数有不同的学习率,即自适应学习率,对Adagrad进行了优化,通过指数平滑只考虑一定窗口内的二阶动量。
Adadelta, 考虑了二阶动量,与RMSprop类似,但是更加复杂一些,自适应性更强。
Adam, 同时考虑了一阶动量和二阶动量,可以看成RMSprop上进一步考虑了Momentum。
Nadam, 在Adam基础上进一步考虑了 Nesterov Acceleration。

回调函数callbacks(tf.keras.callbacks.Callback)

本质上不是函数,而是个类。
检查点日志等有许多非常常用的回调函数,吃透官网所有API。
tf.keras.callbacks.Callback首先这个类是所有回调函数的父类,这个类包括训练,预测,测试都有很多方法。
什么是回调函数呢?我们可以将回调函数传入到fit函数中,也可以传入到evaluate函数中,也可以传入到predict函数中,这些方法有一个参数就是callbacks,将实例化对象以列表的形式传入。
想自定义回调函数就要继承这个类并重写一些方法。
tf.keras的回调函数实际上是一个类,一般是在model.fit时作为参数指定,用于控制在训练过程开始或者在训练过程结束,在每个epoch训练开始或者训练结束,在每个batch训练开始或者训练结束时执行一些操作,例如收集一些日志信息,改变学习率等超参数,提前终止训练过程等等。
同样地,针对model.evaluate或者model.predict也可以指定callbacks参数,用于控制在评估或预测开始或者结束时,在每个batch开始或者结束时执行一些操作,但这种用法相对少见。
大部分时候,keras.callbacks子模块中定义的回调函数类已经足够使用了,如果有特定的需要,我们也可以通过对keras.callbacks.Callbacks实施子类化构造自定义的回调函数。
所有回调函数都继承至 keras.callbacks.Callbacks基类,拥有params和model这两个属性。
其中params 是一个dict,记录了 training parameters (eg. verbosity, batch size, number of epochs…).
model即当前关联的模型的引用。
此外,对于回调类中的一些方法如on_epoch_begin,on_batch_end,还会有一个输入参数logs, 提供有关当前epoch或者batch的一些信息,并能够记录计算结果,如果model.fit指定了多个回调函数类,这些logs变量将在这些回调函数类的同名函数间依顺序传递。

内置回调函数

BaseLogger: 收集每个epoch上metrics在各个batch上的平均值,对stateful_metrics参数中的带中间状态的指标直接拿最终值无需对各个batch平均,指标均值结果将添加到logs变量中。该回调函数被所有模型默认添加,且是第一个被添加的。
History: 将BaseLogger计算的各个epoch的metrics结果记录到history这个dict变量中,并作为model.fit的返回值。该回调函数被所有模型默认添加,在BaseLogger之后被添加。
EarlyStopping: 当被监控指标在设定的若干个epoch后没有提升,则提前终止训练。
TensorBoard: 为Tensorboard可视化保存日志信息。支持评估指标,计算图,模型参数等的可视化。
ModelCheckpoint: 在每个epoch后保存模型。
ReduceLROnPlateau:如果监控指标在设定的若干个epoch后没有提升,则以一定的因子减少学习率。
TerminateOnNaN:如果遇到loss为NaN,提前终止训练。
LearningRateScheduler:学习率控制器。给定学习率lr和epoch的函数关系,根据该函数关系在每个epoch前调整学习率。
CSVLogger:将每个epoch后的logs结果记录到CSV文件中。
ProgbarLogger:将每个epoch后的logs结果打印到标准输出流中。

自定义回调函数

可以使用callbacks.LambdaCallback编写较为简单的回调函数,也可以通过对callbacks.Callback子类化编写更加复杂的回调函数逻辑。
如果需要深入学习tf.Keras中的回调函数,不要犹豫阅读内置回调函数的源代码。

TensorFlow高阶API

模型构建

可以使用以下3种方式构建模型:
使用Sequential按层顺序构建模型;
使用函数式API构建任意结构模型;
继承Model基类构建自定义模型。
对于顺序结构的模型,优先使用Sequential方法构建。
如果模型有多输入或者多输出,或者模型需要共享权重,或者模型具有残差连接等非顺序结构,推荐使用函数式API进行创建。
如果无特定必要,尽可能避免使用Model子类化的方式构建模型,这种方式提供了极大的灵活性,但也有更大的概率出错。
有三种方式构建模型,layer类所提供的方法,在model中都可以使用,优点就是没有super也没有call这些内容。

The Sequential model

序列管道的方式构建模型,适用于每一层只有一个输入张量和输出张量,具体见官网。这种方法非常不推荐,使用场合非常有限,只要出现了残差网络和多分枝基本上都用不了,通过列表的形式构建好了之后,就能访问属性变量了。也就是可以用列表的形式实例化一个Sequential模型,还有一种方法是先定义一个空的Sequential模型然后调用model.add函数不断往这个空的模型里添加。有add函数也就一定有pop函数了,pop就是删除层弹出层的意思。

还有迁移学习,迁移学习的意思就是把某几层的权重参数冻结住,然后只训练其他没冻住层的网络参数。冻结的意思就是layer.trainable = Flase。将预训练模型堆叠起来,然后比如bert,堆叠的意思就是冻结好了的模型,再往上加几层,这怎么加呢?其实就是通过keras.Sequential用列表的方式加到一起,所以叫堆叠,详情见官网。这个可以就是往预训练模型比如bert上加一个分类层。

The Functional API(重点,到官网好好看一看吧)

函数的方式构建模型,通过keras,只需要传入输入和输出。Keras函数API比tf.keras.Sequential API更加灵活的模型创建方式,处理一些非线性模型,还能做层的共享,多分枝,多输入多输出。
要使用函数式API构建此模型,先创建一个输入节点通过keras.Input API,将这个input作为实例化网络的输入得到输出x,然后不停的堆叠。

使用相同的层计算图定义多个模型

在函数式API中,模型是通过在层计算图中指定其输入和输出来创建的,这意味着可以使用单个层计算图来生成多个模型。

集成模型

原来集成模型这里官方文档里有,同样的输入三个模型得到三个不同的输出结果,将这个三个预测结果平均一下,
outputs = layer.average([y1,y2,y3]),平均一下,得到平均结果,有了平均结果和一开始的输入,传入keras.Model就可以得到集成模型了。

处理复杂的计算图拓扑

这部分主要是处理多个输入和多个输出,比如三个输入肯定有三个input层,两个输出就肯定有两个pred层,做法就是所有的input层封装到一个列表里面,所有的pred层封装到一个列表里,将两个列表作为输入和输出就行。这种模型构建好了之后就需要编译compile和训练了fit。编译阶段可以为不同的输入对应不同的损失函数。训练的时候,输入是通过字典键值对的方法传入输入。

残差

非线性拓扑结构。layer.add就是堆叠的意思,把其他block传进去就行,就是残差嘛。

共享层

其实就是不同的输入传入同一层。

Making new Layers and Models via subclassing

子类继承的方式构建模型。
这里主要分为两份部分,子类化layer构建新的层,子类化model构建新的模型。所以要先明确tf.keras.Model和tf.keras.layer是什么关系?首先layer继承的是Module,而Model既继承Layer类又继承了Module,所以layer所具有的方法在model中都可以使用,除此之外model自己也定义了很多独有方法。

自定义子类层

首先在__init__函数中初始化好权重,可以通过tf,valuble()也可以是tf.add_weight(),但是最好的做法是在创建一个build函数,将input_shape传到这个build函数,根据这个输入形状再构建权重w和b。层也可以嵌套,嵌套起来的层称做block。layer也有一个很常用的方法,就是layer.loss,再编写自定义循环的时候,这些loss是需要人为累加的,但是如果用fit()方法的话,就不用管那么多不用手动添加了。
call函数中有个training参数,这个参数在两种情况有不一样效果,BN层或者Dropout层。还有mask参数,自回归模型非常常用

自定义子类模型

model和layer有什么区别?model是继承于layer的,也就是layer能用的方法model都能用,model用的方法layer也都能用,除此之外model还有自己的方法。

三种模型灵活训练技巧

训练阶段分为三个步骤,训练、评估、预测。
Model.fit()
该方法功能非常强大, 支持对numpy array, tf.data.Dataset以及 Python generator数据进行训练。
并且可以通过设置回调函数实现对训练过程的复杂控制逻辑。
Model.evaluate()
该内置方法相比较fit方法更加灵活,可以不通过回调函数而直接在批次层次上更加精细地控制训练的过程。
Model.predict()
自定义训练循环无需编译模型,直接利用优化器根据损失函数反向传播迭代参数,拥有最高的灵活性。

分布式训练(tf.distribute.Strategy)

这里一共有六种策略

数据并行

tf.distribute.MirroredStrategy是一种基于归约算法的同步性学习,支持在一台机器的多个GPU上进行同步分布式训练,调用tf.distribute.MirroredStrategy这个API为每一个GPU创建副本,模型中每个变量都会在所有副本之间进行镜像,这些变量将共同形成一个名为MirroredVariable的单个概念变量,这些变量会保持同步。然后通过归约算法整合所有处理器的梯度
见官方文档。

这里有几个注意的点

1.在构造完dataset,并对dataset分批次的时候batch_size要设置成全局的,也就是所有GPU设备batch_size的总和。
2.在构建模型的时候,要在策略的作用域内构建,也就是要带上with strate.scope(),使模型在镜像策略中去构建。
3.fit很智能,自动梯度汇总更新了。

模型并行

你可能感兴趣的:(学习理解人工智能,tensorflow,深度学习,keras)