3.5.1 使用HDF5格式保存模型

通常把Keras模型保存为HDF5格式,该文件包含模型结构、模型权重值及优化器状态。使用HDF5格式时,即使你不再有权访问创建模型的代码,也可以从该文件重新创建相同的模型。

继续以印第安人发生糖尿病的二元分类问题为例,创建一个小型深度学习模型,并将整个模型都保存到HDF5文件中。

> # 在当前目录中创建models文件夹
> dir.create('models',showWarnings = TRUE)
> # # 导入数据集
> dataset <- read.csv('../data/pima-indians-diabetes.csv',skip = 9,header = F)
> X = as.matrix(dataset[,1:8])
> y = dataset[,9]
> # 定义及编译模型
> library(keras)
> # 定义及编译模型
> library(keras)
> model <- keras_model_sequential()
> model %>%
+   layer_dense(units = 12,input_shape = c(8),kernel_initializer = 'uniform', activation = 'relu') %>%
+   layer_dense(units = 8,kernel_initializer = 'uniform',activation = 'relu') %>%
+   layer_dense(units = 1,kernel_initializer = 'uniform',activation = 'sigmoid') %>%
+   compile(loss='binary_crossentropy',optimizer = 'adam',metrics = c('accuracy'))
> # 训练模型
> model %>% fit(
+   X, y,
+   batch_size = 10,
+   epochs = 100,
+   verbose = 0,
+   validation_split = 0.33
+ )
> # 保存整个模型
> save_model_hdf5(model,'models/model.h5')
> # 查看保存的文件
> list.files('models')
[1] "model.h5"

运行完以上程序代码后,在models文件夹中生成了保存整个模型的model.h5文件,可以通过load_model_hdf5将其导入。

> # 加载HDF5文件
> new_model <- load_model_hdf5('models/model.h5')
> # 利用加载的模型对X进行预测
> new_prediction <- predict(new_model,X)
> # 利用之前训练好的模型对X进行预测
> prediction <- predict(model,X)
> # 判断两次预测结果是否完全相同
> all.equal(new_prediction,prediction)
[1] TRUE

我们还可以将整体模型导出为TensorFlow的SavedModel格式,SaveModel是TensorFlow对象的独立序列化格式。注意,model_to_save_model仅适用于TensorFlow1.14及以上版本。

> # 保存为SaveModel格式
> model_to_saved_model(model,'models/savemodel/')
> list.files('models/savemodel/')
[1] "assets"         "saved_model.pb" "variables"
> # 重新创建完全一致的模型
> new_model1 <- model_from_saved_model('models/savemodel/')
> # 与之前的预测结果进行对比
> new_prediction1 <- predict(new_model1,X)
> all.equal(new_prediction1,prediction)
[1] TRUE

SaveModel在新文件夹savemodel中创建的文件包含如下内容。

  • 一个包含模型权重的TensorFlow检查点。
  • 一个包含基础TensorFlow图的SaveModel原型。保存单独的图形以进行预测、训练和评估。如果模型不是以前编译过的,则仅导出推理图。
  • 模型的结构配置(如果存在的话)。

有时候,如果你只对结构感兴趣,则无须保存权重值或优化器。在这种情况下,可以通过get_config()方法得到模型的结构,该方法返回一个命名列表,使得可以从头开始初始化重新创建相同的模型,而无须获取先前模型在训练期间学到的任何信息。

> config <- get_config(model)
> config
{'name': 'sequential', 'layers': [{'class_name': 'Dense', 'config': {'name': 'dense', 'trainable': True, 'batch_input_shape': (None, 8), 'dtype': 'float32', 'units': 12, 'activation': 'relu', 'use_bias': True, 'kernel_initializer': {'class_name': 'RandomUniform', 'config': {'minval': -0.05, 'maxval': 0.05, 'seed': None}}, 'bias_initializer': {'class_name': 'Zeros', 'config': {}}, 'kernel_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}}, {'class_name': 'Dense', 'config': {'name': 'dense_1', 'trainable': True, 'dtype': 'float32', 'units': 8, 'activation': 'relu', 'use_bias': True, 'kernel_initializer': {'class_name': 'RandomUniform', 'config': {'minval': -0.05, 'maxval': 0.05, 'seed': None}}, 'bias_initializer': {'class_name': 'Zeros', 'config': {}}, 'kernel_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}}, {'class_name': 'Dense', 'config': {'name': 'dense_2', 'trainable': True, 'dtype': 'float32', 'units': 1, 'activation': 'sigmoid', 'use_bias': True, 'kernel_initializer': {'class_name': 'RandomUniform', 'config': {'minval': -0.05, 'maxval': 0.05, 'seed': None}}, 'bias_initializer': {'class_name': 'Zeros', 'config': {}}, 'kernel_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}}]}

通过from_config()函数得到一个重新初始化的模型,该模型仅仅继承了之前模型的网络结构,并不包含模型训练得到的权重值(此时的权重值为初始化值)和优化器,如图3-25所示。

> reinitialized_model <- from_config(config)
> summary(reinitialized_model)
095-1

图3-25 通过from_config()函数得到重新初始化的模型

现在,利用重新初始化的模型对训练数据集进行预测,并判断是否与最初模型model的预测结果一致。

> new_prediction2 <- predict(reinitialized_model,X)
> all.equal(new_prediction2,prediction)
[1] "Mean relative difference: 0.3864167"

从返回结果可知,两个结果是有差异的,原因在于我们重新初始化的模型只保留了模型结构,并不会保留模型状态(权重值和优化器)。

我们可以利用get_weights()函数得到模型权重值,返回一个数组列表。比如,在定义网络时,bias(偏置)项的初始化权重值默认为0。下面利用get_weights()函数查看model和reinitialized_model两者第一个隐藏层偏置项的权重值。

> # 查看第一个隐藏层的偏置项的权重值
> get_weights(model)[[2]]
 [1] -0.032459475  0.000000000  0.853002846  0.884543836  1.109097242
 [6]  0.820603907 -0.349359781  0.788280964 -1.016711950 -0.009251177
[11] -0.999970675 -0.015671620
> get_weights(reinitialized_model)[[2]]
 [1] 0 0 0 0 0 0 0 0 0 0 0 0

可见,reinitialized_model仅仅继承了model的网络结构,并未保留model中的网络权重值,因为reinitialized_model每层中各神经元的偏置项的权重值均为0。可以利用set_weights()函数将reinitialized_model的权重值设置为与model的权重值一致。

> # 设置权重值
> weight <- get_weights(model)
> set_weights(reinitialized_model,weight)
> get_weights(reinitialized_model)[[2]]
  [1] -0.032459475  0.000000000  0.853002846  0.884543836  1.109097242
  [6]  0.820603907 -0.349359781  0.788280964 -1.016711950 -0.009251177
 [11] -0.999970675 -0.015671620
> new_prediction3 <- predict(reinitialized_model,X)
> all.equal(new_prediction3,prediction)
  [1] TRUE

我们可以组合使用get_config()/from_config()和get_weights()/set_weights(),以相同状态重新创建模型。但是,与save_model_hdf5()不同,这将不包括训练配置和优化器。所以,如果想使用该模型进行训练,我们必须先使用compile()函数重新编译网络。

> # 直接编译会出错
> reinitialized_model %>% fit(
+   X, y,
+   batch_size = 10,
+   epochs = 100,
+   verbose = 0,
+   validation_split = 0.33)
Error in py_call_impl(callable, dots$args, dots$keywords) :
    RuntimeError: You must compile your model before training/testing. Use `model.compile(optimizer, loss)`.

错误提示说明在利用reinitialized_model模型训练前需要利用compile()函数进行网络编译。

我们也可以通过save_model_weights_hdf5()函数将模型权重保存到磁盘中,并通过load_model_weights_hdf5()函数将其导入R。

> # 保存模型权重值到磁盘中
> save_model_hdf5(model,'models/model_weights.h5')
> # 加载到R中
> new_model3 <- from_config(get_config(model))
> load_model_weights_hdf5(new_model3,'models/model_weights.h5')

新创建的new_models模型与reinitialized_model相同,注意,此模型的优化器也未保留。所以,最简单的保存模型的方法如下:

save_model_hdf5(model, "model.h5")
new_model <- load_model_hdf5("model.h5")