Google I/O est terminé ! Suivez les sessions TensorFlow Afficher les sessions

Enregistrer et charger des modèles

Voir sur TensorFlow.org Exécuter dans Google Colab Voir la source sur GitHub Télécharger le cahier

La progression du modèle peut être enregistrée pendant et après l'entraînement. Cela signifie qu'un modèle peut reprendre là où il s'était arrêté et éviter de longs temps d'entraînement. L'enregistrement signifie également que vous pouvez partager votre modèle et que d'autres peuvent recréer votre travail. Lors de la publication de modèles et de techniques de recherche, la plupart des praticiens de l'apprentissage automatique partagent :

  • code pour créer le modèle, et
  • les poids formés, ou paramètres, pour le modèle

Le partage de ces données aide les autres à comprendre le fonctionnement du modèle et à l'essayer eux-mêmes avec de nouvelles données.

Choix

Il existe différentes manières d'enregistrer des modèles TensorFlow en fonction de l'API que vous utilisez. Ce guide utilise tf.keras , une API de haut niveau pour créer et former des modèles dans TensorFlow. Pour d'autres approches, consultez le guide TensorFlow Save and Restore ou Saving in impatient .

Installer

Installe et importe

Installez et importez TensorFlow et ses dépendances :

pip install pyyaml h5py  # Required to save models in HDF5 format
import os

import tensorflow as tf
from tensorflow import keras

print(tf.version.VERSION)
2.8.0-rc1

Obtenir un exemple d'ensemble de données

Pour montrer comment enregistrer et charger des pondérations, vous utiliserez l' ensemble de données MNIST . Pour accélérer ces exécutions, utilisez les 1 000 premiers exemples :

(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

Définir un modèle

Commencez par construire un modèle séquentiel 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
_________________________________________________________________

Enregistrer les points de contrôle pendant la formation

Vous pouvez utiliser un modèle formé sans avoir à le recycler, ou reprendre la formation là où vous l'avez laissée au cas où le processus de formation serait interrompu. Le rappel tf.keras.callbacks.ModelCheckpoint vous permet de sauvegarder continuellement le modèle pendant et à la fin de la formation.

Utilisation du rappel de point de contrôle

Créez un rappel tf.keras.callbacks.ModelCheckpoint qui enregistre les pondérations uniquement pendant l'entraînement :

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
23/32 [====================>.........] - ETA: 0s - loss: 1.3666 - sparse_categorical_accuracy: 0.6060 
Epoch 1: saving model to training_1/cp.ckpt
32/32 [==============================] - 1s 10ms/step - loss: 1.1735 - sparse_categorical_accuracy: 0.6690 - val_loss: 0.7180 - val_sparse_categorical_accuracy: 0.7750
Epoch 2/10
24/32 [=====================>........] - ETA: 0s - loss: 0.4238 - sparse_categorical_accuracy: 0.8789
Epoch 2: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 5ms/step - loss: 0.4201 - sparse_categorical_accuracy: 0.8810 - val_loss: 0.5621 - val_sparse_categorical_accuracy: 0.8150
Epoch 3/10
24/32 [=====================>........] - ETA: 0s - loss: 0.2795 - sparse_categorical_accuracy: 0.9336
Epoch 3: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 5ms/step - loss: 0.2815 - sparse_categorical_accuracy: 0.9310 - val_loss: 0.4790 - val_sparse_categorical_accuracy: 0.8430
Epoch 4/10
24/32 [=====================>........] - ETA: 0s - loss: 0.2027 - sparse_categorical_accuracy: 0.9427
Epoch 4: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 5ms/step - loss: 0.2016 - sparse_categorical_accuracy: 0.9440 - val_loss: 0.4361 - val_sparse_categorical_accuracy: 0.8610
Epoch 5/10
24/32 [=====================>........] - ETA: 0s - loss: 0.1739 - sparse_categorical_accuracy: 0.9583
Epoch 5: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 5ms/step - loss: 0.1683 - sparse_categorical_accuracy: 0.9610 - val_loss: 0.4640 - val_sparse_categorical_accuracy: 0.8580
Epoch 6/10
23/32 [====================>.........] - ETA: 0s - loss: 0.1116 - sparse_categorical_accuracy: 0.9796
Epoch 6: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 5ms/step - loss: 0.1125 - sparse_categorical_accuracy: 0.9780 - val_loss: 0.4420 - val_sparse_categorical_accuracy: 0.8580
Epoch 7/10
24/32 [=====================>........] - ETA: 0s - loss: 0.0978 - sparse_categorical_accuracy: 0.9831
Epoch 7: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 5ms/step - loss: 0.0989 - sparse_categorical_accuracy: 0.9820 - val_loss: 0.4163 - val_sparse_categorical_accuracy: 0.8590
Epoch 8/10
21/32 [==================>...........] - ETA: 0s - loss: 0.0669 - sparse_categorical_accuracy: 0.9911
Epoch 8: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 6ms/step - loss: 0.0690 - sparse_categorical_accuracy: 0.9910 - val_loss: 0.4411 - val_sparse_categorical_accuracy: 0.8600
Epoch 9/10
22/32 [===================>..........] - ETA: 0s - loss: 0.0495 - sparse_categorical_accuracy: 0.9972
Epoch 9: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 5ms/step - loss: 0.0516 - sparse_categorical_accuracy: 0.9950 - val_loss: 0.4064 - val_sparse_categorical_accuracy: 0.8650
Epoch 10/10
24/32 [=====================>........] - ETA: 0s - loss: 0.0436 - sparse_categorical_accuracy: 0.9948
Epoch 10: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 5ms/step - loss: 0.0437 - sparse_categorical_accuracy: 0.9960 - val_loss: 0.4061 - val_sparse_categorical_accuracy: 0.8770
<keras.callbacks.History at 0x7eff8d865390>

Cela crée une collection unique de fichiers de points de contrôle TensorFlow qui sont mis à jour à la fin de chaque époque :

os.listdir(checkpoint_dir)
['checkpoint', 'cp.ckpt.index', 'cp.ckpt.data-00000-of-00001']

Tant que deux modèles partagent la même architecture, vous pouvez partager des poids entre eux. Ainsi, lors de la restauration d'un modèle à partir de pondérations uniquement, créez un modèle avec la même architecture que le modèle d'origine, puis définissez ses pondérations.

Maintenant, reconstruisez un nouveau modèle non formé et évaluez-le sur l'ensemble de test. Un modèle non formé fonctionnera à des niveaux aléatoires (précision d'environ 10 %) :

# 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.4473 - sparse_categorical_accuracy: 0.0980 - 145ms/epoch - 5ms/step
Untrained model, accuracy:  9.80%

Chargez ensuite les poids à partir du point de contrôle et réévaluez :

# 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.4061 - sparse_categorical_accuracy: 0.8770 - 65ms/epoch - 2ms/step
Restored model, accuracy: 87.70%

Options de rappel de point de contrôle

Le rappel fournit plusieurs options pour fournir des noms uniques aux points de contrôle et ajuster la fréquence des points de contrôle.

Entraînez un nouveau modèle et enregistrez des points de contrôle nommés de manière unique une fois toutes les cinq époques :

# 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)

batch_size = 32

# 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,
    save_freq=5*batch_size)

# 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, 
          batch_size=batch_size, 
          callbacks=[cp_callback],
          validation_data=(test_images, test_labels),
          verbose=0)
Epoch 5: saving model to training_2/cp-0005.ckpt

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

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

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

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

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

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

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

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

Epoch 50: saving model to training_2/cp-0050.ckpt
<keras.callbacks.History at 0x7eff807703d0>

Maintenant, regardez les points de contrôle résultants et choisissez le plus récent :

os.listdir(checkpoint_dir)
['cp-0005.ckpt.data-00000-of-00001',
 'cp-0050.ckpt.index',
 'checkpoint',
 'cp-0010.ckpt.index',
 'cp-0035.ckpt.data-00000-of-00001',
 'cp-0000.ckpt.data-00000-of-00001',
 'cp-0050.ckpt.data-00000-of-00001',
 'cp-0010.ckpt.data-00000-of-00001',
 'cp-0020.ckpt.data-00000-of-00001',
 'cp-0035.ckpt.index',
 'cp-0040.ckpt.index',
 'cp-0025.ckpt.data-00000-of-00001',
 'cp-0045.ckpt.index',
 'cp-0020.ckpt.index',
 'cp-0025.ckpt.index',
 'cp-0030.ckpt.data-00000-of-00001',
 'cp-0030.ckpt.index',
 'cp-0000.ckpt.index',
 'cp-0045.ckpt.data-00000-of-00001',
 'cp-0015.ckpt.index',
 'cp-0015.ckpt.data-00000-of-00001',
 'cp-0005.ckpt.index',
 'cp-0040.ckpt.data-00000-of-00001']
latest = tf.train.latest_checkpoint(checkpoint_dir)
latest
'training_2/cp-0050.ckpt'

Pour tester, réinitialisez le modèle et chargez le dernier point de contrôle :

# 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.4996 - sparse_categorical_accuracy: 0.8770 - 150ms/epoch - 5ms/step
Restored model, accuracy: 87.70%

Quels sont ces fichiers ?

Le code ci-dessus stocke les pondérations dans une collection de fichiers au format de point de contrôle qui contiennent uniquement les pondérations formées dans un format binaire. Les points de contrôle contiennent :

  • Une ou plusieurs partitions contenant les poids de votre modèle.
  • Un fichier d'index qui indique quelles pondérations sont stockées dans quelle partition.

Si vous entraînez un modèle sur une seule machine, vous aurez une partition avec le suffixe : .data-00000-of-00001

Enregistrer manuellement les poids

Enregistrement manuel des poids avec la méthode Model.save_weights . Par défaut, tf.keras — et save_weights en particulier — utilise le format de point de contrôle TensorFlow avec une extension .ckpt (l'enregistrement en HDF5 avec une extension .h5 est traité dans le guide Enregistrer et sérialiser les modèles ) :

# 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.4996 - sparse_categorical_accuracy: 0.8770 - 143ms/epoch - 4ms/step
Restored model, accuracy: 87.70%

Enregistrer le modèle entier

Appelez model.save pour enregistrer l'architecture, les pondérations et la configuration d'entraînement d'un modèle dans un seul fichier/dossier. Cela vous permet d'exporter un modèle afin qu'il puisse être utilisé sans avoir accès au code Python d'origine*. Étant donné que l'état de l'optimiseur est récupéré, vous pouvez reprendre l'entraînement exactement là où vous l'aviez laissé.

Un modèle entier peut être enregistré dans deux formats de fichiers différents ( SavedModel et HDF5 ). Le format TensorFlow SavedModel est le format de fichier par défaut dans TF2.x. Cependant, les modèles peuvent être enregistrés au format HDF5 . Plus de détails sur l'enregistrement de modèles entiers dans les deux formats de fichier sont décrits ci-dessous.

L'enregistrement d'un modèle entièrement fonctionnel est très utile : vous pouvez les charger dans TensorFlow.js ( Modèle enregistré , HDF5 ), puis les entraîner et les exécuter dans des navigateurs Web, ou les convertir pour qu'ils s'exécutent sur des appareils mobiles à l'aide de TensorFlow Lite ( Modèle enregistré , HDF5 ). )

*Les objets personnalisés (par exemple, des modèles ou des calques sous-classés) nécessitent une attention particulière lors de l'enregistrement et du chargement. Voir la section Enregistrement d'objets personnalisés ci-dessous

Format du modèle enregistré

Le format SavedModel est un autre moyen de sérialiser des modèles. Les modèles enregistrés dans ce format peuvent être restaurés à l'aide tf.keras.models.load_model et sont compatibles avec TensorFlow Serving. Le guide SavedModel explique en détail comment servir/inspecter le SavedModel. La section ci-dessous illustre les étapes pour enregistrer et restaurer le modèle.

# 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.1988 - sparse_categorical_accuracy: 0.6550
Epoch 2/5
32/32 [==============================] - 0s 2ms/step - loss: 0.4180 - sparse_categorical_accuracy: 0.8930
Epoch 3/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2900 - sparse_categorical_accuracy: 0.9220
Epoch 4/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2070 - sparse_categorical_accuracy: 0.9540
Epoch 5/5
32/32 [==============================] - 0s 2ms/step - loss: 0.1593 - sparse_categorical_accuracy: 0.9630
2022-01-26 07:30:22.888387: W tensorflow/python/util/util.cc:368] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them.
WARNING:tensorflow:Detecting that an object or model or tf.train.Checkpoint is being deleted with unrestored values. See the following logs for the specific values in question. To silence these warnings, use `status.expect_partial()`. See https://www.tensorflow.org/api_docs/python/tf/train/Checkpoint#restorefor details about the status object returned by the restore function.
WARNING:tensorflow:Value in checkpoint could not be found in the restored object: (root).optimizer.iter
WARNING:tensorflow:Value in checkpoint could not be found in the restored object: (root).optimizer.beta_1
WARNING:tensorflow:Value in checkpoint could not be found in the restored object: (root).optimizer.beta_2
WARNING:tensorflow:Value in checkpoint could not be found in the restored object: (root).optimizer.decay
WARNING:tensorflow:Value in checkpoint could not be found in the restored object: (root).optimizer.learning_rate
WARNING:tensorflow:Detecting that an object or model or tf.train.Checkpoint is being deleted with unrestored values. See the following logs for the specific values in question. To silence these warnings, use `status.expect_partial()`. See https://www.tensorflow.org/api_docs/python/tf/train/Checkpoint#restorefor details about the status object returned by the restore function.
WARNING:tensorflow:Value in checkpoint could not be found in the restored object: (root).optimizer.iter
WARNING:tensorflow:Value in checkpoint could not be found in the restored object: (root).optimizer.beta_1
WARNING:tensorflow:Value in checkpoint could not be found in the restored object: (root).optimizer.beta_2
WARNING:tensorflow:Value in checkpoint could not be found in the restored object: (root).optimizer.decay
WARNING:tensorflow:Value in checkpoint could not be found in the restored object: (root).optimizer.learning_rate
INFO:tensorflow:Assets written to: saved_model/my_model/assets

Le format SavedModel est un répertoire contenant un binaire protobuf et un point de contrôle TensorFlow. Inspectez le répertoire du modèle enregistré :

# my_model directory
ls saved_model

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

Rechargez un nouveau modèle Keras à partir du modèle enregistré :

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
_________________________________________________________________

Le modèle restauré est compilé avec les mêmes arguments que le modèle d'origine. Essayez d'exécuter l'évaluation et la prédiction avec le modèle chargé :

# 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.4577 - sparse_categorical_accuracy: 0.8430 - 156ms/epoch - 5ms/step
Restored model, accuracy: 84.30%
(1000, 10)

Format HDF5

Keras fournit un format de sauvegarde de base utilisant la norme 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.1383 - sparse_categorical_accuracy: 0.6970
Epoch 2/5
32/32 [==============================] - 0s 2ms/step - loss: 0.4094 - sparse_categorical_accuracy: 0.8920
Epoch 3/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2936 - sparse_categorical_accuracy: 0.9160
Epoch 4/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2050 - sparse_categorical_accuracy: 0.9460
Epoch 5/5
32/32 [==============================] - 0s 2ms/step - loss: 0.1485 - sparse_categorical_accuracy: 0.9690

Maintenant, recréez le modèle à partir de ce fichier :

# 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()
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
_________________________________________________________________

Vérifiez son exactitude :

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.4266 - sparse_categorical_accuracy: 0.8620 - 141ms/epoch - 4ms/step
Restored model, accuracy: 86.20%

Keras enregistre les modèles en inspectant leurs architectures. Cette technique sauve tout :

  • Les valeurs de poids
  • L'architecture du modèle
  • La configuration d'entraînement du modèle (ce que vous transmettez à la méthode .compile() )
  • L'optimiseur et son état, le cas échéant (cela vous permet de reprendre l'entraînement là où vous l'aviez laissé)

Keras n'est pas en mesure de sauvegarder les optimiseurs v1.x (de tf.compat.v1.train ) car ils ne sont pas compatibles avec les points de contrôle. Pour les optimiseurs v1.x, vous devez recompiler le modèle après le chargement, ce qui perd l'état de l'optimiseur.

Enregistrement d'objets personnalisés

Si vous utilisez le format SavedModel, vous pouvez ignorer cette section. La principale différence entre HDF5 et SavedModel est que HDF5 utilise des configurations d'objet pour enregistrer l'architecture du modèle, tandis que SavedModel enregistre le graphique d'exécution. Ainsi, SavedModels est capable d'enregistrer des objets personnalisés tels que des modèles sous-classés et des couches personnalisées sans nécessiter le code d'origine.

Pour enregistrer des objets personnalisés dans HDF5, vous devez procéder comme suit :

  1. Définissez une méthode get_config dans votre objet et éventuellement une méthode de from_config .
    • get_config(self) renvoie un dictionnaire JSON sérialisable des paramètres nécessaires pour recréer l'objet.
    • from_config(cls, config) utilise la configuration renvoyée par get_config pour créer un nouvel objet. Par défaut, cette fonction utilisera la configuration comme kwargs d'initialisation ( return cls(**config) ).
  2. Passez l'objet à l'argument custom_objects lors du chargement du modèle. L'argument doit être un dictionnaire mappant le nom de classe de chaîne à la classe Python. Par exemple tf.keras.models.load_model(path, custom_objects={'CustomLayer': CustomLayer})

Consultez le didacticiel Rédaction de calques et de modèles à partir de rien pour obtenir des exemples d'objets personnalisés et de get_config .

# MIT License
#
# 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.