Klasifikasi optical digit dengan artificial neural network


by AriaPrabono in 13 days ago 753
Penulis mengasumsikan pembaca sudah memiliki pengetahuan dasar tentang machine learning /pemelajaran mesin secara umum, dan/atau artificial neural network secara khusus.
Artificial neural network (ANN) atau jaringan saraf tiruan (JST) adalah salah satu jenis model machine learning yang terinspirasi dari cara kerja otak. Seperti model machine learning lainnya, biasanya ANN digunakan untuk persoalan regresi (misal, memprediksi harga rumah berdasarkan luas dan jumlah ruangan, mempredikisi suhu suatu wilayah berdasarkan data terdahulu) atau klasifikasi (misal, menentukan jenis hewan berdasarkan citra, menetukan jenis sentimen berdasarkan teks cuitan di twitter). Di artikel ini kita akan mencoba membuat model ANN untuk klasifikasi optical digit . Penulis menggunakan IDE lazarus sebagai pengatar. Di bawah ini adalah visualisasi beberapa sampel yang terdapat pada dataset.

Persiapan

Pascal tidak menyediakan tipe data bawaan yang mampu mengakomodasi kebutuhan machine learning (dalam hal ini matriks) sekaligus melakukan operasi aritmatika dengannya. Terkait hal ini, penulis mengembangkan framework "noe " yang dapat di-clone di repositori github ini, atau juga dapat diunduh dalam format zip di sini kemudian diekstraksi. Untuk mempermudah, penulis meletakkan foldernya di C:\noe. Framework ini menyediakan kelas TTensor yang merepresentasikan array multi-dimensi, sekaligus fitur-fitur lain yang mempermudah pembuatan ANN. TTensor cukup fleksibel, karena dapat menangani tidak hanya array 1D (vektor) dan 2D (matriks), namun sembarang dimensi (tak negatif). Lebih banyak ulasan mengenai pembuatan dan manipulasi tensor dapat di laman yang dapat diakses melalui tautan ini.

Opsional: GNU Plot

Di tulisan ini akan ada bagian penampilan citra. Noe menyediakan antarmuka dengan GNU plot untuk keperluan menampilkan plot , scatter , citra, dsb. Silakan unduh pemasangnya melalui tautan ini. Pastikan executable gnuplot ada pada environment variable .

Opsional: OpenBLAS

Untuk mempercepat komputasi, noe memanfaatkan subrutin OpenBLAS dalam beberapa fungsinya, misalnya perkalian matriks. Untuk Windows, cukup sertakan berkas libopenblas.dll dalam satu direktori yang sama dengan kode sumber. Anda bisa mengunduh berkas binernya secara langsung di sini. Versi di tautan tersebut relatif lawas (sekitar 2015), namun cukup kompatibel. Pada versi yang lebih baru, anda harus kompilasi sendiri dll-nya (dan ini disarankan). Untuk sistem operasi lain, silakan cek tautan ini. Penulis sendiri belum melakukan pengujian di sistem operasi selain Windows dan Ubuntu Linux 19.10.

Buat proyek baru

Di jendela lazarus, klik File > New... > Project > Simple Program > OK. Kemudia sertakan framework noe pada search path dengan cara klik Project > Project Options... > Compiler Options > Paths. Kemudian pada bagian "Other unit files (-Fu)", tambahkan path noe\src (disesuaikan dengan di mana anda mengunduh dan mengekstraksinya). image-20200206203813742
Kemudian coba ketikkan dan kompilasi kode di bawah ini.

uses
  sysutils,
  noe,
  noe.Math,
  noe.neuralnet,
  noe.plot.gnuplot,
  noe.utils;
begin
end.

Memuat dataset

Jika sejauh ini tidak ada kesalahan, kita dapat menuju ke langkah selanjutnya, yaitu memuat dataset. Deklarasikan variabel yang diperlukan untuk menampung dataset. Praktik umum dalam machine learning adalah membagi dataset menjadi setidaknya dua bagian: dataset training (untuk melatih model ANN) serta dataset testing (untuk melakukan pengujian serta evaluasi performa keakuratan model). Dataset training dan testing harus saling terpisah: tak ada sampel yang overlap .

var
  DatasetTrain, DatasetTest: TTensor;
Dataset “optical recognition of handwritten digits data set” ini berasal dari UCI Machine Learning Repository. Untuk mempermudah, dataset disertakan pada repositori noe di folder noe\examples\datasets. Keduanya berformat comma-separated values (CSV). Disertakan pula fungsi ReadCSV (dalam unit noe.utils) untuk memuat berkas CSV ke dalam tensor sebagai berikut:

begin
    DatasetTrain := ReadCSV('C:\noe\examples\datasets\optdigits-train.csv');
    DatasetTest  := ReadCSV('C:\noe\examples\datasets\optdigits-test.csv');
    WriteLn('Ukuran dataset training : ', DatasetTrain.Shape[0], 'x', DatasetTrain.Shape[1]);
    WriteLn('Ukuran dataset testing  : ', DatasetTest.Shape[0], 'x', DatasetTest.Shape[1]);
    WriteLn;
    
    readln;
end.
Jika dijalankan, akan muncul tampilan di bawah ini. Seperti ditunjukkan pada gambar, dataset memiliki 65 kolom. Pada 64 kolom pertama terdapat informasi berupa intensitas pixel dari 0 hingga 16. Jumlah 64 kolom ini berasal dari ukuran citra masing-masing sampel yaitu 8x8 pixel yang kemudian diratakan menjadi satu baris. Kemudian satu kolom terakhir berisi informasi label digit "0", "1", "2", ..., atau "9", image-20200206200100425
Kemudian feature (X) dan label (y) kita pisah, baik untuk dataset training maupun testing:

var
  ...
  ...
  XTrain, yTrain, XTest, yTest: TTensor;
begin
  ...
  ...
  
  // Dari DatasetTrain, ambil mulai dari kolom pertama (index 0),
  // sebanyak 64 kolom.
  XTrain := GetColumnRange(DatasetTrain, 0, 64);
  
  // Menyesuaikan.
  XTest  := GetColumnRange(DatasetTest, 0, 64);
  
  // Dari DatasetTrain, ambil kolom ke-65 (index 64)
  yTrain := GetColumn(DatasetTrain, 64);
  
  // Menyesuaikan.
  yTest  := GetColumn(DatasetTest, 64);
  
  ReadLn
end.
image-20200209131423557 Gambar di atas mengilustrasikan struktur dari dataset yang sedang kita gunakan. Mari kita coba ambil dan tampilkan contoh salah satu sampel dari dataset training. Anda bisa melewati langkah ini jika tidak melakukan pemasangan GNU Plot .

  ...
  ...
  
  // Contoh visualisasi salah satu sampel dari dataset training di
  // baris dengan index 100
  ImageShow(
    GetRow(XTrain, 100).Reshape([8, 8]),           // Data.
    'Digit: ' + IntToStr(Round(yTrain.GetAt(100))) // Judul.
  );
  
  ReadLn;
end.
image-20200208150205045
Kemudian, perlu dilakukan konversi label menjadi representasi biner. Proses ini disebut juga one-hot encoding . Misal, label "0" direpresentasikan dengan [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], label "1" direpresentasikan dengan [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], dst. Proses one-hot encoding dapat dilakukan dengan kelas TOneHotEncoder. Hanya label pada dataset training saja yang perlu di-encode . Tambahkan beberapa baris kode berikut:

var
  ...
  ...
  Encoder: TOneHotEncoder;
begin  
  ...
  ...
  Encoder := TOneHotEncoder.Create;
  yTrain  := Encoder.Encode(yTrain);
  
  // cetak 5 baris pertama label yang telah di-encode
  WriteLn('Contoh label dataset training ter-encode:');
  PrintTensor(GetRowRange(yTrain, 0, 5));
  WriteLn;
  ReadLn;
end.     
image-20200209131622958
Catatan: Encoding ini dilakukan agar kita bisa memperoleh tidak hanya prediksi label digit, namun juga nilai peluang: - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]: "peluang bahwa sampel merupakan digit '0' adalah 1, serta peluang sampel tersebut merupakan digit lainnya adalah 0". - [0, 0, 0, 0, 1, 0, 0, 0, 0, 0]: "peluang bahwa sampel merupakan digit '4' adalah 1, serta peluang sampel tersebut merupakan digit lainnya adalah 0" - dst.

Persiapan model klasifikasi

Deklarasikan model ANN berupa variabel MyNeuralNet dengan tipe TModel. Kemudian inisialisasi MyNeuralNet dengan beberapa layer.

var
  ...
  ...
  MyNeuralNet: TModel;
begin
  ...
  ...
  
  // Inisialisasi model
  MyNeuralNet := TModel.Create([
    TDenseLayer.Create(64, 128),
    TLeakyReLULayer.Create(0.2),
    TDenseLayer.Create(128, 64),
    TLeakyReLULayer.Create(0.2),
    TDenseLayer.Create(64, 10),
    TSoftMaxLayer.Create(1)
  ]);
  
  ReadLn;
end.
TDenseLayer pertama mengonversi feature masukan berjumlah 64 kolom menjadi 128 kolom (atau dalam konteks ANN, "kolom" disebut "jumlah neuron") melalui transformasi linear, kemudian diikuti fungsi aktivasi nonlinear rectified linear unit (ReLU ) dengan leakiness =0.2. Kemudian kembali diikuti dengan transformasi linear kedua yang mengonversi luaran layer sebelumnya yang berjumlah 128 menjadi 64, yang juga kemudian diikuti oleh fungsi aktivasi ReLU yang sama dengan sebelumnya. TDenseLayer yang terakhir akan mengonversi luaran layer sebelumnya dengan jumlah neuron 64 menjadi 10, sejumlah dengan kelas yang kita punya. Dalam hal ini, jumlah jenis digit ("0" s.d. "9"). Terakhir, softmax layer akan mengonversi luaran dense layer terakhir menjadi distribusi probabilitas. Prediksi dapat dilakukan dengan memanggil method MyNeuralNet.Eval(Input). Sebagai contoh, kita akan melakukan prediksi label digit dari data di baris 100 (yaitu digit "4", sesuai contoh di atas).

begin
  ...
  ...
  
  WriteLn('Contoh prediksi mula-mula:');
  PrintTensor(MyNeuralNet.Eval(GetRow(XTrain, 100, True)));
  WriteLn;
  ReadLn;
end. 
image-20200209132114321
Seperti ditunjukkan pada gambar, kita memperoleh luaran [[0.02, 0.00, 0.01, 0.42, 0.07, 0.36, 0.00, 0.06, 0.00, 0.07]]. Jika mengacu pada luaran, digit yang terprediksi adalah "3" karena memiliki nilai peluang terbesar, yaitu 0.42. Sementara, nilai peluang untuk digit "4" adalah 0.07, sehingga prediksi final bukan "4", melainkan "3". Tentu saja prediksi ini keliru dan wajar, karena model ANN kita masih belum dilatih. Luaran yang anda peroleh bisa saja berbeda dengan yang dihasilkan pada contoh, karena bobot internal MyNeuralNet diinisialisasi secara acak. Jika ingin mendapatkan hasil yang sama dengan contoh di atas, atur "RandSeed := 1;" di awal program. Catatan: Untuk melakukan prediksi keseluruhan baris, cukup panggil fungsi MyNeuralNet.Eval(XTrain) untuk dataset training, atau MyNeuralNet.Eval(XTest) untuk dataset testing. Luarannya adalah distribusi probabilitas sejumlah baris pada input.

Melatih model ANN

Model ANN dilatih dengan cara memperbarui bobot-bobotnya secara berulang (optimasi iteratif). Ada banyak strategi untuk memperbarui bobot dalam berbagai variasi algoritma. Contohnya, stochastic gradient descent , RMSPROP , adam , dsb. Noe menyediakan implementasi dari beberapa algoritma tersebut. Sertakan unit noe.optimizer dalam uses clause . Kemudian deklarasikan serta inisialisasi variabel optimizer . Pada contoh ini, penulis menggunakan Adam optimizer karena ia digunakan secara luas di berbagai penelitian dan aplikasi. Dapat dikatakan bahwa Adam adalah pilihan pertama untuk banyak kasus optimasi ANN (hingga artikel ini ditulis). Deklarasikan pula variable pembantu lainnya untuk menampung hasil prediksi serta menampung akurasi model.

uses
  ...
  ...
  noe.optimizer; 
  
var
  ...
  ...
  optimizer: TAdamOptimizer;
  yPred,              // menampung hasil prediksi.
  Loss: TVariable;    // menampung nilai loss sementara.
  TrainingAcc,        // menampung akurasi model terhadap data training.
  TestingAcc: double; // menampung akurasi model terhadap data testing.
  i: integer;
begin
  ...
  ...
  
  // Proses melatih model ANN
  optimizer := TAdamOptimizer.Create;
  for i := 1 to 150 do
  begin
    yPred := MyNeuralNet.Eval(XTrain);
    Loss  := CrossEntropyLoss(yPred, yTrain);
    optimizer.UpdateParams(Loss, MyNeuralNet.Params);
  end;
  
  // Evaluasi hasil training
  TrainingAcc := AccuracyScore(Encoder.Decode(ypred.Data),
    Encoder.Decode(ytrain));
  WriteLn('Akurasi training : ', TrainingAcc: 2: 3);
  // Evaluasi hasil testing
  yPred      := MyNeuralNet.Eval(XTest);
  TestingAcc := AccuracyScore(Encoder.Decode(ypred.Data), yTest);
  WriteLn('Akurasi testing  : ', TestingAcc: 2: 3);   
  
  ReadLn;
end.
Tahapan optimasi model ANN secara umum adalah sebagai berikut, dilakukan secara iteratif: 1. Lakukan prediksi dengan memanggil method MyNeuralNet.Eval(XTrain). 2. Hitung nilai loss atau tingkat kesalahan prediksi model. Fungsi loss yang umum digunakan untuk klasifikasi multi-kelas adalah cross-entropy (CrossEntropyLoss(yPred, yTrain)). 3. Propagasi balik (backpropagate ) nilai loss untuk melakukan pembarauan parameter-parameter internal ANN sehingga diharapkan. Backpropagation dan pembaruan terjadi dalam method optimizer.UpdateParams(Loss, MyNeuralNet.Params). Jika program dieksekusi, proses training akan berjalan. Silakan menunggu beberapa saat. Lama atau cepat, tergantung spesifikasi komputer anda. Jika menggunakan backend OpenBLAS, mungkin tidak sampai satu menit. Kalau selesai, seharusnya akan muncul tampilan seperti pada gambar di bawah. Nilai tingkat kesalahan atau loss cenderung terus menerus turun tiap iterasi. Ini menunjukkan bahwa model kita memang mampu "belajar". Nilai akurasi digunakan untuk mengevaluasi performa model. Diperoleh akurasi training sebesar 92.3%, yang dapat dikatakan cukup tinggi. Ini menunjukkan bahwa model kita mampu mempelajari fungsi yang memetakan sampel-sample di feature ke label terkait dengan baik. Namun yang lebih penting adalah informasi akurasi testing . Mengapa akurasi testing lebih penting? Karena inti persoalan (supervised ) machine learning adalah "bagaimana model kita dapat memprediksi dengan baik data yang tak pernah dilihat sebelumnya ", dalam hal ini, dataset testing -- Ingat bahwa sejauh ini kita hanya melatih model kita berdasarkan dataset training saja, dan saat training, model belum pernah "melihat" dataset testing. image-20200209133300078
Untuk akurasi testing... Ya... Boleh juga lah. 88.6% itu lumayan baik. Mari kita lihat secara visual apakah model dapat memprediksi label dari data pada dataset testing.

var
  ...
  ...
  ImageSample: TTensor;
  PredictedLabel, ActualLabel: integer;
  PredictedProbability: double;
  
begin
  ...
  ...
  
  // Contoh prediksi dengan model terlatih
  ImageSample    := GetRow(XTest, 10, True);
  ActualLabel    := Round(yTest.GetAt(10));
  PredictedLabel := Round(ArgMax(MyNeuralNet.Eval(ImageSample).Data, 1).Val[0]);
  PredictedProbability := Max(MyNeuralNet.Eval(ImageSample)).Data.Val[0];
  ImageShow(
    ImageSample.Reshape([8, 8]),
    'Pred: ' + IntToStr(PredictedLabel) + '; ' +
    'Prob: ' + FloatToStrF(PredictedProbability, ffFixed, 2, 3) + '; ' +
    'True: ' + IntToStr(ActualLabel)
  );   
  
end.
image-20200209143309127
Kebetulan, untuk sampel ini, tebakannya tepat: terprediksi sebagai digit "0" dengan nilai peluang 0.83 dan memang digit sebenarnya adalah "0". Bagaimana dengan sampel test lainnya? Silakan bereksperimen sendiri.

Kode sumber lengkap


program tutorial_digit;
uses
  SysUtils,
  noe,
  noe.Math,
  noe.neuralnet,
  noe.plot.gnuplot,
  noe.utils,
  noe.optimizer;
var
  DatasetTrain, DatasetTest: TTensor;
  XTrain, yTrain, XTest, yTest: TTensor;
  Encoder: TOneHotEncoder;
  MyNeuralNet: TModel;
  optimizer: TAdamOptimizer;
  yPred,              // menampung hasil prediksi.
  Loss: TVariable;    // menampung nilai loss sementara.
  TrainingAcc,        // menampung akurasi model terhadap data training.
  TestingAcc: double; // menampung akurasi model terhadap data testing.
  i: integer;
  ImageSample: TTensor;
  PredictedLabel, ActualLabel: integer;
  PredictedProbability: double;
begin
  RandSeed     := 1;
  DatasetTrain := ReadCSV('C:\noe\examples\datasets\optdigits-train.csv');
  DatasetTest  := ReadCSV('C:\noe\examples\datasets\optdigits-test.csv');
  WriteLn('Ukuran dataset training : ', DatasetTrain.Shape[0], 'x', DatasetTrain.Shape[1]);
  WriteLn('Ukuran dataset testing  : ', DatasetTest.Shape[0], 'x', DatasetTest.Shape[1]);
  WriteLn;
  // Dari DatasetTrain, ambil mulai dari kolom pertama (index 0),
  // sebanyak 64 kolom.
  XTrain := GetColumnRange(DatasetTrain, 0, 64);
  // Menyesuaikan.
  XTest := GetColumnRange(DatasetTest, 0, 64);
  // Dari DatasetTrain, ambil kolom ke-65 (index 64)
  yTrain := GetColumn(DatasetTrain, 64);
  // Menyesuaikan.
  yTest := GetColumn(DatasetTest, 64);
  // Contoh visualisasi salah satu sampel dari dataset training
  ImageShow(
    GetRow(XTrain, 100).Reshape([8, 8]),           // Data.
    'Digit: ' + IntToStr(Round(yTrain.GetAt(100))) // Judul.
    );
  Encoder := TOneHotEncoder.Create;
  yTrain  := Encoder.Encode(yTrain);
  // cetak 5 baris pertama label yang telah di-encode
  WriteLn('Contoh label dataset training ter-encode:');
  PrintTensor(GetRowRange(yTrain, 0, 5));
  WriteLn;
  // Inisialisasi model
  MyNeuralNet := TModel.Create([
    TDenseLayer.Create(64, 128),
    TLeakyReLULayer.Create(0.2),
    TDenseLayer.Create(128, 64),
    TLeakyReLULayer.Create(0.2),
    TDenseLayer.Create(64, 10),
    TSoftMaxLayer.Create(1)
  ]);
  WriteLn('Contoh prediksi mula-mula:');
  PrintTensor(MyNeuralNet.Eval(GetRow(XTrain, 100, True)));
  WriteLn;
  // Proses melatih model ANN
  optimizer := TAdamOptimizer.Create;
  for i := 1 to 100 do
  begin
    yPred := MyNeuralNet.Eval(XTrain);
    Loss  := CrossEntropyLoss(yPred, yTrain);
    optimizer.UpdateParams(Loss, MyNeuralNet.Params);
  end;
  // Evaluasi hasil training
  TrainingAcc := AccuracyScore(Encoder.Decode(ypred.Data),
    Encoder.Decode(ytrain));
  WriteLn('Akurasi training : ', TrainingAcc: 2: 3);
  // Evaluasi hasil testing
  yPred      := MyNeuralNet.Eval(XTest);
  TestingAcc := AccuracyScore(Encoder.Decode(ypred.Data), yTest);
  WriteLn('Akurasi testing  : ', TestingAcc: 2: 3);
  // Contoh prediksi dengan model terlatih
  ImageSample    := GetRow(XTest, 10, True);
  ActualLabel    := Round(yTest.GetAt(10));
  PredictedLabel := Round(ArgMax(MyNeuralNet.Eval(ImageSample).Data, 1).Val[0]);
  PredictedProbability := Max(MyNeuralNet.Eval(ImageSample)).Data.Val[0];
  ImageShow(
    ImageSample.Reshape([8, 8]),
    'Pred: ' + IntToStr(PredictedLabel) + '; ' +
    'Prob: ' + FloatToStrF(PredictedProbability, ffFixed, 2, 3) + '; ' +
    'True: ' + IntToStr(ActualLabel)
  );
  ReadLn;
end.
Local Business Directory, Search Engine Submission & SEO Tools FreeWebSubmission.com SonicRun.com