From b44469bb60ec94c9eccee4b5a9e593e992c5ecc7 Mon Sep 17 00:00:00 2001 From: Pierre Baud Date: Mon, 16 Feb 2026 19:59:53 +0100 Subject: [PATCH] [ADD]: Symetric BCE + binary class --- prototype_v1/classifier.py | 146 +++++++---- .../explication_bitstring_comparaison.txt | 54 ++++ prototype_v1/model.py | 56 +++-- prototype_v1/test_model.py | 232 ++++++++++++------ 4 files changed, 338 insertions(+), 150 deletions(-) create mode 100644 prototype_v1/explication_bitstring_comparaison.txt diff --git a/prototype_v1/classifier.py b/prototype_v1/classifier.py index 4b5d938..e316f48 100644 --- a/prototype_v1/classifier.py +++ b/prototype_v1/classifier.py @@ -1,10 +1,11 @@ """ -Classifier avec Cross-Entropy +Classifier Binaire avec Symmetric BCE Loss -Transforme les embeddings contextualisés en prédictions par nœud. +Transforme les embeddings contextualisés en prédictions binaires par nœud. +La Symmetric BCE Loss gère automatiquement la symétrie des solutions (MaxCut). Input: [batch, n_nodes, hidden_dim] -Output: [batch, n_nodes, num_classes] (logits) +Output: [batch, n_nodes] (probabilités entre 0 et 1) """ import torch @@ -14,41 +15,38 @@ class Classifier(nn.Module): """ - Classifier multi-classe pour les problèmes d'optimisation. + Classifier binaire pour les problèmes d'optimisation sur graphes. - Supporte: - - MaxCut, Vertex Cover, Independent Set: 2 classes - - Graph Coloring: k classes + Supporte: MaxCut, Vertex Cover, Independent Set (tous binaires). + + Loss: Symmetric BCE + loss = min(BCE(pred, target), BCE(pred, 1-target)) + → Gère automatiquement la symétrie des solutions """ - def __init__(self, hidden_dim=256, max_classes=10, dropout=0.1): + def __init__(self, hidden_dim=256, dropout=0.1): super().__init__() - self.hidden_dim = hidden_dim - self.max_classes = max_classes - self.layers = nn.Sequential( nn.LayerNorm(hidden_dim), nn.Linear(hidden_dim, hidden_dim // 2), nn.GELU(), nn.Dropout(dropout), - nn.Linear(hidden_dim // 2, max_classes) + nn.Linear(hidden_dim // 2, 1) # 1 seule sortie → binaire ) - def forward(self, x, num_classes=2): + def forward(self, x): """ Args: x: [batch, n_nodes, hidden_dim] - embeddings contextualisés - num_classes: int - nombre de classes Returns: - logits: [batch, n_nodes, num_classes] - probs: [batch, n_nodes, num_classes] - predictions: [batch, n_nodes] + probs: [batch, n_nodes] - probabilités entre 0 et 1 + predictions: [batch, n_nodes] - 0 ou 1 """ - logits = self.layers(x)[:, :, :num_classes] - probs = F.softmax(logits, dim=-1) - predictions = torch.argmax(logits, dim=-1) + logits = self.layers(x).squeeze(-1) # [batch, n_nodes] + probs = torch.sigmoid(logits) # Sigmoid → [0, 1] + predictions = (probs > 0.5).long() # Seuil → 0 ou 1 return { 'logits': logits, @@ -58,47 +56,105 @@ def forward(self, x, num_classes=2): def compute_loss(self, logits, targets, mask=None): """ - Cross-Entropy Loss. + Symmetric BCE Loss. + + Calcule la BCE dans les deux sens (target et 1-target) + et garde le minimum → gère la symétrie. Args: - logits: [batch, n_nodes, num_classes] - targets: [batch, n_nodes] - classes {0, 1, ..., k-1} - mask: [batch, n_nodes] - optionnel + logits: [batch, n_nodes] - sorties brutes (avant sigmoid) + targets: [batch, n_nodes] - valeurs 0 ou 1 + mask: [batch, n_nodes] - optionnel (pour graphes de tailles différentes) Returns: loss: scalar """ - b, n, c = logits.shape - logits_flat = logits.reshape(-1, c) - targets_flat = targets.reshape(-1).long() + targets = targets.float() if mask is not None: - mask_flat = mask.reshape(-1).float() - loss = F.cross_entropy(logits_flat, targets_flat, reduction='none') - return (loss * mask_flat).sum() / mask_flat.sum().clamp(min=1) + mask = mask.float() + + # Loss directe : pred vs target + loss_direct = F.binary_cross_entropy_with_logits( + logits, targets, reduction='none' + ) + loss_direct = (loss_direct * mask).sum() / mask.sum().clamp(min=1) + + # Loss inversée : pred vs (1 - target) + loss_inverse = F.binary_cross_entropy_with_logits( + logits, 1.0 - targets, reduction='none' + ) + loss_inverse = (loss_inverse * mask).sum() / mask.sum().clamp(min=1) + else: + # Loss directe : pred vs target + loss_direct = F.binary_cross_entropy_with_logits(logits, targets) - return F.cross_entropy(logits_flat, targets_flat) + # Loss inversée : pred vs (1 - target) + loss_inverse = F.binary_cross_entropy_with_logits(logits, 1.0 - targets) + + # Symmetric : on prend le minimum des deux + loss = torch.min(loss_direct, loss_inverse) + + return loss + + def compute_similarity(self, predictions, targets): + """ + Calcule le pourcentage de ressemblance (en tenant compte de la symétrie). + + Args: + predictions: [batch, n_nodes] - 0 ou 1 + targets: [batch, n_nodes] - 0 ou 1 + + Returns: + similarity: float entre 0 et 1 (1 = parfait) + """ + predictions = predictions.float() + targets = targets.float() + + # Ressemblance directe + match_direct = (predictions == targets).float().mean() + + # Ressemblance inversée + match_inverse = (predictions == (1.0 - targets)).float().mean() + + # Meilleure des deux + similarity = torch.max(match_direct, match_inverse) + + return similarity.item() if __name__ == "__main__": - print("=== Test Classifier ===") + print("=== Test Classifier (Binaire + Symmetric BCE) ===\n") x = torch.randn(4, 6, 256) # [batch, n_nodes, hidden_dim] - classifier = Classifier(hidden_dim=256, max_classes=10) + classifier = Classifier(hidden_dim=256) - # Test 2 classes (MaxCut) - output = classifier(x, num_classes=2) - print(f"Logits (2 classes): {output['logits'].shape}") + # Forward + output = classifier(x) + print(f"Logits: {output['logits'].shape}") + print(f"Probs: {output['probs'].shape}") + print(f"Predictions: {output['predictions'].shape}") + print(f"Exemple probs: {output['probs'][0].tolist()}") + print(f"Exemple preds: {output['predictions'][0].tolist()}") - # Test 5 classes (Graph Coloring) - output = classifier(x, num_classes=5) - print(f"Logits (5 classes): {output['logits'].shape}") + # Test Symmetric Loss + targets = torch.tensor([[1, 0, 1, 0, 1, 0]] * 4).float() - # Test loss - targets = torch.randint(0, 2, (4, 6)) - output = classifier(x, num_classes=2) loss = classifier.compute_loss(output['logits'], targets) - print(f"Loss: {loss.item():.4f}") - - print(f"Params: {sum(p.numel() for p in classifier.parameters()):,}") + print(f"\nSymmetric BCE Loss: {loss.item():.4f}") + + # Test symétrie : target et 1-target doivent donner la même loss + loss_normal = classifier.compute_loss(output['logits'], targets) + loss_inverted = classifier.compute_loss(output['logits'], 1 - targets) + print(f"Loss (target normal): {loss_normal.item():.4f}") + print(f"Loss (target inversé): {loss_inverted.item():.4f}") + print(f"Égales ? {'✅ OUI' if abs(loss_normal.item() - loss_inverted.item()) < 1e-6 else '❌ NON'}") + + # Test similarité + pred = torch.tensor([[0, 1, 0, 1, 0, 1]]) + target = torch.tensor([[1, 0, 1, 0, 1, 0]]) + sim = classifier.compute_similarity(pred, target) + print(f"\nSimilarité [0,1,0,1,0,1] vs [1,0,1,0,1,0]: {sim:.0%}") + + print(f"\nParams: {sum(p.numel() for p in classifier.parameters()):,}") print("✅ OK") diff --git a/prototype_v1/explication_bitstring_comparaison.txt b/prototype_v1/explication_bitstring_comparaison.txt new file mode 100644 index 0000000..1ab5c48 --- /dev/null +++ b/prototype_v1/explication_bitstring_comparaison.txt @@ -0,0 +1,54 @@ +Format QAOA (Qiskit) + +result = solver.solve(problem) +print(result.x) # numpy array: [0, 1, 0, 1] +print(type(result.x)) # +Format Notre Modèle + +output = model(x, edge_index, problem_id=0) +print(output['predictions']) # tensor([[0, 1, 0, 1]]) +print(type(output['predictions'])) # + + +Sont-ils comparables ? + QAOA Notre Modèle +Type numpy.ndarray torch.Tensor +Shape [n_nodes] [batch, n_nodes] +Valeurs {0, 1} {0, 1} + +Signification Nœud i dans set 0 ou 1 Nœud i dans set 0 ou 1 +OUI mais il faut convertir : + + +# QAOA → Tensor pour comparaison +qaoa_target = torch.tensor(result.x) # [0, 1, 0, 1] + +# Notre modèle → squeeze pour enlever batch dim +model_pred = output['predictions'].squeeze(0) # [0, 1, 0, 1] + +# Maintenant comparable ! +⚠️ ATTENTION : Symétrie du problème ! +Pour MaxCut, il y a une subtilité : + + +Solution [0, 1, 0, 1] = Set A: {0, 2}, Set B: {1, 3} +Solution [1, 0, 1, 0] = Set A: {1, 3}, Set B: {0, 2} + +CE SONT LES MÊMES SOLUTIONS ! (juste inversées) +Donc si : + + +QAOA dit: [0, 1, 0, 1] +Modèle dit: [1, 0, 1, 0] + +→ C'est CORRECT ! Même partition, juste labels inversés. +Solutions pour gérer la symétrie +Option 1 : Normaliser (forcer à commencer par 0) + +def normalize_bitstring(bits): + if bits[0] == 1: + return 1 - bits # Inverser + return bits + +target = normalize_bitstring(qaoa_result) +pred = normalize_bitstring(model_pred) diff --git a/prototype_v1/model.py b/prototype_v1/model.py index b721db2..6720648 100644 --- a/prototype_v1/model.py +++ b/prototype_v1/model.py @@ -5,7 +5,7 @@ Graph → GNN Encoder → E_local, E_global problem_id → Lookup Table → E_prob Concat [E_global || E_local || E_prob] → Transformer → embeddings contextualisés - Classifier → logits → Cross-Entropy Loss → Backpropagation + Classifier binaire → probs → Symmetric BCE Loss → Backpropagation """ import torch @@ -20,11 +20,12 @@ class QuantumGraphModel(nn.Module): """ Modèle complet pour résoudre des problèmes d'optimisation sur graphes. - Supporte: + Supporte (tous binaires): - MaxCut (2 classes) - Vertex Cover (2 classes) - Independent Set (2 classes) - - Graph Coloring (k classes) + + Loss: Symmetric BCE (gère la symétrie des solutions) """ def __init__( @@ -36,7 +37,6 @@ def __init__( transformer_layers=4, num_heads=8, num_problems=10, - max_classes=10, dropout=0.1 ): super().__init__() @@ -67,24 +67,22 @@ def __init__( dropout=dropout ) - # 4. Classifier + # 4. Classifier (binaire) self.classifier = Classifier( hidden_dim=hidden_dim, - max_classes=max_classes, dropout=dropout ) - def forward(self, x, edge_index, problem_id, batch=None, num_classes=2): + def forward(self, x, edge_index, problem_id, batch=None): """ Args: x: [n_nodes, node_input_dim] edge_index: [2, n_edges] problem_id: int ou [batch_size] batch: [n_nodes] (optionnel) - num_classes: int Returns: - dict avec logits, probs, predictions + dict avec logits, probs, predictions (tous [batch, n_nodes]) """ # 1. GNN Encoder → E_local, E_global e_local, e_global = self.encoder(x, edge_index, batch) @@ -105,8 +103,8 @@ def forward(self, x, edge_index, problem_id, batch=None, num_classes=2): # 3. Transformer → embeddings contextualisés contextualized = self.transformer(e_local, e_global, e_prob) - # 4. Classifier → logits, probs, predictions - output = self.classifier(contextualized, num_classes=num_classes) + # 4. Classifier binaire → probs, predictions + output = self.classifier(contextualized) return output @@ -126,14 +124,19 @@ def _batch_node_embeddings(self, e_local, batch, batch_size): return out def compute_loss(self, logits, targets, mask=None): - """Cross-Entropy Loss""" + """Symmetric BCE Loss""" return self.classifier.compute_loss(logits, targets, mask) - def forward_with_loss(self, x, edge_index, problem_id, targets, batch=None, num_classes=2): + def compute_similarity(self, predictions, targets): + """Pourcentage de ressemblance (avec symétrie)""" + return self.classifier.compute_similarity(predictions, targets) + + def forward_with_loss(self, x, edge_index, problem_id, targets, batch=None): """Forward + Loss en une seule passe""" - output = self.forward(x, edge_index, problem_id, batch, num_classes) + output = self.forward(x, edge_index, problem_id, batch) loss = self.compute_loss(output['logits'], targets) - return output, loss + similarity = self.compute_similarity(output['predictions'], targets) + return output, loss, similarity if __name__ == "__main__": @@ -159,24 +162,27 @@ def forward_with_loss(self, x, edge_index, problem_id, targets, batch=None, num_ print(f"Paramètres: {sum(p.numel() for p in model.parameters()):,}") # Forward (MaxCut) - output = model(x, edge_index, problem_id=0, num_classes=2) - print(f"\nMaxCut (2 classes):") - print(f" Logits: {output['logits'].shape}") + output = model(x, edge_index, problem_id=0) + print(f"\nMaxCut:") + print(f" Probs: {output['probs'].shape}") print(f" Predictions: {output['predictions']}") - # Loss + # Symmetric Loss targets = torch.tensor([[1, 0, 1, 0, 1, 0]]) loss = model.compute_loss(output['logits'], targets) print(f" Loss: {loss.item():.4f}") + # Test symétrie + loss_inv = model.compute_loss(output['logits'], 1 - targets) + print(f" Loss inversée: {loss_inv.item():.4f}") + print(f" Symétrie OK ? {'✅' if abs(loss.item() - loss_inv.item()) < 1e-6 else '❌'}") + + # Similarité + sim = model.compute_similarity(output['predictions'], targets) + print(f" Similarité: {sim:.0%}") + # Backprop loss.backward() print(" Backprop OK") - # Graph Coloring - output = model(x, edge_index, problem_id=3, num_classes=5) - print(f"\nGraph Coloring (5 classes):") - print(f" Logits: {output['logits'].shape}") - print(f" Predictions: {output['predictions']}") - print("\n✅ Tous les tests passés!") diff --git a/prototype_v1/test_model.py b/prototype_v1/test_model.py index 655900f..07de69d 100644 --- a/prototype_v1/test_model.py +++ b/prototype_v1/test_model.py @@ -3,9 +3,10 @@ Teste: 1. Chaque composant individuellement -2. Le modèle complet +2. Le modèle complet (binaire uniquement) 3. La backpropagation -4. Différents problèmes (binaire et multi-classe) +4. La Symmetric BCE Loss (symétrie des solutions) +5. La métrique de similarité """ import torch @@ -57,7 +58,7 @@ def test_encoder(): assert e_local.shape == (6, 128), f"Expected (6, 128), got {e_local.shape}" assert e_global.shape == (1, 128), f"Expected (1, 128), got {e_global.shape}" - print("✅ Encoder OK\n") + print("OK Encoder\n") return True @@ -87,7 +88,7 @@ def test_problem_embedding(): assert e_prob_batch.shape == (4, 128) assert diff > 0, "Les embeddings devraient être différents" - print("✅ Problem Embedding OK\n") + print("OK Problem Embedding\n") return True @@ -121,14 +122,14 @@ def test_transformer(): assert output.shape == (batch_size, n_nodes, 256) - print("✅ Transformer OK\n") + print("OK Transformer\n") return True -def test_classifier(): - """Test du Classifier""" +def test_classifier_binary(): + """Test du Classifier binaire""" print("=" * 60) - print("TEST 4: Classifier") + print("TEST 4: Classifier (Binaire + Sigmoid)") print("=" * 60) batch_size = 2 @@ -136,38 +137,102 @@ def test_classifier(): hidden_dim = 256 x = torch.randn(batch_size, n_nodes, hidden_dim) - classifier = Classifier(hidden_dim=256, max_classes=10) - - # Test binaire (2 classes) - output_2 = classifier(x, num_classes=2) - print(f"Binaire (2 classes):") - print(f" Logits: {output_2['logits'].shape}") - print(f" Probs: {output_2['probs'].shape}") - print(f" Predictions: {output_2['predictions'].shape}") - print(f" Exemple: {output_2['predictions'][0].tolist()}") - - # Test multi-classe (5 classes) - output_5 = classifier(x, num_classes=5) - print(f"\nMulti-classe (5 classes):") - print(f" Logits: {output_5['logits'].shape}") - print(f" Predictions: {output_5['predictions'][0].tolist()}") - - # Test loss - targets = torch.randint(0, 2, (batch_size, n_nodes)) - loss = classifier.compute_loss(output_2['logits'], targets) - print(f"\nLoss (CE): {loss.item():.4f}") - - assert output_2['logits'].shape == (batch_size, n_nodes, 2) - assert output_5['logits'].shape == (batch_size, n_nodes, 5) - - print("✅ Classifier OK\n") + classifier = Classifier(hidden_dim=256) + + output = classifier(x) + print(f"Logits: {output['logits'].shape}") + print(f"Probs: {output['probs'].shape}") + print(f"Predictions: {output['predictions'].shape}") + print(f"Exemple probs: {[f'{p:.2f}' for p in output['probs'][0].tolist()]}") + print(f"Exemple preds: {output['predictions'][0].tolist()}") + + # Vérifier les shapes (binaire = pas de dimension de classes) + assert output['logits'].shape == (batch_size, n_nodes) + assert output['probs'].shape == (batch_size, n_nodes) + assert output['predictions'].shape == (batch_size, n_nodes) + + # Vérifier que les probs sont entre 0 et 1 + assert output['probs'].min() >= 0 and output['probs'].max() <= 1 + + print("OK Classifier Binaire\n") + return True + + +def test_symmetric_loss(): + """Test de la Symmetric BCE Loss""" + print("=" * 60) + print("TEST 5: Symmetric BCE Loss") + print("=" * 60) + + classifier = Classifier(hidden_dim=256) + + # Logits fixes pour les tests + logits = torch.tensor([[2.0, -2.0, 2.0, -2.0, 2.0, -2.0]]) + + # Target et son inverse + target = torch.tensor([[1.0, 0.0, 1.0, 0.0, 1.0, 0.0]]) + target_inv = torch.tensor([[0.0, 1.0, 0.0, 1.0, 0.0, 1.0]]) + + loss_normal = classifier.compute_loss(logits, target) + loss_inverse = classifier.compute_loss(logits, target_inv) + + print(f"Target: {target[0].tolist()}") + print(f"Target inversé: {target_inv[0].tolist()}") + print(f"Loss (normal): {loss_normal.item():.4f}") + print(f"Loss (inversé): {loss_inverse.item():.4f}") + print(f"Différence: {abs(loss_normal.item() - loss_inverse.item()):.6f}") + + # Les deux loss doivent être identiques + assert abs(loss_normal.item() - loss_inverse.item()) < 1e-5, \ + f"Les loss devraient être égales: {loss_normal.item():.6f} vs {loss_inverse.item():.6f}" + + print("OK Symmetric Loss (les deux loss sont identiques)\n") + return True + + +def test_similarity_metric(): + """Test de la métrique de similarité""" + print("=" * 60) + print("TEST 6: Métrique de Similarité") + print("=" * 60) + + classifier = Classifier(hidden_dim=256) + + # Cas 1: identique + pred1 = torch.tensor([[0, 1, 0, 1, 0, 1]]) + target1 = torch.tensor([[0, 1, 0, 1, 0, 1]]) + sim1 = classifier.compute_similarity(pred1, target1) + print(f"Identique: {pred1[0].tolist()} vs {target1[0].tolist()} → {sim1:.0%}") + + # Cas 2: inversé (symétrie) + pred2 = torch.tensor([[1, 0, 1, 0, 1, 0]]) + target2 = torch.tensor([[0, 1, 0, 1, 0, 1]]) + sim2 = classifier.compute_similarity(pred2, target2) + print(f"Inversé: {pred2[0].tolist()} vs {target2[0].tolist()} → {sim2:.0%}") + + # Cas 3: partiellement correct + pred3 = torch.tensor([[0, 1, 0, 0, 0, 1]]) + target3 = torch.tensor([[0, 1, 0, 1, 0, 1]]) + sim3 = classifier.compute_similarity(pred3, target3) + print(f"Partiel (1 err):{pred3[0].tolist()} vs {target3[0].tolist()} → {sim3:.0%}") + + # Cas 4: tout faux + pred4 = torch.tensor([[0, 0, 0, 0, 0, 0]]) + target4 = torch.tensor([[0, 1, 0, 1, 0, 1]]) + sim4 = classifier.compute_similarity(pred4, target4) + print(f"Moitié: {pred4[0].tolist()} vs {target4[0].tolist()} → {sim4:.0%}") + + assert sim1 == 1.0, f"Identique devrait être 100%, got {sim1:.0%}" + assert sim2 == 1.0, f"Inversé devrait être 100%, got {sim2:.0%}" + + print("OK Similarité\n") return True def test_full_model(): """Test du modèle complet""" print("=" * 60) - print("TEST 5: Modèle Complet (QuantumGraphModel)") + print("TEST 7: Modèle Complet (QuantumGraphModel)") print("=" * 60) x, edge_index = create_test_graph(n_nodes=6, n_features=7) @@ -183,31 +248,33 @@ def test_full_model(): n_params = sum(p.numel() for p in model.parameters()) print(f"Paramètres totaux: {n_params:,}") - # Test MaxCut (problem_id=0, 2 classes) - print(f"\n--- MaxCut (problem_id=0, 2 classes) ---") - output = model(x, edge_index, problem_id=0, num_classes=2) - print(f"Logits: {output['logits'].shape}") + # Test MaxCut (problem_id=0) + print(f"\n--- MaxCut (problem_id=0) ---") + output = model(x, edge_index, problem_id=0) + print(f"Probs: {output['probs'].shape}") print(f"Predictions: {output['predictions'].tolist()}") - # Test Vertex Cover (problem_id=1, 2 classes) - print(f"\n--- Vertex Cover (problem_id=1, 2 classes) ---") - output = model(x, edge_index, problem_id=1, num_classes=2) + # Test Vertex Cover (problem_id=1) + print(f"\n--- Vertex Cover (problem_id=1) ---") + output = model(x, edge_index, problem_id=1) print(f"Predictions: {output['predictions'].tolist()}") - # Test Graph Coloring (problem_id=3, 4 classes) - print(f"\n--- Graph Coloring (problem_id=3, 4 classes) ---") - output = model(x, edge_index, problem_id=3, num_classes=4) - print(f"Logits: {output['logits'].shape}") + # Test Independent Set (problem_id=2) + print(f"\n--- Independent Set (problem_id=2) ---") + output = model(x, edge_index, problem_id=2) print(f"Predictions: {output['predictions'].tolist()}") - print("✅ Modèle Complet OK\n") + assert output['probs'].shape == (1, 6) + assert output['predictions'].shape == (1, 6) + + print("OK Modèle Complet\n") return True def test_backpropagation(): - """Test de la backpropagation à travers tout le modèle""" + """Test de la backpropagation avec Symmetric BCE""" print("=" * 60) - print("TEST 6: Backpropagation") + print("TEST 8: Backpropagation (Symmetric BCE)") print("=" * 60) x, edge_index = create_test_graph(n_nodes=6, n_features=7) @@ -219,17 +286,21 @@ def test_backpropagation(): ) # Forward - output = model(x, edge_index, problem_id=0, num_classes=2) + output = model(x, edge_index, problem_id=0) # Loss targets = torch.tensor([[1, 0, 1, 0, 1, 0]]) loss = model.compute_loss(output['logits'], targets) - print(f"Loss avant backward: {loss.item():.4f}") + print(f"Loss: {loss.item():.4f}") + + # Similarité + sim = model.compute_similarity(output['predictions'], targets) + print(f"Similarité: {sim:.0%}") # Backward loss.backward() - # Vérifier que les gradients existent partout + # Vérifier gradients components = { 'GNN Encoder': model.encoder, 'Problem Embedding': model.problem_embedding, @@ -238,26 +309,25 @@ def test_backpropagation(): } print("\nGradients par composant:") + all_ok = True for name, component in components.items(): has_grad = any(p.grad is not None and p.grad.abs().sum() > 0 for p in component.parameters() if p.requires_grad) - status = "✅" if has_grad else "❌" - print(f" {status} {name}") + status = "OK" if has_grad else "FAIL" + if not has_grad: + all_ok = False + print(f" [{status}] {name}") - # Vérifier spécifiquement la lookup table - lookup_grad = model.problem_embedding.embedding_table.weight.grad - if lookup_grad is not None: - grad_problem_0 = lookup_grad[0].abs().sum().item() - print(f"\n Gradient lookup table (ID=0): {grad_problem_0:.6f}") + assert all_ok, "Tous les composants doivent avoir des gradients" - print("✅ Backpropagation OK\n") + print("OK Backpropagation\n") return True def test_training_step(): - """Simule une étape d'entraînement""" + """Simule une étape d'entraînement avec Symmetric BCE""" print("=" * 60) - print("TEST 7: Training Step Simulation") + print("TEST 9: Training Step (Symmetric BCE)") print("=" * 60) x, edge_index = create_test_graph(n_nodes=6, n_features=7) @@ -271,28 +341,28 @@ def test_training_step(): optimizer = torch.optim.Adam(model.parameters(), lr=0.001) targets = torch.tensor([[1, 0, 1, 0, 1, 0]]) - print("Simulation de 5 steps d'entraînement:") + print("Simulation de 5 steps:") for step in range(5): optimizer.zero_grad() - output = model(x, edge_index, problem_id=0, num_classes=2) - loss = model.compute_loss(output['logits'], targets) + output, loss, similarity = model.forward_with_loss( + x, edge_index, problem_id=0, targets=targets + ) loss.backward() optimizer.step() - accuracy = (output['predictions'] == targets).float().mean().item() - print(f" Step {step+1}: Loss = {loss.item():.4f}, Accuracy = {accuracy:.2%}") + print(f" Step {step+1}: Loss = {loss.item():.4f}, Similarité = {similarity:.0%}") - print("✅ Training Step OK\n") + print("OK Training Step\n") return True def test_different_graph_sizes(): """Test avec différentes tailles de graphes""" print("=" * 60) - print("TEST 8: Différentes Tailles de Graphes") + print("TEST 10: Différentes Tailles de Graphes") print("=" * 60) model = QuantumGraphModel( @@ -304,30 +374,32 @@ def test_different_graph_sizes(): for n_nodes in [4, 8, 16, 32]: x, edge_index = create_test_graph(n_nodes=n_nodes, n_features=7) - output = model(x, edge_index, problem_id=0, num_classes=2) + output = model(x, edge_index, problem_id=0) - print(f" {n_nodes} nœuds: predictions shape = {output['predictions'].shape}") + print(f" {n_nodes} nœuds: probs = {output['probs'].shape}, preds = {output['predictions'].shape}") assert output['predictions'].shape == (1, n_nodes) - print("✅ Différentes Tailles OK\n") + print("OK Différentes Tailles\n") return True def run_all_tests(): """Lance tous les tests""" print("\n" + "=" * 60) - print(" TESTS DU PROTOTYPE v1 - QuantumGraphModel") + print(" TESTS PROTOTYPE v1 - Binaire + Symmetric BCE Loss") print("=" * 60 + "\n") tests = [ ("Encoder", test_encoder), ("Problem Embedding", test_problem_embedding), ("Transformer", test_transformer), - ("Classifier", test_classifier), - ("Full Model", test_full_model), + ("Classifier Binaire", test_classifier_binary), + ("Symmetric BCE Loss", test_symmetric_loss), + ("Similarité", test_similarity_metric), + ("Modèle Complet", test_full_model), ("Backpropagation", test_backpropagation), ("Training Step", test_training_step), - ("Different Sizes", test_different_graph_sizes), + ("Différentes Tailles", test_different_graph_sizes), ] results = [] @@ -336,7 +408,7 @@ def run_all_tests(): success = test_fn() results.append((name, success)) except Exception as e: - print(f"❌ ERREUR dans {name}: {e}\n") + print(f"ERREUR dans {name}: {e}\n") results.append((name, False)) # Résumé @@ -348,15 +420,15 @@ def run_all_tests(): total = len(results) for name, success in results: - status = "✅" if success else "❌" - print(f" {status} {name}") + status = "OK" if success else "FAIL" + print(f" [{status}] {name}") print(f"\n Total: {passed}/{total} tests passés") if passed == total: - print("\n 🎉 TOUS LES TESTS SONT PASSÉS ! 🎉") + print("\n TOUS LES TESTS SONT PASSES !") else: - print("\n ⚠️ Certains tests ont échoué.") + print("\n Certains tests ont échoué.") print("=" * 60 + "\n")