Data Science Portfolio

Xception Fine Tuning

30 Mar 2023

Index


Sobre o Projeto.

  • O dataset contém uma série de imagens de 4 tipos de grãos de café: dark, green, light e medium.
  • Estarei utilizando o modelo pré treinado do keras chamado Xception, cujo tem apresentado uma acurácia geral de 94.5% na classificação de imagens e uma quantidade de parâmetros relativamente pequena(22.9M).

Coleta de Dados

Configurando a API do Kaggle para a extração do dataset.

#hide
%%writefile kaggle.json
{"username":"your_username","key":"your_API_key"} # credenciais pessoais do Kaggle
Overwriting kaggle.json

Instalando as dependências do Kaggle

#hide
!pip install -q -U kaggle
!pip install --upgrade --force-reinstall --no-deps kaggle
!mkdir ~/.kaggle
!cp kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json
Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/
Collecting kaggle
  Using cached kaggle-1.5.12-py3-none-any.whl
Installing collected packages: kaggle
  Attempting uninstall: kaggle
    Found existing installation: kaggle 1.5.12
    Uninstalling kaggle-1.5.12:
      Successfully uninstalled kaggle-1.5.12
Successfully installed kaggle-1.5.12
mkdir: cannot create directory ‘/root/.kaggle’: File exists

Fazendo o download do dataset pela API do Kaggle.

#getting the dataset
!kaggle datasets download -d gpiosenka/coffee-bean-dataset-resized-224-x-224
coffee-bean-dataset-resized-224-x-224.zip: Skipping, found more recently modified local copy (use --force to force download)

Importando dependências necessárias.

import os
import pandas as pd
from shutil import move
import pathlib
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt
import numpy as np
import random
import itertools
import zipfile
from keras_preprocessing.image import ImageDataGenerator
import tensorflow as tf
import tensorflow_hub as hub
from tensorflow.keras.preprocessing import image

Armazenando o dataset em uma pasta.

if not os.path.exists(os.path.join('/content', 'coffee_dataset')):
  os.mkdir(os.path.join('/content', 'coffee_dataset'))

with zipfile.ZipFile('/content/coffee-bean-dataset-resized-224-x-224.zip', 'r') as zip_ref:
  zip_ref.extractall('/content/coffee_dataset')
print('Done extracting')
Done extracting
coffee_catalog = pd.read_csv('/content/coffee_dataset/Coffee Bean.csv')
coffee_catalog

Pre-processamento

#coffee Dark Green Light Medium
dark_test = os.listdir('/content/coffee_dataset/test/Dark')
green_test = os.listdir('/content/coffee_dataset/test/Green')
light_test = os.listdir('/content/coffee_dataset/test/Light')
medium_test = os.listdir('/content/coffee_dataset/test/Medium')

print(f'Dark test coffee: {len(dark_test)} \nGreen test coffee: {len(green_test)} \nLight test coffee: {len(light_test)} \nMedium train coffee: {len(medium_test)}')

#training folder check
dark_train = os.listdir('/content/coffee_dataset/train/Dark')
green_train = os.listdir('/content/coffee_dataset/train/Green')
light_train = os.listdir('/content/coffee_dataset/train/Light')
medium_train = os.listdir('/content/coffee_dataset/train/Medium')

print(f'\nDark train coffee: {len(dark_train)} \nGreen train coffee: {len(green_train)} \nLight train coffee: {len(light_train)} \nMedium train coffee: {len(medium_train)}')
Dark test coffee: 100 
Green test coffee: 100 
Light test coffee: 100 
Medium train coffee: 100

Dark train coffee: 300 
Green train coffee: 300 
Light train coffee: 300 
Medium train coffee: 300

Criando uma pasta para armazenar imagens para validação do modelo.

  • Dados de validação são usados para verificar a estabilidade do modelo.
  • Funcionam como uma espécie de garantia de que o modelo tenha captado a maioria dos padrões dos dados.
  • Evita a captação de ruídos (variância e viés baixos).
#creating a validation folder
if not os.path.exists(os.path.join('/content/coffee_dataset', 'validation')):
  os.mkdir(os.path.join('/content/coffee_dataset', 'validation'))
  os.mkdir(os.path.join('/content/coffee_dataset/validation', 'Dark'))#Dark
  os.mkdir(os.path.join('/content/coffee_dataset/validation', 'Green'))#Green
  os.mkdir(os.path.join('/content/coffee_dataset/validation', 'Light'))#Light
  os.mkdir(os.path.join('/content/coffee_dataset/validation', 'Medium'))#Medium

Armazenando o caminho de cada pasta de validação.

train_path = '/content/coffee_dataset/train'
val_dark = '/content/coffee_dataset/validation/Dark'
val_green = '/content/coffee_dataset/validation/Green'
val_light = '/content/coffee_dataset/validation/Light'
val_medium = '/content/coffee_dataset/validation/Medium'

#function to move files to validation folder
def moveFiles(walk_path, path_to, name, batch_file):
  counter = 0
  if len(os.listdir(path_to)) == 0: #recently added
    for root, subfolder, filename in os.walk(walk_path):
      for files in filename:
        if files.lower().startswith(name) and counter < batch_file:
          path_from = os.path.join(root ,files)
          move(path_from, path_to)
          counter += 1
    print(f'Done! moved {counter} from {path_from} to {path_to}')
  else:
    return 'Folder has already files in inside'
  

moveFiles(train_path, val_dark, 'dark', 30)
moveFiles(train_path, val_green, 'green', 30)
moveFiles(train_path, val_light, 'light', 30)
moveFiles(train_path, val_medium, 'medium', 30)

'Folder has already files in inside'

Já que o dataset não contém um conjunto de validação, criei a função moveFiles() para fazer exatamente isso. Ela move uma determinada quantidade de dados do conjunto de treino, neste caso 30 imagens de cada tipo de grão de café para o cojnunto de dados de validação.

Agora com os 3 conjuntos devidamente separados(train, test, validation), podemos prosseguir com a etapa de pre-processamento das imagens.

train_path = '/content/coffee_dataset/train'
test_path = '/content/coffee_dataset/test'
validation_path = '/content/coffee_dataset/validation'

Data Augmentation(Aumento de Dados)

  • A etapa de aumento de dados é utilizada para aumentar a diversidade e quantidade dos dados do dataset.
  • Muitas vezes, coletar milhões de imagens pra resolver um determinado problema pode ser um processo custoso, inviável e demorado. Para isso, recorremos a este método.
  • No processo de Data Augmentation é realizado vários processos de tratamento de imagens como: Rotação da imagem, orientação da imagem, ampliação, recorte, flipping(vertical e horizontal), etc.
  • No bloco de código abaixo, farei alguns dos processos de tratamento citados acima.

size = (224, 224)
batch_size = 30

# for data augmentation
train_datagen = ImageDataGenerator(rescale = 1.0/255, # redução de dimensionalidade
                                   rotation_range = 60, # rotação
                                   shear_range = 0.2, # distorção da imagem
                                   zoom_range = 0.2,  # ampliação
                                   horizontal_flip = True, # flipping
                                   fill_mode = 'nearest' # preenchimento
                                   )

train_generator = train_datagen.flow_from_directory(
    train_path,
    target_size = (224, 224),
    batch_size = batch_size,
    color_mode = 'rgb',
    class_mode = 'categorical'
)

test_generator = train_datagen.flow_from_directory(
    test_path,
    target_size = (224, 224),
    batch_size = batch_size,
    color_mode = 'rgb',
    class_mode = 'categorical',
    shuffle = False
)

validation_generator = train_datagen.flow_from_directory(
    validation_path,
    target_size = (224,224),
    color_mode = 'rgb',
    batch_size = batch_size,
    class_mode = 'categorical',
    shuffle = False
)
Found 1200 images belonging to 4 classes.
Found 400 images belonging to 4 classes.
Found 120 images belonging to 4 classes.

Explicação:

  • train_datagen é a instância criada do método ImageDataGenerator do TensorFlow que será responsável por realizar os processos de aumento de dados(amplicação, rotação, etc) nas imagens.
  • Como o nome sugere, train_generator, test_generator, validation_generator aplicam o processo de aumento de dados instanciado em train_datagen nos conjuntos de treino, teste e validação.
  • O modelo Xception trabalha melhor com imagens de dimensão 224x224, então redimensionei todas as imagens de treino, teste e validação.
  • batch_size corresponde a porção de elementos que ele seleciona por vez para aplicar o tratamento das imagens.
  • shuffle=False são específicas para o treino e validação, uma vez que ambos não serão utilizados no treinamento, não há necessidade de aleatoriedade. Já no treino, a aleatoriedade é necessária de forma a evitar vieses e consequentemente o overfitting.

Verificando as Imagens

Imagens antes do Data Augmentation!

def show_images(folder_path, samples = 5, figsize = (30, 5)):
  folder_names = os.listdir(folder_path)

  #iterando entre as pastas
  for folder_name in folder_names:
    images_path = f'{folder_path}/{folder_name}'

    # criando subplots
    fig, ax = plt.subplots(1, samples, figsize = figsize)
    plt.suptitle("Coffee beans belong to " + folder_name, color = 'crimson', fontsize = 20)

    # Percorrendo as pastas e exibindo o número de amostras
    for image_num in range(samples):
      # Selecionando uma imagem aleatória
      random_image_name = random.choice(os.listdir(images_path))

      # Retorna uma matriz das imagens contendo os valores dos pixels.
      image_matrix = plt.imread(images_path + '/' + random_image_name)
            
      # transformando a matriz de pixels em imagem
      ax[image_num].imshow(image_matrix)
            
      # exibindo o tipo de grão de café selecionado.
      ax[image_num].set_title(random_image_name)
    
# amostras do conjuto de treino
show_images(train_path, 7, figsize = (30,5))

Grãos de café do tipo "green".

png

Grãos de café do tipo "light".

png

Grãos de café do tipo "dark".

png

Grãos de café do tipo "medium".

png

#samples from test folders
show_images(test_path, 7, figsize = (30,5))

Grãos de café do tipo “green”
png

Grãos de café do tipo “light”
png

Grãos de café do tipo “dark”
png

Grãos de café do tipo “medium”
png

#samples from validation folders
show_images(validation_path, 7, figsize = (30,5))

Grãos de café do tipo “green”
png

Grãos de café do tipo “light”
png

Grãos de café do tipo “dark”
png

Grãos de café do tipo “medium”
png

Amostras Após o Data Augmentation!

def data_transformation_images(folder_path, image_gen_object, no_of_samples = 5, figsize = (35, 5)):
    
    # Nome das pastas armazenados aqui
    folder_names = os.listdir(folder_path)
    
    # Iterando em cada pasta
    for folder_name in folder_names:
        images_path = folder_path + '/' + folder_name
        
         # Criando subplots para as imagens
        fig, ax = plt.subplots(1, no_of_samples, figsize = figsize)
        
        # Coletando amostras
        random_image_name = random.choice(os.listdir(folder_path + '/' + folder_name))
        random_image = plt.imread(folder_path + '/' + folder_name + '/' + random_image_name)
        
        plt.suptitle("Coffee beans belong to " + folder_name + '-> ' + 'Selected image: ' + random_image_name, color = 'crimson', fontsize = 20)
        # Transformando em imagens e exibindo
        for image_num in range(no_of_samples):
            ax[image_num].imshow(train_datagen.random_transform(random_image))
            ax[image_num].set_title('Transformation Number: {}'.format(image_num + 1))
data_transformation_images(train_path, train_datagen, no_of_samples = 10)

png

png

png

png

Transfer Learning com Xception

Como Utilizar o Transfer Learning?

  • O transfer learning éo processo de reutilizar os pesos(weights) de um modelo pré-treinado para relizar uma tarefa semelhante.
  • É comumente utilizada quando fica difícil aumentar a acuracidade da classificação devida a baixa quantidade de imagens de treino.
  • É importante especificar, que neste processo de transfer learning, estarei utilizando da técnica de Extrator de Atributos Integrados, onde as camadas do modelo pré treinado são 'congeladas' durante o treino e somente as novas camadas criadas unicamente pra a classificação dos grãos de café serão treinadas para resolver o problema.
  • O congelameno é necessário para as camadas NÃO treinarem novamente, caso contrário, todos os pesos do modelo ja treinado serão modificados e alterados novamente.
input_shape = (224, 224, 3)

base_model = tf.keras.applications.Xception(include_top = False) # importando o Xception
base_model.trainable = False # congelamento das camadas

#estrutura do modelo
inputs = tf.keras.Input(shape = input_shape) # tamando e dimensões das imagens
x = base_model(inputs, training = False)
x = tf.keras.layers.GlobalAveragePooling2D()(x)
outputs = tf.keras.layers.Dense(4, activation = 'softmax')(x) # output layer com 4 saidas (green, light, dark ou medium)


xception = tf.keras.Model(inputs, outputs)
xception.summary()
Model: "model_3"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_8 (InputLayer)        [(None, 224, 224, 3)]     0         
                                                                 
 xception (Functional)       (None, None, None, 2048)  20861480  
                                                                 
 global_average_pooling2d_3   (None, 2048)             0         
 (GlobalAveragePooling2D)                                        
                                                                 
 dense_3 (Dense)             (None, 4)                 8196      
                                                                 
=================================================================
Total params: 20,869,676
Trainable params: 8,196
Non-trainable params: 20,861,480
_________________________________________________________________

Entendendo o Transfer Learning

  • No resumo do modelo, temos uma visualização melhor da importância do transfer learning.
  • Verificamos que o modelo possui mais de 20 milhões de parâmetros, mas apenas 8,196 será treinado.
  • Isso ocorre devido ao congelamento das camadas, desta forma, obtemos os pesos de um modelo treinado com muito robustez de maneira instantânea, imaginem quanto poder de processamento e tempo levaria pra treinar mais de 20 milhões de parâmetros do zero.
  • A única camada que será treinada é a camada de saída, sendo esta sendo uma camada especial e necessária responsável por delimitar o número de classificações que desejamos(4 saídas neste caso).
xception.compile(
    optimizer = 'Adam',
    loss = 'categorical_crossentropy',
    metrics = ['accuracy']
)

Aqui, foi definido o tipo de otimizador "Adam", amplamente difundido entre a comunidade por ser um método realmente eficiente que utiliza pouca memória. como loss function optei pelo "Categorical Crossentropy" por sem um modelo de multiplas classificações e utilizar uma camada softmax.


callback = tf.keras.callbacks.EarlyStopping(monitor='loss', patience=3)
history = xception.fit(
    train_generator,
    epochs = 10,
    validation_data = validation_generator,
    validation_steps = validation_generator.samples // validation_generator.batch_size,
    steps_per_epoch = train_generator.samples // train_generator.batch_size,
    callbacks =[callback],
    verbose = 1

)
Epoch 1/10
40/40 [==============================] - 21s 459ms/step - loss: 0.8138 - accuracy: 0.7275 - val_loss: 0.4601 - val_accuracy: 0.8833
Epoch 2/10
40/40 [==============================] - 17s 428ms/step - loss: 0.3606 - accuracy: 0.8975 - val_loss: 0.2851 - val_accuracy: 0.9417
Epoch 3/10
40/40 [==============================] - 17s 433ms/step - loss: 0.2548 - accuracy: 0.9367 - val_loss: 0.2327 - val_accuracy: 0.9500
Epoch 4/10
40/40 [==============================] - 17s 431ms/step - loss: 0.2141 - accuracy: 0.9475 - val_loss: 0.2043 - val_accuracy: 0.9333
Epoch 5/10
40/40 [==============================] - 17s 429ms/step - loss: 0.1891 - accuracy: 0.9475 - val_loss: 0.1940 - val_accuracy: 0.9333
Epoch 6/10
40/40 [==============================] - 17s 434ms/step - loss: 0.1643 - accuracy: 0.9567 - val_loss: 0.1795 - val_accuracy: 0.9667
Epoch 7/10
40/40 [==============================] - 18s 447ms/step - loss: 0.1476 - accuracy: 0.9550 - val_loss: 0.1601 - val_accuracy: 0.9500
Epoch 8/10
40/40 [==============================] - 17s 429ms/step - loss: 0.1354 - accuracy: 0.9608 - val_loss: 0.1604 - val_accuracy: 0.9333
Epoch 9/10
40/40 [==============================] - 17s 436ms/step - loss: 0.1233 - accuracy: 0.9617 - val_loss: 0.1058 - val_accuracy: 0.9833
Epoch 10/10
40/40 [==============================] - 17s 431ms/step - loss: 0.1205 - accuracy: 0.9700 - val_loss: 0.1328 - val_accuracy: 0.9583
  • Utilizei um callback de early stopping, isto significa que ele vai parar o treinamento caso não note nenhuma melhora num periodo de 3 épocas.
  • Passei o conjunto de dados de validação para monitorar o modelo a cada época e verificar seu treinamento.
  • steps_per_epoch diz quantos "passos" o modelo vai realizar em uma única época. Como definimos um batch_size = 30, o modelo realizará 30 forward propagations, então a loss function será calculada em todas as 30 predições que o modelo realizou.
  • Chamamos de uma época quando o modelo realizou vários "passos" e percorreu todo o conjunto de treinamento.
#evaluation
xception.evaluate(x = test_generator )
14/14 [==============================] - 5s 367ms/step - loss: 0.1190 - accuracy: 0.9725





[0.11898920685052872, 0.9725000262260437]

O modelo atingiu 97% de acurácia em 10 épocas.

Fine Tuning

  • Esta etapa simplesmente consiste em fazer pequenos ajustes durante o processo de refinamento do modelo.
  • Farei isso descongelando as 10 ultimas do modelo pre-treinado xception, isto quer dizer que as 10 últimas camadas não estarão mais congeladas, elas treinarão como as novas camada inseridas e em seguida, verificarei como o modelo performou.
#Set all trainable back to True
base_model.trainable = True

#making making the last 10 layers trainable
for layer in base_model.layers[:-10]:
  layer.trainable = False

#checking the trainable layers, uncomment to see
#for layer in base_model.layers:
  #print(layer.trainable)

#recompiling the model
xception.compile(loss = 'categorical_crossentropy', optimizer = tf.keras.optimizers.Adam(learning_rate = 0.0001), metrics = ['accuracy'])

#for layer in xception.layers:
#  print(layer.name, layer.trainable)
xception.summary()
Model: "model"
_________________________________________________________________
Layer (type)                Output Shape              Param #   
=================================================================
input_2 (InputLayer)        [(None, 224, 224, 3)]     0         
                                                                
xception (Functional)       (None, None, None, 2048)  20861480  
                                                                
global_average_pooling2d (G  (None, 2048)             0         
lobalAveragePooling2D)                                          
                                                                
dense (Dense)               (None, 4)                 8196      
                                                                
=================================================================
Total params: 20,869,676
Trainable params: 5,504,516
Non-trainable params: 15,365,160

Como podemos ver, apenas com 10 camadas liberadas para o treinamento, temos agora mais de 5 milhões de parâmetros.

#fine tune for more 15 epochs to reduce loss value

history_fine_tune = xception.fit(
    train_generator,
    epochs = 15,
    validation_data = validation_generator,
    validation_steps = validation_generator.samples // validation_generator.batch_size,
    steps_per_epoch = train_generator.samples // train_generator.batch_size,
    verbose = 1,
    callbacks = [callback]
                                 )
Epoch 1/15
40/40 [==============================] - 21s 461ms/step - loss: 0.1631 - accuracy: 0.9375 - val_loss: 0.0718 - val_accuracy: 0.9750
Epoch 2/15
40/40 [==============================] - 18s 437ms/step - loss: 0.0853 - accuracy: 0.9658 - val_loss: 0.0845 - val_accuracy: 0.9667
Epoch 3/15
40/40 [==============================] - 18s 452ms/step - loss: 0.0612 - accuracy: 0.9800 - val_loss: 0.0751 - val_accuracy: 0.9667
Epoch 4/15
40/40 [==============================] - 17s 434ms/step - loss: 0.0480 - accuracy: 0.9792 - val_loss: 0.0515 - val_accuracy: 0.9833
Epoch 5/15
40/40 [==============================] - 18s 436ms/step - loss: 0.0610 - accuracy: 0.9767 - val_loss: 0.1119 - val_accuracy: 0.9583
Epoch 6/15
40/40 [==============================] - 18s 441ms/step - loss: 0.0684 - accuracy: 0.9717 - val_loss: 0.0586 - val_accuracy: 0.9917
Epoch 7/15
40/40 [==============================] - 17s 433ms/step - loss: 0.0342 - accuracy: 0.9867 - val_loss: 0.0200 - val_accuracy: 1.0000
Epoch 8/15
40/40 [==============================] - 18s 437ms/step - loss: 0.0376 - accuracy: 0.9858 - val_loss: 0.0159 - val_accuracy: 1.0000
Epoch 9/15
40/40 [==============================] - 17s 432ms/step - loss: 0.0396 - accuracy: 0.9875 - val_loss: 0.0534 - val_accuracy: 0.9750
Epoch 10/15
40/40 [==============================] - 18s 450ms/step - loss: 0.0406 - accuracy: 0.9858 - val_loss: 0.0163 - val_accuracy: 0.9917

Com o fine tuning, podemos ver que a loss function diminuiu e a acurácia geral aumentou, exatamente como esperado.

Avaliando o Modelo

Matriz de Confusão

  • Matriz de Confusão é uma métrica muito utilizada em problemas de classificação pois auxilia na avaliação de modelos de classificação, mapeando imagens que o modelo classificou corretamente(verdadeiros positivos) e as que ele classificou de maneira equivocada(falso positivo), o mesmo serve para o caso oposto(verdadeiro negativo, falso negativo).
  • Desta forma, podemos verificar se possui algum caso específico em que o modelo esteja errando bastante como por exemplo, grande parte dos erros poderia ser em apenas um caso em que o modelo precisaria classificar grãos de café do tipo 'dark'.
#Confusion Matrix Function


def get_CM(cm, classes, normalize = False, title = 'Confusion Matrix', cmap = plt.cm.Blues):
  plt.figure(figsize=(12,8))
  plt.imshow(cm, interpolation='nearest', cmap=cmap)
  plt.title(title)
  plt.colorbar()
  tick_marks = np.arange(len(classes))
  plt.xticks(tick_marks, classes, rotation=45)
  plt.yticks(tick_marks, classes)

  if normalize:
      cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
      print("Normalized confusion matrix")
  else:
      print('Confusion matrix, without normalization')

  print(cm)

  thresh = cm.max() / 2.
  for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
      plt.text(j, i, cm[i, j],
          horizontalalignment="center",
          color="white" if cm[i, j] > thresh else "black")

  plt.tight_layout()
  plt.ylabel('True label')
  plt.xlabel('Predicted label')



predictions = xception.predict(x = test_generator, steps = len(test_generator), verbose = 0)  

cm = confusion_matrix(y_true = test_generator.classes, y_pred = predictions.argmax(axis = 1))

cm_plot_labels = ['Dark','Green','Light','Medium']
get_CM(cm = cm, classes = cm_plot_labels, title = 'Confusion Matrix')
Confusion matrix, without normalization
[[ 99   0   0   1]
 [  0 100   0   0]
 [  0   1  98   1]
 [  3   0   0  97]]

png

train_generator.batch_size
train_generator.samples
train_generator.class_indices
{'Dark': 0, 'Green': 1, 'Light': 2, 'Medium': 3}

Gráfico de uma Função de Erro

losses = pd.DataFrame(history.history)
losses[['loss', 'val_loss']].plot(figsize = (15,8))
plt.show()

png

  • Função de erro: Uma função de erro serve para avaliar a predição do modelo. Se houver exemplos conhecidos, uma função de erro poderá fazer uma comparação para avaliar a precisão do modelo.
  • Quanto menor for o valor da função de erro, melhor o modelo performará.
  • Como podemos ver, a medida em que as épocas aumentam, a função de erro diminui, o que é esperado que aconteça.

Gráfico de Acurácia

losses[['accuracy', 'val_accuracy']].plot(figsize = (15, 8))
plt.axhline(0.90, color = 'green', linestyle = '--')
plt.axvline(5, color = 'green', linestyle = '--')
plt.show()

png

  • A acurácia serve para verificarmos o quão preciso o modelo foi, quanto maior o valor, melhor.
  • Como podemos ver, a medida em que as épocas aumentam, maior a acurácia fica, o é esperado de um modelo bem treinado.

Testando Novas Imagens

Para fazer o teste com uma imagem específica, estarei coletando do conjunto de validação, desde que é um conjunto que o modelo não treinou, então ele nunca 'viu' as imagens.

Para realizar esta tarefa, criei um randomizador de coleta das imagens e 2 funções:

  1. load_and_prep() - coleta as imagens, transforma ela em um vetor e as redimensiona para que possa ser avaliada pelo modelo.
  2. pred_and_plot() - passa a imagem vetorizada para o modelo, o modelo avalia a imagem dando notas de 0 a 1 para cada tipo de grão de café. O maior valor entre eles(argmax) será a classificação final do modelo, mostrando o nome do arquivo, o nome da classe que o modelo previu e uma foto do respectivo grão.
data_dir = pathlib.Path(validation_path)
class_names = np.array(sorted([item.name for item in data_dir.glob("*")])) # creating a list of class names from subdirectory 
print(f'Classes are: {class_names}')


#image vectorizing function
def load_and_prep_image(filename, img_shape = 224):
  img = tf.io.read_file(filename) #lendo a imagem
  img = tf.image.decode_image(img) # decodificando para um formato tensor 
  img = tf.image.resize(img, size = [img_shape, img_shape]) # redimensionando a imagem
  img = img/255. # rescale the image
  return img
Classes are: ['Dark' 'Green' 'Light' 'Medium']
def pred_and_plot(model, filename, class_names):
  
  # usando a função acima
  img = load_and_prep_image(filename) 
  # fazendo a previsão
  pred = model.predict(tf.expand_dims(img, axis=0))

  # Get the predicted class
  if len(pred[0]) > 1: # check for multi-class
    pred_class = class_names[pred.argmax()] # pegando o maior valor da classificação
  else:
    pred_class = class_names[int(tf.round(pred)[0][0])] # if only one output, round

  # exibindo a imagem
  plt.figure(figsize=(12,8))
  plt.imshow(img)
  plt.title(f"ORIGINAL FILE: {filename.split('/')[-1]}\n\nMODEL PREDICTION: {pred_class}", color = 'orange', fontsize = 20)
  plt.axis(False)
# selecionando um grão de café aleatório dentro do conjunto de validação.
random_folder = random.choice(os.listdir(validation_path))
# selecionando um arquivo de grão de café aleatório
random_image = random.choice(os.listdir(validation_path + '/' + random_folder + '/'))

# unindo a pasta aleatória com o arquivo aleatório
test_image = validation_path + '/' + random_folder + '/' + random_image

#print(f"Original File Path: {test_image}")
pred_and_plot(xception, test_image, class_names)

png

Como podemos ver, o arquivo original(medium (37).png) era um grão médio de café e a previsão do modelo foi exatamente um grão médio =D.

Por hoje é só e muito obrigado =P. críticas e feedbacks construtivos são bem vindos. Caso queira checar o código completo basta clicar aqui.