![](https://habrastorage.org/getpro/habr/upload_files/bfe/a2f/c11/bfea2fc11a4e95d20a7f20598c502803.png)
![](https://habrastorage.org/getpro/habr/upload_files/c3c/29d/6a2/c3c29d6a21b2db5f985f37973f0f7968.jpg)
Автор статьи: Рустем Галиев
IBM Senior DevOps Engineer & Integration Architect
Привет, Хабр! На связи Рустем, IBM Senior DevOps Engineer & Integration Architect.
Сегодня мы будем работать с открытым набором данных по рентгенографии грудной клетки которые, использовали для этого исследования, с предварительно обученной моделью MobileNet_v2 для классификации изображений TensorFlow и переносом обучения для создания классификатора пневмонии, который работает с рентгенограммами грудной клетки.
Целью этой статьи является не столько получение навыков классификации изображений, а сколько понимание того, насколько легко вы можете создать соответствующую модель.
Я надеюсь, вам понравится это!
Загрузка модулей и данных
Я знаю, вы хотите сразу приступить к разработке методов обнаружения пневмонии, но сначала давайте импортируем некоторые необходимые модули:
import numpy as np
import os
import pathlib
import matplotlib.pylab as plt
import tensorflow as tf
import tensorflow_hub as hub
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
from tqdm import tqdm
AUTOTUNE = tf.data.experimental.AUTOTUNE
Теперь давайте загрузим данные в память.
data_dir = pathlib.Path('tflite/images')
image_count_train = len(list(data_dir.glob('train/*/*.jpeg')))
image_count_test = len(list(data_dir.glob('test/*/*.jpeg')))
image_count_val = len(list(data_dir.glob('val/*/*.jpeg')))
BATCH_SIZE = 32
IMG_HEIGHT = 224
IMG_WIDTH = 224
IMG_SHAPE = (IMG_HEIGHT, IMG_WIDTH, 3)
STEPS_PER_EPOCH = np.ceil(image_count_train/BATCH_SIZE)
EPOCHS = 10
SAVED_MODEL = "pneumonia_saved_model"
(image_count_test, image_count_train, image_count_val)
Теперь, когда у нас есть все изображения BATCH_SIZE для обучения, мы настроим классы для обучения (метки):
CLASS_NAMES = np.array([item.name for item in data_dir.glob('train/*') if item.name != "LICENSE.txt"])
num_classes = len(CLASS_NAMES)
CLASS_NAMES
Изображения находятся в папках train, test или val. Например, чтобы увидеть изображение, вам нужно выполнить:
pneumonia = list(data_dir.glob('train/PNEUMONIA/*.jpeg'))
for image_path in pneumonia[:3]:
Image.open(str(image_path))
Создание наборов данных
Теперь, когда все загружено, мы можем создать итераторы, которые будут давать каждому набор изображений для обучения. TensorFlow предлагает отличные утилиты для этой задачи в наборах данных TensorFlow:
test_ds = tf.data.Dataset.list_files(str(data_dir/'test/*/*'))
train_ds = tf.data.Dataset.list_files(str(data_dir/'train/*/*'))
val_ds = tf.data.Dataset.list_files(str(data_dir/'val/*/*'))
for f in test_ds.take(5):
print(f.numpy())
print('Datasets loaded')
На данном этапе мы видим, что у нас есть итераторы для каждой группы: обучение, тестирование и проверка. Теперь нам нужно сделать несколько вещей:
Мы должны декодировать каждое изображение в каналы RGB.
Мы должны изменить размер каждого изображения до наших предопределенных размеров.
Для каждого изображения мы должны вычислить метку, дающую 1 одному классу и 0 другому.
Мы должны применить каждый из этих шагов к итераторам, чтобы мы могли перебирать изображения с измененными размерами с их метками.
Для этих задач мы будем использовать следующие ютилити методы:
def get_label(file_path):
parts = tf.strings.split(file_path, os.path.sep)
return parts[-2] == CLASS_NAMES[0]
def decode_img(img):
img = tf.image.decode_jpeg(img, channels=3)
img = tf.image.convert_image_dtype(img, tf.float32)
return tf.image.resize(img, [IMG_WIDTH, IMG_HEIGHT])
def process_path(file_path):
label = get_label(file_path)
img = tf.io.read_file(file_path)
img = decode_img(img)
return img, label
def format_image(image, label):
image = tf.image.resize(image, IMAGE_SIZE) / 255.0
return image, label
print('Utility methods loaded!')
Применив это к нашим итераторам, теперь мы можем итерировать тестовые примеры, чтобы они имели соответствующие шейпы:
train_examples = train_ds.map(process_path, num_parallel_calls=AUTOTUNE)
test_examples = test_ds.map(process_path, num_parallel_calls=AUTOTUNE)
validation_examples = val_ds.map(process_path, num_parallel_calls=AUTOTUNE)
for image, label in test_examples.take(5):
print("Image shape: ", image.numpy().shape)
print("Label: ", label.numpy())
print('Check the shapes!')
Интересно отметить, что теперь каждая итерация набора данных выдает тензорное изображение и тензорную метку.
Теперь, на этом шаге, нам нужно пакетировать каждый набор данных, добавить кэш для повышения производительности и выполнить предварительную выборку по мере необходимости! Этот метод обычно повышает производительность пакетной обработки в 10 раз и взят непосредственно из учебника Google:
def prepare_for_training(ds, cache=True, shuffle_buffer_size=1000):
if cache:
if isinstance(cache, str):
ds = ds.cache(cache)
else:
ds = ds.cache()
ds = ds.shuffle(buffer_size=shuffle_buffer_size)
ds = ds.repeat()
ds = ds.batch(BATCH_SIZE)
ds = ds.prefetch(buffer_size=AUTOTUNE)
return ds
train_examples_dataset = prepare_for_training(train_examples)
test_examples_dataset = prepare_for_training(test_examples)
validation_examples_dataset = prepare_for_training(validation_examples)
Можем сделать вызовimage_batch, label_batch = next(iter(test_examples_dataset))
Приступим к определению модели!
Обучение модели
Теперь, когда у нас есть итератор, все дело в модели!
Все, что нам нужно сделать, это поместить линейный классификатор поверх слоя feature_extractor_layer
с помощью модуля Hub.
Для скорости мы начинаем с необучаемого feature_extractor_layer, но вы также можете включить тонкую настройку для большей точности.
Модули-концентраторы для TensorFlow 1.x здесь не будут работать, поэтому мы можем использовать один из следующих вариантов:
module_selection = ("mobilenet_v2", 224, 1280) #or use ["(\"mobilenet_v2\", 224, 1280)", "(\"inception_v3\", 299, 2048)"] {type:"raw", allow-input: true}
handle_base, pixels, FV_SIZE = module_selection
MODULE_HANDLE ="https://tfhub.dev/google/tf2-preview/{}/feature_vector/4".format(handle_base)
IMAGE_SIZE = (pixels, pixels)
print("Using {} with input size {} and output dimension {}".format(MODULE_HANDLE, IMAGE_SIZE, FV_SIZE))
Обратите внимание, что мы используем вектор признаков, а не полную модель. Это потому, что мы не хотим тонкой настройки (для избежания проблем со временем). Однако, если вы хотите выполнить точную настройку, загрузите полную модель (она находится в TensorFlow Hub).
Загрузите модуль TFHub:
feature_extractor = hub.KerasLayer(MODULE_HANDLE,
input_shape=IMAGE_SIZE + (3,),
output_shape=[FV_SIZE],
trainable=False)
feature_extractor.trainable = False
print("Building model with", MODULE_HANDLE)
model = tf.keras.Sequential([ feature_extractor, tf.keras.layers.Dense(num_classes, activation='softmax')])
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
model.summary()
Как видите, большинство наших параметров не поддаются обучению (см. параметр из MobileNet), поэтому обучение должно быть быстрым. Для обучения нам потребуется запустить следующее:
hist = model.fit(train_examples_dataset, epochs=EPOCHS, steps_per_epoch=image_count_train/BATCH_SIZE, validation_steps=np.floor(image_count_val/BATCH_SIZE), validation_data=validation_examples_dataset)
tf.saved_model.save(model, SAVED_MODEL)
Мы можем проверить, что модель имеет правильную подпись, загрузив ее снова и показав информацию:
loaded = tf.saved_model.load(SAVED_MODEL)
print(list(loaded.signatures.keys()))
infer = loaded.signatures["serving_default"]
print(infer.structured_input_signature)
print(infer.structured_outputs)
Мы используем следующую команду, чтобы проверить, можете ли вы также использовать интерфейс командной строки TensorFlow для проверки подписи (вне Python):saved_model_cli show --dir $1 --tag_set serve --signature_def serving_default
Это было невероятно легко; с трансферным обучением мы можем легко, в четыре строки кода, проделать работу целых исследовательских групп!
Теперь, когда у нас есть рабочая модель, давайте перейдем к работе по ее оптимизации.
Преобразование классификатора пневмонии TensorFlow в TensorFlow Lite с помощью квантования
Мы можем легко преобразовать модель из обычного TensorFlow в TensorFlow Lite с помощью Python Converter API. Этот шаг необходим для запуска наших моделей на периферийных и мобильных устройствах.
Квантование с помощью конвертера TensorFlow Lite
Теперь, когда у нас есть сохраненный объект SavedModel, первое, что вам нужно сделать, чтобы преобразовать его в модель TensorFlow Lite, — создать экземпляр преобразователя:
import numpy as np
import os
import pathlib
import matplotlib.pylab as plt
import tensorflow as tf
import tensorflow_hub as hub
import tensorflow_datasets as tfds
tfds.disable_progress_bar()
from tqdm import tqdm
AUTOTUNE = tf.data.experimental.AUTOTUNE
SAVED_MODEL = "pneumonia_saved_model"
converter = tf.lite.TFLiteConverter.from_saved_model(SAVED_MODEL)
Помните, что мы можем создать конвертер из моделей SavedModel, ConcreteFunction или Keras!
Квантование после обучения
Простейшая форма квантования после обучения квантует от плавающей запятой до 8-битной точности. Этот метод включен в качестве опции в конвертере TensorFlow Lite. При выводе вес преобразуются из 8-битной точности в числа с плавающей запятой и вычисляются с использованием ядер с плавающей запятой. Это преобразование выполняется один раз и кэшируется для уменьшения задержки.
converter.optimizations = [tf.lite.Optimize.DEFAULT]
Эта оптимизация была сделана путем размышления между оптимизацией размера и задержки. Если бы мы хотели оптимизировать только размер, мы могли бы сделать следующее:
converter.optimizations = [tf.lite.Optimize.OPTIMIZE_FOR_SIZE]
Точно так же мы можем преобразовать нашу модель, и она будет квантована:
tflite_model = converter.convert()
tflite_model_file = 'converted_model.tflite'
with open(tflite_model_file, "wb") as f:
f.write(tflite_model)
print('Done quantizing')
Это было удивительно просто и быстро, хотя квантование модели звучит сложно и красиво на бэкэнде, но ее весьма легко реализовать.
Проверка уменьшения размера
Давайте проверим, что квантованная модель действительно меньше:
from pathlib import Path
saved_model = Path(SAVED_MODEL)
full_model_size = sum(f.stat().st_size for f in saved_model.glob('**/*') if f.is_file() )/(1024*1024)
print(f'Full model size {full_model_size} MB')
converted_model = Path(tflite_model_file)
converted_model_size = converted_model.stat().st_size / (1024*1024)
print(f'Converted model size {converted_model_size} MB')
![](https://habrastorage.org/getpro/habr/upload_files/5b2/511/9a5/5b25119a5c36395ea8561a233b19d360.png)
Мы видим, что за одно простое квантование мы увеличили размер почти на 80%
Мы можем добиться дальнейшего улучшения задержки, сокращения пикового использования памяти и доступа к аппаратным ускорителям только для целых чисел, убедившись, что вся математика модели квантована. Для этого нам нужно измерить динамический диапазон активаций и входов с репрезентативным набором данных. Таким образом, вы просто создадите генератор входных данных и предоставите его вашему конвертеру:
Для этого сначала вернём наш тестовый набор данных:
def get_label(file_path):
parts = tf.strings.split(file_path, os.path.sep)
return parts[-2] == CLASS_NAMES[0]
@tf.autograph.experimental.do_not_convert
def decode_img(img):
img = tf.image.decode_jpeg(img, channels=3)
img = tf.image.convert_image_dtype(img, tf.float32)
return tf.image.resize(img, [IMG_WIDTH, IMG_HEIGHT])
@tf.autograph.experimental.do_not_convert
def process_path(file_path):
label = get_label(file_path)
img = tf.io.read_file(file_path)
img = decode_img(img)
return img, label
def format_image(image, label):
image = tf.image.resize(image, IMAGE_SIZE) / 255.0
return image, label
def prepare_for_training(ds, cache=True, shuffle_buffer_size=1000):
if cache:
if isinstance(cache, str):
ds = ds.cache(cache)
else:
ds = ds.cache()
ds = ds.shuffle(buffer_size=shuffle_buffer_size)
ds = ds.repeat()
ds = ds.batch(BATCH_SIZE)
ds = ds.prefetch(buffer_size=AUTOTUNE)
return ds
data_dir = pathlib.Path('tflite/images')
BATCH_SIZE = 32
IMG_HEIGHT = 224
IMG_WIDTH = 224
IMG_SHAPE = (IMG_HEIGHT, IMG_WIDTH, 3)
CLASS_NAMES = np.array([item.name for item in data_dir.glob('train/*') if item.name != "LICENSE.txt"])
test_ds = tf.data.Dataset.list_files(str(data_dir/'test/*/*'))
test_examples = test_ds.map(process_path, num_parallel_calls=AUTOTUNE)
test_examples_dataset = prepare_for_training(test_examples)
А теперь давайте определим репрезентативный набор данных. Это будет настроено на то, чтобы преобразователь выполнял некоторые выводы по мере квантования, чтобы поддерживать как можно большую точность, а также преобразовывать все возможные веса и активации в INT8:
def representative_data_gen():
for image_batch, label_batch in test_examples_dataset.take(1):
for image in image_batch:
yield [[image]]
len(list(representative_data_gen()))
converter.representative_dataset = representative_data_gen
Результирующая модель будет полностью квантована, но для удобства по-прежнему будет принимать входные и выходные данные с плавающей запятой.
Операции, которые не имеют квантованных реализаций, автоматически останутся с плавающей запятой. Это позволяет выполнять преобразование гладко, но может ограничивать развертывание ускорителями, поддерживающими float.
Полноцелочисленное квантование (необязательно, просто для знания)
Чтобы преобразователь выдавал только целочисленные операции, можно указать:converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
Однако имейте в виду, что если преобразователь не может найти поддерживаемую INT8-совместимую операцию с вашей моделью, он не будет работать. Этот шаг обычно необязателен, но в некоторых случаях, например при развертывании на TPU, он необходим, поскольку это оборудование поддерживает только операции INT8.
Преобразование и проверка модели
Наконец, давайте преобразуем нашу модель:
tflite_model = converter.convert()
tflite_model_file = 'converted_model_int8.tflite'
with open(tflite_model_file, "wb") as f:
f.write(tflite_model)
print('Done quantizing with Representative Dataset')
Сравнение размеров
Можно подумать, что новая переделанная модель меньше, но это не всегда так. Преобразование в операции INT8 в значительной степени сосредоточено на требованиях к памяти и скорости:
from pathlib import Path
quantized_weights = Path('converted_model.tflite')
weights_quantized_size = quantized_weights.stat().st_size/(1024*1024)
print(f'Quantized for weights model size {weights_quantized_size} MB')
weights_and_activations_model = Path('converted_model_int8.tflite')
weights_and_activations_model_size = weights_and_activations_model.stat().st_size/(1024*1024)
print(f'Quantized for weights and activations size {weights_and_activations_model_size} MB')
![](https://habrastorage.org/getpro/habr/upload_files/714/431/c5a/714431c5a04434c45615d9a3f73b1735.png)
И мы видим, что оба размера одинаковы. На следующем этапе мы рассмотрим, что происходит со скоростью!
Протестируем модель TensorFlow Lite с помощью интерпретатора Python.
Теперь, когда у нас есть наши квантованные модели, мы можем протестировать их и проверить их точность!
Во-первых, давайте загрузим квантованную модель весов. Для этого нам нужно выделить тензоры для прогнозов:
weights_tflite_model_file = 'converted_model.tflite'
interpreter = tf.lite.Interpreter(model_path=weights_tflite_model_file)
interpreter.allocate_tensors()
input_index = interpreter.get_input_details()[0]["index"]
output_index = interpreter.get_output_details()[0]["index"]
Теперь давайте создадим простую партию из 15 изображений (из соображений производительности) и проверим ее показатели:
import time
start_time = time.time()
predictions = []
test_labels, test_imgs = [], []
debug = 0
image_batch, label_batch = next(iter(test_examples_dataset))
for img, label in zip(image_batch, label_batch):
debug += 1
if debug % 5 == 1:
print(f'I am treating image {debug} with label {label}')
if debug == 15:
break
interpreter.set_tensor(input_index, np.array([img]))
interpreter.invoke()
predictions.append(interpreter.get_tensor(output_index))
test_labels.append(label.numpy())
test_imgs.append(img)
print(f'Predictions calculated in {time.time() - start_time} seconds')
![](https://habrastorage.org/getpro/habr/upload_files/252/a6a/ec4/252a6aec4d00041735380e9dddd5ba46.png)
Теперь у нас есть все прогнозы. Рассчитаем точность, чувствительность и специфичность:
ok_value = 0
wrong_value = 0
true_positives = 0
total = 0
true_negatives = 0
false_positives = 0
false_negatives = 0
for predictions_array, true_label in zip(predictions, test_labels):
predicted_label = np.argmax(predictions_array)
if predicted_label == true_label:
ok_value += 1
if CLASS_NAMES[int(true_label)] == 'NORMAL':
true_negatives += 1
else:
true_positives += 1
else:
wrong_value += 1
if CLASS_NAMES[predicted_label] == 'NORMAL':
false_negatives +=1
else:
false_positives += 1
total += 1
print(f'Accuracy: {(true_positives + true_negatives) / total} \n ')
print(f'Sensitivity: {true_positives/ (true_positives + false_negatives)} \n ')
print(f'Specificity: {true_negatives / (true_negatives + false_positives)}')
![](https://habrastorage.org/getpro/habr/upload_files/006/349/dcc/006349dccbfa1b7fc2de5e4b21a5741f.png)
Наша модель очень хороша: ее, наверное, можно было бы улучшить, но получить такой результат за 30 минут — это очень хорошо!
Теперь давайте проверим квантованную модель весов и активаций:
weights_tflite_model_file = 'converted_model_int8.tflite'
interpreter = tf.lite.Interpreter(model_path=weights_tflite_model_file)
interpreter.allocate_tensors()
input_index = interpreter.get_input_details()[0]["index"]
output_index = interpreter.get_output_details()[0]["index"]
Как и раньше, давайте создадим простую партию из 15 изображений (опять же, из соображений производительности) и проверим ее метрики:
import time
start_time = time.time()
predictions = []
test_labels, test_imgs = [], []
debug = 0
image_batch, label_batch = next(iter(test_examples_dataset))
for img, label in zip(image_batch, label_batch):
debug += 1
if debug % 5 == 1:
print(f'I am treating image {debug} with label {label}')
if debug == 15:
break
interpreter.set_tensor(input_index, np.array([img]))
interpreter.invoke()
predictions.append(interpreter.get_tensor(output_index))
test_labels.append(label.numpy())
test_imgs.append(img)
print(f'Predictions calculated in {time.time() - start_time} seconds')
![](https://habrastorage.org/getpro/habr/upload_files/1e2/311/1a3/1e23111a3ee54c7158cdcb9596801924.png)
Теперь у нас есть все прогнозы. Рассчитаем точность, чувствительность и специфичность:
ok_value = 0
wrong_value = 0
true_positives = 0
total = 0
true_negatives = 0
false_positives = 0
false_negatives = 0
for predictions_array, true_label in zip(predictions, test_labels):
predicted_label = np.argmax(predictions_array)
if predicted_label == true_label:
ok_value += 1
if CLASS_NAMES[int(true_label)] == 'NORMAL':
true_negatives += 1
else:
true_positives += 1
else:
wrong_value += 1
if CLASS_NAMES[predicted_label] == 'NORMAL':
false_negatives +=1
else:
false_positives += 1
total += 1
print(f'Accuracy: {(true_positives + true_negatives) / total} \n ')
print(f'Sensitivity: {true_positives/ (true_positives + false_negatives)} \n ')
print(f'Specificity: {true_negatives / (true_negatives + false_positives)}')
![](https://habrastorage.org/getpro/habr/upload_files/81b/805/f95/81b805f9575b5b43e2eaa92796e9ab27.png)
Мы видим, что, хотя модель с оптимизацией весов немного меньше и лучше, модель INT8 намного быстрее. Идея квантования INT8 состоит в том, чтобы потерять точность (немного) для увеличения скорости.
Резюмируя:
Мы загрузили набор рентгеновских данных из репозитория лаборатории.
Мы создали данные наборы данных с помощью наборов данных TensorFlow, которые создают итераторы из наших изображений.
Мы узнали, как адаптировать эти наборы данных, сопоставив несколько методов, которые позволяли переформатировать изображения до 224 x 224 x 3 и возвращали правильную метку, причем все в пакетном режиме.
Мы создали, обучили и сохранили нашу модель с передачей обучения, используя MobileNet v2 и простой слой softmax над ним.
Мы квантовали сохраненную модель классификации пневмонии для весов в качестве оптимизации после обучения.
Мы подтвердили, что мы увеличиваем размер на 80%, просто делая это.
Мы узнали о квантовании активаций с репрезентативным набором данных и о квантовании с полным целым числом.
Мы оценили обе модели, чтобы проверить производительность.
Так как статья подготовлена в преддверии старта курса Machine Learning. Professional, хочу пригласить всех на бесплатный урок курса, где преподаватели OTUS расскажут какие подходы к ансамблированию сегодня существуют в машинном обучении, как устроены такие популярные техники ансамблирования как Bagging, Random Forest и Gradient Boosting. Когда и как их стоит применять для решения ML-задач.