Deep Learning adalah pendekatan yang bagus untuk menangani data tidak terstruktur seperti teks, suara, video, dan gambar. Banyak penerapan Deep Learning dalam klasifikasi citra dan pendeteksian citra, seperti mengklasifikasi citra penyakit tanaman jagung yang akan kita gunakan pada artikel ini. Dataset penyakit tanaman jagung diperoleh dari PlantVillage-Dataset.
Tujuan dari artikel ini adalah kita dapat membedakan mana daun tanaman jagung yang sehat dan mana daun tanaman jagung yang sakit, dan apa nama penyakit dari tiga jenis penyakit yang kita gunakan untuk di training.
Target dari artikel ini, kita dapat memperoleh model dengan tingkat accuracy > 80 % baik pada uji ke data validasi dan juga data test.
Berikut adalah proses untuk mengklasifikasikan citra tiga jenis penyakit tanaman jagung yang gejalanya dapat dilihat pada daun dan satu jenis daun tanaman jagung sehat.
Langkah-langkah dalam melakukan klasifikasi menggunakan Convolutional Neural Network adalah sebagai berikut:
Berikut dalah library yang diperlukan.
# Data Wrangling
library(tidyverse)
# Image Manipulation
library(imager)
# Deep Learning
library(keras)
# Model Evaluation
library(caret)
# Conda Environment
use_condaenv("tf")
# Set Graphic Theme
theme_set(theme_minimal())
options(scipen = 999)Kemudian, Untuk memastikan apakah model Keras telah terhubung dan dapat digunakan, kita perlu mengujinya dengan cara sebagai berikut.
p <- keras_model_sequential(name = "Test Keras") %>%
# Hidden Layer Pertama
layer_dense(units = 3,
input_shape = 3,
activation = "sigmoid",
name = "Hidden_layer"
) %>%
# Output Layer
layer_dense(units = 2, activation = "sigmoid", name = "output")
p## Model
## Model: "Test Keras"
## ________________________________________________________________________________
## Layer (type) Output Shape Param #
## ================================================================================
## Hidden_layer (Dense) (None, 3) 12
## ________________________________________________________________________________
## output (Dense) (None, 2) 8
## ================================================================================
## Total params: 20
## Trainable params: 20
## Non-trainable params: 0
## ________________________________________________________________________________
Kemudian kita perlu mengeksplorasi data yang akan kita gunakan, salah satunya dengan melihat dimensi data citra baik tinggi maupun lebar citra. Karena dimensi citra yang berbeda-beda akan mempengaruhi model yang akan dibuat.
Data training disimpan dalam folder train, dimana setiap kelas/kategori penyakit disimpan dalam sub-folder masing-masing, dan kemudian untuk data testing, semua citra dari setiap kelas/kategori disimpan jadi satu folder dalam folder test. Skema folder sebagai berikut :
Ekplorasi dilakukan dengan melakukan input data, kemudian kita melakukan mengecekan pada list folder
folder_list <- list.files("data_input/train/")
folder_list## [1] "Common_Rust" "Gray_Leaf_Spot" "Healthy"
## [4] "Northern_Leaf_Blight"
Terdapat tiga (3) folder / kelas ataupun label yaitu “Common_Rust”, “Gray_Leaf_Spot”, “Healthy” dan “Northern_Leaf_Blight”.
Kemudian kita perlu mengakses file yang masing-masing ada di dalam folder label, maka path/direktorinya perlu ditambahkan.
folder_path <- paste0("data_input/train/", folder_list, "/")
folder_path## [1] "data_input/train/Common_Rust/"
## [2] "data_input/train/Gray_Leaf_Spot/"
## [3] "data_input/train/Healthy/"
## [4] "data_input/train/Northern_Leaf_Blight/"
Kemudian kita perlu mengambil setiap file yang ada di ketiga folder label di atas dan memeriksanya, baik memeriksa path, jumlah citra dan dimensinya.
# Mendapat nama citra
file_name <- map(folder_path,
function(x) paste0(x, list.files(x))
) %>%
unlist()
# Mengecek 6 file teratas
head(file_name)## [1] "data_input/train/Common_Rust/RS_Rust 1564.JPG"
## [2] "data_input/train/Common_Rust/RS_Rust 1565.JPG"
## [3] "data_input/train/Common_Rust/RS_Rust 1566.JPG"
## [4] "data_input/train/Common_Rust/RS_Rust 1567.JPG"
## [5] "data_input/train/Common_Rust/RS_Rust 1568.JPG"
## [6] "data_input/train/Common_Rust/RS_Rust 1570.JPG"
# Mengecek 6 file terbawah
tail(file_name)## [1] "data_input/train/Northern_Leaf_Blight/fe2aa542-8ace-4e0d-8315-48d4c6d023ef___RS_NLB 4271.JPG"
## [2] "data_input/train/Northern_Leaf_Blight/fed381d0-4407-4ec5-a178-d70ec1ca93d3___RS_NLB 3914.JPG"
## [3] "data_input/train/Northern_Leaf_Blight/ff1721eb-609e-4476-983d-a372b7469ab0___RS_NLB 4039 copy 2.jpg"
## [4] "data_input/train/Northern_Leaf_Blight/ff8577c3-ad10-4272-84f6-b7c6d6042d1f___RS_NLB 3506.JPG"
## [5] "data_input/train/Northern_Leaf_Blight/ffa82f14-8320-469b-a621-6475c92a4bb8___RS_NLB 3649.JPG"
## [6] "data_input/train/Northern_Leaf_Blight/ffbc3745-0a6d-4145-8f92-781edc626d9f___RS_NLB 4260.JPG"
Karena kita sudah dapat meload semua citra, kita dapat memeriksa jumlah citra yang akan digunakan sebagai train set.
# Mengecek banyaknya citra
length(file_name)## [1] 3702
Total citra dari keempat kelas / label adalah sebanyak 3702 citra.
Kemudian setelah mendapatkan nama file, kita dapat mencoba untuk melihat citranya secara acak. Dalam kasus ini, kita akan melihat 6 citra secara acak.
# Pemilihan citra secara acak
set.seed(99)
sample_image <- sample(file_name, 6)
# Meload citra kedalam R
img <- map(sample_image, load.image)
# Menampilkan citra
par(mfrow = c(2, 3)) # Create 2 x 3 image grid
map(img, plot)## [[1]]
## Image. Width: 256 pix Height: 256 pix Depth: 1 Colour channels: 3
##
## [[2]]
## Image. Width: 256 pix Height: 256 pix Depth: 1 Colour channels: 3
##
## [[3]]
## Image. Width: 256 pix Height: 256 pix Depth: 1 Colour channels: 3
##
## [[4]]
## Image. Width: 256 pix Height: 256 pix Depth: 1 Colour channels: 3
##
## [[5]]
## Image. Width: 256 pix Height: 256 pix Depth: 1 Colour channels: 3
##
## [[6]]
## Image. Width: 256 pix Height: 256 pix Depth: 1 Colour channels: 3
Salah satu aspek penting dalam klasifikasi citra adalah memahami dimensi dari citra yang akan digunakan pada data train. Kita perlu mengetahui distribusi dimensi gambar untuk membuat dimensi input yang tepat untuk membangun model deep learning. Mari kita periksa properti dari gambar pertama.
Kemudian kita dapat memeriksa dimensi setiap citra dengan menggunakan function berikut.
# Fungsi untuk mendapatkan dimensi tinggi dan lebar suatu citra
get_dim <- function(x){
img <- load.image(x)
df_img <- data.frame(height = height(img),
width = width(img),
filename = x
)
return(df_img)
}
get_dim(file_name[1])Dari fungsi di atas diperoleh bahwa dimensi citra RS_Rust 1564.JPG memiliki tinggi 256 pixel dan lebar 256 pixel.
Kemudian fungsi di atas dapat kita terapkan untuk mengetahui dimensi beberapa citra sekaligus secara random menggunakan fungsi berikut.
# Mendapatkan 1000 contoh citra secara random
set.seed(123)
sample_file <- sample(file_name, 1000)
# menjalankan fungsi get_dim() untuk setiap citra
file_dim <- map_df(sample_file, get_dim)
head(file_dim, 10)Dari hasil pemeriksaan dimensi citra secara acak, diketahui bahwa dimensi citra yang akan digunakan seragam dimensinya, untuk lebih detail kita dapat memeriksa statistik dari dimensi citra tersebut dengan menggunakan fungsi berikut.
# Statistik dari dimensi image
summary(file_dim)## height width filename
## Min. :256 Min. :256 Length:1000
## 1st Qu.:256 1st Qu.:256 Class :character
## Median :256 Median :256 Mode :character
## Mean :256 Mean :256
## 3rd Qu.:256 3rd Qu.:256
## Max. :256 Max. :256
Diketahui bahwa semua citra seragam dengan dimensi panjang dan lebar sama yaitu 256 pixel.
Kita perlu mengetahui dimensi dari citra yang akan digunakan adalah karena dengan memahami dimensi gambar akan membantu kita pada bagian proses selanjutnya yaitu pra-pemrosesan data sebab gambar yang digunakan untuk training model harus memiliki dimensi yang sama. Jika ukuran citra kecil, misalkan 30 x 30, maka proses training model akan lebih cepat, tetapi banyak informasi yang hilang, sedangkan ukuran citra yang besar misalkan 250 x 250, maka memerlukan waktu training model yang lebih lama. Dimensi citra yang umum dipakai adalah 64 x 64 pixel.
Pada data preprocessing, kita perlu melakukan penyeragaman dimensi citra agar dimensinya sama panjang dan lebar, kita akan menggunakan dimensi citra 64 pixel dan saat mentraining model menggunakan batch_size = 32.
# Menentukan dimensi tinggi dan lebar citra
target_size <- c(64, 64)
# Batch size untuk training model
batch_size <- 32Pada data preprocessing kita juga perlu melakukan data augmentation, yaitu dari satu citra kita membuat beberapa citra dengan angle yang berbeda-beda dengan cara flip, rotate, zoom in, crop, contrast, blur dan lain-lain, dengan data augmentation kita dapat mendapatkan jumlah data yang sangat banyak.
Berikut adalah fungsi untuk membuat data augmentation.
# Image Generator
train_data_gen <- image_data_generator(rescale = 1/255, # Scaling pixel value
horizontal_flip = T, # Flip image horizontally
vertical_flip = T, # Flip image vertically
rotation_range = 45, # Rotate image from 0 to 90 degrees
zoom_range = 0.5, # Zoom in or zoom out range
validation_split = 0.2 # 20% data as validation data
)Kemudian kita melakukan training dataset dan validation dataset dengan memasukkan fungsi image_data_generator, fungsi target-size, fungsi batch_size yang telah kita assign di atas dengan fungsi sebagai berikut.
# Training Dataset
train_image_array_gen <- flow_images_from_directory(directory = "data_input/train/", # Folder of the data
target_size = target_size, # target of the image dimension (100 x 100)
color_mode = "rgb", # use RGB color
batch_size = batch_size ,
seed = 123, # set random seed
subset = "training", # declare that this is for training data
generator = train_data_gen
)
# Validation Dataset
val_image_array_gen <- flow_images_from_directory(directory = "data_input/train/",
target_size = target_size,
color_mode = "rgb",
batch_size = batch_size ,
seed = 123,
subset = "validation", # declare that this is the validation data
generator = train_data_gen
)Terdapat proporsi training vs validation dataset sebesar 2963 citra vs 739 citra (80 : 20). Pembagian ini diperlukan agar kita dapat menguji model yang telah di training dengan menggunakan training dataset sebanyak 2963 citra dari tiga kategori lalu kemudian menguji hasil training tersebut dengan citra yang baru yang belum pernah dilihat oleh model yaitu dari citra pada validation dataset, sehingga kita dapat menghitung akurasi dari model yang kita buat pada confusion matrix.
Tahap terakhir untuk data preprocessing yaitu kita perlu menyiapkan beberapa variabel yaitu : 1. Jumlah training samples 2. Jumlah validation samples 3. Jumlah target class/kategori 4. Mendapatkan proporsi data, apakah balance atau tidak
# Number of training samples
train_samples <- train_image_array_gen$n
# Number of validation samples
valid_samples <- val_image_array_gen$n
# Number of target classes/categories
output_n <- n_distinct(train_image_array_gen$classes)
# Get the class proportion
table("\nFrequency" = factor(train_image_array_gen$classes)
) %>%
prop.table()##
## Frequency
## 0 1 2 3
## 0.3084711 0.1252109 0.3003712 0.2659467
Jika dilihat dari proporsinya, jumlah citra dari masing-masing label tidak seimbang, dimana : 0 = Common_Rust sebanyak 30.8% 1 = Gray_Leaf_Spot sebanyak 12.5% 2 = Healthy sebanyak 30% 3 = Northern_Leaf_Blight sebanyak 26.6%
Proporsi kelas data setiap kelas tidak sama, jika proporsi data berbeda, maka model akan cenderung lebih terlatih pada data dengan jumlah yang paling banyak, sehingga model kurang akurat untuk mengklasifikasikan data dengan jumlah yang sedikit jika nantinya diaplikasikan pada level produksi. Untuk membuktikan hal ini, kita lanjutkan analysisnya dan kita lihat hasilnya pada confusion matriks.
Selanjutnya kita membangun model arsitektur deep learning. Pada tahap awal kita membuat model arsitektur yang sederhana dahulu, apabila diperoleh hasil yang dinilai kurang baik, maka kita dapat memodifikasi model arsitekturnya dan hyper parameternya untuk memperoleh hasil yang diharapkan.
Kita akan membangun model arsitektur sederhana dengan layer sebagai berikut :
relu.softmax.Kemudian kita mengatur dimensi citra dan channel citra yang digunakan, karena kita menggunakan citra RGB, maka angka akhirnya adalah 3.
# input shape of the image
c(target_size, 3) ## [1] 64 64 3
Telah diatur dimensi 64 x 64 pixel dengan channel RGB.
# Set Initial Random Weight
tensorflow::tf$random$set_seed(123)
model_sederhana <- keras_model_sequential(name = "model_sederhana") %>%
# Convolution Layer
layer_conv_2d(filters = 32,
kernel_size = c(3,3),
padding = "same",
activation = "relu",
input_shape = c(target_size, 3)
) %>%
# Max Pooling Layer
layer_max_pooling_2d(pool_size = c(2,2)) %>%
# Flattening Layer
layer_flatten() %>%
# Dense Layer
layer_dense(units = 16,
activation = "relu") %>%
# Output Layer
layer_dense(units = output_n,
activation = "softmax",
name = "Output")
model_sederhana## Model
## Model: "model_sederhana"
## ________________________________________________________________________________
## Layer (type) Output Shape Param #
## ================================================================================
## conv2d (Conv2D) (None, 64, 64, 32) 896
## ________________________________________________________________________________
## max_pooling2d (MaxPooling2D) (None, 32, 32, 32) 0
## ________________________________________________________________________________
## flatten (Flatten) (None, 32768) 0
## ________________________________________________________________________________
## dense (Dense) (None, 16) 524304
## ________________________________________________________________________________
## Output (Dense) (None, 4) 68
## ================================================================================
## Total params: 525,268
## Trainable params: 525,268
## Non-trainable params: 0
## ________________________________________________________________________________
Kita dapat mulai memasukkan data ke dalam model. Kemudian kita mengkompilasi model dengan menentukan loss function dan optimizer.
Pada model pertama ini kita dapat menggunakan 50 epoch untuk melatih data. Untuk klasifikasi multilabel, loss function yang kita gunakan adalah cross-entropy. Dalam kasus ini akna menggunakan optimizer adam dengan learning rate 0.001. Model akan dievaluasi dengan data validasi dan generator.
model_sederhana %>%
compile(
loss = "categorical_crossentropy",
optimizer = optimizer_adam(lr = 0.001),
metrics = "accuracy"
)
# Fit data into model
history <- model_sederhana %>%
fit(
# training data
train_image_array_gen,
# training epochs
steps_per_epoch = as.integer(train_samples / batch_size),
epochs = 50,
# validation data
validation_data = val_image_array_gen,
validation_steps = as.integer(valid_samples / batch_size)
)
plot(history)## `geom_smooth()` using formula 'y ~ x'
Kemudian kita dapat menyimpan model sederhana di atas kedalam file hdf5, kita beri nama corn_disease_sederhana
save_model_hdf5(model_sederhana, "corn_disease_sederhana.hdf5")Sekarang kita akan mengevaluasi model lebih lanjut dan memperoleh confusion matrix menggunakan data validasi dari generator.
Pertama kita perlu mendapatkan nama file gambar yang digunakan sebagai validasi data. Dari nama file, kita akan mengekstrak label kategoris sebagai nilai aktual dari variabel target.
val_data <- data.frame(file_name = paste0("data_input/train/", val_image_array_gen$filenames)) %>%
mutate(class = str_extract(file_name, "Common_Rust|Gray_Leaf_Spot|Healthy|Northern_Leaf_Blight"))
head(val_data, 10)tail(val_data, 10)Kemudian kita perlu mengkonversi citra menjadi array dengan fungsi berikut
# Function to convert image to array
image_prep <- function(x) {
arrays <- lapply(x, function(path) {
img <- image_load(path, target_size = target_size,
grayscale = F # Set FALSE if image is RGB
)
x <- image_to_array(img)
x <- array_reshape(x, c(1, dim(x)))
x <- x/255 # rescale image pixel
})
do.call(abind::abind, c(arrays, list(along = 1)))
}test_x <- image_prep(val_data$file_name)
# Check dimension of testing data set
dim(test_x)## [1] 739 64 64 3
# Predict label on array
pred_test <- predict_classes(model_sederhana, test_x)
head(pred_test, 10)## [1] 0 0 0 0 0 0 0 0 0 0
Untuk mempermudah interpretasi prediksi, perlu mengkonversi encoding menjadi label class yang lebih sempurna
# Convert encoding to label
decode <- function(x){
case_when(x == 0 ~ "Common_Rust",
x == 1 ~ "Gray_Leaf_Spot",
x == 2 ~ "Healthy",
x == 3 ~ "Northern_Leaf_Blight"
)
}
pred_test <- sapply(pred_test, decode)
head(pred_test, 10)## [1] "Common_Rust" "Common_Rust" "Common_Rust" "Common_Rust" "Common_Rust"
## [6] "Common_Rust" "Common_Rust" "Common_Rust" "Common_Rust" "Common_Rust"
Evaluasi Model dengan confusion matrix
confusionMatrix(as.factor(pred_test),
as.factor(val_data$class)
)## Confusion Matrix and Statistics
##
## Reference
## Prediction Common_Rust Gray_Leaf_Spot Healthy Northern_Leaf_Blight
## Common_Rust 228 0 0 0
## Gray_Leaf_Spot 0 55 0 7
## Healthy 0 0 222 0
## Northern_Leaf_Blight 0 37 0 190
##
## Overall Statistics
##
## Accuracy : 0.9405
## 95% CI : (0.9209, 0.9564)
## No Information Rate : 0.3085
## P-Value [Acc > NIR] : < 0.00000000000000022
##
## Kappa : 0.9176
##
## Mcnemar's Test P-Value : NA
##
## Statistics by Class:
##
## Class: Common_Rust Class: Gray_Leaf_Spot Class: Healthy
## Sensitivity 1.0000 0.59783 1.0000
## Specificity 1.0000 0.98918 1.0000
## Pos Pred Value 1.0000 0.88710 1.0000
## Neg Pred Value 1.0000 0.94535 1.0000
## Prevalence 0.3085 0.12449 0.3004
## Detection Rate 0.3085 0.07442 0.3004
## Detection Prevalence 0.3085 0.08390 0.3004
## Balanced Accuracy 1.0000 0.79350 1.0000
## Class: Northern_Leaf_Blight
## Sensitivity 0.9645
## Specificity 0.9317
## Pos Pred Value 0.8370
## Neg Pred Value 0.9863
## Prevalence 0.2666
## Detection Rate 0.2571
## Detection Prevalence 0.3072
## Balanced Accuracy 0.9481
Dari hasil training model dan evaluasi model dengan confusion matrix, diketahui beberapa hal sebagai berikut :
Sekarang kita akan menggunakan model_sederhana yang sudah kita training untuk memprediksi citra yang ada pada folder test.
Pertama data image kita load dan akan kita konversi citra 2D menjadi citra 1D array.
folder_list_test <- list.files("data_input/test", full.names = TRUE)
head(folder_list_test)## [1] "data_input/test/GLSp 4287.JPG" "data_input/test/GLSp 4306.JPG"
## [3] "data_input/test/GLSp 4307.JPG" "data_input/test/GLSp 4328.JPG"
## [5] "data_input/test/GLSp 4333.JPG" "data_input/test/GLSp 4335.JPG"
Kemudian dengan menggunakan fungsi map() kita akan mengumpulkan nama filenya
# Get file name
file_name_test <- map(folder_list_test,
function(x) paste0(x, list.files(x))
) %>%
unlist()
# first 6 file name
head(file_name_test)## [1] "data_input/test/GLSp 4287.JPG" "data_input/test/GLSp 4306.JPG"
## [3] "data_input/test/GLSp 4307.JPG" "data_input/test/GLSp 4328.JPG"
## [5] "data_input/test/GLSp 4333.JPG" "data_input/test/GLSp 4335.JPG"
Kemudian secara acak memilih dan meload citra data test ke R dan menampilkannya untuk melihat seperti apa citra dari data test.
# Randomly select image
set.seed(123)
sample_image_test <- sample(file_name_test, 6)
# Load image into R
img_test <- map(sample_image_test, load.image)
# Plot image
par(mfrow = c(2, 3)) # Create 2 x 3 image grid
map(img_test, plot)## [[1]]
## Image. Width: 256 pix Height: 256 pix Depth: 1 Colour channels: 3
##
## [[2]]
## Image. Width: 256 pix Height: 256 pix Depth: 1 Colour channels: 3
##
## [[3]]
## Image. Width: 256 pix Height: 256 pix Depth: 1 Colour channels: 3
##
## [[4]]
## Image. Width: 256 pix Height: 256 pix Depth: 1 Colour channels: 3
##
## [[5]]
## Image. Width: 256 pix Height: 256 pix Depth: 1 Colour channels: 3
##
## [[6]]
## Image. Width: 256 pix Height: 256 pix Depth: 1 Colour channels: 3
Cek dimensi citra
img_test <- load.image("data_input/test/GLSp 4287.JPG")
img_test## Image. Width: 256 pix Height: 256 pix Depth: 1 Colour channels: 3
Memprediksi data citra pada folder test dengan model corn_disease_sederhana yang telah disimpan dalam file hdf5
# Memanggil model
model_corn <- load_model_hdf5("corn_disease_sederhana.hdf5")
#summary(model_teguh)test_data <- image_prep(file_name_test)
dim(test_data)## [1] 150 64 64 3
Kemudian kita membuat prediksi pada data di folder test dengan menggunakan model corn_disease_sederhana yang telah kita load dengan nama model_corn
pred_test_data <- predict_classes(model_corn, test_data)
head(pred_test_data)## [1] 3 1 1 1 3 1
Kemudian mengkonversi encoding menjadi label
## Convert encoding to label
decode_test <- function(x){
case_when(x == 0 ~ "Common_Rust",
x == 1 ~ "Gray_Leaf_Spot",
x == 2 ~ "Healthy",
x == 3 ~ "Northern_Leaf_Blight"
)
}Kemudian menyimpannya dalam bentuk data frame
# Create data submission
submission <- data.frame(id = file_name_test, # Harus dari folder test
label = sapply(pred_test_data, decode_test)
) %>%
mutate(id = str_remove(id, "data_input/test/")) # remove file path and only keep the file nameSetelah itu kita dapat menyimpan hasil prediksi kedalam bentuk file csv
# Write submission
write.csv(submission, "hasil prediksi corn leaf disease.csv", row.names=F)Melihat 10 baris hasil prediksi secara acak dari data frame hasil prediksi
# Melihat hasil prediksi secara acak
hasil <- sample_n(submission, 10) # Baris sampel dari data dengan dplyr
hasil # Print sampel dataBerikut adalah padanan untuk kolom id :
HL = Healty RS Rust = Common Rust GLS = Gray Leaf Spot NLB = Northern Leaf Blight
Dari model yang telah dibuat diperoleh accuracy yang cukup baik yaitu 0.94. Namun demikian masih terdapat kesalahan prediksi pada Northern Leaf Blight yang di prediksi sebagai Gray Leaf Spot, dan sebaliknya. Model perlu di fine tuning dengan menambahkan jumlah sampel citra terutama pada kedua kategori tersebut yang jumlahnya lebih sedikit dibanding kategori common rust dan healthy. Selain itu juga dapat melakukan ekplorasi model architecture dan fine tuning hyper parameter.