Introduction IA : Deep learning, réseau de neurones et Tensorflow¶
Même si ce notebook ne traite pas spécialement de données géographiques, compte-tenu de l'importance prise aujourd'hui par l'IA (plus précisément le deep learning) et sachant que Python est aujourd'hui le langage privilégié pour commencer à programmer dans ce domaine, il semblait difficile de ne pas traiter dans cette série le deep learning. Pour se former au deep learning, je ne peux que conseiller de consulter le contenu de la formation Fidle, principalement sa chaine youtube et son gitlab. Fidle est une formation libre, gratuite, proposée en distanciel, par le CNRS, l’Université Grenoble Alpes et le MIAI.
La descente de gradient¶
Si vous cherchez de la documentation sur internet concernant l'intelligence artificielle (deep learing/réseau de neurones/llm), vous tomberez forcément sur la descente de gradient. En effet, à la base, derrière tous ces termes se cachent, des calculs, des mathématiques, de l'optimisation. La descente de gradient est un algorithme d'optimisation fondamental en deep learning. Son rôle principal est de minimiser la fonction de coût d'un modèle en ajustant itérativement ses paramètres. Pour faire simple, la descente de gradient essaie de tirer un maximum de bénéfices d'outils comme l'apprentissage profond, les réseaux de neurones en optimisant le mieux possible les paramètres de ces modèles. Contrairement aux méthodes de résolution plus classiquement utilisées dans le domaine de l'optimisation, par exemple pour résoudre des problèmes de localisation-allocation (https://sergelhomme.fr/notebook/Intro_opt.html), la descente de gradient est un algorithme peu performant qui peut facilement tomber dans des optimums locaux. Néanmoins, il est très rapide et dans le cadre des réseaux de neurones il se montre efficace et suffisant pour apprendre... Pour s'y familiariser, je vous conseille ces vidéos :
Dans un premier temps, on va simplement utiliser la descente de gradient pour estimer les paramètres (a et b) d'une régression linéaire de type y = ax + b. Depuis Legendre et Gauss, on dispose d'algorithmes optimaux très efficaces pour trouver les solutions à ce problème, c'est ce que l'on utilise classiquement dans les cours de statistiques (avec Excel, R ou Python) : la méthode des moindres carrés. Néanmoins, quand les jeux de données deviennent importants, ces solutions peuvent apparaitre longues à obtenir. Il n'est alors pas dénué de sens d'utiliser des méthodes comme la descente de gradient, qui très souvent ne va pas trouver la solution optimale à ce problème, mais peut s'en approcher rapidement (c'est en quelque sorte un algorithme heuristique pour ce problème). Ci-dessous, on récupère la fonction get_dataset() de l'introduction à Python (https://sergelhomme.fr/notebook/Intro_Python.html) pour générer des jeux de données pertinents pour notre problème.
import numpy as np
import matplotlib.pyplot as plt
def get_dataset(a , b, n = 100, bruit = 5, xmax = 10) :
x = xmax * np.random.rand(n, 1)
y = (a * x + b) + bruit * np.random.rand(n, 1)
return x, y
data_x, data_y = get_dataset(2, 10)
plt.scatter(data_x, data_y)
plt.show()
La fonction polytfit() résout très bien ce problème à l'aide des calculs statistiques classiques : la méthode des moindres carrés. Cette solution est optimale. On obtient les meilleurs valeurs de a et de b afin de minimiser la somme des écarts entre la régression et les données réelles (les résidus) élevés au carré (on parle de somme ou moyenne quadratique). On peut afficher l'erreur de cette approximation du jeu de données par la droite de régression, il est impossible de faire mieux dans le cadre de la minimisation des erreurs quadratiques.
a, b = np.polyfit(data_x[:,0], data_y[:,0], 1)
print("a :",a," b :",b)
plt.scatter(data_x, data_y)
r = np.array([np.min(data_x), np.max(data_x)])
plt.plot(r, a*r + b, c='red')
plt.show()
a : 2.073236306628855 b : 12.22713619269508
predictions = a * data_x + b
errors = (data_y - predictions) ** 2
cost = np.mean(errors)
print("Perte (moyenne quadratique des erreurs) :", cost)
Perte (moyenne quadratique des erreurs) : 2.073923560509483
Pour calculer ces paramètres à l'aide de la descente de gradient, on va choisir aléatoirement deux valeurs a et b pour initialiser le calcul. Ici, on les met à zéro. A partir de cela, on peut calculer une première prédiction des valeurs de y à partir des valeurs de x, notre modèle étant alors y = 0X + 0. Ainsi, notre modèle prédit y = 0 pour toutes les valeurs de x. Cela génère des erreurs (écarts, résidus) par rapport aux données réelles que l'on peut quantifier (par une fonction de coûts, une métrique de précision). Clairement actuellement notre modèle n'est pas bon, car les prédictions ne sont pas bonnes, car les écarts sont élevés, le coût (la perte) est élevé. C'est là qu'intervient notre calcul de gradient. On va alors chercher à améliorer les prédictions en calculant nos gradients et ainsi commencer notre descente de la fonction de coûts. Pour cela, on calcule la dérivée de notre fonction de coûts (ici la dérivée de la somme (moyenne) du carré des erreurs (mse)). C'est ce qui correspond dans notre code au calcul de a_gradient et b_gradient. On ajoute ces valeurs de gradients aux valeurs initiales de a et b (à zéro) en tenant compte d'un certain taux (le learning rate, dans le code lr). On peut calculer une nouvelle prédiction à partir de nos nouveaux paramètres. On recalcule les erreurs à partir de ces nouvelles prédictions. Normalement les erreurs ont diminué et on reproduit ce principe un certains nombre de fois (c'est ce que l'on appelle les époques (epochs)).
Pour faire plus bref, la descente de gradient comporte deux phases : une phase de propagation où l'on utilise les paramètres estimés (on a besoin au départ d'une initialisation aléatoire de ces paramètres) pour faire des prédictions et mesurer la qualité de ces prédictions, puis une phase de rétropropagation qui propose à l'aide du calcul des gradients (qui s'appuie sur les erreurs commises et la dérivée de la fonction de coût) de recalculer (de corriger) les paramètres du modèle pour obtenir de meilleurs résultats (de meilleures prédictions).
def gradient_descent(X, Y, a_init = 0, b_init = 0, lr = 0.01, epochs = 1000):
a, b = a_init, b_init
m = len(Y)
cost_history = []
predictions = a * X + b
cost_history.append((1 / (2 * m)) * np.sum((predictions - Y) ** 2))
for epoch in range(epochs):
predictions = a * X + b
error = predictions - Y
a_gradient = - (1 / m) * np.sum(error * X) #Le coeur de la descente de gradient, la dérivée partielle des erreurs pour a
b_gradient = - (1 / m) * np.sum(error) #Le coeur de la descente de gradient, la dérivée partielle des erreurs pour b
a = a + (lr * a_gradient)
b = b + (lr * b_gradient)
pre = a * X + b
cost_history.append((1 / m) * np.sum((pre - Y) ** 2))
return a, b, cost_history
a_est, b_est, cost_history = gradient_descent(data_x, data_y)
print('a par gradient : ', a_est, 'b par gradient :', b_est)
a par gradient : 2.1896795944921226 b par gradient : 11.468248819621769
plt.scatter(data_x, data_y)
r = np.array([np.min(data_x), np.max(data_x)])
plt.plot(r, a_est*r + b_est, c='orange', linestyle="dashed", label='RL par descente de gradient')
plt.plot(r, a*r + b, c='red', label='Régression linéaire (optimale)')
plt.legend()
plt.show()
La fonction de descente de gradient codée ci-dessus (gradient_descent()) retourne aussi la fonction de perte (de coût) que l'on cherche à minimiser. On peut afficher cette fonction. On voit alors qu'au fur et à mesure que l'on calcule nos gradients, et que l'on utilise ces gradients pour estimer nos paramètres a et b, le coût (les pertes) diminue. C'est précisément l'objectif du calcul de la descente de gradient. Pour cela, il faut connaitre la fonction calculant ces gradients, qui correspond à la dérivée de la fonction de coûts. Si on ne sait pas dériver une fonction, il est impossible de coder cette méthode. C'est pour cela notamment qu'il existe des bibliothèques qui feront les calculs pour nous. On précisera juste comment calculer cette fonction de coûts (par exemple MSE, la moyenne des écarts au carré comme c'est le cas ici).
print("Perte finale (moyenne quadratique des erreurs) :", cost_history[-1])
plt.scatter(list(range(len(cost_history))), cost_history, s=10)
plt.show()
Perte finale (moyenne quadratique des erreurs) : 2.2346248773108623
A noter que les résultats obtenus avec la descente de gradient dépendent : du choix des valeurs initiales des paramètres, de la fonction de coûts retenue, du taux d'apprentissage (learnin rate), du nombre d'itérations (nombre d'epochs). Vous pouvez essayer de changer ces paramètres pour étudier cela.
Réseau à un neurone : perceptron¶
La descente de gradient comporte les deux phases essentielles du deep learning (des réseaux de neurones) : la propagation et la rétropropagation. La propagation avant correspond à la phase où le modèle fait des prédictions à partir de paramètres estimés. La rétropropagation correspond au calcul des gradients et à la mise à jour des paramètres afin d'améliorer les futures prédictions, c'est le coeur de l'apprentissage. Mais pour coder un neurone, il faut ajouter une subtilité : une fonction d'activation. En effet, sans fonction d'activation, le modèle ne pourrait chercher à prédire que des phénomènes linéaires. Pour certains problèmes, on a aussi besoin d'avoir une sortie cohérente avec ce que l'on cherche à modéliser (pour une probabilité : une valeur comprise entre 0 et 1 (sigmoide), pour une décision : retenir la valeur maximale des possibilités (softmax)), c'est le rôle de la fonction d'activation du neurone de sortie.
Un réseau à un neurone, s'appelle un perceptron. Le perceptron permet de traiter des problématiques qui reste linéairement séparables. Néanmoins, il permet de prédire des variables binaires, contrairement à un modèle de régression linéaire classique. Dans cet exemple, on va créer un dataset (get_dataset_binaire()) qui contient des points définis par des coordonnées x et y et une variable z de type binaire. Ces coordonnées x, y peuvent correspondre à n'importe quelle variable quantitative (par exemple : x = un nombre de globules blancs ; y = un nombre de globules rouges). z est donc ici une variable binaire qui ne peut prendre que deux valeurs 0 ou 1 : par exemple 0 = personne saine ; 1 = personne malade. La fonction get_dataset_binaire() produit des personnes malades (représenté en bleu, z = 1) qui ont des globules blancs et des globules rouges faibles (x et y < 0), tandis que les personnes saines (en violet, z = 0) ont des globules blancs et des globules rouges élevés (x et y > 0). Pour permettre les calculs de gradient, les données x et y ont donc été centrées autour de zéro.
import numpy as np
import matplotlib.pyplot as plt
# premiere version de la fonction get_dataset_binaire
def get_dataset_binaire(n = 50) :
x1 = (np.random.rand(n, 1))
y1 = (np.random.rand(n, 1))
x2 = np.random.rand(n, 1) - 1
y2 = np.random.rand(n, 1) - 1
x = np.vstack([x1, x2])
y = np.vstack([y1, y2])
bin = np.concatenate((np.zeros(n), np.zeros(n) + 1))
return x, y, bin
data_x, data_y, z = get_dataset_binaire()
plt.scatter(data_x, data_y, c=z, cmap=plt.cm.Spectral)
plt.show()
La première fonction get_dataset_binaire() permet de distinguer nettement les personnes saines et malades. En effet, elle produit des valeurs x,y > 0 pour les personnes saines et des valeurs x,y < 0 pour les personnes malades. On peut travailler sur un jeu de données plus complexe en rajoutant du bruit, les nuages se superposant plus ou moins totalement en fonction du bruit.
# version finale de la fonction get_dataset_binaire
def get_dataset_binaire(n = 50, bruit = 0, xmax = 2) :
x1 = (np.random.rand(n, 1)) * xmax - bruit / 2 * xmax
y1 = (np.random.rand(n, 1)) * xmax - bruit / 2 * xmax
x2 = - xmax * (1 - bruit / 2) + ( np.random.rand(n, 1) * xmax )
y2 = - xmax * (1 - bruit / 2) + ( np.random.rand(n, 1) * xmax )
x = np.vstack([x1, x2])
y = np.vstack([y1, y2])
bin = np.concatenate((np.zeros(n), np.zeros(n) + 1))
return x, y, bin
data_x, data_y, z = get_dataset_binaire(bruit = .5, xmax = 4)
plt.scatter(data_x, data_y, c=z, cmap=plt.cm.Spectral)
plt.show()
On va donc effectuer une descente de gradient incluant une fonction d'activation pour coder un réseau d'un neurone. Mais avant de commencer, la phase de propagation nécessite une initialisation des paramètres (on a 3 paramètres : 2 poids car on a deux variables x et y ; 1 biais correspondant à une constante). Ici on crée une fonction spécifique pour cela (init_variables()). On l'utilisera qu'une seule fois dans notre code.
def init_variables():
weights = np.random.normal(size=2)
bias = 0
return weights, bias
data_x, data_y, z = get_dataset_binaire(xmax = 4) #Jeu de données sans bruit pour tester le modèle
w, b = init_variables()
print(w, b)
[ 0.36145548 -0.48962197] 0
Pour chaque couple x,y on va effectuer une somme pondérée entre les données x,y, les poids et le biais obtenus lors de l'initialisation (on va utiliser pour cela la fonction np.dot()). C'est la pré-activation. On obtient alors pour chaque couple x,y une valeur associée (une valeur synthétique). C'est le début de la propagation. Les réseaux de neurones, c'est essentiellement des calculs de sommes pondérées.
def pre_activation(features, weights, bias):
return np.dot(features, weights) + bias
z1 = pre_activation(np.column_stack((data_x,data_y)),w,b)
print("Comparaison du premier résultat obtenu avec pre-activation (z1[0]) et le détail de ce calcul : ", z1[0])
print(data_x[0], '*', w[0], '+', data_y[0], '*', w[1], '+', b, ' = ',
data_x[0] * w[0] + data_y[0] * w[1] + b)
Comparaison du premier résultat obtenu avec pre-activation (z1[0]) et le détail de ce calcul : -0.1925033891049781 [2.25661875] * 0.36145548077204015 + [2.05907958] * -0.4896219723591414 + 0 = [-0.19250339]
En prenant chaque valeur synthétique obtenue après pré-activation, on peut faire une première prédiction pour chaque couple x,y à l'aide de la fonction d'activation (ici une fonction sigmoide). En effet, la sigmoide permet tout simplement de transformer les valeurs synthétiques en valeurs comprises entre 0 et 1. Par conséquent, lorsque l'on sera proche de zéro (inférieur à 0.5), la prédiction sera 0. Lorsque l'on sera proche de un (supérieur à 0.5), la prédiction sera 0. C'est la fonction np.round() qui permet cet arrondi final. C'est alors la fin de la première propagation. A noter que cette procédure d'activation est courante dans les réseaux de neurones, mais pas obligatoire, cela dépend du type de sortie que l'on veut.
def activation(z):
return 1 / (1 + np.exp(-z)) #La fonction sigmoide
z2 = activation(z1)
print("Valeur obtenue après transformation par la sigmoide de la valeur z1[0] affichée plus haut :", z2[0])
print("Prédiction associée après arrondi :", np.round(z2[0]))
print("Valeur réelle :", z[0])
#Estimation de la prediction
print("Précision (% de bonnes prédictions après activation et arrondi) : ", np.sum(z == np.round(z2)) / len(z) * 100, "%")
print("Coût :" , np.mean((np.round(z2) - z)**2))
Valeur obtenue après transformation par la sigmoide de la valeur z1[0] affichée plus haut : 0.4520222228880919 Prédiction associée après arrondi : 0.0 Valeur réelle : 0.0 Précision (% de bonnes prédictions après activation et arrondi) : 69.0 % Coût : 0.31
Désormais, l'objectif va être de faire diminuer le taux d'erreur du modèle (qui est à l'heure actuelle un simple modèle aléatoire, les valeurs des poids et le biais étant déterminés aléatoirement dans la fonction init_variables()). Le modèle avec ces paramètres a 1 chance sur 2 de se tromper. C'est là que débute l'entrainement du modèle, c'est la phase de rétropropagation ( "Backward" ) qui consiste à recalculer les poids. Ici, comme dans la partie précédente, on va utiliser la méthode de la descente de gradient pour recalculer ces poids. On appellera cette fonction train().
def train(features, z, weights, bias):
epochs = 100
lr = 0.1
for epoch in range(epochs):
# Initialisation des gradients
gradient_w = np.zeros(weights.shape)
gradient_b = 0.
for feature, target in zip(features, z):
# prediction initiale
z1 = pre_activation(feature, weights, bias)
z2 = activation(z1)
# MAJ gradients (le coeur de la méthode de la descente de gradients)
gradient_w += (z2 - target) * (z2 * (1 - z2 )) * feature # (z2 * (1 - z2 )) DERIVATION
gradient_b += (z2 - target) * (z2 * (1 - z2 )) # (z2 * (1 - z2 )) DERIVATION
# MAJ variables
weights -= lr * gradient_w
bias -= lr * gradient_b
# Nouvelle prediction avec MAJ des variables (poids et biais)
z1 = pre_activation(features, weights, bias)
z2 = activation(z1)
predictions = np.round(z2)
# Résultat final et affichage de la précision
z1 = pre_activation(features, weights, bias)
z2 = activation(z1)
predictions = np.round(z2)
print("Précision : " , np.mean(predictions == z))
return predictions
pred = train(np.column_stack((data_x,data_y)), z, w, b)
print(pred)
Précision : 1.0 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
Ci-dessous, un exemple de prédiction avec des données bruitées. Ca marche moins bien (précision moins forte), car le bruit rend impossible une réponse exacte, sauf si on a de la chance... Pas besoin d'utiliser les fonctions pre_activation() et activation(), c'est la fonction train() qui gère tout et utilise ces fonctions.
data_x, data_y, z = get_dataset_binaire(xmax = 4, bruit = .5)
w, b = init_variables()
pred = train(np.column_stack((data_x,data_y)), z, w, b)
Précision : 0.91
Réseau à N neurones : résoudre le XOR¶
Le problème XOR (OU exclusif) est un problème de classification binaire qui illustre parfaitement la limitation des modèles linéaires, comme le perceptron simple. Le XOR est une opération logique qui prend deux entrées binaires (0 ou 1) et produit une sortie binaire selon la règle suivante : si les deux entrées sont différentes, la sortie est 1 ; si les deux entrées sont identiques, la sortie est 0. Si vous essayez de tracer une ligne droite pour séparer les points de sortie 0 des points de sortie 1, vous constaterez que c'est impossible. C'est là que réside le problème : les données XOR ne sont pas linéairement séparables. Il faudrait tracer deux lignes pour résoudre ce problème, pour cela il faut envisager un réseau de neurones à au moins deux neurones. La fonction get_dataset_xor() permet de simuler une situation XOR dans la continuité des précédents dataset.
import numpy as np
import matplotlib.pyplot as plt
def get_dataset_xor(n = 50) :
x1 = (np.random.rand(n, 1))
y1 = (np.random.rand(n, 1))
x2 = np.random.rand(n, 1) - 1
y2 = np.random.rand(n, 1) - 1
x3 = np.random.rand(n, 1)
y3 = np.random.rand(n, 1) - 1
x4 = np.random.rand(n, 1) - 1
y4 = np.random.rand(n, 1)
x = np.vstack([x1, x2, x3, x4])
y = np.vstack([y1, y2, y3, y4])
bin = np.concatenate((np.zeros(2*n), np.zeros(2*n) + 1))
return x, y, bin
data_x, data_y, z = get_dataset_xor()
plt.scatter(data_x, data_y, c=z, cmap=plt.cm.Spectral)
plt.show()
Pour visualiser le problème : https://playground.tensorflow.org/ . Pour comprendre comment le résoudre : https://www.youtube.com/watch?v=kRRRw5y0fjM . Avec un seul neurone, ce sera difficile de faire mieux que 50% de précision, mais avec un peu d'astuces on peut monter jusqu'à 66% de précision.
w, b = init_variables()
pred = train(np.column_stack((data_x,data_y)), z, w, b)
Précision : 0.545
Il est tout à fait possible de coder un réseau de deux neurones, comme on l'a fait pour un neurone avec le perceptron. Néanmoins, on comprend l'idée, de nombreux problèmes vont demander d'utiliser plusieurs neurones, voire énormément de neurones et là ce sera difficile de tout coder. Il convient donc d'utiliser des bibliothèques spécifiques conçues pour créer des réseaux de neurones. En python, il existe principalement trois bibliothèques pour cela : Tensorflow, PyTorch et Keras. Tensorflow est la bibliothèque historique, Keras s'utilise aujourd'hui avec Tensoflow, tandis que Pytorch est peut-être aujourd'hui la bibliothèque de référence dans le domaine académique. Tenserflow c'est Google, Pytorch c'est Facebook. On va utiliser ici Tensorflow. Pour l'installer, taper la ligne de commande suivante :
pip install tensorflow
On peut alors tester que c'est bien installé.
import tensorflow as tf
On commence par créer son réseau de neurones, son modèle. Pour cela, on utilise la fonction tf.keras.Sequential(). A l'intérieur de cette fonction, on place une liste avec les neurones que l'on souhaite créer. Au minimum, il y a deux couches : une couche d'entrée (la première couche dite cachée) et une couche de sortie. Le premier élément de la liste c'est la première couche, le deuxième c'est la deuxième couche, le dernier c'est la couche de sortie. Pour créer ces couches, on utilise la fonction tf.keras.layers.Dense(). Ici, on a que deux couches : une couche de six neurones pour l'entrée ; une couche de sortie avec un seul neurone. On précise la fonction d'activation pour chaque couche, ici une sigmoide, notre problème étant binaire c'est l'approche la plus simple. Pour la première couche, on donne aussi des informations sur la forme des données d'entrée (ici on indique qu'il y a 2 variables x et y).
model = tf.keras.Sequential([
tf.keras.layers.Dense(6, activation='sigmoid', input_shape=(2,)), # 6 neurones cachés
tf.keras.layers.Dense(1, activation='sigmoid') # 1 neurone de sortie
])
Ensuite, on prépare le modèle pour l'entraînement. C'est la compilation. On donne au modèle les instructions sur comment il va apprendre et comment évaluer ses performances. La compilation du modèle consiste à spécifier trois éléments clés déjà évoqué précédemment : la fonction de perte (de coûts) (la loss function, c’est la fonction mathématique que le modèle va minimiser pendant l’entraînement, elle mesure l’écart entre les prédictions du modèle et les valeurs réelles, pour un problème de classification binaire c'est généralement binary_crossentropy, pour une régression c'est généralement mean_squared_error) ; l’optimiseur (optimizer, c’est l’algorithme qui ajuste les poids du modèle pour minimiser la fonction de perte, on a présenté la descente de gradient, mais on peut aussi utiliser adam très populaire, rapide et robuste) ; les métriques (metrics, pour évaluer les performances du modèle, mais elles n’influencent pas l’apprentissage directement - contrairement à la fonction perte - on pourra par exemple mesurer la précision).
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
Puis, c'est l'entrainement. On donne les données (les variables explicatives et les variables à prédire) au modèle et on précise le nombre d'itérations (epochs) pour effectuer ces calculs.
model.fit(np.column_stack((data_x,data_y)), z, epochs=1000, verbose=0)
<keras.src.callbacks.history.History at 0x22ccb24ebd0>
On pourra alors évaluer le modèle calculé.
loss, accuracy = model.evaluate(np.column_stack((data_x,data_y)), z, verbose=0)
print(f"Perte : {loss:.4f} - Précision : {accuracy:.4f}")
Perte : 0.5612 - Précision : 0.7700
Enfin, on peut récupérer les prédictions et les afficher.
predictions = model.predict(np.column_stack((data_x,data_y)))
print("Prédictions :", (predictions > 0.5).astype(int).flatten())
plt.scatter(data_x, data_y, c=np.round(predictions), cmap=plt.cm.Spectral)
plt.show()
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step Prédictions : [0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 1 1 1 0 0 1 0 0 1 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 1 0 0 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1]
Pour mieux visualiser si le modèle a réussi à appréhender la problématique posée, il est possible d'afficher les frontières de décision. Ici, on voudrait des frontières courbées, ce qui n'est pas vraiment le cas.
fig, ax = plt.subplots()
xx, yy = np.meshgrid(np.linspace(-1, 1, 100), np.linspace(-1, 1, 100))
grid = np.c_[xx.ravel(), yy.ravel()]
predictions = model.predict(grid).reshape(xx.shape)
ax.clear()
ax.contourf(xx, yy, predictions, alpha=0.5, cmap='coolwarm')
ax.scatter(data_x, data_y, c=z, s=20, edgecolors='k', cmap='coolwarm')
ax.set_title("Frontière de décision XOR")
plt.show()
313/313 ━━━━━━━━━━━━━━━━━━━━ 0s 810us/step