Zobacz na TensorFlow.org | Uruchom w Google Colab | Wyświetl źródło na GitHub | Pobierz notatnik |
Postęp modelu można zapisać w trakcie i po treningu. Oznacza to, że model może wznowić pracę od miejsca, w którym został przerwany i uniknąć długich czasów szkolenia. Zapisywanie oznacza również, że możesz udostępnić swój model, a inni mogą odtworzyć Twoją pracę. Podczas publikowania modeli i technik badawczych większość praktyków uczenia maszynowego dzieli się:
- kod do stworzenia modelu, i
- wytrenowane wagi lub parametry dla modelu
Udostępnianie tych danych pomaga innym zrozumieć, jak działa model i wypróbować go samodzielnie z nowymi danymi.
Opcje
Istnieją różne sposoby zapisywania modeli TensorFlow w zależności od używanego interfejsu API. Ten przewodnik wykorzystuje tf.keras , wysokopoziomowy interfejs API do budowania i trenowania modeli w TensorFlow. Aby poznać inne podejścia, zobacz przewodnik TensorFlow dotyczący zapisywania i przywracania lub Zapisywanie w eager .
Ustawiać
Instalacje i importy
Zainstaluj i zaimportuj TensorFlow i zależności:
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
Pobierz przykładowy zbiór danych
Aby zademonstrować, jak zapisywać i ładować wagi, użyjesz zestawu danych MNIST . Aby przyspieszyć te przebiegi, użyj pierwszych 1000 przykładów:
(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
Zdefiniuj model
Zacznij od zbudowania prostego modelu sekwencyjnego:
# 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 _________________________________________________________________
Zapisuj punkty kontrolne podczas treningu
Wytrenowanego modelu można używać bez konieczności jego ponownego szkolenia lub wznowić szkolenie w miejscu, w którym zostało przerwane w przypadku przerwania procesu uczenia. Wywołanie zwrotne tf.keras.callbacks.ModelCheckpoint
umożliwia ciągłe zapisywanie modelu zarówno podczas , jak i po zakończeniu szkolenia.
Użycie wywołania zwrotnego w punkcie kontrolnym
Utwórz wywołanie zwrotne tf.keras.callbacks.ModelCheckpoint
, które zapisuje ciężary tylko podczas treningu:
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>
Tworzy to pojedynczą kolekcję plików punktów kontrolnych TensorFlow, które są aktualizowane pod koniec każdej epoki:
os.listdir(checkpoint_dir)
['checkpoint', 'cp.ckpt.index', 'cp.ckpt.data-00000-of-00001']
Dopóki dwa modele mają tę samą architekturę, możesz dzielić między nimi wagi. Dlatego podczas przywracania modelu tylko z wag, utwórz model o tej samej architekturze, co model oryginalny, a następnie ustaw jego wagi.
Teraz odbuduj nowy, niewytrenowany model i oceń go na zestawie testowym. Niewyszkolony model będzie działał na poziomach przypadkowych (~ 10% dokładności):
# 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%
Następnie załaduj ciężary z punktu kontrolnego i ponownie oceń:
# 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%
Opcje oddzwaniania do punktu kontrolnego
Wywołanie zwrotne udostępnia kilka opcji zapewniających unikalne nazwy dla punktów kontrolnych i dostosowywanie częstotliwości punktów kontrolnych.
Trenuj nowy model i zapisuj unikatowo nazwane punkty kontrolne raz na pięć epok:
# 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>
Teraz spójrz na powstałe punkty kontrolne i wybierz najnowszy:
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'
Aby przetestować, zresetuj model i załaduj najnowszy punkt kontrolny:
# 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%
Co to za pliki?
Powyższy kod przechowuje wagi w kolekcji plików sformatowanych w punktach kontrolnych , które zawierają tylko przeszkolone wagi w formacie binarnym. Punkty kontrolne zawierają:
- Co najmniej jeden fragment zawierający wagi modelu.
- Plik indeksu wskazujący, które wagi są przechowywane w którym fragmencie.
Jeśli trenujesz model na jednej maszynie, będziesz miał jeden fragment z sufiksem: .data-00000-of-00001
Ręcznie zapisuj ciężary
Ręczne zapisywanie wag za pomocą metody Model.save_weights
. Domyślnie tf.keras
— a w szczególności save_weights
— używa formatu punktu kontrolnego TensorFlow z rozszerzeniem .ckpt
(zapisywanie w HDF5 z rozszerzeniem .h5
omówiono w przewodniku po modelach zapisywania i serializacji ):
# 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%
Zapisz cały model
Wywołaj model.save
, aby zapisać architekturę modelu, wagi i konfigurację uczenia w jednym pliku/folderze. Pozwala to wyeksportować model, dzięki czemu można go używać bez dostępu do oryginalnego kodu Pythona*. Ponieważ stan optymalizatora zostanie przywrócony, możesz wznowić trening od miejsca, w którym zostało przerwane.
Cały model można zapisać w dwóch różnych formatach plików ( SavedModel
i HDF5
). Format TensorFlow SavedModel
jest domyślnym formatem plików w TF2.x. Jednak modele można zapisać w formacie HDF5
. Więcej szczegółów na temat zapisywania całych modeli w dwóch formatach plików opisano poniżej.
Zapisanie w pełni funkcjonalnego modelu jest bardzo przydatne — można je załadować w TensorFlow.js ( Saved Model , HDF5 ), a następnie przeszkolić i uruchomić je w przeglądarkach internetowych lub przekonwertować do uruchamiania na urządzeniach mobilnych za pomocą TensorFlow Lite ( Saved Model , HDF5 )
*Obiekty niestandardowe (np. modele lub warstwy podklasy) wymagają szczególnej uwagi podczas zapisywania i ładowania. Zobacz sekcję Zapisywanie obiektów niestandardowych poniżej
Zapisany format modelu
Format SavedModel to kolejny sposób serializacji modeli. Modele zapisane w tym formacie można przywrócić za pomocą tf.keras.models.load_model
i są one kompatybilne z TensorFlow Serving. Przewodnik SavedModel zawiera szczegółowe informacje na temat obsługi/kontroli SavedModel. Poniższa sekcja ilustruje kroki zapisywania i przywracania modelu.
# 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
Format SavedModel to katalog zawierający plik binarny protobuf i punkt kontrolny TensorFlow. Sprawdź zapisany katalog modeli:
# 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
Przeładuj nowy model Keras z zapisanego modelu:
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 _________________________________________________________________
Przywrócony model jest kompilowany z tymi samymi argumentami, co model oryginalny. Spróbuj przeprowadzić ocenę i prognozę z załadowanym modelem:
# 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 zapewnia podstawowy format zapisu przy użyciu standardu 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
Teraz odtwórz model z tego pliku:
# 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 _________________________________________________________________
Sprawdź jego dokładność:
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 zapisuje modele, sprawdzając ich architekturę. Ta technika oszczędza wszystko:
- Wartości wagi
- Architektura modelu
- Konfiguracja uczenia modelu (co przekazujesz do metody
.compile()
) - Optymalizator i jego stan, jeśli istnieje (pozwala to na wznowienie treningu od miejsca, w którym zostało przerwane)
Keras nie jest w stanie zapisać optymalizatorów v1.x
(z tf.compat.v1.train
), ponieważ nie są one kompatybilne z punktami kontrolnymi. W przypadku optymalizatorów w wersji 1.x należy ponownie skompilować model po załadowaniu — tracąc stan optymalizatora.
Zapisywanie niestandardowych obiektów
Jeśli używasz formatu SavedModel, możesz pominąć tę sekcję. Kluczowa różnica między HDF5 i SavedModel polega na tym, że HDF5 używa konfiguracji obiektów do zapisywania architektury modelu, podczas gdy SavedModel zapisuje wykres wykonania. W ten sposób SavedModels są w stanie zapisywać niestandardowe obiekty, takie jak modele podklas i niestandardowe warstwy, bez konieczności używania oryginalnego kodu.
Aby zapisać niestandardowe obiekty w HDF5, musisz wykonać następujące czynności:
- Zdefiniuj metodę
get_config
w swoim obiekcie i opcjonalniefrom_config
.-
get_config(self)
zwraca słownik parametrów, które można serializować w formacie JSON, potrzebne do odtworzenia obiektu. -
from_config(cls, config)
używa zwróconej konfiguracji zget_config
do utworzenia nowego obiektu. Domyślnie ta funkcja użyje konfiguracji jako kwargs inicjalizacji (return cls(**config)
).
-
- Przekaż obiekt do argumentu
custom_objects
podczas ładowania modelu. Argument musi być słownikiem odwzorowującym nazwę klasy ciągu na klasę Pythona. Nptf.keras.models.load_model(path, custom_objects={'CustomLayer': CustomLayer})
Zobacz samouczek Pisanie warstw i modeli od podstaw , aby zapoznać się z przykładami obiektów niestandardowych i 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.