improve cnn : Epoch 100/100
124/124 ━━━━━━━━━━━━━━━━━━━━ 1s 6ms/step - accuracy: 0.8963 - loss: 0.3435 - val_accuracy: 0.8532 - val_loss: 0.8805 : # Import necessary libraries
import tensorflow as tf
import os
import numpy as np
import glob
from PIL import Image
import matplotlib.pyplot as plt

def getFENtileLabel(fen,letter,number):
  """Given a fen string and a rank (number) and file (letter), return label vector"""
  l2i = lambda l:  ord(l)-ord('A') # letter to index
  number = 8-number # FEN has order backwards
  piece_letter = fen[number*8+number + l2i(letter)]
  label = np.zeros(13, dtype=np.uint8)
  label['1KQRBNPkqrbnp'.find(piece_letter)] = 1 # note the 1 instead of ' ' due to FEN notation
  # We ignore shorter FENs with numbers > 1 because we generate the FENs ourselves
  return label

# We'll define the 12 pieces and 1 spacewith single characters 
#  KQRBNPkqrbnp
def getLabelForSquare(letter,number):
  """Given letter and number (say 'B3'), return one-hot label vector
     (12 pieces + 1 space == no piece, so 13-long vector)"""
  l2i = lambda l:  ord(l)-ord('A') # letter to index
  piece2Label = lambda piece: ' KQRBNPkqrbnp'.find(piece)
  # build mapping to index
  # Starter position
  starter_mapping = np.zeros([8,8], dtype=np.uint8)
  starter_mapping[0, [l2i('A'), l2i('H')]] = piece2Label('R')
  starter_mapping[0, [l2i('B'), l2i('G')]] = piece2Label('N')
  starter_mapping[0, [l2i('C'), l2i('F')]] = piece2Label('B')
  starter_mapping[0, l2i('D')] = piece2Label('Q')
  starter_mapping[0, l2i('E')] = piece2Label('K')
  starter_mapping[1, :] = piece2Label('P')

  starter_mapping[7, [l2i('A'), l2i('H')]] = piece2Label('r')
  starter_mapping[7, [l2i('B'), l2i('G')]] = piece2Label('n')
  starter_mapping[7, [l2i('C'), l2i('F')]] = piece2Label('b')
  starter_mapping[7, l2i('D')] = piece2Label('q')
  starter_mapping[7, l2i('E')] = piece2Label('k')
  starter_mapping[6, :] = piece2Label('p')
  # Note: if we display the array, the first row is white,
  # normally bottom, but arrays show it as top
  
  # Generate one-hot label
  label = np.zeros(13, dtype=np.uint8)
  label[starter_mapping[number-1, l2i(letter), ]] = 1
  return label

def name2Label(name):
  """Convert label vector into name of piece"""
  return ' KQRBNPkqrbnp'.find(name)

def labelIndex2Name(label_index):
  """Convert label index into name of piece"""
  return ' KQRBNPkqrbnp'[label_index]

def label2Name(label):
  """Convert label vector into name of piece"""
  return labelIndex2Name(label.argmax())

def shortenFEN(fen):
  """Reduce FEN to shortest form (ex. '111p11Q' becomes '3p2Q')"""
  return fen.replace('11111111','8').replace('1111111','7') \
            .replace('111111','6').replace('11111','5') \
            .replace('1111','4').replace('111','3').replace('11','2')

def lengthenFEN(fen):
  """Lengthen FEN to 71-character form (ex. '3p2Q' becomes '111p11Q')"""
  return fen.replace('8','11111111').replace('7','1111111') \
            .replace('6','111111').replace('5','11111') \
            .replace('4','1111').replace('3','111').replace('2','11')

def unflipFEN(fen):
    if len(fen) < 71:
        fen = lengthenFEN(fen)
    return '/'.join([ r[::-1] for r in fen.split('/') ][::-1])


# For Training in IPython Notebooks
def loadFENtiles(image_filepaths):
  """Load Tiles with FEN string in filename for labels.
  return both images and labels"""
  # Each tile is a 32x32 grayscale image, add extra axis for working with MNIST Data format
  images = np.zeros([image_filepaths.size, 32, 32, 1], dtype=np.uint8)
  labels = np.zeros([image_filepaths.size, 13], dtype=np.float64)

  for i, image_filepath in enumerate(image_filepaths):
    if i % 1000 == 0:
      #print("On #%d/%d : %s" % (i,image_filepaths.size, image_filepath))
      print(".",)
    
    # Image
    images[i,:,:,0] = np.asarray(Image.open(image_filepath), dtype=np.uint8)

    # Label
    fen = image_filepath[-78:-7]
    _rank = image_filepath[-6]
    _file = int(image_filepath[-5])
    labels[i,:] = getFENtileLabel(fen, _rank, _file)
  print("Done")
  return images, labels

def loadLabels(image_filepaths):
  """Load label vectors from list of image filepaths"""
  # Each filepath contains which square we're looking at, 
  # since we're in starter position, we know which
  # square has which piece, 12 distinct pieces 
  # (6 white and 6 black) and 1 as empty = 13 labels
  training_data = np.zeros([image_filepaths.size, 13], dtype=np.float64)
  for i, image_filepath in enumerate(image_filepaths):
    training_data[i,:] = getLabelForSquare(image_filepath[-6],int(image_filepath[-5]))
  return training_data

def loadImages(image_filepaths):
  # Each tile is a 32x32 grayscale image, add extra axis for working with MNIST Data format
  training_data = np.zeros([image_filepaths.size, 32, 32, 1], dtype=np.uint8)
  for i, image_filepath in enumerate(image_filepaths):
    if i % 100 == 0:
      print("On #%d/%d : %s" % (i,image_filepaths.size, image_filepath))
    img = Image.open(image_filepath)
    training_data[i,:,:,0] = np.asarray(img, dtype=np.uint8)
  return training_data

# Set NumPy print options for better readability
np.set_printoptions(precision=2, suppress=True)

# Load and prepare the data
# Adjust the path according to your data location
data_path = "tiles/*/*.png"  # Update this path as needed
all_paths = np.array(glob.glob(data_path))  # Ensure labels are set correctly

# Verify that the data path contains files
if len(all_paths) == 0:
    print(f"No image files found in the path: {data_path}")
    exit(1)

# Shuffle the paths
np.random.shuffle(all_paths)

# Split the data into training and testing sets (90/10 ratio)
ratio = 0.9  # Training/testing ratio
divider = int(len(all_paths) * ratio)
train_paths = all_paths[:divider]
test_paths = all_paths[divider:]

# Load training dataset
print(f"Loading {train_paths.size} Training tiles")
train_images, train_labels = loadFENtiles(train_paths)  # Ensure this function works correctly

# Load testing dataset
print(f"Loading {test_paths.size} Testing tiles")
test_images, test_labels = loadFENtiles(test_paths)

# Reshape images to match input shape for the CNN (32x32x1)
train_images = train_images.reshape(-1, 32, 32, 1)
test_images = test_images.reshape(-1, 32, 32, 1)

# Build the CNN model using TensorFlow 2 and Keras
print("Setting up CNN...")

model = tf.keras.Sequential([
    tf.keras.layers.InputLayer(input_shape=(32, 32, 1)),

    # First convolutional layer: 32 features, 5x5 kernel
    tf.keras.layers.Conv2D(filters=32, kernel_size=(5, 5), padding='same', activation='relu'),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=2, padding='same'),

    # Second convolutional layer: 64 features, 5x5 kernel
    tf.keras.layers.Conv2D(filters=64, kernel_size=(5, 5), padding='same', activation='relu'),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=2, padding='same'),

    # Flatten layer to convert 2D data to 1D
    tf.keras.layers.Flatten(),

    # Densely connected layer with 1024 neurons
    tf.keras.layers.Dense(units=1024, activation='relu'),

    # Dropout layer to reduce overfitting
    tf.keras.layers.Dropout(rate=0.5),

    # Output layer with softmax activation for classification
    tf.keras.layers.Dense(units=13, activation='softmax'),
])

# Compile the model
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),
              loss='categorical_crossentropy',
              metrics=['accuracy'])

# Set training parameters
do_training = True  # Set to True to train the model
epochs = 25         # Adjust the number of epochs as needed
batch_size = 100    # Batch size for training

if do_training:
    # Train the model
    print(f"Training for {epochs} epochs...")
    model.fit(train_images, train_labels, epochs=epochs, batch_size=batch_size, 
              validation_data=(test_images, test_labels), verbose=1)
    print("Finished training.")
    
    # Ensure 'saved_models' directory exists
    if not os.path.exists('saved_models'):
        os.makedirs('saved_models')
    
    # Save the trained model
    model.save('saved_models/model_{}.h5'.format(epochs))
    print(f"Model saved in file: saved_models/model_{epochs}.h5")
else:
    # Load the pre-trained model
    model_name = 'saved_models/model_{}.h5'.format(epochs)
    print(f"Loading model '{model_name}'")
    if os.path.exists(model_name):
        model = tf.keras.models.load_model(model_name)
        print("Model loaded.")
    else:
        print(f"Model file '{model_name}' not found.")
        exit(1)

# Evaluate the model on the testing dataset
test_loss, test_accuracy = model.evaluate(test_images, test_labels, verbose=0)
print(f"Accuracy on test set: {test_accuracy * 100:.2f}%\n")

# Identify and display any misclassified images

# Get predictions
predictions = model.predict(test_images)
predicted_labels = np.argmax(predictions, axis=1)
true_labels = np.argmax(test_labels, axis=1)
correct_predictions = predicted_labels == true_labels
mistake_indices = np.where(~correct_predictions)[0]

# Display mistakes
if mistake_indices.size > 0:
    print(f"{mistake_indices.size} mistakes found:")
    for idx in np.random.choice(mistake_indices, min(5, mistake_indices.size), replace=False):
        actual_label = label2Name(test_labels[idx])
        guessed_label = labelIndex2Name(predicted_labels[idx])
        print("---")
        print(f"\t#{idx} | Actual: '{actual_label}', Guessed: '{guessed_label}'")
        print("Actual label vector:", test_labels[idx])
        print("Predicted probabilities:", predictions[idx])
        # Display the misclassified image
        plt.imshow(test_images[idx].reshape(32, 32), cmap='gray')
        plt.title(f"Actual: '{actual_label}', Guessed: '{guessed_label}'")
        plt.show()
else:
    print("No mistakes found.")

# Display several random samples, including successes
sample_indices = np.random.choice(test_images.shape[0], 5, replace=False)
for idx in sample_indices:
    actual_label = label2Name(test_labels[idx])
    predicted_label = labelIndex2Name(predicted_labels[idx])
    print(f"#{idx} | Actual: '{actual_label}', Guessed: '{predicted_label}'")
    # Display the image
    plt.imshow(test_images[idx].reshape(32, 32), cmap='gray')
    plt.title(f"Actual: '{actual_label}', Guessed: '{predicted_label}'")
    plt.show()