Pomoc chronić Wielkiej Rafy Koralowej z TensorFlow na Kaggle Dołącz Wyzwanie

Zapisz i załaduj modele

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.6.0

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

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
32/32 [==============================] - 1s 7ms/step - loss: 1.1073 - sparse_categorical_accuracy: 0.6970 - val_loss: 0.6858 - val_sparse_categorical_accuracy: 0.8060

Epoch 00001: saving model to training_1/cp.ckpt
Epoch 2/10
32/32 [==============================] - 0s 3ms/step - loss: 0.4064 - sparse_categorical_accuracy: 0.8920 - val_loss: 0.5468 - val_sparse_categorical_accuracy: 0.8370

Epoch 00002: saving model to training_1/cp.ckpt
Epoch 3/10
32/32 [==============================] - 0s 3ms/step - loss: 0.2853 - sparse_categorical_accuracy: 0.9260 - val_loss: 0.4540 - val_sparse_categorical_accuracy: 0.8520

Epoch 00003: saving model to training_1/cp.ckpt
Epoch 4/10
32/32 [==============================] - 0s 3ms/step - loss: 0.2085 - sparse_categorical_accuracy: 0.9530 - val_loss: 0.4369 - val_sparse_categorical_accuracy: 0.8520

Epoch 00004: saving model to training_1/cp.ckpt
Epoch 5/10
32/32 [==============================] - 0s 4ms/step - loss: 0.1573 - sparse_categorical_accuracy: 0.9690 - val_loss: 0.4295 - val_sparse_categorical_accuracy: 0.8580

Epoch 00005: saving model to training_1/cp.ckpt
Epoch 6/10
32/32 [==============================] - 0s 4ms/step - loss: 0.1141 - sparse_categorical_accuracy: 0.9780 - val_loss: 0.4432 - val_sparse_categorical_accuracy: 0.8540

Epoch 00006: saving model to training_1/cp.ckpt
Epoch 7/10
32/32 [==============================] - 0s 3ms/step - loss: 0.0844 - sparse_categorical_accuracy: 0.9850 - val_loss: 0.4102 - val_sparse_categorical_accuracy: 0.8630

Epoch 00007: saving model to training_1/cp.ckpt
Epoch 8/10
32/32 [==============================] - 0s 4ms/step - loss: 0.0597 - sparse_categorical_accuracy: 0.9940 - val_loss: 0.4085 - val_sparse_categorical_accuracy: 0.8700

Epoch 00008: saving model to training_1/cp.ckpt
Epoch 9/10
32/32 [==============================] - 0s 3ms/step - loss: 0.0497 - sparse_categorical_accuracy: 0.9970 - val_loss: 0.4164 - val_sparse_categorical_accuracy: 0.8670

Epoch 00009: saving model to training_1/cp.ckpt
Epoch 10/10
32/32 [==============================] - 0s 3ms/step - loss: 0.0398 - sparse_categorical_accuracy: 0.9990 - val_loss: 0.4289 - val_sparse_categorical_accuracy: 0.8670

Epoch 00010: saving model to training_1/cp.ckpt
<keras.callbacks.History at 0x7f271e52a8d0>

Tworzy to pojedynczą kolekcję plików punktów kontrolnych TensorFlow, które są aktualizowane pod koniec każdej epoki:

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

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.3458 - sparse_categorical_accuracy: 0.0820
Untrained model, accuracy:  8.20%

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.4289 - sparse_categorical_accuracy: 0.8670
Restored model, accuracy: 86.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)
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
<keras.callbacks.History at 0x7f271cc3a190>

Teraz spójrz na powstałe punkty kontrolne i wybierz najnowszy:

os.listdir(checkpoint_dir)
['cp-0050.ckpt.index',
 'cp-0010.ckpt.index',
 'cp-0005.ckpt.index',
 'cp-0045.ckpt.data-00000-of-00001',
 'cp-0030.ckpt.index',
 'cp-0045.ckpt.index',
 'cp-0025.ckpt.index',
 'cp-0005.ckpt.data-00000-of-00001',
 'cp-0035.ckpt.index',
 'cp-0010.ckpt.data-00000-of-00001',
 'cp-0030.ckpt.data-00000-of-00001',
 'cp-0035.ckpt.data-00000-of-00001',
 'cp-0000.ckpt.index',
 'cp-0020.ckpt.index',
 'cp-0050.ckpt.data-00000-of-00001',
 'cp-0040.ckpt.index',
 'cp-0025.ckpt.data-00000-of-00001',
 'cp-0000.ckpt.data-00000-of-00001',
 'cp-0020.ckpt.data-00000-of-00001',
 'cp-0015.ckpt.data-00000-of-00001',
 'checkpoint',
 'cp-0015.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.5194 - sparse_categorical_accuracy: 0.8660
Restored model, accuracy: 86.60%

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.5194 - sparse_categorical_accuracy: 0.8660
Restored model, accuracy: 86.60%

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.2016 - sparse_categorical_accuracy: 0.6480
Epoch 2/5
32/32 [==============================] - 0s 2ms/step - loss: 0.4371 - sparse_categorical_accuracy: 0.8770
Epoch 3/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2941 - sparse_categorical_accuracy: 0.9230
Epoch 4/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2033 - sparse_categorical_accuracy: 0.9490
Epoch 5/5
32/32 [==============================] - 0s 2ms/step - loss: 0.1621 - sparse_categorical_accuracy: 0.9640
2021-10-26 01:31:11.741740: W tensorflow/python/util/util.cc:348] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them.
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 świeży 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.4271 - sparse_categorical_accuracy: 0.8630
Restored model, accuracy: 86.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.1418 - sparse_categorical_accuracy: 0.6670
Epoch 2/5
32/32 [==============================] - 0s 2ms/step - loss: 0.4352 - sparse_categorical_accuracy: 0.8730
Epoch 3/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2826 - sparse_categorical_accuracy: 0.9320
Epoch 4/5
32/32 [==============================] - 0s 2ms/step - loss: 0.2112 - sparse_categorical_accuracy: 0.9430
Epoch 5/5
32/32 [==============================] - 0s 2ms/step - loss: 0.1652 - sparse_categorical_accuracy: 0.9660

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

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.4063 - sparse_categorical_accuracy: 0.8690
Restored model, accuracy: 86.90%

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:

  1. Zdefiniuj metodę get_config w swoim obiekcie i opcjonalnie from_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 z get_config do utworzenia nowego obiektu. Domyślnie ta funkcja użyje konfiguracji jako kwargs inicjalizacji ( return cls(**config) ).
  2. Przekaż obiekt do argumentu custom_objects podczas ładowania modelu. Argument musi być słownikiem odwzorowującym nazwę klasy ciągu na klasę Pythona. Np tf.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.