|TensorFlow.org에서 보기||구글 코랩(Colab)에서 실행하기||깃허브(GitHub) 소스 보기||Download notebook|
이 노트북은 영화 리뷰(review) 텍스트를 긍정(positive) 또는 부정(negative)으로 분류합니다. 이 예제는 이진(binary)-또는 클래스(class)가 두 개인- 분류 문제입니다. 이진 분류는 머신러닝에서 중요하고 널리 사용됩니다.
이 튜토리얼에서는 텐서플로 허브(TensorFlow Hub)와 케라스(Keras)를 사용한 기초적인 전이 학습(transfer learning) 애플리케이션을 보여줍니다.
여기에서는 인터넷 영화 데이터베이스(Internet Movie Database)에서 수집한 50,000개의 영화 리뷰 텍스트를 담은 IMDB 데이터셋을 사용하겠습니다. 25,000개 리뷰는 훈련용으로, 25,000개는 테스트용으로 나뉘어져 있습니다. 훈련 세트와 테스트 세트의 클래스는 균형이 잡혀 있습니다. 즉 긍정적인 리뷰와 부정적인 리뷰의 개수가 동일합니다.
import numpy as np import tensorflow as tf !pip install -q tensorflow-hub !pip install -q tfds-nightly import tensorflow_hub as hub import tensorflow_datasets as tfds print("버전: ", tf.__version__) print("즉시 실행 모드: ", tf.executing_eagerly()) print("허브 버전: ", hub.__version__) print("GPU", "사용 가능" if tf.config.experimental.list_physical_devices("GPU") else "NOT AVAILABLE")
버전: 2.3.0 즉시 실행 모드: True 허브 버전: 0.8.0 GPU 사용 가능
IMDB 데이터셋 다운로드
# 훈련 세트를 6대 4로 나눕니다. # 결국 훈련에 15,000개 샘플, 검증에 10,000개 샘플, 테스트에 25,000개 샘플을 사용하게 됩니다. train_data, validation_data, test_data = tfds.load( name="imdb_reviews", split=('train[:60%]', 'train[60%:]', 'test'), as_supervised=True)
Downloading and preparing dataset imdb_reviews/plain_text/1.0.0 (download: 80.23 MiB, generated: Unknown size, total: 80.23 MiB) to /home/kbuilder/tensorflow_datasets/imdb_reviews/plain_text/1.0.0... Shuffling and writing examples to /home/kbuilder/tensorflow_datasets/imdb_reviews/plain_text/1.0.0.incompleteDRY98T/imdb_reviews-train.tfrecord Shuffling and writing examples to /home/kbuilder/tensorflow_datasets/imdb_reviews/plain_text/1.0.0.incompleteDRY98T/imdb_reviews-test.tfrecord Shuffling and writing examples to /home/kbuilder/tensorflow_datasets/imdb_reviews/plain_text/1.0.0.incompleteDRY98T/imdb_reviews-unsupervised.tfrecord Dataset imdb_reviews downloaded and prepared to /home/kbuilder/tensorflow_datasets/imdb_reviews/plain_text/1.0.0. Subsequent calls will reuse this data.
잠시 데이터 형태를 알아 보죠. 이 데이터셋의 샘플은 전처리된 정수 배열입니다. 이 정수는 영화 리뷰에 나오는 단어를 나타냅니다. 레이블(label)은 정수 0 또는 1입니다. 0은 부정적인 리뷰이고 1은 긍정적인 리뷰입니다.
처음 10개의 샘플을 출력해 보겠습니다.
train_examples_batch, train_labels_batch = next(iter(train_data.batch(10))) train_examples_batch
<tf.Tensor: shape=(10,), dtype=string, numpy= array([b"This was an absolutely terrible movie. Don't be lured in by Christopher Walken or Michael Ironside. Both are great actors, but this must simply be their worst role in history. Even their great acting could not redeem this movie's ridiculous storyline. This movie is an early nineties US propaganda piece. The most pathetic scenes were those when the Columbian rebels were making their cases for revolutions. Maria Conchita Alonso appeared phony, and her pseudo-love affair with Walken was nothing but a pathetic emotional plug in a movie that was devoid of any real meaning. I am disappointed that there are movies like this, ruining actor's like Christopher Walken's good name. I could barely sit through it.", b'I have been known to fall asleep during films, but this is usually due to a combination of things including, really tired, being warm and comfortable on the sette and having just eaten a lot. However on this occasion I fell asleep because the film was rubbish. The plot development was constant. Constantly slow and boring. Things seemed to happen, but with no explanation of what was causing them or why. I admit, I may have missed part of the film, but i watched the majority of it and everything just seemed to happen of its own accord without any real concern for anything else. I cant recommend this film at all.', b'Mann photographs the Alberta Rocky Mountains in a superb fashion, and Jimmy Stewart and Walter Brennan give enjoyable performances as they always seem to do. <br /><br />But come on Hollywood - a Mountie telling the people of Dawson City, Yukon to elect themselves a marshal (yes a marshal!) and to enforce the law themselves, then gunfighters battling it out on the streets for control of the town? <br /><br />Nothing even remotely resembling that happened on the Canadian side of the border during the Klondike gold rush. Mr. Mann and company appear to have mistaken Dawson City for Deadwood, the Canadian North for the American Wild West.<br /><br />Canadian viewers be prepared for a Reefer Madness type of enjoyable howl with this ludicrous plot, or, to shake your head in disgust.', b'This is the kind of film for a snowy Sunday afternoon when the rest of the world can go ahead with its own business as you descend into a big arm-chair and mellow for a couple of hours. Wonderful performances from Cher and Nicolas Cage (as always) gently row the plot along. There are no rapids to cross, no dangerous waters, just a warm and witty paddle through New York life at its best. A family film in every sense and one that deserves the praise it received.', b'As others have mentioned, all the women that go nude in this film are mostly absolutely gorgeous. The plot very ably shows the hypocrisy of the female libido. When men are around they want to be pursued, but when no "men" are around, they become the pursuers of a 14 year old boy. And the boy becomes a man really fast (we should all be so lucky at this age!). He then gets up the courage to pursue his true love.', b"This is a film which should be seen by anybody interested in, effected by, or suffering from an eating disorder. It is an amazingly accurate and sensitive portrayal of bulimia in a teenage girl, its causes and its symptoms. The girl is played by one of the most brilliant young actresses working in cinema today, Alison Lohman, who was later so spectacular in 'Where the Truth Lies'. I would recommend that this film be shown in all schools, as you will never see a better on this subject. Alison Lohman is absolutely outstanding, and one marvels at her ability to convey the anguish of a girl suffering from this compulsive disorder. If barometers tell us the air pressure, Alison Lohman tells us the emotional pressure with the same degree of accuracy. Her emotional range is so precise, each scene could be measured microscopically for its gradations of trauma, on a scale of rising hysteria and desperation which reaches unbearable intensity. Mare Winningham is the perfect choice to play her mother, and does so with immense sympathy and a range of emotions just as finely tuned as Lohman's. Together, they make a pair of sensitive emotional oscillators vibrating in resonance with one another. This film is really an astonishing achievement, and director Katt Shea should be proud of it. The only reason for not seeing it is if you are not interested in people. But even if you like nature films best, this is after all animal behaviour at the sharp edge. Bulimia is an extreme version of how a tormented soul can destroy her own body in a frenzy of despair. And if we don't sympathise with people suffering from the depths of despair, then we are dead inside.", b'Okay, you have:<br /><br />Penelope Keith as Miss Herringbone-Tweed, B.B.E. (Backbone of England.) She\'s killed off in the first scene - that\'s right, folks; this show has no backbone!<br /><br />Peter O\'Toole as Ol\' Colonel Cricket from The First War and now the emblazered Lord of the Manor.<br /><br />Joanna Lumley as the ensweatered Lady of the Manor, 20 years younger than the colonel and 20 years past her own prime but still glamourous (Brit spelling, not mine) enough to have a toy-boy on the side. It\'s alright, they have Col. Cricket\'s full knowledge and consent (they guy even comes \'round for Christmas!) Still, she\'s considerate of the colonel enough to have said toy-boy her own age (what a gal!)<br /><br />David McCallum as said toy-boy, equally as pointlessly glamourous as his squeeze. Pilcher couldn\'t come up with any cover for him within the story, so she gave him a hush-hush job at the Circus.<br /><br />and finally:<br /><br />Susan Hampshire as Miss Polonia Teacups, Venerable Headmistress of the Venerable Girls\' Boarding-School, serving tea in her office with a dash of deep, poignant advice for life in the outside world just before graduation. Her best bit of advice: "I\'ve only been to Nancherrow (the local Stately Home of England) once. I thought it was very beautiful but, somehow, not part of the real world." Well, we can\'t say they didn\'t warn us.<br /><br />Ah, Susan - time was, your character would have been running the whole show. They don\'t write \'em like that any more. Our loss, not yours.<br /><br />So - with a cast and setting like this, you have the re-makings of "Brideshead Revisited," right?<br /><br />Wrong! They took these 1-dimensional supporting roles because they paid so well. After all, acting is one of the oldest temp-jobs there is (YOU name another!)<br /><br />First warning sign: lots and lots of backlighting. They get around it by shooting outdoors - "hey, it\'s just the sunlight!"<br /><br />Second warning sign: Leading Lady cries a lot. When not crying, her eyes are moist. That\'s the law of romance novels: Leading Lady is "dewy-eyed."<br /><br />Henceforth, Leading Lady shall be known as L.L.<br /><br />Third warning sign: L.L. actually has stars in her eyes when she\'s in love. Still, I\'ll give Emily Mortimer an award just for having to act with that spotlight in her eyes (I wonder . did they use contacts?)<br /><br />And lastly, fourth warning sign: no on-screen female character is "Mrs." She\'s either "Miss" or "Lady."<br /><br />When all was said and done, I still couldn\'t tell you who was pursuing whom and why. I couldn\'t even tell you what was said and done.<br /><br />To sum up: they all live through World War II without anything happening to them at all.<br /><br />OK, at the end, L.L. finds she\'s lost her parents to the Japanese prison camps and baby sis comes home catatonic. Meanwhile (there\'s always a "meanwhile,") some young guy L.L. had a crush on (when, I don\'t know) comes home from some wartime tough spot and is found living on the street by Lady of the Manor (must be some street if SHE\'s going to find him there.) Both war casualties are whisked away to recover at Nancherrow (SOMEBODY has to be "whisked away" SOMEWHERE in these romance stories!)<br /><br />Great drama.', b'The film is based on a genuine 1950s novel.<br /><br />Journalist Colin McInnes wrote a set of three "London novels": "Absolute Beginners", "City of Spades" and "Mr Love and Justice". I have read all three. The first two are excellent. The last, perhaps an experiment that did not come off. But McInnes\'s work is highly acclaimed; and rightly so. This musical is the novelist\'s ultimate nightmare - to see the fruits of one\'s mind being turned into a glitzy, badly-acted, soporific one-dimensional apology of a film that says it captures the spirit of 1950s London, and does nothing of the sort.<br /><br />Thank goodness Colin McInnes wasn\'t alive to witness it.', b'I really love the sexy action and sci-fi films of the sixties and its because of the actress\'s that appeared in them. They found the sexiest women to be in these films and it didn\'t matter if they could act (Remember "Candy"?). The reason I was disappointed by this film was because it wasn\'t nostalgic enough. The story here has a European sci-fi film called "Dragonfly" being made and the director is fired. So the producers decide to let a young aspiring filmmaker (Jeremy Davies) to complete the picture. They\'re is one real beautiful woman in the film who plays Dragonfly but she\'s barely in it. Film is written and directed by Roman Coppola who uses some of his fathers exploits from his early days and puts it into the script. I wish the film could have been an homage to those early films. They could have lots of cameos by actors who appeared in them. There is one actor in this film who was popular from the sixties and its John Phillip Law (Barbarella). Gerard Depardieu, Giancarlo Giannini and Dean Stockwell appear as well. I guess I\'m going to have to continue waiting for a director to make a good homage to the films of the sixties. If any are reading this, "Make it as sexy as you can"! I\'ll be waiting!', b'Sure, this one isn\'t really a blockbuster, nor does it target such a position. "Dieter" is the first name of a quite popular German musician, who is either loved or hated for his kind of acting and thats exactly what this movie is about. It is based on the autobiography "Dieter Bohlen" wrote a few years ago but isn\'t meant to be accurate on that. The movie is filled with some sexual offensive content (at least for American standard) which is either amusing (not for the other "actors" of course) or dumb - it depends on your individual kind of humor or on you being a "Bohlen"-Fan or not. Technically speaking there isn\'t much to criticize. Speaking of me I find this movie to be an OK-movie.'], dtype=object)>
처음 10개의 레이블도 출력해 보겠습니다.
<tf.Tensor: shape=(10,), dtype=int64, numpy=array([0, 0, 0, 1, 1, 1, 0, 0, 0, 0])>
신경망은 층을 쌓아서 만듭니다. 여기에는 세 개의 중요한 구조적 결정이 필요합니다:
- 어떻게 텍스트를 표현할 것인가?
- 모델에서 얼마나 많은 층을 사용할 것인가?
- 각 층에서 얼마나 많은 은닉 유닛(hidden unit)을 사용할 것인가?
이 예제의 입력 데이터는 문장으로 구성됩니다. 예측할 레이블은 0 또는 1입니다.
텍스트를 표현하는 한 가지 방법은 문장을 임베딩(embedding) 벡터로 바꾸는 것입니다. 그러면 첫 번째 층으로 사전 훈련(pre-trained)된 텍스트 임베딩을 사용할 수 있습니다. 여기에는 두 가지 장점이 있습니다.
- 텍스트 전처리에 대해 신경 쓸 필요가 없습니다.
- 전이 학습의 장점을 이용합니다.
- 임베딩은 고정 크기이기 때문에 처리 과정이 단순해집니다.
테스트해 볼 수 있는 사전 훈련된 모델이 세 개 더 있습니다:
- google/tf2-preview/gnews-swivel-20dim-with-oov/1 - google/tf2-preview/gnews-swivel-20dim/1와 동일하지만 어휘 사전(vocabulary)의 2.5%가 OOV 버킷(bucket)으로 변환되었습니다. 이는 해당 문제의 어휘 사전과 모델의 어휘 사전이 완전히 겹치지 않을 때 도움이 됩니다.
- google/tf2-preview/nnlm-en-dim50/1 - 더 큰 모델입니다. 차원 크기는 50이고 어휘 사전의 크기는 1백만 개 이하입니다.
- google/tf2-preview/nnlm-en-dim128/1 - 훨씬 더 큰 모델입니다. 차원 크기는 128이고 어휘 사전의 크기는 1백만 개 이하입니다.
먼저 문장을 임베딩시키기 위해 텐서플로 허브 모델을 사용하는 케라스 층을 만들어 보죠. 그다음 몇 개의 샘플을 입력하여 테스트해 보겠습니다. 입력 텍스트의 길이에 상관없이 임베딩의 출력 크기는
(num_examples, embedding_dimension)가 됩니다.
embedding = "https://tfhub.dev/google/tf2-preview/gnews-swivel-20dim/1" hub_layer = hub.KerasLayer(embedding, input_shape=, dtype=tf.string, trainable=True) hub_layer(train_examples_batch[:3])
<tf.Tensor: shape=(3, 20), dtype=float32, numpy= array([[ 1.765786 , -3.882232 , 3.9134233 , -1.5557289 , -3.3362343 , -1.7357955 , -1.9954445 , 1.2989551 , 5.081598 , -1.1041286 , -2.0503852 , -0.72675157, -0.65675956, 0.24436149, -3.7208383 , 2.0954835 , 2.2969332 , -2.0689783 , -2.9489717 , -1.1315987 ], [ 1.8804485 , -2.5852382 , 3.4066997 , 1.0982676 , -4.056685 , -4.891284 , -2.785554 , 1.3874227 , 3.8476458 , -0.9256538 , -1.896706 , 1.2113281 , 0.11474707, 0.76209456, -4.8791065 , 2.906149 , 4.7087674 , -2.3652055 , -3.5015898 , -1.6390051 ], [ 0.71152234, -0.6353217 , 1.7385626 , -1.1168286 , -0.5451594 , -1.1808156 , 0.09504455, 1.4653089 , 0.66059524, 0.79308075, -2.2268345 , 0.07446612, -1.4075904 , -0.70645386, -1.907037 , 1.4419787 , 1.9551861 , -0.42660055, -2.8022065 , 0.43727064]], dtype=float32)>
이제 전체 모델을 만들어 보겠습니다:
model = tf.keras.Sequential() model.add(hub_layer) model.add(tf.keras.layers.Dense(16, activation='relu')) model.add(tf.keras.layers.Dense(1)) model.summary()
Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= keras_layer (KerasLayer) (None, 20) 400020 _________________________________________________________________ dense (Dense) (None, 16) 336 _________________________________________________________________ dense_1 (Dense) (None, 1) 17 ================================================================= Total params: 400,373 Trainable params: 400,373 Non-trainable params: 0 _________________________________________________________________
순서대로 층을 쌓아 분류기를 만듭니다:
- 첫 번째 층은 텐서플로 허브 층입니다. 이 층은 사전 훈련된 모델을 사용하여 하나의 문장을 임베딩 벡터로 매핑합니다. 여기서 사용하는 사전 훈련된 텍스트 임베딩 모델(google/tf2-preview/gnews-swivel-20dim/1)은 하나의 문장을 토큰(token)으로 나누고 각 토큰의 임베딩을 연결하여 반환합니다. 최종 차원은
- 이 고정 크기의 출력 벡터는 16개의 은닉 유닛(hidden unit)을 가진 완전 연결 층(
- 마지막 층은 하나의 출력 노드를 가진 완전 연결 층입니다.
sigmoid활성화 함수를 사용하므로 확률 또는 신뢰도 수준을 표현하는 0~1 사이의 실수가 출력됩니다.
이제 모델을 컴파일합니다.
손실 함수와 옵티마이저
모델이 훈련하려면 손실 함수(loss function)과 옵티마이저(optimizer)가 필요합니다. 이 예제는 이진 분류 문제이고 모델이 확률을 출력하므로(출력층의 유닛이 하나이고
sigmoid 활성화 함수를 사용합니다),
binary_crossentropy 손실 함수를 사용하겠습니다.
다른 손실 함수를 선택할 수 없는 것은 아닙니다. 예를 들어
mean_squared_error를 선택할 수 있습니다. 하지만 일반적으로
binary_crossentropy가 확률을 다루는데 적합합니다. 이 함수는 확률 분포 간의 거리를 측정합니다. 여기에서는 정답인 타깃 분포와 예측 분포 사이의 거리입니다.
나중에 회귀(regression) 문제(예를 들어 주택 가격을 예측하는 문제)에 대해 살펴 볼 때 평균 제곱 오차(mean squared error) 손실 함수를 어떻게 사용하는지 알아 보겠습니다.
이제 모델이 사용할 옵티마이저와 손실 함수를 설정해 보죠:
model.compile(optimizer='adam', loss=tf.keras.losses.BinaryCrossentropy(from_logits=True), metrics=['accuracy'])
이 모델을 512개의 샘플로 이루어진 미니배치(mini-batch)에서 20번의 에포크(epoch) 동안 훈련합니다.
y_train 텐서에 있는 모든 샘플에 대해 20번 반복한다는 뜻입니다. 훈련하는 동안 10,000개의 검증 세트에서 모델의 손실과 정확도를 모니터링합니다:
history = model.fit(train_data.shuffle(10000).batch(512), epochs=20, validation_data=validation_data.batch(512), verbose=1)
Epoch 1/20 30/30 [==============================] - 2s 61ms/step - loss: 1.1185 - accuracy: 0.5013 - val_loss: 0.7317 - val_accuracy: 0.5039 Epoch 2/20 30/30 [==============================] - 2s 56ms/step - loss: 0.7027 - accuracy: 0.5207 - val_loss: 0.6865 - val_accuracy: 0.5415 Epoch 3/20 30/30 [==============================] - 2s 55ms/step - loss: 0.6602 - accuracy: 0.5534 - val_loss: 0.6416 - val_accuracy: 0.5861 Epoch 4/20 30/30 [==============================] - 2s 56ms/step - loss: 0.6171 - accuracy: 0.6216 - val_loss: 0.6062 - val_accuracy: 0.6431 Epoch 5/20 30/30 [==============================] - 2s 55ms/step - loss: 0.5824 - accuracy: 0.6652 - val_loss: 0.5756 - val_accuracy: 0.6795 Epoch 6/20 30/30 [==============================] - 2s 55ms/step - loss: 0.5489 - accuracy: 0.6983 - val_loss: 0.5448 - val_accuracy: 0.7014 Epoch 7/20 30/30 [==============================] - 2s 55ms/step - loss: 0.5142 - accuracy: 0.7288 - val_loss: 0.5135 - val_accuracy: 0.7255 Epoch 8/20 30/30 [==============================] - 2s 55ms/step - loss: 0.4784 - accuracy: 0.7600 - val_loss: 0.4831 - val_accuracy: 0.7600 Epoch 9/20 30/30 [==============================] - 2s 55ms/step - loss: 0.4418 - accuracy: 0.7885 - val_loss: 0.4534 - val_accuracy: 0.7772 Epoch 10/20 30/30 [==============================] - 2s 56ms/step - loss: 0.4071 - accuracy: 0.8095 - val_loss: 0.4261 - val_accuracy: 0.7956 Epoch 11/20 30/30 [==============================] - 2s 55ms/step - loss: 0.3735 - accuracy: 0.8314 - val_loss: 0.4010 - val_accuracy: 0.8113 Epoch 12/20 30/30 [==============================] - 2s 56ms/step - loss: 0.3412 - accuracy: 0.8518 - val_loss: 0.3784 - val_accuracy: 0.8248 Epoch 13/20 30/30 [==============================] - 2s 56ms/step - loss: 0.3125 - accuracy: 0.8676 - val_loss: 0.3604 - val_accuracy: 0.8399 Epoch 14/20 30/30 [==============================] - 2s 55ms/step - loss: 0.2869 - accuracy: 0.8791 - val_loss: 0.3461 - val_accuracy: 0.8488 Epoch 15/20 30/30 [==============================] - 2s 56ms/step - loss: 0.2638 - accuracy: 0.8915 - val_loss: 0.3324 - val_accuracy: 0.8514 Epoch 16/20 30/30 [==============================] - 2s 56ms/step - loss: 0.2420 - accuracy: 0.9015 - val_loss: 0.3236 - val_accuracy: 0.8514 Epoch 17/20 30/30 [==============================] - 2s 57ms/step - loss: 0.2212 - accuracy: 0.9102 - val_loss: 0.3158 - val_accuracy: 0.8645 Epoch 18/20 30/30 [==============================] - 2s 55ms/step - loss: 0.2033 - accuracy: 0.9207 - val_loss: 0.3094 - val_accuracy: 0.8654 Epoch 19/20 30/30 [==============================] - 2s 55ms/step - loss: 0.1882 - accuracy: 0.9284 - val_loss: 0.3059 - val_accuracy: 0.8645 Epoch 20/20 30/30 [==============================] - 2s 56ms/step - loss: 0.1748 - accuracy: 0.9354 - val_loss: 0.3042 - val_accuracy: 0.8651
모델의 성능을 확인해 보죠. 두 개의 값이 반환됩니다. 손실(오차를 나타내는 숫자이므로 낮을수록 좋습니다)과 정확도입니다.
results = model.evaluate(test_data.batch(512), verbose=2) for name, value in zip(model.metrics_names, results): print("%s: %.3f" % (name, value))
49/49 - 1s - loss: 0.3185 - accuracy: 0.8571 loss: 0.319 accuracy: 0.857
이 예제는 매우 단순한 방식을 사용하므로 87% 정도의 정확도를 달성했습니다. 고급 방법을 사용한 모델은 95%에 가까운 정확도를 얻습니다.
문자열 입력을 다루는 조금 더 일반적인 방법과 훈련 과정에서 정확도나 손실 변화에 대한 자세한 분석은 여기를 참고하세요.
# # 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.