یک خودرمزنگار(Autoencoder) ساده برای بازسازی تصاویر

سلام امیدوارم حالتون خوب باشه و حسابی درگیر یادگرفتن و پیاده سازی کلی چیزای هیجان انگیز باشید. برخلاف نوشته‌هایی که تا الان براتون گذاشتم تصمیم گرفتم که از این به بعد علاوه بر مباحث مهندسی نرم‌افزار(گاه گاهی هم توانایی های فردی) به سراغ مباحث علم داده هم برم و براتون از کارایی که می‌شه انجام و یا خودم دارم انجام می‌دم بنویسم. به عنوان اولین نمونه هم سعی دارم پیاده سازی یک خودرمزنگار(Autoencode) که یکی از ساده‌ترین نوع شبکه‌های عصبی هست رو براتون توضیح بدم. امیدوارم که چیز به درد بخوری از توش در بیاد.

همونطور که می‌دونید (اگر نمی‌دونید اینجا رو بخونید) خود رمزنگارها شبکه‌های عصبی هستن که ورودی و خروجی یکسانی دارن. یعنی شما یک ورودی (عکس، متن یا هرچیز دیگه) رو به این شبکه می‌دین و انتظار دارید همون رو هم در خروجی ببینید! شاید یکم گیج شده باشید چون احتمالا تا الان از شبکه‌های عصبی برای پیش بینی و کلاس‌بندی و این چیزا استفاده می‌کردید و اینکه یه چیزی بدی و دوباره همون رو تحویل بگیرید یکم ناجور باشه. اما این شبکه‌ها کارشون همینه. یعنی یک ورودی از کاربر می‌گیرن و سعی می‌کنن با دقت بالایی همون رو توی خروجی تولید کنن. اما این کار به چه دردی می‌خوره؟! شبکه با این کار اصلی‌ترین ویژگی های ورودی رو یاد می‌گیره و بر اساس اون آموخته‌ها دوباره همون ورودی رو توی خروجی تولید می‌کنه. همین کار به ظاهر ساده کلی استفاده برای ما داره که به صورت تیتر وار براتون می‌گم:

  • استخراج ویژگی(همین که گفتیم اصلی‌ترین ویژگی‌ها رو یاد می‌گیره و سعی می‌کنه از اول مدل رو بسازه)
  • کاهش فضای مسئله(وقتی شما تونستید ویژگی‌های اصلی یک مسئله رو در بیارید یعنی تونستید اون رو خلاصه کنید)
  • از بین بردن نویز(مثلا تصاویری که شامل نویز هستن رو می‌تونید با این شبکه‌ها باز سازی کنید و نویزها رو از بین ببرید)
  • فشرده سازی اطلاعات

حالا که یه خلاصه از این که خودرمزنگار چیه و چه کاربردی داره گفتیم، می‌خوایم بریم سراغ پیاده سازی یک خودرمزنگار ساده. خودرمزنگارها انواع مختلفی دارن مثل خودرمزنگار ساده، خودرمزنگار کانولوشن، خودرمزنگار اسپارس و... که ما سراغ خودرمزنگار ساده می‌ریم. برای پیاده سازی هم از Keras عزیز استفاده می‌کنیم(با وجود تنفر اینجانب از پایتون) و مجموعه داده‌مون هم همون مجموعه داده معروف اعداد دست نویس انگلیسی (mnist digit) هستش.

پیاده سازی

برای پیاده سازی اول ما نیاز داریم تا یک سری از کتابخونه‌ها رو وارد کنیم:

from keras.datasets import mnist
import matplotlib.pyplot as plt


بعد از اون باید مجموعه داده‌ای که مدنظرمون هست رو به برنامه اضافه کنیم، خوشبختانه چون مجموعه داده مد نظر ما خیلی پرکاربرد و ساده است کراس اون رو توی خودش داره و ما نیاز نداریم که دانلودش کنیم. توی تیکه کد زیر ما مجموعه داده mnist که شامل تصاویر دست خط اعداد انگلیسی است رو توی متغییرهای Train و Testمون می‌ریزیم:

# load (downloaded if needed) the MNIST dataset
(X_train, y_train), (X_test, y_test) = mnist.load_data()

بریم چندتا از این اعداد رو نشون بدیم که ببینیم با چی سر و کار داریم. با تیکه کد زیر می‌شه با استفاده از کتابخونه matplotlib این کار رو انجام داد. ما از این کتابخونه می‌خوایم که چهارتا عدد اول داده‌ها رو برامون نشون بده:

# plot 4 images as gray scale
plt.subplot(221)
plt.imshow(X_train[0], cmap=plt.get_cmap('gray'))
plt.subplot(222)
plt.imshow(X_train[1], cmap=plt.get_cmap('gray'))
plt.subplot(223)
plt.imshow(X_train[2], cmap=plt.get_cmap('gray'))
plt.subplot(224)
plt.imshow(X_train[3], cmap=plt.get_cmap('gray'))

# show the plot
plt.show()

که می‌شه به این صورت:


حالا که داده‌ها لود شدن باید ببینیم چه پیش پردازش‌هایی نیاز داره تا با اعمال کردن اونها بتونیم از داده به شکل درستی استفاده کنیم. اگر متغیر X_train رو چاپ کنیم، متوجه می‌شیم که این متغییر شامل 60 هزار تا ماتریس 28*28 هستش که هرکدوم از این ماتریس‌ها یک عکس هستش. هر کدوم از خونه‌های این ماتریس هم یک عدد بین 0 تا 255 هستش که نشون دهنده رنگ اون در فرمت RGB است. تنها پیش پردازشی که ما برای این داده‌ها نیاز داریم نرمال‌سازی(normalization) داده‌هاست که برای هم اسکیل کردن و بردن داده‌ها توی بازه‌ی 0 تا 1به جای بازه 0 تا 255 هستش. این کار رو با کد زیر می‌تونیم انجام بدیم:

X_train_flat = X_train.reshape(60000, X_train.shape[1]*X_train.shape[2])
X_test_flat = X_test.reshape(10000, X_test.shape[1]*X_test.shape[2])

# Normalize values
X_train_flat = X_train_flat/255
X_test_flat = X_test_flat/255

حالا که داده‌ها رو هم نرمال کردیم، نوبت به درست کردن شبکه عصبی یا مدلمون می‌رسه:) شبکه‌های خودرمزنگار از دو قسمت encoder و decoder تشکیل شدن. توی بخش اول یا encoder ما داده رو از ابعاد با اندازه بالا(اینجا 784تایی) می‌بریم به ابعاد کوچکتر مثلا 32تایی یا حتی کمتر. این کار رو می‌تونیم توی چند لایه انجام بدیم مثلا از 784 به 512 بعد به 256 بعد 128 بعد 64 و در آخر هم 32 یا اینکه مستقیم از 784 به 32 بریم انتخاب این معماری‌ها به فاکتورهای زیادی وابسته است. ما اینجا به روش اول عمل می‌کنیم یعنی از 784 به 32 و بعد از اون هم به 2 میریم. توی بخش دوم که decoder هستش داده‌ها از ابعاد کمتر به ابعاد بزرگتر می‌رن مثلا اینجا از 2 به 784 می‌ریم. توی بخش encode شبکه ویژگی‌ها رو یاد می‌گیره و توی بخش decode سعی می‌کنه با چیزایی که یاد گرفته دوباره ورودی رو بسازه و به خروجی ببره. معماری‌ای که ما انتخاب کردیم یک ورودی با 784 نورون، یک لایه مخفی با 32 نورون، بعدش لایه مخفی دوم با 2 نورون لایه مخفی بعدی با 32 نورون و آخر هم لایه خروجی با 784 نورون هستش که توی شکل زیر یک شماتیک ازش رو براتون نشون می‌دیم:


برای درست کردن مدل، از روش تابعی(functional) توی کراس استفاده می‌کنیم. یعنی هر لایه رو جدا درست می‌کنیم و بعد به لایه بعدی ارتباطش می‌دیدم. برای این که بتونیم مدلمون رو بسازیم نیاز داریم یه سری کلاس‌ها رو از کراس بگیریم. توی خط اول تیکه کد زیر ما کلاس‌های Dense و Input رو از کراس وارد برنامه می‌کنیم و در ادامه هم لایه‌ها رو می‌سازیم. غیر از لایه آخر که تابع فعال Sigmoid برای اون استفاده شده، لایه‌های قبلی همه از تابع Relu استفاده می‌کنن:

from keras.layers import Input, Dense
input_1 = Input(shape=(X_train_flat.shape[1],))
hidden_1 = Dense(32, activation='relu')(input_1)
latent_space = Dense(2, activation='relu')(hidden_1)
hidden_2 = Dense(32, activation='relu')(latent_space)
output_1 = Dense(X_train_flat.shape[1], activation='sigmoid')(hidden_2)

حالا که لایه‌ها مون رو ساختیم و اونها رو به هم وصل کردیم باید اونها رو به مدلمون بدیم پس:

from keras.models import Model
autoencoder = Model(inputs=input_1, outputs=output_1)
encoder = Model(inputs=input_1, outputs=latent_space)
decoder_input = Input(shape=(2,)) 
decoder_layer_1 = autoencoder.layers[-2](decoder_input)
decoder_output = autoencoder.layers[-1](decoder_layer_1)
decoder = Model(inputs=decoder_input, outputs=decoder_output)

حالا که مدلمون آماده شده باید مدل رو کامپایل کنیم. برای تابع هزینه از تابع binary_crossentropy استفاده می‌کنیم چون اینجا داده‌هامون از نوع 0 و 1 هستن، یعنی یک پیکسل یا سفید یا سیاه. برای optimizer هم از تابع adam استفاده می‌کنیم:

autoencoder.compile(loss='binary_crossentropy', optimizer='adam',accuracy='')

حالا مدلمون آماده استفاده است. کافیه که اون رو با داده‌های آموزشیمون آموزش بدیم:

autoencoder.fit(X_train_flat, X_train_flat, epochs=10, validation_data=(X_test_flat, X_test_flat))

برای اینکه خروجی رو ببینیم باید اول از encoder بخوایم که داده‌ها رو برامون کد کنه. پس داده‌های آموزشی رو بهش می‌دیم و خروجی رو توی متغیر encoded_values می‌ریزیم.

encoded_values = encoder.predict(X_train_flat)

بعد از این که داده‌های کد شده رو گرفته باید اونها رو دیکد کنیم و دوباره اونها رو به اندازه 28 در 28 پیکسل برگردونیم تا بتونیم اونها رو ببینیم:

decoded_values = decoder.predict(encoded_values)
decoded_values = decoded_values.reshape(60000, 28, 28)

حالا خروجی ما آماده است. اول بریم 10 تا از داده‌های اصلی رو نشون بدیم بعد ببینیم که خودرمزنگار اونها رو چجوری توی خروجی بازسازی کرده. تیکه کد زیر برای ما داده‌های 110 تا 120 مجموعه داده اصلی رو نشون می‌ده:


# Display some images
fig, axes = plt.subplots(ncols=10, sharex=False,
sharey=True, figsize=(20, 7))
counter = 0
for i in range(110, 120):
    axes[counter].set_title(y_train[i])
    axes[counter].imshow(decoded_values[i], cmap='gray')
    axes[counter].get_xaxis().set_visible(False)
    axes[counter].get_yaxis().set_visible(False)
    counter += 1
plt.show()

که خروجی زیر رو تولید می‌کنه:

برای نشون دادن داده‌های دیکد شده هم از کد زیر استفاده می‎‌کنیم و داده‌های 110 تا 120 اون رو نشون می‌دیم:

# Display some images
fig, axes = plt.subplots(ncols=10, sharex=False,
sharey=True, figsize=(20, 7))
counter = 0
for i in range(110, 120):
    axes[counter].set_title(y_train[i])
    axes[counter].imshow(decoded_values[i], cmap='gray')
    axes[counter].get_xaxis().set_visible(False)
    axes[counter].get_yaxis().set_visible(False)
    counter += 1
plt.show()

عکس زیر خروجی خودرمزنگار رو نشون می‌ده که سعی کرده تا اعداد رو بازسازی کنه:

همونطور که می‌بینید تونستیم تا حدود زیادی اعداد رو به همون شکلی که هستن توی خروجی بازسازی کنیم. احتمالا شما بتونید با تغییر دادن بعضی ویژگی‌های شبکه اون رو دقیق‌تر کنید و میزان خطا رو کاهش بدید. راستی کد رو هم براتون توی این کولب گذاشتم، می‌تونید اونجا ببینید و اجرا کنید.


مطالب مرتبط با این نوشته در وبلاگ من: