前一段时间有个朋友问我这样一个问题:google官网给的bert-base模型的ckpt文件大小只有400M,为什么我进行微调-训练之后,保存的ckpt模型就是1.19G呢?

我当时的回答是:因为google给的bert-base模型的ckpt文件仅包含bert的transform每一层的参数,不包含其他参数。而你自己在微调训练过程中有增加了其他的一些参数,所以会比较大。

现在想一想,感觉自己的回答也有些模棱两可,也许也有其他同学会有这也的疑问吧。那么我今天就详细讲解一些,为什么保存时会多出那么多参数?

我们首先通过tf.train.NewCheckpointReader来直接读取google官网给的bert-base模型的ckpt文件,这种方法的好处是,我们不需要重新加载model模型,就可以看到保存的所有节点。

from tensorflow.python import pywrap_tensorflow

ckpt_model_path = "chinese_L-12_H-768_A-12/bert_model.ckpt"
model_ckpt = pywrap_tensorflow.NewCheckpointReader(ckpt_model_path)
var_dict = model_ckpt.get_variable_to_shape_map()
for key in var_dict:
    print("bert_parameter:", key)

得到的结果如下(由于参数过多,因此只列出部分参数):

bert_parameter: bert/embeddings/LayerNorm/beta
bert_parameter: bert/embeddings/LayerNorm/gamma
bert_parameter: bert/encoder/layer_9/attention/output/LayerNorm/beta
bert_parameter: bert/encoder/layer_9/attention/output/dense/bias
bert_parameter: bert/encoder/layer_9/attention/output/dense/kernel
bert_parameter: bert/encoder/layer_9/attention/self/key/kernel
bert_parameter: bert/encoder/layer_9/attention/self/query/bias
bert_parameter: bert/encoder/layer_9/attention/self/query/kernel
bert_parameter: bert/encoder/layer_9/intermediate/dense/bias
bert_parameter: bert/encoder/layer_9/intermediate/dense/kernel
bert_parameter: bert/encoder/layer_9/output/LayerNorm/beta
bert_parameter: bert/encoder/layer_9/output/dense/bias
bert_parameter: bert/encoder/layer_9/output/dense/kernel
bert_parameter: bert/pooler/dense/bias
bert_parameter: bert/pooler/dense/kernel
bert_parameter: cls/predictions/transform/LayerNorm/beta
bert_parameter: cls/predictions/transform/LayerNorm/gamma
bert_parameter: cls/predictions/transform/dense/bias
bert_parameter: cls/predictions/transform/dense/kernel
bert_parameter: cls/seq_relationship/output_bias
bert_parameter: cls/seq_relationship/output_weights

我们可以发现,其实google给的bert-base模型的ckpt文件,不仅仅是保存了bert-transform每一层的参数,而且也存储了embedding和预训练需要的预测参数(例如:在进行NPS预测时所需的全连接层参数)。因此,我之前给那位朋友的回答是存在偏差的。

接下来,我们通过tf.train.NewCheckpointReader来读取我们fine-tune之后的模型,看一下都保存了什么参数。

from tensorflow.python import pywrap_tensorflow

ckpt_model_path = "my_model\\bert_model.ckpt"
model_ckpt = pywrap_tensorflow.NewCheckpointReader(ckpt_model_path)
var_dict = model_ckpt.get_variable_to_shape_map()
for key in var_dict:
    print("bert_parameter:", key)

得到的结果如下(依然只列出部分参数):

bert_parameter: bert/embeddings/LayerNorm/beta
bert_parameter: bert/embeddings/LayerNorm/beta/adam_v
bert_parameter: bert/encoder/layer_9/attention/output/LayerNorm/beta/adam_m
bert_parameter: bert/encoder/layer_9/attention/output/LayerNorm/beta/adam_v
bert_parameter: bert/encoder/layer_9/attention/output/dense/bias/adam_m
bert_parameter: bert/encoder/layer_9/attention/output/dense/bias/adam_v
bert_parameter: bert/encoder/layer_9/attention/output/dense/kernel
bert_parameter: bert/encoder/layer_9/attention/output/dense/kernel/adam_m
bert_parameter: bert/encoder/layer_9/attention/output/dense/kernel/adam_v
bert_parameter: bert/encoder/layer_9/attention/self/key/kernel
bert_parameter: bert/encoder/layer_9/attention/self/key/kernel/adam_m
bert_parameter: bert/encoder/layer_9/attention/self/key/kernel/adam_v
bert_parameter: bert/encoder/layer_9/attention/self/query/kernel/adam_m
bert_parameter: bert/encoder/layer_9/attention/self/query/kernel/adam_v
bert_parameter: bert/encoder/layer_9/attention/self/value/bias
bert_parameter: bert/encoder/layer_9/attention/self/value/bias/adam_m
bert_parameter: bert/encoder/layer_9/attention/self/value/bias/adam_v
bert_parameter: bert/encoder/layer_9/attention/self/value/kernel
bert_parameter: bert/encoder/layer_9/attention/self/value/kernel/adam_m
bert_parameter: bert/encoder/layer_9/attention/self/value/kernel/adam_v
bert_parameter: bert/encoder/layer_9/intermediate/dense/bias
bert_parameter: bert/encoder/layer_9/intermediate/dense/bias/adam_m
bert_parameter: bert/encoder/layer_9/intermediate/dense/bias/adam_v
bert_parameter: bert/encoder/layer_9/output/LayerNorm/beta/adam_v
bert_parameter: bert/encoder/layer_9/output/dense/bias
bert_parameter: bert/encoder/layer_9/output/dense/kernel/adam_m
bert_parameter: bert/encoder/layer_9/output/dense/kernel/adam_v
bert_parameter: bert/pooler/dense/bias
bert_parameter: bert/pooler/dense/bias/adam_m
bert_parameter: bert/pooler/dense/bias/adam_v
bert_parameter: bert/pooler/dense/kernel
bert_parameter: bert/pooler/dense/kernel/adam_m
bert_parameter: bert/pooler/dense/kernel/adam_v

看到这个结果时,我相信大家应该都会恍然大悟,其实我们在做微调的时候,并没有新增多少参数变量。导致我们保存的ckpt文件到达1.19G的原因,其实是多保存了每个变量的adam_m和adam_v。这样算来,一个变量变成了3个变量,正好是从400M到1.19G。

接下来,应该会有同学问:adam_m和adam_v是什么呢,为什么会保存这些参数?

答:在模型进行训练优化(误差传递)时,我们通常使用Adam优化器进行优化。在优化的过程中,我们通常需要维护(保存下之前时刻的滑动平均值)每个参数的一阶矩(对应adam_m)和二阶矩(对应adam_v)来保证梯度的顺利更新。

简单地来讲,就是在模型训练的过程中,每个参数都需要额外变量参数存储一些信息,用于误差的传递以及梯度的更新。而这些额外的变量参数,在训练停止之后,其实也就失去了它的作;并且在模型预测阶段或者调用该模型为其他模型进行参数初始化阶段,都是不需要使用这些参数变量的。但是一般我们在存储模型时,都会默认将所有参数变量都进行保存,所有才会导致我们保存的bert模型有1.19G。

为了使我们保存的模型变小,减缓我们硬盘的压力,我们可以在保存模型时,进行如下操作:

tf.train.Saver(tf.trainable_variables()).save(sess, save_model_path)

这样保存的模型,就是只包含训练参数,而额外的存储参数是不会进行保存的。仅修改一行代码,就可以减轻硬盘2/3的压力,何乐而不为呢!

下面是我们平时使用保存模型的代码,这样保存是将所有参数都保存下来。

tf.train.Saver().save(sess, save_model_path)
等于
tf.train.Saver(tf.all_variables()).save(sess, save_model_path)

实践是检验真理的唯一标准。有时,你认为的仅仅是你认为的;你做出来的,才是真的。

09-14 17:09