Tensorflow2入门

Tensorflow2入门

前言

  • 最近更新了TF2,发现以前的TF代码都跑不了了。改动是真的很大。并且在重写原来的代码的时候遇到了很多坑,在这里记录一下学习过程。
  • 没有安装TF2之前,我对其印象停留在TF2强行引入了keras。但是我上手的时候受到了震惊:placeholder与session都被删除了。。。
  • 官方有一个简单的代码升级脚本,但是其仅仅是把tf替换为tf.compat.v1,并没有太大的作用。
  • 由于我个人对keras的便捷、代码易读性比较喜欢,所以对新版本并不排斥。但是如果只用keras,底层的很多操作就不支持了,比如训练的单步进行。所以需要学习TF2或者keras中更底层的部分。

爬过的坑,尝试直接将keras和TF底层结构拼接在一起

  • 由于keras被引入了,我有一个大胆的猜想:把keras高层API和TF底层代码直接接在一起:
1
2
3
4
5
6
7
def bin_reg_model():
inputs = layers.Input(shape=(1,), dtype=np.float32)
h1 = layers.Dense(1, input_shape=(1,))(inputs)
outputs = tf.multiply(w, h1) + b
model = tf.keras.models.Model(inputs=inputs, outputs=outputs)
model.summary()
return model
  • 运行的时候发现不可行,报错显示有参数不能被训练
  • 查询资料的过程中发现已经有人提出了这些问题:主要矛盾:keras的layer与tf的底层不能结合,如果要使用keras模块,就只能用keras的compile,不能用TF代码来训练,很不方便。 https://github.com/tensorflow/tensorflow/issues/26844#issuecomment-516755626
  • 这条路就这么断了,目前来说TF的高层和底层API还是隔离开的,感觉keras强行弄进来没有什么太大的意义,希望TF以后可以做到高层底层API混合调用。

TF2 eager模式(用以代替和改写原来的session静态图模式)

  • 首先,回顾一下TF1的代码:
1
2
3
4
5
6
7
8
9
10
cost = tf.reduce_mean((tf.square(predict - yh)) / 2) # 最小二乘法代价函数
optimizer = tf.train.AdamOptimizer(0.01) # 使用ADAM优化,学习率0.01
train_step = optimizer.minimize(cost) # 一个训练步骤
with tf.Session() as sess:
sess.run(init) # 初始化
for j in range(epoch): # 进行epoch次大循环
for i in range(30): # 对每个数据点的遍历
sess.run(train_step, {xh: x[i], yh: y[i]}) # 塞入一个数据点
  • TF2直接采用动态图的方法,与TF1相比,不再需要提前构造静态的神经网络,再采用Session与静态图进行交互。Session作为TF的标志性操作之一,在TF2中被删去,这个变化有很多人不适应,TF似乎面目全非了。但是在eager模式下,原TF的代码也可以完全改写过来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def bin_reg_model(xh):
return tf.multiply(w, xh * xh) + b # 预测值
def loss_object(predict, yh):
return tf.reduce_mean((tf.square(predict - yh)) / 2) # 最小二乘法代价函数
optimizer = tf.optimizers.Adam(1) # 使用ADAM优化,学习率0.01
def train_step(x_train, y_train):
with tf.GradientTape() as tape: # 在tape下才能进行反向传播求导
logits = bin_reg_model(x_train) # bin_reg_model输出模型的预测值
loss_value = loss_object(y_train, logits) # loss_object函数计算误差值
print('[+] Loss', loss_value.numpy())
grads = tape.gradient(loss_value, [w, b])
optimizer.apply_gradients(zip(grads, [w, b])) # optimizer可以自定义,也可以使用内置的类
for j in range(epoch): # 进行epoch次大循环
for i in range(30): # 对每个数据点的遍历
train_step(x[i], y[i]) # 塞入一个数据点
  • 需要注意的是,在TF1中也有apply_gradients更新的操作,只不过为了简便,TF1中一般使用optimizer.minimize(cost)代替gradientoptimizer.apply_gradients两步,即optimizer.minimize(cost)整合了这两步。TF2也有optimizer.minimize()函数,但是在TF2中,minimize函数似乎没有TF1好用。
  • 另外,由于TF2使用了eager模式,可以一边构造神经网络一边debug,还可以通过a.shape得到当前向量的尺寸,可以更方便地编写代码。

TF2 tensorboard

  • 与TF1相比,TF2的TFB变得更加简洁:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    log_dir = 'tf2_log'
    writer = tf.summary.create_file_writer(log_dir)
    ...
    for j in range(epoch): # 进行epoch次大循环
    for i in range(30): # 对每个数据点的遍历
    loss = train_step(x[i], y[i]) # 塞入一个数据点
    with writer.as_default(): # tensorboard记录
    tf.summary.scalar('loss', loss, step=j*epoch+i) # 在任何位置都可以调用

keras自定义层

  • 由于TF2主推keras,但是有的时候需要更加底层的操作修改,所以需要学习自定义层的编写。

注意事项

  • 尽量保证自定义层类属性和方法的完整性,不然模型储存过程中会出现各种问题(get_config、__init__中的name等)
  • 自定义层调用时需要设置name,模型重载时需要设置对应的name字典

需要重写(或可以重写)的函数

1
2
3
4
5
6
7
8
## 必重写
def __init__(self, position, d_model, name="PositionEmbedding", **kwargs): # 申请、储存本层需要用到的属性、对象等
def call(self, x): # 调用该层时进行的运算
def get_config(self): # 返回初始化变量,用于模型读取时使用
## 可重写
def build(self, input_shape): # 需要根据input_shape改变配置时要重写的函数
def compute_output_shape(self, input_shape): # 返回的矩阵大小

一些例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
'''
keras层
为input添加一个可训练权重 => input · weight
'''
class Add_weight(layers.Layer):
def __init__(self, name="Add_weight", **kwargs): # 申请、储存本层需要用到的属性、对象等
super().__init__(name=name, **kwargs)
def build(self, input_shape): # 需要根据input_shape改变配置时要重写的函数
# 添加的可训练的权重
self.weight_to_mut = tf.Variable(tf.constant(0.1, shape=[input_shape[2],input_shape[2]]),trainable=True)
super().build(input_shape) # 一定要在最后调用它
def call(self, x): # 调用该层时进行的运算
return K.dot(x, self.weight_to_mut) # 点乘
def get_config(self): # 返回初始化变量,用于模型读取时使用
config=super().get_config().copy()
return config
def compute_output_shape(self, input_shape): # 返回的矩阵大小
return (input_shape[2], input_shape[2])
input_wq = Add_weight(name='add_1')(inputs)
'''
attention is all you need 论文中的 position embedding层
继承keras layer
h2 = PositionEmbedding(time_step, embedding_size)(h1)
'''
class PositionEmbedding(layers.Layer):
def __init__(self, position, d_model, name="PositionEmbedding", **kwargs):
super().__init__(name=name, **kwargs)
# 储存用于恢复模型的init参数,用在get_config函数中
#####
self.position = position
self.d_model = d_model
#####
self.pos_encoding = self.positional_encoding(position, d_model)
def get_angles(self, position, i, d_model):
angles = 1 / tf.pow(10000, (2 * (i // 2)) /
tf.cast(d_model, tf.float32))
return position * angles
def positional_encoding(self, position, d_model):
angle_rads = self.get_angles(
position=tf.range(position, dtype=tf.float32)[:, tf.newaxis],
i=tf.range(d_model, dtype=tf.float32)[tf.newaxis, :],
d_model=d_model)
# apply sin to even index in the array
sines = tf.math.sin(angle_rads[:, 0::2])
# apply cos to odd index in the array
cosines = tf.math.cos(angle_rads[:, 1::2])
pos_encoding = tf.concat([sines, cosines], axis=-1)
pos_encoding = pos_encoding[tf.newaxis, ...]
return tf.cast(pos_encoding, tf.float32)
def call(self, inputs):
return inputs + self.pos_encoding[:, :tf.shape(inputs)[1], :]
def get_config(self):
config = super().get_config().copy()
config.update({
'position': self.position,
'd_model': self.d_model
})
return config
h2 = PositionEmbedding(time_step, embedding_size,
name="PositionEmbedding")(h1) # 自定义层设置name

包含自定义层的模型重载

1
2
3
4
5
6
7
model = tf.keras.models.load_model('model/attention.keras', custom_objects={
'PositionEmbedding': PositionEmbedding,
'add_1':Add_weight,
'add_2':Add_weight,
'add_3':Add_weight,
}) # 需要指定每个name对应的层的class

TF2 keras的坑(keras本身的坑)

问题

  • 在学习完attention有一段时间后,我想使用attention也写一个模型来检测webshell,同时也想实践一下keras自定义层。于是写了以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
'''
attention is all you need 论文中的 position embedding层
继承keras layer
h2 = PositionEmbedding(time_step, embedding_size)(h1)
'''
class PositionEmbedding(layers.Layer):
...
'''
keras层
为input添加一个可训练权重 => input · weight
'''
class Add_weight(layers.Layer):
def __init__(self, name="Add_weight", **kwargs): # 申请、储存本层需要用到的属性、对象等
super().__init__(name=name, **kwargs)
def build(self, input_shape): # 需要根据input_shape改变配置时要重写的函数
# 添加的可训练的权重
self.weight_to_mut = tf.Variable(tf.constant(0.1, shape=[input_shape[2],input_shape[2]]),trainable=True)
super().build(input_shape) # 一定要在最后调用它
def call(self, x): # 调用该层时进行的运算
return K.dot(x, self.weight_to_mut) # 点乘
def get_config(self): # 返回初始化变量,用于模型读取时使用
config=super().get_config().copy()
return config
def compute_output_shape(self, input_shape): # 返回的矩阵大小
return (input_shape[2], input_shape[2])
def build_model():
inputs = tf.keras.Input(shape=(n_steps, n_inputs,), batch_size=BATCH_SIZE)
# weights
input_wq = Add_weight(name='add_1')(inputs)
input_wv = Add_weight(name='add_2')(inputs)
input_wk = Add_weight(name='add_3')(inputs)
h1 = layers.Attention()([
input_wq,
input_wv,
input_wk
]) # self-attention [query,value,key]
# h1 = inputs
h2 = PositionEmbedding(time_step, embedding_size,
name="PositionEmbedding")(h1) # 自定义层设置name
h3 = layers.Flatten()(h2) # 展开后使用全连接
h4 = layers.Dense(n_classes, input_shape=(time_step, embedding_size))(h3)
outputs = layers.Activation('softmax')(h4)
model = tf.keras.Model(inputs=inputs, outputs=outputs)
model.compile(optimizer=tf.keras.optimizers.Adam(),
loss=tf.keras.losses.BinaryCrossentropy(),
metrics=['accuracy'])
return model
if __name__ == '__main__':
if CONTINUE_TRAIN:
model = tf.keras.models.load_model('model/attention.keras', custom_objects={
'PositionEmbedding': PositionEmbedding,
'add_1':Add_weight,
'add_2':Add_weight,
'add_3':Add_weight,
})
else:
model = build_model()
model.summary()
...
model.fit(x_train, y_train, batch_size=BATCH_SIZE,
epochs=training_iters//x_train.shape[0], validation_split=0.1)
model.save('model/attention.keras')
  • 在save的时候报错,说是get_config函数未重写。仔细检查了代码,反复确定,Add_weight层与PositionEmbedding层都重写了该函数,但是由于其他地方调用的都是官方的层,让我一直怀疑是自己的代码不规范造成的。
  • 最后只能直接debug。刚开始的时候,由于我使用了VSCODE,debug设置默认不会跟进库函数,加上对于官方库的信任,我直接没有管官方keras库。发现我自己写的get_config函数被调用了,调动之后直接就报错了,让我误认为还是自己的问题。直到最后我想继续跟下去的时候,准备跟进官方库文件看看。
  • 修改VSCODE debug设置为:"justMyCode":false,再次跟进:
  • 发现base_layer.py代码抛出异常的位置,由于官方库并没有写具体的层信息,导致层错误的对象不明确,于是略加修改库代码。
  • base_layer.py的572行开始,修改抛出异常的库代码:
1
2
3
if len(extra_args) > 1 and hasattr(self.get_config, '_is_default'):
raise NotImplementedError('Layers with arguments in `__init__` must '
'override `get_config`.'+' name: '+config['name'])

再次运行模型训练得到输出:

1
NotImplementedError: Layers with arguments in `__init__` must override `get_config`. name:attention
  • 问题终于找到了,是官方的Attention层没有重写get_config函数,真坑。。。

解决

  • 在网上也找到了这个问题的issue:https://github.com/tensorflow/tensorflow/issues/32662,原来别人早就发现了,可是不知道为啥我自己遇到的时候怎么搜都没有相关的,我找到问题了它也出来了:)
  • 虽然github中说在新的版本中已解决该问题,但为了使代码更具通用性,我使用重载参数的方式储存,这样就不会因为get_config函数没有重写而报错。

后记

  • TF2与TF1相比,使用eager模式,去掉了静态图模式,在调试上更快捷。同时也引入了keras高层API,但是keras和底层API之间不能混用。并且由于官方过于强调keras,在其例子中主要都是keras搭建神经网络,底层的使用比较少,这使其不太友好。
  • 由于keras底层可以使用TF,所以TF和keras相辅相成,熟悉TF后就可以轻松地修改keras代码,并且keras提供了很多自定义的接口,这使keras框架用起来非常方便。
  • 最后推荐苏剑林大佬的博客中有关keras的部分:https://spaces.ac.cn/search/keras/