ConvNet nedir?

Tek bir cümleyle özetleyecek olursak, "Convolutional Neural Networks" (ConvNet) en çok kullanılan derin öğrenme mimarisidir.

ConvNet'ler yeterli ve kaliteli veri ile buluştuğu zaman bir çok problemde "accuracy" değeri çok yüksek sonuçlar elde edilmesine olanak sağlamaktadır.

Günümüzde özellikle "computer vision" alanı ConvNet'ler ile çağ atlamıştır. Yani ConvNet'ler sayesinde bilgisayarlar insanlara yakın ve hatta insanlardan daha yüksek bir doğrulukla resimleri tanıyabilmekte, resimlerin içindeki nesneleri tespit edebilmekte ve daha birçok operasyonu gerçekleştirebilmektedir.

ConvNet mimarilerin atası sayılabilecek ilk mimariyi Yann LeCun'ın 1989 yılındaki "Object Recognition with Gradient-Based Learning" makalesinde görüyoruz. Daha sonra bu mimari biraz daha geliştirilerek 1998 yılında LeNet-5 halini almıştır. Bu yöntemin o yıllarda karakter tanıma ile ilgili ticari ürünlerde de kullanıldığını belirtmekte fayda var.

lecun.png

Yann LeCun (AI Director of Facebook 2019)

Her ne kadar hatırı sayılır bir süredir bu metotlar biliniyor olsa da, bunların gerçek anlamda faydalı hale gelmesi GPU teknolojisinin gelişip çok daha derin mimarilerin büyük veriler üzerinde eğitilmesi sonucu olmuştur. ConvNet'ler ilk büyük zaferini 2012 yılında AlexNet mimarisinin, "ImageNet Large Scale Visual Recognition Challenge" yarışmasında rakiplerine fark atarak birinci olmasıyla elde etmişlerdir.

first_conv_net.png

Yukarıdaki mimariyi biraz incelediğimizde temelde bir aşağıdaki adımların olduğunu görüyoruz:

  1. Resim üzerindeki convolution operasyonları
  2. Subsampling operasyonları
  3. Full connection ve Gaussian Connection operasyonları

Bu aşamaların herbirine katman (layer) denilmektedir. Bu mimaride bahsedilenlerin dışında max pooling, batch normalization gibi birçok katman daha günümüzdeki mimarilerde kullanılmaktadır.

ConvNet katmanlardan oluşmuş bir yapıdır. Her katman 3 boyutlu bir girdiyi yine 3 boyutlu bir çıktıya dönüştürür.
Bu yazımızda ConvNet'lerde kullanılan önemli katmanların aslında ne iş yaptığını inceleyeceğiz.

Katmanlar

Convolutional Layer

"Convolution" lafını her duyduğumda aklıma sayın hocam Bülent Sankur gelir. Kendisi sinyal dersinde bize bu kelimenin ne hangi etimolojik kökenden geldiğini anlatmıştı. Daha dün gibi hatılıyorum "Convolution" kelimesinin "Co-Evolution"dan geldiğini söylemişti, yani birlikte evrim geçirmek. Gerçekten de iki sinyalin geçirdiği transformasyonu çok güzel anlatan bir kelime olduğunu düşünmüştüm. Bunun Türkçe çevriminin de doğal olarak "evrişim" olduğunu belirtmişti.

Basitleştirilmiş tanım: "Convolution" büyük bir resmin daha küçük bir resim ile çarpılıp toplanmasıdır. Bu çarpım ve toplama işlemi büyük resim üzerinde kaydırma yapılarak her adım için gerçekleştirilir.

Python'da "convolution" nasıl yapılır? Çıkan sonuçlar nasıl yorumlanır?

Bir resmi başka bir resim ile "convolve" yaptığımızda aslında bir filtreleme operasyonu gerçekleştiririz. Yani küçük resimde bulunan şekillerin veya küçük resme benzeyen yapıların olduğu yerlerde "convolution" sonucu yüksek olur. Şekillerin tam olarak benzemesine de gerek yoktur. Tam olarak benzerse sonuç çok yüksek biraz benzerse sonuç biraz daha düşük çıkar. Basit bir örnekle açıklayalım.

"Convolution" renkli imgelere de uygulanabilir, fakat örneğin basit olması açısından burada siyah-beyaz imgeler (yani 2 boyutlu) üzerinde bir örnek yapalım

Elimizde paint programı yoluyla hazırlanmış 128x128'lik bir imge olsun. Bu imgenin içinde 3x3 piksel boyutlarında artılar mevcut

In [3]:
%matplotlib notebook
# opencv modülünü yüklüyoruz
import cv2
import matplotlib.pyplot as plt
pluses = cv2.imread("pluses.bmp")
# siyah beyaza çevirelim
pluses = cv2.cvtColor(pluses,cv2.COLOR_BGR2GRAY)
plt.figure()
plt.imshow(pluses,cmap="gray")
Out[3]:
<matplotlib.image.AxesImage at 0x1cec78430c8>

Yukarıdaki resimle "convolve" yapacağımız "kernel/filtre" (küçük resim) tek bir artıdan oluşsun. Yani 3x3 boyutlarında bir artıdan söz ediyorum.

In [4]:
single_plus = cv2.imread("single_plus.bmp")
# siyah beyaza çevirelim
single_plus = cv2.cvtColor(single_plus,cv2.COLOR_BGR2GRAY)
plt.figure()
plt.imshow(single_plus,cmap="gray")
Out[4]:
<matplotlib.image.AxesImage at 0x1cec7b381c8>
In [5]:
# Convolution için scipy kütüphanesini kullanacağız
from scipy.signal import convolve2d
import numpy as np
out = convolve2d(pluses,single_plus,mode="full")
plt.figure()
plt.imshow(out,cmap="gray");

Yukarıdaki "convolution" çıktısı olan resimde artıların olduğu noktalarda yüksek yoğunluklu bir beyazlık olduğunu görüyoruz. Yani bu çıktı ile ilk imgedeki artıların tam merkezlerini bulmak mümkün!

In [6]:
locations = np.where([out == np.max(out)])
print("y koordinatları")
print(locations[1])
print("x koordinatları")
print(locations[2])
y koordinatları
[  8   9  14  21  23  43  58  74  77 104 106 106]
x koordinatları
[116   7  70  38  95  26  64 101  29 107  17  68]

ConvNet'te filtreler neler öğreniyor?

ConvNet mimarilerinde bu "kernel" veya "filtre" dediğimiz yapılardan (örnekteki küçük artı) bir sürü var. İşin güzel tarafı bu filtrelerin katsayıları yani içerdikleri rakamlar yapay sinir ağının eğitimi sırasında otomatik olarak belirleniyorlar. Yani eğitim için kullandığınız veri setindeki temel yapıları öğreniyorlar!

Örneğin aşağıdaki makalede bir convnet'in öğrendiği filtreleri örneklendirmişler. Buradaki filtrelerin ve genellikle derin öğrenmede kulllanılan filtrelerin üç boyutlu olduğunu belirtmekte fayda var.

Pooling Layer

Şimdi "convolution"dan çıktığımıza göre bir sıradaki katmanımız "pooling layer"ı inceleyelim.

"Pooling" katmanı "convolution"a çok benzer. Bu operasyonda da yine belirli bir büyüklüğe sahip filtremiz vardır ve bu filtreyi büyük resmin üzerinde dolaştırırız.

Yapılan işlem ise filtrenin gezdiği yerlerde çarpma ve toplama işlemi yapmak yerine, buradan büyük resme ait tek bir sayıyı seçmektir. Bu sayıyı nasıl seçtiğimiz filtrenin tipini belirler. Örneğin:

  • Eğer en büyük sayı seçiliyorsa "max pooling"
  • Sayıların ortalamaları alınıyorsa "average pooling" katmanı kullanılıyor deriz

max_pool.png

Activation Layer

Aktivasyon katmanları genellikle "Flatten" veya "Convolution" katmanlarından sonra kullanılır ve neuronların çıktısını regüle etmeye yarar. Bu kısımdaki fonksiyonlar oldukça önemli oldukları için bunların üzerinde biraz durup, inceleyelim

Belli başlı kullanılan aktivasyon fonksiyonlarını sayacak olursak:

ReLU (Rectified Linear Unit)

En çok kullanılan aktivasyon fonksiyonudur. Ne yapacağınızı bilmiyorsanız bunu kullanın :) Sıfırdan büyük sayılar için lineer davranır, sıfırdan küçük sayılar için 0'dır.

In [7]:
import numpy as np
import matplotlib.pyplot as plt
def relu(x):
    out = np.zeros(len(x))
    out[x>0] = x[x>0]
    return out

x = np.linspace(-10,10,100)
y = relu(x)


plt.figure()
plt.plot(x,y)
plt.grid()
plt.title("ReLU")
plt.show()

Sigmoid

Bunu belki logistic regression'dan hatırlarsınız. Yaptığı şey girdiyi 0 ile 1 arasına sıkıştırmaktır. Çok büyük değerler yani yaklaşık 5'ten sonra çıktı olarak 1 verir. Aşağıdaki gibi bir fonksiyona sahiptir.

\begin{equation*} f(x)=\frac{1}{1+e^{-x}} \end{equation*}
In [8]:
def sigmoid(x):
    # ilk denklem
    out = 1 / (1 + np.exp(-x))

    return out

x = np.linspace(-10,10,100)
y = sigmoid(x)


plt.figure()
plt.plot(x,y)
plt.grid()
plt.title("Sigmoid")
plt.show()

Bu fonksiyon geçmişte çok kullanılmıştır. Çünkü karakteristiği fiziksel bir nöronun ateşlenmesine çok benzemektedir.

Fakat sigmoid fonksiyonu çok tercih edilmez. Çünkü sinir ağlarının eğitimi sırasında gradyanların kaybolmasına sebep olur, bir başka deyişle yapay sinir ağları kısa bir sürede öğrenemez hale gelir. Kullanılmamasının ikinci bir sebebi de bu fonksiyonun 0 etrafında simetrik olmamasıdır.

Tanh

Sigmoid'in 0 etrafında simetrik hale getirilmesi ile oluşur. Sigmoid'e göre simetrik olması nedeniyle daha iyidir ama bu da çok tercih edilen bir fonksiyon değildir.

In [9]:
def tanh(x):
    # ilk denklem
    out = 2*sigmoid(2*x) - 1

    return out

x = np.linspace(-10,10,100)
y = tanh(x)


plt.figure()
plt.plot(x,y)
plt.grid()
plt.title("Tanh")
plt.show()

Leaky ReLU

ReLU'nun çok avantajlı olduğunu söylemiştik. Tekrar edecek olursak:

  • Sigmoid ve Tanh'deki exp operasyonlarına karşılık ReLU'da sadece kıyaslama kullanılır bu sebeple çok hızlıdır
  • Sigmoid ve Tanh'ye kıyasla yapay sinir ağının çok hızlı yakınsayıp, kısa sürede yüksek performans değerleri elde etmesini sağlar.

Ne yazık ki ReLU'nun da bazı dezavantajarı vardır. Bunlardan en başta geleni; nöronun eğitim sırasında ölmesine sebep olmasıdır. Ölen nöron eğitim boyunca bir daha aktive olmaz. Öğrenme hızı yüksek yapıldığında genellikle bu fenomenle karılaşılmaktadır.

ReLu'nun bu dezavantajını gidermek için negatif değerlerde 0 yerine çok küçük değer alan "Leaky Relu" önerilmektedir.

In [10]:
def leaky_relu(x,alpha=0.05):
    out = np.zeros(len(x))
    out[x>0] = x[x>0]
    out[x<=0] = x[x<=0]*alpha
    return out

x = np.linspace(-10,10,100)
y = leaky_relu(x)


plt.figure()
plt.plot(x,y)
plt.grid()
plt.title("Leaky ReLU")
plt.show()

Literatürde bu yöntemin başarılı olduğuna dair bir fikir birliği yoktur. *

Maxout

Ian Goodfellow tarafından önerilmiş bir yöntemdir. Şu fonksiyonu kullanır: $$\max(w_1^Tx+b_1, w_2^Tx + b_2)$$ Bu yöntemde aktivasyon fonksiyonu da $w_1,b_1,w_2,b_2$ parametreleri vasıtası ile öğrenilmektedir. Ne yazık ki bu yöntem katman başına parametrelerin ikiye katlanmasına sebep olmaktadır.

Flatten Layer

Bu katman kendine gelen girdiyi tek boyutlu bir hale indirger. Yani diyelim ki (64,64,3) boyutunda bir imgemiz var. Bunu "flatten" yaptığımız zaman 64*64*3 = 12288x1 boyutunda bir vektör elde ederiz.

Genellikle "Fully Connected" katmandan önce kullanılır.

Fully Connected Layer

Bir önceki katmana tümüyle bağlanan nöronlardan oluşur. Klasik bir yapay sinir ağını anlatmaktadır.

Normalization Layer

İstatistiksel normalizasyon en basit haliyle bir veriyi ortalaması 0 ve varyansı 1 haline getirmek olarak tanımlanabilir.

Derin öğrenme mimarilerinde "batch normalization" genellikle bir katmandan hemen sonra aktivasyon fonksiyonu çağrılmadan önce kullanılır. Buradaki amaç çıktıları normalize ederek regularizasyon sağlamak ve yapay sinir ağının "overfit" yapmasını engellemektir.

Batch Normalization ile ilgili en önemli makale aşağıdaki bağlantıdadır:
Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift

Dropout Layer

Bu katman eğitim sırasında her iterasyonda belirli oranda nöronun rastgele kapatılmasını sağlar. Ayrıntısına girmeden bunun da önemli bir regülarizasyon tekniği olduğunu söyleyebiliriz. Aşağıdaki örnekte dropout değeri 0.5 olan katmanın her seferinde farklı nöronlarının aktive olduğunu görebiliriz.

Bu konuda en önemli makale Dropout Paper olarak da bilinir, daha fazla bilgi için bakabilirsiniz:
http://www.cs.toronto.edu/~rsalakhu/papers/srivastava14a.pdf

Bir ConvNet Örneği - VGGNet

Large Scale Visual Recognition Challenge 2014 (ILSVRC2014) yarışmasında birinci olan modeli biraz inceleyelim
Orjinal makale: VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION

imagenet_vgg16.png

Şimdi VGG'yi yükleyelim ve katmanlarına bakalım.

In [14]:
from tensorflow import keras
vgg = keras.applications.vgg16.VGG16()
print(vgg.summary())
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/vgg16/vgg16_weights_tf_dim_ordering_tf_kernels.h5
553467096/553467096 [==============================] - 97s 0us/step
Model: "vgg16"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 224, 224, 3)]     0         
                                                                 
 block1_conv1 (Conv2D)       (None, 224, 224, 64)      1792      
                                                                 
 block1_conv2 (Conv2D)       (None, 224, 224, 64)      36928     
                                                                 
 block1_pool (MaxPooling2D)  (None, 112, 112, 64)      0         
                                                                 
 block2_conv1 (Conv2D)       (None, 112, 112, 128)     73856     
                                                                 
 block2_conv2 (Conv2D)       (None, 112, 112, 128)     147584    
                                                                 
 block2_pool (MaxPooling2D)  (None, 56, 56, 128)       0         
                                                                 
 block3_conv1 (Conv2D)       (None, 56, 56, 256)       295168    
                                                                 
 block3_conv2 (Conv2D)       (None, 56, 56, 256)       590080    
                                                                 
 block3_conv3 (Conv2D)       (None, 56, 56, 256)       590080    
                                                                 
 block3_pool (MaxPooling2D)  (None, 28, 28, 256)       0         
                                                                 
 block4_conv1 (Conv2D)       (None, 28, 28, 512)       1180160   
                                                                 
 block4_conv2 (Conv2D)       (None, 28, 28, 512)       2359808   
                                                                 
 block4_conv3 (Conv2D)       (None, 28, 28, 512)       2359808   
                                                                 
 block4_pool (MaxPooling2D)  (None, 14, 14, 512)       0         
                                                                 
 block5_conv1 (Conv2D)       (None, 14, 14, 512)       2359808   
                                                                 
 block5_conv2 (Conv2D)       (None, 14, 14, 512)       2359808   
                                                                 
 block5_conv3 (Conv2D)       (None, 14, 14, 512)       2359808   
                                                                 
 block5_pool (MaxPooling2D)  (None, 7, 7, 512)         0         
                                                                 
 flatten (Flatten)           (None, 25088)             0         
                                                                 
 fc1 (Dense)                 (None, 4096)              102764544 
                                                                 
 fc2 (Dense)                 (None, 4096)              16781312  
                                                                 
 predictions (Dense)         (None, 1000)              4097000   
                                                                 
=================================================================
Total params: 138,357,544
Trainable params: 138,357,544
Non-trainable params: 0
_________________________________________________________________
None

VGG'nin belli başlı özelliklerini saymak gerekirse:

  • VGG'de her bir convolution adımı 3x3 matrislerden oluşmaktadır.
  • Girdi imgelerine 1 piksel "pad" yapılmıştır. Böylece 3x3 bir matris ile "convolve" edildiği zaman yine aynı boyutta bir çıktı elde ediyoruz.
  • "Stride" parametresi ise 1 olarak belirlenmiştir.
  • Max Pooling katmanlar ise 2x2 ve "stride" değeri ise 2 olarak tanımlanmıştır.

Bu teorik bilgilerimizi, yüklediğimiz modeli inceleyerek doğrulamaya çalışalım:

In [15]:
# Keras'ta bir modelin katmanlarını analiz etmek için layers metodu kullanılır
# Bu metod indekslenebilir. 
# Örneğin ilk katmanın konfigürasyonunu bulalım
vgg.layers[0].get_config()
Out[15]:
{'batch_input_shape': (None, 224, 224, 3),
 'dtype': 'float32',
 'sparse': False,
 'ragged': False,
 'name': 'input_1'}
In [16]:
# Şimdi ilk convolution katmanının 
# konfigürasyonuna bakalım 
vgg.layers[1].get_config()
Out[16]:
{'name': 'block1_conv1',
 'trainable': True,
 'dtype': 'float32',
 'filters': 64,
 'kernel_size': (3, 3),
 'strides': (1, 1),
 'padding': 'same',
 'data_format': 'channels_last',
 'dilation_rate': (1, 1),
 'groups': 1,
 'activation': 'relu',
 'use_bias': True,
 'kernel_initializer': {'class_name': 'GlorotUniform',
  'config': {'seed': None}},
 'bias_initializer': {'class_name': 'Zeros', 'config': {}},
 'kernel_regularizer': None,
 'bias_regularizer': None,
 'activity_regularizer': None,
 'kernel_constraint': None,
 'bias_constraint': None}

Gördüğünüz gibi ilk "convolutional layer"daki parametreleri kolaylıkla elde ettik. Yukarıda konuştuğumuz temel "convolution" kavramlarını hatırlayalım ve buradaki karşılıklarına bakalım:

  • 'padding': 'same': Bu parametre "padding" yapıldığını gösteriyor. "same" girdi ile çıktının aynı şekilde olması için yeterli miktarda sıfır eklendiğini gösteriyor.
  • 'kernel_size': (3, 3): Filtre boyutunun 3x3 olduğunu gösteriyor.
  • 'strides': (1, 1): İlk sayı sütun eksenindeki kaydırma miktarını, ikinci sayı satır eksenindeki kaydırma miktarını belirtiyor. Bunların bir olduğunu görüyoruz.
  • 'filters': 64: Tam 64 adet filtre olduğunu anlıyoruz.

İlk katmandaki filtrelerin görselleştirilmesi

Bu ilk katmandaki filtreleri görselleştirebilir miyiz acaba? Bunun için ilk katmandaki "weigth"leri yani filrelerin katsayılarını almamız lazım.

In [17]:
weights = vgg.layers[1].get_weights()
print(weights[0].shape)
(3, 3, 3, 64)
In [18]:
# 64 adet filtre olduğunu görüyoruz. Bunların her birinin boyutu 3x3x3. Sonundaki 3 katsayısı R,G,B kanalllarını belirtiyor.
# İlk önce değerler nasıl dağılmış bir bakalım
plt.figure()
# Tüm ağırlıkları alıp çizdirelim
plt.hist(np.ndarray.flatten(weights[0]))
plt.show()

Buradaki rakamları 0-255 arasında göndermeliyiz ki imge halinde görebilelim. François Chollet Bey'in önerdiği fonksiyonu kullanalım.

In [19]:
# https://blog.keras.io/how-convolutional-neural-networks-see-the-world.html
def deprocess_image(x):
    # normalize tensor: center on 0., ensure std is 0.1
    x -= x.mean()
    x /= (x.std() + 1e-5)
    x *= 0.1

    # clip to [0, 1]
    x += 0.5
    x = np.clip(x, 0, 1)

    # convert to RGB array
    x *= 255
    x = x.transpose((1, 2, 0))
    x = np.clip(x, 0, 255).astype('uint8')
    return x
In [20]:
processed = deprocess_image(weights[0][:,:,:,0])
plt.figure()
plt.imshow(processed)
plt.show()
In [21]:
# Şimdi 64 adet filtrenin hepsini birden gösterelim isterseniz
plt.figure()
for i in range(64):
    ax = plt.subplot(8,8,i+1)
    processed = deprocess_image(weights[0][:,:,:,i])
    plt.imshow(processed)
    ax.set_xticks([])
    ax.set_yticks([])
plt.show()

Buraya kadar okuduğunuz için çok teşekkürler. Bir başka yazıda görüşmek üzere, esen kalın!