Cognitive Toolkit ile GPU dünyasına giriş kısım 1

Eski adı Microsoft Computational Network ToolKit, yeni adı Microsoft Cognitive Toolkit olan bu anlatacağım ürün adından da anlaşılacağı üzere Microsoft'un yapay sinir ağlarına yönelik bir geliştirme kütüphanesi. Kendisi bir çok dil ve framework ile kullanılabilse de ön plana python, .net, ve .net core çıkıyor. Açık kaynaklı bir kütüphane olan CNTK hem Windows hem de Linux tarafında çalışabiliyor.

Ürün kendisini kanıtlamış ve güvenilir bir ürün. Kendisine Microsoft'un bir çok ürününde arkaplanda rastlamak mümkün. Cortana, Bing, HoloLens, Skype, XBOX ve bir çok ürün CNTK'dan akıl almakta. Biz de bu yazıda C# ile basit bir örnek yapacağız. Yapacağımız örneğin en çarpıcı noktası ise GPU kullanan bir örnek olması olacak. Zira ne zaman bir ML ürünü hakkında sunum yapıyor olsam mutlaka ama mutlaka birisi "GPU kullanabiliyor muyuz?" şeklinde sormakta. Bu yazı onlara tavsiye edebileceğim bir yazı olacaktır diye düşünüyorum.

Fakat yazının en başında söylemekte fayda var. Grafik kartının nimetlerinden yararlanmak için CUDA destekli bir karta yani Nvidia marka işlemcisi olan bir karta ihtiyacınız var. Bazı uçuk yöntemler ile CUDA'yı farklı marka kartlarda çalıştırmak mümkün olsa da ben bu yöntemleri bu yazı için yok kabul ediyorum. Bunu sağlamıyor olmanız CNTK kullanamayacağınız anlamına gelmiyor. CPU ile yolunuza devam edebilir, karta sahip olduğunuzda ufak bir değişikle GPU perfomansına erişebilirsiniz. Fakat, AMD kartınız veya kartlarınız varsa bu durumda yol yakınken başka framework'lere bakmanızı öneririm. Zorlamanın anlamı yok. 🙄

💻 Peki neden GPU bu kadar önemli? CPU içindeki bir işlem birimini profesör, GPU içindeki bir işlem birimini ise öğrenci olarak düşünelim, beynimizde bir kenarda dursun. ML tarafında sıklıkla eş zamanlı çalıştırması mümkün olan bir çok basit işlem olmaktadır. Bu işlemlere benzer hayali bir senaryo düşünelim ilk senaryomuza bağlayalım. Yüzlerce bakkal defterindeki borç ve alacaklar elle sayısal ortama aktarılacak olsun. Bu aktarma işlemi için birim maliyeti ₺10 olan 100 öğrenci mi tercih ederdiniz yoksa birim maliyeti ₺1000 olan 100 profesör mü? O kadar profesörün bu işi daha kısa sürede bitirecek olması bile maliyetin yüksekliği açısından bizi öğrencilere yöneltir. GPU'lar CPU'lara göre çok fazla işlem birimine sahiptir,buna karşın bu işlem birimleri daha yavaş ve daha sınırlı komutları işleyebilmektedir. Biz de GPU'ların bu yeteneğini makine öğrenmesi sırasında kullanabiliyoruz. Benzer işlem gücü için 100lerce CPU ve bunu destekleyecek yan donanımları almak yerine 1 tane GPU almak çok daha ucuza geliyor. E niye işlemci var o zaman? Bunu açıklamak için anolojimize geri dönelim, sayısala çevrilmiş verinin anlamlandırılması için 1 kişi seçilecek olsun, bu durumda profesörü seçmek daha anlamlı olacaktır zira öğrenci bu işi yapacak yetkinlikte ya değil ya da çok uzun zaman içinde yapabilecektir.

Yeni bir .net core 3.0 projesi açarak başlıyoruz. Öncelikle yapmamız gereken ilk şey pek tabii ilgili paketi yüklemek olacak.

CUDA CC 3.0 destekli kartınız varsa

Grafik kartınızın CUDA desteği (nvidia) olduğundan ve Compute Capability değerinin 3 ve üzeri olduğundan emin olmanız gerekiyor. Ve yine ilgili CUDA kurulumlarını sizin yapmış olmanız bekleniyor.

CUDA kurulumu için

  1. En güncel sürücüleri yükleyin.
  2. https://developer.nvidia.com/cuda-downloads adresinden sisteminiza uygun CUDA 10.0'ı yükleyin.

🎆 Dikkat. Resmi CNTK dokümanı sizi 9.0'a yönlendirecektir. 9.0 sürümü RTX serisi kartları görmemekte ve CDNN'i ayrıca yüklemenizi beklemektedir.

file

file

🎍 CUDA ile beraber gelen "SAMPLE" klasörünü incelemenizi tavsiye ederim. Hoşunuza giderse bırakamayacağınız bir şeye dönüşebilir.

Projeye Devam

Projeyi açtık, gerekli paketleri yükledik. Harıl harıl kodlamaya girişmeden önce ortamı test edecek bir iki satır kod yazmamız gerekiyor. Aşağıdaki basit örnek ile başlayalım.

using CNTK;
using System;
namespace CNTKDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var cihaz = DeviceDescriptor.GPUDevice(0);
            Console.WriteLine(cihaz.AsString());
            Console.ReadKey();
        }
    }
}

Bu kod ile seçilen ekran kartımızı öğrenmeye niyetleniyoruz. Fakat çalıştırdığınız zaman şöyle bir hata ile karşılaşabilirsiniz:
file

Bu hatanın sebebi CNTK'in illa ki 64bit derleme kabul etmesi. Bunu sağlamak için "Solution"'a sağ tıklıyoruz ve "Configuration Manager"'ı açıyoruz.

file

Daha sonra yeni bir ayar oluşturmak istediğimizi belirtiyoruz. X64'ü seçiyoruz.

file

file

İşlemi tamamladığımızda bir kaç dosya projemize kopyalanacak ve uygulama çalışır hale gelecektir. Ben çalıştırdığımda aşağıdaki sonucu alıyorum:
file

Buradan grafik kartımızın CNTK tarafından tanındığını biliyoruz ve yola devam edebiliriz.

Verinin temini

Aşağıdaki başlıklarda eğitim verisini nasıl temin ettiğime ilişkin özet bilgiler aktardım. Bu kısımları atlayıp size hazırladığım dosyayı indirebilirsiniz.

MNIST ve IDX

Veri olarak ünlü el yazısı koleksiyonu olan MNIST'i kullanacağız. Kendisini şu adresten temin edebilirsiniz:
http://yann.lecun.com/exdb/mnist/

Buradan indirmeniz gereken 4 adet dosya var.

train-images-idx3-ubyte.gz:  eğitim resimleri (9912422 bytes)
train-labels-idx1-ubyte.gz:  eğitim etiketleri (28881 bytes)
t10k-images-idx3-ubyte.gz:   test resimleri (1648877 bytes)
t10k-labels-idx1-ubyte.gz:   test etiketleri (4542 bytes)

Dosyaların her biri sıkıştırılmış durumda, bunları açtığımızda idx1 ve idx3 biçiminde dosyalar çıkıyor olacak. Dosyaların yapısı ile ilgili bilgiler indirme sayfasının sonunda mevcut. Ben C#'a göre özet geçeyim.

idx1 dosyalarının yapısı:

[2049|int][elemansayisi|int][eleman0|byte][eleman1|byte][eleman2|byte]

idx3 dosyalarının yapısı:

[2051|int][elemansayisi|int][genişlik|int][yükseklik|int][piksel0|byte][piksel1|byte][piksel2|byte]

Dosyada big-endian kullanıldığından okuma işleminde bitlerin ters dizilmesi gerekmekte.

CNTK Dosyaları

CNTK'nin CTF biçiminde kendi dosya biçimi bulunmakta. Bu dosyanın yapısına ise https://docs.microsoft.com/en-us/cognitive-toolkit/brainscript-cntktextformat-reader adresinden ulaşabilirsiniz.

Yine bu biçimin en temel halini özetlersem:

|akis1 0 0 0 0 1    |akis2 1 1 100 250 25

Bir satır için veride birden fazla akış belirliyoruz. Bu akışlar genellikle niteliklerin veya etiketlerin akışları olmakta. MNIST için önrek vermek gerekirse:

|label 1 0 0 0 0 0 0 0 0 0  |features 255 125 120 10...
|label 0 1 0 0 0 0 0 0 0 0  |features 155 15 200 120...

🔥 Değerler boşluk karakteri ile ayrılırken, akışlar tab karakteri ile ayrılmakta.

Örnek veride label kısmı ilgili resmin hangi rakama ait olduğunu gösteriyor. Burada "one hot encoding" mantığı uygulanmış durumda. Yani olası her durum için bir kolon var (10 rakam için 10 kolon) ve ilgili resim hangi rakama ait ise ilgili kolon 1 diğerleri 0 durumunda. Diğer akış ise piksellerin değerlerini belirtmekte. Mevcut veri 256renk gri tonlamalı biçimde olduğundan bir pikseli ifade etmek için bu değerler yeterli.

Hazırı var

Yukarıdaki işlemlerin kodları genel olarak CNTK ile ilgisi olmadığından yazıyı daha da uzatmamak adına aşağıda paylaşıyorum.
https://drive.google.com/open?id=1ZGE8vkNw6pZpx44LZcL3Ns9NIefF_ISm

Daha sonra başka bir yazıda yukarıdaki işlemlerin örnek kodlarını paylaşacağım

Dosyaları proje dizininde "Data" klasörüne koyalım ve "copy to output" özelliğini açalım.

Kod yazacaktık...

Evet gelelim işin kodlama boyutuna. Öncelikle sabitlerimizi oluşturarak başlıyoruz.

var cihaz = DeviceDescriptor.GPUDevice(0);
const string nitelikAkisAdi = "features";
const string etiketAkisAdi = "labels";
const string siniflandiriciAdi = "siniflandiriciCikti";
int[] resimBoyutu = {784};
var resimOlcusu = resimBoyutu[0];
const int etiketAdedi = 10;
const string modelDosyasi = "MNISTMLP.model";

Cihaz kısmı öğrenme işinin nerede yapılacağını belirtmek için kullanılıyor. CPU da seçenek çıkmaz iken GPU'da mevcut kartlarımızdan birisini seçmemiz gerekiyor. Tek ekran kartınız varsa 0 parametresini vererek ilgili kartı seçebilirsiniz. Birden fazla kartınız varsa tek tek isimlerine bakarak doğru kartı seçebilirsiniz.

Akış adlarını CTK biçimindeki dosyadaki akış isimleri ile uyuşmasına dikkat ederek ekliyoruz.

Sınıflandırıcı adını ileride doğrulama için kullanacağız. Ne değer verdiğimizin şimdilik pek bir önemi yok. Karışık modeller oluşturduğumuz zaman bu gerekli olmaya başlayacak.

Resim boyutu ve ölçüsü ikisi de girdinin boyutunu belirtmekte. Resim olcusu değişkeninin dizi olmasının sebebi tek boyutta 784 değer içeren bir verimiz olduğunu belirtiyor. Şayet bu bir matris olsaydı {20,20} gibi değer verecektik.

Etiket adedi yani sınıf adedi örneğimiz için rakam sayısını ifade ediyor. Kedi-Köpek resimlerinden oluşan bir veri setimiz olsaydı bu değer 2 olacaktı.

Model dosyası, öğrenme işleminin ardından oluşan modelin dosya adını tutuyor. Başka bir projeye sadece bu model dosyasını koymak bir rakam verdiğimizde tahmin etmesi yeterli olacak.

CTK dosyasındaki akışları temsil edecek satırlarla devam ediyoruz.

IList<StreamConfiguration> akisAyarlari = new[]
{
    new StreamConfiguration(nitelikAkisAdi, resimOlcusu),
    new StreamConfiguration(etiketAkisAdi, etiketAdedi)
};

Sıra geldi sinir ağını oluşturma kısmına. Burada çok basit bir derin öğrenme ağı oluşturuyor olacağız.

Oluşturacağımız model şöyle bir şey olacak:
file

Modelimizin girdi katmanı 768 düğüm, gizli katmanlarda sırayla 100, 200 , 100 düğüm ve çıktı katmanı her bir sınıfı temsil eden 10 düğüm içerecek.

Variable girdi = CNTKLib.InputVariable(resimBoyutu, DataType.Float);
Function normallestirilmisGirdi = CNTKLib.ElementTimes(Constant.Scalar(0.00390625f, cihaz), girdi);

var gizliKatman1 = TamBagliDogrusalModel(normallestirilmisGirdi, 100, cihaz);
var aktiveEdilmisGizliKatman1 = CNTKLib.ReLU(gizliKatman1);

var gizliKatman2 = TamBagliDogrusalModel(aktiveEdilmisGizliKatman1, 200, cihaz);
var aktiveEdilmisGizliKatman2 = CNTKLib.ReLU(gizliKatman2);

var gizliKatman3 = TamBagliDogrusalModel(aktiveEdilmisGizliKatman2, 100, cihaz);
var aktiveEdilmisGizliKatman3 = CNTKLib.ReLU(gizliKatman3);

var siniflandiriciCiktisi = TamBagliDogrusalModel(aktiveEdilmisGizliKatman3, etiketAdedi, cihaz, siniflandiriciAdi);

İlk iki satırda özellikle "var" kullanmadım çünkü sağ taraftaki metotların dönüş tiplerini görün istedim. CNTK kullanırken Variable ve Function isimli 2 temel tip bizim için oldukça önemli. Variable model içindeki bir değişkeni ki bu skalar, vektör veya tensor olabilir ifade ederken Function ise herhangi bir fonksiyonu ifade etmektedir.

İlk satırımızda bir Girdi değişkeni oluşturuyoruz. Bu değişkenin boyutlarını, türünü ve adını veriyoruz. İstersek buna bir isim de verebiliyoruz. Bu isimler daha sonra modelleri görselleştirdiğimizde oldukça işe yarayacaklar.

İkinci satırda ise "girdi" değişkenimizi bir çarpma fonksiyonuna sokuyoruz. 0.00390625f değeri oldukça bilimsel dursa da aslında 256'nın çarpmaya göre tersinden başka bir şey değil. Bu sayede piksel değerlerini 0 ile 1 arasında ifade edebilir hale geliyoruz. 255 için 255 * 0.00390625 \approx 1; 128 için 0.5 gibi.

Gizli katmanlı satırların hepsi aynı kod satırlarına sahip. Detayına aşağıda gireceğim TamBagliDogrusalModel metodunu çağırıyor ve çıktısını ReLU fonksiyonuna sokuyor. CNTK bir çok aktivasyon fonksiyonunu desteklemektedir. Resimli konularda genellikle ReLU ve Tanh fonksiyonları kullanıldığından ben de adete uydum.

Gelelim şu TamBagliDogrusalModel metoduna.

public static Function TamBagliDogrusalModel(Variable girdi, int ciktiBoyutu, DeviceDescriptor cihaz,string cikisAdi = "")
{
    var girdiBoyutu = girdi.Shape[0];

    int[] s = { ciktiBoyutu, girdiBoyutu };
    var agirliklar = new Parameter(shape:s, 
        dataType: DataType.Float,
        initializer:CNTKLib.GlorotUniformInitializer(CNTKLib.DefaultParamInitScale,
                                             CNTKLib.SentinelValueForInferParamInitRank,
                                             CNTKLib.SentinelValueForInferParamInitRank,
                                             1),
        device: cihaz,
        "timesParam");

    var carpmaFonksiyonu = CNTKLib.Times(agirliklar, girdi, "carpma");

    int[] s2 = { ciktiBoyutu };
    var bias = new Parameter(s2, 0.0f, cihaz, "toplama"); 
    return CNTKLib.Plus(bias, carpmaFonksiyonu, cikisAdi);
}

Bu kısımda bir önceki katmanın her bir hücresini mevcut katmana bağlayan bir yapı oluşturuyoruz. Kurduğumuz yapının görsel hali şu şekilde :
file

Buradaki denklemimiz şu:


f = W * x + b

🧪 Bildiğiniz doğru denkleminden başka bir şey değil aslında. Adının da nereden geldiği çıktı.

Girdiyi önce bir ağırlık ile çarpacağız ardından yanlılık (bias) değerini ekleyeceğiz ve arkadaşı bir sonraki katmana göndereceğiz.

Ağırlıklar kısmında "Parameter" adında yeni bir tür ile karşılaşıyoruz. Aslında "Parameter"'lar "Variable" dan türemiş nesneler ama onların en büyük özelliği eğitim esnasında öğretici tarafından değerlerinin değiştirilecek olmaları. Fikir vermesi açısından şöyle bir örnek verebilirim; girdi olarak 5 geldiğinde ben buna değeri 2 olan bir parametre ile çarpıp değeri 1 olan başka parametre eklemiş olayım. 5 * 2 +1 = 11 elde ederim. Ama 5 için ulaşmam gereken sayı 22 olsun. Bu durumda eğitici algoritma parametreler üzerinde oynama yapacaktır. Örneğin parametre olan 2 ve 1 değerlerini 3 ve 2 haline getirip 16 değerini elde edip arzu edilen değere yaklaşacak ama biz yeterince deneme hakkı sunduysak yeterli görmeyip en sonunda 4 ve 2 değerlerini bulacaktır. Girdi (variable) olan 5 e dokunmayacaktır. Bu sayede eğitim bittiğinde 5 verdiğimde hızlıca 22 yanıtı alabilirim.

"Parameter"'ın bir diğer farkı ise ilk değerini belirleyen bir fonksiyon almaktadır. Burada "GlorotUniformInitializer" fonksiyonu kullanılmıştır. Bu fonksiyon eşit olasılıklı sayılar üretmektedir. Seed değeri ise üretilen sayıların hep aynı olmasını sağlamaktadır ki algoritmanın parametrelerinde değişikliler yaptığımızda sadece değişen yaptığımız değişiklikler olsun. Aynı zamanda ağırlıklara atanmış değerlerde değişirse bu durumda yeni modelin bir önceki modelden daha mı iyi daha mı kötü olduğunu bilmemizin bir yolu kalmaz.

"carpmaFonksiyonu" değişkenine girdiyi ağırlık ile çarpmasını söylüyoruz.

Ardından bir yanlılık(bias) parametresi ekliyoruz. Buna ilk değer olarak 0 gelmesi gerektiğini söylüyoruz.

En son adımda ise çarpma işleminin ardına toplamayı ekliyoruz.

Ve bir sonraki yazıda görüşmeyi umuyorum.

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir