# -*- coding: utf-8 -*-
"""Tugas Image Classifier - Multi Layer Perceptron Final

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/162dEzns1WnA_R3ud1QcRpxZxYKTHf5iR

# Kelompok 1
### Muhammad Bintang Bahy (17/412643/PA/17962)
1. Pembuatan kode program

### Perdo Kurniawan (17/412649/PA/17968)
1. Pembuatan laporan dan analisis model
"""

# Dependencies
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.model_selection import train_test_split
import os
import matplotlib.image as mpimg
from skimage import data, color
from skimage.transform import rescale, resize, downscale_local_mean
import random

# Download datasets
! pip install -q kaggle
! mkdir ~/.kaggle
! cp kaggle.json ~/.kaggle/
! chmod 600 ~/.kaggle/kaggle.json
! kaggle datasets download 'alxmamaev/flowers-recognition'
! mkdir -p flowers_recognition
! unzip flowers-recognition.zip -d flowers_recognition

"""Potongan script di atas akan melakukan pengambilan dataset dari kaggle seperti link https://www.kaggle.com/alxmamaev/flowers-recognition. Setelah proses download selesai, akan dilakukan unzip yang selanjutnya akan disimpan ke dalam directory flowers_recognition"""

# Import datasets and visualize
dataset_path ='./flowers_recognition/flowers/'

k = 0
for fil in os.listdir(dataset_path + 'daisy/'):
  link = dataset_path + 'daisy/' + fil
  img = mpimg.imread(link)
  plt.imshow(img)
  plt.show()
  k += 1
  if k == 3:
    break

"""Pada potonga script di atas, data pada subdirectory daisy akan di-load untuk kemudian divisualisasikan dengan menggunakan libary mpimg. Data yang divisualisasikan/ditampilkan hanya 3 data awal saja. Data yang sudah divisualisasikan terlihat seperti pada gambar di atas."""

# Fuction to import to DataFrame: Transform to grayscale and resize to 320 x 240
debug = True

def create_dataset(path, arr, label):
    global debug
    arr = []
    for fil in os.listdir(path):
        link = path + fil
        if link[-3:] == 'pyc' or link[-2:] == 'py':
            continue
        img = mpimg.imread(link)
        img = rgb2gray(img)
        if debug:
            plt.imshow(img, cmap = plt.get_cmap('gray'), vmin = np.min(img), vmax = np.max(img))
            plt.show()
            debug = False
        img = resize(img,(320, 240, 1), anti_aliasing=True)
        img = img.reshape(320 * 240)
        img = np.array(img)
        img = np.concatenate(([1], img))
        arr.append({
            'features': img,
            'label': label
        })
    
    debug = True
    return arr

def rgb2gray(rgb):

    r, g, b = rgb[:,:,0], rgb[:,:,1], rgb[:,:,2]
    gray = 0.2989 * r + 0.5870 * g + 0.1140 * b

    return gray

"""Fungsi rg2gray merupakan fungsi yang akan mengkoversi image RGB ke dalam bentuk grayscale.

Fungsi create_dataset di atas adalah fungsi yang bertujuan untuk me-load image dengan menggunakan bantuan mpimg. Image ini kemudian akan dikonversi ke dalam bentuk grayscale dengan memanggil fungsi rgb2gray. Kemudian image akan diresize dengan dimensi 320 x 240 pixel. Kemudian data ini akan dikonversi ke dalam bentu data frame yang memuat label features yang merupakan nilai pixel image grayscale serta label yang memuat class dari data image tersebut.
"""

# Import datasets
datasets = []

sunflower = []
sunflower = create_dataset(dataset_path + 'sunflower/',sunflower, 0)
datasets += sunflower[:100]

rose = []
rose = create_dataset(dataset_path + 'rose/', rose, 1)
datasets += rose[:100]

dandelion = []
dandelion = create_dataset(dataset_path + 'dandelion/', dandelion, 2)
datasets += dandelion[:100]

print(datasets[:10])

"""Potongan script di atas merupakan script yang akan melakukan pengambilan dataset untuk kemudian disimpan pada variabel sunflower, rose dan dandelion. Kemudian ketiga jenis dataset tersebut akan dikumpulkan ke dalam variabel dataset. Setelah itu akan ditampilkan 10 dataset awal dalam bentuk tipe data array seperti yang bisa dilihat pada output di atas."""

# Sigmoid function
def sigmoid_function(z):
  return 1 / (1 + np.exp(-z))

"""Fungsi sigmoid_function di atas merupakan fungsi akan melakukan perhitungan sigmoid untuk nilai x."""

# Backpropagation algorithm - initiate weight and bias
def initialize_network(n_inputs, n_hidden, n_outputs):
    theta_1 = np.random.randn(n_hidden, n_inputs + 1)
    theta_2 = np.random.randn(n_outputs, n_hidden + 1)
    return theta_1, theta_2

def add_bias(vector):
    bias = np.ones((vector.shape[0], 1))
    return np.append(bias, vector, axis = 1)

"""Pada fungsi initialize_network, diinisiasi nilai theta awal, yaitu matriks yang berisi bilangan random. Theta 1 merupakan nilai weight dari input ke hidden layer dan theta 2 merupakan nilai weight dari hidden layer ke output layer. 
Fungsi add_bias merupakan fungsi yang akan menambahkan bias pada suatu vector.
"""

# Backpropagation algorithm - error func
def gradient_sigmoid_function(a):
    return np.multiply(a, (1 - a))

"""Fungsi gradient_sigmoid_function di atas merupakan derivative dari fungsi sigmoid yang akan digunakan di proses backward
propagation untuk menghitung error.
"""

# Backpropagation algorithm - feed forward
def get_neuron_value(theta, inputs):
    return sigmoid_function(np.dot(inputs, theta.T))

def forward_propagation(feature, theta_1, theta_2):
    hidden_neuron = get_neuron_value(theta_1, feature)
    hidden_neuron = add_bias(hidden_neuron)
    output_neuron = get_neuron_value(theta_2, hidden_neuron)
            
    return hidden_neuron, output_neuron

"""Fungsi get_neuron_value di merupakan fungsi untuk mendapatkan nilai sigmoid dari perkalian thetas dan inputs.
Fungsi forward_propagation di atas akan menghasilkan nilai aktivasi dari neuron pada hidden layer serta output layer.

"""

# Backpropagation algorithm - backward propagation
def get_hidden_layer_error(next_layer_error, theta, neuron_value):
    error = np.dot(next_layer_error, theta) * gradient_sigmoid_function(neuron_value)
    
    # Delete error for bias neuron
    return np.delete(error, 0, axis=1)

def set_delta(error, neuron_value, target):
    delta = np.dot(error.T, neuron_value)
    return delta / len(target)

def update_weight(theta, delta, alpha):
    return theta - alpha * delta

def back_propagation(feature, target, hidden_neuron, output_neuron, theta_1, theta_2, alpha):
    output_error = (output_neuron - target) * gradient_sigmoid_function(output_neuron)
    hidden_error = get_hidden_layer_error(output_error, theta_2, hidden_neuron)
    
    input_delta = set_delta(hidden_error, feature, target)
    hidden_delta = set_delta(output_error, hidden_neuron, target)
    
    new_theta_1 = update_weight(theta_1, input_delta, alpha)
    new_theta_2 = update_weight(theta_2, hidden_delta, alpha)
    
    return new_theta_1, new_theta_2

"""Fungsi get_hidden_layer merupakan fungsi yang digunakan untuk mendapatkan error. Error didapatkan dari dot product pada next_layer_error dan thete yang kemudian dikalikan dengan nilai dari gradient_sigmoid dengan parameter neuron_value.
Fungsi update weight merupakan fungsi yang digunakan untuk melakukan update theta.

Fungsi back_propagation di atas merupakan fungsi yang akan mengevaluasi weight dari setiap neuron lalu melakukan update. Update weigth akan bergantung pada nilai input_delta dan hidden_delta serta learning rate.
"""

# Backpropagation algorithm - Accuracy
def predict(output_value):
    predictions = np.zeros(output_value.shape)
    for i in range(output_value.shape[0]):
        max_val = np.amax(output_neuron[i])
        for j in range(output_value.shape[1]):
            if(output_value[i][j] == max_val):
                predictions[i][j] = 1
            else:
                predictions[i][j] = 0
    return predictions

def calculate_accuracy(predictions, targets):
    correct = 0
    for i in range(len(targets)):
        if all(predictions[i] == targets[i]):
            correct += 1
    return correct / float(len(targets))

"""Fungsi predict di atas merupakan fungsi yang digunakan untuk menyimpan nilai preidksi. Nilai prediksi disimpan berdasarkan representasi one hot encoding.
Pada fungsi calculate_accurary di atas, akan dilakukan perhitungan akurasi dari model yang telah dibuat.
"""

# Cost function
def cost_function(predicts, targets):
    cost = np.sum(np.square(predicts - targets)) / len(targets)
    return cost

"""Fungsi cost_function merupakan fungsi yang akan menghitung cost/ J function. Formula yang didapat pada perhitungan ini seperti halnya pada cost function yang terdapat pada logistic regression."""

# Split train and test
train_ratio = 0.8

random.shuffle(datasets)
counter = [0, 0, 0]
X_train = []
Y_train = []
X_test = []
Y_test = []

for data in datasets:
    if counter[data['label']] < len(datasets) / 3 * train_ratio:
        X_train.append(data['features'])
        Y_train.append(data['label'])
    else:
        X_test.append(data['features'])
        Y_test.append(data['label'])
    counter[data['label']] += 1

X_train = np.array(X_train)
Y_train = np.array(Y_train)
X_test = np.array(X_test)
Y_test = np.array(Y_test)

"""Potongan script di atas bertujuan untuk splitting data train dan data set dengan porsi 80% untuk data train dan 20% untuk data test."""

# Change to one hot encoding
def one_hot_encoding(target):
    nClass = np.max(target) + 1
    target_encoded = np.eye(nClass)[target]
    return target_encoded

Y_test = one_hot_encoding(Y_test)
Y_train = one_hot_encoding(Y_train)

"""Potongan script di atas bertujuan untuk merubah representasi label pada data test data data train dengan menggunakan one hot encoding."""

# Generate initial network
input_layer = 320 * 240
hidden_layer = 10
output_layer = 3
theta1, theta2 = initialize_network(input_layer, hidden_layer, output_layer)

print(X_train[0].shape)

"""potongan script di atas akan mendifinisikan arsitektur MLP dengan 1 hidden layer dengan jumlah neuron sebanyak 10. Input layer diinisiasi sebanyak 320 * 240 dengan 3 output layer. """

hidden_neuron, output_neuron = forward_propagation(X_train, theta1, theta2)
train_error = []
train_accuracy = []
test_error = []
test_accuracy = []
learning_rate = 0.1
epoch = 50

for i in range(epoch):
    print("Running epoch ", i + 1, "...")
    hidden_neuron, output_neuron = forward_propagation(X_train, theta1, theta2)
    acc = calculate_accuracy(predict(output_neuron), Y_train)
    cost = cost_function(output_neuron, Y_train)
    train_accuracy.append(acc)
    train_error.append(cost)
    
    theta1, theta2 = back_propagation(X_train, Y_train, hidden_neuron, output_neuron, theta1, theta2, learning_rate)
    
    hidden_neuron, output_neuron = forward_propagation(X_test, theta1, theta2)
    acc = calculate_accuracy(predict(output_neuron), Y_test)
    cost = cost_function(output_neuron, Y_test)
    test_accuracy.append(acc)
    test_error.append(cost)
    print("Cost: ", cost)
    print("Accuracy: ", acc)

"""Pada potongan script di atas, learning rate diinisiasi dengan nilai 0.1 dengan epoch 50. Kemudian akan dilakukan training terhadap data train."""

print(test_accuracy[-1])

"""Potongan script di atas berfungsi untuk melakukan penghitungan akurasi dengan memanggil fungsi calculate_accuray. Dari hasil di atas dapat terlihat bahwa akurasi masih jauh dari kata baik. Akurasi yang diperoleh hanya sekitar 30%. Hal ini bisa disebabkan karena model yang dibuat belum baik. Struktur MLP hanya menggunakan 10 neuron. Hal ini sangat berpengaruh terhadap hasil akurasi karena dataset merupakan image yang pada dasarnya termasuk permasalahan yang cukup komplek namun neuron yang dipakai hanya sebanyak 10."""

from matplotlib import pyplot as plt

def visualize(train, test, ylabel, title):
    plt.plot(np.arange(len(train)), train, label='Train')
    plt.plot(np.arange(len(test)), test, label='Test')
    plt.xlabel('Epochs')
    plt.ylabel(ylabel)
    plt.title(title)
    plt.legend()
    plt.show()

visualize(train_error, test_error, 'Error', 'Error')
visualize(train_accuracy, test_accuracy, 'Accuracy', 'Model Accuracy for Test Set')

"""Potongan script di atas berfungsi untuk memplot cost function pada tiap epoch dan menampilkan akurasi model pada data train dan test. Pada gambar di atas terlihat bahwa model yang dibuat belum mampu menghasilkan cost function yang mendekati 0. Cost function pada data train masih berada di atas 0.70 dan cost function pada data test berada di atas 0.84. Dari data tersebut dapat diketahui model yang didapat masih sangat buruk dan underfit. Begitupun dengan akurasi yang didapat pada data train dan data test. Akurasi yang didapat berada di bawah 36%."""

# Generate initial network
theta1, theta2 = initialize_network(320 * 240, 10, 3)

"""Script di atas digunakan untuk menginisiai network dengan 1 hidden layer serta 10 neuron. Theta1 dan Theta2 akan bernilai random sesuai dengan return value pada pemanggilan fungsi initialize_network."""

hidden_neuron, output_neuron = forward_propagation(X_train, theta1, theta2)
train_error = []
train_accuracy = []
test_error = []
test_accuracy = []
learning_rate = 0.8
epoch = 50

for i in range(epoch):
    print("Running epoch ", i + 1, "...")
    hidden_neuron, output_neuron = forward_propagation(X_train, theta1, theta2)
    acc = calculate_accuracy(predict(output_neuron), Y_train)
    cost = cost_function(output_neuron, Y_train)
    train_accuracy.append(acc)
    train_error.append(cost)
    
    theta1, theta2 = back_propagation(X_train, Y_train, hidden_neuron, output_neuron, theta1, theta2, learning_rate)
    
    hidden_neuron, output_neuron = forward_propagation(X_test, theta1, theta2)
    acc = calculate_accuracy(predict(output_neuron), Y_test)
    cost = cost_function(output_neuron, Y_test)
    test_accuracy.append(acc)
    test_error.append(cost)
    print("Cost: ", cost)
    print("Accuracy: ", acc)

"""Script di atas digunakan untuk melakukan training terhadap data train. Epoch diinisiai 50 dengan learning rate bernilai 0.8."""

print(test_accuracy[-1])

"""Script di atas berfungsi untuk melakukan perhitungan akurasi dari model yang telah dibuat. Dari output di atas, didapatkan akurasi sekitar 38%. Dibandingkan dengan model sebelumnya yang menggunakan learning rate 0.1, akurasi pada model kali ini lebih baik. Hal ini disebabkan karena perubahan theta pada model ini jauh lebih cepat dibanding model sebelumnya, sehingga proses update theta untuk mendapatkan theta yang lebih baik juga lebih cepat. """

visualize(train_error, test_error, 'Error', 'Error')
visualize(train_accuracy, test_accuracy, 'Accuracy', 'Model Accuracy for Test Set')

"""Script di atas berfungsi untuk memplot cost function. Dari gambar di atas terlihat cost function mengalami penurunan yang cukup signifikan dibanding model sebelumnya. Hal ini terlihat dari cost function yang didapat pada data train maupun data test mampu berada pada nilai di bawah 0.85 pada epoch 50. Meskipun begitu, model ini juga masih termasuk buruk karena akurasi yang diperoleh masih disekitar 36%.

# Kesimpulan

Dari kedua model yang telah dibuat, dapat disimpulkan bahwa learning rate sangat berpengaruh terhadap model yang dibuat. Model dengan learning 0.8 memiliki akurasi yang lebih baik dibanding dengan learning 0.1. Kedua model yang telah dibuat masih jauh dari model yang baik karena akurasi yang didapatkan hanya sekitar 30%.

# Source Code Github
https://github.com/vido21/Machine-Learning/blob/main/Tugas%20Image%20Classifier%20-%20Multi%20Layer%20Perceptron%20Final/tugas_image_classifier_multi_layer_perceptron_final.py

# CNN
"""

import numpy as np
import matplotlib.pyplot as plt
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import Adam
from keras.utils.np_utils import to_categorical
from keras.layers import Dropout, Dense, Conv2D, MaxPool2D , Flatten
from keras.callbacks import ModelCheckpoint, EarlyStopping
from sklearn.model_selection import train_test_split

imageDimensions = (320, 240, 1)

def myModel():
    model = Sequential()
    model.add((Conv2D(64, (5, 5), input_shape=imageDimensions, activation='relu')))
    model.add((Conv2D(64, (5, 5), activation='relu')))
    model.add(MaxPool2D(pool_size=(2, 2)))
    model.add((Conv2D(32, (3, 3), activation='relu')))
    model.add((Conv2D(32, (3, 3), activation='relu')))
    model.add(MaxPool2D(pool_size=(2, 2)))
    model.add(Dropout(0.5))
    model.add(Flatten())
    model.add(Dense(512, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(3, activation='softmax')) 
    model.compile(Adam(learning_rate=0.001), loss='categorical_crossentropy', metrics=['accuracy'])
    return model

"""Potongan script di atas merupakan arsitektur dari CNN dengan menggunakan 2 convolution layer awal dengan 64 filter. Input merupakan iamge dengan dimensi 320 x 240 grayscale. Kernel size yang digunakan pada kedua layer pertama adalah 5 x 5. Kemudian terdapat 1 layer pooling dengan ukuran 2 x 2 yang diikuti 2 layer convolution. Dua layer convolution dengan 32 filter dengan kernel size 3 x 3. Terdapat pooling layer terakhir dengan size 2 x 2. Kemudian digunakan regularisasi dengan lamda 0.5. Kemudian feature tersebut di-flatten untuk menjadi input pada hidden layer berikutnya. Hidden layer didefinisikan sebanyak 512 neuron dengan fungsi aktivasi relu. Setelah itu dilakukan regularisasi dengan nilai lamda sama dengan 0.5. Kemudian terdapat output layer sebanyak 3 neuron dengan fungsi aktivasi softmax.

Kemudian arsitektur ini akan diimplementasikan dengan optimizer adam dengan learning rate 0.001, loss function categorical crossentropy dan parameter yang dievaluasi adalah accuracy.
"""

# Add validation set
X_train, X_validation, Y_train, Y_validation = train_test_split(X_train, Y_train, test_size = 0.2)

"""Potongan script di atas digunakan untuk splitting data training menjadi data training dan data validasi dengan ratio 0.2 untuk data validasi."""

# Remove bias
X_train = X_train[:, 1:]
X_validation = X_validation[:, 1:]
X_test = X_test[:, 1:]

"""Script di atas digunakan untuk memfilter/menghilangkan bias yang telah diinisiasi pada model sebelumnya."""

# Change shape to fit the model
X_train = X_train.reshape(X_train.shape[0], imageDimensions[0], imageDimensions[1], 1)
X_validation = X_validation.reshape(X_validation.shape[0], imageDimensions[0], imageDimensions[1], 1)
X_test = X_test.reshape(X_test.shape[0], imageDimensions[0], imageDimensions[1], 1)

"""Potongan script di atas bertujuan untuk reshaping image/data agar bisa diproses pada arsitektur yang telah didefinisikan."""

model = myModel()
print(model.summary())

history = model.fit(
        x = X_train,
        y = Y_train,
        epochs = epoch,
        validation_data = (X_validation, Y_validation),
        shuffle = 1
    )

"""Potongan script di atas merupakan bagian untuk melakukan training dengan nilai epoch yang telah diinisiasi yaitu 50.

Dari hasil di atas dapat diketahui bahwa model menghasilkan accuracy yang sangat baik pada training yang bisa dilihat dari nilai accuracy yang sangat baik dan loss function yang sangat rendah. Namun, pada data validasi nilai accuracy yang didapatkan hanya berkisar pada rentang 0.4 - 0.5 dengan loss function yang cukup tinggi pada rentang 3 - 4.
"""

plt.figure(1)
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.legend(['training','validation'])
plt.title('loss')
plt.xlabel('epoch')
plt.figure(2)
plt.plot(history.history['accuracy'])
plt.plot(history.history['val_accuracy'])
plt.legend(['training','validation'])
plt.title('Acurracy')
plt.xlabel('epoch')
plt.show()
score = model.evaluate(X_test, Y_test, verbose=0)
print('Test Score:', score[0])
print('Test Accuracy:', score[1])

"""Script di atas digunakan untuk memplot loss function dan akurasi dari data training dan validation. Kemudian terdapat bagian untuk melihat hasil akurasi dari data test.
Data output di atas terlihat bahwa nilai akurasi yang didapatkan pada data validasi dan data test masih sangat buruk. Didapatkan nilai test akurasi dari data test sebesar 50%.

Dari kedua hasil di atas dapat diketahui bahwa model yang telah merupakan model yang overfit. Hal ini bisa dilihat dari baiknya nilai akurasi pada data train tetapi nilai akurasi pada data validasi dan data test justru sangat buruk.

# Perbandinga model MLP dan CNN

Pada model MLP yang telah dibuat baik dengan menggunakan learning rate 0.1 dan 0.8 keduanya hanya mendapatkan nilai akurasi sebesar 30%. Sedangkan pada model yang menggunakan CNN akurasi yang didapat sekitar 50%. Dari nilai akurasi yang diperoleh ini dapat disimpulkan bahwa model yang menggunakan CNN jauh lebih baik dibanding model MLP yang telah dibuat. Meskipun model yang menggunakan CNN mempunyai akurasi yang lebih baik, model ini masih mengalami overfit sehingga bisa dikatakan model yang dibuat belum cukup baik.
"""
c515bec32dc64739437c32b7615df3dd5e13e483
# -*- coding: utf-8 -*-
"""Tugas Image Classifier - Multi Layer Perceptron Final

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/162dEzns1WnA_R3ud1QcRpxZxYKTHf5iR

# Kelompok 1
### Muhammad Bintang Bahy (17/412643/PA/17962)
1. Pembuatan kode program

### Perdo Kurniawan (17/412649/PA/17968)
1. Pembuatan laporan dan analisis model
"""

# Dependencies
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.model_selection import train_test_split
import os
import matplotlib.image as mpimg
from skimage import data, color
from skimage.transform import rescale, resize, downscale_local_mean
import random

# Download datasets
! pip install -q kaggle
! mkdir ~/.kaggle
! cp kaggle.json ~/.kaggle/
! chmod 600 ~/.kaggle/kaggle.json
! kaggle datasets download 'alxmamaev/flowers-recognition'
! mkdir -p flowers_recognition
! unzip flowers-recognition.zip -d flowers_recognition

"""Potongan script di atas akan melakukan pengambilan dataset dari kaggle seperti link https://www.kaggle.com/alxmamaev/flowers-recognition. Setelah proses download selesai, akan dilakukan unzip yang selanjutnya akan disimpan ke dalam directory flowers_recognition"""

# Import datasets and visualize
dataset_path ='./flowers_recognition/flowers/'

k = 0
for fil in os.listdir(dataset_path + 'daisy/'):
  link = dataset_path + 'daisy/' + fil
  img = mpimg.imread(link)
  plt.imshow(img)
  plt.show()
  k += 1
  if k == 3:
    break

"""Pada potonga script di atas, data pada subdirectory daisy akan di-load untuk kemudian divisualisasikan dengan menggunakan libary mpimg. Data yang divisualisasikan/ditampilkan hanya 3 data awal saja. Data yang sudah divisualisasikan terlihat seperti pada gambar di atas."""

# Fuction to import to DataFrame: Transform to grayscale and resize to 320 x 240
debug = True

def create_dataset(path, arr, label):
    global debug
    arr = []
    for fil in os.listdir(path):
        link = path + fil
        if link[-3:] == 'pyc' or link[-2:] == 'py':
            continue
        img = mpimg.imread(link)
        img = rgb2gray(img)
        if debug:
            plt.imshow(img, cmap = plt.get_cmap('gray'), vmin = np.min(img), vmax = np.max(img))
            plt.show()
            debug = False
        img = resize(img,(320, 240, 1), anti_aliasing=True)
        img = img.reshape(320 * 240)
        img = np.array(img)
        img = np.concatenate(([1], img))
        arr.append({
            'features': img,
            'label': label
        })
    
    debug = True
    return arr

def rgb2gray(rgb):

    r, g, b = rgb[:,:,0], rgb[:,:,1], rgb[:,:,2]
    gray = 0.2989 * r + 0.5870 * g + 0.1140 * b

    return gray

"""Fungsi rg2gray merupakan fungsi yang akan mengkoversi image RGB ke dalam bentuk grayscale.

Fungsi create_dataset di atas adalah fungsi yang bertujuan untuk me-load image dengan menggunakan bantuan mpimg. Image ini kemudian akan dikonversi ke dalam bentuk grayscale dengan memanggil fungsi rgb2gray. Kemudian image akan diresize dengan dimensi 320 x 240 pixel. Kemudian data ini akan dikonversi ke dalam bentu data frame yang memuat label features yang merupakan nilai pixel image grayscale serta label yang memuat class dari data image tersebut.
"""

# Import datasets
datasets = []

sunflower = []
sunflower = create_dataset(dataset_path + 'sunflower/',sunflower, 0)
datasets += sunflower[:100]

rose = []
rose = create_dataset(dataset_path + 'rose/', rose, 1)
datasets += rose[:100]

dandelion = []
dandelion = create_dataset(dataset_path + 'dandelion/', dandelion, 2)
datasets += dandelion[:100]

print(datasets[:10])

"""Potongan script di atas merupakan script yang akan melakukan pengambilan dataset untuk kemudian disimpan pada variabel sunflower, rose dan dandelion. Kemudian ketiga jenis dataset tersebut akan dikumpulkan ke dalam variabel dataset. Setelah itu akan ditampilkan 10 dataset awal dalam bentuk tipe data array seperti yang bisa dilihat pada output di atas."""

# Sigmoid function
def sigmoid_function(z):
  return 1 / (1 + np.exp(-z))

"""Fungsi sigmoid_function di atas merupakan fungsi akan melakukan perhitungan sigmoid untuk nilai x."""

# Backpropagation algorithm - initiate weight and bias
def initialize_network(n_inputs, n_hidden, n_outputs):
    theta_1 = np.random.randn(n_hidden, n_inputs + 1)
    theta_2 = np.random.randn(n_outputs, n_hidden + 1)
    return theta_1, theta_2

def add_bias(vector):
    bias = np.ones((vector.shape[0], 1))
    return np.append(bias, vector, axis = 1)

"""Pada fungsi initialize_network, diinisiasi nilai theta awal, yaitu matriks yang berisi bilangan random. Theta 1 merupakan nilai weight dari input ke hidden layer dan theta 2 merupakan nilai weight dari hidden layer ke output layer. 
Fungsi add_bias merupakan fungsi yang akan menambahkan bias pada suatu vector.
"""

# Backpropagation algorithm - error func
def gradient_sigmoid_function(a):
    return np.multiply(a, (1 - a))

"""Fungsi gradient_sigmoid_function di atas merupakan derivative dari fungsi sigmoid yang akan digunakan di proses backward
propagation untuk menghitung error.
"""

# Backpropagation algorithm - feed forward
def get_neuron_value(theta, inputs):
    return sigmoid_function(np.dot(inputs, theta.T))

def forward_propagation(feature, theta_1, theta_2):
    hidden_neuron = get_neuron_value(theta_1, feature)
    hidden_neuron = add_bias(hidden_neuron)
    output_neuron = get_neuron_value(theta_2, hidden_neuron)
            
    return hidden_neuron, output_neuron

"""Fungsi get_neuron_value di merupakan fungsi untuk mendapatkan nilai sigmoid dari perkalian thetas dan inputs.
Fungsi forward_propagation di atas akan menghasilkan nilai aktivasi dari neuron pada hidden layer serta output layer.

"""

# Backpropagation algorithm - backward propagation
def get_hidden_layer_error(next_layer_error, theta, neuron_value):
    error = np.dot(next_layer_error, theta) * gradient_sigmoid_function(neuron_value)
    
    # Delete error for bias neuron
    return np.delete(error, 0, axis=1)

def set_delta(error, neuron_value, target):
    delta = np.dot(error.T, neuron_value)
    return delta / len(target)

def update_weight(theta, delta, alpha):
    return theta - alpha * delta

def back_propagation(feature, target, hidden_neuron, output_neuron, theta_1, theta_2, alpha):
    output_error = (output_neuron - target) * gradient_sigmoid_function(output_neuron)
    hidden_error = get_hidden_layer_error(output_error, theta_2, hidden_neuron)
    
    input_delta = set_delta(hidden_error, feature, target)
    hidden_delta = set_delta(output_error, hidden_neuron, target)
    
    new_theta_1 = update_weight(theta_1, input_delta, alpha)
    new_theta_2 = update_weight(theta_2, hidden_delta, alpha)
    
    return new_theta_1, new_theta_2

"""Fungsi get_hidden_layer merupakan fungsi yang digunakan untuk mendapatkan error. Error didapatkan dari dot product pada next_layer_error dan thete yang kemudian dikalikan dengan nilai dari gradient_sigmoid dengan parameter neuron_value.
Fungsi update weight merupakan fungsi yang digunakan untuk melakukan update theta.

Fungsi back_propagation di atas merupakan fungsi yang akan mengevaluasi weight dari setiap neuron lalu melakukan update. Update weigth akan bergantung pada nilai input_delta dan hidden_delta serta learning rate.
"""

# Backpropagation algorithm - Accuracy
def predict(output_value):
    predictions = np.zeros(output_value.shape)
    for i in range(output_value.shape[0]):
        max_val = np.amax(output_neuron[i])
        for j in range(output_value.shape[1]):
            if(output_value[i][j] == max_val):
                predictions[i][j] = 1
            else:
                predictions[i][j] = 0
    return predictions

def calculate_accuracy(predictions, targets):
    correct = 0
    for i in range(len(targets)):
        if all(predictions[i] == targets[i]):
            correct += 1
    return correct / float(len(targets))

"""Fungsi predict di atas merupakan fungsi yang digunakan untuk menyimpan nilai preidksi. Nilai prediksi disimpan berdasarkan representasi one hot encoding.
Pada fungsi calculate_accurary di atas, akan dilakukan perhitungan akurasi dari model yang telah dibuat.
"""

# Cost function
def cost_function(predicts, targets):
    cost = np.sum(np.square(predicts - targets)) / len(targets)
    return cost

"""Fungsi cost_function merupakan fungsi yang akan menghitung cost/ J function. Formula yang didapat pada perhitungan ini seperti halnya pada cost function yang terdapat pada logistic regression."""

# Split train and test
train_ratio = 0.8

random.shuffle(datasets)
counter = [0, 0, 0]
X_train = []
Y_train = []
X_test = []
Y_test = []

for data in datasets:
    if counter[data['label']] < len(datasets) / 3 * train_ratio:
        X_train.append(data['features'])
        Y_train.append(data['label'])
    else:
        X_test.append(data['features'])
        Y_test.append(data['label'])
    counter[data['label']] += 1

X_train = np.array(X_train)
Y_train = np.array(Y_train)
X_test = np.array(X_test)
Y_test = np.array(Y_test)

"""Potongan script di atas bertujuan untuk splitting data train dan data set dengan porsi 80% untuk data train dan 20% untuk data test."""

# Change to one hot encoding
def one_hot_encoding(target):
    nClass = np.max(target) + 1
    target_encoded = np.eye(nClass)[target]
    return target_encoded

Y_test = one_hot_encoding(Y_test)
Y_train = one_hot_encoding(Y_train)

"""Potongan script di atas bertujuan untuk merubah representasi label pada data test data data train dengan menggunakan one hot encoding."""

# Generate initial network
input_layer = 320 * 240
hidden_layer = 10
output_layer = 3
theta1, theta2 = initialize_network(input_layer, hidden_layer, output_layer)

print(X_train[0].shape)

"""potongan script di atas akan mendifinisikan arsitektur MLP dengan 1 hidden layer dengan jumlah neuron sebanyak 10. Input layer diinisiasi sebanyak 320 * 240 dengan 3 output layer. """

hidden_neuron, output_neuron = forward_propagation(X_train, theta1, theta2)
train_error = []
train_accuracy = []
test_error = []
test_accuracy = []
learning_rate = 0.1
epoch = 50

for i in range(epoch):
    print("Running epoch ", i + 1, "...")
    hidden_neuron, output_neuron = forward_propagation(X_train, theta1, theta2)
    acc = calculate_accuracy(predict(output_neuron), Y_train)
    cost = cost_function(output_neuron, Y_train)
    train_accuracy.append(acc)
    train_error.append(cost)
    
    theta1, theta2 = back_propagation(X_train, Y_train, hidden_neuron, output_neuron, theta1, theta2, learning_rate)
    
    hidden_neuron, output_neuron = forward_propagation(X_test, theta1, theta2)
    acc = calculate_accuracy(predict(output_neuron), Y_test)
    cost = cost_function(output_neuron, Y_test)
    test_accuracy.append(acc)
    test_error.append(cost)
    print("Cost: ", cost)
    print("Accuracy: ", acc)

"""Pada potongan script di atas, learning rate diinisiasi dengan nilai 0.1 dengan epoch 50. Kemudian akan dilakukan training terhadap data train."""

print(test_accuracy[-1])

"""Potongan script di atas berfungsi untuk melakukan penghitungan akurasi dengan memanggil fungsi calculate_accuray. Dari hasil di atas dapat terlihat bahwa akurasi masih jauh dari kata baik. Akurasi yang diperoleh hanya sekitar 30%. Hal ini bisa disebabkan karena model yang dibuat belum baik. Struktur MLP hanya menggunakan 10 neuron. Hal ini sangat berpengaruh terhadap hasil akurasi karena dataset merupakan image yang pada dasarnya termasuk permasalahan yang cukup komplek namun neuron yang dipakai hanya sebanyak 10."""

from matplotlib import pyplot as plt

def visualize(train, test, ylabel, title):
    plt.plot(np.arange(len(train)), train, label='Train')
    plt.plot(np.arange(len(test)), test, label='Test')
    plt.xlabel('Epochs')
    plt.ylabel(ylabel)
    plt.title(title)
    plt.legend()
    plt.show()

visualize(train_error, test_error, 'Error', 'Error')
visualize(train_accuracy, test_accuracy, 'Accuracy', 'Model Accuracy for Test Set')

"""Potongan script di atas berfungsi untuk memplot cost function pada tiap epoch dan menampilkan akurasi model pada data train dan test. Pada gambar di atas terlihat bahwa model yang dibuat belum mampu menghasilkan cost function yang mendekati 0. Cost function pada data train masih berada di atas 0.70 dan cost function pada data test berada di atas 0.84. Dari data tersebut dapat diketahui model yang didapat masih sangat buruk dan underfit. Begitupun dengan akurasi yang didapat pada data train dan data test. Akurasi yang didapat berada di bawah 36%."""

# Generate initial network
theta1, theta2 = initialize_network(320 * 240, 10, 3)

"""Script di atas digunakan untuk menginisiai network dengan 1 hidden layer serta 10 neuron. Theta1 dan Theta2 akan bernilai random sesuai dengan return value pada pemanggilan fungsi initialize_network."""

hidden_neuron, output_neuron = forward_propagation(X_train, theta1, theta2)
train_error = []
train_accuracy = []
test_error = []
test_accuracy = []
learning_rate = 0.8
epoch = 50

for i in range(epoch):
    print("Running epoch ", i + 1, "...")
    hidden_neuron, output_neuron = forward_propagation(X_train, theta1, theta2)
    acc = calculate_accuracy(predict(output_neuron), Y_train)
    cost = cost_function(output_neuron, Y_train)
    train_accuracy.append(acc)
    train_error.append(cost)
    
    theta1, theta2 = back_propagation(X_train, Y_train, hidden_neuron, output_neuron, theta1, theta2, learning_rate)
    
    hidden_neuron, output_neuron = forward_propagation(X_test, theta1, theta2)
    acc = calculate_accuracy(predict(output_neuron), Y_test)
    cost = cost_function(output_neuron, Y_test)
    test_accuracy.append(acc)
    test_error.append(cost)
    print("Cost: ", cost)
    print("Accuracy: ", acc)

"""Script di atas digunakan untuk melakukan training terhadap data train. Epoch diinisiai 50 dengan learning rate bernilai 0.8."""

print(test_accuracy[-1])

"""Script di atas berfungsi untuk melakukan perhitungan akurasi dari model yang telah dibuat. Dari output di atas, didapatkan akurasi sekitar 38%. Dibandingkan dengan model sebelumnya yang menggunakan learning rate 0.1, akurasi pada model kali ini lebih baik. Hal ini disebabkan karena perubahan theta pada model ini jauh lebih cepat dibanding model sebelumnya, sehingga proses update theta untuk mendapatkan theta yang lebih baik juga lebih cepat. """

visualize(train_error, test_error, 'Error', 'Error')
visualize(train_accuracy, test_accuracy, 'Accuracy', 'Model Accuracy for Test Set')

"""Script di atas berfungsi untuk memplot cost function. Dari gambar di atas terlihat cost function mengalami penurunan yang cukup signifikan dibanding model sebelumnya. Hal ini terlihat dari cost function yang didapat pada data train maupun data test mampu berada pada nilai di bawah 0.85 pada epoch 50. Meskipun begitu, model ini juga masih termasuk buruk karena akurasi yang diperoleh masih disekitar 36%.

# Kesimpulan

Dari kedua model yang telah dibuat, dapat disimpulkan bahwa learning rate sangat berpengaruh terhadap model yang dibuat. Model dengan learning 0.8 memiliki akurasi yang lebih baik dibanding dengan learning 0.1. Kedua model yang telah dibuat masih jauh dari model yang baik karena akurasi yang didapatkan hanya sekitar 30%.

# Source Code Github
https://github.com/vido21/Machine-Learning/blob/main/Tugas%20Image%20Classifier%20-%20Multi%20Layer%20Perceptron%20Final/tugas_image_classifier_multi_layer_perceptron_final.py

# CNN
"""

import numpy as np
import matplotlib.pyplot as plt
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import Adam
from keras.utils.np_utils import to_categorical
from keras.layers import Dropout, Dense, Conv2D, MaxPool2D , Flatten
from keras.callbacks import ModelCheckpoint, EarlyStopping
from sklearn.model_selection import train_test_split

imageDimensions = (320, 240, 1)

def myModel():
    model = Sequential()
    model.add((Conv2D(64, (5, 5), input_shape=imageDimensions, activation='relu')))
    model.add((Conv2D(64, (5, 5), activation='relu')))
    model.add(MaxPool2D(pool_size=(2, 2)))
    model.add((Conv2D(32, (3, 3), activation='relu')))
    model.add((Conv2D(32, (3, 3), activation='relu')))
    model.add(MaxPool2D(pool_size=(2, 2)))
    model.add(Dropout(0.5))
    model.add(Flatten())
    model.add(Dense(512, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(3, activation='softmax')) 
    model.compile(Adam(learning_rate=0.001), loss='categorical_crossentropy', metrics=['accuracy'])
    return model

"""Potongan script di atas merupakan arsitektur dari CNN dengan menggunakan 2 convolution layer awal dengan 64 filter. Input merupakan iamge dengan dimensi 320 x 240 grayscale. Kernel size yang digunakan pada kedua layer pertama adalah 5 x 5. Kemudian terdapat 1 layer pooling dengan ukuran 2 x 2 yang diikuti 2 layer convolution. Dua layer convolution dengan 32 filter dengan kernel size 3 x 3. Terdapat pooling layer terakhir dengan size 2 x 2. Kemudian digunakan regularisasi dengan lamda 0.5. Kemudian feature tersebut di-flatten untuk menjadi input pada hidden layer berikutnya. Hidden layer didefinisikan sebanyak 512 neuron dengan fungsi aktivasi relu. Setelah itu dilakukan regularisasi dengan nilai lamda sama dengan 0.5. Kemudian terdapat output layer sebanyak 3 neuron dengan fungsi aktivasi softmax.

Kemudian arsitektur ini akan diimplementasikan dengan optimizer adam dengan learning rate 0.001, loss function categorical crossentropy dan parameter yang dievaluasi adalah accuracy.
"""

# Add validation set
X_train, X_validation, Y_train, Y_validation = train_test_split(X_train, Y_train, test_size = 0.2)

"""Potongan script di atas digunakan untuk splitting data training menjadi data training dan data validasi dengan ratio 0.2 untuk data validasi."""

# Remove bias
X_train = X_train[:, 1:]
X_validation = X_validation[:, 1:]
X_test = X_test[:, 1:]

"""Script di atas digunakan untuk memfilter/menghilangkan bias yang telah diinisiasi pada model sebelumnya."""

# Change shape to fit the model
X_train = X_train.reshape(X_train.shape[0], imageDimensions[0], imageDimensions[1], 1)
X_validation = X_validation.reshape(X_validation.shape[0], imageDimensions[0], imageDimensions[1], 1)
X_test = X_test.reshape(X_test.shape[0], imageDimensions[0], imageDimensions[1], 1)

"""Potongan script di atas bertujuan untuk reshaping image/data agar bisa diproses pada arsitektur yang telah didefinisikan."""

model = myModel()
print(model.summary())

history = model.fit(
        x = X_train,
        y = Y_train,
        epochs = epoch,
        validation_data = (X_validation, Y_validation),
        shuffle = 1
    )

"""Potongan script di atas merupakan bagian untuk melakukan training dengan nilai epoch yang telah diinisiasi yaitu 50.

Dari hasil di atas dapat diketahui bahwa model menghasilkan accuracy yang sangat baik pada training yang bisa dilihat dari nilai accuracy yang sangat baik dan loss function yang sangat rendah. Namun, pada data validasi nilai accuracy yang didapatkan hanya berkisar pada rentang 0.4 - 0.5 dengan loss function yang cukup tinggi pada rentang 3 - 4.
"""

plt.figure(1)
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.legend(['training','validation'])
plt.title('loss')
plt.xlabel('epoch')
plt.figure(2)
plt.plot(history.history['accuracy'])
plt.plot(history.history['val_accuracy'])
plt.legend(['training','validation'])
plt.title('Acurracy')
plt.xlabel('epoch')
plt.show()
score = model.evaluate(X_test, Y_test, verbose=0)
print('Test Score:', score[0])
print('Test Accuracy:', score[1])

"""Script di atas digunakan untuk memplot loss function dan akurasi dari data training dan validation. Kemudian terdapat bagian untuk melihat hasil akurasi dari data test.
Data output di atas terlihat bahwa nilai akurasi yang didapatkan pada data validasi dan data test masih sangat buruk. Didapatkan nilai test akurasi dari data test sebesar 50%.

Dari kedua hasil di atas dapat diketahui bahwa model yang telah merupakan model yang overfit. Hal ini bisa dilihat dari baiknya nilai akurasi pada data train tetapi nilai akurasi pada data validasi dan data test justru sangat buruk.

# Perbandinga model MLP dan CNN

Pada model MLP yang telah dibuat baik dengan menggunakan learning rate 0.1 dan 0.8 keduanya hanya mendapatkan nilai akurasi sebesar 30%. Sedangkan pada model yang menggunakan CNN akurasi yang didapat sekitar 50%. Dari nilai akurasi yang diperoleh ini dapat disimpulkan bahwa model yang menggunakan CNN jauh lebih baik dibanding model MLP yang telah dibuat. Meskipun model yang menggunakan CNN mempunyai akurasi yang lebih baik, model ini masih mengalami overfit sehingga bisa dikatakan model yang dibuat belum cukup baik.
"""