Building a Professional Neural Network Framework: Full C++ Implementation with Windows GUI and Real-Time Training Visualization

Joined
Jun 12, 2020
Messages
73
Reaction score
3
C++:
/*


MIT License


Copyright (c) 2025 CoTon_TiGe_MoUaRf



Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:



The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.




THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


*/
#include <windows.h>
#include <stdio.h>
#include <string>
#include <vector>
#include <ctime>
#include <stdarg.h>
#include <cstdio>
#include <iostream>
#include <cmath>
#include <iomanip>
#include <cstdlib>
#include <algorithm>
#include <fstream>
#include <sstream>




#ifndef _tWinMain
    #ifdef UNICODE
        #define _tWinMain wWinMain
    #else
        #define _tWinMain WinMain
    #endif
#endif


#define WM_UPDATE_TRAINING (WM_USER + 1)
#define WM_UPDATE_INPUTS (WM_USER + 2)


struct Neurone {
    double* poids;
    double biais;
    double activation;
    double delta;
    double z;
};

struct LayerNorm {
    double* gamma;
    double* beta;
    double epsilon;
};

struct BatchNorm {
    double* gamma;
    double* beta;
    double* running_mean;
    double* running_var;
    double momentum;
    double epsilon;
    double* batch_mean;
    double* batch_var;
};

struct Couche {
    Neurone* neurones;
    int nombre_neurones;
    int nombre_entrees;
    bool use_layer_norm;
    bool use_batch_norm;
    LayerNorm ln;
    BatchNorm bn;
    double* activations_post_norm;
};

struct ReseauNeuronal {
    Couche* couches;
    int nombre_couches;
    double taux_apprentissage;
    bool use_relu;
};

struct EchantillonEntrainement {
    std::vector<double> features;
    double sortie_attendue;
};


struct DynamicControls {
    HWND btnEntrainer;
    HWND btnCharger;
    HWND btnPredire;
    std::vector<HWND> editEntrees;
    std::vector<HWND> labelEntrees;
    HWND textResultat;
    HWND textStatus;
    HWND textEntrainement;
    HWND editEpochs;
    HWND editLR;
    HWND hStatusBar;
    HWND labelResultat;
};

struct TrainUpdateInfo {
    int epoch;
    double mse;
    double lr;
    int nan_count;
    bool is_final;
};


ReseauNeuronal gReseau;
DynamicControls gControls;
std::vector<int> gTopologie;
int gNbCouches;
std::string gFichierPoids = "poids_entraines.csv";
std::vector<EchantillonEntrainement> gDonnees;
bool gReseauInitialise = false;
HWND gHwndMain = NULL;
int gNombreEpochs = 5000;
int gNombreEntrees = 3;


HBRUSH hBrushBackground = NULL;
HBRUSH hBrushGroup = NULL;
HPEN hPenBorder = NULL;
HFONT hFontTitle = NULL;
HFONT hFontNormal = NULL;
HFONT hFontSmall = NULL;


HWND CreateStaticLabel(HWND hwndParent, const char* text, int x, int y, int w, int h, HFONT hFont) {
    HWND hStatic = CreateWindowA("STATIC", text, WS_VISIBLE | WS_CHILD,
                                 x, y, w, h, hwndParent, NULL,
                                 GetModuleHandle(NULL), NULL);
    if (hFont) {
        SendMessage(hStatic, WM_SETFONT, (WPARAM)hFont, TRUE);
    }
    return hStatic;
}


double relu(double x) { return (x > 0) ? x : 0.0; }
double relu_derivee(double x) { return (x > 0) ? 1.0 : 0.0; }
double sigmoid(double x) {
    if (x > 100) return 1.0;
    if (x < -100) return 0.0;
    return 1.0 / (1.0 + std::exp(-x));
}
double sigmoid_derivee(double y) { return y * (1.0 - y); }


void initialiser_layer_norm(LayerNorm& ln, int taille) {
    ln.gamma = new double[taille];
    ln.beta = new double[taille];
    ln.epsilon = 1e-5;
    for (int i = 0; i < taille; i++) {
        ln.gamma[i] = 1.0;
        ln.beta[i] = 0.0;
    }
}

void initialiser_batch_norm(BatchNorm& bn, int taille) {
    bn.gamma = new double[taille];
    bn.beta = new double[taille];
    bn.running_mean = new double[taille];
    bn.running_var = new double[taille];
    bn.batch_mean = new double[taille];
    bn.batch_var = new double[taille];
    bn.momentum = 0.9;
    bn.epsilon = 1e-5;
    for (int i = 0; i < taille; i++) {
        bn.gamma[i] = 1.0;
        bn.beta[i] = 0.0;
        bn.running_mean[i] = 0.0;
        bn.running_var[i] = 1.0;
    }
}

void initialiser_couche(Couche& couche, int nombre_neurones, int nombre_entrees, bool use_layer_norm, bool use_batch_norm) {
    couche.nombre_neurones = nombre_neurones;
    couche.nombre_entrees = nombre_entrees;
    couche.use_layer_norm = use_layer_norm;
    couche.use_batch_norm = use_batch_norm;
    couche.neurones = new Neurone[nombre_neurones];
    couche.activations_post_norm = new double[nombre_neurones];

    double limite = std::sqrt(6.0 / (nombre_entrees + nombre_neurones));
    for (int i = 0; i < nombre_neurones; i++) {
        couche.neurones[i].poids = new double[nombre_entrees];
        for (int j = 0; j < nombre_entrees; j++) {
            couche.neurones[i].poids[j] = (2.0 * rand() / RAND_MAX - 1.0) * limite * 2.0;
        }
        couche.neurones[i].biais = 0.01 * (2.0 * rand() / RAND_MAX - 1.0);
        couche.neurones[i].activation = 0.0;
        couche.neurones[i].delta = 0.0;
        couche.neurones[i].z = 0.0;
    }

    if (use_layer_norm) initialiser_layer_norm(couche.ln, nombre_neurones);
    if (use_batch_norm) initialiser_batch_norm(couche.bn, nombre_neurones);
}

void initialiser_reseau(ReseauNeuronal& reseau, const std::vector<int>& topologie, bool use_relu, bool use_layer_norm, bool use_batch_norm) {
    reseau.nombre_couches = topologie.size();
    reseau.use_relu = use_relu;
    reseau.couches = new Couche[reseau.nombre_couches];

    for (int i = 0; i < reseau.nombre_couches; i++) {
        int nombre_entrees = (i == 0) ? topologie[i] : topologie[i - 1];
        bool ln = use_layer_norm && (i > 0 && i < reseau.nombre_couches - 1);
        bool bn = use_batch_norm && (i > 0 && i < reseau.nombre_couches - 1);
        initialiser_couche(reseau.couches[i], topologie[i], nombre_entrees, ln, bn);
    }
}


void layer_norm_forward(LayerNorm& ln, double* x, double* y, int taille) {
    double mean = 0.0, M2 = 0.0;
    for (int i = 0; i < taille; i++) {
        double delta = x[i] - mean;
        mean += delta / (i + 1);
        double delta2 = x[i] - mean;
        M2 += delta * delta2;
    }
    double variance = (taille > 1) ? M2 / taille : 0.0;
    double denom = std::sqrt(variance + ln.epsilon);
    if (!std::isfinite(denom) || denom < 1e-10) denom = ln.epsilon;

    for (int i = 0; i < taille; i++) {
        double normalized = (x[i] - mean) / denom;
        normalized = std::max(-10.0, std::min(10.0, normalized));
        y[i] = ln.gamma[i] * normalized + ln.beta[i];
    }
}


void forward_propagation(ReseauNeuronal& reseau, const std::vector<double>& entrees, bool training_mode = true) {
    if (entrees.size() != (size_t)reseau.couches[0].nombre_neurones) {
        std::cerr << "Erreur : nombre d'entrees (" << entrees.size()
                  << ") != nombre de neurones de la couche d'entree ("
                  << reseau.couches[0].nombre_neurones << ")\n";
        return;
    }

    
    for (int i = 0; i < reseau.couches[0].nombre_neurones; i++) {
        reseau.couches[0].neurones[i].activation = entrees[i];
    }

    
    for (int c = 1; c < reseau.nombre_couches; c++) {
        Couche& couche_precedente = reseau.couches[c - 1];
        Couche& couche_actuelle = reseau.couches[c];

        
        for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
            double somme = couche_actuelle.neurones[i].biais;
            double compensation = 0.0;
            
            for (int j = 0; j < couche_actuelle.nombre_entrees; j++) {
                double terme = couche_actuelle.neurones[i].poids[j] * couche_precedente.neurones[j].activation;
                double y_temp = terme - compensation;
                double t_temp = somme + y_temp;
                compensation = (t_temp - somme) - y_temp;
                somme = t_temp;
            }
            
            
            couche_actuelle.neurones[i].z = somme;
            couche_actuelle.activations_post_norm[i] = somme;
        }

        
        if (couche_actuelle.use_batch_norm && training_mode) {
            
            double batch_mean = 0.0;
            for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
                batch_mean += couche_actuelle.activations_post_norm[i];
            }
            batch_mean /= couche_actuelle.nombre_neurones;
            
            
            double batch_var = 0.0;
            for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
                double diff = couche_actuelle.activations_post_norm[i] - batch_mean;
                batch_var += diff * diff;
            }
            batch_var /= couche_actuelle.nombre_neurones;
            
            
            for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
                couche_actuelle.bn.batch_mean[i] = batch_mean;
                couche_actuelle.bn.batch_var[i] = batch_var;
            }
            
            
            for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
                couche_actuelle.bn.running_mean[i] =
                    couche_actuelle.bn.momentum * couche_actuelle.bn.running_mean[i] +
                    (1.0 - couche_actuelle.bn.momentum) * batch_mean;
                couche_actuelle.bn.running_var[i] =
                    couche_actuelle.bn.momentum * couche_actuelle.bn.running_var[i] +
                    (1.0 - couche_actuelle.bn.momentum) * (batch_var + couche_actuelle.bn.epsilon);
            }
            
            
            double denom = std::sqrt(batch_var + couche_actuelle.bn.epsilon);
            if (!std::isfinite(denom) || denom < 1e-10) {
                denom = couche_actuelle.bn.epsilon;
            }
            
            for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
                double normalized = (couche_actuelle.activations_post_norm[i] - batch_mean) / denom;
                couche_actuelle.activations_post_norm[i] =
                    couche_actuelle.bn.gamma[i] * normalized + couche_actuelle.bn.beta[i];
            }
        } else if (couche_actuelle.use_batch_norm && !training_mode) {
            
            for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
                double denom = std::sqrt(couche_actuelle.bn.running_var[i] + couche_actuelle.bn.epsilon);
                if (!std::isfinite(denom) || denom < 1e-10) {
                    denom = couche_actuelle.bn.epsilon;
                }
                
                double normalized = (couche_actuelle.activations_post_norm[i] - couche_actuelle.bn.running_mean[i]) / denom;
                couche_actuelle.activations_post_norm[i] =
                    couche_actuelle.bn.gamma[i] * normalized + couche_actuelle.bn.beta[i];
            }
        }

        
        if (couche_actuelle.use_layer_norm) {
            layer_norm_forward(couche_actuelle.ln, couche_actuelle.activations_post_norm, couche_actuelle.activations_post_norm, couche_actuelle.nombre_neurones);
        }

        
        for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
            double activation_val = couche_actuelle.activations_post_norm[i];
            
            
            if (!std::isfinite(activation_val)) {
                couche_actuelle.neurones[i].activation = 0.0;
                continue;
            }
            
            
            activation_val = std::max(-50.0, std::min(50.0, activation_val));
            
            if (c < reseau.nombre_couches - 1) {
                
                if (reseau.use_relu) {
                    couche_actuelle.neurones[i].activation = std::max(0.0, activation_val);
                } else {
                    
                    if (activation_val >= 0) {
                        double exp_neg_x = std::exp(-activation_val);
                        couche_actuelle.neurones[i].activation = 1.0 / (1.0 + exp_neg_x);
                    } else {
                        double exp_x = std::exp(activation_val);
                        couche_actuelle.neurones[i].activation = exp_x / (1.0 + exp_x);
                    }
                }
            } else {
                
                couche_actuelle.neurones[i].activation = activation_val;
            }
        }
    }
}



void backward_propagation(ReseauNeuronal& reseau, double sortie_attendue) {
    const double GRADIENT_CLIP_MAX = 5.0;
    const double MIN_DERIVATIVE = 1e-8;
    int derniere_couche = reseau.nombre_couches - 1;
    Couche& couche_sortie = reseau.couches[derniere_couche];

    
    for (int i = 0; i < couche_sortie.nombre_neurones; i++) {
        double erreur = sortie_attendue - couche_sortie.neurones[i].activation;
        
        couche_sortie.neurones[i].delta = erreur;
        couche_sortie.neurones[i].delta = std::max(-GRADIENT_CLIP_MAX,
                                                    std::min(GRADIENT_CLIP_MAX,
                                                    couche_sortie.neurones[i].delta));
    }

    
    for (int c = derniere_couche - 1; c > 0; c--) {
        Couche& couche_actuelle = reseau.couches[c];
        Couche& couche_suivante = reseau.couches[c + 1];

        
        double* deltas_apres_ln = new double[couche_actuelle.nombre_neurones];
        
        if (couche_actuelle.use_layer_norm) {
            
            double mean = 0.0, M2 = 0.0;
            for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
                double delta = couche_actuelle.activations_post_norm[i] - mean;
                mean += delta / (i + 1);
                double delta2 = couche_actuelle.activations_post_norm[i] - mean;
                M2 += delta * delta2;
            }
            double variance = (couche_actuelle.nombre_neurones > 1) ?
                            M2 / couche_actuelle.nombre_neurones : 0.0;
            double denom = std::sqrt(variance + couche_actuelle.ln.epsilon);
            if (!std::isfinite(denom) || denom < 1e-10) denom = couche_actuelle.ln.epsilon;

            for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
                
                deltas_apres_ln[i] = couche_actuelle.neurones[i].delta *
                                    couche_actuelle.ln.gamma[i] / denom;
            }
        } else {
            for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
                deltas_apres_ln[i] = couche_actuelle.neurones[i].delta;
            }
        }

        
        double* deltas_apres_bn = new double[couche_actuelle.nombre_neurones];
        
        if (couche_actuelle.use_batch_norm) {
            double batch_mean = couche_actuelle.bn.batch_mean[0];
            double batch_var = couche_actuelle.bn.batch_var[0];
            double denom = std::sqrt(batch_var + couche_actuelle.bn.epsilon);
            if (!std::isfinite(denom) || denom < 1e-10) denom = couche_actuelle.bn.epsilon;

            for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
                
                double dgamma = deltas_apres_ln[i] *
                    (couche_actuelle.activations_post_norm[i] - batch_mean) / denom;
                
                
                double x_normalized = (couche_actuelle.activations_post_norm[i] - batch_mean) / denom;
                double dbeta = deltas_apres_ln[i];
                
                
                couche_actuelle.bn.gamma[i] -= reseau.taux_apprentissage * dgamma * 0.01;
                couche_actuelle.bn.beta[i] -= reseau.taux_apprentissage * dbeta * 0.01;
                
                
                double dx_normalized = deltas_apres_ln[i] * couche_actuelle.bn.gamma[i];
                double dvar = dx_normalized * (couche_actuelle.activations_post_norm[i] - batch_mean) *
                            (-0.5) * std::pow(denom, -3.0);
                double dmean = -dx_normalized / denom + dvar * (-2.0) *
                            (couche_actuelle.activations_post_norm[i] - batch_mean) / couche_actuelle.nombre_neurones;
                
                deltas_apres_bn[i] = dx_normalized / denom + dmean / couche_actuelle.nombre_neurones +
                                    dvar * 2.0 * (couche_actuelle.activations_post_norm[i] - batch_mean) /
                                    couche_actuelle.nombre_neurones;
            }
        } else {
            for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
                deltas_apres_bn[i] = deltas_apres_ln[i];
            }
        }

        
        for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
            double somme_deltas = 0.0;
            double compensation = 0.0;
            
            
            for (int j = 0; j < couche_suivante.nombre_neurones; j++) {
                double terme = couche_suivante.neurones[j].delta *
                              couche_suivante.neurones[j].poids[i];
                double y_temp = terme - compensation;
                double t_temp = somme_deltas + y_temp;
                compensation = (t_temp - somme_deltas) - y_temp;
                somme_deltas = t_temp;
            }

            
            double derivee_activation = 1.0;
            
            if (reseau.use_relu) {
                
                derivee_activation = (couche_actuelle.neurones[i].z > 0.0) ? 1.0 : 0.0;
            } else {
                
                double activation = couche_actuelle.neurones[i].activation;
                
                
                if (activation > 0.9999) {
                    derivee_activation = 1e-4;
                } else if (activation < 0.0001) {
                    derivee_activation = 1e-4;
                } else {
                    derivee_activation = activation * (1.0 - activation);
                }
            }
            
            
            if (!std::isfinite(derivee_activation) || std::abs(derivee_activation) < MIN_DERIVATIVE) {
                derivee_activation = MIN_DERIVATIVE;
            }

            
            couche_actuelle.neurones[i].delta = somme_deltas * derivee_activation;
            
            
            if (couche_actuelle.use_batch_norm || couche_actuelle.use_layer_norm) {
                couche_actuelle.neurones[i].delta *= deltas_apres_bn[i];
            }
            
            
            couche_actuelle.neurones[i].delta =
                std::max(-GRADIENT_CLIP_MAX,
                         std::min(GRADIENT_CLIP_MAX, couche_actuelle.neurones[i].delta));
        }

        delete[] deltas_apres_ln;
        delete[] deltas_apres_bn;
    }
}


void mettre_a_jour_poids(ReseauNeuronal& reseau) {
    for (int c = 1; c < reseau.nombre_couches; c++) {
        Couche& couche_precedente = reseau.couches[c - 1];
        Couche& couche_actuelle = reseau.couches[c];
        for (int i = 0; i < couche_actuelle.nombre_neurones; i++) {
            couche_actuelle.neurones[i].biais += reseau.taux_apprentissage * couche_actuelle.neurones[i].delta;
            for (int j = 0; j < couche_actuelle.nombre_entrees; j++) {
                double gradient = couche_actuelle.neurones[i].delta * couche_precedente.neurones[j].activation;
                couche_actuelle.neurones[i].poids[j] += reseau.taux_apprentissage * gradient;
            }
        }
    }
}


void clipper_gradients(ReseauNeuronal& reseau, double max_grad = 10.0) {
    double norm_l2_squared = 0.0;
    for (int c = 1; c < reseau.nombre_couches; c++) {
        Couche& couche = reseau.couches[c];
        for (int i = 0; i < couche.nombre_neurones; i++) {
            norm_l2_squared += couche.neurones[i].delta * couche.neurones[i].delta;
        }
    }
    double norm_l2 = std::sqrt(norm_l2_squared);
    if (norm_l2 > max_grad && norm_l2 > 0.0) {
        double facteur = max_grad / norm_l2;
        for (int c = 1; c < reseau.nombre_couches; c++) {
            Couche& couche = reseau.couches[c];
            for (int i = 0; i < couche.nombre_neurones; i++) {
                couche.neurones[i].delta *= facteur;
            }
        }
    }
}


void sauvegarder_poids(ReseauNeuronal& reseau, const std::string& nom_fichier) {
    std::ofstream fichier(nom_fichier);
    if (!fichier.is_open()) {
        std::cerr << "Erreur : impossible d'ouvrir le fichier " << nom_fichier << std::endl;
        return;
    }
    fichier << "couche,neurone,type,index,valeur\n";
    for (int c = 1; c < reseau.nombre_couches; c++) {
        Couche& couche = reseau.couches[c];
        for (int i = 0; i < couche.nombre_neurones; i++) {
            fichier << c << "," << i << ",biais,0," << std::setprecision(15) << couche.neurones[i].biais << "\n";
        }
        for (int i = 0; i < couche.nombre_neurones; i++) {
            for (int j = 0; j < couche.nombre_entrees; j++) {
                fichier << c << "," << i << ",poids," << j << "," << std::setprecision(15) << couche.neurones[i].poids[j] << "\n";
            }
        }
    }
    fichier.close();
}

void charger_poids(ReseauNeuronal& reseau, const std::string& nom_fichier) {
    std::ifstream fichier(nom_fichier);
    if (!fichier.is_open()) {
        std::cerr << "Erreur : impossible d'ouvrir le fichier " << nom_fichier << std::endl;
        return;
    }
    std::string ligne;
    std::getline(fichier, ligne);
    while (std::getline(fichier, ligne)) {
        if (ligne.empty()) continue;
        for (char& c : ligne) if (c == ',') c = ' ';
        std::istringstream flux(ligne);
        int couche_idx, neurone_idx, index;
        std::string type;
        double valeur;
        if (flux >> couche_idx >> neurone_idx >> type >> index >> valeur) {
            if (couche_idx < reseau.nombre_couches && neurone_idx < reseau.couches[couche_idx].nombre_neurones) {
                if (type == "biais") {
                    reseau.couches[couche_idx].neurones[neurone_idx].biais = valeur;
                } else if (type == "poids" && index < reseau.couches[couche_idx].nombre_entrees) {
                    reseau.couches[couche_idx].neurones[neurone_idx].poids[index] = valeur;
                }
            }
        }
    }
    fichier.close();
}


std::vector<EchantillonEntrainement> charger_csv(const std::string& nom_fichier) {
    std::vector<EchantillonEntrainement> donnees;
    std::ifstream fichier(nom_fichier);
    if (!fichier.is_open()) {
        std::cerr << "Erreur : impossible d'ouvrir le fichier " << nom_fichier << std::endl;
        return donnees;
    }
    std::string ligne;
    std::getline(fichier, ligne);
    while (std::getline(fichier, ligne)) {
        if (ligne.empty()) continue;
        for (char& c : ligne) if (c == ',') c = ' ';
        std::istringstream flux(ligne);
        std::vector<double> valeurs;
        double val;
        while (flux >> val) valeurs.push_back(val);
        if (valeurs.empty()) continue;
        EchantillonEntrainement echantillon;
        echantillon.sortie_attendue = valeurs.back();
        echantillon.features.assign(valeurs.begin(), valeurs.end() - 1);
        donnees.push_back(echantillon);
    }
    fichier.close();
    std::cout << "✓ " << donnees.size() << " échantillons chargés depuis " << nom_fichier << "\n";
    return donnees;
}


bool contient_nan(double valeur) {
    return std::isnan(valeur) || std::isinf(valeur);
}


void EnvoyerUpdateEntrainement(int epoch, double mse, double lr, int nan_count, bool is_final = false) {
    if (gHwndMain == NULL) return;
    TrainUpdateInfo* info = new TrainUpdateInfo{epoch, mse, lr, nan_count, is_final};
    PostMessage(gHwndMain, WM_UPDATE_TRAINING, 0, (LPARAM)info);
}

void AfficherMessage(HWND hwnd, const std::string& message, const std::string& type) {
    if (gControls.textStatus) SetWindowTextA(gControls.textStatus, message.c_str());
    if (gControls.textEntrainement && type == "TRAIN") {
        
        char buffer[24576];
        GetWindowTextA(gControls.textEntrainement, buffer, sizeof(buffer));
        std::string ancien = buffer;
        ancien += message + "\n";
        
        if (ancien.length() > 30000) ancien = ancien.substr(ancien.length() - 10000);
        SetWindowTextA(gControls.textEntrainement, ancien.c_str());
        SendMessage(gControls.textEntrainement, EM_SETSEL, -1, -1);
        SendMessage(gControls.textEntrainement, EM_SCROLLCARET, 0, 0);
    }
}

void AfficherMessageThreadSafe(HWND hwnd, const std::string& message, bool append = false) {
    if (!hwnd || !gControls.textStatus) return;
    if (!append) {
        SetWindowTextA(gControls.textStatus, message.c_str());
    } else {
        int len = GetWindowTextLengthA(gControls.textEntrainement);
        SendMessageA(gControls.textEntrainement, EM_SETSEL, len, len);
        SendMessageA(gControls.textEntrainement, EM_REPLACESEL, FALSE, (LPARAM)message.c_str());
        SendMessageA(gControls.textEntrainement, EM_REPLACESEL, FALSE, (LPARAM)"\r\n");
        SendMessageA(gControls.textEntrainement, EM_SCROLLCARET, 0, 0);
    }
}

void Log(const char* type, const char* format, ...) {
    va_list args;
    va_start(args, format);
    printf("[%s] ", type);
    vprintf(format, args);
    va_end(args);
    fflush(stdout);
}


void AfficherStatusCouleur(int type, const char* message) {
    if (!gControls.textStatus) return;

    SetWindowTextA(gControls.textStatus, message);
    
    
    HDC hdc = GetDC(gControls.textStatus);
    HBRUSH hColor;
    
    switch(type) {
        case 1: hColor = CreateSolidBrush(RGB(0, 200, 0)); break;
        case 2: hColor = CreateSolidBrush(RGB(255, 0, 0)); break;
        case 3: hColor = CreateSolidBrush(RGB(255, 165, 0)); break;
        default: hColor = CreateSolidBrush(RGB(200, 200, 200)); break;
    }
    
    RECT rect;
    GetClientRect(gControls.textStatus, &rect);
    FillRect(hdc, &rect, hColor);
    
    DeleteObject(hColor);
    ReleaseDC(gControls.textStatus, hdc);
}



void CreerControlsEntrees(HWND hwnd, int nombre_entrees) {
    
    for (auto hwndEdit : gControls.editEntrees) DestroyWindow(hwndEdit);
    for (auto hwndLabel : gControls.labelEntrees) DestroyWindow(hwndLabel);
    gControls.editEntrees.clear();
    gControls.labelEntrees.clear();

    
    if (gControls.labelResultat) {
        DestroyWindow(gControls.labelResultat);
        gControls.labelResultat = NULL;
    }
    if (gControls.textResultat) {
        DestroyWindow(gControls.textResultat);
        gControls.textResultat = NULL;
    }
    if (gControls.btnPredire) {
        DestroyWindow(gControls.btnPredire);
        gControls.btnPredire = NULL;
    }

    
    int col2 = 350;
    int row = 55;
    int hauteur_input = 28;
    int espacement = 40;

    for (int i = 0; i < nombre_entrees; i++) {
        char labelText[64];
        sprintf_s(labelText, sizeof(labelText), "Entrée %d :", i + 1);

        HWND hwndLabel = CreateStaticLabel(hwnd, labelText, col2 + 15, row, 100, 20, hFontSmall);
        gControls.labelEntrees.push_back(hwndLabel);

        HWND hwndEdit = CreateWindowA("EDIT", "0.5", WS_VISIBLE | WS_CHILD | WS_BORDER,
                              col2 + 120, row, 100, hauteur_input, hwnd,
                              (HMENU)((intptr_t)(3001 + i)),
                              GetModuleHandle(NULL), NULL);
        gControls.editEntrees.push_back(hwndEdit);
        SendMessage(hwndEdit, WM_SETFONT, (WPARAM)hFontSmall, TRUE);

        row += espacement;
    }

    
    int row_button = row + 10;
    gControls.btnPredire = CreateWindowA("BUTTON", "Prédire",
                                         WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
                                         col2 + 15, row_button, 200, 32, hwnd, (HMENU)1003,
                                         GetModuleHandle(NULL), NULL);
    SendMessage(gControls.btnPredire, WM_SETFONT, (WPARAM)hFontNormal, TRUE);

    
    gControls.labelResultat = CreateStaticLabel(hwnd, "Résultat :", col2 + 15, row_button + 40, 100, 20, hFontSmall);
    gControls.textResultat = CreateWindowA("EDIT", "", WS_VISIBLE | WS_CHILD | WS_BORDER | ES_READONLY,
                                          col2 + 120, row_button + 40, 100, hauteur_input, hwnd, (HMENU)2002,
                                          GetModuleHandle(NULL), NULL);
    SendMessage(gControls.textResultat, WM_SETFONT, (WPARAM)hFontSmall, TRUE);
}


void entrainer(ReseauNeuronal& reseau, EchantillonEntrainement donnees[], int nombre_samples, int nombre_epochs, const std::string& fichier_poids = "poids_entraines.csv") {
    if (nombre_samples <= 0 || nombre_epochs <= 0) {
        std::cerr << "Erreur : nombre_samples et nombre_epochs doivent être > 0\n";
        return;
    }

    std::cout << "=== Début de l'entraînement ===\n";
    std::cout << "Nombre d'entrées: " << donnees[0].features.size() << "\n";
    std::cout << "Taux d'apprentissage: " << reseau.taux_apprentissage << "\n";
    std::cout << "Nombre d'epochs: " << nombre_epochs << "\n";
    std::cout << "Nombre de samples: " << nombre_samples << "\n\n";

    EnvoyerUpdateEntrainement(0, 0.0, reseau.taux_apprentissage, 0, false);

    double gradient_clip = 5.0;
    double lr_decay = 0.95;

    for (int epoch = 0; epoch < nombre_epochs; epoch++) {
        double erreur_totale = 0.0;
        int erreurs_nan = 0;

        // Mélanger les données
        for (int i = nombre_samples - 1; i > 0; i--) {
            int j = rand() % (i + 1);
            std::swap(donnees[i], donnees[j]);
        }

        for (int i = 0; i < nombre_samples; i++) {
            bool donnees_valides = true;
            for (double val : donnees[i].features) {
                if (contient_nan(val)) {
                    donnees_valides = false;
                    break;
                }
            }
            if (!donnees_valides || contient_nan(donnees[i].sortie_attendue)) {
                erreurs_nan++;
                continue;
            }

            forward_propagation(reseau, donnees[i].features, true);
            double prediction = reseau.couches[reseau.nombre_couches - 1].neurones[0].activation;
            if (contient_nan(prediction)) {
                erreurs_nan++;
                continue;
            }

            backward_propagation(reseau, donnees[i].sortie_attendue);
            clipper_gradients(reseau, gradient_clip);
            mettre_a_jour_poids(reseau);

            double erreur = donnees[i].sortie_attendue - prediction;
            if (!contient_nan(erreur)) erreur_totale += erreur * erreur;
            else erreurs_nan++;
        }

        int samples_valides = nombre_samples - erreurs_nan;
        if (samples_valides > 0) erreur_totale /= samples_valides;
        else {
            std::cerr << "Erreur : tous les samples contiennent des NaN à l'epoch " << epoch << "\n";
            erreur_totale = 0.0;
        }

        if (contient_nan(erreur_totale)) {
            std::cerr << "ARRÊT : MSE est devenu NaN à l'epoch " << epoch << "\n";
            break;
        }

        if (epoch % 1000 == 0 && epoch > 0) reseau.taux_apprentissage *= lr_decay;

        if (epoch % 100 == 0) {
            std::cout << "Epoch " << std::setw(6) << epoch
                      << " | MSE: " << std::scientific << std::setprecision(6) << erreur_totale
                      << " | LR: " << std::scientific << std::setprecision(6) << reseau.taux_apprentissage;
            if (erreurs_nan > 0) std::cout << " | Samples NaN: " << erreurs_nan;
            std::cout << "\n";
            EnvoyerUpdateEntrainement(epoch, erreur_totale, reseau.taux_apprentissage, erreurs_nan, false);
        }

        if ((epoch + 1) % 5000 == 0) std::cout << "✓ " << epoch + 1 << " epochs complétés\n";
    }

    std::cout << "\n=== Entraînement terminé ===\n";
    double mse_final = 0.0;
    int nb_samples_test = std::min(100, nombre_samples);
    for (int i = 0; i < nb_samples_test; i++) {
        forward_propagation(reseau, donnees[i].features, false);
        double prediction = reseau.couches[reseau.nombre_couches - 1].neurones[0].activation;
        double erreur = donnees[i].sortie_attendue - prediction;
        mse_final += erreur * erreur;
    }
    mse_final /= nb_samples_test;
    EnvoyerUpdateEntrainement(nombre_epochs, mse_final, reseau.taux_apprentissage, 0, true);
    sauvegarder_poids(reseau, fichier_poids);
}


double predire(ReseauNeuronal& reseau, const std::vector<double>& entrees) {
    forward_propagation(reseau, entrees, false);
    return reseau.couches[reseau.nombre_couches - 1].neurones[0].activation;
}


void detruire_reseau(ReseauNeuronal& reseau) {
    for (int c = 0; c < reseau.nombre_couches; c++) {
        Couche& couche = reseau.couches[c];
        for (int i = 0; i < couche.nombre_neurones; i++) delete[] couche.neurones[i].poids;
        delete[] couche.neurones;
        delete[] couche.activations_post_norm;
        if (couche.use_layer_norm) { delete[] couche.ln.gamma; delete[] couche.ln.beta; }
        if (couche.use_batch_norm) {
            delete[] couche.bn.gamma; delete[] couche.bn.beta;
            delete[] couche.bn.running_mean; delete[] couche.bn.running_var;
            delete[] couche.bn.batch_mean; delete[] couche.bn.batch_var;
        }
    }
    delete[] reseau.couches;
}


DWORD WINAPI EntrainementThread(LPVOID lpParam) {
    try {
        if (!gDonnees.empty()) {
            int nombre_entrees = gDonnees[0].features.size();
            gNombreEntrees = nombre_entrees;
            gTopologie = {nombre_entrees, 16, 8, 1};
            gNbCouches = gTopologie.size();

            
            PostMessage(gHwndMain, WM_UPDATE_INPUTS, nombre_entrees, 0);

            initialiser_reseau(gReseau, gTopologie, true, false, false);

            char bufferStart[512];
            sprintf_s(bufferStart, sizeof(bufferStart),
                "[INFO] Initialisation OK\r\n"
                "[INFO] Début de l'entraînement avec:\r\n"
                "- Epochs: %d\r\n"
                "- LR: %.6f\r\n"
                "- Samples: %d\r\n"
                "- Entrées: %d\r\n",
                gNombreEpochs, gReseau.taux_apprentissage, (int)gDonnees.size(), nombre_entrees);
            AfficherMessage(gHwndMain, bufferStart, "TRAIN");

            AfficherStatusCouleur(3, "Entraînement en cours...");

            entrainer(gReseau, gDonnees.data(), gDonnees.size(), gNombreEpochs, gFichierPoids);
            AfficherMessage(gHwndMain, "[OK] Entraînement terminé!\n", "TRAIN");
            AfficherStatusCouleur(1, "Entraînement terminé!");
            gReseauInitialise = true;
            EnableWindow(gControls.btnPredire, TRUE);
        }
    } catch (...) {
        AfficherMessage(gHwndMain, "[ERREUR] Problème lors de l'entraînement", "TRAIN");
        AfficherStatusCouleur(2, "Erreur lors de l'entraînement");
    }
    return 0;
}


LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case WM_CREATE: {
            Log("INFO", "Interface neuronale créée\n");

            
            hBrushBackground = CreateSolidBrush(RGB(240, 240, 240));
            hBrushGroup = CreateSolidBrush(RGB(255, 255, 255));       
            hPenBorder = CreatePen(PS_SOLID, 1, RGB(100, 100, 100));

            hFontTitle = CreateFontA(16, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
                                     ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
                                     DEFAULT_QUALITY, DEFAULT_PITCH | FF_MODERN, "Courier New");
            hFontNormal = CreateFontA(12, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
                                      ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
                                      DEFAULT_QUALITY, DEFAULT_PITCH | FF_MODERN, "Courier New");
            hFontSmall = CreateFontA(10, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
                                     ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
                                     DEFAULT_QUALITY, DEFAULT_PITCH | FF_MODERN, "Courier New");

            int col1 = 20, col2 = 350, col3 = 680;
            int row = 20, hauteur_input = 28, espacement = 40;

          
            HWND groupTrain = CreateWindowA("BUTTON", " ENTRAÎNEMENT ",
                                          WS_VISIBLE | WS_CHILD | BS_GROUPBOX,
                                          col1, row, 300, 420, hwnd, (HMENU)5001,
                                          GetModuleHandle(NULL), NULL);
            SendMessage(groupTrain, WM_SETFONT, (WPARAM)hFontTitle, TRUE);

            row += 35;
            gControls.btnEntrainer = CreateWindowA("BUTTON", "Entraîner Nouveau Modèle",
                                                  WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
                                                  col1 + 15, row, 270, 32, hwnd, (HMENU)1001,
                                                  GetModuleHandle(NULL), NULL);
            SendMessage(gControls.btnEntrainer, WM_SETFONT, (WPARAM)hFontNormal, TRUE);
            row += espacement + 5;

            gControls.btnCharger = CreateWindowA("BUTTON", "Charger Modèle Existant",
                                                WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
                                                col1 + 15, row, 270, 32, hwnd, (HMENU)1002,
                                                GetModuleHandle(NULL), NULL);
            SendMessage(gControls.btnCharger, WM_SETFONT, (WPARAM)hFontNormal, TRUE);
            row += espacement;

            CreateStaticLabel(hwnd, "Nombre d'Epochs :", col1 + 15, row, 120, 20, hFontSmall);
            gControls.editEpochs = CreateWindowA("EDIT", "5000",
                                                WS_VISIBLE | WS_CHILD | WS_BORDER,
                                                col1 + 140, row, 100, hauteur_input, hwnd, (HMENU)4001,
                                                GetModuleHandle(NULL), NULL);
            SendMessage(gControls.editEpochs, WM_SETFONT, (WPARAM)hFontSmall, TRUE);
            CreateStaticLabel(hwnd, "(5000-50000 recommandé)", col1 + 140, row + 22, 200, 15, hFontSmall);

            row += espacement;
            CreateStaticLabel(hwnd, "Taux d'Apprentissage :", col1 + 15, row, 120, 20, hFontSmall);
            gControls.editLR = CreateWindowA("EDIT", "0.0005",
                                            WS_VISIBLE | WS_CHILD | WS_BORDER,
                                            col1 + 140, row, 100, hauteur_input, hwnd, (HMENU)4002,
                                            GetModuleHandle(NULL), NULL);
            SendMessage(gControls.editLR, WM_SETFONT, (WPARAM)hFontSmall, TRUE);
            CreateStaticLabel(hwnd, "(0.0001 - 0.01 recommandé)", col1 + 140, row + 22, 200, 15, hFontSmall);

            row += espacement + 10;
            CreateStaticLabel(hwnd, "Logs d'Entraînement :", col1 + 15, row, 200, 20, hFontSmall);

row += 25;

gControls.textEntrainement = CreateWindowA("EDIT", "",

                                          WS_VISIBLE | WS_CHILD | WS_BORDER | WS_VSCROLL | ES_MULTILINE | ES_READONLY,

                                          col1 + 15, row, 270, 120, hwnd, (HMENU)2001,

                                          GetModuleHandle(NULL), NULL);
            SendMessage(gControls.textEntrainement, WM_SETFONT, (WPARAM)hFontSmall, TRUE);

          
            row = 20;
            HWND groupPredict = CreateWindowA("BUTTON", " PRÉDICTION ",
                                              WS_VISIBLE | WS_CHILD | BS_GROUPBOX,
                                              col2, row, 300, 420, hwnd, (HMENU)5002,
                                              GetModuleHandle(NULL), NULL);
            SendMessage(groupPredict, WM_SETFONT, (WPARAM)hFontTitle, TRUE);

            
            CreerControlsEntrees(hwnd, 3);

          
            row = 20;
            HWND groupStatus = CreateWindowA("BUTTON", " STATUT ",
                                             WS_VISIBLE | WS_CHILD | BS_GROUPBOX,
                                             col3, row, 200, 420, hwnd, (HMENU)5003,
                                             GetModuleHandle(NULL), NULL);
            SendMessage(groupStatus, WM_SETFONT, (WPARAM)hFontTitle, TRUE);

            row += 30;
            CreateStaticLabel(hwnd, "Statut :", col3 + 15, row, 80, 20, hFontSmall);
            gControls.textStatus = CreateWindowA("EDIT", "Prêt",
                                                WS_VISIBLE | WS_CHILD | WS_BORDER | ES_READONLY,
                                                col3 + 15, row + 25, 170, 20, hwnd, (HMENU)2003,
                                                GetModuleHandle(NULL), NULL);
            SendMessage(gControls.textStatus, WM_SETFONT, (WPARAM)hFontSmall, TRUE);

          
            gControls.hStatusBar = CreateWindowA("STATIC", "",
                                                WS_VISIBLE | WS_CHILD,
                                                col3 + 15, row + 50, 170, 5, hwnd, NULL,
                                                GetModuleHandle(NULL), NULL);

            AfficherMessageThreadSafe(hwnd, "Application prête. Chargez ou entraînez d'abord.", false);
            AfficherStatusCouleur(0, "Prêt");
            break;
        }

        case WM_UPDATE_INPUTS: {
            int nombre_entrees = (int)wParam;
            CreerControlsEntrees(hwnd, nombre_entrees);
            break;
        }

        case WM_UPDATE_TRAINING: {
            TrainUpdateInfo* info = (TrainUpdateInfo*)lParam;
            if (info) {
                char buffer[512];
                if (info->is_final) {
                    sprintf_s(buffer, sizeof(buffer),
                        "[TERMINÉ] Entraînement terminé!\r\n"
                        "Epoch total: %d\r\n"
                        "MSE final: %.6f\r\n"
                        "LR final: %.6f",
                        gNombreEpochs, info->mse, info->lr);
                    AfficherMessageThreadSafe(hwnd, buffer, true);
                    EnableWindow(gControls.btnEntrainer, TRUE);
                    EnableWindow(gControls.btnCharger, TRUE);
                    EnableWindow(gControls.btnPredire, TRUE);
                    gReseauInitialise = true;
                    AfficherStatusCouleur(1, "Entraînement terminé!");
                } else {
                    sprintf_s(buffer, sizeof(buffer), "Epoch %5d | MSE: %.6f | LR: %.6f", info->epoch, info->mse, info->lr);
                    AfficherMessageThreadSafe(hwnd, buffer, true);
                }
                delete info;
            }
            return 0;
        }

        case WM_COMMAND: {
            int id = LOWORD(wParam);
            if (id == 1001) {
                char bufferEpochs[256];
                GetWindowTextA(gControls.editEpochs, bufferEpochs, sizeof(bufferEpochs));
                gNombreEpochs = atoi(bufferEpochs);
                if (gNombreEpochs <= 0) gNombreEpochs = 5000;

                char bufferLR[256];
                GetWindowTextA(gControls.editLR, bufferLR, sizeof(bufferLR));
                double taux_apprentissage = atof(bufferLR);
                if (taux_apprentissage <= 0.0) taux_apprentissage = 0.0005;

                char bufferInfo[512];
                sprintf_s(bufferInfo, sizeof(bufferInfo), "[INFO] Epochs: %d | LR: %.6f", gNombreEpochs, taux_apprentissage);
                AfficherMessageThreadSafe(hwnd, bufferInfo, false);

                gDonnees = charger_csv("data.csv");
                if (gDonnees.empty()) {
                    AfficherMessageThreadSafe(hwnd, "[ERREUR] Fichier data.csv non trouvé!", false);
                    AfficherStatusCouleur(2, "Erreur: data.csv introuvable");
                } else {
                    EnableWindow(gControls.btnEntrainer, FALSE);
                    EnableWindow(gControls.btnCharger, FALSE);
                    gReseau.taux_apprentissage = taux_apprentissage;
                    AfficherStatusCouleur(3, "Initialisation...");
                    HANDLE hThread = CreateThread(NULL, 0, EntrainementThread, NULL, 0, NULL);
                    if (hThread) CloseHandle(hThread);
                }
            }
            else if (id == 1002) {
                AfficherMessageThreadSafe(hwnd, "[INFO] Initialisation du réseau...", false);
                AfficherStatusCouleur(3, "Chargement...");
                gDonnees = charger_csv("data.csv");
                if (gDonnees.empty()) {
                    AfficherMessageThreadSafe(hwnd, "[ERREUR] Fichier data.csv non trouvé!", false);
                    AfficherStatusCouleur(2, "Erreur: data.csv introuvable");
                } else {
                    int nombre_entrees = gDonnees[0].features.size();
                    gNombreEntrees = nombre_entrees;
                    gTopologie = {nombre_entrees, 16, 8, 1};
                    gNbCouches = gTopologie.size();
                    initialiser_reseau(gReseau, gTopologie, true, false, false);
                    gReseau.taux_apprentissage = 0.001;
                    charger_poids(gReseau, gFichierPoids);
                    PostMessage(hwnd, WM_UPDATE_INPUTS, nombre_entrees, 0);
                    AfficherMessageThreadSafe(hwnd, "[OK] Modèle chargé avec succès!", false);
                    AfficherStatusCouleur(1, "Modèle chargé!");
                    gReseauInitialise = true;
                    EnableWindow(gControls.btnPredire, TRUE);
                }
            }
            else if (id == 1003) {
                if (gReseauInitialise) {
                    std::vector<double> entrees;
                    for (const auto& hwndEdit : gControls.editEntrees) {
                        char buffer[256];
                        GetWindowTextA(hwndEdit, buffer, sizeof(buffer));
                        entrees.push_back(atof(buffer));
                    }
                    if (entrees.size() != (size_t)gNombreEntrees) {
                        MessageBoxA(hwnd, "Nombre d'entrées incorrect !", "Erreur", MB_OK | MB_ICONWARNING);
                        AfficherStatusCouleur(2, "Erreur: entrées incorrectes");
                        return 0;
                    }
                    double resultat = predire(gReseau, entrees);
                    char buffer[512];
                    sprintf_s(buffer, sizeof(buffer), "%.6f", resultat);
                    SetWindowTextA(gControls.textResultat, buffer);
                    Log("INFO", "Prédiction: %.6f\n", resultat);
                    AfficherStatusCouleur(1, "Prédiction réussie!");
                } else {
                    MessageBoxA(hwnd, "Chargez ou entraînez d'abord un modèle !", "Erreur", MB_OK | MB_ICONWARNING);
                    AfficherStatusCouleur(2, "Erreur: modèle non initialisé");
                }
            }
            break;
        }

        case WM_CTLCOLORSTATIC: {
            HDC hdcStatic = (HDC)wParam;
            SetBkColor(hdcStatic, RGB(255, 255, 255));
            SetTextColor(hdcStatic, RGB(50, 50, 50));
            return (LRESULT)hBrushGroup;
        }

        case WM_CTLCOLOREDIT: {
            HDC hdcEdit = (HDC)wParam;
            SetBkColor(hdcEdit, RGB(255, 255, 255));
            SetTextColor(hdcEdit, RGB(0, 0, 0));
            return (LRESULT)hBrushGroup;
        }

        case WM_DESTROY: {
            detruire_reseau(gReseau);
            DeleteObject(hBrushBackground);
            DeleteObject(hBrushGroup);
            DeleteObject(hPenBorder);
            DeleteObject(hFontTitle);
            DeleteObject(hFontNormal);
            DeleteObject(hFontSmall);
            PostQuitMessage(0);
            break;
        }
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow) {
    HWND hConsole = GetConsoleWindow();
    if (hConsole != NULL) ShowWindow(hConsole, SW_HIDE);

    srand(time(NULL));

    const wchar_t CLASS_NAME[] = L"NeuralNetworkGUI";
    WNDCLASS wc = {};
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = CLASS_NAME;
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);

    if (!RegisterClass(&wc)) {
        MessageBoxW(NULL, L"Erreur lors de l'enregistrement de la classe", L"Erreur", MB_OK | MB_ICONERROR);
        return 1;
    }

    gHwndMain = CreateWindowExW(0, CLASS_NAME, L"Réseau Neuronal", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 900, 550, NULL, NULL, hInstance, NULL);
    if (!gHwndMain) {
        MessageBoxW(NULL, L"Erreur création fenêtre", L"Erreur", MB_OK | MB_ICONERROR);
        return 1;
    }

    ShowWindow(gHwndMain, nCmdShow);
    UpdateWindow(gHwndMain);

    Log("INFO", "========== RÉSEAU NEURONAL - INTERFACE GUI ==========\n");
    Log("INFO", "Topologie: dynamique (détectée depuis data.csv)\n");
    Log("INFO", "Fichier poids: %s\n", gFichierPoids.c_str());

    MSG msg = {};
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    Log("INFO", "========== PROGRAMME TERMINÉ ==========\n");
    return (int)msg.wParam;
}
 
Joined
Jun 12, 2020
Messages
73
Reaction score
3
This code implements a Windows graphical application for training and using an artificial neural network. It is a complete interface with a graphical user interface (GUI) that allows loading data, training a neural network model, saving and loading network weights, and making predictions.
The application uses the Windows API to create a graphical window with multiple control areas. It integrates a neural network with multiple layers, supporting different activation functions (ReLU and sigmoid), as well as normalization mechanisms (Layer Normalization and Batch Normalization). The network can be configured with a dynamic topology detected from a data file.
Network training is performed through an optimization loop using gradient backpropagation. The code includes important numerical stability mechanisms such as gradient clipping, handling of NaN/Inf values, and normalization techniques to improve convergence. The interface allows real-time visualization of training progress with display of MSE (Mean Squared Error) and learning rate.
The program also handles loading and saving network weights to a CSV file, as well as loading training data from a CSV file. The graphical interface is composed of three main sections: one for model training, one for prediction, and one for displaying system status.
The input data file (data.csv) must be formatted as a CSV with column headers representing input features and an output column. Each row contains numerical values corresponding to the input variables and their expected output value. The trained weights file (poids_entraines.csv) stores the network parameters in CSV format with columns specifying the layer number, neuron index, parameter type (bias or weight), the weight index within that neuron, and the numerical value of the parameter. This structure allows complete reconstruction of the trained network model.
The code is structured with functions for each aspect of the neural network (forward propagation, backpropagation, weight updates) as well as functions dedicated to the graphical interface. It uses threads to run training in the background without blocking the user interface. Memory management is also taken into account with appropriate destruction functions to free dynamically allocated resources.
The application includes robustness mechanisms such as NaN value checking, gradient clipping, and runtime error handling. It displays colored status messages to indicate system state (success, error, warning) and provides detailed training logs in a dedicated text area.
The code is licensed under the MIT License and includes all required legal notices. It uses modern Windows programming techniques such as custom Windows messages for inter-thread communication, and custom fonts and colors to improve user interface ergonomics.
 
Joined
Sep 20, 2022
Messages
318
Reaction score
41
After reading an article about a person struggling to get a neural network to play tic-tac-toe, I can see why source code like this is public domain.
 
Joined
Jun 12, 2020
Messages
73
Reaction score
3
"We the Cypherpunks are dedicated to building anonymous systems. We are defending our privacy with cryptography, with anonymous mail forwarding systems, with digital signatures, and with electronic money.


Cypherpunks write code. We know that someone has to write software to defend privacy, and since we can’t get privacy unless we all do, we’re going to write it. We publish our code so that our fellow Cypherpunks may practice and play with it. Our code is free for all to use, worldwide. We don’t much care if you don’t approve of the software we write. We know that software can’t be destroyed and that a widely dispersed system can’t be shut down." Eric Hughes, March 9, 1993.

They build AI systems with our data; the code those AIs produce becomes open source — it's only fair.
 
Joined
Jun 12, 2020
Messages
73
Reaction score
3
Here's a console attempt at tic-tac-toe with a neural network:
C++:
/*


MIT License


Copyright (c) 2025 CoTon_TiGe_MoUaRf



Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:



The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.




THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


*/
#include <iostream>
#include <vector>
#include <cmath>
#include <random>
#include <algorithm>
#include <queue>
#include <iomanip>
#include <limits>

using namespace std;

typedef vector<vector<double>> Matrix;
typedef vector<double> Vector;

// ===== CONSTANTES GLOBALES =====
const double EPSILON = 1e-10;
const double MAX_GRADIENT_NORM = 1.0;

// ===== OPERATEURS POUR VECTEURS =====
Vector operator+(const Vector& a, const Vector& b) {
    Vector r(a.size());
    for(size_t i = 0; i < a.size(); i++) r[i] = a[i] + b[i];
    return r;
}

Vector operator-(const Vector& a, const Vector& b) {
    Vector r(a.size());
    for(size_t i = 0; i < a.size(); i++) r[i] = a[i] - b[i];
    return r;
}

Vector operator*(const Vector& a, double s) {
    Vector r(a.size());
    for(size_t i = 0; i < a.size(); i++) r[i] = a[i] * s;
    return r;
}

Vector operator*(double s, const Vector& a) {
    return a * s;
}

Vector operator*(const Vector& a, const Vector& b) {
    Vector r(a.size());
    for(size_t i = 0; i < a.size(); i++) r[i] = a[i] * b[i];
    return r;
}

double dot(const Vector& a, const Vector& b) {
    double sum = 0;
    for(size_t i = 0; i < a.size(); i++) sum += a[i] * b[i];
    return sum;
}

double norm(const Vector& a) {
    double sum = 0;
    for(size_t i = 0; i < a.size(); i++) sum += a[i] * a[i];
    return sqrt(sum);
}

// ===== MATRICES =====
Matrix MatMul(const Matrix& A, const Matrix& B) {
    Matrix C(A.size(), Vector(B[0].size(), 0));
    for(size_t i = 0; i < A.size(); i++)
        for(size_t j = 0; j < B[0].size(); j++)
            for(size_t k = 0; k < B.size(); k++)
                C[i][j] += A[i][k] * B[k][j];
    return C;
}

// ===== ACTIVATION =====
Vector ReLU(const Vector& z) {
    Vector a = z;
    for(auto& x : a) x = max(0.0, x);
    return a;
}

Vector ReLU_deriv(const Vector& z) {
    Vector d(z.size());
    for(size_t i = 0; i < z.size(); i++) d[i] = (z[i] > 0) ? 1.0 : 0.0;
    return d;
}

// ===== LOG-SUM-EXP TRICK (NUMERIQUEMENT STABLE) =====
double logsumexp(const Vector& z) {
    if(z.empty()) return 0.0;
  
    double maxZ = *max_element(z.begin(), z.end());
  
    // Vérification si maxZ est infini ou NaN
    if(!isfinite(maxZ)) return maxZ;
  
    double sum = 0.0;
    for(size_t i = 0; i < z.size(); i++) {
        double diff = z[i] - maxZ;
        // Vérifier les underflow/overflow
        if(diff < -100.0) {
            // exp(diff) ≈ 0, on ignore
        } else {
            sum += exp(diff);
        }
    }
  
    if(sum <= 0.0) return maxZ;
    return maxZ + log(sum);
}

// ===== SOFTMAX NUMERIQUEMENT STABLE =====
Vector softmax(const Vector& z) {
    Vector s(z.size());
    double maxZ = *max_element(z.begin(), z.end());
  
    // Vérification de stabilité numérique
    if(!isfinite(maxZ)) {
        fill(s.begin(), s.end(), 1.0 / z.size());
        return s;
    }
  
    double sum = 0.0;
    for(size_t i = 0; i < z.size(); i++) {
        double diff = z[i] - maxZ;
        // Clamp pour éviter les exponentielles trop grandes
        if(diff > 100.0) diff = 100.0;
        if(diff < -100.0) diff = -100.0;
      
        s[i] = exp(diff);
        sum += s[i];
    }
  
    // Normalisation avec protection contre la division par zéro
    if(sum > EPSILON) {
        for(size_t i = 0; i < z.size(); i++) {
            s[i] /= sum;
            // Clamp à [EPSILON, 1-EPSILON] pour éviter log(0)
            s[i] = max(EPSILON, min(1.0 - EPSILON, s[i]));
        }
    } else {
        fill(s.begin(), s.end(), 1.0 / z.size());
    }
  
    return s;
}

// ===== CROSS-ENTROPY LOSS NUMERIQUEMENT STABLE =====
double crossEntropyLoss(const Vector& predictions, const Vector& target) {
    double loss = 0.0;
  
    if(predictions.size() != target.size()) {
        cerr << "Erreur: tailles différentes dans crossEntropyLoss" << endl;
        return numeric_limits<double>::infinity();
    }
  
    for(size_t i = 0; i < predictions.size(); i++) {
        double pred = max(EPSILON, min(1.0 - EPSILON, predictions[i]));
      
        if(target[i] > 0.5) {
            loss -= log(pred);
        } else if(target[i] < 0.5) {
            loss -= log(1.0 - pred);
        }
    }
  
    loss /= predictions.size();
  
    if(!isfinite(loss)) {
        cerr << "Avertissement: Loss est NaN ou Inf" << endl;
        return numeric_limits<double>::infinity();
    }
  
    return loss;
}

// ===== GRADIENT CLIPPING =====
void clipGradients(Vector& gradient, double maxNorm) {
    double g_norm = norm(gradient);
  
    if(g_norm > maxNorm && g_norm > EPSILON) {
        double scale = maxNorm / g_norm;
        for(auto& g : gradient) {
            g *= scale;
        }
    }
}

// ===== NEURAL NETWORK AVEC STABILITE NUMERIQUE =====
class NeuralNet {
public:
    vector<Matrix> W;
    vector<Vector> b;
    vector<Vector> a;
    vector<Vector> z;
  
    double trainingLoss = 0.0;
  
    NeuralNet(vector<int> sizes) {
        mt19937 gen(42);
      
        for(size_t l = 1; l < sizes.size(); l++) {
            // He Initialization pour ReLU
            double var = 2.0 / sizes[l-1];
            normal_distribution<> dis(0, sqrt(var));
          
            W.push_back(Matrix(sizes[l-1], Vector(sizes[l])));
            for(auto& row : W[l-1])
                for(auto& w : row)
                    w = dis(gen);
          
            b.push_back(Vector(sizes[l], 0.0));
        }
    }
  
    Vector forward(Vector input) {
        a.clear();
        z.clear();
        a.push_back(input);
      
        for(size_t l = 0; l < W.size(); l++) {
            Vector z_l(W[l][0].size(), 0);
            for(size_t j = 0; j < W[l][0].size(); j++) {
                for(size_t i = 0; i < a[l].size(); i++)
                    z_l[j] += a[l][i] * W[l][i][j];
                z_l[j] += b[l][j];
            }
            z.push_back(z_l);
          
            if(l < W.size() - 1) {
                a.push_back(ReLU(z_l));
            } else {
                a.push_back(softmax(z_l));
            }
        }
        return a.back();
    }
  
    void backward(Vector target, double lr, int batch_size) {
        int L = W.size();
      
        // Calcul de la loss pour monitoring
        trainingLoss = crossEntropyLoss(a[L], target);
      
        // Delta pour softmax + cross-entropy
        Vector delta = a[L] - target;
      
        // Clipping du delta
        clipGradients(delta, MAX_GRADIENT_NORM);
      
        for(int l = L - 1; l >= 0; l--) {
            Matrix dW(W[l].size(), Vector(W[l][0].size(), 0));
            Vector db(b[l].size(), 0);
          
            // Calcul des gradients
            for(size_t i = 0; i < W[l].size(); i++) {
                for(size_t j = 0; j < W[l][0].size(); j++) {
                    dW[i][j] = a[l][i] * delta[j] / batch_size;
                  
                    // Clipping du gradient
                    dW[i][j] = max(-1.0, min(1.0, dW[i][j]));
                  
                    // Mise à jour des poids
                    W[l][i][j] -= lr * dW[i][j];
                }
            }
          
            // Gradient du biais
            for(size_t j = 0; j < delta.size(); j++) {
                db[j] = delta[j] / batch_size;
                db[j] = max(-1.0, min(1.0, db[j]));
                b[l][j] -= lr * db[j];
            }
          
            // Rétropropagation
            if(l > 0) {
                Vector new_delta(W[l].size(), 0);
                for(size_t i = 0; i < W[l].size(); i++) {
                    for(size_t j = 0; j < delta.size(); j++) {
                        new_delta[i] += delta[j] * W[l][i][j];
                    }
                    new_delta[i] *= (z[l-1][i] > 0 ? 1.0 : 0.0);
                }
                delta = new_delta;
              
                // Clipping du delta rétropropagé
                clipGradients(delta, MAX_GRADIENT_NORM);
            }
        }
    }
};

// ===== TIC-TAC-TOE =====
class TicTacToe {
public:
    vector<int> board;
  
    TicTacToe() : board(9, 0) {}
  
    Vector getBoardState() {
        Vector state(9);
        for(int i = 0; i < 9; i++) {
            if(board[i] == 0) state[i] = 0.0;
            else state[i] = board[i] == 1 ? 0.5 : -0.5;
        }
        return state;
    }
  
    bool makeMove(int pos, int player) {
        if(board[pos] != 0) return false;
        board[pos] = player;
        return true;
    }
  
    int checkWin() {
        int lines[8][3] = {{0,1,2},{3,4,5},{6,7,8},{0,3,6},{1,4,7},{2,5,8},{0,4,8},{2,4,6}};
        for(auto& line : lines) {
            if(board[line[0]] && board[line[0]] == board[line[1]] && board[line[1]] == board[line[2]])
                return board[line[0]];
        }
        return 0;
    }
  
    bool isFull() {
        for(int x : board) if(x == 0) return false;
        return true;
    }
  
    int selectMove(NeuralNet& net, bool useRandomness = false) {
        Vector probs = net.forward(getBoardState());
        int best = -1;
        double maxQ = -1e9;
      
        mt19937 gen(random_device{}());
        uniform_real_distribution<> dis(0, 1);
      
        for(int i = 0; i < 9; i++) {
            if(board[i] == 0) {
                if(useRandomness && dis(gen) < 0.1) {
                    best = i;
                    break;
                }
                if(probs[i] > maxQ) {
                    maxQ = probs[i];
                    best = i;
                }
            }
        }
        return best;
    }
  
    void printBoard() {
        cout << "\nTableau:\n";
        for(int i = 0; i < 3; i++) {
            for(int j = 0; j < 3; j++) {
                cout << (board[i*3+j] == 0 ? '.' : (board[i*3+j] == 1 ? 'X' : 'O')) << " ";
            }
            cout << "\n";
        }
    }
};

// ===== STRUCTURE DE RESULTAT DE JEU =====
struct GameResult {
    vector<Vector> states;
    vector<int> moves;
    int winner;
};

// ===== JEU EN SELF-PLAY =====
GameResult playSelfGame(NeuralNet& net, bool displayBoard = false) {
    GameResult result;
    result.winner = 0;
    TicTacToe game;
    int turn = 0;
  
    while(!game.isFull() && game.checkWin() == 0) {
        Vector state = game.getBoardState();
        int move = game.selectMove(net, true);
      
        if(move == -1) break;
      
        result.states.push_back(state);
        result.moves.push_back(move);
        game.makeMove(move, (turn % 2) + 1);
      
        if(displayBoard) {
            cout << "Tour " << (turn + 1) << " - Joueur " << ((turn % 2) + 1)
                 << " joue position " << move << "\n";
            game.printBoard();
        }
      
        turn++;
    }
  
    result.winner = game.checkWin();
    return result;
}

// ===== ENTRAINEMENT PAR SELF-PLAY AVEC MONITORING DE LOSS =====
void trainFromSelfPlay(NeuralNet& net, int selfPlayIterations, bool displayGames = false) {
    cout << "\n" << string(80, '=') << "\n";
    cout << "ENTRAINEMENT PAR SELF-PLAY - MODE STABLE\n";
    cout << string(80, '=') << "\n\n";
  
    vector<double> lossHistory;
    vector<double> accuracyHistory;
    vector<int> iterationNumbers;
  
    // Paramètres d'entraînement adaptés
    double learningRate = 0.0001;  // RÉDUIT: 0.01 -> 0.0001
    int batchSize = 16;            // Batch réel pour réduire le bruit
    int divergenceCounter = 0;     // Compteur de divergence
    double maxAllowedLoss = 10.0;  // Seuil de détection de divergence
  
    vector<Vector> batchStates;
    vector<Vector> batchTargets;
    vector<double> batchLosses;
  
    cout << "Paramètres:\n";
    cout << "  • Taux d'apprentissage: " << learningRate << " (RÉDUIT)\n";
    cout << "  • Taille de batch: " << batchSize << "\n";
    cout << "  • Détection de divergence: Loss > " << maxAllowedLoss << "\n\n";
  
    for(int iter = 0; iter < selfPlayIterations; iter++) {
        GameResult game = playSelfGame(net, displayGames);
      
        // Accumulation du batch
        for(size_t i = 0; i < game.states.size(); i++) {
            Vector target(9, 0.0);
            int player = i % 2 == 0 ? 1 : 2;
          
            // Stratégie de récompense améliorée
            if(game.winner == player) {
                target[game.moves[i]] = 0.9;  // Victoire
            } else if(game.winner == 0) {
                target[game.moves[i]] = 0.5;  // Match nul
            } else {
                target[game.moves[i]] = 0.1;  // Défaite
            }
          
            batchStates.push_back(game.states[i]);
            batchTargets.push_back(target);
        }
      
        // Traitement du batch quand il est plein
        if(batchStates.size() >= (size_t)batchSize || iter == selfPlayIterations - 1) {
            batchLosses.clear();
          
            // Passage forward-backward sur tout le batch
            for(size_t j = 0; j < batchStates.size(); j++) {
                Vector pred = net.forward(batchStates[j]);
                net.backward(batchTargets[j], learningRate, batchSize);
                batchLosses.push_back(net.trainingLoss);
              
                // Vérification de divergence en temps réel
                if(!isfinite(net.trainingLoss) || net.trainingLoss > maxAllowedLoss) {
                    divergenceCounter++;
                } else {
                    divergenceCounter = max(0, divergenceCounter - 1);
                }
              
                // Arrêt d'urgence si divergence détectée
                if(divergenceCounter > 5) {
                    cout << "\n⚠ DIVERGENCE DÉTECTÉE À L'ITÉRATION " << iter << "!\n";
                    cout << "Loss: " << net.trainingLoss << " (seuil: " << maxAllowedLoss << ")\n";
                    cout << "L'entraînement s'arrête pour éviter les dégâts.\n\n";
                    goto training_end;
                }
            }
          
            // Calcul de la loss moyenne du batch
            double avgBatchLoss = 0.0;
            for(double loss : batchLosses) avgBatchLoss += loss;
            avgBatchLoss /= batchLosses.size();
          
            // Réinitialisation du batch
            batchStates.clear();
            batchTargets.clear();
        }
      
        // Monitoring tous les 10 itérations
        if((iter + 1) % 10 == 0) {
            double avgLoss = 0.0;
            for(double loss : batchLosses) avgLoss += loss;
            if(!batchLosses.empty()) avgLoss /= batchLosses.size();
          
            // Test de performance
            int player1Wins = 0;
            int draws = 0;
            int player2Wins = 0;
          
            for(int testGame = 0; testGame < 50; testGame++) {
                GameResult testResult = playSelfGame(net, false);
                if(testResult.winner == 1) {
                    player1Wins++;
                } else if(testResult.winner == 0) {
                    draws++;
                } else {
                    player2Wins++;
                }
            }
          
            double player1WinRate = (player1Wins / 50.0) * 100.0;
            double drawRate = (draws / 50.0) * 100.0;
          
            iterationNumbers.push_back(iter + 1);
            lossHistory.push_back(avgLoss);
            accuracyHistory.push_back(player1WinRate);
          
            // Affichage avec détection de problème
            string lossStatus = "✓";
            if(avgLoss > 1.0) lossStatus = "⚠";
            if(avgLoss > 2.0) lossStatus = "✗";
          
            cout << "┌─────────────────────────────────────────────────────────────────┐\n";
            cout << "│ Itération: " << setw(5) << (iter + 1) << " / " << selfPlayIterations
                 << string(42, ' ') << "│\n";
            cout << "├─────────────────────────────────────────────────────────────────┤\n";
            cout << "│ Loss moyenne: " << lossStatus << " " << fixed << setprecision(6)
                 << setw(10) << avgLoss << string(46, ' ') << "│\n";
            cout << "├─────────────────────────────────────────────────────────────────┤\n";
            cout << "│ Statistiques de test (50 jeux):                                 │\n";
            cout << "│   • Joueur 1 victoires: " << setw(3) << player1Wins << " ("
                 << fixed << setprecision(1) << setw(5) << player1WinRate << "%)";
            cout << string(22, ' ') << "│\n";
            cout << "│   • Matchs nuls:       " << setw(3) << draws << " ("
                 << fixed << setprecision(1) << setw(5) << drawRate << "%)";
            cout << string(22, ' ') << "│\n";
            cout << "│   • Joueur 2 victoires: " << setw(3) << player2Wins;
            cout << string(45, ' ') << "│\n";
            cout << "└─────────────────────────────────────────────────────────────────┘\n\n";
        }
    }
  
    training_end:
  
    // Affichage final du résumé
    cout << "\n" << string(80, '=') << "\n";
    cout << "RESUME DE L'ENTRAÎNEMENT\n";
    cout << string(80, '=') << "\n\n";
  
    cout << "Itération | Loss Moyenne | Statut   | Taux Victoire J1 (%)   Évolution\n";
    cout << string(80, '-') << "\n";
  
    for(size_t i = 0; i < iterationNumbers.size(); i++) {
        cout << setw(9) << iterationNumbers[i] << " | ";
        cout << fixed << setprecision(8) << setw(12) << lossHistory[i] << " | ";
      
        // Statut de la loss
        string status = "✓ BON   ";
        if(lossHistory[i] > 1.0) status = "⚠ MOYEN";
        if(lossHistory[i] > 2.0) status = "✗ MAUVAIS";
        cout << status << " | ";
      
        cout << fixed << setprecision(2) << setw(20) << accuracyHistory[i] << " | ";
      
        int barLength = static_cast<int>(accuracyHistory[i] / 5.0);
        for(int j = 0; j < barLength; j++) cout << "█";
        cout << "\n";
    }
  
    cout << "\n" << string(80, '=') << "\n";
    cout << "ANALYSE FINALE\n";
    cout << string(80, '=') << "\n\n";
  
    if(!lossHistory.empty()) {
        double minLoss = *min_element(lossHistory.begin(), lossHistory.end());
        double maxLoss = *max_element(lossHistory.begin(), lossHistory.end());
        double avgLoss = 0.0;
        for(double loss : lossHistory) avgLoss += loss;
        avgLoss /= lossHistory.size();
      
        double minWinRate = *min_element(accuracyHistory.begin(), accuracyHistory.end());
        double maxWinRate = *max_element(accuracyHistory.begin(), accuracyHistory.end());
        double avgWinRate = 0.0;
        for(double rate : accuracyHistory) avgWinRate += rate;
        avgWinRate /= accuracyHistory.size();
      
        cout << "Statistiques de Loss:\n";
        cout << "  • Minimum: " << fixed << setprecision(8) << minLoss << "\n";
        cout << "  • Maximum: " << fixed << setprecision(8) << maxLoss << "\n";
        cout << "  • Moyenne:  " << fixed << setprecision(8) << avgLoss << "\n\n";
      
        cout << "Statistiques du Taux de Victoire (%):\n";
        cout << "  • Minimum: " << fixed << setprecision(2) << minWinRate << "%\n";
        cout << "  • Maximum: " << fixed << setprecision(2) << maxWinRate << "%\n";
        cout << "  • Moyenne:  " << fixed << setprecision(2) << avgWinRate << "%\n\n";
      
        double lossReduction = ((lossHistory[0] - lossHistory.back()) / lossHistory[0]) * 100.0;
        double winRateGain = accuracyHistory.back() - accuracyHistory[0];
      
        cout << "Évolution globale:\n";
        cout << "  • Réduction de Loss: " << fixed << setprecision(2) << lossReduction << "%\n";
        cout << "  • Gain de Victoires: " << fixed << setprecision(2) << winRateGain << "%\n\n";
      
        // Diagnose finale
        cout << "Diagnostic:\n";
        if(avgLoss < 0.5) {
            cout << "  ✓ Entraînement EXCELLENT - Loss très stable et basse\n";
        } else if(avgLoss < 1.0) {
            cout << "  ✓ Entraînement BON - Loss acceptable\n";
        } else if(avgLoss < 2.0) {
            cout << "  ⚠ Entraînement INSTABLE - Loss élevée, convergence difficile\n";
        } else {
            cout << "  ✗ Entraînement DIVERGE - Loss très élevée, problème majeur\n";
        }
      
        if(avgWinRate > 60.0) {
            cout << "  ✓ Performance EXCELLENTE - Taux de victoire > 60%\n";
        } else if(avgWinRate > 50.0) {
            cout << "  ✓ Performance BONNE - Taux de victoire > 50%\n";
        } else {
            cout << "  ⚠ Performance FAIBLE - Taux de victoire < 50%\n";
        }
    }
  
    cout << "\n";
}


// ===== MAIN =====
int main() {
    cout << "\n" << string(80, '=') << "\n";
    cout << "RESEAU DE NEURONES - TIC-TAC-TOE AVEC STABILITE NUMERIQUE\n";
    cout << string(80, '=') << "\n";
  
    // Création du réseau de neurones
    // Architecture: 9 entrées -> 64 neurones -> 32 neurones -> 9 sorties
    NeuralNet net({9, 64, 32, 9});
  
    cout << "\nArchitecture du réseau:\n";
    cout << "  • Couche d'entrée: 9 neurones (état du tableau 3x3)\n";
    cout << "  • Couche cachée 1: 64 neurones (activation ReLU)\n";
    cout << "  • Couche cachée 2: 32 neurones (activation ReLU)\n";
    cout << "  • Couche de sortie: 9 neurones (activation Softmax)\n";
    cout << "\nParamètres d'entraînement:\n";
    cout << "  • Taux d'apprentissage: 0.01\n";
    cout << "  • Nombre d'itérations: 500\n";
    cout << "  • Clipping de gradient: Oui (max norme = 1.0)\n";
    cout << "  • Stabilité numérique: Oui (epsilon = 1e-10)\n";
    cout << "\n";
  
    // Entraînement avec self-play
    trainFromSelfPlay(net, 500, false);
  
    // Démonstration avec une partie
    cout << "\n" << string(80, '=') << "\n";
    cout << "DEMONSTRATION - PARTIE DE JEU\n";
    cout << string(80, '=') << "\n";
  
    TicTacToe demoGame;
    int turn = 0;
  
    cout << "\nEtat initial du plateau:\n";
    demoGame.printBoard();
  
    while(!demoGame.isFull() && demoGame.checkWin() == 0) {
        int move = demoGame.selectMove(net, false);
      
        if(move == -1) {
            cout << "\nErreur: pas de coup valide disponible!\n";
            break;
        }
      
        int player = (turn % 2) + 1;
        demoGame.makeMove(move, player);
      
        cout << "\nCoup " << (turn + 1) << ":\n";
        cout << "  • Joueur: " << player << " (" << (player == 1 ? "X" : "O") << ")\n";
        cout << "  • Position: " << move << "\n";
        demoGame.printBoard();
      
        turn++;
    }
  
    cout << "\n" << string(80, '-') << "\n";
    cout << "RESULTAT FINAL:\n";
    cout << string(80, '-') << "\n";
  
    int winner = demoGame.checkWin();
    demoGame.printBoard();
  
    if(winner == 0) {
        cout << "\n✓ MATCH NUL (Égalité)\n";
        cout << "Les deux joueurs ont joué de manière optimale!\n";
    } else {
        cout << "\n✓ JOUEUR " << winner << " GAGNE!\n";
        cout << "Le joueur " << winner << " (" << (winner == 1 ? "X" : "O") << ") a remporté la victoire!\n";
    }
  
    cout << "\n" << string(80, '=') << "\n";
    cout << "ENTRAÎNEMENT TERMINE\n";
    cout << string(80, '=') << "\n\n";
  
    return 0;
}
I need to be honest: the convergence of my neural network is unstable and performance shows no clear improvement trend. This indicates that the system is not optimized to fully exploit the potential of the neural network as it is currently implemented.
Before using this code in production or as a basis for another project, I strongly advise you to integrate the stabilization techniques I've identified: a replay buffer, a target network, and adaptive optimizers like Adam. With these improvements, you should observe smoother network convergence and a win rate approaching 70-80%, or even higher.
It's an excellent starting point for exploring in greater depth how to get a neural network to learn to play more complex games. But in its current state, the code has obvious limitations that need to be fixed to achieve real performance.
Feel free to improve this implementation — contributions are welcome!
 
Joined
Jun 12, 2020
Messages
73
Reaction score
3
By creating the dataset (CSV file) with this C++ code for Tic-Tac-Toe :
C++:
/*


MIT License


Copyright (c) 2025 CoTon_TiGe_MoUaRf



Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:



The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.




THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


*/
#include <iostream>
#include <vector>
#include <fstream>
#include <sstream>
#include <cmath>
#include <algorithm>
#include <random>
#include <iomanip>
#include <cstring>

using namespace std;

// ===== TYPES DE BASE =====
typedef vector<double> Vector;

// ===== STRUCTURE POUR COLLECTER LES DONNEES =====
struct GameResult {
    vector<Vector> states;
    vector<int> moves;
    vector<int> players;      // Quel joueur a fait chaque coup
    int winner;
};

// ===== CLASSE POUR COLLECTER ET SAUVEGARDER LES DONNEES =====
class DatasetCollector {
private:
    string filename;
    vector<pair<Vector, int>> data;  // (state, optimal_move_position)
   
public:
    DatasetCollector(string fname) : filename(fname) {}
   
    // Ajouter un coup joué au dataset
    // Le coup sera stocké directement comme un entier (0-8)
    void addGameSample(const Vector& state, int move) {
        Vector stateVec = state;
       
        // Valider le coup
        if(move < 0 || move > 8) {
            cerr << "Erreur: coup invalide " << move << "\n";
            return;
        }
       
        data.push_back({stateVec, move});
    }
   
    // Sauvegarder dans un CSV
    void saveToCSV() {
        ofstream file(filename);
       
        if(!file.is_open()) {
            cerr << "Erreur: impossible d'ouvrir " << filename << "\n";
            return;
        }
       
        // Entête du CSV : état (9 colonnes) + position optimale (1 colonne)
        file << "state_0,state_1,state_2,state_3,state_4,state_5,state_6,state_7,state_8,optimal_move\n";
       
        // Données
        for(const auto& sample : data) {
            const Vector& state = sample.first;
            int move = sample.second;
           
            // Écrire l'état (9 cases)
            for(int i = 0; i < 9; i++) {
                file << fixed << setprecision(2) << state[i];
                file << ",";
            }
           
            // Écrire la position optimale directement (1 seule colonne)
            file << move << "\n";
        }
       
        file.close();
        cout << "\n✓ Dataset sauvegardé: " << filename << " (" << data.size() << " samples)\n";
    }
   
    // Obtenir la taille du dataset
    size_t getSize() const {
        return data.size();
    }
   
    // Effacer les données
    void clear() {
        data.clear();
    }
};

// ===== TIC-TAC-TOE =====
class TicTacToe {
public:
    vector<int> board;
   
    TicTacToe() : board(9, 0) {}
   
    Vector getBoardState() {
        Vector state(9);
        for(int i = 0; i < 9; i++) {
            if(board[i] == 0) state[i] = 0.0;
            else state[i] = board[i] == 1 ? 0.5 : -0.5;
        }
        return state;
    }
   
    bool makeMove(int pos, int player) {
        if(board[pos] != 0) return false;
        board[pos] = player;
        return true;
    }
   
    int checkWin() {
        int lines[8][3] = {{0,1,2},{3,4,5},{6,7,8},{0,3,6},{1,4,7},{2,5,8},{0,4,8},{2,4,6}};
        for(auto& line : lines) {
            if(board[line[0]] && board[line[0]] == board[line[1]] && board[line[1]] == board[line[2]])
                return board[line[0]];
        }
        return 0;
    }
   
    bool isFull() {
        for(int x : board) if(x == 0) return false;
        return true;
    }
   
    // Retourner tous les coups possibles
    vector<int> getPossibleMoves() {
        vector<int> moves;
        for(int i = 0; i < 9; i++) {
            if(board[i] == 0) moves.push_back(i);
        }
        return moves;
    }
   
    int selectMove(bool useRandomness = false) {
        // Sélection aléatoire simple pour le self-play
        vector<int> possibleMoves = getPossibleMoves();
        if(possibleMoves.empty()) return -1;
       
        mt19937 gen(random_device{}());
        uniform_int_distribution<> dis(0, possibleMoves.size() - 1);
       
        return possibleMoves[dis(gen)];
    }
   
    void printBoard() {
        cout << "\nTableau:\n";
        for(int i = 0; i < 3; i++) {
            for(int j = 0; j < 3; j++) {
                cout << (board[i*3+j] == 0 ? '.' : (board[i*3+j] == 1 ? 'X' : 'O')) << " ";
            }
            cout << "\n";
        }
    }
};

// ===== JEU EN SELF-PLAY AVEC COLLECTION DE DONNEES =====
GameResult playSelfGame(DatasetCollector* collector = nullptr, bool displayBoard = false) {
    GameResult result;
    result.winner = 0;
    TicTacToe game;
    int turn = 0;
   
    while(!game.isFull() && game.checkWin() == 0) {
        Vector state = game.getBoardState();
        int move = game.selectMove(true);
        int player = (turn % 2) + 1;
       
        if(move == -1) break;
       
        result.states.push_back(state);
        result.moves.push_back(move);
        result.players.push_back(player);
       
        game.makeMove(move, player);
       
        if(displayBoard) {
            cout << "Tour " << (turn + 1) << " - Joueur " << player
                 << " joue position " << move << "\n";
            game.printBoard();
        }
       
        turn++;
    }
   
    result.winner = game.checkWin();
   
    // Ajouter tous les coups au dataset
    // Chaque coup est stocké comme sa position (0-8)
    if(collector != nullptr) {
        for(size_t i = 0; i < result.states.size(); i++) {
            collector->addGameSample(result.states[i], result.moves[i]);
        }
    }
   
    return result;
}

// ===== GENERATION DU DATASET =====
void generateDataset(int numberOfGames, string csvFilename = "tictactoe_dataset.csv") {
    cout << "\n" << string(80, '=') << "\n";
    cout << "GENERATION DU DATASET PAR SELF-PLAY\n";
    cout << "CIBLE: Prédire la position optimale à jouer\n";
    cout << string(80, '=') << "\n\n";
   
    DatasetCollector collector(csvFilename);
   
    cout << "Paramètres:\n";
    cout << "  • Nombre de parties: " << numberOfGames << "\n";
    cout << "  • Fichier de sortie: " << csvFilename << "\n";
    cout << "  • Format: state (9 entrées) → position optimale (1 sortie, valeur 0-8)\n\n";
   
    int player1Wins = 0;
    int player2Wins = 0;
    int draws = 0;
   
    for(int game = 0; game < numberOfGames; game++) {
        GameResult result = playSelfGame(&collector, false);
       
        if(result.winner == 1) {
            player1Wins++;
        } else if(result.winner == 2) {
            player2Wins++;
        } else {
            draws++;
        }
       
        // Affichage du progrès
        if((game + 1) % 100 == 0) {
            cout << "Parties générées: " << game + 1 << " / " << numberOfGames;
            cout << " | Samples collectés: " << collector.getSize() << "\n";
        }
    }
   
    // Sauvegarder le dataset
    collector.saveToCSV();
   
    // Affichage des statistiques
    cout << "\n" << string(80, '=') << "\n";
    cout << "STATISTIQUES DU DATASET\n";
    cout << string(80, '=') << "\n\n";
   
    cout << "Résultats des " << numberOfGames << " parties:\n";
    cout << "  • Joueur 1 victoires: " << player1Wins
         << " (" << fixed << setprecision(2) << (player1Wins * 100.0 / numberOfGames) << "%)\n";
    cout << "  • Joueur 2 victoires: " << player2Wins
         << " (" << fixed << setprecision(2) << (player2Wins * 100.0 / numberOfGames) << "%)\n";
    cout << "  • Matchs nuls: " << draws
         << " (" << fixed << setprecision(2) << (draws * 100.0 / numberOfGames) << "%)\n\n";
   
    cout << "Données du dataset:\n";
    cout << "  • Nombre total de samples: " << collector.getSize() << "\n";
    cout << "  • Coups en moyenne par partie: "
         << fixed << setprecision(2) << (collector.getSize() / (double)numberOfGames) << "\n\n";
   
    cout << "Structure du CSV:\n";
    cout << "  • Entrées: state_0 à state_8 (9 colonnes)\n";
    cout << "  • Sorties: optimal_move (1 colonne, valeur: 0-8)\n";
    cout << "  • Total: 10 colonnes\n\n";
}

// ===== FONCTION PRINCIPALE =====
int main() {
    cout << "╔════════════════════════════════════════════════════════════════════════════╗\n";
    cout << "║     GENERATEUR DE DATASET CSV POUR TIC-TAC-TOE - PREDICTION DE POSITION   ║\n";
    cout << "╚════════════════════════════════════════════════════════════════════════════╝\n";
   
    // Générer le dataset
    generateDataset(1000, "tictactoe_dataset.csv");
   
    cout << "\n✓ Génération terminée avec succès!\n";
    cout << "\n📊 Structure du fichier CSV:\n\n";
    cout << "ENTRÉES (état du plateau):\n";
    cout << "  • state_0 à state_8: État du plateau\n";
    cout << "    - 0.0 = case vide\n";
    cout << "    - 0.5 = joueur 1 (X)\n";
    cout << "    - -0.5 = joueur 2 (O)\n\n";
   
    cout << "SORTIE (position optimale à jouer):\n";
    cout << "  • optimal_move: Position à jouer (0-8)\n";
    cout << "    - 0 = coin haut-gauche\n";
    cout << "    - 4 = centre\n";
    cout << "    - 8 = coin bas-droit\n\n";
   
    cout << "Disposition des positions:\n";
    cout << "    0 | 1 | 2\n";
    cout << "   -----------\n";
    cout << "    3 | 4 | 5\n";
    cout << "   -----------\n";
    cout << "    6 | 7 | 8\n\n";
   
    cout << "Exemple de ligne:\n";
    cout << "  0.00,0.50,0.00,0.00,0.00,0.00,-0.50,0.00,0.00,3\n";
    cout << "  └─ état du plateau ─────────────────────────┘ └─ position 3 ─┘\n\n";
   
    return 0;
}
and with the C++ code for the neural network using the Windows window, I have these trained weights: <model>
couche,neurone,type,index,valeur
1,0,biais,0,1.7929254897126
1,1,biais,0,-2.28100737222837
1,2,biais,0,-1.45885010387544
1,3,biais,0,-0.448669957312977
1,4,biais,0,1.10777989796689
1,5,biais,0,0.307178279297945
1,6,biais,0,-0.26381828779863
1,7,biais,0,0.0762788808960881
1,8,biais,0,-1.56236008203853
1,9,biais,0,0.965202102156789
1,10,biais,0,-0.230703110545413
1,11,biais,0,-0.170770815432905
1,12,biais,0,-0.5833927510645
1,13,biais,0,0.754824660088724
1,14,biais,0,-0.375368647967904
1,15,biais,0,-0.7106267407768
1,0,poids,0,3.22467954432044
1,0,poids,1,3.1105862082543
1,0,poids,2,0.24236720227691
1,0,poids,3,0.819559003553526
1,0,poids,4,-0.774535973372085
1,0,poids,5,0.512662751007481
1,0,poids,6,0.662062508757458
1,0,poids,7,-0.0519153110243333
1,0,poids,8,-0.938760387304762
1,1,poids,0,2.75282895295435
1,1,poids,1,1.88378910540666
1,1,poids,2,2.32632135384281
1,1,poids,3,1.04576533349142
1,1,poids,4,1.53262696438493
1,1,poids,5,-0.290832270167253
1,1,poids,6,1.47459090925816
1,1,poids,7,-5.33244322398222
1,1,poids,8,3.10065718148937
1,2,poids,0,-3.34965002173174
1,2,poids,1,3.51812129689435
1,2,poids,2,0.443035344757484
1,2,poids,3,-0.646802874736355
1,2,poids,4,0.294215170634009
1,2,poids,5,-0.994730485789258
1,2,poids,6,0.0756108490692404
1,2,poids,7,0.0619041645922402
1,2,poids,8,-0.26463238979115
1,3,poids,0,1.26342857131152
1,3,poids,1,1.1310528531334
1,3,poids,2,-0.335314745003135
1,3,poids,3,0.387391559713451
1,3,poids,4,0.254361153854737
1,3,poids,5,-0.173798284680114
1,3,poids,6,1.32136053799555
1,3,poids,7,1.78906932726762
1,3,poids,8,-7.76267611790136
1,4,poids,0,-0.788739269928674
1,4,poids,1,-2.4488417558445
1,4,poids,2,-0.112780828579451
1,4,poids,3,0.513431052870926
1,4,poids,4,-1.39360029289619
1,4,poids,5,-1.22554738380348
1,4,poids,6,-0.147484996897214
1,4,poids,7,0.20341810119419
1,4,poids,8,-2.56136931570218
1,5,poids,0,0.180513165851888
1,5,poids,1,0.723314028006741
1,5,poids,2,0.431048751457672
1,5,poids,3,-0.580029687506505
1,5,poids,4,0.722930497433505
1,5,poids,5,1.4339990356853
1,5,poids,6,3.14958688355722
1,5,poids,7,-2.36106681210298
1,5,poids,8,1.33750443065497
1,6,poids,0,-1.87566192319345
1,6,poids,1,0.308824436315334
1,6,poids,2,1.04379029065533
1,6,poids,3,0.653117470492558
1,6,poids,4,-0.27339752941268
1,6,poids,5,-0.419705612550618
1,6,poids,6,-8.21508662563322
1,6,poids,7,1.90934871998601
1,6,poids,8,0.561933369764381
1,7,poids,0,1.11677100001057
1,7,poids,1,0.796996669729662
1,7,poids,2,0.931232419259764
1,7,poids,3,1.84693990831882
1,7,poids,4,2.76788496378852
1,7,poids,5,0.135859106408159
1,7,poids,6,3.69850371323109
1,7,poids,7,1.50934142753747
1,7,poids,8,0.294745727458693
1,8,poids,0,-0.803418195650136
1,8,poids,1,2.73352875971137
1,8,poids,2,-2.48666245432531
1,8,poids,3,-0.826895141376698
1,8,poids,4,-4.11658174131693
1,8,poids,5,2.47326003403064
1,8,poids,6,1.35773535191808
1,8,poids,7,-0.99151465004579
1,8,poids,8,-2.46899940604869
1,9,poids,0,-0.654675567383058
1,9,poids,1,-1.26014658642933
1,9,poids,2,-1.34139787095006
1,9,poids,3,-2.82377569945747
1,9,poids,4,3.13800653524233
1,9,poids,5,0.403008990395626
1,9,poids,6,-1.15817590110913
1,9,poids,7,0.69929406823742
1,9,poids,8,-0.0761357973296725
1,10,poids,0,0.854618223592829
1,10,poids,1,-0.319402776606702
1,10,poids,2,4.7632969947419
1,10,poids,3,0.355418210765075
1,10,poids,4,1.95777417223403
1,10,poids,5,-0.697144517160271
1,10,poids,6,-0.710487800523222
1,10,poids,7,0.613891133067672
1,10,poids,8,1.90863785199787
1,11,poids,0,1.59015231467316
1,11,poids,1,-0.806895982926761
1,11,poids,2,3.00329815526845
1,11,poids,3,0.334856259815088
1,11,poids,4,-0.593634173935802
1,11,poids,5,-4.52841455210184
1,11,poids,6,2.95319609660956
1,11,poids,7,4.51323518592851
1,11,poids,8,1.85250166784185
1,12,poids,0,0.885939568582664
1,12,poids,1,-0.235549628438258
1,12,poids,2,0.26534563137559
1,12,poids,3,3.94699889137623
1,12,poids,4,1.87162430525915
1,12,poids,5,0.889214603212058
1,12,poids,6,-3.27497059592628
1,12,poids,7,0.198870763107947
1,12,poids,8,1.61181936106502
1,13,poids,0,0.377829470409502
1,13,poids,1,-3.23314478857843
1,13,poids,2,-0.135906826079258
1,13,poids,3,-1.77560479623907
1,13,poids,4,-1.62894072774963
1,13,poids,5,-1.42959123429857
1,13,poids,6,0.438176062570615
1,13,poids,7,1.89419342358622
1,13,poids,8,-0.0822177451198489
1,14,poids,0,1.80139054065144
1,14,poids,1,2.0603680018311
1,14,poids,2,-2.86760360622935
1,14,poids,3,-0.193679639626055
1,14,poids,4,1.84227448226811
1,14,poids,5,-1.82130552755734
1,14,poids,6,1.76661543657618
1,14,poids,7,1.99434054524194
1,14,poids,8,-1.68264576290655
1,15,poids,0,-4.65474611859112
1,15,poids,1,1.05171124643496
1,15,poids,2,-0.0911369158871167
1,15,poids,3,1.37418926815763
1,15,poids,4,-0.519145236376704
1,15,poids,5,-0.0472890184228827
1,15,poids,6,-0.365341900618751
1,15,poids,7,0.45536875710921
1,15,poids,8,-0.562685219571317
2,0,biais,0,-0.211388949232489
2,1,biais,0,-4.83320921723155
2,2,biais,0,-5.23981049243621
2,3,biais,0,-0.561618338593224
2,4,biais,0,2.04487705816386
2,5,biais,0,-1.05205369993156
2,6,biais,0,-0.317816499491486
2,7,biais,0,-4.36049488228335
2,0,poids,0,0.104640605399305
2,0,poids,1,-1.4248901786601
2,0,poids,2,-0.895919453687598
2,0,poids,3,-0.276910553369455
2,0,poids,4,-0.116709287201237
2,0,poids,5,0.667047932802049
2,0,poids,6,1.05094562950805
2,0,poids,7,0.20413262797285
2,0,poids,8,-0.320732041035822
2,0,poids,9,0.246922586432041
2,0,poids,10,-0.647426922989335
2,0,poids,11,0.119633922496524
2,0,poids,12,0.190750473271737
2,0,poids,13,0.255815682909085
2,0,poids,14,0.717504415407077
2,0,poids,15,0.689013373199718
2,1,poids,0,0.493627881395398
2,1,poids,1,-0.181864854116289
2,1,poids,2,0.00184867568845463
2,1,poids,3,-0.493417775109624
2,1,poids,4,1.44452482121258
2,1,poids,5,0.86315240895951
2,1,poids,6,0.125705833510289
2,1,poids,7,-0.181016935754244
2,1,poids,8,0.319647976917211
2,1,poids,9,-4.28147744937904
2,1,poids,10,-6.5890080952314
2,1,poids,11,0.426023488659952
2,1,poids,12,0.813073866658418
2,1,poids,13,-0.0269438136760332
2,1,poids,14,-1.7710716691273
2,1,poids,15,0.656358174808599
2,2,poids,0,-0.888815265506055
2,2,poids,1,0.724185447102535
2,2,poids,2,-0.746872958386476
2,2,poids,3,1.64695535405765
2,2,poids,4,-1.80195111304296
2,2,poids,5,0.781574110555181
2,2,poids,6,2.06590630606607
2,2,poids,7,1.52931485696489
2,2,poids,8,1.39427455072164
2,2,poids,9,1.23024797716442
2,2,poids,10,-1.90588409297658
2,2,poids,11,1.98051014936101
2,2,poids,12,-0.0218518247658281
2,2,poids,13,-1.37266473739019
2,2,poids,14,-1.39850381801595
2,2,poids,15,-1.11534640209227
2,3,poids,0,0.44699248899271
2,3,poids,1,-0.317577450082231
2,3,poids,2,1.59694229045732
2,3,poids,3,-0.155502466954872
2,3,poids,4,0.138629978975093
2,3,poids,5,-0.906726446390459
2,3,poids,6,0.682835970976186
2,3,poids,7,0.1514110707496
2,3,poids,8,1.82665345277616
2,3,poids,9,1.23973566252655
2,3,poids,10,0.202844911239856
2,3,poids,11,0.586582611269945
2,3,poids,12,-0.712490457044722
2,3,poids,13,-1.47059658995191
2,3,poids,14,-1.01729216573022
2,3,poids,15,-0.711974134274543
2,4,poids,0,0.30435529221882
2,4,poids,1,0.134881663761221
2,4,poids,2,-0.486551786657673
2,4,poids,3,-0.0289511574736988
2,4,poids,4,0.00698036598617427
2,4,poids,5,-0.0710457251828115
2,4,poids,6,-0.996368603511107
2,4,poids,7,0.135008454749163
2,4,poids,8,-1.10423869779418
2,4,poids,9,-0.113873015543075
2,4,poids,10,0.337393154277337
2,4,poids,11,-0.081256593130693
2,4,poids,12,0.238891327263599
2,4,poids,13,0.828294762448065
2,4,poids,14,-0.596770884906166
2,4,poids,15,0.388314601356973
2,5,poids,0,0.394157298244241
2,5,poids,1,-0.174708474716254
2,5,poids,2,0.474607488521894
2,5,poids,3,0.922025277029871
2,5,poids,4,-1.66112701831599
2,5,poids,5,0.0958230337316126
2,5,poids,6,-0.0349505121581125
2,5,poids,7,1.71543984359486
2,5,poids,8,0.357577623816596
2,5,poids,9,0.364365934463199
2,5,poids,10,-0.376145169526972
2,5,poids,11,0.291280970914756
2,5,poids,12,0.598563542014647
2,5,poids,13,-1.45589333678439
2,5,poids,14,-0.314126135518085
2,5,poids,15,-0.174011354724731
2,6,poids,0,0.690010670715671
2,6,poids,1,-1.12560216962511
2,6,poids,2,-0.160575008467232
2,6,poids,3,-0.855425810205606
2,6,poids,4,1.83991761312632
2,6,poids,5,0.562508524593122
2,6,poids,6,-1.15147335328178
2,6,poids,7,-1.62769239226961
2,6,poids,8,-0.871574517506334
2,6,poids,9,0.465774196829078
2,6,poids,10,0.945750069630828
2,6,poids,11,-0.423287263338634
2,6,poids,12,1.02903691264726
2,6,poids,13,-0.330718214467768
2,6,poids,14,1.03020992570446
2,6,poids,15,0.990995389147868
2,7,poids,0,0.839415603573907
2,7,poids,1,0.349285732740977
2,7,poids,2,1.43342861147325
2,7,poids,3,-1.15433444714437
2,7,poids,4,0.600896909149096
2,7,poids,5,0.463402516119024
2,7,poids,6,-0.299241297714024
2,7,poids,7,-1.17580727463814
2,7,poids,8,0.289719237964034
2,7,poids,9,-0.905096009795449
2,7,poids,10,0.624790219549155
2,7,poids,11,-0.984584095652579
2,7,poids,12,0.246708634408636
2,7,poids,13,1.45132260251401
2,7,poids,14,1.15329637502846
2,7,poids,15,0.905762488030723
3,0,biais,0,-0.120935026299256
3,0,poids,0,0.492329132511675
3,0,poids,1,-1.167366727574
3,0,poids,2,-0.678788781521486
3,0,poids,3,0.52056040283356
3,0,poids,4,0.604345905613096
3,0,poids,5,0.420961579218051
3,0,poids,6,0.553536786289996
3,0,poids,7,0.623095746674858
</model> in the CSV file poids_entraines.csv, which I believe allows predicting the position to play, so by modifying the neural network interface is it possible to play? by normalizing the neural network's predictions...
 
Joined
Jun 12, 2020
Messages
73
Reaction score
3
After the DWORD WINAPI EntrainementThread function from the first code (neural network with window interface), replace the LRESULT CALLBACK WindowProc function with the following code :
C++:
/*


MIT License


Copyright (c) 2025 CoTon_TiGe_MoUaRf



Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:



The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.




THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


*/
// ============ FONCTIONS TIC-TAC-TOE ============
void ConvertirEntriesEnGrille(const std::vector<double>& entrees, char grille[9]) {
    for (int i = 0; i < 9; i++) {
        if (i < (int)entrees.size()) {
            if (entrees[i] > 0.25)      grille[i] = 'X';
            else if (entrees[i] < -0.25) grille[i] = 'O';
            else                         grille[i] = ' ';
        } else {
            grille[i] = ' ';
        }
    }
}

std::string AfficherGrilleASCII(const char grille[9], int positionPredite = -1) {
    std::string resultat =
        " 0 | 1 | 2\r\n"
        "-----------\r\n"
        " 3 | 4 | 5\r\n"
        "-----------\r\n"
        " 6 | 7 | 8\r\n\r\n"
        "État du plateau:\r\n"
        "───────────\r\n";

    char bufTemp[256];
    char case0 = (positionPredite == 0) ? '[' : ' ';
    char case1 = (positionPredite == 1) ? '[' : ' ';
    char case2 = (positionPredite == 2) ? '[' : ' ';
    char case3 = (positionPredite == 3) ? '[' : ' ';
    char case4 = (positionPredite == 4) ? '[' : ' ';
    char case5 = (positionPredite == 5) ? '[' : ' ';
    char case6 = (positionPredite == 6) ? '[' : ' ';
    char case7 = (positionPredite == 7) ? '[' : ' ';
    char case8 = (positionPredite == 8) ? '[' : ' ';

    char end0 = (positionPredite == 0) ? ']' : ' ';
    char end1 = (positionPredite == 1) ? ']' : ' ';
    char end2 = (positionPredite == 2) ? ']' : ' ';
    char end3 = (positionPredite == 3) ? ']' : ' ';
    char end4 = (positionPredite == 4) ? ']' : ' ';
    char end5 = (positionPredite == 5) ? ']' : ' ';
    char end6 = (positionPredite == 6) ? ']' : ' ';
    char end7 = (positionPredite == 7) ? ']' : ' ';
    char end8 = (positionPredite == 8) ? ']' : ' ';

    sprintf_s(bufTemp, sizeof(bufTemp),
        " %c%c%c | %c%c%c | %c%c%c\r\n"
        "───────────\r\n"
        " %c%c%c | %c%c%c | %c%c%c\r\n"
        "───────────\r\n"
        " %c%c%c | %c%c%c | %c%c%c\r\n",
        case0, grille[0], end0,
        case1, grille[1], end1,
        case2, grille[2], end2,
        case3, grille[3], end3,
        case4, grille[4], end4,
        case5, grille[5], end5,
        case6, grille[6], end6,
        case7, grille[7], end7,
        case8, grille[8], end8);
    resultat += bufTemp;
    return resultat;
}

bool EstPositionValide(const char grille[9], int position) {
    return position >= 0 && position < 9 && grille[position] == ' ';
}

int ConvertirPredictionEnPosition(double prediction) {
    int position = (int)round(prediction); // Arrondi à l'entier le plus proche (3.749 → 4)
    if (position < 0) return 0;
    if (position > 8) return 8;
    return position;
}
// ============ CALLBACK DE LA FENÊTRE ============
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case WM_CREATE: {
            Log("INFO", "Interface neuronale creee\n");

            // Initialiser les couleurs et polices
            hBrushBackground = CreateSolidBrush(RGB(240, 240, 240));
            hBrushGroup = CreateSolidBrush(RGB(255, 255, 255));
            hPenBorder = CreatePen(PS_SOLID, 1, RGB(100, 100, 100));

            hFontTitle = CreateFontA(16, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
                                     ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
                                     DEFAULT_QUALITY, DEFAULT_PITCH | FF_MODERN, "Courier New");
            hFontNormal = CreateFontA(12, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
                                      ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
                                      DEFAULT_QUALITY, DEFAULT_PITCH | FF_MODERN, "Courier New");
            hFontSmall = CreateFontA(10, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
                                     ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
                                     DEFAULT_QUALITY, DEFAULT_PITCH | FF_MODERN, "Courier New");

            int col1 = 20, col2 = 350, col3 = 680, col4 = 950;
            int row = 20, hauteur_input = 28, espacement = 40;

            // ===== GROUPE ENTRAÎNEMENT =====
            HWND groupTrain = CreateWindowA("BUTTON", " ENTRAÎNEMENT ",
                                          WS_VISIBLE | WS_CHILD | BS_GROUPBOX,
                                          col1, row, 300, 420, hwnd, (HMENU)5001,
                                          GetModuleHandle(NULL), NULL);
            SendMessage(groupTrain, WM_SETFONT, (WPARAM)hFontTitle, TRUE);

            row += 35;
            gControls.btnEntrainer = CreateWindowA("BUTTON", "Entraîner Nouveau Modele",
                                                  WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
                                                  col1 + 15, row, 270, 32, hwnd, (HMENU)1001,
                                                  GetModuleHandle(NULL), NULL);
            SendMessage(gControls.btnEntrainer, WM_SETFONT, (WPARAM)hFontNormal, TRUE);
            row += espacement + 5;

            gControls.btnCharger = CreateWindowA("BUTTON", "Charger Modele Existant",
                                                WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
                                                col1 + 15, row, 270, 32, hwnd, (HMENU)1002,
                                                GetModuleHandle(NULL), NULL);
            SendMessage(gControls.btnCharger, WM_SETFONT, (WPARAM)hFontNormal, TRUE);
            row += espacement;

            CreateStaticLabel(hwnd, "Nombre d'Epochs :", col1 + 15, row, 120, 20, hFontSmall);
            gControls.editEpochs = CreateWindowA("EDIT", "5000",
                                                WS_VISIBLE | WS_CHILD | WS_BORDER,
                                                col1 + 140, row, 100, hauteur_input, hwnd, (HMENU)4001,
                                                GetModuleHandle(NULL), NULL);
            SendMessage(gControls.editEpochs, WM_SETFONT, (WPARAM)hFontSmall, TRUE);
            CreateStaticLabel(hwnd, "(5000-50000 recommande)", col1 + 140, row + 22, 200, 15, hFontSmall);

            row += espacement;
            CreateStaticLabel(hwnd, "Taux d'Apprentissage :", col1 + 15, row, 120, 20, hFontSmall);
            gControls.editLR = CreateWindowA("EDIT", "0.0005",
                                            WS_VISIBLE | WS_CHILD | WS_BORDER,
                                            col1 + 140, row, 100, hauteur_input, hwnd, (HMENU)4002,
                                            GetModuleHandle(NULL), NULL);
                        SendMessage(gControls.editLR, WM_SETFONT, (WPARAM)hFontSmall, TRUE);
            CreateStaticLabel(hwnd, "(0.0001 - 0.01 recommande)", col1 + 140, row + 22, 200, 15, hFontSmall);

            row += espacement + 10;
            CreateStaticLabel(hwnd, "Logs d'Entraînement :", col1 + 15, row, 200, 20, hFontSmall);

            row += 25;
            gControls.textEntrainement = CreateWindowA("EDIT", "",
                WS_VISIBLE | WS_CHILD | WS_BORDER | WS_VSCROLL | ES_MULTILINE | ES_READONLY,
                col1 + 15, row, 270, 120, hwnd, (HMENU)2001,
                GetModuleHandle(NULL), NULL);
            SendMessage(gControls.textEntrainement, WM_SETFONT, (WPARAM)hFontSmall, TRUE);

            // ===== GROUPE PReDICTION =====
            row = 20;
            HWND groupPredict = CreateWindowA("BUTTON", " PReDICTION ",
                WS_VISIBLE | WS_CHILD | BS_GROUPBOX,
                col2, row, 300, 420, hwnd, (HMENU)5002,
                GetModuleHandle(NULL), NULL);
            SendMessage(groupPredict, WM_SETFONT, (WPARAM)hFontTitle, TRUE);

            // Creer les champs d'entree dynamiques (9 par defaut pour Tic-Tac-Toe)
            CreerControlsEntrees(hwnd, 9);

            // ===== GROUPE GRILLE TIC-TAC-TOE =====
            HWND groupGrille = CreateWindowA("BUTTON", " GRILLE & COUP ",
                WS_VISIBLE | WS_CHILD | BS_GROUPBOX,
                col3, row, 250, 420, hwnd, (HMENU)5004,
                GetModuleHandle(NULL), NULL);
            SendMessage(groupGrille, WM_SETFONT, (WPARAM)hFontTitle, TRUE);

            row += 35;
            CreateStaticLabel(hwnd, "etat du plateau:", col3 + 15, row, 220, 20, hFontSmall);
            row += 25;
            gControls.textGrille = CreateWindowA("EDIT", "",
                WS_VISIBLE | WS_CHILD | WS_BORDER | ES_MULTILINE | ES_READONLY,
                col3 + 15, row, 220, 160, hwnd, (HMENU)2004,
                GetModuleHandle(NULL), NULL);
            SendMessage(gControls.textGrille, WM_SETFONT, (WPARAM)hFontSmall, TRUE);

            row += 170;
            CreateStaticLabel(hwnd, "Coup recommande:", col3 + 15, row, 220, 20, hFontSmall);
            row += 25;
            gControls.textCoupPredit = CreateWindowA("EDIT", "",
                WS_VISIBLE | WS_CHILD | WS_BORDER | ES_READONLY,
                col3 + 15, row, 220, hauteur_input, hwnd, (HMENU)2005,
                GetModuleHandle(NULL), NULL);
            SendMessage(gControls.textCoupPredit, WM_SETFONT, (WPARAM)hFontSmall, TRUE);

            row += 45;
            CreateStaticLabel(hwnd, "Confiance:", col3 + 15, row, 100, 20, hFontSmall);
            gControls.textConfiance = CreateWindowA("EDIT", "",
                WS_VISIBLE | WS_CHILD | WS_BORDER | ES_READONLY,
                col3 + 120, row, 115, hauteur_input, hwnd, (HMENU)2006,
                GetModuleHandle(NULL), NULL);
            SendMessage(gControls.textConfiance, WM_SETFONT, (WPARAM)hFontSmall, TRUE);

            // ===== GROUPE STATUS =====
            row = 20;
            HWND groupStatus = CreateWindowA("BUTTON", " STATUT ",
                WS_VISIBLE | WS_CHILD | BS_GROUPBOX,
                col4, row, 200, 420, hwnd, (HMENU)5003,
                GetModuleHandle(NULL), NULL);
            SendMessage(groupStatus, WM_SETFONT, (WPARAM)hFontTitle, TRUE);

            row += 30;
            CreateStaticLabel(hwnd, "Statut :", col4 + 15, row, 80, 20, hFontSmall);
            gControls.textStatus = CreateWindowA("EDIT", "Prêt",
                WS_VISIBLE | WS_CHILD | WS_BORDER | ES_READONLY,
                col4 + 15, row + 25, 170, 20, hwnd, (HMENU)2003,
                GetModuleHandle(NULL), NULL);
            SendMessage(gControls.textStatus, WM_SETFONT, (WPARAM)hFontSmall, TRUE);

            gControls.hStatusBar = CreateWindowA("STATIC", "",
                WS_VISIBLE | WS_CHILD,
                col4 + 15, row + 50, 170, 5, hwnd, NULL,
                GetModuleHandle(NULL), NULL);

            AfficherMessageThreadSafe(hwnd, "Application prête. Chargez ou entraînez d'abord.", false);
            AfficherStatusCouleur(0, "Prêt");
            break;
        }

        case WM_UPDATE_INPUTS: {
            int nombre_entrees = (int)wParam;
            CreerControlsEntrees(hwnd, nombre_entrees);
            break;
        }

        case WM_COMMAND: {
            int id = LOWORD(wParam);
            if (id == 1001) { // Entraîner
                char bufferEpochs[256];
                GetWindowTextA(gControls.editEpochs, bufferEpochs, sizeof(bufferEpochs));
                gNombreEpochs = atoi(bufferEpochs);
                if (gNombreEpochs <= 0) gNombreEpochs = 5000;

                char bufferLR[256];
                GetWindowTextA(gControls.editLR, bufferLR, sizeof(bufferLR));
                double taux_apprentissage = atof(bufferLR);
                if (taux_apprentissage <= 0.0) taux_apprentissage = 0.0005;

                char bufferInfo[512];
                sprintf_s(bufferInfo, sizeof(bufferInfo), "[INFO] Epochs: %d | LR: %.6f", gNombreEpochs, taux_apprentissage);
                AfficherMessageThreadSafe(hwnd, bufferInfo, false);

                gDonnees = charger_csv("data.csv");
                if (gDonnees.empty()) {
                    AfficherMessageThreadSafe(hwnd, "[ERREUR] Fichier data.csv non trouve!", false);
                    AfficherStatusCouleur(2, "Erreur: data.csv introuvable");
                } else {
                    EnableWindow(gControls.btnEntrainer, FALSE);
                    EnableWindow(gControls.btnCharger, FALSE);
                    gReseau.taux_apprentissage = taux_apprentissage;
                    AfficherStatusCouleur(3, "Initialisation...");
                    HANDLE hThread = CreateThread(NULL, 0, EntrainementThread, NULL, 0, NULL);
                    if (hThread) CloseHandle(hThread);
                }
            }
            else if (id == 1002) { // Charger
                AfficherMessageThreadSafe(hwnd, "[INFO] Initialisation du reseau...", false);
                AfficherStatusCouleur(3, "Chargement...");
                gDonnees = charger_csv("data.csv");
                if (gDonnees.empty()) {
                    AfficherMessageThreadSafe(hwnd, "[ERREUR] Fichier data.csv non trouve!", false);
                    AfficherStatusCouleur(2, "Erreur: data.csv introuvable");
                } else {
                    int nombre_entrees = gDonnees[0].features.size();
                    gNombreEntrees = nombre_entrees;
                    gTopologie = {nombre_entrees, 16, 8, 1};
                    gNbCouches = gTopologie.size();
                    initialiser_reseau(gReseau, gTopologie, true, false, false);
                    gReseau.taux_apprentissage = 0.001;
                    charger_poids(gReseau, gFichierPoids);
                    PostMessage(hwnd, WM_UPDATE_INPUTS, nombre_entrees, 0);
                    AfficherMessageThreadSafe(hwnd, "[OK] Modele charge avec succes!", false);
                    AfficherStatusCouleur(1, "Modele charge!");
                    gReseauInitialise = true;
                    EnableWindow(gControls.btnPredire, TRUE);
                }
            }
            // Dans la section else if (id == 1003) { // Prédire
else if (id == 1003) { // Prédire
    if (gReseauInitialise) {
        std::vector<double> entrees;
        for (const auto& hwndEdit : gControls.editEntrees) {
            char buffer[256];
            GetWindowTextA(hwndEdit, buffer, sizeof(buffer));
            entrees.push_back(atof(buffer));
        }

        char grille[9];
        ConvertirEntriesEnGrille(entrees, grille);

        if (entrees.size() != (size_t)gNombreEntrees) {
            MessageBoxA(hwnd, "Nombre d'entrees incorrect !", "Erreur", MB_OK | MB_ICONWARNING);
            AfficherStatusCouleur(2, "Erreur: entrees incorrectes");
            SetWindowTextA(gControls.textCoupPredit, "ERREUR");
            return 0;
        }

        double prediction = predire(gReseau, entrees);
        int position = ConvertirPredictionEnPosition(prediction); // Fonction corrigée

        // Afficher la grille avec le coup prédit
        std::string affichageGrille = AfficherGrilleASCII(grille, position);
        SetWindowTextA(gControls.textGrille, affichageGrille.c_str());

        Log("INFO", "Prédiction brute: %.6f | Position: %d\n", prediction, position);
        AfficherStatusCouleur(1, "Prédiction réussie!");
    } else {
        MessageBoxA(hwnd, "Chargez ou entraînez d'abord un modèle !", "Erreur", MB_OK | MB_ICONWARNING);
        AfficherStatusCouleur(2, "Erreur: modèle non initialisé");
        SetWindowTextA(gControls.textCoupPredit, "NON INITIALISÉ");
    }
}
            break;
        }

        case WM_CTLCOLORSTATIC: {
            HDC hdcStatic = (HDC)wParam;
            SetBkColor(hdcStatic, RGB(255, 255, 255));
            SetTextColor(hdcStatic, RGB(50, 50, 50));
            return (LRESULT)hBrushGroup;
        }

        case WM_CTLCOLOREDIT: {
            HDC hdcEdit = (HDC)wParam;
            SetBkColor(hdcEdit, RGB(255, 255, 255));
            SetTextColor(hdcEdit, RGB(0, 0, 0));
            return (LRESULT)hBrushGroup;
        }

        case WM_DESTROY: {
            detruire_reseau(gReseau);
            DeleteObject(hBrushBackground);
            DeleteObject(hBrushGroup);
            DeleteObject(hPenBorder);
            DeleteObject(hFontTitle);
            DeleteObject(hFontNormal);
            DeleteObject(hFontSmall);
            PostQuitMessage(0);
            break;
        }
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
and complete the structure:
C++:
struct DynamicControls {
    HWND btnEntrainer;
    HWND btnCharger;
    HWND btnPredire;
    std::vector<HWND> editEntrees;
    std::vector<HWND> labelEntrees;
    HWND textResultat;
    HWND textStatus;
    HWND textEntrainement;
    HWND editEpochs;
    HWND editLR;
    HWND hStatusBar;
    HWND labelResultat; // Ajout pour stocker le label "Résultat :"
    HWND textGrille;       // Affichage de la grille
HWND textCoupPredit;   // Position du coup prédit
HWND textConfiance;    // Score de confiance

};
And the first code, trained with the dataset created from the code of the previous message and modified with the code above, is able to predict the position to play and places the move in the grid with: '[]'.
 
Last edited:
Joined
Jun 12, 2020
Messages
73
Reaction score
3
Here is the code to generate the dataset with move validation (so you don't predict a move onto an already occupied square in the grid) :
C++:
#include <iostream>
#include <vector>
#include <fstream>
#include <sstream>
#include <cmath>
#include <algorithm>
#include <random>
#include <iomanip>
#include <cstring>

using namespace std;

// ===== TYPES DE BASE =====
typedef vector<double> Vector;

// ===== STRUCTURE POUR COLLECTER LES DONNEES =====
struct GameResult {
    vector<Vector> states;
    vector<int> moves;
    vector<int> players;      // Quel joueur a fait chaque coup
    int winner;
};

// ===== CLASSE POUR COLLECTER ET SAUVEGARDER LES DONNEES =====
class DatasetCollector {
private:
    string filename;
    vector<pair<Vector, int>> data;  // (state, optimal_move_position)
    int rejectedSamples = 0;
    
public:
    DatasetCollector(string fname) : filename(fname) {}
    
    // Ajouter un coup joué au dataset
    // Le coup sera stocké directement comme un entier (0-8)
    // ✓ CORRIGE: Retourne maintenant un bool pour valider le coup
    bool addGameSample(const Vector& state, int move) {
        // Valider le coup (plage valide)
        if(move < 0 || move > 8) {
            cerr << "Erreur: coup invalide " << move << "\n";
            rejectedSamples++;
            return false;
        }
        
        // ✓ NOUVEAU: Vérifier que la position est bien vide dans l'état (0.0)
        if(state[move] != 0.0) {
            cerr << "Erreur: la position " << move
                 << " est déjà occupée dans l'état (valeur: " << state[move] << ")!\n";
            rejectedSamples++;
            return false;
        }
        
        data.push_back({state, move});
        return true;
    }
    
    // Sauvegarder dans un CSV
    void saveToCSV() {
        ofstream file(filename);
        
        if(!file.is_open()) {
            cerr << "Erreur: impossible d'ouvrir " << filename << "\n";
            return;
        }
        
        // Entête du CSV : état (9 colonnes) + position optimale (1 colonne)
        file << "state_0,state_1,state_2,state_3,state_4,state_5,state_6,state_7,state_8,optimal_move\n";
        
        // Données
        for(const auto& sample : data) {
            const Vector& state = sample.first;
            int move = sample.second;
            
            // Écrire l'état (9 cases)
            for(int i = 0; i < 9; i++) {
                file << fixed << setprecision(2) << state[i];
                file << ",";
            }
            
            // Écrire la position optimale directement (1 seule colonne)
            file << move << "\n";
        }
        
        file.close();
        cout << "\n✓ Dataset sauvegardé: " << filename << " (" << data.size() << " samples)\n";
    }
    
    // Obtenir la taille du dataset
    size_t getSize() const {
        return data.size();
    }
    
    // Obtenir le nombre de samples rejetés
    int getRejectedCount() const {
        return rejectedSamples;
    }
    
    // Effacer les données
    void clear() {
        data.clear();
        rejectedSamples = 0;
    }
};

// ===== TIC-TAC-TOE =====
class TicTacToe {
public:
    vector<int> board;
    
    TicTacToe() : board(9, 0) {}
    
    Vector getBoardState() {
        Vector state(9);
        for(int i = 0; i < 9; i++) {
            if(board[i] == 0) state[i] = 0.0;
            else state[i] = board[i] == 1 ? 0.5 : -0.5;
        }
        return state;
    }
    
    bool makeMove(int pos, int player) {
        if(board[pos] != 0) return false;
        board[pos] = player;
        return true;
    }
    
    int checkWin() {
        int lines[8][3] = {{0,1,2},{3,4,5},{6,7,8},{0,3,6},{1,4,7},{2,5,8},{0,4,8},{2,4,6}};
        for(auto& line : lines) {
            if(board[line[0]] && board[line[0]] == board[line[1]] && board[line[1]] == board[line[2]])
                return board[line[0]];
        }
        return 0;
    }
    
    bool isFull() {
        for(int x : board) if(x == 0) return false;
        return true;
    }
    
    // Retourner tous les coups possibles
    vector<int> getPossibleMoves() {
        vector<int> moves;
        for(int i = 0; i < 9; i++) {
            if(board[i] == 0) moves.push_back(i);
        }
        return moves;
    }
    
    int selectMove(bool useRandomness = false) {
        // Sélection aléatoire simple pour le self-play
        vector<int> possibleMoves = getPossibleMoves();
        if(possibleMoves.empty()) return -1;
        
        mt19937 gen(random_device{}());
        uniform_int_distribution<> dis(0, possibleMoves.size() - 1);
        
        return possibleMoves[dis(gen)];
    }
    
    void printBoard() {
        cout << "\nTableau:\n";
        for(int i = 0; i < 3; i++) {
            for(int j = 0; j < 3; j++) {
                cout << (board[i*3+j] == 0 ? '.' : (board[i*3+j] == 1 ? 'X' : 'O')) << " ";
            }
            cout << "\n";
        }
    }
};

// ===== JEU EN SELF-PLAY AVEC COLLECTION DE DONNEES =====
GameResult playSelfGame(DatasetCollector* collector = nullptr, bool displayBoard = false) {
    GameResult result;
    result.winner = 0;
    TicTacToe game;
    int turn = 0;
    
    while(!game.isFull() && game.checkWin() == 0) {
        Vector state = game.getBoardState();
        int move = game.selectMove(true);
        int player = (turn % 2) + 1;
        
        if(move == -1) break;
        
        result.states.push_back(state);
        result.moves.push_back(move);
        result.players.push_back(player);
        
        game.makeMove(move, player);
        
        if(displayBoard) {
            cout << "Tour " << (turn + 1) << " - Joueur " << player
                 << " joue position " << move << "\n";
            game.printBoard();
        }
        
        turn++;
    }
    
    result.winner = game.checkWin();
    
    // ✓ CORRIGE: Ajouter tous les coups au dataset avec validation
    // Chaque coup est stocké comme sa position (0-8)
    if(collector != nullptr) {
        for(size_t i = 0; i < result.states.size(); i++) {
            if(!collector->addGameSample(result.states[i], result.moves[i])) {
                cerr << "⚠ Attention: sample rejeté à l'index " << i
                     << " (position " << result.moves[i] << ")\n";
            }
        }
    }
    
    return result;
}

// ===== GENERATION DU DATASET =====
void generateDataset(int numberOfGames, string csvFilename = "tictactoe_dataset.csv") {
    cout << "\n" << string(80, '=') << "\n";
    cout << "GENERATION DU DATASET PAR SELF-PLAY\n";
    cout << "CIBLE: Prédire la position optimale à jouer\n";
    cout << string(80, '=') << "\n\n";
    
    DatasetCollector collector(csvFilename);
    
    cout << "Paramètres:\n";
    cout << "  • Nombre de parties: " << numberOfGames << "\n";
    cout << "  • Fichier de sortie: " << csvFilename << "\n";
    cout << "  • Format: state (9 entrées) → position optimale (1 sortie, valeur 0-8)\n\n";
    
    int player1Wins = 0;
    int player2Wins = 0;
    int draws = 0;
    
    for(int game = 0; game < numberOfGames; game++) {
        GameResult result = playSelfGame(&collector, false);
        
        if(result.winner == 1) {
            player1Wins++;
        } else if(result.winner == 2) {
            player2Wins++;
        } else {
            draws++;
        }
        
        // Affichage du progrès
        if((game + 1) % 100 == 0) {
            cout << "Parties générées: " << game + 1 << " / " << numberOfGames;
            cout << " | Samples collectés: " << collector.getSize() << "\n";
        }
    }
    
    // Sauvegarder le dataset
    collector.saveToCSV();
    
    // Affichage des statistiques
    cout << "\n" << string(80, '=') << "\n";
    cout << "STATISTIQUES DU DATASET\n";
    cout << string(80, '=') << "\n\n";
    
    cout << "Résultats des " << numberOfGames << " parties:\n";
    cout << "  • Joueur 1 victoires: " << player1Wins
         << " (" << fixed << setprecision(2) << (player1Wins * 100.0 / numberOfGames) << "%)\n";
    cout << "  • Joueur 2 victoires: " << player2Wins
         << " (" << fixed << setprecision(2) << (player2Wins * 100.0 / numberOfGames) << "%)\n";
    cout << "  • Matchs nuls: " << draws
         << " (" << fixed << setprecision(2) << (draws * 100.0 / numberOfGames) << "%)\n\n";
    
    cout << "Données du dataset:\n";
    cout << "  • Nombre total de samples: " << collector.getSize() << "\n";
    cout << "  • Samples rejetés: " << collector.getRejectedCount() << "\n";
    cout << "  • Coups en moyenne par partie: "
         << fixed << setprecision(2) << (collector.getSize() / (double)numberOfGames) << "\n\n";
    
    cout << "Structure du CSV:\n";
    cout << "  • Entrées: state_0 à state_8 (9 colonnes)\n";
    cout << "  • Sorties: optimal_move (1 colonne, valeur: 0-8)\n";
    cout << "  • Total: 10 colonnes\n\n";
    
    // ✓ NOUVEAU: Afficher les résultats de validation
    cout << "Contrôle de qualité:\n";
    cout << "  • ✓ Tous les samples ont été validés\n";
    cout << "  • ✓ Chaque position est bien vide dans son état\n";
    cout << "  • ✓ Aucune synchronisation perdue\n\n";
}

// ===== FONCTION PRINCIPALE =====
int main() {
    cout << "╔════════════════════════════════════════════════════════════════════════════╗\n";
    cout << "║     GENERATEUR DE DATASET CSV POUR TIC-TAC-TOE - PREDICTION DE POSITION   ║\n";
    cout << "║                      AVEC VALIDATION DE COHERENCE                          ║\n";
    cout << "╚════════════════════════════════════════════════════════════════════════════╝\n";
    
    // Générer le dataset
    generateDataset(3000, "tictactoe_dataset.csv");
    
    cout << "\n Génération terminée avec succès!\n";
    cout << "\n Structure du fichier CSV:\n\n";
    cout << "ENTRÉES (état du plateau):\n";
    cout << "  • state_0 à state_8: État du plateau\n";
    cout << "    - 0.0 = case vide\n";
    cout << "    - 0.5 = joueur 1 (X)\n";
    cout << "    - -0.5 = joueur 2 (O)\n\n";
    
    cout << "SORTIE (position optimale à jouer):\n";
    cout << "  • optimal_move: Position à jouer (0-8)\n";
    cout << "    - 0 = coin haut-gauche\n";
    cout << "    - 4 = centre\n";
    cout << "    - 8 = coin bas-droit\n\n";
    
    cout << "Disposition des positions:\n";
    cout << "    0 | 1 | 2\n";
    cout << "   -----------\n";
    cout << "    3 | 4 | 5\n";
    cout << "   -----------\n";
    cout << "    6 | 7 | 8\n\n";
    
    cout << "Exemple de ligne:\n";
    cout << "  0.00,0.50,0.00,0.00,0.00,0.00,-0.50,0.00,0.00,3\n";
    cout << "  └─ état du plateau ─────────────────────────┘ └─ position 3 ─┘\n\n";
    
    cout << " VALIDATION: Chaque coup enregistré est vérifié pour assurer\n";
    cout << "  que la position est bien vide (0.0) dans l'état correspondant.\n\n";
    
    return 0;
}
The trained network model looks like this after 5,000 epochs with a learning rate of 0.0001 :
Code:
couche,neurone,type,index,valeur
1,0,biais,0,2.95576373002201
1,1,biais,0,-0.0336936154046481
1,2,biais,0,-0.0137318639577331
1,3,biais,0,-0.454123419162303
1,4,biais,0,4.94742665060066
1,5,biais,0,0.161739350450226
1,6,biais,0,0.0630736098110813
1,7,biais,0,-0.261800403749549
1,8,biais,0,-1.31955332455954
1,9,biais,0,1.46809230567346
1,10,biais,0,0.0839713527779533
1,11,biais,0,1.0905969966838
1,12,biais,0,0.116168951489331
1,13,biais,0,0.539820425241425
1,14,biais,0,0.379798630305683
1,15,biais,0,2.63152091276433
1,0,poids,0,-1.44998921451166
1,0,poids,1,5.10707072602797
1,0,poids,2,0.286415177006301
1,0,poids,3,0.627765908516564
1,0,poids,4,-0.628549557254447
1,0,poids,5,-0.112296959710598
1,0,poids,6,0.775463551801161
1,0,poids,7,-0.598567184779013
1,0,poids,8,1.43318681271905
1,1,poids,0,-1.46143922762437
1,1,poids,1,1.43172399529438
1,1,poids,2,-0.13482683060109
1,1,poids,3,-0.493976439919229
1,1,poids,4,0.873107701948103
1,1,poids,5,0.901304347478417
1,1,poids,6,-0.0687357550189665
1,1,poids,7,0.579352697842507
1,1,poids,8,3.55689721190965
1,2,poids,0,-5.88010466639753
1,2,poids,1,0.769519743862894
1,2,poids,2,0.976230673944823
1,2,poids,3,-1.10552089658329
1,2,poids,4,-0.579006117532542
1,2,poids,5,-1.15923918971825
1,2,poids,6,0.826498093750314
1,2,poids,7,0.742913159999895
1,2,poids,8,-1.8881529604696
1,3,poids,0,0.128265864488924
1,3,poids,1,-0.278105471064022
1,3,poids,2,-0.30718701526035
1,3,poids,3,-0.393755499895023
1,3,poids,4,0.012396578286596
1,3,poids,5,-0.332851084191525
1,3,poids,6,0.75169565620611
1,3,poids,7,0.743361378313212
1,3,poids,8,-3.40246729699253
1,4,poids,0,4.61725160580781
1,4,poids,1,-4.97613394406311
1,4,poids,2,4.239883165463
1,4,poids,3,0.959951645312613
1,4,poids,4,-1.8482343372574
1,4,poids,5,-0.278108810464351
1,4,poids,6,-0.837015652447357
1,4,poids,7,-0.530557523384643
1,4,poids,8,-0.607257289336236
1,5,poids,0,-0.668453441179719
1,5,poids,1,-1.80604536585588
1,5,poids,2,2.13125393972912
1,5,poids,3,-0.297496313554819
1,5,poids,4,-1.43046119516284
1,5,poids,5,-5.20164093872155
1,5,poids,6,0.995748104597532
1,5,poids,7,-1.81823817034172
1,5,poids,8,1.81016311350786
1,6,poids,0,-0.462091507828641
1,6,poids,1,0.33768912887924
1,6,poids,2,-0.939899358313973
1,6,poids,3,-0.362886216469445
1,6,poids,4,-0.344275659838401
1,6,poids,5,0.80251759415074
1,6,poids,6,-1.91639847953435
1,6,poids,7,-0.533069798330943
1,6,poids,8,0.500991935316295
1,7,poids,0,-1.40174386494004
1,7,poids,1,0.175639169469671
1,7,poids,2,0.131929321979683
1,7,poids,3,0.724472611752716
1,7,poids,4,0.12217821244055
1,7,poids,5,0.765275389761008
1,7,poids,6,5.89826424488402
1,7,poids,7,-0.36318300491567
1,7,poids,8,-0.353134245864288
1,8,poids,0,1.24556846064649
1,8,poids,1,2.50671861550236
1,8,poids,2,0.959282725827102
1,8,poids,3,1.10460144019232
1,8,poids,4,-3.11232531702563
1,8,poids,5,-0.126224038175802
1,8,poids,6,0.770349771274638
1,8,poids,7,-0.58988951981178
1,8,poids,8,-0.304477276889306
1,9,poids,0,1.49012223541754
1,9,poids,1,-0.991478546486939
1,9,poids,2,-1.68444701360421
1,9,poids,3,-2.24528898465149
1,9,poids,4,0.541834281135841
1,9,poids,5,0.801681176465653
1,9,poids,6,0.912791624749049
1,9,poids,7,0.615337199380405
1,9,poids,8,-1.13876124918717
1,10,poids,0,-0.949352792746139
1,10,poids,1,-1.14137026332068
1,10,poids,2,4.75353791857048
1,10,poids,3,-1.20087104978812
1,10,poids,4,0.5928444997963
1,10,poids,5,-0.854673665892346
1,10,poids,6,-0.276918107892744
1,10,poids,7,-0.973445906198515
1,10,poids,8,1.76316607341719
1,11,poids,0,-0.916612910075929
1,11,poids,1,-0.00567714195983139
1,11,poids,2,-0.876421614200318
1,11,poids,3,-0.127708859095636
1,11,poids,4,-0.924116311049515
1,11,poids,5,-0.263565688938662
1,11,poids,6,-1.21440415824272
1,11,poids,7,5.47984245518944
1,11,poids,8,1.05110145984616
1,12,poids,0,-1.66475064812139
1,12,poids,1,-2.46332960180145
1,12,poids,2,-2.61301555980543
1,12,poids,3,1.62328266363505
1,12,poids,4,-1.36725665558458
1,12,poids,5,1.65217859395751
1,12,poids,6,-1.20868308329203
1,12,poids,7,0.532287084156562
1,12,poids,8,0.483095408322719
1,13,poids,0,2.21385101367807
1,13,poids,1,-2.7621823140322
1,13,poids,2,1.77361937189992
1,13,poids,3,-0.257282768146697
1,13,poids,4,-0.972475607478759
1,13,poids,5,2.12761544484544
1,13,poids,6,-1.45566812125236
1,13,poids,7,-2.2089063514502
1,13,poids,8,-0.367979045901584
1,14,poids,0,-0.886210558830684
1,14,poids,1,0.504699813680106
1,14,poids,2,-0.899943512082973
1,14,poids,3,0.228837852448717
1,14,poids,4,1.22016230663471
1,14,poids,5,0.400657030743643
1,14,poids,6,0.201252617460687
1,14,poids,7,1.48367975469548
1,14,poids,8,-0.34809339825406
1,15,poids,0,-1.58561028372403
1,15,poids,1,-1.10776648113563
1,15,poids,2,-3.34952378491794
1,15,poids,3,0.727102025002059
1,15,poids,4,0.057872835838389
1,15,poids,5,-2.02205404619711
1,15,poids,6,2.15992814398554
1,15,poids,7,1.02519176995741
1,15,poids,8,-0.515287671517841
2,0,biais,0,-3.00750936421713
2,1,biais,0,-0.753858985240208
2,2,biais,0,1.21438779145455
2,3,biais,0,-0.169656210618315
2,4,biais,0,-1.70299212731341
2,5,biais,0,-2.07565914569516
2,6,biais,0,-0.818492650063717
2,7,biais,0,-0.721817166736952
2,0,poids,0,0.230890808185742
2,0,poids,1,-1.3734861273125
2,0,poids,2,1.66549313566354
2,0,poids,3,-0.335221716166572
2,0,poids,4,-0.421375162031727
2,0,poids,5,-0.770501611825975
2,0,poids,6,1.25282442173653
2,0,poids,7,-1.38608670193121
2,0,poids,8,2.40015017291836
2,0,poids,9,1.05962037236603
2,0,poids,10,1.44423909220934
2,0,poids,11,0.522095879410769
2,0,poids,12,0.887404446144204
2,0,poids,13,0.267417282840505
2,0,poids,14,0.65769722621119
2,0,poids,15,1.4080239250693
2,1,poids,0,-0.16855472519656
2,1,poids,1,-0.3318977844165
2,1,poids,2,-0.325449770924716
2,1,poids,3,0.994241732084591
2,1,poids,4,-0.593984791909281
2,1,poids,5,1.44049089798507
2,1,poids,6,0.311221870889133
2,1,poids,7,-1.2024312923367
2,1,poids,8,0.94092137533697
2,1,poids,9,0.0496317689954359
2,1,poids,10,-0.737873332113188
2,1,poids,11,0.537183767106501
2,1,poids,12,1.16402429839253
2,1,poids,13,0.550710949541942
2,1,poids,14,-0.127796134134368
2,1,poids,15,-0.765908427448296
2,2,poids,0,-0.308365007555113
2,2,poids,1,-1.87727061673083
2,2,poids,2,1.34012901322309
2,2,poids,3,0.720278817989645
2,2,poids,4,-0.270328684232401
2,2,poids,5,-0.946867628168985
2,2,poids,6,0.61743626405428
2,2,poids,7,-0.310823810741672
2,2,poids,8,-0.23735465302371
2,2,poids,9,0.486181568525776
2,2,poids,10,0.788554755349445
2,2,poids,11,1.78239764339334
2,2,poids,12,-0.5428854161892
2,2,poids,13,-0.713760193131258
2,2,poids,14,-0.700201275138472
2,2,poids,15,0.474091382790132
2,3,poids,0,0.876438163231744
2,3,poids,1,-1.29242644643623
2,3,poids,2,-0.0977431078394412
2,3,poids,3,-0.888276907769795
2,3,poids,4,0.0881130572261482
2,3,poids,5,-0.510732513476032
2,3,poids,6,-0.477143660262301
2,3,poids,7,-0.513585473506871
2,3,poids,8,-0.552862140725793
2,3,poids,9,1.20224574199322
2,3,poids,10,0.365341552070822
2,3,poids,11,1.06070883602598
2,3,poids,12,-0.550198280371945
2,3,poids,13,-0.56083978029739
2,3,poids,14,-2.10425618198858
2,3,poids,15,0.0368632839605344
2,4,poids,0,-1.15796399952832
2,4,poids,1,-0.327043743762788
2,4,poids,2,0.646346158717926
2,4,poids,3,-1.02921654308301
2,4,poids,4,0.494930461477994
2,4,poids,5,0.0501656222933141
2,4,poids,6,-0.47681914746385
2,4,poids,7,-0.0293084236835394
2,4,poids,8,-0.11567040190435
2,4,poids,9,0.674388787917201
2,4,poids,10,0.824598973403821
2,4,poids,11,-0.219427146851142
2,4,poids,12,1.31844744281742
2,4,poids,13,-0.647325104274288
2,4,poids,14,-1.69719429171822
2,4,poids,15,0.181776216291591
2,5,poids,0,-0.0465687515998858
2,5,poids,1,-0.319599932627778
2,5,poids,2,1.51915467908979
2,5,poids,3,-1.19145127360511
2,5,poids,4,0.52547044270853
2,5,poids,5,-0.764948464846321
2,5,poids,6,2.030212097722
2,5,poids,7,0.750494330859048
2,5,poids,8,-1.03877856792848
2,5,poids,9,-0.470574838015933
2,5,poids,10,0.626659633294521
2,5,poids,11,-0.266870855320215
2,5,poids,12,0.234773090607427
2,5,poids,13,-0.824660854685957
2,5,poids,14,0.379249325115518
2,5,poids,15,0.227409504678519
2,6,poids,0,0.548907646653077
2,6,poids,1,0.362292598402164
2,6,poids,2,-0.795059479666164
2,6,poids,3,0.208336892404425
2,6,poids,4,1.07652424023246
2,6,poids,5,-0.274617090059648
2,6,poids,6,0.297805550843816
2,6,poids,7,-0.114545470419201
2,6,poids,8,-1.68890370984012
2,6,poids,9,0.013509474363322
2,6,poids,10,-0.57677012696362
2,6,poids,11,-0.520733716867022
2,6,poids,12,-1.16222528399907
2,6,poids,13,-0.608932532278007
2,6,poids,14,1.91274736659019
2,6,poids,15,-0.115461148634201
2,7,poids,0,-0.0365342266575673
2,7,poids,1,1.66789974597892
2,7,poids,2,-0.173854996132847
2,7,poids,3,-1.360512836788
2,7,poids,4,0.127354227545205
2,7,poids,5,-0.187367559140592
2,7,poids,6,-2.79818062948253
2,7,poids,7,-0.397113791788533
2,7,poids,8,0.436637489993245
2,7,poids,9,-1.21782340269007
2,7,poids,10,-0.211487567982093
2,7,poids,11,-0.321463565945986
2,7,poids,12,-0.0276897650907944
2,7,poids,13,0.616476785203308
2,7,poids,14,-1.52314297615243
2,7,poids,15,0.499434894401841
3,0,biais,0,-1.75366105140367
3,0,poids,0,0.655057334564041
3,0,poids,1,-0.978795220336148
3,0,poids,2,-0.603678495653098
3,0,poids,3,0.534365473374783
3,0,poids,4,0.423151621379716
3,0,poids,5,0.564403513906265
3,0,poids,6,0.603538840851426
3,0,poids,7,0.877336568993815
 
Joined
Sep 20, 2022
Messages
318
Reaction score
41
tic-tac-toe has 765 possible boards.

Your NN has about 1 connection for every 2 boards.

That doesn't bode well for chess.
 
Joined
Jun 12, 2020
Messages
73
Reaction score
3
Here are the corrected functions to make the network larger:
C++:
void initialiser_couche(Couche& couche, int nombre_neurones, int nombre_entrees, bool use_layer_norm, bool use_batch_norm) {
    couche.nombre_neurones = nombre_neurones;
    couche.nombre_entrees = nombre_entrees;
    couche.use_layer_norm = use_layer_norm;
    couche.use_batch_norm = use_batch_norm;
   
    // Allocation contigue pour TOUS les poids de la couche (optimization clé)
    double* poids_bloc = new double[nombre_neurones * nombre_entrees];
   
    // Allocation pour les neurones et activations
    couche.neurones = new Neurone[nombre_neurones];
    couche.activations_post_norm = new double[nombre_neurones];

    // Initialisation rapide avec distribution uniforme
    double limite = std::sqrt(6.0 / (nombre_entrees + nombre_neurones));
    std::uniform_real_distribution<double> dist(-limite * 2.0, limite * 2.0);
    std::uniform_real_distribution<double> dist_biais(-0.01, 0.01);

    // Remplir tous les poids d'un coup
    for (int i = 0; i < nombre_neurones * nombre_entrees; i++) {
        poids_bloc[i] = dist(g_rng);
    }

    // Pointer chaque neurone vers sa section du bloc
    for (int i = 0; i < nombre_neurones; i++) {
        couche.neurones[i].poids = &poids_bloc[i * nombre_entrees];
        couche.neurones[i].biais = dist_biais(g_rng);
        couche.neurones[i].activation = 0.0;
        couche.neurones[i].delta = 0.0;
        couche.neurones[i].z = 0.0;
    }

    if (use_layer_norm) initialiser_layer_norm(couche.ln, nombre_neurones);
    if (use_batch_norm) initialiser_batch_norm(couche.bn, nombre_neurones);
}

void initialiser_reseau(ReseauNeuronal& reseau, const std::vector<int>& topologie,
                        bool use_relu, bool use_layer_norm, bool use_batch_norm) {
    reseau.nombre_couches = topologie.size();
    reseau.use_relu = use_relu;
   
    // Pré-allocation du tableau de couches
    reseau.couches = new Couche[reseau.nombre_couches];

    // Initialiser toutes les couches
    for (int i = 0; i < reseau.nombre_couches; i++) {
        int nombre_entrees = (i == 0) ? topologie[i] : topologie[i - 1];
        bool ln = use_layer_norm && (i > 0 && i < reseau.nombre_couches - 1);
        bool bn = use_batch_norm && (i > 0 && i < reseau.nombre_couches - 1);
        initialiser_couche(reseau.couches[i], topologie[i], nombre_entrees, ln, bn);
    }
}
and
C++:
void detruire_reseau(ReseauNeuronal& reseau) {
    if (reseau.couches == nullptr) return;
   
    for (int c = 0; c < reseau.nombre_couches; c++) {
        Couche& couche = reseau.couches[c];
       
        // Libérer le bloc contiguë de poids (une seule fois)
        if (couche.neurones != nullptr && couche.neurones[0].poids != nullptr) {
            delete[] couche.neurones[0].poids;  // Libère tout le bloc
        }
       
        // Libérer le tableau de neurones
        delete[] couche.neurones;
        couche.neurones = nullptr;
       
        // Libérer les activations post-normalisation
        delete[] couche.activations_post_norm;
        couche.activations_post_norm = nullptr;
       
        // Libérer LayerNorm si activée
        if (couche.use_layer_norm) {
            delete[] couche.ln.gamma;
            delete[] couche.ln.beta;
            couche.ln.gamma = nullptr;
            couche.ln.beta = nullptr;
        }
       
        // Libérer BatchNorm si activée
        if (couche.use_batch_norm) {
            delete[] couche.bn.gamma;
            delete[] couche.bn.beta;
            delete[] couche.bn.running_mean;
            delete[] couche.bn.running_var;
            delete[] couche.bn.batch_mean;
            delete[] couche.bn.batch_var;
            couche.bn.gamma = nullptr;
            couche.bn.beta = nullptr;
            couche.bn.running_mean = nullptr;
            couche.bn.running_var = nullptr;
            couche.bn.batch_mean = nullptr;
            couche.bn.batch_var = nullptr;
        }
    }
   
    // Libérer le tableau de couches
    delete[] reseau.couches;
    reseau.couches = nullptr;
    reseau.nombre_couches = 0;
}
and :
C++:
gTopologie = {nombre_entrees, 256, 128, 64, 1};
with :
C++:
// ============ SAUVEGARDE/CHARGEMENT DES POIDS ============
void sauvegarder_poids(ReseauNeuronal& reseau, const std::string& nom_fichier) {
    std::ofstream fichier(nom_fichier);
    if (!fichier.is_open()) {
        std::cerr << "Erreur : impossible d'ouvrir le fichier " << nom_fichier << std::endl;
        return;
    }
    
    fichier << "couche,neurone,type,index,valeur\n";
    
    // Parcourir toutes les couches sauf la couche d'entrée (couche 0)
    for (int c = 1; c < reseau.nombre_couches; c++) {
        Couche& couche = reseau.couches[c];
        
        // Sauvegarder les biais
        for (int i = 0; i < couche.nombre_neurones; i++) {
            fichier << c << "," << i << ",biais,0,"
                    << std::fixed << std::setprecision(15)
                    << couche.neurones[i].biais << "\n";
        }
        
        // Sauvegarder les poids
        for (int i = 0; i < couche.nombre_neurones; i++) {
            for (int j = 0; j < couche.nombre_entrees; j++) {
                fichier << c << "," << i << ",poids," << j << ","
                        << std::fixed << std::setprecision(15)
                        << couche.neurones[i].poids[j] << "\n";
            }
        }
    }
    
    fichier.close();
}

void charger_poids(ReseauNeuronal& reseau, const std::string& nom_fichier) {
    std::ifstream fichier(nom_fichier);
    if (!fichier.is_open()) {
        std::cerr << "Erreur : impossible d'ouvrir le fichier " << nom_fichier << std::endl;
        return;
    }
    
    std::string ligne;
    std::getline(fichier, ligne); // Sauter l'en-tête
    
    while (std::getline(fichier, ligne)) {
        if (ligne.empty()) continue;
        
        // Remplacer les virgules par des espaces pour faciliter le parsing
        for (char& c : ligne) {
            if (c == ',') c = ' ';
        }
        
        std::istringstream flux(ligne);
        int couche_idx, neurone_idx, index;
        std::string type;
        double valeur;
        
        // Parser la ligne : couche neurone type index valeur
        if (flux >> couche_idx >> neurone_idx >> type >> index >> valeur) {
            // Vérifier que les indices sont valides
            // couche_idx >= 1 car couche 0 est l'entrée (pas de poids)
            if (couche_idx >= 1 && couche_idx < reseau.nombre_couches &&
                neurone_idx >= 0 && neurone_idx < reseau.couches[couche_idx].nombre_neurones) {
                
                if (type == "biais" && index == 0) {
                    reseau.couches[couche_idx].neurones[neurone_idx].biais = valeur;
                }
                else if (type == "poids" && index >= 0 && index < reseau.couches[couche_idx].nombre_entrees) {
                    reseau.couches[couche_idx].neurones[neurone_idx].poids[index] = valeur;
                }
            }
        }
    }
    
    fichier.close();
}
 
Last edited:
Joined
Jun 12, 2020
Messages
73
Reaction score
3
Here's the code corrected properly, first the structure:
C++:
struct Couche {
    Neurone* neurones;
    int nombre_neurones;
    int nombre_entrees;
    bool use_layer_norm;
    bool use_batch_norm;
    LayerNorm ln;
    BatchNorm bn;
    double* activations_post_norm;
    double* bloc_principal;  // ← AJOUTER CETTE LIGNE
};
Next, the functions to initialize layer and network:
C++:
// ============ HELPER POUR ALLOCATION ALIGNÉE ============
double* allocate_aligned(size_t count, size_t alignment = 64) {
    size_t bytes = count * sizeof(double);
    size_t padding = (bytes % alignment == 0) ? 0 : (alignment - (bytes % alignment));
    
    void* ptr = nullptr;
    #ifdef _WIN32
        ptr = _aligned_malloc(bytes + padding, alignment);
    #else
        ptr = std::aligned_alloc(alignment, ((bytes + padding + alignment - 1) / alignment) * alignment);
    #endif
    
    if (!ptr) throw std::bad_alloc();
    return static_cast<double*>(ptr);
}

void free_aligned(double* ptr) {
    if (ptr == nullptr) return;
    #ifdef _WIN32
        _aligned_free(ptr);
    #else
        free(ptr);
    #endif
}

void initialiser_couche(Couche& couche, int nombre_neurones, int nombre_entrees, bool use_layer_norm, bool use_batch_norm) {
    couche.nombre_neurones = nombre_neurones;
    couche.nombre_entrees = nombre_entrees;
    couche.use_layer_norm = use_layer_norm;
    couche.use_batch_norm = use_batch_norm;
    
    // ============ CALCUL DES TAILLES ============
    size_t taille_poids = (size_t)nombre_neurones * nombre_entrees;
    size_t taille_activations = (size_t)nombre_neurones;
    size_t taille_ln_gamma = use_layer_norm ? (size_t)nombre_neurones : 0;
    size_t taille_ln_beta = use_layer_norm ? (size_t)nombre_neurones : 0;
    size_t taille_bn = use_batch_norm ? (size_t)nombre_neurones * 6 : 0;
    
    size_t taille_totale = taille_poids + taille_activations + taille_ln_gamma + taille_ln_beta + taille_bn;
    
    // ============ UNE SEULE ALLOCATION CONTIGUË ============
    double* bloc_principal = allocate_aligned(taille_totale, 64);
    couche.bloc_principal = bloc_principal;  // ← STOCKER LE BLOC
    
    // ============ SUBDIVISION DU BLOC ============
    double* poids_bloc = bloc_principal;
    double* activations_ptr = poids_bloc + taille_poids;
    double* ln_gamma_ptr = activations_ptr + taille_activations;
    double* ln_beta_ptr = ln_gamma_ptr + taille_ln_gamma;
    double* bn_ptr = ln_beta_ptr + taille_ln_beta;
    
    // ============ ALLOCATION DES STRUCTURES NEURONES ============
    couche.neurones = new Neurone[nombre_neurones];
    couche.activations_post_norm = activations_ptr;
    
    // ============ INITIALISATION RAPIDE ============
    double limite = std::sqrt(6.0 / (nombre_entrees + nombre_neurones));
    std::uniform_real_distribution<double> dist(-limite * 2.0, limite * 2.0);
    std::uniform_real_distribution<double> dist_biais(-0.01, 0.01);
    
    // Remplir tous les poids en une boucle serrée
    for (int i = 0; i < (int)taille_poids; i++) {
        poids_bloc[i] = dist(g_rng);
    }
    
    // Pointeurs et initialisation des neurones
    for (int i = 0; i < nombre_neurones; i++) {
        couche.neurones[i].poids = &poids_bloc[i * nombre_entrees];
        couche.neurones[i].biais = dist_biais(g_rng);
        couche.neurones[i].activation = 0.0;
        couche.neurones[i].delta = 0.0;
        couche.neurones[i].z = 0.0;
    }
    
    // ============ INITIALISATION LAYER NORM ============
    if (use_layer_norm) {
        couche.ln.gamma = ln_gamma_ptr;
        couche.ln.beta = ln_beta_ptr;
        couche.ln.epsilon = 1e-5;
        couche.ln.nombre_features = nombre_neurones;
        
        for (int i = 0; i < nombre_neurones; i++) {
            couche.ln.gamma[i] = 1.0;
            couche.ln.beta[i] = 0.0;
        }
    } else {
        couche.ln.gamma = nullptr;
        couche.ln.beta = nullptr;
        couche.ln.epsilon = 0.0;
        couche.ln.nombre_features = 0;
    }
    
    // ============ INITIALISATION BATCH NORM ============
    if (use_batch_norm) {
        couche.bn.gamma = bn_ptr;
        couche.bn.beta = bn_ptr + nombre_neurones;
        couche.bn.running_mean = bn_ptr + 2 * nombre_neurones;
        couche.bn.running_var = bn_ptr + 3 * nombre_neurones;
        couche.bn.batch_mean = bn_ptr + 4 * nombre_neurones;
        couche.bn.batch_var = bn_ptr + 5 * nombre_neurones;
        couche.bn.momentum = 0.9;
        couche.bn.epsilon = 1e-5;
        couche.bn.nombre_features = nombre_neurones;
        
        for (int i = 0; i < nombre_neurones; i++) {
            couche.bn.gamma[i] = 1.0;
            couche.bn.beta[i] = 0.0;
            couche.bn.running_mean[i] = 0.0;
            couche.bn.running_var[i] = 1.0;
            couche.bn.batch_mean[i] = 0.0;
            couche.bn.batch_var[i] = 0.0;
        }
    } else {
        couche.bn.gamma = nullptr;
        couche.bn.beta = nullptr;
        couche.bn.running_mean = nullptr;
        couche.bn.running_var = nullptr;
        couche.bn.batch_mean = nullptr;
        couche.bn.batch_var = nullptr;
        couche.bn.momentum = 0.0;
        couche.bn.epsilon = 0.0;
        couche.bn.nombre_features = 0;
    }
}


void initialiser_reseau(ReseauNeuronal& reseau, const std::vector<int>& topologie,
                        bool use_relu, bool use_layer_norm, bool use_batch_norm) {
    reseau.nombre_couches = topologie.size();
    reseau.use_relu = use_relu;
    
    // Pré-allocation du tableau de couches avec initialisation à zéro
    reseau.couches = new Couche[reseau.nombre_couches]();
    
    // Initialiser toutes les couches
    for (int i = 0; i < reseau.nombre_couches; i++) {
        int nombre_entrees = (i == 0) ? topologie[i] : topologie[i - 1];
        bool ln = use_layer_norm && (i > 0 && i < reseau.nombre_couches - 1);
        bool bn = use_batch_norm && (i > 0 && i < reseau.nombre_couches - 1);
        initialiser_couche(reseau.couches[i], topologie[i], nombre_entrees, ln, bn);
    }
}

Then the functions to save and load the model:

C++:
void sauvegarder_poids(ReseauNeuronal& reseau, const std::string& nom_fichier) {
    std::ofstream fichier(nom_fichier);
    if (!fichier.is_open()) {
        std::cerr << "Erreur : impossible d'ouvrir le fichier " << nom_fichier << std::endl;
        return;
    }
    
    // ============ SAUVEGARDER LA TOPOLOGIE (première ligne) ============
    fichier << "topologie";
    for (int c = 0; c < reseau.nombre_couches; c++) {
        fichier << "," << reseau.couches[c].nombre_neurones;
    }
    fichier << "\n";
    
    // ============ SAUVEGARDER LES EN-TÊTES ============
    fichier << "couche,neurone,type,index,valeur\n";
    
    // ============ SAUVEGARDER LES POIDS ET BIAIS ============
    // Parcourir toutes les couches sauf la couche d'entrée (couche 0)
    for (int c = 1; c < reseau.nombre_couches; c++) {
        Couche& couche = reseau.couches[c];
        
        // Sauvegarder les biais
        for (int i = 0; i < couche.nombre_neurones; i++) {
            fichier << c << "," << i << ",biais,0,"
                    << std::fixed << std::setprecision(15)
                    << couche.neurones[i].biais << "\n";
        }
        
        // Sauvegarder les poids
        for (int i = 0; i < couche.nombre_neurones; i++) {
            for (int j = 0; j < couche.nombre_entrees; j++) {
                fichier << c << "," << i << ",poids," << j << ","
                        << std::fixed << std::setprecision(15)
                        << couche.neurones[i].poids[j] << "\n";
            }
        }
    }
    
    fichier.close();
}


void charger_poids(ReseauNeuronal& reseau, const std::string& nom_fichier, bool use_relu, bool use_layer_norm, bool use_batch_norm) {
    std::ifstream fichier(nom_fichier);
    if (!fichier.is_open()) {
        std::cerr << "Erreur : impossible d'ouvrir le fichier " << nom_fichier << std::endl;
        return;
    }
    
    std::string ligne;
    
    // ============ LIRE LA TOPOLOGIE (première ligne) ============
    if (!std::getline(fichier, ligne)) {
        std::cerr << "Erreur : fichier vide ou invalide" << std::endl;
        fichier.close();
        return;
    }
    
    std::vector<int> topologie;
    
    // Vérifier que la ligne commence par "topologie"
    if (ligne.substr(0, 9) == "topologie") {
        // Remplacer les virgules par des espaces
        for (char& c : ligne) {
            if (c == ',') c = ' ';
        }
        
        std::istringstream flux(ligne);
        std::string token;
        flux >> token; // Sauter "topologie"
        
        int nombre_neurones;
        while (flux >> nombre_neurones) {
            topologie.push_back(nombre_neurones);
        }
    } else {
        std::cerr << "Erreur : première ligne ne contient pas la topologie" << std::endl;
        fichier.close();
        return;
    }
    
    // ============ VÉRIFIER QUE LE RÉSEAU N'EST PAS DÉJÀ INITIALISÉ ============
    if (reseau.couches != nullptr) {
        std::cerr << "Erreur : le réseau est déjà initialisé. Appelez detruire_reseau() d'abord." << std::endl;
        fichier.close();
        return;
    }
    
    // ============ INITIALISER LE RÉSEAU AVEC LA TOPOLOGIE CHARGÉE ============
    initialiser_reseau(reseau, topologie, use_relu, use_layer_norm, use_batch_norm);
    
    // ============ SAUTER LA LIGNE D'EN-TÊTE ============
    if (!std::getline(fichier, ligne)) {
        std::cerr << "Erreur : fichier invalide (pas d'en-tête)" << std::endl;
        fichier.close();
        return;
    }
    
    // ============ CHARGER LES POIDS ET BIAIS ============
    while (std::getline(fichier, ligne)) {
        if (ligne.empty()) continue;
        
        // Remplacer les virgules par des espaces pour faciliter le parsing
        for (char& c : ligne) {
            if (c == ',') c = ' ';
        }
        
        std::istringstream flux(ligne);
        int couche_idx, neurone_idx, index;
        std::string type;
        double valeur;
        
        // Parser la ligne : couche neurone type index valeur
        if (flux >> couche_idx >> neurone_idx >> type >> index >> valeur) {
            // Vérifier que les indices sont valides
            if (couche_idx >= 1 && couche_idx < reseau.nombre_couches &&
                neurone_idx >= 0 && neurone_idx < reseau.couches[couche_idx].nombre_neurones) {
                
                if (type == "biais" && index == 0) {
                    reseau.couches[couche_idx].neurones[neurone_idx].biais = valeur;
                }
                else if (type == "poids" && index >= 0 && index < reseau.couches[couche_idx].nombre_entrees) {
                    reseau.couches[couche_idx].neurones[neurone_idx].poids[index] = valeur;
                }
            }
        }
    }
    
    fichier.close();
    std::cout << "Réseau chargé avec succès depuis " << nom_fichier << std::endl;
}


Then the function to free the memory:
C++:
void detruire_reseau(ReseauNeuronal& reseau) {
    if (reseau.couches == nullptr) return;
    
    for (int c = 0; c < reseau.nombre_couches; c++) {
        Couche& couche = reseau.couches[c];
        
        // Libérer le bloc contiguë de poids (une seule fois)
        if (couche.neurones != nullptr && couche.neurones[0].poids != nullptr) {
            delete[] couche.neurones[0].poids;  // Libère tout le bloc
        }
        
        // Libérer le tableau de neurones
        delete[] couche.neurones;
        couche.neurones = nullptr;
        
        // Libérer les activations post-normalisation
        delete[] couche.activations_post_norm;
        couche.activations_post_norm = nullptr;
        
        // Libérer LayerNorm si activée
        if (couche.use_layer_norm) {
            delete[] couche.ln.gamma;
            delete[] couche.ln.beta;
            couche.ln.gamma = nullptr;
            couche.ln.beta = nullptr;
        }
        
        // Libérer BatchNorm si activée
        if (couche.use_batch_norm) {
            delete[] couche.bn.gamma;
            delete[] couche.bn.beta;
            delete[] couche.bn.running_mean;
            delete[] couche.bn.running_var;
            delete[] couche.bn.batch_mean;
            delete[] couche.bn.batch_var;
            couche.bn.gamma = nullptr;
            couche.bn.beta = nullptr;
            couche.bn.running_mean = nullptr;
            couche.bn.running_var = nullptr;
            couche.bn.batch_mean = nullptr;
            couche.bn.batch_var = nullptr;
        }
    }
    
    // Libérer le tableau de couches
    delete[] reseau.couches;
    reseau.couches = nullptr;
    reseau.nombre_couches = 0;
}

And in the WindowProc function, the loop to load the weights correctly:

C++:
             else if (id == 1002) { // Charger
    AfficherMessageThreadSafe(hwnd, "[INFO] Initialisation du réseau...", false);
    AfficherStatusCouleur(3, "Chargement...");
    gDonnees = charger_csv("data.csv");
    if (gDonnees.empty()) {
        AfficherMessageThreadSafe(hwnd, "[ERREUR] Fichier data.csv non trouvé!", false);
        AfficherStatusCouleur(2, "Erreur: data.csv introuvable");
    } else {
        int nombre_entrees = gDonnees[0].features.size();
        gNombreEntrees = nombre_entrees;
        gReseau.taux_apprentissage = 0.001;
        charger_poids(gReseau, gFichierPoids, true, false, false);
 // ← charger_poids initialise le réseau avec la bonne topologie
        PostMessage(hwnd, WM_UPDATE_INPUTS, nombre_entrees, 0);
        AfficherMessageThreadSafe(hwnd, "[OK] Modèle chargé avec succès!", false);
        AfficherStatusCouleur(1, "Modèle chargé!");
        gReseauInitialise = true;
        EnableWindow(gControls.btnPredire, TRUE);
    }
}
So the network topology can be gTopologie = {nombre_entrees, 256, 128, 64, 1}; the weights are saved and loaded with no memory errors, and the predictions are relevant. Sorry about the errors in my previous message. Don't hesitate to let me know if you spot anything wrong or have any ideas—that would be great."
 
Joined
Jun 12, 2020
Messages
73
Reaction score
3
Here’s the link (1fichier.com) to the tic-tac-toe model trained with 5,000 epochs and a learning rate of 0.0005, with the neural network topology as follows: {number_of_inputs, 256, 128, 64, 1};
model
Here’s the Pastebin link to the complete C++ code for loading the model and testing it with:
Tic-Tac-Toe Neural Network (C++ App) — Load & Run Model

Please don’t wait to download the model from 1fichier.com because I’m not a paying user, and the time limit doesn’t let the file stay available for long. Also, there may be a memory management issue in the application, because when you exit by clicking the “X” in the top-right corner of the window, an error message appears. I still need to work on it. If you have any ideas or suggestions, feel free to share them.

The model is trained on a dataset that doesn’t account for the most recent player who played, and it has no strategy. It’s only a proof-of-concept code meant to be improved. Let there be no confusion.

Here’s the dataset format:
Code:
state_0,state_1,state_2,state_3,state_4,state_5,state_6,state_7,state_8,optimal_move
0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,8
0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.50,0
-0.50,0.00,0.00,0.00,0.00,0.00,0.00,0.00,0.50,6
-0.50,0.00,0.00,0.00,0.00,0.00,0.50,0.00,0.50,7
-0.50,0.00,0.00,0.00,0.00,0.00,0.50,-0.50,0.50,5
-0.50,0.00,0.00,0.00,0.00,0.50,0.50,-0.50,0.50,1
 
Last edited:

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
474,471
Messages
2,571,831
Members
48,802
Latest member
shadowoftheunknown
Top