You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

835 lines
40 KiB

  1. #INIT
  2. from init import *
  3. os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'
  4. os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
  5. st.set_page_config(page_title="Models", page_icon="🦾")
  6. st.write(r"""<style>.stAppDeployButton { display: none; }</style>""", unsafe_allow_html=True) # Hide Deploy button
  7. models = os.listdir("../../models/artifacts/")
  8. mo_db_path = "../../dataset/"
  9. names_csv_path = "../../notebooks/mushroom_observer/dataset-mushroom-observer/names.csv"
  10. @st.cache_data
  11. def get_champi_name(mo_db_path, names_csv_path):
  12. """
  13. Retourne le nom de la classe du champignon depuis le fichier names.csv de Mushroom Observer.
  14. Requiere numpy, pandas et os.
  15. Args:
  16. mo_db_path : Chemin vers le dossier contenant les classes
  17. names_csv_path : Chemin vers le fichier names.csv
  18. Returns:
  19. Dataframe Pandas avec IDs et noms
  20. """
  21. # Imports des sources
  22. data_files = os.listdir(mo_db_path)
  23. names = pd.read_csv(names_csv_path, delimiter='\t', index_col=0)
  24. # Recupération des ID des classes
  25. # champi_classes = []
  26. # for item in data_files:
  27. # champi_classes.append(int(item))
  28. champi_classes = []
  29. for item in data_files:
  30. # Check if the item is a digit (integer)
  31. if item.isdigit():
  32. champi_classes.append(int(item))
  33. else:
  34. print(f"Skipping non-integer file: {item}")
  35. # Creation du DataFrame
  36. df = names[["text_name"]].rename(columns={'text_name': 'name'})
  37. df = df.loc[champi_classes]
  38. # Resultat
  39. return df
  40. @st.cache_data
  41. def get_class_names(data_dir):
  42. dataset = ImageFolder(root=data_dir)
  43. class_names = dataset.classes
  44. return class_names
  45. def pred_name(df, classe):
  46. pred_name = df[df.index == int(classe)]['name'].values[0]
  47. return pred_name
  48. champi = get_champi_name(mo_db_path, names_csv_path)
  49. def view_model_arch(model):
  50. # Capture the model summary
  51. buffer = io.StringIO() # Create a buffer to hold the summary
  52. sys.stdout = buffer # Redirect stdout to the buffer
  53. model.summary() # Call model.summary() to populate the buffer
  54. sys.stdout = sys.__stdout__ # Reset redirect to stdout
  55. # Get the summary from the buffer
  56. model_summary = buffer.getvalue()
  57. with st.expander("Afficher la construction l'architecture CNN", expanded=False):
  58. st.text(model_summary) # Display the model summary inside the expander
  59. class GradCAM:
  60. def __init__(self, model):
  61. self.model = model
  62. self.gradients = None
  63. self.activation_map = None
  64. # Hook the model to get gradients and activation maps
  65. self.model.layer4[1].register_backward_hook(self.backward_hook)
  66. self.model.layer4[1].register_forward_hook(self.forward_hook)
  67. def forward_hook(self, module, input, output):
  68. self.activation_map = output
  69. def backward_hook(self, module, grad_input, grad_output):
  70. self.gradients = grad_output[0]
  71. def generate_cam(self, input_tensor, class_index):
  72. # Forward pass
  73. output = self.model(input_tensor)
  74. # Zero gradients
  75. self.model.zero_grad()
  76. # Backward pass
  77. class_loss = output[0][class_index]
  78. class_loss.backward()
  79. # Get the gradients and activation map
  80. gradients = self.gradients.data.numpy()[0]
  81. activation = self.activation_map.data.numpy()[0]
  82. # Compute weights
  83. weights = np.mean(gradients, axis=(1, 2))
  84. # Generate CAM
  85. cam = np.zeros(activation.shape[1:], dtype=np.float32)
  86. for i, w in enumerate(weights):
  87. cam += w * activation[i, :, :]
  88. # Apply ReLU
  89. cam = np.maximum(cam, 0)
  90. # Normalize the CAM
  91. cam = cam - np.min(cam)
  92. cam = cam / np.max(cam)
  93. return cam
  94. def gradcam_keras(model, img_array, pred_index=None, alpha=0.4):
  95. '''
  96. Function to visualize Grad-CAM heatmaps and display them over the original image.
  97. Parameters:
  98. - model: The trained Keras model.
  99. - img_array: Preprocessed image array for display.
  100. - pred_index: Index of the predicted class (optional). If None, the top predicted class is used.
  101. - alpha: Transparency for heatmap overlay (default is 0.4).
  102. Returns:
  103. - heatmap: The computed heatmap.
  104. '''
  105. # Automatically get the last convolutional layer
  106. last_conv_layer_name = None
  107. for layer in reversed(model.layers):
  108. if 'conv' in layer.name or 'conv' in layer.__class__.__name__.lower():
  109. last_conv_layer_name = layer.name
  110. break
  111. if last_conv_layer_name is None:
  112. raise ValueError("No convolutional layer found in the model.")
  113. # Create a Grad-CAM model
  114. grad_model = tf.keras.models.Model(
  115. model.inputs,
  116. [model.get_layer(last_conv_layer_name).output, model.output]
  117. )
  118. # Compute gradients
  119. with tf.GradientTape() as tape:
  120. conv_outputs, predictions = grad_model(img_array)
  121. if pred_index is None:
  122. pred_index = tf.argmax(predictions[0])
  123. class_channel = predictions[:, pred_index]
  124. # Compute gradients of the top predicted class for the output feature map
  125. grads = tape.gradient(class_channel, conv_outputs)
  126. # Pool the gradients over all the axes leaving only the last one
  127. pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
  128. # Weigh the feature map with the pooled gradients
  129. conv_outputs = conv_outputs[0].numpy()
  130. # Multiply each channel by the corresponding gradient
  131. for i in range(pooled_grads.shape[-1]):
  132. conv_outputs[:, :, i] *= pooled_grads[i]
  133. # Compute the heatmap by averaging over all channels
  134. heatmap = np.mean(conv_outputs, axis=-1)
  135. # ReLU to keep only positive activations and normalize the heatmap
  136. heatmap = np.maximum(heatmap, 0)
  137. heatmap /= np.max(heatmap)
  138. # Resize heatmap to the original image size
  139. heatmap = np.uint8(255 * heatmap)
  140. heatmap = np.clip(heatmap, 0, 255)
  141. # Use shape to get the dimensions
  142. heatmap = Image.fromarray(heatmap).resize((img_array.shape[2], img_array.shape[1]), Image.BILINEAR)
  143. heatmap = np.array(heatmap)
  144. # Create a heatmap using Matplotlib
  145. heatmap = plt.get_cmap('jet')(heatmap / 255.0)[:, :, :3] # Convert to RGB format
  146. heatmap = (heatmap * 255).astype(np.uint8)
  147. # Superimpose the heatmap on the original image
  148. original_image = np.array(img_array[0]) # Convert from batch dimension
  149. superimposed_img = heatmap * alpha + original_image
  150. superimposed_img = np.clip(superimposed_img, 0, 255).astype('uint8')
  151. # Display the result using Streamlit
  152. st.image(superimposed_img, caption='Grad-CAM', use_column_width=True)
  153. return heatmap
  154. def gradcam_pytorch(img, model):
  155. # Preprocess the image
  156. preprocess = transforms.Compose([
  157. transforms.Resize((224, 224)),
  158. transforms.ToTensor(),
  159. transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
  160. ])
  161. img_tensor = preprocess(img).unsqueeze(0) # Add batch dimension
  162. # Initialize Grad-CAM
  163. grad_cam = GradCAM(model)
  164. # Get the predicted class index
  165. with torch.no_grad():
  166. output = model(img_tensor)
  167. predicted_class_index = output.argmax(dim=1).item()
  168. # Generate CAM
  169. cam = grad_cam.generate_cam(img_tensor, predicted_class_index)
  170. # Ensure CAM is between 0 and 1
  171. cam = np.clip(cam, 0, 1)
  172. cam = (cam * 255).astype(np.uint8) # Convert to 0-255 range for visualization
  173. # Create a heatmap using the 'jet' colormap
  174. heatmap = cm.jet(cam) # Apply the jet colormap
  175. heatmap = (heatmap[..., :3] * 255).astype(np.uint8) # Convert to 8-bit RGB
  176. # Convert the CAM to a PIL image
  177. cam_image = Image.fromarray(heatmap) # Create RGB heatmap image
  178. # Resize the heatmap to match the original image size
  179. cam_image = cam_image.resize(img.size, Image.LANCZOS) # Use LANCZOS for high-quality resizing
  180. # Combine the original image with the heatmap
  181. overlay = Image.blend(img.convert("RGB"), cam_image, alpha=0.5) # Overlay the images
  182. return overlay
  183. # SIDEBAR
  184. st.sidebar.title("Modèle séléctionné")
  185. model_selection = st.sidebar.selectbox("Sélectionnez un modèle :", options=models)
  186. st.sidebar.divider()
  187. st.sidebar.write('Liste des ', len(champi), ' classes du Dataset')
  188. st.sidebar.dataframe(champi, height=850)
  189. # BODY
  190. colored_header(
  191. label=model_selection.upper(),
  192. description="Démonstration des modèles entrainés",
  193. color_name="red-70",
  194. )
  195. # MODELS
  196. if model_selection == 'heuzef_lenet_001.keras':
  197. # Charger le modèle
  198. model = tf.keras.models.load_model("../../models/artifacts/"+model_selection)
  199. class_names = ['1174', '15162', '1540', '2749', '29997', '330', '344', '362', '373', '382', '39842', '42', '50164', '63454']
  200. # Presentation
  201. st.subheader("Présentation d'un modèle avec une architecture Lenet")
  202. st.caption("Auteur : [Heuzef](https://heuzef.com) - 2024")
  203. st.markdown("""
  204. Nous avons débuté nos expérimentations par un premier modèle avec une architecture LeNet (Y. LeCun et al., 1998) sans attente particulière de performance.
  205. *(L'entraînement n'ayant pas été effectué sur des données préparées correctement).*
  206. En effet, nous procédons d'abord par l'augmentation des données puis la division, *(le jeu de validation contient alors des images trop proches de l'entraînement, car simplement modifié par l'augmentation des données).*
  207. Ainsi, nous obtenons un score d'exactitude de 95%, mais cela ne reflète absolument pas la réalité de la prédiction.
  208. Ce modèle s'avère donc médiocre et sera rapidement abandonné au profit des algorithmes de transfert learning pour leurs efficacités.
  209. """)
  210. view_model_arch(model)
  211. # Metriques
  212. st.divider()
  213. st.subheader("Métriques")
  214. st.image("../img/lenet_001.png")
  215. # Prediction
  216. st.divider()
  217. st.subheader("Test de prédiction")
  218. st.warning("""Attention, ce modèle n'est pas entrainé sur toutes les classes du dataset.
  219. Les espèces disponible pour ce modèle sont les suivantes : """+str(class_names))
  220. url = st.text_input("Indiquez l'URL d'une photo pour exécuter le modèle. L'espèce doit appartenir à l'une des classes entrainés.", "https://www.mycodb.fr/photos/Amanita_muscaria_2005_ov_2.jpg")
  221. if url is not None:
  222. st.markdown("""
  223. # 🦾 Exécution !
  224. """)
  225. def champi_lenet_predict(url):
  226. champi_path = tf.keras.utils.get_file(origin=url)
  227. img = tf.keras.utils.load_img(champi_path, target_size=(224, 224))
  228. img_array = tf.keras.utils.img_to_array(img)
  229. img_array = tf.expand_dims(img_array, 0)
  230. predictions = model.predict(img_array)
  231. score = predictions[0]
  232. return int(class_names[np.argmax(score)])
  233. # Faire une prédiction
  234. prediction = champi_lenet_predict(url)
  235. # Afficher la prédiction
  236. st.info("Résultat de la prédiction : \n\n"+"🔎 ID : "+str(prediction)+" \n\n 🍄 NAME : "+pred_name(champi, prediction).upper())
  237. st.link_button("🔗 Consulter sur Wikipédia", "https://fr.wikipedia.org/w/index.php?search="+pred_name(champi, prediction))
  238. st.image(url)
  239. elif model_selection == 'florent_resnet18.pth':
  240. def load_model(model_path): # Charger le modèle pré-entraîné
  241. model = torchvision.models.resnet18(pretrained=True)
  242. model.fc = nn.Linear(model.fc.in_features, 23)
  243. model.load_state_dict(torch.load(model_path, map_location=torch.device('cpu')))
  244. model.eval()
  245. return model
  246. # Charger le modèle
  247. model = load_model('../../models/artifacts/'+model_selection)
  248. # Presentation
  249. st.subheader("Présentation du modèle pré-entrainé en Transfert learning avec Resnet18")
  250. st.caption("Auteur : Florent Constant - 2024")
  251. st.markdown("""
  252. Ce modèle à donné un résultat encourageant avec un score d'accuracy sur le jeu de données de validation, suppérieur à 97%.
  253. Ce dernier à été entrainé en transfert learning depuis le modèle pré-entrainé **ResNet18**, mis en oeuvre a l'aide de Pytorch.
  254. Outre les bons résultats obtenus, différentes expérimentations ont été réalisées afin d'évaluer la relation entre quantité de données et performance resultante du modèle.
  255. Une première série d'entrainement a permis d'évaluer l'impact de la data augmentation sur les résultat du modèle :
  256. - En faisant de l'oversampling pour pour équilibrer le volume de données disponible pour chaque classe (260 images par classe), le score d'accuracy obtenu est de 96.870925684485%.
  257. - En faisant de l'augmentation pour atteindre un volume de 500 images par classes, le score d'accuracy obtenu est de 97.0013037809648%.
  258. Ces résultats plutôt contre-intuitifs montrent que das notre cas d'usage la data-augmentation n'a pas permis d'améliorer les résultats du modèle.
  259. Une seconde serie de tests a été réalisée afin d'évaluer le volume de données nécéssaires pour obtenir des résultats satisfaisant.
  260. Le volume à été limité à 80, 70, 60, 50, 40, 30, 20 puis 10 images pour chacune des classes. Les résultats se sont montrés surprenant avec :
  261. - un score équivalent au meilleur score obtenu sur la totalité du dataset, avec seulement 80 images par classes.
  262. - un score encore au dessus de 90% avec seulement 30 images par classes.
  263. - un score encore au dessus de 80% avec seulement 10 images par classes.
  264. """)
  265. st.image("../img/resnet18_01.png")
  266. st.image("../img/resnet18_02.png")
  267. # Metriques
  268. st.divider()
  269. st.subheader("Métriques")
  270. col1, col2, col3 = st.columns(3)
  271. col1.metric(label="Validation - Exactitude", value=0.970)
  272. col2.metric(label="Validation - Précision", value=0.966)
  273. col3.metric(label="Validation - F1 score", value=0.965)
  274. style_metric_cards()
  275. st.image("../img/conf_matrix_resnet18.png", caption="La matrice de confusion ne fait pas apparaitre de problèmes de confusion entre deux classes particulières")
  276. # Prediction
  277. st.divider()
  278. st.subheader("Test de prédiction")
  279. uploaded_file = st.file_uploader("Choisissez une photo pour exécuter le modèle. L'espèce doit appartenir à l'une des classes entrainés.", type=["jpg", "jpeg"])
  280. if uploaded_file is not None:
  281. st.markdown("""
  282. # 🦾 Exécution !
  283. """)
  284. def preprocess_image(image): # Prétraiter l'image
  285. transform = transforms.Compose([
  286. transforms.Resize((224, 224)),
  287. transforms.ToTensor(),
  288. transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
  289. ])
  290. return transform(image).unsqueeze(0)
  291. def predict(model, image, class_names): # Faire une prédiction
  292. image_tensor = preprocess_image(image)
  293. with torch.no_grad():
  294. output = model(image_tensor)
  295. _, predicted = torch.max(output, 1)
  296. class_index = predicted.item()
  297. class_name = class_names[class_index]
  298. return class_name
  299. image = Image.open(uploaded_file)
  300. largeur, hauteur = image.size
  301. if(largeur > hauteur):
  302. margin = (largeur - hauteur) / 2
  303. zone_recadrage = (margin, 0, hauteur+margin, hauteur)
  304. else:
  305. margin = (hauteur - largeur) / 2
  306. zone_recadrage = (0, margin, largeur, largeur+margin)
  307. image = image.crop(zone_recadrage)
  308. # Lire les noms de classes à partir de la structure du répertoire
  309. class_names = get_class_names(mo_db_path)
  310. # Faire une prédiction
  311. prediction = predict(model, image, class_names)
  312. # Afficher la prédiction
  313. col1, col2 = st.columns(2)
  314. with col1:
  315. st.image(image, caption="Image téléchargée")
  316. with col2:
  317. imgrgb = Image.open(uploaded_file).convert("RGB")
  318. heatmap = gradcam_pytorch(imgrgb, model)
  319. st.image(heatmap, caption='Grad-CAM')
  320. st.info("Résultat de la prédiction : \n\n"+"🔎 ID : "+str(prediction)+" \n\n 🍄 NAME : "+pred_name(champi, prediction).upper())
  321. st.link_button("🔗 Consulter sur Wikipédia", "https://fr.wikipedia.org/w/index.php?search="+pred_name(champi, prediction))
  322. elif model_selection == 'heuzef_efficientnetb1_010.keras':
  323. # Charger le modèle
  324. model = tf.keras.models.load_model("../../models/artifacts/"+model_selection)
  325. class_names = ['1174', '15162', '1540', '267', '271', '330', '344', '362', '373', '382', '401', '407', '42', '4920', '53', '939']
  326. # Presentation
  327. st.subheader("Présentation du modèle pré-entrainé en Transfert learning avec EfficientNETB1")
  328. st.caption("Auteur : [Heuzef](https://heuzef.com) - 2024")
  329. st.markdown("""
  330. Le transfert learning avec le modèle EfficientNetB1 a permis d'obtenir des résultats satisfaisants malgré les limitations matérielles.
  331. En optimisant l'utilisation des ressources, le modèle a pu maintenir, avec un simple CPU AMD Ryzen 7 2700X, de bonnes performances.
  332. L'utilisation d'un modèle pré-entraîné sur ImageNet fourni une base solide pour la classification. L'entrainement ici est effectué sur **160000 photos, pour 16 classes**.
  333. Avec optimisation et utilisation de callbacks, le modèle généralise correctement jusqu'à la quatrième epoch avant de subir un overfitting.
  334. **Les performances du modèle ont montré une précision d'entraînement remarquable à 96% et une précision de validation de 86%.**
  335. Bien que les résultats soient encourageants, ces conclusions ouvrent la voie à des pistes d'amélioration, telles que l'optimisation des hyperparamètres pour affiner les scores de précision sur le jeu d'évaluation ainsi qu'une meilleure gestion des données pour minimiser le risque de sur-apprentissage.
  336. """)
  337. with st.expander("Afficher la segmentation du Dataset après ré-échantillonnage"):
  338. col1, col2 = st.columns(2)
  339. col1.image("../img/efficientnetb1_dataset.png")
  340. col2.code("""
  341. Jeu d'entrainement :
  342. Found 112000 files
  343. Jeu de validation :
  344. Found 48000 files
  345. Jeu de test :
  346. Found 1360 files
  347. 16 Classes :
  348. ['1174',
  349. '15162',
  350. '1540',
  351. '267',
  352. '271',
  353. '330',
  354. '344',
  355. '362',
  356. '373',
  357. '382',
  358. '401',
  359. '407',
  360. '42',
  361. '4920',
  362. '53',
  363. '939']
  364. batch_size = 32
  365. """)
  366. view_model_arch(model)
  367. # Metriques
  368. st.divider()
  369. st.subheader("Métriques")
  370. col1, col2, col3, col4 = st.columns(4)
  371. col1.metric(label="Test - Exactitude", value=0.928)
  372. col2.metric(label="Test - Precision", value=0.933)
  373. col3.metric(label="Test - Recall", value=0.928)
  374. col4.metric(label="Test - F1-score", value=0.929)
  375. style_metric_cards()
  376. st.image("../img/efficientnetb1_metrics.png")
  377. st.image("../img/efficientnetb1_matrix_02.png")
  378. st.image("../img/efficientnetb1_predictions.png", caption="Exemples de predictions sur les 16 classes")
  379. # Prediction
  380. st.divider()
  381. st.subheader("Test de prédiction")
  382. st.warning("""Attention, ce modèle n'est pas entrainé sur toutes les classes du dataset.
  383. Les espèces disponible pour ce modèle sont les suivantes : """+str(class_names))
  384. url = st.text_input("Indiquez l'URL d'une photo pour exécuter le modèle. L'espèce doit appartenir à l'une des classes entrainés.", "https://upload.wikimedia.org/wikipedia/commons/c/cd/Mycena_haematopus_64089.jpg")
  385. if url is not None:
  386. st.markdown("""
  387. # 🦾 Exécution !
  388. """)
  389. def champi_effnetb1_predict(url):
  390. champi_path = tf.keras.utils.get_file(origin=url)
  391. img = tf.keras.utils.load_img(champi_path, target_size=(224, 224))
  392. img_array = tf.keras.utils.img_to_array(img)
  393. img_array = tf.expand_dims(img_array, 0)
  394. predictions = model.predict(img_array)
  395. score = predictions[0]
  396. return int(class_names[np.argmax(score)])
  397. def champi_effnetb1_gradcam(url):
  398. champi_path = tf.keras.utils.get_file(origin=url)
  399. img = tf.keras.utils.load_img(champi_path, target_size=(224, 224))
  400. img_array = tf.keras.utils.img_to_array(img)
  401. img_array = tf.expand_dims(img_array, 0)
  402. predictions = model.predict(img_array)
  403. score = predictions[0]
  404. heatmap = gradcam_keras(model, img_array, pred_index=np.argmax(score), alpha=0.4)
  405. return heatmap
  406. # Faire une prédiction
  407. prediction = champi_effnetb1_predict(url)
  408. # Afficher la prédiction
  409. col1, col2 = st.columns(2)
  410. with col1:
  411. st.image(url, caption='Image téléchargée', use_column_width=True)
  412. with col2:
  413. champi_effnetb1_gradcam(url)
  414. st.info("Résultat de la prédiction : \n\n"+"🔎 ID : "+str(prediction)+" \n\n 🍄 NAME : "+pred_name(champi, prediction).upper())
  415. st.link_button("🔗 Consulter sur Wikipédia", "https://fr.wikipedia.org/w/index.php?search="+pred_name(champi, prediction))
  416. elif model_selection == 'vik_resnet50.h5':
  417. # Charger le modèle
  418. model = tf.keras.models.load_model("../../models/artifacts/"+model_selection)
  419. class_names = ['42', '53', '267', '271','330', '344', '362', '373', '382', '401', '407', '939', '1174', '1540','4920', '15162']
  420. index_to_label ={0: '1174', 1: '15162', 2: '1540', 3: '267', 4: '271', 5: '330', 6: '344', 7: '362', 8: '373', 9: '382', 10: '401', 11: '407', 12: '42', 13: '4920', 14: '53', 15: '939'}
  421. index_mapping = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 7: 5, 8: 6, 9: 7, 10: 8, 11: 9, 13: 10, 14: 11, 15: 12, 17: 13, 19: 14, 22: 15}
  422. # Presentation
  423. st.subheader("Présentation du modèle ResNet50")
  424. st.caption("Auteur : Viktoriia Saveleva - 2024")
  425. st.markdown("""
  426. Le modèle est pré-entraîné sur **ImageNet** (transfer learning). """)
  427. st.markdown("### Architecture du Modèle")
  428. with st.expander("Modèle de base", expanded=False):
  429. st.markdown("""
  430. 1. **Blocs Convolutionnels** : Les couches initiales se composent de couches convolutionnelles qui extraient des caractéristiques de l'image d'entrée en utilisant de petits champs récepteurs.
  431. 2. **Blocs Résiduels** : L'innovation majeure de ResNet repose sur l'intégration de blocs résiduels, qui introduisent des connexions de contournement. Ces connexions facilitent le passage des gradients durant l'entraînement, permettant ainsi de former des réseaux de neurones très profonds tout en atténuant le problème de la vanishing gradient.
  432. 3. **Normalisation par Lot / Batch Normalization** : Intégrée après chaque couche convolutionnelle, la normalisation par lot normalise la sortie, accélérant l'entraînement et améliorant la convergence.
  433. 4. **Fonctions d'Activation** : La fonction d'activation ReLU (Rectified Linear Unit) est appliquée après chaque convolution et normalisation par lot pour introduire de la non-linéarité.
  434. 5. **Couches de Pooling** : Les couches de pooling (comme le max pooling) sont utilisées pour réduire les dimensions spatiales des cartes de caractéristiques.
  435. 6. **Couches Entièrement Connectées** : À la fin du réseau, les couches entièrement connectées prennent les décisions de classification finales en fonction des caractéristiques apprises.
  436. """)
  437. st.info("**Problème :** surapprentissage\n\n" +
  438. f"**Précision de validation :** 0.84")
  439. with st.expander("Modèle personnalisé : Nouvelles Couches", expanded=False):
  440. st.markdown("""
  441. 7. **Dropouts** : Des couches de dropout (taux de 0.7) sont appliquées après les couches entièrement connectées pour réduire le surapprentissage (overfitting) en désactivant aléatoirement des neurones pendant l'entraînement.
  442. 8. **Régularisation** : La régularisation L2 est appliquée sur les couches denses pour pénaliser les poids trop grands, ce qui aide également à prévenir le surapprentissage.
  443. 9. **Callbacks** : Des rappels (callbacks) sont utilisés pour améliorer le processus d'entraînement :
  444. - **Early Stopping** : Arrête l'entraînement si la perte de validation ne s'améliore pas pendant un certain nombre d'époques, permettant d'éviter le surapprentissage.
  445. - **Reduce Learning Rate on Plateau** : Réduit le taux d'apprentissage lorsque la perte de validation atteint un plateau, ce qui permet un ajustement plus fin des poids.
  446. """)
  447. st.info("**Problème :** surapprentissage\n\n" +
  448. f"**Précision de validation :** 0.80")
  449. with st.expander("Modèle personnalisé : Congélation des Couches", expanded=False):
  450. st.markdown("""
  451. 10. **Congélation des Couches** : Différentes configurations de congélation des couches ont été testées pour évaluer leur impact sur la performance du modèle :
  452. - **Congélation Complète**
  453. - **Congélation de 5 Couches** : Seules les 5 premières couches sont gelées, permettant aux couches supérieures de s'adapter davantage aux données.
  454. - **Congélation de 10 Couches** : Une approche intermédiaire, où 10 couches sont gelées.
  455. - **Congélation de 15 Couches** : Permet un apprentissage plus approfondi en libérant certaines couches supérieures.
  456. - **Aucune Congélation** : Toutes les couches sont entraînables, offrant la flexibilité maximale pour apprendre des caractéristiques pertinentes. Modèle final.
  457. """)
  458. st.info("**Problème :** surapprentissage\n\n" +
  459. f"**Précision de validation :** 0.84")
  460. with st.expander("Modèle personnalisé : 10 vs 16 classes", expanded=False):
  461. st.markdown("""
  462. 11. **Augmentation des Données (Classes)** : En augmentant de 10 à 16 classes, le modèle a moins sur-appris. Cela s'explique par une meilleure représentation des données, réduisant ainsi le risque de mémorisation excessive et améliorant sa capacité de généralisation.
  463. """)
  464. st.info("**Problème :** surapprentissage --> est moins prononcé\n\n" +
  465. f"**Précision de validation :** 0.93")
  466. st.markdown("""
  467. **Conclusions** :
  468. Il n'y a pas d'effet significatif du nombre de couches sur les performances du modèle.
  469. Cependant, augmenter le nombre de classes dans l'entraînement a amélioré l'exactitude du modèle ainsi que d'autres scores.
  470. """)
  471. view_model_arch(model)
  472. # Metriques
  473. st.divider()
  474. st.subheader("Métriques")
  475. col1, col2, col3, col4 = st.columns(4)
  476. col1.metric(label="Test - Accurcay", value=0.9404)
  477. col2.metric(label="Test - Precision", value=0.9446)
  478. col3.metric(label="Test - Recall", value=0.9404)
  479. col4.metric(label="Test - F1-score", value=0.9407)
  480. style_metric_cards()
  481. st.image("../img/resnet50_model_last.png")
  482. st.image("../img/resnet50_cm.png")
  483. st.markdown("""
  484. **Conclusions** :
  485. D'après la matrice de confusion, certains champignons sont encore mal reconnus.
  486. Prochaines étapes : Entraîner le modèle sur des images des champignons en noir et blanc.
  487. """)
  488. # Prediction
  489. st.divider()
  490. st.subheader("Test de prédiction")
  491. st.warning("""Attention, ce modèle n'est pas entrainé sur toutes les classes du dataset.
  492. Les espèces disponible pour ce modèle sont les suivantes : """+str(class_names))
  493. def predict_image_resnet50(uploaded_file):
  494. # Load and preprocess the image
  495. img = Image.open(uploaded_file).convert("RGB") # Ensure it's in RGB format
  496. img = img.resize((224, 224)) # Resize the image
  497. img_array = tf.keras.preprocessing.image.img_to_array(img) # Convert to array
  498. img_array = np.expand_dims(img_array, axis=0) # Add batch dimension
  499. img_array = tf.keras.applications.resnet50.preprocess_input(img_array) # Preprocess for ResNet50
  500. # Make prediction
  501. predictions = model.predict(img_array)
  502. # Get predicted class
  503. predicted_index = np.argmax(predictions, axis=-1)[0] # Get the predicted class index
  504. predicted_index_2 = index_mapping[predicted_index]
  505. predicted_class_label = index_to_label[predicted_index_2]
  506. return img, predicted_class_label, predicted_index_2
  507. # Upload Image
  508. uploaded_file = st.file_uploader("Télécharger une image", type=["jpg", "jpeg", "png"])
  509. if uploaded_file is not None:
  510. # Load and preprocess the image
  511. img = Image.open(uploaded_file).convert("RGB")
  512. img = img.resize((224, 224))
  513. img_array = tf.keras.preprocessing.image.img_to_array(img)
  514. img_array = np.expand_dims(img_array, axis=0)
  515. img_array = tf.keras.applications.resnet50.preprocess_input(img_array) # Use ResNet preprocessing
  516. img, predicted_class_label, predicted_index = predict_image_resnet50(uploaded_file)
  517. predicted_mushroom_name = pred_name(champi, predicted_class_label)
  518. col1, col2 = st.columns(2)
  519. with col1:
  520. st.image(img, caption='Image téléchargée', use_column_width=True)
  521. with col2:
  522. heatmap = gradcam_keras(model, img_array, pred_index=predicted_index, alpha=0.4)
  523. st.info("Résultat de la prédiction : \n\n" +
  524. "🔎 ID : " + str(predicted_class_label) +
  525. #f"🔎 Index : {predicted_index} \n\n" +
  526. "\n\n 🍄 NAME : " + predicted_mushroom_name.upper())
  527. #f"\n\n 🍄 NAME : {predicted_class_label.upper()}")
  528. st.link_button("🔗 Consulter sur Wikipédia", "https://fr.wikipedia.org/w/index.php?search="+predicted_class_label)
  529. elif model_selection == 'yvan_jarvispore.h5':
  530. # Charger le modèle
  531. model = tf.keras.models.load_model("../../models/artifacts/"+model_selection)
  532. # Lire les noms de classes à partir de la structure du répertoire
  533. class_names = ['01_Agaricus augustus', '02_Amanita augusta', '03_Amanita bisporigera', '04_Amanita muscaria', '05_Amanita velosa', '06_Baorangia bicolor', '07_Bolbitius titubans', '08_Boletinellus merulioides', '09_Boletus edulis', '10_Boletus rex-veris', '11_Cantharellus cinnabarinus', '12_Ceratiomyxa fruticulosa', '13_Craterellus fallax', '14_Flammulina velutipes', '15_Fomitopsis mounceae', '16_Fuligo septica', '17_Ganoderma oregonense', '18_Lactarius indigo', '19_Morchella importuna', '20_Mycena haematopus', '21_Pluteus petasatus', '22_Stropharia ambigua', '23_Trametes versicolor']
  534. #index_to_label = [0: '01_Agaricus augustus', 1: '02_Amanita augusta', 2: '03_Amanita bisporigera', '04_Amanita muscaria', '05_Amanita velosa', '06_Baorangia bicolor', '07_Bolbitius titubans', '08_Boletinellus merulioides', '09_Boletus edulis', '10_Boletus rex-veris', '11_Cantharellus cinnabarinus', '12_Ceratiomyxa fruticulosa', '13_Craterellus fallax', '14_Flammulina velutipes', '15_Fomitopsis mounceae', '16_Fuligo septica', '17_Ganoderma oregonense', '18_Lactarius indigo', '19_Morchella importuna', '20_Mycena haematopus', '21_Pluteus petasatus', '22_Stropharia ambigua', '23_Trametes versicolor']
  535. # Presentation
  536. st.subheader("Présentation du modèle CNN JarviSpore")
  537. st.caption("Auteur : Yvan Rolland - 2024")
  538. st.image("../img/jarvispore.png")
  539. st.write(f"""
  540. {mention(
  541. label="Modèle disponible sur HuggingFace",
  542. icon="🤗",
  543. url="https://huggingface.co/YvanRLD/JarviSpore",
  544. write=False
  545. )}
  546. """, unsafe_allow_html=True)
  547. st.markdown("""
  548. Suite aux résultats offert par le transfert Learning, nous avons pris l'initiative de créer un modèle de zéro.
  549. Ce modèle effectue l'entraînement, l'évaluation et l'interprétation d'un modèle de réseau de neurones convolutif (CNN) pour une tâche de classification d'images.
  550. Les résultats attendues sont :
  551. * Précision du Modèle : La métrique mesurée est la précision, elle permet de mesurer le pourcentage de classifications correctes effectuées.
  552. * Interprétabilité avec Grad-CAM : Les heatmaps générées par Grad-CAM doivent indiquer les parties pertinentes de l'image, ce qui aide à comprendre le fonctionnement du modèle.
  553. * Généralisation : Avec l'utilisation des callbacks et des pondérations de classe, le modèle doit éviter le sur-apprentissage et bien généraliser sur les données de validation et de test.
  554. """)
  555. with st.expander("Afficher les différentes étapes et le processus utilisés"):
  556. st.markdown("""
  557. 1. Importation des Bibliothèques
  558. Nous commençons par importer les bibliothèques nécessaires pour la manipulation des données, l'entraînement du modèle, l'évaluation et la visualisation des résultats. Les bibliothèques incluent TensorFlow pour la construction du modèle, NumPy pour les calculs numériques, Pandas pour la gestion des données et OpenCV pour le traitement des images.
  559. 2. Extraction des Versions des Bibliothèques
  560. Nous vérifions les versions des bibliothèques utilisées afin d'assurer la compatibilité des versions.
  561. 3. Chargement des Datasets (structurées et non structurées)
  562. Nous définissons les chemins pour les datasets d'entraînement, de validation et de test. Nous utilisons la fonction image_dataset_from_directory pour charger les images en les redimensionnant à la taille (224, 224) avec un batch size de 32 images. Les ensembles de données sont ensuite configurés pour être mis en cache en mémoire vive, préchargés et optimisés.
  563. 4. Chargement des Classes
  564. Nous chargeons les noms des classes à partir d'un fichier CSV (API MushroomObserver) pour obtenir la liste des classes disponibles. Cela permet au modèle d'associer les indices des classes avec les noms réels lors de l'affichage des résultats.
  565. 5. Construction du Modèle Convolutionnel
  566. Nous construisons un CNN personnalisé avec plusieurs couches de convolution suivies de la normalisation par lots (Batch Normalization), du sous-échantillonnage (MaxPooling) et d'une couche de sortie utilisant softmax pour la classification des 23 classes. Les couches de convolution permettent d'extraire les caractéristiques des images, tandis que les couches denses à la fin effectuent la classification.
  567. 6. Compilation du Modèle
  568. Le modèle est compilé avec l'optimiseur Adam et la fonction de perte sparse_categorical_crossentropy, adaptée à la classification multi-classes avec des étiquettes sous forme d'entiers.
  569. 7. Ajout de l'Early Stopping et du Model Checkpoint
  570. Nous configurons des callbacks pour arrêter l'entraînement si la précision de validation n'augmente plus après 5 époques (early stopping) et pour sauvegarder le meilleur modèle lors de l'entraînement (ModelCheckpoint).
  571. 8.Gestion du Déséquilibre des Classes
  572. Nous vérifions le déséquilibre des classes dans l'ensemble d'entraînement. Si certaines classes sont moins représentées, nous utilisons des pondérations de classe (class_weight) pour accorder plus d'importance aux classes sous-représentées afin d'améliorer la généralisation du modèle.
  573. 9. Entraînement du Modèle
  574. Le modèle est entraîné sur 20 époques, en utilisant les pondérations de classe pour mieux gérer les déséquilibres. Les callbacks configurés permettent de surveiller la performance et de sauvegarder le meilleur modèle.
  575. 10. Génération de la Matrice de Confusion
  576. Après l'entraînement, nous générons une matrice de confusion sur l'ensemble de validation pour évaluer la capacité du modèle à classifier correctement les images. La matrice de confusion est affichée avec les noms des classes pour faciliter l'interprétation des résultats.
  577. 11. Visualisation des Courbes d'Entraînement
  578. Nous affichons les courbes de précision et de perte pour les ensembles d'entraînement et de validation, ce qui nous permet de visualiser l'évolution des performances du modèle pendant l'entraînement.
  579. 12. Sauvegarde du Modèle et Métadonnées
  580. Nous sauvegardons le modèle entraîné au format .keras ainsi que les métadonnées (date d'entraînement, précision sur l'ensemble de test, nombre d'époques). Cela permet de documenter le modèle pour un suivi ultérieur.
  581. 13. Test et Évaluation du Modèle sur l'Ensemble de Test
  582. Nous testons le modèle sur le jeu de données de test pour obtenir la précision finale et évaluer sa performance générale.
  583. 14. Affichage Grad-CAM
  584. Nous implémentons Grad-CAM pour visualiser les activations des couches de convolution du modèle. Cette technique permet d'afficher les régions de l'image qui ont le plus contribué à la décision du modèle. Les résultats sont affichés pour cinq images aléatoires du jeu de test.
  585. ---
  586. Ces étapes permettent de construire un modèle performant pour la classification d'images, tout en prenant en compte les déséquilibres de classe et en offrant des outils d'interprétation des résultats.
  587. Ce modèle est entrainé grâce à une machine équipée d'une carte graphique RTX 3090 (24 Go de VRAM), de 192 Go de RAM et un CPU Intel i9 14900k.
  588. Pour assurer la compatibilité des bibliothèques utilisées et de leurs versions, nous avons déployé un environnement sous Microsoft Windows en utilisant d'anciennes versions de TensorFlow, Cuda, ...
  589. """)
  590. st.code("""
  591. numpy : 1.26.4
  592. tensorflow : 2.10.0
  593. matplotlib : 3.9.2
  594. scikit-learn : 1.5.2
  595. PIL : 10.4.0
  596. cv2 : 4.10.0
  597. pandas : 2.2.3
  598. """)
  599. view_model_arch(model)
  600. # Metriques
  601. st.divider()
  602. st.subheader("Métriques")
  603. col1, col2, col3, col4 = st.columns(4)
  604. col1.metric(label="Entraînement - Exactitude", value=0.977)
  605. col2.metric(label="Entraînement - Perte", value=0.071)
  606. col3.metric(label="Validation - Exactitude", value=0.945)
  607. col4.metric(label="Validation - Perte", value=0.234)
  608. style_metric_cards()
  609. st.image("../img/jarvispore_001.png")
  610. st.image("../img/jarvispore_002.png")
  611. # Prediction
  612. st.divider()
  613. st.subheader("Test de prédiction")
  614. uploaded_file = st.file_uploader("Choisissez une photo pour exécuter le modèle. L'espèce doit appartenir à l'une des classes entrainés.", type=["jpg", "jpeg", "png"])
  615. if uploaded_file is not None:
  616. st.markdown("""
  617. # 🦾 Exécution !
  618. """)
  619. def jarvispore_predict(uploaded_file):
  620. img = tf.keras.utils.load_img(uploaded_file, target_size=(224, 224))
  621. img_array = tf.keras.utils.img_to_array(img)
  622. img_array = tf.expand_dims(img_array, 0)
  623. predictions = model.predict(img_array)
  624. score = predictions[0]
  625. return class_names[np.argmax(score)]
  626. # def jarvispore_gradcam(uploaded_file):
  627. # img = tf.keras.utils.load_img(uploaded_file, target_size=(224, 224))
  628. # img_array = tf.keras.utils.img_to_array(img)
  629. # img_array = tf.expand_dims(img_array, 0)
  630. # predictions = model.predict(img_array)
  631. # score = predictions[0]
  632. # heatmap = gradcam_keras(model, img_array, pred_index=np.argmax(score), alpha=0.4)
  633. # return heatmap
  634. # Faire une prédiction
  635. prediction = jarvispore_predict(uploaded_file)
  636. # Afficher la prédiction
  637. st.image(uploaded_file, caption='Image téléchargée', use_column_width=True)
  638. st.info("Résultat de la prédiction : \n\n"+"🔎 ID : "+str(prediction))