Автоэнкодеры для удаления шумов с изображений

c4aeb09a8d46dbfa1ef15fa006844e87.png

17223858c3f53787c786a56ef55ec691.jpgАвтор статьи: Виктория Ляликова

Все глубже погружаясь в машинное и глубокое обучение меня очень заинтересовала тема автоэнкодеров, особенно с точки зрения удаления шумов. Поиски различной интересующей меня информации дали свои результаты, но, к сожалению, почти везде работа автоэнкодеров рассматривается на примере очень популярного набора данныx MNIST. Коллекция изображений данного набора имеет размер 28×28, данные разделены на наборы для обучения и тестирования. Однако, хотелось бы увидеть как работают автоэнкодеры на практике на более реальных изображениях. И прежде, чем приступать к практике, давайте сначала немного вспомним, что из себя представляют автоэнкодеры.

Автоэнкодеры — специальная архитектура искусственных нейронных сетей, которая обучена, чтобы копировать свои входные данные в выходные. Предположим, что мы имеем изображение, автоэнкодер сначала кодирует это изображение в представление более низкой размерности, а затем декодирует представление обратно в изображение.

cd8eb86d8738a297246eec3d11440044.png

Как можно увидеть из рисунка, автоэнкодер имеет 3 основных компонента:

  1. Энкодер или кодировщик — сжимает данные в представление более низкой размерности.

  2. Код — внутренний скрытый слой, часть архитектуры, которая представляет сжатые данные, которые затем отправляются в декодер.

  3. Декодер — распаковывает или восстанавливает данные из низкого и представления до их исходного размера.

Для того, чтобы создать модель автоэнкодера необходимы две разные функции: кодировщик и декодер. Обычно эти две функции реализуются с помощью нейронных сетей, причем часто при работе с изображениями обычно используют сверточные нейронные сети, а при работе, например, с текстом можно использовать сети долгой краткосрочной памяти LSTM

Теперь обратимся к математике. Энкодер переводит входной сигнал в его представление (код) вида h=g(x), где x — наш входной вектор, g — энкодер. Далее декодер восстанавливает сигнал по его коду x=f(h), f — декодер. Задача автоэнкодера — минимизировать функционал ошибки L(x, f(g(x)). При этом семейства функций g и f ограничены, чтобы автоэнкодер был вынужден отбирать наиболее важные свойства сигнала.

Для того, чтобы приступать к обучению автоэнкодера сначала необходимо определить следующие параметры:

  1. Размер кода. Чем меньше размер кода, тем больше сжатие.

  2. Количество слоев в энкодере и декодере. Глубины слоев могут достигать любого уровня.

  3. Количество узлов на уровне. Обычно количество узлов на уровне уменьшается с каждым последующим уровнем энкодера, а затем снова начинает увеличиваться с каждым последующим уровнем декодера. т. е. декодер симметричен структуре кодировщика, но это не является обязательным требованием.

  4. Функция потерь. Самыми популярными вариантами являются среднеквадратическая ошибка (MSE) или двоичная кросс‑энтропия.

Для моделирования нейронных сетей в python очень удобно использовать высокоуровневую библиотеку Keras, которая является оболочкой над Tensorflow.

В Keras для построения моделей нейронных сетей (models) мы собираем слои (layers). Для описания стандартных архитектур нейронных сетей в Keras уже существуют предопределенные классы для слоев:

  • Dense() — полносвязный слой;

  • Conv1D, Conv2D, Conv3D — сверточные слои;

  • Conv2DTranspose, Conv3DTranspose — транспонированные (обратные) сверточные слои;

  • SimpleRNN, LSTM, GRU — рекуррентные слои;

  • MaxPooling2D, UpSampling2D, Dropout, BatchNormalization — вспомогательные слои

Теперь можно приступать к построению автоэнкодера для шумоподавления изображений…

Для реализации этой задачи воспользуемся набором данных, содержащим изображения МРТ головного мозга. Сначала загрузим необходимые библиотеки:

import imutils
import cv2
import os
from imutils import paths
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
from keras.layers import Input, Conv2D, MaxPool2D, UpSampling2D
from keras.models import Model
from sklearn.model_selection import train_test_split

Данные разбиты на 2 папки, в одной папке содержатся изображения, не имеющие аномалий, а во второй имеющие опухоли головного мозга. Всего 253 изображения. Пока загрузим по 1 изображению из каждой папки и посмотрим на них…

image=cv2.imread('D:/*****/brain_tumor_dataset/no/23 no.jpg')
image=cv2.imread('D:/*****/brain_tumor_dataset/no/10 no.jpg')
plt.imshow(image)

1230f52348abc5394f06cc3a1492f421.png

Видим, что высота первого изображения 338, ширина — 276, а высота второго изображении — 201, ширина — 173, цветовых канала — 3.

Теперь наша задача состоит в том, чтобы добавить шум в изображения, а далее на вход автоэнкодера подавать зашумленные изображения и на выходе сравнивать с данными без шума.

Теперь можно приступать к работе с данными. Сначала получим список содержимого из каталогов «no» и «yes» с помощью команды os.listdir (). Далее с помощью библиотеки OpenCV и команды cv2.imread () прочитаем наше изображение в трехмерный массив.

Во первых все изображения разного размера, а значит размер надо как‑то унифицировать, а во вторых необходимо матричные представления изображения перевести в вектор [0,1]. Приведем все изображения к размеру 256×256, а затем каждое значение пикселя разделим на 255.

img_r =256
folder_path = "D:/*******/brain_tumor_dataset"
no_images = os.listdir(folder_path + '/no/')
yes_images = os.listdir(folder_path + '/yes/')
dataset=[]
for image_name in no_images:
	image=cv2.imread(folder_path + '/no/' + image_name)
	image=Image.fromarray(image)
	image=image.resize((img_r ,img_r ))
	image2arr = np.array(image)/255
	dataset.append(image2arr)
    
for image_name in yes_images:
	image=cv2.imread(folder_path + '/yes/' + image_name)
	#image = image+noise2
	image=Image.fromarray(image)
	image=image.resize((img_r ,img_r ))    
	image2arr = np.array(image)/255
	dataset.append(image2arr) 

Теперь разделим весь набор данных на обучающую и тестовую выборку, а потом будем к этим данным добавлять шум.

X_train, X_test = train_test_split(dataset, test_size= 0.25, random_state = 42)

Сначала с помощью библиотеки numpy смоделируем гауссов шум с нулевым математическим ожиданием и среднеквадратическим отклонением, равным единице, а затем добавим его к обучающему и тестовому набору с коэффициентом 0,4.

noise =  np.random.normal(loc=0, scale=1, size=(img_r,img_r,1))
x_train_noise = np.clip((np.array(X_train)+noise*0.4),0,1)
x_test_noise = np.clip((np.array(X_test)+noise*0.4),0,1)

Теперь можно приступать к архитектуре нейронной сети. Размер входного слоя равен размеру изображения (256, 256,3). В связи стем, что наши данные представляют собой изображения, тогда будем создавать автоэнкодер, состоящий из сверточных слоев. Как было сказано выше, нам необходимо создать две функции: энкодер и декодер. Энкодер будет состоять из двух сверточных слоев 256×3×3 и 128×3×3 соответственно и двух слоев с максимальным объединением 2×2.

# input layer
input_layer = Input(shape=(img_r,img_r,3))
#encoder
encoded_layer1 = Conv2D(256, (3, 3), activation='relu', padding='same')(input_layer)
encoded_layer1 = MaxPool2D( (2, 2), padding='same')(encoded_layer1)
encoded_layer2 = Conv2D(128, (3, 3), activation='relu', padding='same')(encoded_layer1)
encoded_layer2 = MaxPool2D( (2, 2), padding='same')(encoded_layer2)
encoded = Conv2D(64, (3, 3), activation='relu', padding='same')(encoded_layer2)

Декодер по сути является полной противоположностью энкодера, так как мы восстанавливаем 2D представление наших изображений. Он будет состоять из двух сверточных слоев 128×3х3 и 256×3х3 соответственно и двух слоев с повышающей дискретизацией 2×2.

decoded_layer1 = Conv2D(128, (3, 3), activation='relu', padding='same')(encoded)
decoded_layer1 = UpSampling2D((2, 2))(decoded_layer1)
decoded_layer2 = Conv2D(256, (3, 3), activation='relu', padding='same')(decoded_layer1)
decoded_layer2 = UpSampling2D((2, 2))(decoded_layer2)
output_layer   = Conv2D(3, (3, 3), padding='same', activation='sigmoid')(decoded_layer2)

Во всех сверточных слоях, кроме последнего используется линейная активационная функция Relu, так как мы имеем дело со значениями пикселей. В последнем слое декодера используется сигмоидальная активационная функция.

Соединяем вход и выход, чтобы сформировать и скомпилировать автоэнкодер. В качестве функции потерь будем использовать среднеквадратическую ошибку. Визуализируем нашу сеть с помощью функции model.summary ().

# compile the model
model = Model(input_layer, output_layer)
model.compile(optimizer='adam', loss='mse')
model.summary()

b1636bcdd58eb311a543432b476844e8.png

Теперь можно приступать к обучению нашей модели.

history = model.fit(x_train_noise, x_train, epochs=50, validation_data=(x_test_noise, x_test)).

Как мы видим, энкодер получает на вход зашумленные изображения x_train_noise и учиться их сжимать, а затем декодер учиться распаковывать их в чистые изображения без шума.

Посмотрим на графики потерь на этапах обучения и проверки.

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('Потери на этапах проверки и обучения')
plt.ylabel('Потери')
plt.xlabel('Эпохи')
plt.legend(['Потери на этапе обучения', 'Потери на этапе проверки'], loc='upper left')
plt.show()

973da1182a2ca52db4450c45bc045eb1.png

Видим, что даже после 50 эпох сеть медленно, но все еще обучается, потери на этапах проверки также падают, как и на этапах обучения. После обучения нейронной сети можно сохранить веса автоэнкодера для дальнейшего их использования.

json_string = model.to_json()
model.save_weights('autoencoder.h5')
open('autoencoder_N_04_50.h5','w').write(json_string)

Теперь можно визуализировать полученные данные. Посмотрим на первые 5 исходных изображений, зашумленных изображений и восстановленных изображений автоэнкодером при различном числе эпох обучения.

n = 5
plt.figure(figsize=(20, 20))

for i in range(n):
	# оригинальные изображения
	ax = plt.subplot(3, n, i + 1)
	plt.imshow((x_test[i]))
	plt.gray()
	ax.get_xaxis().set_visible(False)
	ax.get_yaxis().set_visible(False)
    
            # зашумленные изображения
	ax = plt.subplot(3, n, i + 1 + n)
	plt.imshow(x_test_noise[i])
	plt.gray()
	ax.get_xaxis().set_visible(False)
	ax.get_yaxis().set_visible(False)    

	# восстановленные изображения автоэнкодером
	ax = plt.subplot(3, n, i + 1 + 2*n)
	plt.imshow(np.array(decoded_imgs[i]))
	plt.gray()
	ax.get_xaxis().set_visible(False)
	ax.get_yaxis().set_visible(False)
plt.show()

6f2ce0c01958f729ef2314c6a78de37a.png

Видно, что с увеличением числа эпох обучений автоэнкодер учится более качественно восстанавливать изображения. Далее, оценим количественно точность сети с помощью среднеквадратической ошибки RMSE — корень среднего квадрата ошибок по формуле

RMSE = \sqrt{\frac{\sum(y_i-\overline y_i)^2}{n}}

где y_i — настоящие значения, \overline y_i — предсказанные значения

lab_err = []
for i in range(5):
	pred = np.array(decoded_imgs[i])
	target = x_test[i]
	err = np.sqrt(np.mean((target-pred)**2))
	lab_err.append(err)
print("Image error:",lab_err,'\n')

RMSE для 10 эпох обучения

Image error: [0.11112235583367339, 0.09649739151342915, 0.07507075125156328, 0.09451597002239683, 0.11461512072947239] 

RMSE для 50 эпох обучения

Image error: [0.077847916992295, 0.06904349103850838, 0.052240344819613975, 0.04785264086429222, 0.0692672959161245] 

И, в принципе, заметно, что с увеличением числа эпох обучения, ошибка уменьшается. Думаю, что для наглядности эксперимента можно пока остановиться на 50 эпохах обучения. Можно только еще попробовать к оригинальным изображениям добавить шум с меньшим коэффициентом равным 0,2 и также обучить автоэнкодер на 50 эпохах, а затем вычислить RMSE.

f467bce897f21deb8b3eacde2d0ccb47.png

RMSE после 50 эпох обучения

Image error: [0.0650505273762427, 0.05470585227284887, 0.04235355301246957, 0.03651446513302648, 0.05535199588180513] 

Как и следовало ожидать, значение RMSE меньше для изображений с коэффициентом шума 0,2, чем для изображений с коэффициентом 0,4. Автоэнкодеру легче восстанавливать менее зашумленные изображения.

И напоследок, захотелось провести эксперимент по восстановлению зашумленных цветных изображений. Для этого был выбран набор данных, содержащий изображения лесов и морских побережий. Архитектура нейронной сети осталась такая же, поэтому приведу здесь только полученные результаты.

eeb90d84ccc801b9d32a5c45ae4bdbc4.png

В целом, можно сказать, что автоэнкодер справляется со своей задачей. Введенный искусственным образом шум удаляется с изображения. Для более качественного восстановления изображений можно увеличить число эпох обучения, использовать большее количество обучающих образцов, поэкспериментировать с параметрами автоэнкодера.

В завершение хочу порекомендовать бесплатный урок от OTUS в рамках которого освоите популярный ML‑алгоритм «дерево решений». Узнаете, для каких задач его используют в машинном обучении и как правильно его применять на практике.

© Habrahabr.ru