Se usó la API de Cloud Translation para traducir esta página.
Switch to English

Guardar y cargar modelos

Ver en TensorFlow.org Ejecutar en Google Colab Ver fuente en GitHub Descargar cuaderno

El progreso del modelo se puede guardar durante y después del entrenamiento. Esto significa que un modelo puede continuar donde lo dejó y evitar largos tiempos de entrenamiento. Guardar también significa que puede compartir su modelo y otros pueden recrear su trabajo. Al publicar modelos y técnicas de investigación, la mayoría de los profesionales del aprendizaje automático comparten:

  • código para crear el modelo, y
  • los pesos entrenados, o parámetros, para el modelo

Compartir estos datos ayuda a otros a comprender cómo funciona el modelo y a probarlo ellos mismos con nuevos datos.

Opciones

Hay diferentes formas de guardar modelos de TensorFlow, según la API que estés usando. Esta guía usa tf.keras , una API de alto nivel para crear y entrenar modelos en TensorFlow. Para conocer otros enfoques, consulte la guía Guardar y restaurar de TensorFlow o Guardar con entusiasmo .

Preparar

Instalaciones e importaciones

Instala e importa TensorFlow y las dependencias:

pip install -q pyyaml h5py  # Required to save models in HDF5 format
WARNING: You are using pip version 20.2.2; however, version 20.2.3 is available.
You should consider upgrading via the '/tmpfs/src/tf_docs_env/bin/python -m pip install --upgrade pip' command.

import os

import tensorflow as tf
from tensorflow import keras

print(tf.version.VERSION)
2.3.0

Obtener un conjunto de datos de ejemplo

Para demostrar cómo guardar y cargar pesos, usará el conjunto de datos MNIST . Para acelerar estas ejecuciones, utilice los primeros 1000 ejemplos:

(train_images, train_labels), (test_images, test_labels) = tf.keras.datasets.mnist.load_data()

train_labels = train_labels[:1000]
test_labels = test_labels[:1000]

train_images = train_images[:1000].reshape(-1, 28 * 28) / 255.0
test_images = test_images[:1000].reshape(-1, 28 * 28) / 255.0

Definir un modelo

Empiece por construir un modelo secuencial simple:

# Define a simple sequential model
def create_model():
  model = tf.keras.models.Sequential([
    keras.layers.Dense(512, activation='relu', input_shape=(784,)),
    keras.layers.Dropout(0.2),
    keras.layers.Dense(10)
  ])

  model.compile(optimizer='adam',
                loss=tf.losses.SparseCategoricalCrossentropy(from_logits=True),
                metrics=[tf.metrics.SparseCategoricalAccuracy()])

  return model

# Create a basic model instance
model = create_model()

# Display the model's architecture
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 512)               401920    
_________________________________________________________________
dropout (Dropout)            (None, 512)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 10)                5130      
=================================================================
Total params: 407,050
Trainable params: 407,050
Non-trainable params: 0
_________________________________________________________________

Guarde los puntos de control durante el entrenamiento

Puede utilizar un modelo entrenado sin tener que volver a entrenarlo o retomar el entrenamiento donde lo dejó, en caso de que el proceso de entrenamiento se interrumpa. La tf.keras.callbacks.ModelCheckpoint llamada tf.keras.callbacks.ModelCheckpoint permite guardar continuamente el modelo tanto durante como al final del entrenamiento.

Uso de devolución de llamada de punto de control

Cree una tf.keras.callbacks.ModelCheckpoint llamada tf.keras.callbacks.ModelCheckpoint que ahorre pesos solo durante el entrenamiento:

checkpoint_path = "training_1/cp.ckpt"
checkpoint_dir = os.path.dirname(checkpoint_path)

# Create a callback that saves the model's weights
cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_path,
                                                 save_weights_only=True,
                                                 verbose=1)

# Train the model with the new callback
model.fit(train_images, 
          train_labels,  
          epochs=10,
          validation_data=(test_images,test_labels),
          callbacks=[cp_callback])  # Pass callback to training

# This may generate warnings related to saving the state of the optimizer.
# These warnings (and similar warnings throughout this notebook)
# are in place to discourage outdated usage, and can be ignored.
Epoch 1/10
27/32 [========================>.....] - ETA: 0s - loss: 1.2283 - sparse_categorical_accuracy: 0.6574
Epoch 00001: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 8ms/step - loss: 1.1420 - sparse_categorical_accuracy: 0.6810 - val_loss: 0.7102 - val_sparse_categorical_accuracy: 0.7740
Epoch 2/10
28/32 [=========================>....] - ETA: 0s - loss: 0.4317 - sparse_categorical_accuracy: 0.8795
Epoch 00002: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 5ms/step - loss: 0.4257 - sparse_categorical_accuracy: 0.8780 - val_loss: 0.5635 - val_sparse_categorical_accuracy: 0.8300
Epoch 3/10
28/32 [=========================>....] - ETA: 0s - loss: 0.2886 - sparse_categorical_accuracy: 0.9241
Epoch 00003: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 5ms/step - loss: 0.2908 - sparse_categorical_accuracy: 0.9210 - val_loss: 0.4967 - val_sparse_categorical_accuracy: 0.8370
Epoch 4/10
28/32 [=========================>....] - ETA: 0s - loss: 0.2128 - sparse_categorical_accuracy: 0.9453
Epoch 00004: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 4ms/step - loss: 0.2104 - sparse_categorical_accuracy: 0.9460 - val_loss: 0.4355 - val_sparse_categorical_accuracy: 0.8580
Epoch 5/10
28/32 [=========================>....] - ETA: 0s - loss: 0.1396 - sparse_categorical_accuracy: 0.9654
Epoch 00005: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 4ms/step - loss: 0.1474 - sparse_categorical_accuracy: 0.9650 - val_loss: 0.4435 - val_sparse_categorical_accuracy: 0.8530
Epoch 6/10
29/32 [==========================>...] - ETA: 0s - loss: 0.1060 - sparse_categorical_accuracy: 0.9860
Epoch 00006: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 4ms/step - loss: 0.1096 - sparse_categorical_accuracy: 0.9840 - val_loss: 0.4177 - val_sparse_categorical_accuracy: 0.8650
Epoch 7/10
28/32 [=========================>....] - ETA: 0s - loss: 0.0966 - sparse_categorical_accuracy: 0.9844
Epoch 00007: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 4ms/step - loss: 0.0932 - sparse_categorical_accuracy: 0.9850 - val_loss: 0.4236 - val_sparse_categorical_accuracy: 0.8610
Epoch 8/10
27/32 [========================>.....] - ETA: 0s - loss: 0.0644 - sparse_categorical_accuracy: 0.9931
Epoch 00008: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 4ms/step - loss: 0.0658 - sparse_categorical_accuracy: 0.9920 - val_loss: 0.4141 - val_sparse_categorical_accuracy: 0.8680
Epoch 9/10
27/32 [========================>.....] - ETA: 0s - loss: 0.0478 - sparse_categorical_accuracy: 1.0000
Epoch 00009: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 5ms/step - loss: 0.0499 - sparse_categorical_accuracy: 0.9980 - val_loss: 0.4156 - val_sparse_categorical_accuracy: 0.8680
Epoch 10/10
27/32 [========================>.....] - ETA: 0s - loss: 0.0369 - sparse_categorical_accuracy: 0.9988
Epoch 00010: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 5ms/step - loss: 0.0368 - sparse_categorical_accuracy: 0.9990 - val_loss: 0.4239 - val_sparse_categorical_accuracy: 0.8680

<tensorflow.python.keras.callbacks.History at 0x7fef1c745c18>

Esto crea una colección única de archivos de puntos de control de TensorFlow que se actualizan al final de cada época:

ls {checkpoint_dir}
checkpoint  cp.ckpt.data-00000-of-00001  cp.ckpt.index

Crea un modelo nuevo y sin formación. Al restaurar un modelo solo a partir de pesos, debe tener un modelo con la misma arquitectura que el modelo original. Dado que es la misma arquitectura de modelo, puede compartir pesos a pesar de que es una instancia diferente del modelo.

Ahora reconstruya un modelo nuevo y no entrenado y evalúelo en el conjunto de prueba. Un modelo sin entrenamiento funcionará a niveles de probabilidad (~ 10% de precisión):

# Create a basic model instance
model = create_model()

# Evaluate the model
loss, acc = model.evaluate(test_images,  test_labels, verbose=2)
print("Untrained model, accuracy: {:5.2f}%".format(100*acc))
32/32 - 0s - loss: 2.3319 - sparse_categorical_accuracy: 0.1410
Untrained model, accuracy: 14.10%

Luego cargue los pesos desde el punto de control y reevalúe:

# Loads the weights
model.load_weights(checkpoint_path)

# Re-evaluate the model
loss,acc = model.evaluate(test_images,  test_labels, verbose=2)
print("Restored model, accuracy: {:5.2f}%".format(100*acc))
32/32 - 0s - loss: 0.4239 - sparse_categorical_accuracy: 0.8680
Restored model, accuracy: 86.80%

Opciones de devolución de llamada de punto de control

La devolución de llamada proporciona varias opciones para proporcionar nombres únicos para los puntos de control y ajustar la frecuencia de los puntos de control.

Entrene un nuevo modelo y guarde puntos de control con nombres únicos una vez cada cinco épocas:

# Include the epoch in the file name (uses `str.format`)
checkpoint_path = "training_2/cp-{epoch:04d}.ckpt"
checkpoint_dir = os.path.dirname(checkpoint_path)

# Create a callback that saves the model's weights every 5 epochs
cp_callback = tf.keras.callbacks.ModelCheckpoint(
    filepath=checkpoint_path, 
    verbose=1, 
    save_weights_only=True,
    period=5)

# Create a new model instance
model = create_model()

# Save the weights using the `checkpoint_path` format
model.save_weights(checkpoint_path.format(epoch=0))

# Train the model with the new callback
model.fit(train_images, 
          train_labels,
          epochs=50, 
          callbacks=[cp_callback],
          validation_data=(test_images,test_labels),
          verbose=0)
WARNING:tensorflow:`period` argument is deprecated. Please use `save_freq` to specify the frequency in number of batches seen.
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.iter
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.beta_1
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.beta_2
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.decay
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.learning_rate
WARNING:tensorflow:A checkpoint was restored (e.g. tf.train.Checkpoint.restore or tf.keras.Model.load_weights) but not all checkpointed values were used. See above for specific issues. Use expect_partial() on the load status object, e.g. tf.train.Checkpoint.restore(...).expect_partial(), to silence these warnings, or use assert_consumed() to make the check explicit. See https://www.tensorflow.org/guide/checkpoint#loading_mechanics for details.

Epoch 00005: saving model to training_2/cp-0005.ckpt

Epoch 00010: saving model to training_2/cp-0010.ckpt

Epoch 00015: saving model to training_2/cp-0015.ckpt

Epoch 00020: saving model to training_2/cp-0020.ckpt

Epoch 00025: saving model to training_2/cp-0025.ckpt

Epoch 00030: saving model to training_2/cp-0030.ckpt

Epoch 00035: saving model to training_2/cp-0035.ckpt

Epoch 00040: saving model to training_2/cp-0040.ckpt

Epoch 00045: saving model to training_2/cp-0045.ckpt

Epoch 00050: saving model to training_2/cp-0050.ckpt

<tensorflow.python.keras.callbacks.History at 0x7fef6ff62898>

Ahora, mire los puntos de control resultantes y elija el último:

ls {checkpoint_dir}
checkpoint            cp-0025.ckpt.index
cp-0000.ckpt.data-00000-of-00001  cp-0030.ckpt.data-00000-of-00001
cp-0000.ckpt.index        cp-0030.ckpt.index
cp-0005.ckpt.data-00000-of-00001  cp-0035.ckpt.data-00000-of-00001
cp-0005.ckpt.index        cp-0035.ckpt.index
cp-0010.ckpt.data-00000-of-00001  cp-0040.ckpt.data-00000-of-00001
cp-0010.ckpt.index        cp-0040.ckpt.index
cp-0015.ckpt.data-00000-of-00001  cp-0045.ckpt.data-00000-of-00001
cp-0015.ckpt.index        cp-0045.ckpt.index
cp-0020.ckpt.data-00000-of-00001  cp-0050.ckpt.data-00000-of-00001
cp-0020.ckpt.index        cp-0050.ckpt.index
cp-0025.ckpt.data-00000-of-00001

latest = tf.train.latest_checkpoint(checkpoint_dir)
latest
'training_2/cp-0050.ckpt'

Para probar, restablezca el modelo y cargue el último punto de control:

# Create a new model instance
model = create_model()

# Load the previously saved weights
model.load_weights(latest)

# Re-evaluate the model
loss, acc = model.evaluate(test_images,  test_labels, verbose=2)
print("Restored model, accuracy: {:5.2f}%".format(100*acc))
32/32 - 0s - loss: 0.5075 - sparse_categorical_accuracy: 0.8730
Restored model, accuracy: 87.30%

¿Qué son estos archivos?

El código anterior almacena los pesos en una colección de archivos con formato de punto de control que contienen solo los pesos entrenados en formato binario. Los puntos de control contienen:

  • Uno o más fragmentos que contienen los pesos de su modelo.
  • Un archivo de índice que indica qué pesos se almacenan en un fragmento.

Si solo está entrenando un modelo en una sola máquina, tendrá un fragmento con el sufijo: .data-00000-of-00001

Guardar pesos manualmente

Viste cómo cargar los pesos en un modelo. Model.save_weights manualmente es igual de sencillo con el método Model.save_weights . De forma predeterminada, tf.keras , y save_weights en particular, usa el formato de punto de control de TensorFlow con una extensión .ckpt (guardar en HDF5 con una extensión .h5 se trata en la guía Guardar y serializar modelos ):

# Save the weights
model.save_weights('./checkpoints/my_checkpoint')

# Create a new model instance
model = create_model()

# Restore the weights
model.load_weights('./checkpoints/my_checkpoint')

# Evaluate the model
loss,acc = model.evaluate(test_images,  test_labels, verbose=2)
print("Restored model, accuracy: {:5.2f}%".format(100*acc))
32/32 - 0s - loss: 0.5075 - sparse_categorical_accuracy: 0.8730
Restored model, accuracy: 87.30%

Guardar el modelo completo

Llame a model.save para guardar la arquitectura, los pesos y la configuración de entrenamiento de un modelo en un solo archivo / carpeta. Esto le permite exportar un modelo para que pueda usarse sin acceso al código Python original *. Dado que se recupera el estado del optimizador, puede reanudar el entrenamiento desde exactamente donde lo dejó.

El modelo completo se puede guardar en dos formatos de archivo diferentes ( SavedModel y HDF5 ). Cabe señalar que el formato de SavedModel TensorFlow es el formato de archivo predeterminado en TF2.x. Sin embargo, el modelo se puede guardar en formato HDF5 . A continuación se describen más detalles sobre cómo guardar el modelo completo en los dos formatos de archivo.

Guardar un modelo completamente funcional es muy útil: puede cargarlos en TensorFlow.js ( modelo guardado , HDF5 ) y luego entrenarlos y ejecutarlos en navegadores web, o convertirlos para que se ejecuten en dispositivos móviles con TensorFlow Lite ( modelo guardado , HDF5) )

* Los objetos personalizados (por ejemplo, modelos o capas en subclases) requieren una atención especial al guardar y cargar. Consulte la sección Guardar objetos personalizados a continuación.

Formato de modelo guardado

El formato SavedModel es otra forma de serializar modelos. Los modelos guardados en este formato se pueden restaurar con tf.keras.models.load_model y son compatibles con TensorFlow Serving. La guía de modelo guardado detalla cómo entregar / inspeccionar el modelo guardado. La siguiente sección ilustra los pasos para guardar y restaurar el modelo.

# Create and train a new model instance.
model = create_model()
model.fit(train_images, train_labels, epochs=5)

# Save the entire model as a SavedModel.
!mkdir -p saved_model
model.save('saved_model/my_model') 
Epoch 1/5
32/32 [==============================] - 0s 2ms/step - loss: 1.1973 - sparse_categorical_accuracy: 0.6330
Epoch 2/5
32/32 [==============================] - 0s 2ms/step - loss: 0.4247 - sparse_categorical_accuracy: 0.8740
Epoch 3/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2784 - sparse_categorical_accuracy: 0.9380
Epoch 4/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2029 - sparse_categorical_accuracy: 0.9490
Epoch 5/5
32/32 [==============================] - 0s 2ms/step - loss: 0.1441 - sparse_categorical_accuracy: 0.9730
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/training/tracking/tracking.py:111: Model.state_updates (from tensorflow.python.keras.engine.training) is deprecated and will be removed in a future version.
Instructions for updating:
This property should not be used in TensorFlow 2.0, as updates are applied automatically.
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/training/tracking/tracking.py:111: Layer.updates (from tensorflow.python.keras.engine.base_layer) is deprecated and will be removed in a future version.
Instructions for updating:
This property should not be used in TensorFlow 2.0, as updates are applied automatically.
INFO:tensorflow:Assets written to: saved_model/my_model/assets

El formato SavedModel es un directorio que contiene un binario protobuf y un punto de control de Tensorflow. Inspeccione el directorio del modelo guardado:

# my_model directory
!ls saved_model

# Contains an assets folder, saved_model.pb, and variables folder.
!ls saved_model/my_model
my_model
assets  saved_model.pb  variables

Vuelva a cargar un modelo de Keras nuevo del modelo guardado:

new_model = tf.keras.models.load_model('saved_model/my_model')

# Check its architecture
new_model.summary()
Model: "sequential_5"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_10 (Dense)             (None, 512)               401920    
_________________________________________________________________
dropout_5 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_11 (Dense)             (None, 10)                5130      
=================================================================
Total params: 407,050
Trainable params: 407,050
Non-trainable params: 0
_________________________________________________________________

El modelo restaurado se compila con los mismos argumentos que el modelo original. Intente ejecutar evaluar y predecir con el modelo cargado:

# Evaluate the restored model
loss, acc = new_model.evaluate(test_images,  test_labels, verbose=2)
print('Restored model, accuracy: {:5.2f}%'.format(100*acc))

print(new_model.predict(test_images).shape)
32/32 - 0s - loss: 0.4378 - sparse_categorical_accuracy: 0.8570
Restored model, accuracy: 85.70%
(1000, 10)

Formato HDF5

Keras proporciona un formato de guardado básico utilizando el estándar HDF5 .

# Create and train a new model instance.
model = create_model()
model.fit(train_images, train_labels, epochs=5)

# Save the entire model to a HDF5 file.
# The '.h5' extension indicates that the model should be saved to HDF5.
model.save('my_model.h5') 
Epoch 1/5
32/32 [==============================] - 0s 2ms/step - loss: 1.1785 - sparse_categorical_accuracy: 0.6550
Epoch 2/5
32/32 [==============================] - 0s 2ms/step - loss: 0.4266 - sparse_categorical_accuracy: 0.8740
Epoch 3/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2987 - sparse_categorical_accuracy: 0.9170
Epoch 4/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2228 - sparse_categorical_accuracy: 0.9400
Epoch 5/5
32/32 [==============================] - 0s 2ms/step - loss: 0.1612 - sparse_categorical_accuracy: 0.9660

Ahora, vuelva a crear el modelo a partir de ese archivo:

# Recreate the exact same model, including its weights and the optimizer
new_model = tf.keras.models.load_model('my_model.h5')

# Show the model architecture
new_model.summary()
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.iter
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.beta_1
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.beta_2
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.decay
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.learning_rate
WARNING:tensorflow:A checkpoint was restored (e.g. tf.train.Checkpoint.restore or tf.keras.Model.load_weights) but not all checkpointed values were used. See above for specific issues. Use expect_partial() on the load status object, e.g. tf.train.Checkpoint.restore(...).expect_partial(), to silence these warnings, or use assert_consumed() to make the check explicit. See https://www.tensorflow.org/guide/checkpoint#loading_mechanics for details.
Model: "sequential_6"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_12 (Dense)             (None, 512)               401920    
_________________________________________________________________
dropout_6 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_13 (Dense)             (None, 10)                5130      
=================================================================
Total params: 407,050
Trainable params: 407,050
Non-trainable params: 0
_________________________________________________________________

Verifique su precisión:

loss, acc = new_model.evaluate(test_images,  test_labels, verbose=2)
print('Restored model, accuracy: {:5.2f}%'.format(100*acc))
32/32 - 0s - loss: 0.4458 - sparse_categorical_accuracy: 0.8510
Restored model, accuracy: 85.10%

Keras guarda modelos inspeccionando la arquitectura. Esta técnica lo salva todo:

  • Los valores de peso
  • La arquitectura del modelo
  • La configuración de entrenamiento del modelo (lo que pasó a compilar)
  • El optimizador y su estado, si lo hay (esto le permite reiniciar el entrenamiento donde lo dejó)

Keras no puede guardar los optimizadores v1.x (de tf.compat.v1.train ) ya que no son compatibles con los puntos de control. Para los optimizadores v1.x, debe volver a compilar el modelo después de la carga, perdiendo el estado del optimizador.

Guardar objetos personalizados

Si está utilizando el formato SavedModel, puede omitir esta sección. La diferencia clave entre HDF5 y SavedModel es que HDF5 usa configuraciones de objetos para guardar la arquitectura del modelo, mientras que SavedModel guarda el gráfico de ejecución. Por lo tanto, SavedModels puede guardar objetos personalizados como modelos en subclases y capas personalizadas sin requerir el código original.

Para guardar objetos personalizados en HDF5, debe hacer lo siguiente:

  1. Defina un método get_config en su objeto y, opcionalmente, un from_config clase from_config.
    • get_config(self) devuelve un diccionario serializable JSON de parámetros necesarios para recrear el objeto.
    • from_config(cls, config) usa la configuración devuelta por get_config para crear un nuevo objeto. Por defecto, esta función usará la configuración como kwargs de inicialización ( return cls(**config) ).
  2. Pase el objeto al argumento custom_objects al cargar el modelo. El argumento debe ser un diccionario que asigne el nombre de la clase de cadena a la clase de Python. Por ejemplo, tf.keras.models.load_model(path, custom_objects={'CustomLayer': CustomLayer})

Consulte el tutorial Escribir capas y modelos desde cero para ver ejemplos de objetos personalizados y get_config .


#
# Copyright (c) 2017 François Chollet
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.