Keras:Tensorflow的黄金标准
By 苏剑林 | 2019-11-06 | 72802位读者 |这两周投入了比较多的精力去做bert4keras的开发,除了一些API的规范化工作外,其余的主要工作量是构建预训练部分的代码。在昨天,预训练代码基本构建完毕,并同时在TPU/多GPU环境下测试通过,从而有志(有算力)改进预训练模型的同学多了一个选择。——这可能是目前最为清晰易懂的bert及其预训练代码。
预训练代码链接: https://github.com/bojone/bert4keras/tree/master/pretraining
经过这两周的开发(填坑),笔者的最大感想就是:Keras已经成为了tensorflow的黄金标准了。只要你的代码按照Keras的标准规范写,那可以轻松迁移到tf.keras中去,继而可以非常轻松地在TPU或多GPU环境下训练,真正的几乎是一劳永逸。相反,如果你的写法过于灵活,包括像笔者之前介绍的很多“移花接木”式的Keras技巧,就可能会有不少问题,甚至可能出现的一种情况是:就算你已经在多GPU上跑通了,在TPU上你也死活调不通。
不遗余力的支持 #
大家都说tensorflow 2.0主推tf.keras,但事实上,从tensorflow 1.14开始,tf.keras就已经成为了它的黄金标准了,所以如果大家要体验Google爸爸是如何不遗余力支持keras的,只需要tensorflow 1.14+就行了,不一定要升级到2.0。目前bert4keras的代码同时支持原版keras以及tf.keras,在通常的单卡finetune任务里,大家可以用keras或者tf.keras都行,但如果要用多GPU训练甚至用TPU训练,那最好的选择还是tf.keras。
要入门tf.keras,首先建议参考一个很不错的网站:https://tf.wiki/
在tf.keras中,将一个模型从单卡变成多卡训练,代码非常简单:
strategy = tf.distribute.MirroredStrategy()
with strategy.scope():
model = create_a_model()
model.compile(loss='mse', optimizer='adam')
model.fit(train_x, train_y, epochs=10)
也就是说,只要定义一个strategy
,然后在这个strategy
的scope
下建立的模型,就是多卡的模型了。多卡训练,从未如此简单~
顺便提一下,Keras本身自带了multi_gpu_model
函数来实现多GPU训练,但笔者亲身测试multi_gpu_model
并不好用,有时候还无法生效。总之还是推荐用tf.keras。另外,上面是单机多卡的写法,多机多卡类似,但我没有相应的环境测试,所以就没有给出例子,要测试的朋友,请参考https://tf.wiki/里边的介绍。
那TPU呢?一样简单,把strategy
替换一下就行了(tensorflow 2.0有小改动):
resolver = tf.distribute.cluster_resolver.TPUClusterResolver(tpu=tpu_address)
tf.config.experimental_connect_to_host(resolver.master())
tf.tpu.experimental.initialize_tpu_system(resolver)
strategy = tf.distribute.experimental.TPUStrategy(resolver)
有没有感觉到简单得不可思议?这时候大家总该明白我前面说的“不遗余力”支持keras的含义了吧。从tensorflow 1.14开始,只要你用标准的keras写法,那么几乎可以无往而不利。
怎样才算是标准? #
前面不断强调用标准的keras写法,那怎样才算是标准呢?这里汇总一些经验。
1、尽可能全用keras自带的层、loss函数和优化器实现我们所需要的功能,如果一个模型全都是用keras内置的层、loss函数和优化器,那么基本上可以保证在多GPU或TPU上都能跑通了。
2、如果要自定义层,要严格按照规范来,尤其是要把get_config方法写好。测试自己写得规范与否的一个方法是:用你自定义的层去构建一个模型,然后看看能不能被clone_model函数成功克隆该模型,如果可以,说明你这个层的定义已经规范了。
3、如果要用TPU训练,不要在模型的最后用add_loss自定义loss,也不要用add_metric来添加metric。如果需要自定义复杂的loss或metric,请将它们定义为一个层的输出,参考这种写法。
4、如果要用TPU训练,切忌在训练过程中使用动态(变长)的写法,比如使用tf.where时参数x,y不能为None,否则tf.where的结果长度不确定,还有tf中几乎所有带有dynamic字眼的函数都不能用。
可以看到,所谓的“标准”,其实就是最大程度上模仿着keras已有的写法了,尽量少自己创造。只要做到1、2点,就可以在tf.keras下轻易用多GPU训练了;后面3、4两点是针对TPU填的坑,总的来说就是一切要是静态的。
尽管tensorflow 2.0开始已经默认动态图了,但笔者并不推荐使用,个人认为我们应当去习惯静态图的模型构建流程。动态图虽然方便调试,但会让我们对这种即时的输出结果产生严重依赖,降低我们面对复杂问题时的debug能力。类似地,我也不推荐使用代码补全、代码提示等工具,这些工具会让我们产生过多依赖,使得我们不真正去了解你要用的函数本身。(个人观点,不喜勿喷。)
人性化的胜利 #
如果没有记错,笔者是2015年初接触的Keras,当时也没有太多深度学习框架,只是想找个趁手的工具实现几个简单的模型,于是就找到了Keras,一直用到现在。不仅仅是笔者,也许Keras的作者们当时都没有想到,在今天Keras居然成为了tensorflow的黄金标准。
笔者觉得这并非偶然。tensorflow曾有过不少上层API框架,比如tensorlayer,比如tf.slim,比如TFLearn,但为啥最终选择了Keras?除了Keras的“历史悠久”之外,还在于Keras真正当之无愧为一个优雅的封装实现。这一年来,笔者时不时会去读Keras源码,每次读Keras源码都会被它的严密性、优雅性所震撼,它是一件当之无愧的艺术品,一个人性化的创造。
所以,这是人性化的胜利。
转载到请包括本文地址:https://www.spaces.ac.cn/archives/7055
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Nov. 06, 2019). 《Keras:Tensorflow的黄金标准 》[Blog post]. Retrieved from https://www.spaces.ac.cn/archives/7055
@online{kexuefm-7055,
title={Keras:Tensorflow的黄金标准},
author={苏剑林},
year={2019},
month={Nov},
url={\url{https://www.spaces.ac.cn/archives/7055}},
}
November 7th, 2019
苏神,问下,你的bert4keras支持徐亮的albert_tiny吗
REAMDE里边已经写得很清楚了。
November 11th, 2019
感谢苏神总结,非常受用
December 1st, 2019
苏神,看了这篇文章和bert4keras上对roberta的pretraining后,我在按照你的写法,改写task_seq2seq为单机多卡训练。但是一直报下面的错误:
Traceback (most recent call last):
File "/home/dell/anaconda3/envs/gpu/lib/python3.6/site-packages/tensorflow_core/python/framework/ops.py", line 1610, in _create_c_op
c_op = c_api.TF_FinishOperation(op_desc)
tensorflow.python.framework.errors_impl.InvalidArgumentError: slice index -1 of dimension 0 out of bounds. for 'loss/seq2seq_loss_loss/strided_slice' (op: 'StridedSlice') with input shapes: [0], [1], [1], [1] and with computed input tensors: input[1] = , input[2] = , input[3] = .
我模型构建部分的代码是:
def build_multi_gpu_model():
"""
rewrite loss function to a Lambda layer;
using tf.keras and tf.distributed
"""
Lambda = keras.layers.Lambda
_model = keras.models.Model
bert = build_bert_model(
config_path,
application='seq2seq',
keep_words=keep_words, # 只保留keep_words中的字,精简原字表
return_keras_model=False
)
bert_model = bert.model
proba = bert_model.output
def seq2seq_loss(inputs):
y_in, y_pred, y_mask = inputs
y_in = y_in[:, 1:]
y_mask = y_mask[:, 1:]
y_pred = y_pred[:, :-1]
cross_entropy = K.sparse_categorical_crossentropy(y_in, y_pred)
cross_entropy = K.sum(cross_entropy * y_mask) / K.sum(y_mask)
return cross_entropy
loss = Lambda(seq2seq_loss, name='seq2seq_loss')([bert_model.inputs[0], proba, bert_model.inputs[1]])
train_model = _model(bert_model.inputs, loss)
# config optimizer
opt = extend_with_weight_decay(Adam)
if which_optimizer == 'lamb':
opt = extend_with_layer_adaptation(opt)
opt = extend_with_piecewise_linear_lr(opt)
opt_params = {
'learning_rate': learning_rate,
'lr_schedule': lr_schedule,
'weight_decay_rate': weight_decay_rate,
'exclude_from_weight_decay': exclude_from_weight_decay,
'bias_correction': False,
}
if grad_accum_steps > 1:
opt = extend_with_gradient_accumulation(opt)
opt_params['grad_accum_steps'] = grad_accum_steps
optimizer = opt(**opt_params)
# compile final model for multi GPU/TPU training
train_model.compile(
loss={
'seq2seq_loss': lambda y_true, y_pred: y_pred,
},
optimizer=optimizer,
)
# must pass checkpoint_path here, otherwise error occurs
if checkpoint_path is not None:
bert.load_weights_from_checkpoint(checkpoint_path)
else:
pass
return train_model
训练的时候:
strategy = tf.distribute.MirroredStrategy()
with strategy.scope():
model = build_multi_gpu_model()
model.summary()
model.fit_generator(
data_generator(),
steps_per_epoch=steps_per_epoch,
epochs=epochs,
callbacks=[evaluator, checkpoint, csv_logger]
)
想请教一下苏神,是我模型构建部分出错了么?还是多GPU的时候不能用model.fit_generator?
最好为每个输入设置name参数,然后generator中以dict的形式返回数据(key就是设置的name)。
December 3rd, 2019
感谢苏神,我按照你说的,给generator中yield出来的inputs换成字典,key是模型输入层的name,但是还是报了一样的错误:
Traceback (most recent call last):
File "/home/dell/anaconda3/envs/gpu/lib/python3.6/site-packages/tensorflow_core/python/framework/ops.py", line 1610, in _create_c_op
c_op = c_api.TF_FinishOperation(op_desc)
tensorflow.python.framework.errors_impl.InvalidArgumentError: slice index -1 of dimension 0 out of bounds. for 'loss/seq2seq_loss_loss/strided_slice' (op: 'StridedSlice') with input shapes: [0], [1], [1], [1] and with computed input tensors: input[1] = , input[2] = , input[3] = .
模型构建部分:
Lambda = keras.layers.Lambda
Input = keras.layers.Input
_model = keras.models.Model
bert = build_bert_model(
config_path,
application='seq2seq',
keep_words=keep_words, # 只保留keep_words中的字,精简原字表
return_keras_model=False
)
bert_model = bert.model
proba = bert_model.output
token_ids = Input(shape=(None,), dtype='float32', name='token_ids') # 目标id
is_masked = Input(shape=(None,), dtype='float32', name='is_masked') # mask标记
def seq2seq_loss(inputs):
y_in, y_pred, y_mask = inputs
y_in = y_in[:, 1:]
y_mask = y_mask[:, 1:]
y_pred = y_pred[:, :-1]
cross_entropy = K.sparse_categorical_crossentropy(y_in, y_pred)
cross_entropy = K.sum(cross_entropy * y_mask) / K.sum(y_mask)
return cross_entropy
loss = Lambda(seq2seq_loss, name='seq2seq_loss')([token_ids, proba, is_masked])
train_model = _model(bert_model.inputs + [token_ids, is_masked], loss)
# config optimizer
opt = extend_with_weight_decay(Adam)
if which_optimizer == 'lamb':
opt = extend_with_layer_adaptation(opt)
opt = extend_with_piecewise_linear_lr(opt)
opt_params = {
'learning_rate': learning_rate,
'lr_schedule': lr_schedule,
'weight_decay_rate': weight_decay_rate,
'exclude_from_weight_decay': exclude_from_weight_decay,
'bias_correction': False,
}
if grad_accum_steps > 1:
opt = extend_with_gradient_accumulation(opt)
opt_params['grad_accum_steps'] = grad_accum_steps
optimizer = opt(**opt_params)
# compile final model for multi GPU/TPU training
train_model.compile(
loss={
'seq2seq_loss': lambda y_true, y_pred: y_pred,
},
optimizer=optimizer,
)
是不是我的loss层有问题呢?谢谢苏神!
注:评论里怎么插入代码块呀,我用latex的\begin{lstlisting}[language=Python]\end{lstlisting}模块好像在预览里不起作用。。。
评论插不了代码块,能看个大概意思就行了。
你这究竟是pretraining还是seq2seq?你之前说的是seq2seq,如果是seq2seq你插is_masked这些进来干嘛?
我是做seq2seq的,只是想把你的task_seq2seq代码参考pretraining里面多GPU用MirroredStrategy的写法,来训练GPU模型。之前在单GPU的时候的keras模型,你的代码里loss是这么定义的:
y_in = model.input[0][:, 1:] # 目标tokens
y_mask = model.input[1][:, 1:]
y = model.output[:, :-1] # 预测tokens,预测与目标错开一位
cross_entropy = K.sparse_categorical_crossentropy(y_in, y)
cross_entropy = K.sum(cross_entropy * y_mask) / K.sum(y_mask)
model.add_loss(cross_entropy)
现在为了把loss改写成一个层,增加两个输入层,一个是目标的token即y_in,一个是输入是否mask即y_mask,是不是这么做不行?
我有空弄一个多gpu的demo好了~
请问一下,朋友解决了该问题嘛,我在使用yolov3(从keras转到tf.keras 2.0)时发生了同样的错误。如果有解决方案或者思路,希望可以找我一起讨论讨论,谢谢!
邮箱:396174609@qq.com
什么错误?
slice index -1 of dimension 0 out of bounds. for 'loss/seq2seq_loss_loss/strided_slice' (op: 'StridedSlice') with input shapes: [0], [1], [1], [1] and with computed input tensors: input[1] = , input[2] = , input[3] = .
跟上面层主一样,我知道是切片相关的张量形状有问题,但是不清楚怎么定位。
我源代码在tensorflow1.13.1 keras2.2.4上运行没有问题,但是换成tf2.0 tf.keras以后出问题了。
不好意思,bert4keras对tf 2.0的支持还不够完善。这主要原始tf 2.0无端端切换到难用的动态图了~
December 3rd, 2019
接上面的。。。我的data_generator长这样:
\begin{lstlisting}[language=Python]
def data_generator():
while True:
x_list, s_list = [], []
for a, b in read_texts():
x, s = tokenizer.encode(a, second_text=b, max_length=512)
x_list.append(x)
s_list.append(s)
if len(x) == batch_size:
x_list = sequence_padding(x_list)
s_list = sequence_padding(s_list)
inputs = {
'Input-Token': x_list,
'Input-Segment': s_list,
'token_ids': x_list,
'is_masked': s_list,
}
yield inputs, None
x_list, s_list = [], []
\end{lstlisting}@schwarz|comment-12510
March 12th, 2020
在跑bert4keras demo时候 报这个错怎么解决呀tensorflow.python.framework.errors_impl.NotFoundError: Key bert/pooler/dense/kernel not found in checkpoint
用了不适当的权重。
July 2nd, 2020
在跑tf2.0 keras模型,fit的时候,跑到training_eager.py中,line 127 :outs = model(inputs, **kwargs),这个就是实际计算loss的时候的y_pred,但这个y_pred没有_keras_history,最开始compile的时候y_pred是有的,之前tf1.0也是有的,不知道是我使用的问题还是tf2.0更新的问题,他的base_layer.py也加了好多有关keras_history的东西,苏神有注意这个地方吗,困扰我好久了
你困扰的地方是什么?有没有又如何?