Tensorflow 2.0 使用流程详解
前言 :明确神经网络搭建流程,列举了过程中所有实现方法。 絮叨几句: 自己最初就是想借 tensorflow 架构一个简单网络,但看了网上诸多教程,依旧对 tensorflow 如何去实现感到糊涂,官方文档教程和指南也感觉逻辑搞得相当混乱和复杂,各种方法混用,看了反而更莫名其妙,获取到的知识碎片化严重,还记不牢。
更有些教程知识点反而集中到了感知机、线性回归、各类神经网络上。我只是想搞清架构网络的方法而已,比如怎么架构,最重要的有几种实现方法,毕竟tensorflow只是工具而已,没有整体概念,知识无法宏观掌控,就会觉得这玩意的学习乱七八糟、没有尽头似的。摸不清工具的底细是很难受的。
索性写笔记,记录获取到的知识点。也做总结归纳用。夹杂自己的理解,难免有错,仅供参考。
0. 写在前面
Tensorflow 模型架构比较成熟,系统复杂庞大,学习代价相对高些,但优点是稳定,可这一点就足以受到工业青睐。事实上,目前工业上大多都是使用这一架构部署人工智能网络。
至于其它的一些人工智能架构,一般特点是易用性强,易上手。多用于科研。如PyTorch,还有曾经的 Keras,但随着谷歌对Keras的收购,其单独的 Keras 库不再更新,取而代之的是,Keras 融入到了 tensorflow 中,变成其一个子模块tf.keras ,tf.keras 的所有实现都是以 tensorflow 为基础。(注:Keras 和 tf.keras 有些地方会有稍微的差别)
自己感觉学习 tensorflow 比较好,因为它就是神经网络架构的大杂烩,啥都有,啥都不缺,底层的基础函数、中层API、高层API,应有尽有。实现某一功能,可以借助不同层级的 API ,从而达到多种实现,这可能也是看完诸多教程知识混乱的原因。
贴一个比较重要的图,Tensorflow 的系统架构,对tensorflow整体情况有个了解,如下
其中,作为新手,只把它当做人工智能研究的工具,只需关注客户端部分就可以了。我们用python编写 tensorflow 代码就是客户端层面。
客户端层面,编写代码本质上就是函数组合调用,或者说是使用tensorflow提供的 API。但这也分情况:
-
一种是直接调用底层函数 如变量常量声明、一些数学运算等。对应于tf.Variable ,tf.constant ,tf.nn.conv2d ,tf.GradientTape 等等 -
一种是调用 tensorflow 已经把底层函数组合封装好以实现某一常用功能的函数(其实自己用底层函数也可以实现)。 如封装好一些卷积运算、激活函数的层,封装好的网络评估函数等。对应于 tf.keras.layers , tf.keras.losses , tf.keras.optimizers 等等,这些函数可能存在优化,可能比自己实现计算效率要高。
在未收购 keras 前,tensorflow 也有 tf.layers , tf.losses 等,现在仍旧存在。应该跟tf.keras.layers ,tf.keras.losses 差别不到哪去。
事实上,还有第三种,更高层的 API。就是
记得最初接触 tensorflow.v1 的时候,编写网络还都是青一色的自己手写各种层的实现(第一种API),手写网络性能评估。可能源于收购 Keras 的原因,现在更方便更普遍的是直接调用更高一层封装好的功能函数(第二种API)。Pytorch等其它网络的易用性也体现在此处,开发人员已经把常用功能函数封装,提供API接口,因此实现某个新网络搭建更快捷,使用者直接调用即可。
所以综上所述,可以把 tensorflow 提供的函数,或者说 API 分为三类:低阶API、中阶API、高阶API。
用一张图表示更清晰明了些:
所以,搭建神经网络时,就不止一种方式可以实现之。
不仅如此,训练网络也不止一种方法可以实现之。
更有甚者,你想要用 tensorflow 实现某一功能,都总有很多种方法可以实现之。
1. 数据准备
先明确一个目标吧。因为自己多处理图像,就实现用一个简单神经网络实现一个简单功能。确切说是
用经典的 LeNet 网络实现 手写数字识别。
1.1 数据获取
最通用方法就是下载数据集,现在热点人工智能方面的问题,网上都会有对应的公开数据集。当然也可以自己制作,比如借助网络和爬虫的力量 【使用爬虫构建新闻文本数据集】 。
至于手写数据集,神经网络的hello world ,网络上有。当然,使用起来有两种方法,
现在也有个 Fasion-MNIST 数据集,其外在特征跟 MNIST 完全一致,只是内容变成了10类商品,克服手写数字集太简单问题,更复杂、具象化些。下载从这:【Fasion-MNIST 数据集】
-
使用代码,自动下载 from tensorflow.keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
-
下载至本地,直接使用
import numpy as np
MNIST = np.load('./mnist.npz')
x_train, y_train = MNIST['x_train'], MNIST['y_train']
x_test, y_test = MNIST['x_test'], MNIST['x_test']
(x_train, y_train), (x_test, y_test) = mnist.load_data(r'd:\mnist-npz\mnist.npz')
1.2 数据处理
本例数据比较简单,已经划分好训练集和测试集,数据量也小,不用怎么处理。
只要想办法载入就可以了,遵循五步走即可,直接上代码
def preprocess(x, y):
x = tf.cast(x, tf.float32)/ 255
y = tf.cast(y, tf.int32)
y = tf.one_hot(y, depth=10)
return x, y
train_data = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_data = train_data.map(preprocess)
train_data = train_data.repeat(3)
train_data = train_data.shuffle(5000)
batch_size = 500
train_data = train_data.repeat(3).shuffle(5000).batch(batch_size)
另外值得强调的是!真实项目中数据处理是极其极其极其重要的一部分,并且这一重要部分牵扯到至少三个重要知识点:
(1)数据集划分
包括训练集、验证集、测试集等。
听起来似乎很简单,但这是个技术活,好的数据集划分和使用可以大大提高网络性能。
(2)数据增广
当训练集中数据很少时,通过一些手段来增加训练数据数量。见【深度学习之数据增强方式和实现总结】。
(3)数据读取
-
当数据量很少时,可以直接读取,加载到内存中,导入到网络中,进行训练。很推荐这么做,这种方法很高效、快捷。 -
当数据量很大时,不能一次性全部加载入内存,此时就需要一些手段来处理这些数据。此时会牵扯到 标准tensorflow数据格式(TFRecords) , **数据输入方法(tf.data API, Queue API, PreloadedData, Feeding)**等一系列”复杂“内容。
此外还可能包括数据归一化处理等,每个都是一个值得研究的知识点。现不详述,现也不深究。(曾痛苦地深究过,但都已忘记)
但先有概念、框架,了解其在整个流程的大概位置,具体内容后续再详细了解,再向框架中定点填充实物。
2. 网络搭建
经典 LeNet 网络,结构如下:
Layer | Description | output_shape | params |
---|
INPUT | Input_data | 32 x 32 | 0 | C1 | Conv 5x5 , strides= 1, filter_number = 6 | 6, 28 x 28 | 156 | S2 | Pooling 2x2, strides = 2 / sigmoid | 6, 14 x 14 | 12 | C3 | Conv 5x5, strides = 1, filter_number = 16 | 16, 10 x 10 | 24164 | S4 | Pooling 2x2, strides = 2 / sigmoid | 16, 5 x 5 | 32 | C5 | FC 120 | 120 | 48120 | F6 | FC 84 /sigmoid | 84 | 10164 | OUTPUT | FC 10 | 10 | 840 |
下面使用前述三种层次的 API 进行搭建。这里用到了四种方法,可能还会有别的方法。
提前说明
2.1 方式 1 ------ 应用 tensorflow 底层函数搭建(低阶 API)
w1 = tf.Variable(tf.random.truncated_normal(shape = [5,5,1,6], stddev= 0.1))
b1 = tf.Variable(tf.constant(0.1, shape = [6]))
w2 = tf.Variable(tf.random.truncated_normal(shape = [5,5,6,16], stddev= 0.1))
b2 = tf.Variable(tf.constant(0.1, shape = [16]))
w3 = tf.Variable(tf.random.truncated_normal(shape = [5*5*16, 120], stddev= 0.1))
b3 = tf.Variable(tf.constant(0.1, shape = [120]))
w4 = tf.Variable(tf.random.truncated_normal(shape = [120, 84], stddev= 0.1))
b4 = tf.Variable(tf.constant(0.1, shape = [84]))
w5 = tf.Variable(tf.random.truncated_normal(shape = [84, 10], stddev= 0.1))
b5 = tf.Variable(tf.constant(0.1, shape = [10]))
w_b = [w1, b1, w2, b2, w3, b3, w4, b4, w5, b5]
def myNet(inputs):
inputs = tf.reshape(inputs, [-1, 28, 28, 1])
conv1 = tf.nn.bias_add(tf.nn.conv2d(inputs, w1, strides=[1,1,1,1], padding='SAME'), b1)
pool1 = tf.nn.max_pool(conv1, ksize=[1,2,2,1], strides=[1,2,2,1],padding='VALID')
o1 = tf.nn.relu(pool1)
conv2 = tf.nn.bias_add(tf.nn.conv2d(o1, w2, strides=[1,1,1,1], padding='VALID'), b2)
pool2 = tf.nn.max_pool(conv2, ksize=[1,2,2,1], strides=[1,2,2,1],padding='VALID')
o2 = tf.nn.relu(pool2)
f = tf.reshape(o2, [-1, 5*5*16])
fc3 = tf.matmul(f, w3) + b3
o3 = tf.nn.relu(fc3)
fc4 = tf.matmul(o3, w4) + b4
o4 = tf.nn.relu(fc4)
outputs = tf.matmul(o4, w5) + b5
return outputs
说明1
tensorflow 2.0 好像不支持 feed_dict 了,同样也不支持 Session() ,取而代之的是,Eager execution ,使用另一种方式实现 gragh。
下面自己训练这部分模型时,没有使用相关技巧,所以运算相当慢。
说明2
因为 说明1 中的缘故:没有 feed 和 Seesion 了,自己把整个网络过程搭建到函数中了,以前 tensorflow1.0 是不需要的 。
tensorflow2.0 后面训练时要用到求梯度,其中就需要整个 y_pred = f (x_inputs) 过程,所以就把它写到一个函数myNet() 里了。
说明3
这里需要注意记录下网络中的需要训练的参数,后续训练时,需要指定训练参数(更新参数) trainable_variable ,即这里 w1, b1, w2 …。且!一定要 在网络函数外 提前定义好,不要放在函数里! 也可以使用字典定义。
2.2 方式 2 ------ 应用 Sequential 容器线性搭建(中阶 API)
代码
model = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(filters = 6, kernel_size = 5, strides= 1, input_shape=(28,28,1), padding="SAME"),
tf.keras.layers.MaxPooling2D(pool_size = 2, strides = 2),
tf.keras.layers.ReLU(),
tf.keras.layers.Conv2D(filters = 16, kernel_size = 3, strides = 1, padding="VALID"),
tf.keras.layers.MaxPooling2D(pool_size = 2, strides = 2),
tf.keras.layers.ReLU(),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(units = 120, activation = 'relu'),
tf.keras.layers.Dense(units = 84, activation = 'relu'),
tf.keras.layers.Dense(10, activation = None)
])
model.summary()
说明1:
也可以使用tf.keras.Sequential() ,俩一样。
另,在向sequential 容器中加入网络相关层时,也可以使用函数.add() 添加方式,如下
model = tf.keras.models.Sequential()
model.add(tf.keras.layers.Conv2D())
model.add()
......
效果一样
说明2:
tf.keras.layers 包含的函数还有很多,具体可参见 【tensorflow 官方文档之 tf.keras.layers】,几个比较常用的函数罗列如下:
.Input() :输入层。
.Flatten() :展平层。将多维张量展开为一维。
.Dense() :密集连接层,即线性层或全连接层。
.Conv2D() :二维卷积层。
.MaxPool2D() :二维最大池化层,或下采样层。无训练参数,用于降维。
.AveragePooling2D() :二维平均池化层,或下采样层。无训练参数,用于降维。
.Activation() :激活函数。也可以直接在 Dense, Conv2D 参数中指定。
.Dropout() :随机置零层。正则化方式的一种。
.BatchNormalization() :批量标准化。通过线性变换将输入缩放到稳定均值和标准差,一般在激活函数前使用。
具体参数设置和说明,同理参见上述官方文档。
说明3:
该种方式有局限性,即,只可构造线性模型,只可模型层层堆叠。而不能构建层间有交叉、跨越、反复的复杂网络,针对这些复杂网络可以使用上述的 函数API 或下面的 Model方法。
2.3 方式 3 ------ 应用继承 Model 类灵活搭建(高阶 API)
class myModel(tf.keras.Model):
def __init__(self):
super(myModel, self).__init__()
self.conv1 = tf.keras.layers.Conv2D(filters = 6, kernel_size = 5, strides= 1, padding="SAME")
self.pool1 = tf.keras.layers.MaxPooling2D(pool_size = 2, strides = 2)
self.Relu = tf.keras.layers.ReLU()
self.conv2 = tf.keras.layers.Conv2D(filters = 16, kernel_size = 3, strides = 1, padding="VALID")
self.pool2 = tf.keras.layers.MaxPooling2D(pool_size = 2, strides = 2)
self.f = tf.keras.layers.Flatten()
self.d1 = tf.keras.layers.Dense(units = 120, activation = 'relu')
self.d2 = tf.keras.layers.Dense(units = 84, activation = 'relu')
self.d3 = tf.keras.layers.Dense(10, activation = None)
def call(self, inputs, training = None, mask = None):
x = self.conv1(inputs)
x = self.pool1(x)
x = self.Relu(x)
x = self.conv2(x)
x = self.pool2(x)
x = self.Relu(x)
x = self.f(x)
x = self.d1(x)
x = self.d2(x)
outputs = self.d3(x)
return outputs
model = myModel()
2.4 方式4 ------ 应用 Model() 函数灵活搭建(与方式3等价)
INPUT = tf.keras.Input(shape = (28, 28, 1))
x = tf.keras.layers.Conv2D(filters = 6, kernel_size = 5, strides= 1, padding="SAME")(INPUT)
x = tf.keras.layers.MaxPooling2D(pool_size = 2, strides = 2)(x)
x = tf.keras.layers.ReLU()(x)
x = tf.keras.layers.Conv2D(filters = 16, kernel_size = 3, strides = 1, padding="VALID")(x)
x = tf.keras.layers.MaxPooling2D(pool_size = 2, strides = 2)(x)
x = tf.keras.layers.ReLU()(x)
x = tf.keras.layers.Flatten()(x)
x = tf.keras.layers.Dense(units = 120, activation = 'relu')(x)
x = tf.keras.layers.Dense(units = 84, activation = 'relu')(x)
OUTPUT = tf.keras.layers.Dense(10, activation = None)(x)
model = tf.keras.Model(inputs = INPUT, outputs = OUTPUT)
3. 网络训练与性能评估
3.1 函数式
def cross_entropy(y_pred, y_true):
y_pred = tf.clip_by_value(y_pred, 1e-9, 1.)
res = tf.reduce_mean(-tf.reduce_sum(y_true * tf.math.log(y_pred)))
return res
def accuracy(y_pred, y_true):
correct_prediction = tf.equal(tf.argmax(y_pred, 1), tf.argmax(y_true, 1))
res = tf.reduce_mean(tf.cast(correct_prediction, tf.float32), axis=-1)
return res
learning_rate = 0.01
optimizer = tf.keras.optimizers.Adam(learning_rate)
epoch = 1
display_steps = 100
for n in range(epoch):
for steps,(x, y) in enumerate(train_data):
with tf.GradientTape() as gt:
y_pred = myNet(x)
loss = cross_entropy(y_pred, y)
trainable_variables = w_b
grad = gt.gradient(loss, trainable_variables)
optimizer.apply_gradients(zip(grad, trainable_variables))
if steps % display_steps == 0:
acc = accuracy(y_pred, y)
print("steps: %i, loss: %f, accuracy: %f" % (steps, loss, acc))
说明1
这种方法适用于上面搭建的所有模型。
3.2 fit() 方式
x = tf.reshape(x_train, shape = [-1, 28, 28, 1])
y = tf.keras.utils.to_categorical(y_train)
x = tf.cast(x, tf.float64)
y = tf.cast(y, tf.int32)
model.compile(
optimizer = tf.keras.optimizers.SGD(lr = 0.01),
loss = tf.keras.losses.CategoricalCrossentropy(from_logits = False),
metrics = ['categorical_accuracy']
)
history = model.fit(x, y, batch_size = 1000, epochs = 2)
model.summary()
说明1
尤其需要注意的是,
- 输入、输出要匹配搭建网络的输入输出,包括尺寸、数据类型等。
- 损失函数要根据实际情况选择(比如上面 one-hot 格式与 int 格式的交叉熵函数就不一样)
- 该种方法不适用于自编程实现的模型,如 2.1 中自编的网络。
4. 扩展内容
-
网络可视化:TensorBoard -
高效率模型训练:多GPU训练 -
模型保存与加载 -
模型部署 -
Estimator
再深究时补充
5. 写在后面
(1)上面所有代码都是基于 tensorflow 2.0 编写,都是可直接运行通过的。
(2)再次强烈官方文档,有任何问题可以查这个 —> 【TensorFlow 2.0 docs】。不推荐官方教程、指南。
(3)上面的网络对于手写数字识别结果非常不好,但自己目的也不在于这,而在于理清如何走通整个流程,及弄清所有可用方法。如果想结果好些,可以考虑构建下面简单网络,用 Sequential()容器搭建方法,代码如下
model = tf.keras.models.Sequential([
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(units = 128, activation = 'relu'),
tf.keras.layers.Dense(10, activation = 'softmax')
])
(4)上文中有些名词是自己瞎起的,可意会就行。
花费了一天时间整理,完结!下班! and, Have a nice weekend!
|