Esta página foi traduzida pela API Cloud Translation.
Switch to English

Salvar e carregar modelos

Ver no TensorFlow.org Executar no Google Colab Ver fonte no GitHub Baixar caderno

O progresso do modelo pode ser salvo durante e após o treinamento. Isso significa que um modelo pode retomar de onde parou e evitar longos períodos de treinamento. Salvar também significa que você pode compartilhar seu modelo e outras pessoas podem recriar seu trabalho. Ao publicar modelos e técnicas de pesquisa, a maioria dos profissionais de aprendizado de máquina compartilha:

  • código para criar o modelo, e
  • os pesos treinados, ou parâmetros, para o modelo

Compartilhar esses dados ajuda outras pessoas a entender como o modelo funciona e a experimentá-lo com novos dados.

Opções

Existem diferentes maneiras de salvar modelos do TensorFlow, dependendo da API que você está usando. Este guia usa tf.keras , uma API de alto nível para criar e treinar modelos no TensorFlow. Para outras abordagens, consulte o guia de salvamento e restauração do TensorFlow ou Salvando com antecedência.

Configuração

Instalações e importações

Instale e importe TensorFlow e dependências:

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

import tensorflow as tf
from tensorflow import keras

print(tf.version.VERSION)
2.3.1

Obtenha um conjunto de dados de exemplo

Para demonstrar como salvar e carregar pesos, você usará o conjunto de dados MNIST . Para acelerar essas execuções, use os primeiros 1000 exemplos:

(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
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11493376/11490434 [==============================] - 0s 0us/step

Defina um modelo

Comece construindo um modelo sequencial simples:

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

Salvar pontos de verificação durante o treinamento

Você pode usar um modelo treinado sem ter que retreiná-lo ou retomar o treinamento de onde parou, caso o processo de treinamento seja interrompido. O retorno de chamada tf.keras.callbacks.ModelCheckpoint permite que você salve continuamente o modelo durante e no final do treinamento.

Uso de retorno de chamada de ponto de verificação

Crie um retorno de chamada tf.keras.callbacks.ModelCheckpoint que economiza pesos apenas durante o treinamento:

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
30/32 [===========================>..] - ETA: 0s - loss: 1.2558 - sparse_categorical_accuracy: 0.6406    
Epoch 00001: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 8ms/step - loss: 1.2303 - sparse_categorical_accuracy: 0.6470 - val_loss: 0.7268 - val_sparse_categorical_accuracy: 0.7810
Epoch 2/10
29/32 [==========================>...] - ETA: 0s - loss: 0.4346 - sparse_categorical_accuracy: 0.8793
Epoch 00002: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 5ms/step - loss: 0.4337 - sparse_categorical_accuracy: 0.8770 - val_loss: 0.5663 - val_sparse_categorical_accuracy: 0.8140
Epoch 3/10
29/32 [==========================>...] - ETA: 0s - loss: 0.2827 - sparse_categorical_accuracy: 0.9300
Epoch 00003: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 4ms/step - loss: 0.2761 - sparse_categorical_accuracy: 0.9320 - val_loss: 0.4866 - val_sparse_categorical_accuracy: 0.8460
Epoch 4/10
30/32 [===========================>..] - ETA: 0s - loss: 0.2023 - sparse_categorical_accuracy: 0.9510
Epoch 00004: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 4ms/step - loss: 0.2090 - sparse_categorical_accuracy: 0.9490 - val_loss: 0.4949 - val_sparse_categorical_accuracy: 0.8320
Epoch 5/10
29/32 [==========================>...] - ETA: 0s - loss: 0.1542 - sparse_categorical_accuracy: 0.9731
Epoch 00005: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 4ms/step - loss: 0.1558 - sparse_categorical_accuracy: 0.9710 - val_loss: 0.4261 - val_sparse_categorical_accuracy: 0.8630
Epoch 6/10
29/32 [==========================>...] - ETA: 0s - loss: 0.1084 - sparse_categorical_accuracy: 0.9838
Epoch 00006: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 4ms/step - loss: 0.1066 - sparse_categorical_accuracy: 0.9830 - val_loss: 0.4880 - val_sparse_categorical_accuracy: 0.8430
Epoch 7/10
29/32 [==========================>...] - ETA: 0s - loss: 0.1019 - sparse_categorical_accuracy: 0.9881
Epoch 00007: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 4ms/step - loss: 0.1009 - sparse_categorical_accuracy: 0.9880 - val_loss: 0.4103 - val_sparse_categorical_accuracy: 0.8690
Epoch 8/10
30/32 [===========================>..] - ETA: 0s - loss: 0.0687 - sparse_categorical_accuracy: 0.9927
Epoch 00008: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 4ms/step - loss: 0.0682 - sparse_categorical_accuracy: 0.9930 - val_loss: 0.4236 - val_sparse_categorical_accuracy: 0.8760
Epoch 9/10
30/32 [===========================>..] - ETA: 0s - loss: 0.0555 - sparse_categorical_accuracy: 0.9906
Epoch 00009: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 4ms/step - loss: 0.0547 - sparse_categorical_accuracy: 0.9910 - val_loss: 0.4340 - val_sparse_categorical_accuracy: 0.8550
Epoch 10/10
29/32 [==========================>...] - ETA: 0s - loss: 0.0464 - sparse_categorical_accuracy: 0.9946
Epoch 00010: saving model to training_1/cp.ckpt
32/32 [==============================] - 0s 4ms/step - loss: 0.0451 - sparse_categorical_accuracy: 0.9950 - val_loss: 0.4008 - val_sparse_categorical_accuracy: 0.8760

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

Isso cria uma única coleção de arquivos de checkpoint do TensorFlow que são atualizados no final de cada época:

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

Contanto que dois modelos compartilhem a mesma arquitetura, você pode compartilhar pesos entre eles. Portanto, ao restaurar um modelo apenas de pesos, crie um modelo com a mesma arquitetura do modelo original e defina seus pesos.

Agora reconstrua um modelo novo e não treinado e avalie-o no conjunto de teste. Um modelo não treinado terá desempenho em níveis de chance (precisão de ~ 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.3885 - sparse_categorical_accuracy: 0.0510
Untrained model, accuracy:  5.10%

Em seguida, carregue os pesos do ponto de verificação e reavalie:

# 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.4008 - sparse_categorical_accuracy: 0.8760
Restored model, accuracy: 87.60%

Opções de retorno de chamada de ponto de verificação

O retorno de chamada oferece várias opções para fornecer nomes exclusivos para pontos de verificação e ajustar a frequência dos pontos de verificação.

Treine um novo modelo e salve pontos de verificação com nomes exclusivos a 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)

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, 
          callbacks=[cp_callback],
          validation_data=(test_images, test_labels),
          verbose=0)

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 0x7feb5da37c50>

Agora, olhe para os pontos de verificação resultantes e escolha o mais recente:

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 testar, redefina o modelo e carregue o último ponto de verificação:

# 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.4781 - sparse_categorical_accuracy: 0.8800
Restored model, accuracy: 88.00%

O que são esses arquivos?

O código acima armazena os pesos em uma coleção de arquivos formatados de pontos de verificação que contêm apenas os pesos treinados em um formato binário. Os pontos de verificação contêm:

  • Um ou mais fragmentos que contêm os pesos do seu modelo.
  • Um arquivo de índice que indica quais pesos são armazenados em quais fragmentos.

Se você estiver treinando um modelo em uma única máquina, terá um fragmento com o sufixo: .data-00000-of-00001

Salvar pesos manualmente

Salvar pesos manualmente com o método Model.save_weights . Por padrão, tf.keras - e save_weights em particular - usa o formato de checkpoint do TensorFlow com uma extensão .ckpt (salvar em HDF5 com uma extensão .h5 é abordado no guia Salvar e 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.4781 - sparse_categorical_accuracy: 0.8800
Restored model, accuracy: 88.00%

Salve todo o modelo

Chame model.save para salvar a arquitetura, os pesos e a configuração de treinamento de um modelo em um único arquivo / pasta. Isso permite exportar um modelo para que ele possa ser usado sem acesso ao código Python original *. Uma vez que o estado do otimizador foi recuperado, você pode retomar o treinamento exatamente de onde parou.

Um modelo inteiro pode ser salvo em dois formatos de arquivo diferentes ( SavedModel e HDF5 ). O formato TensorFlow SavedModel é o formato de arquivo padrão em TF2.x. No entanto, os modelos podem ser salvos no formato HDF5 . Mais detalhes sobre como salvar modelos inteiros nos dois formatos de arquivo são descritos abaixo.

Salvar um modelo totalmente funcional é muito útil - você pode carregá-los no TensorFlow.js ( modelo salvo , HDF5 ) e, em seguida, treiná-los e executá-los em navegadores da Web ou convertê-los para execução em dispositivos móveis usando TensorFlow Lite ( modelo salvo , HDF5 )

* Objetos personalizados (por exemplo, modelos ou camadas com subclasses) requerem atenção especial ao salvar e carregar. Veja a seção Salvando objetos personalizados abaixo

Formato SavedModel

O formato SavedModel é outra maneira de serializar modelos. Os modelos salvos neste formato podem ser restaurados usando tf.keras.models.load_model e são compatíveis com o TensorFlow Serving. O guia SavedModel entra em detalhes sobre como servir / inspecionar o SavedModel. A seção abaixo ilustra as etapas para salvar e restaurar o 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.1602 - sparse_categorical_accuracy: 0.6690
Epoch 2/5
32/32 [==============================] - 0s 2ms/step - loss: 0.4142 - sparse_categorical_accuracy: 0.8900
Epoch 3/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2877 - sparse_categorical_accuracy: 0.9120
Epoch 4/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2107 - sparse_categorical_accuracy: 0.9480
Epoch 5/5
32/32 [==============================] - 0s 2ms/step - loss: 0.1456 - sparse_categorical_accuracy: 0.9710
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

O formato SavedModel é um diretório que contém um binário protobuf e um ponto de verificação do TensorFlow. Inspecione o diretório do modelo salvo:

# 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

Recarregue um novo modelo Keras do modelo salvo:

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
_________________________________________________________________

O modelo restaurado é compilado com os mesmos argumentos do modelo original. Tente executar a avaliação e previsão com o modelo carregado:

# 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.4159 - sparse_categorical_accuracy: 0.8580
Restored model, accuracy: 85.80%
(1000, 10)

Formato HDF5

Keras fornece um formato de salvamento básico usando o padrão 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.1419 - sparse_categorical_accuracy: 0.6870
Epoch 2/5
32/32 [==============================] - 0s 2ms/step - loss: 0.4163 - sparse_categorical_accuracy: 0.8810
Epoch 3/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2913 - sparse_categorical_accuracy: 0.9260
Epoch 4/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2085 - sparse_categorical_accuracy: 0.9440
Epoch 5/5
32/32 [==============================] - 0s 2ms/step - loss: 0.1501 - sparse_categorical_accuracy: 0.9720

Agora, recrie o modelo desse arquivo:

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

Verifique sua precisão:

loss, acc = new_model.evaluate(test_images, test_labels, verbose=2)
print('Restored model, accuracy: {:5.2f}%'.format(100 * acc))
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.
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.
32/32 - 0s - loss: 0.4239 - sparse_categorical_accuracy: 0.8650
Restored model, accuracy: 86.50%

Keras salva modelos inspecionando suas arquiteturas. Esta técnica salva tudo:

  • Os valores de peso
  • A arquitetura do modelo
  • A configuração de treinamento do modelo (o que você passa para o método .compile() )
  • O otimizador e seu estado, se houver (isso permite que você reinicie o treinamento de onde parou)

Keras não pode salvar os otimizadores v1.x (de tf.compat.v1.train ), pois eles não são compatíveis com pontos de verificação. Para otimizadores v1.x, você precisa recompilar o modelo após o carregamento - perdendo o estado do otimizador.

Salvando objetos personalizados

Se estiver usando o formato SavedModel, você pode pular esta seção. A principal diferença entre HDF5 e SavedModel é que HDF5 usa configurações de objeto para salvar a arquitetura do modelo, enquanto SavedModel salva o gráfico de execução. Assim, SavedModels são capazes de salvar objetos personalizados, como modelos com subclasses e camadas personalizadas, sem exigir o código original.

Para salvar objetos personalizados em HDF5, você deve fazer o seguinte:

  1. Defina um método get_config em seu objeto e, opcionalmente, um from_config .
    • get_config(self) retorna um dicionário JSON serializável de parâmetros necessários para recriar o objeto.
    • from_config(cls, config) usa a configuração retornada de get_config para criar um novo objeto. Por padrão, esta função usará o config como kwargs de inicialização ( return cls(**config) ).
  2. Passe o objeto para o argumento custom_objects ao carregar o modelo. O argumento deve ser um dicionário que mapeia o nome da classe da string para a classe Python. Por exemplo, tf.keras.models.load_model(path, custom_objects={'CustomLayer': CustomLayer})

Consulte o tutorial Gravando camadas e modelos do zero para obter exemplos de objetos personalizados e 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.