Simpan dan muat model

Lihat di TensorFlow.org Jalankan di Google Colab Lihat sumber di GitHub Unduh buku catatan

Kemajuan model dapat disimpan selama dan setelah pelatihan. Ini berarti model dapat melanjutkan dari posisi sebelumnya dan menghindari waktu pelatihan yang lama. Menyimpan juga berarti Anda dapat membagikan model Anda dan orang lain dapat membuat ulang karya Anda. Saat menerbitkan model dan teknik penelitian, sebagian besar praktisi pembelajaran mesin berbagi:

  • kode untuk membuat model, dan
  • bobot terlatih, atau parameter, untuk model

Berbagi data ini membantu orang lain memahami cara kerja model dan mencobanya sendiri dengan data baru.

Pilihan

Ada berbagai cara untuk menyimpan model TensorFlow bergantung pada API yang Anda gunakan. Panduan ini menggunakan tf.keras , API tingkat tinggi untuk membuat dan melatih model di TensorFlow. Untuk pendekatan lain, lihat panduan TensorFlow Save and Restore atau Saving in bersemangat .

Mempersiapkan

Menginstal dan mengimpor

Instal dan impor TensorFlow dan dependensi:

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

Dapatkan contoh kumpulan data

Untuk mendemonstrasikan cara menyimpan dan memuat bobot, Anda akan menggunakan set data MNIST . Untuk mempercepat proses ini, gunakan 1000 contoh pertama:

(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

Tentukan model

Mulailah dengan membangun model sekuensial sederhana:

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

Simpan pos pemeriksaan selama pelatihan

Anda dapat menggunakan model terlatih tanpa harus melatihnya kembali, atau mengambil pelatihan yang Anda tinggalkan jika proses pelatihan terganggu. Callback tf.keras.callbacks.ModelCheckpoint memungkinkan Anda untuk terus menyimpan model selama dan di akhir pelatihan.

Penggunaan panggilan balik pos pemeriksaan

Buat panggilan balik tf.keras.callbacks.ModelCheckpoint yang menghemat bobot hanya selama pelatihan:

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>

Tindakan ini membuat satu koleksi file pos pemeriksaan TensorFlow yang diperbarui di akhir setiap zaman:

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

Selama dua model berbagi arsitektur yang sama, Anda dapat berbagi bobot di antara keduanya. Jadi, saat memulihkan model dari hanya bobot, buat model dengan arsitektur yang sama dengan model aslinya, lalu atur bobotnya.

Sekarang bangun kembali model baru yang tidak terlatih dan evaluasi pada set pengujian. Model yang tidak terlatih akan tampil pada tingkat kebetulan (~ akurasi 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%

Kemudian muat bobot dari pos pemeriksaan dan evaluasi kembali:

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

Opsi panggilan balik pos pemeriksaan

Callback menyediakan beberapa opsi untuk memberikan nama unik untuk pos pemeriksaan dan menyesuaikan frekuensi pos pemeriksaan.

Latih model baru, dan simpan pos pemeriksaan bernama unik setiap lima zaman:

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

Sekarang, lihat pos pemeriksaan yang dihasilkan dan pilih yang terbaru:

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'

Untuk menguji, setel ulang model dan muat pos pemeriksaan terbaru:

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

Apa file-file ini?

Kode di atas menyimpan bobot ke kumpulan file berformat pos pemeriksaan yang hanya berisi bobot terlatih dalam format biner. Pos pemeriksaan berisi:

  • Satu atau beberapa pecahan yang berisi bobot model Anda.
  • File indeks yang menunjukkan bobot mana yang disimpan di shard mana.

Jika Anda melatih model pada satu mesin, Anda akan memiliki satu pecahan dengan akhiran: .data-00000-of-00001

Menghemat beban secara manual

Menyimpan bobot secara manual dengan metode Model.save_weights . Secara default, tf.keras —dan save_weights khususnya—menggunakan format pos pemeriksaan TensorFlow dengan ekstensi .ckpt (menyimpan dalam HDF5 dengan ekstensi .h5 tercakup dalam panduan Simpan dan buat serialisasi model ):

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

Simpan seluruh model

Panggil model.save untuk menyimpan arsitektur model, bobot, dan konfigurasi pelatihan dalam satu file/folder. Ini memungkinkan Anda mengekspor model sehingga dapat digunakan tanpa akses ke kode Python asli*. Karena status pengoptimal dipulihkan, Anda dapat melanjutkan pelatihan dari tempat terakhir Anda tinggalkan.

Seluruh model dapat disimpan dalam dua format file yang berbeda ( SavedModel dan HDF5 ). Format TensorFlow SavedModel adalah format file default di TF2.x. Namun, model dapat disimpan dalam format HDF5 . Rincian lebih lanjut tentang menyimpan seluruh model dalam dua format file dijelaskan di bawah ini.

Menyimpan model yang berfungsi penuh sangat berguna—Anda dapat memuatnya di TensorFlow.js ( Model Tersimpan , HDF5 ) lalu melatih dan menjalankannya di browser web, atau mengonversinya agar berjalan di perangkat seluler menggunakan TensorFlow Lite ( Model Tersimpan , HDF5 )

*Objek kustom (misalnya model atau lapisan subkelas) memerlukan perhatian khusus saat menyimpan dan memuat. Lihat bagian Menyimpan objek khusus di bawah

Format Model Tersimpan

Format SavedModel adalah cara lain untuk membuat serial model. Model yang disimpan dalam format ini dapat dipulihkan menggunakan tf.keras.models.load_model dan kompatibel dengan TensorFlow Serving. Panduan SavedModel menjelaskan secara rinci tentang cara melayani/memeriksa model SavedModel. Bagian di bawah ini mengilustrasikan langkah-langkah untuk menyimpan dan memulihkan model.

# 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 adalah direktori yang berisi biner protobuf dan pos pemeriksaan TensorFlow. Periksa direktori model yang disimpan:

# 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

Muat ulang model Keras baru dari model yang disimpan:

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
_________________________________________________________________

Model yang dipulihkan dikompilasi dengan argumen yang sama seperti model aslinya. Coba jalankan evaluasi dan prediksi dengan model yang dimuat:

# 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 menyediakan format penyimpanan dasar menggunakan standar 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

Sekarang, buat ulang model dari file itu:

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

Periksa keakuratannya:

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 menyimpan model dengan memeriksa arsitekturnya. Teknik ini menghemat segalanya:

  • Nilai beratnya
  • Arsitektur model
  • Konfigurasi pelatihan model (yang Anda teruskan ke metode .compile() )
  • Pengoptimal dan statusnya, jika ada (ini memungkinkan Anda untuk memulai kembali pelatihan di mana Anda tinggalkan)

Keras tidak dapat menyimpan pengoptimal v1.x (dari tf.compat.v1.train ) karena tidak kompatibel dengan pos pemeriksaan. Untuk pengoptimal v1.x, Anda perlu mengompilasi ulang model setelah memuat—kehilangan status pengoptimal.

Menyimpan objek khusus

Jika Anda menggunakan format SavedModel, Anda dapat melewati bagian ini. Perbedaan utama antara HDF5 dan SavedModel adalah bahwa HDF5 menggunakan konfigurasi objek untuk menyimpan arsitektur model, sedangkan SavedModel menyimpan grafik eksekusi. Dengan demikian, SavedModels dapat menyimpan objek khusus seperti model subkelas dan lapisan khusus tanpa memerlukan kode asli.

Untuk menyimpan objek kustom ke HDF5, Anda harus melakukan hal berikut:

  1. Tentukan metode get_config di objek Anda, dan secara opsional metode from_config .
    • get_config(self) mengembalikan kamus JSON-serializable dari parameter yang diperlukan untuk membuat ulang objek.
    • from_config(cls, config) menggunakan konfigurasi yang dikembalikan dari get_config untuk membuat objek baru. Secara default, fungsi ini akan menggunakan konfigurasi sebagai inisialisasi kwargs ( return cls(**config) ).
  2. Lewati objek ke argumen custom_objects saat memuat model. Argumen harus berupa kamus yang memetakan nama kelas string ke kelas Python. Misalnya tf.keras.models.load_model(path, custom_objects={'CustomLayer': CustomLayer})

Lihat tutorial menulis lapisan dan model dari awal untuk contoh objek khusus dan 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.