Chanbin Lee, Julie Breuil, Lucas Damase, Salma Bahar
# Rapport TIP
## Introduction:
Le projet aborde la problématique de la reconnaissance des mélanomes dans les images médicales SIIM-ISIC à l'aide de techniques de Computer Vision et de Machine Learning (CVML). La reconnaissance précise de ces lésions cutanées est cruciale pour un diagnostic précoce et une prise en charge appropriée des patients.
Afin d'affiner la performance de nos modèles, nous avons investi dans des étapes clés telles que le prétraitement des données et l'augmentation de celles-ci. Pour le volet entraînement, nous avons exploré trois architectures de réseaux de neurones de pointe: SE-ResNeXt, ResNet, et EfficientNet. Chacune de ces architectures a ses propres forces et faiblesses, et leur combinaison dans un modèle d'apprentissage ensembliste vise à tirer parti de cette diversité pour améliorer la généralisation du modèle. Dans ce rapport, nous détaillerons ces étapes, mettrons en lumière le code impliqué, et discuterons des résultats obtenus.
## Prétraitement des données d'entraînement
### Pourquoi faire du prétraitement
Le prétraitement des données d'entraînement revêt une grande importance, surtout en présence d'un déséquilibre significatif entre les classes. Avec seulement 2% d'images de mélanomes et 98% d'images de non-mélanomes, un biais d'apprentissage peut se produire. Les modèles tendent à favoriser la classe majoritaire, négligeant la classe minoritaire. Cela peut entraîner des difficultés de détection des mélanomes en raison de leur faible représentation.
L'augmentation de données consiste à créer de nouvelles données en appliquant des changements aléatoires aux images de la classe minoritaire. Dans notre cas, cela nous permet de générer diverses images synthétiques de mélanomes, aidant ainsi le modèle à mieux reconnaître cette classe moins représentée.
### Génération d'images mélanomes artificielles :
#### Rotation
L'objectif initial était de créer le plus d'images artificielles en faisant tourner les images de mélanomes existantes. Bien que l'idée initiale ait été d'utiliser un angle de rotation de 30 degrés pour générer un nombre maximal d'images, cette approche a malheureusement conduit à des résultats répétitifs. Afin de remédier à cette situation, une rotation plus prononcée de 90 degrés a été adoptée pour réduire les similitudes entre les images générées. Le code Python ci-dessous illustre le processus de rotation et la génération d'images. Pour chaque image d'origine, trois nouvelles images sont créées en appliquant une rotation de 90 degrés à chaque itération de la boucle.
```python
for i in range(3):
new_image_name_base = f"ISIC-{len(df) + image_counter + 1:07d}_rotated_{i+1}"
rotated_image = transforms.functional.rotate(original_image.copy(), 90 * (i + 1))
rotated_image.save(os.path.join("train-resized", new_image_name_base + ".jpg"))
new_entry_rotated = {"image_name": new_image_name_base, "target": 1}
df = df.append(new_entry_rotated, ignore_index=True)
```
#### Fonction de génération de cheveux/poils
Certaines images originales présentent des poils ou des cheveux, ce qui a suscité l'idée de générer davantage d'images de mélanomes en ajoutant artificiellement des poils aux images.
La fonction `draw_hairs` prend une image en entrée et ajoute de manière aléatoire des courbes Bézier représentant des cheveux sur cette image, mais seulement si c'est une image de mélanome. La fonction commence par créer un objet `ImageDraw` associé à l'image d'entrée, permettant ainsi l'application d'opérations de dessin sur cette image. Ensuite, elle récupère la largeur et la hauteur de l'image pour définir les limites des coordonnées de départ des cheveux.
La fonction entre ensuite dans une boucle qui génère 10 cheveux. Les coordonnées de départ du cheveu sont déterminées de manière aléatoire à l'intérieur des limites de l'image. La longueur, l'angle d'inclinaison, l'amplitude et la fréquence d'ondulation du cheveu sont également générés de manière aléatoire. La couleur du cheveu est fixée à noir, et l'épaisseur du cheveu est déterminée de manière aléatoire.
L'utilisation de la courbe de Bézier pour représenter le poil permet de créer des courbes fluides, ce qui est essentiel pour reproduire la nature fluide et ondulante des cheveux. Les points de la courbe sont calculés à l'aide d'une boucle qui prend en compte la position le long du cheveu ainsi que l'amplitude et la fréquence d'ondulation pour créer une légère courbure.
```python
def draw_hairs(image):
draw = ImageDraw.Draw(image)
width, height = image.size
for _ in range(10):
start_x = random.randint(0, width)
start_y = random.randint(0, height)
length = random.randint(10, 100)
angle = random.uniform(20, 60)
wave_amplitude = random.randint(5, 10)
wave_frequency = random.uniform(0.01, 0.05)
end_x = start_x + int(length * math.cos(math.radians(angle)))
end_y = start_y - int(length * math.sin(math.radians(angle)))
points = [(start_x, start_y)]
for t in range(1, 101):
x = int(start_x + t / 100 * (end_x - start_x))
y = int(start_y + t / 100 * (end_y - start_y) + wave_amplitude * math.sin(wave_frequency * t))
points.append((x, y))
hair_color = (0, 0, 0)
hair_thickness = random.uniform(0.5, 0.9)
draw.line(points, fill=hair_color, width=int(hair_thickness))
return image
```

#### Ajout de tâches d'encre
Tout comme avec la fonction `draw_hairs`, en examinant les images originales dans le répertoire `train-resized`, nous avons constaté que certaines images contenaient des gouttes d'encre, d'où l'idée de créer une fonction `add_ink_drops` qui prend une image de mélanome en entrée et ajoute de manière aléatoire des gouttes d'encre sur cette image.
La fonction utilise la largeur et la hauteur de l'image pour définir les limites des positions où les gouttes d'encre peuvent être dessinées. Ensuite, elle entre dans une boucle qui génère des gouttes d'encre. Les positions des gouttes sont déterminées de manière aléatoire à l'intérieur des limites de l'image. Chaque goutte d'encre a une taille aléatoire et une couleur prédéfinie, le bleu. La goutte d'encre est représentée sous la forme d'un cercle à l'aide de la méthode `draw.ellipse`.
```python
def add_ink_drops(image, num_drops):
draw = ImageDraw.Draw(image)
width, height = image.size
for _ in range(num_drops):
drop_x = random.randint(0, width)
drop_y = random.randint(0, height)
drop_size = random.randint(5, 20)
ink_color = (0, 0, 255)
draw.ellipse([drop_x, drop_y, drop_x + drop_size, drop_y + drop_size], fill=ink_color)
return image
```

#### Suppression de la moitié des images non mélanomes
La suppression de la moitié des images non mélanomes a été réalisée pour équilibrer le jeu de données.
Le résultat final comprenait 11680 images mélanomes générées en combinant les différentes fonctions expliquées précédemment et 16271 images non mélanomes, favorisant un meilleur entraînement et une détection plus précise des mélanomes potentiels. Les conséquences de cette opération avant et après l'ajout de tâches seront analysées.
#### Résultat
Nous avons choisi d'utiliser pour nos entrainements deux jeux de données train-resized: un de ~44 000 éléments avec 25% de mélanomes en appliquant seulement le script combinant rotation, fonction cheveux et fonction encre et l'autre avec ~28 000 en appliquant la suppression de la moitié des images non mélanomes ce qui donne 42% d'images mélanomes.
## Training
Cette section expliquera le squelette du code utilisé, mettant en avant l'utilisation des architectures de réseaux de neurones ResNet50, SE-ResNeXt, et EfficientNet_b3. Les choix de paramètres tels que le nombre d'époques, la taille du batch, et la profondeur du modèle seront discutés en détail. Des graphiques illustrant les courbes de pertes par époque seront présentés, suivis d'une interprétation des résultats.
### Explication générale du code
Le code détaillé ici est le code pour le modèle ResNet50. Les autre modèles sont utilisés avec la même structure de code.
#### Les Initialisations
```python
df = pd.read_csv("train-labels.csv")
train_df, val_df = train_test_split(df, test_size=0.2, random_state=42)
```
On utilise deux jeux de données pour l'entraînement. Un contient ~44 000 éléments avec 25% de mélanomes. L'autre contient ~28 000 éléments avec 41% de mélanomes.
On lit le fichier `train-labels.csv` qui contient les labels associés aux données d'apprentissage, pour ensuite les stocker dans un data frame.
Ce dernier sera divisé en deux groupes: un groupe d'entraînement `train_df` et un groupe de validation `val_df`. On réservera à chaque fois 20% pour les données de validation.
```python
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.RandomVerticalFlip(),
transforms.RandomRotation(60),
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
```
Cette partie du code permet de transformer les images de la base afin d'augmenter notre nombre de données d'entraînement et améliorer les performances de notre réseau.
On peut effectuer une inversion horizontale et verticale sur l'image avec une probabilité de 0,5.
On redimensionne ensuite l'image de taille 224x244 et la convertit en un tenseur Pytorch avant de normaliser les valeurs des pixels, pour qu'elle puisse être traitée correctement par le réseau.
```python
train_dataset = CustomDataset(train_df, transform=train_transform)
val_dataset = CustomDataset(val_df, transform=transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
]))
```
Le `train_dataset` utilise la classe `CustomDataset` de Pytorch ainsi que la fonction `train_transform` vu précédemment pour créer les données d'entraînement. Or, on n'utilise pas les transformations de `train_transform` (seulement redimensionnement, conversion en tenseur et normalisation) pour `val_dataset` qui crée les données de validation, car on n'a pas besoin de varier les données comme pour l'étape d'apprentissage.
```python
class_labels = train_df['target'].values
class_weights = compute_class_weight('balanced', classes=[0, 1], y=class_labels)
class_weights = torch.FloatTensor(class_weights)
class_weights = class_weights.to(device)
batch_size = 64
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
resnet = models.resnet50(pretrained=True)
num_classes = 2
num_ftrs = resnet.fc.in_features
resnet.fc = nn.Linear(num_ftrs, num_classes)
resnet = resnet.to(device)
```
On récupère les labels du dataframe d'entraînement, puis on calcule les poids des classes. Puis on crée des objets DataLoader pour les données d'entraînement et de validation, le `shuffle=True` indiquant qu'on mélange les données à chaque époque pour optimiser le modèle.
On charge ensuite le modèle ResNet50 pré-entraîné grâce à la fonction `resnet50` de Pytorch et on remplace la dernière couche de classification par le nombre de classes qu'on doit avoir (2 dans notre cas: mélanomes et non-mélanomes).
```python
criterion = nn.CrossEntropyLoss(weight=class_weights)
optimizer = optim.Adam(resnet.parameters(), lr=0.001)
train_losses = []
train_accuracies = []
val_losses = []
val_accuracies = []
class0_precision = []
class0_recall = []
class0_f1score = []
class0_score = []
class1_precision = []
class1_recall = []
class1_f1score = []
class1_score = []
```
On définit ici la fonction de perte utilisant la perte pondérée, et l'optimiseur qui permettent d'ajuster les poids du modèle lors de l'entraînement.
On définit également les listes qui permettront de stocker les données résultantes (loss, precision, accuracy, etc.) tout au long de l'exécution.
#### Entraînement du modèle
```python
num_epochs = 30
resnet.train()
for epoch in range(num_epochs):
running_loss = 0.0
correct_train = 0
total_train = 0
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = resnet(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
total_train += labels.size(0)
correct_train += (predicted == labels).sum().item()
epoch_loss_train = running_loss / len(train_loader)
train_losses.append(epoch_loss_train)
accuracy_train = 100 * correct_train / total_train
train_accuracies.append(accuracy_train)
print(f"Époque [{epoch + 1}/{num_epochs}] - Perte (entraînement) : {epoch_loss_train:.4f} - Précision (entraînement) : {accuracy_train:.2f}%")
```
L'entraînement est effectué sur 30 époques ici, mais nous discuterons du choix du nombre d'époques dans la partie "choix des paramètres".
Pour chaque époque, trois variables sont initialisées :
- `running_loss` stocke la perte cumulée pendant une époque
- `correct_train` compte le nombre de prédictions correctes pendant une époque
- `total_train` compte le nombre total d'exemples dans une époque
Puis, pour chaque batch, la perte est calculée entre les prédictions du modèle ResNet et les labels réels. Une rétropropagation est ensuite effectuée afin de calculer les gradients des paramètres par rapport à la perte calculée auparavant. Les paramètres du modèle sont ensuite mis à jour.
A la fin des boucles sur chacun des batchs, les variables initialisées plus tôt sont calculées, ce qui permet de connaître la perte et la précision sur une époque sur les données d'entraînement.
#### Evaluation sur les données de validation
```python
resnet.eval()
correct_val = 0
total_val = 0
val_loss = 0.0
all_predicted = []
all_labels = []
with torch.no_grad():
for inputs, labels in val_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = resnet(inputs)
_, predicted = torch.max(outputs.data, 1)
total_val += labels.size(0)
correct_val += (predicted == labels).sum().item()
val_loss += criterion(outputs, labels).item()
all_predicted.extend(predicted.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
epoch_loss_val = val_loss / len(val_loader)
val_losses.append(epoch_loss_val)
accuracy_val = 100 * correct_val / total_val
val_accuracies.append(accuracy_val)
print(f"Époque [{epoch + 1}/{num_epochs}] - Perte (validation) : {epoch_loss_val:.4f} - Précision (validation) : {accuracy_val:.2f}%")
```
Ce script est également réalisé dans une boucle qui itère sur chaque époque. Après avoir ajusté le modèle sur des données d'entraînement, le modèle est testé sur des données de validation.
De la même manière, le script réalise une boucle sur chacun des batchs, calcule la perte entre les labels prédits et réels.
La perte et la précision sur une époque avec les données de validation sont également calculées à fin des boucles sur les batchs.
#### Sauvegarde des résultats
```python
resnet.train()
target_names = ['Non-mélanome', 'Mélanome']
classification_rep = classification_report(all_labels, all_predicted, target_names=target_names, output_dict=True)
for i, class_name in enumerate(target_names):
metrics = classification_rep[class_name]
precision = metrics['precision']
recall = metrics['recall']
f1_score = metrics['f1-score']
support = metrics['support']
print(f"{class_name} : Precision = {precision} ; Recall = {recall} ; F1-score = {f1_score} ; Support = {support}")
if (class_name == "Non-mélanome"):
class0_precision.append(precision)
class0_recall.append(recall)
class0_f1score.append(f1_score)
class0_score.append(support)
else:
class1_precision.append(precision)
class1_recall.append(recall)
class1_f1score.append(f1_score)
class1_score.append(support)
```
Dans cette partie, on revient au mode d'entraînement avec `resnet.train()`, qui sera l'étape de préparation pour la prochaine époque.
Pour cela on calcule les métriques (précision, score F1, etc.) de chaque classe avec `classification_rep`, puis on les stocke dans un dictionnaire `output_dict` pour pouvoir suivre les performances des classes.
On affiche ces métriques et on les stocke dans la liste de la classe correspondante, ce qui servira pour le tracé des courbes de performance de chaque classe.
#### Sortie
```python
result_data = pd.DataFrame({
'train_loss': train_losses,
'train_accuracy': train_accuracies,
'val_loss': val_losses,
'val_accuracy': val_accuracies
})
result_data.to_csv('training_results.csv', index=False)
result_data = pd.DataFrame({
'precision': class0_precision,
'recall': class0_recall,
'f1_score': class0_f1score,
'support': class0_score
})
result_data.to_csv('no-melanoms_metrics.csv', index=False)
result_data = pd.DataFrame({
'precision': class1_precision,
'recall': class1_recall,
'f1_score': class1_f1score,
'support': class1_score
})
result_data.to_csv('melanoms_metrics.csv', index=False)
torch.save({
'model': resnet.state_dict(),
'optimizer': optimizer.state_dict(),
'result_data': result_data
}, 'test_accuracy_perte.pth')
print("Entraînement terminé.")
```
Cette partie permet de sauvergarder les métriques de l'entraînement du modèle pour chaque époque.
Trois fichiers csv sont crées :
- `training_results.csv` stocke les pertes et les précisions pour les données d'entraînement et de validation
- `no-melanoms_metrics.csv` stocke les métriques pour la classe non-mélanomes
- `melanoms_metrics.csv` stocke les métriques pour la classe mélanomes
### Choix des paramètres
Sur chaque modèle nous avons commencé par choisir un nombre d'epochs maximal de 15 que nous avons ensuite fait varier à 50 pour voir si les modèles apprenaient mieux avec plus d'epochs.
Nous nous sommes rendus compte qu'en fonction du modèle, la perte et l'exactitude convergeaient plus ou moins rapidement, nous avons alors fait des tests en diminuant le nombre d'epochs afin d'éviter le surapprentissage.
De plus, nous nous sommes heurtés à la problématique du temps d'entraînement : amener l'entraînement à 50 epochs n'était pas rentable au niveau des résultats vis-à-vis du temps d'entrainement. Finalement, le nombre d'epochs a été grandement limité par nos ressources matérielles et au temps nécéssaire pour l'entraînement des modèles.
Nous avons aussi décidé d'utiliser une taille de batch de 64 car c'était la limite la plus stable que l'on pouvait utiliser avec nos ressources materielles. De plus, après plusieurs tests, une taille de batch de 64 présentait de meilleurs résultats que des valeurs plus petites.
### Comparaison des modèles utilisés
#### Explication du fonctionnement des modèles
##### Resnet-50
Le Resnet (Residual Neural Network ou Réseau neuronal résiduel) est un réseau de neurones profonds utilisant des “sauts de connexion” ou “raccourcis” qui permettent de sauter certaines couches du réseau et éviter le risque de disparition du gradient. En effet, il est souvent possible que le gradient calculé pour la rétropropagation diminue au fil de l’apprentissage et ne modifie plus les poids des couches profondes de manière significative, ce qui ralentit l’apprentissage.
Le ResNet est une solution qui permet d'accélérer ce dernier en ignorant certaines couches, puis en les restaurant lorsque l’espace des caractéristiques aura été suffisamment connu.
Nous avons fait le choix d’utiliser ResNet-50, qui est un réseau neuronal résiduel à 50 couches de profondeur. Cela est possible grâce au grand nombre de données d’entrée que nous possédons, car ce modèle à couches profondes permet un apprentissage plus riche. C’est donc un réseau à grande profondeur, toutefois nous nous sommes limités à 50 couches pour des raisons de disponibilités de ressources (calcul et mémoire).
##### SE-ResNeXt50
Le réseau SE-ResNeXt est une variation du modèle ResNet dans lequel on ajoute le mécanisme de "SE"(Squeeze-and-Excitation) qui permet d'améliorer ses performances.
Ce mécanisme se divise en deux parties: la compression spatiale (Squeeze) et l'excitation.
La compression spatiale sera l'étape où l'on rassemble les informations spatiales de l'image, sur lesquelles on exécute une opération de réduction afin de réduire la dimension spatiale (à une valeur par canal).
Ensuite, l'excitation sera l'étape où l'on va créer un vecteur d'excitation afin de donner plus d'importance aux canaux qui comportent les informations principales.
L'utilisation de ce modèle peut nous permettre une meilleure sélection des canaux, en choisissant ceux qui présentent les caractéristiques les plus pertinentes. Son adaptabilité à différentes résolutions spatiales peut être également avantageux pour une détection de mélanomes qui nécessite de gérer des caractéristiques à différentes échelles.
##### EfficientNet_b3
En effet, le modèle EfficientNet se distingue des autres modèles d'apprentissage car EfficientNet utilise un processus systématique pour équilibrer la largeur, la profondeur et la résolution des images. Cela permet d'avoir une charge de calcul plus faible qu'un autre modèle.
Le modèle utilisé ici est le modèle EfficientNet_b3, ce qui signifie qu'il a une profondeur de réseau de 3. D'autres modèles existent avec des profondeurs plus élevées qui permettent d'avoir un modèle plus complexe mais nécessitant aussi plus de ressources de calcul.
#### Comparaison des métriques
Afin de comparer les différents modèles utilisés, nous allons utiliser 2 métriques qui peuvent être obtenues avec la commande `torchinfo.summary`. Voici l'interprétation que l'on peut en faire:
- Total params est le nombre total de paramètres (notamment les poids et les biais) entraînables dans le modèle. Plus ce nombre est élevé et plus cela signifie que le modèle est complexe donc est potentiellement meilleur. Cependant, si le modèle est trop complexe, il peut faire du surapprentissage.
- Total mult-adds (M ou G) est le nombre d'opérations de multiplications et d'additions (M=en millions, G=en milliard) nécessaires pour effectuer un forward et un backward pass à travers le réseau de neurones pour une seule image. Plus ce nombre est élevé et plus le modèle nécessite des ressources importantes pour fonctionner.
| Métrique | Resnet-50 | SE-ResNeXt50 |EfficientNet_b3 |
| ------------------- | --------- | ---------- |--------------- |
| Total params | 23 512 130| 14,764,610 | 10 699 306 |
| Total mult-adds | 4.09 G | 2.71 G | 961.20 M |
Le modèle possédant la charge de calcul la plus importante est Resnet-50, suivi de SE-ResNeXt puis de EfficientNet_b3. Il y a une distinction significative de complexité entre EfficientNet_b3 et les deux autres modèles. On a effectivement constaté que le modèle qui poussait le plus au niveau des ressources matérielles était Resnet-50.
#### Comparaison des résultats de performance globale

*Comparaison de la fonction de loss des différents modèles*

*Comparaison de l'accuracy des différents modèles*
En comparant la fonction de perte et l'accuracy, le modèle le plus performant est l'efficientNet_b3, puis le SE-ResNeXt50 puis le ResNet 50. Les modèles sont toujours plus performants avec un jeu de données de 28000 images que le jeu de données de 44000 images.
##### Résultats sur l'entraînement et la validation : Resnet-50
Les courbes suivantes sont les résultats obtenus avec 44000 images en entrée.

Pour les données d'entraînement nous voyons que la perte diminue au fil des epochs indiquant que le modèle s'améliore en minimisant la fonction de perte. Les valeurs de perte à la fin de l'entraînement sont assez basses (0,08) ce qui montre que le modèle a atteint une bonne performance.
Les fluctuations sur les données de validation sont certainement dues au fait que nous appliquons un prétraitement sur les données d'entraînement, ainsi au fait qu'il y ait moins de données de validation que de données d'entraînement. Cependant, on remarque quand même que l'évolution de la perte sur les données de validation suit celle des données d'entraînement.

Pour les données d'entraînement nous voyons que l'exactitude augmente bien au fil des epochs. On constate qu'après la 30ème epoch l'exactitude atteint une valeur de 97%, ce qui montre que le modèle peut généraliser efficacement sur de nouvelles données.
Pour les données de validation, on constate que l'exactitude est plus variable. Cependant, elle ne montre pas de tendance claire à la baisse et montre même des valeurs proches de 97%. Cela indique que le modèle généralise bien aux données qu'il n'a pas vu pendant l'entraînement.
##### Résultats sur l'entraînement et la validation : SE-ResNeXt

*Valeur des fonctions de perte pour le modèle SE-ResNeXt avec 28000 images en entrée*

*Valeur de l'accucacy pour le modèle SE-ResNeXt avec 28000 images en entrée*
Sur la courbe représentant la perte, on observe que l'entraînement présente une courbe qui diminue, allant jusqu'à 0.050 au bout de 15 epochs. Le modèle est donc plutôt performant car il évolue de sorte à minimiser la perte. Les données de validation suivent également une évolution similaire.
La courbe d'accuracy des données d'entraînement présente une augmentation au fil des epochs, pour atteindre une valeur de 98%, ce qui montre une bonne efficacité du modèle. Comme pour les courbes de perte, la courbe d'accuracy des données de validation présente une évolution similaire à l'entraînement.
Cependant, on remarque des fluctuations dans les dernières epochs des deux graphes. Cela pourrait être dû à une variabilité naturelle des données mais également à un début de surapprentissage à partir de la dixième epoch.
##### Résultats sur l'entraînement et la validation : EfficientNet_b3

*Valeur des fonction de perte pour le modèle efficientnet_b3 avec 28000 images en entrée*

*Valeur de l'accuracy pour le modèle efficientnet_b3 avec 28000 images en entrée*
Comme pour les modèles précédent, l'apprentissage foncionne puisque la valeur de la fonction de perte diminue au fur et à mesure des périodes et l'exactitude augmente.
Les données de validation fluctuent plus que les données d'entraînement et donnent de moins bons résultats (surtout pour la fonction de perte). Comme cela a déjà été dit, cela peut être du au fait qu'il y a moins de données de validation que de données d'entraînement.
#### Comparasion des résultats par classe
Afin de de mieux comparer les modèles, nous avons décidé de comparer leur F1-Score qui combine les mesures de précision et rappel, elles-mêmes basées sur les taux de vrais positifs, faux positifs et faux négatifs. La formule du F1-Score est : \begin{equation*}
\text{F1-score} = \frac{2}{\frac{1}{\text{precision}} + \frac{1}{\text{recall}}}
\end{equation*}
soit : \begin{equation*} \text{F1-score} = \frac{truepositives}{truepositives+\frac{1}{2}(falsenegatives+falsepositives) } \end{equation*}
Nous avons tracé 4 graphiques afin de comparer les résultats par classe, entre les différents modèles et entre les différents jeux de donnéees.

*Comparaison des performances entre les classes mélanomes et non-mélanomes et entre les différents modèles (avec un jeu de données de 28000 images)*

*Comparaison des performances entre les classes mélanomes et non-mélanomes et entre les différents modèles (avec un jeu de données de 44000 images)*
Dans ces 2 premiers graphiques, on observe que, quelque soit le modèle utilisé et le jeu de donnée utilisé, les performances de la classe non-mélanomes sont meilleures. Cela veut dire que notre prétraitement et notre entraînement n'ont pas permis de gommer complètement le déséquilibre entre les classes.
Avec le jeu de données de 44000 images, on observe que l'écart entre les performances des classes mélanomes et non-mélanomes est encore plus important qu'avec le jeu de données de 28000 images. Le jeu de données de 28000 images en entrée permet mieux de prendre en compte le déséquilibre des classes.
Cela semble assez cohérent car le jeu de données de 28000 images contient 42% d'images mélanomes contre 25% pour le jeu de données de 44000 images. Une piste d'amélioration pourrait être d'accorder plus de poids à la classe mélanome dans la fonction de coût.
Pour le modèle efficientNet_b3 avec 28000 images, l'écart de performance entre la classe mélanomes et non mélanomes semble se resserer lorsque le nombre d'époques augmente. C'est moins significatif pour les autres modèles.

*Comparaison des performances avec différents modèles et différents jeux de données pour la classe mélanomes*

*Comparaison des performances avec différents modèles et différents jeux de données pour la classe non-mélanomes*
Le premier graphique présente les performances des différents modèles pour la classe mélanomes et le deuxième graphique pour la classe non-mélanomes.
Pour la classe mélanomes, les modèles les plus performants sont l'efficientNet_b3 avec 28000 images en entrée puis le SE-ResNeXt50 avec 28000 images en entrée. Ce sont les 2 traces où la performance reste la plus stable au cours des époques. On constate une plus grande fluctuation de la variable f1 pour les modèles avec 44000 données en entrée.
Pour la classe non-mélanomes, tous les modèles à part le resnet50 (avec 28K) ont des performances plus ou moins équivalentes.
## Résultats des tests
Les codes de test permettent d'éprouver les modèles pré-entraînés en les testant sur des images de test.
Le modèle à tester et les poids de ce modèle sont chargés.
Ensuite, des prédictions sont effectuées sur chacune des images de test en utilisant le modèle à tester.

*Résultats Kaggle*
Les résultats obtenus sur Kaggle mettent en évidence des tendances significatives quant à la performance des différents modèles sur nos données d'entrée. Notamment, ResNet-50 semble être moins adapté à notre jeu de données, affichant des scores inférieurs par rapport aux autres architectures évaluées. En revanche, SE-ResNext50 et EfficientNet_B3 se démarquent, particulièrement avec le jeu de données de taille 28K, en affichant des performances nettement meilleures.
Ces observations soulignent l'importance de choisir judicieusement l'architecture du modèle en fonction de la nature spécifique des données, avec une attention particulière à l'équilibre entre les classes. Avec le jeu de données de 28K où les classes sont plus équilibrées, SE-ResNext50 et EfficientNet_B3 démontrent une capacité accrue à traiter cette distribution, ce qui se traduit par des performances améliorées.
### Apprentissage ensembliste
Après avoir évalué les performances individuelles des modèles de réseaux de neurones tels que SE-ResNext50, EfficientNet_B3 et ResNet50, l'idée d'adopter une approche d'apprentissage ensembliste a émergé. L'ensemble consiste à combiner les prédictions de plusieurs modèles pour améliorer la performance globale. Dans notre contexte, l'utilisation de l'apprentissage ensembliste est motivée par la diversité inhérente à chaque réseau de neurones.
Chaque modèle a ses propres forces et faiblesses, et l'apprentissage ensembliste exploite ces différences pour obtenir une meilleure généralisation. Alors que certains modèles peuvent exceller dans la détection de certaines caractéristiques spécifiques des mélanomes, d'autres peuvent se montrer plus performants pour d'autres aspects. La combinaison de ces compétences complémentaires permet de créer un modèle plus robuste et résilient.
L'apprentissage ensembliste agit comme une sorte de vote pondéré, où chaque modèle exprime son opinion sur la classe d'une image, et ces opinions sont combinées pour produire la prédiction finale. Cela réduit les risques d'erreurs individuelles et améliore la fiabilité globale du système. En fin de compte, cette approche permet de tirer parti des forces collectives des différents modèles, créant ainsi une solution plus holistique et performante pour la détection des mélanomes.

```python
file_path1 = "28K_efficientnet_b3_15ep_64batch.csv"
file_path2 = "28K_resnet50_25e_batch64.csv"
file_path3 = "28K_seresnext50d_15ep_64batch.csv"
file_path4 = "44K_efficientnet_b3_15ep_64batch.csv"
file_path5 = "44K_seresnext50_15ep_64batch.csv"
#file_path6 = "44K_resnet50_30ep_batch64.csv"
df1 = pd.read_csv(file_path1)
df2 = pd.read_csv(file_path2)
df3 = pd.read_csv(file_path3)
df4 = pd.read_csv(file_path4)
df5 = pd.read_csv(file_path5)
#df6 = pd.read_csv(file_path6)
df1['target'] = (2*df1['target'] + df2['target'] + 2*df3['target']+ df4['target']+ df5['target'])/7
```
Les modèles SE-ResNeXt28K et EfficientNet28K, ayant démontré des performances plus élevées, ont été assignés des poids plus importants (x2) dans la moyenne pour souligner l'importance de leurs contributions distinctes. On constate que l'ajout du modèle ResNet44K a un impact négatif sur la moyenne, et par conséquent, il est exclu du calcul de la moyenne. Cependant, retirer ResNet28K abaisse également la moyenne, suggérant que même si ce modèle peut afficher une performance individuelle inférieure, il apporte une diversité d'informations cruciales que les autres modèles ne capturent peut-être pas aussi bien. Cette approche de moyenne pondérée permet de tenir compte des forces et des faiblesses de chaque architecture.
Cependant, il faut noter que la moyenne des différents modèles a permis d'augmenter le score uniquement de 0,0085 points (par rapport au modèle SE-ResNeXt50 avec 28K). C'est toujours ça de gagné, surtout dans un domaine aussi important que la santé. Il faut cependant noter que faire la moyenne de différents modèles consomme plus d'énergie et de ressources que de faire tourner un seul modèle, pour un gain qui n'est pas très élevé. Selon le contexte, ce gain de performance ne vaut pas forcément la peine par rapport à l'énergie consommée.
## Conclusion
Le prétraitement minutieux des données a démontré son rôle essentiel dans la correction des biais d'apprentissage, en particulier dans le contexte d'un déséquilibre significatif entre les classes.
Notre exploration des architectures de réseaux de neurones, notamment SE-ResNeXt, ResNet, et EfficientNet, a souligné les différentes compétences de chaque modèle. L'adoption d'une approche d'apprentissage ensembliste nous a permis de combiner les forces de chaque architecture pour créer une meilleure prédiction.
Ces avancées ne sont pas seulement techniques, mais ouvrent également des perspectives concrètes pour une amélioration des diagnostics médicaux.
En conclusion, ce projet représente une contribution significative à l'intersection de la technologie et de la santé, démontrant le pouvoir de l'intelligence artificielle pour des diagnostics précoces et améliorant ainsi les perspectives de traitement pour les patients concernés.