Comprendre l'intelligence artificielle en la construisant. Du neurone aux LLM, avec du code et des maths.
L'intelligence artificielle, c'est quoi au juste ?
Imagine que tu veux apprendre a un enfant a reconnaitre des fruits. Tu lui montres 1000 pommes, 1000 oranges, 1000 bananes. Au debut, il se trompe. Tu corriges. Petit a petit, il apprend les motifs : la pomme est ronde et rouge, l'orange est orange et pele, la banane est longue et jaune.
L'IA fait exactement la meme chose. Sauf qu'au lieu d'un enfant, c'est un programme informatique. Et au lieu de "voir" des fruits, il "voit" des nombres.
APPRENTISSAGE HUMAIN APPRENTISSAGE MACHINE
┌─────────────┐ ┌─────────────┐
│ Regarde │ │ Lit donnees │
│ 1000 pommes │ │ (images) │
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Se trompe │ │ Predit mal │
│ au debut │ │ au debut │
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Quelqu'un │ │ Algo calcule│
│ corrige │ │ l'erreur │
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Apprend les │ │ Ajuste les │
│ motifs │ │ poids │
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Reconnait │ │ Reconnait │
│ les fruits │ │ les images │
└─────────────┘ └─────────────┘
Ces trois termes sont souvent utilises de maniere interchangeable, mais ils sont differents :
| Terme | Definition | Exemple |
|---|---|---|
| Intelligence Artificielle | Tout programme qui simule une intelligence humaine | Chatbot, pathfinding jeu video, recommandation |
| Machine Learning | Sous-ensemble de l'IA qui apprend a partir de donnees | Regression lineaire, arbre de decision, SVM |
| Deep Learning | Sous-ensemble du ML avec reseaux de neurones profonds | ChatGPT, recon. d'images, traduction auto |
┌─────────────────────────────────────────────────────────────┐ │ INTELLIGENCE ARTIFICIELLE │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ MACHINE LEARNING │ │ │ │ ┌───────────────────────────────────────────────────┐ │ │ │ │ │ DEEP LEARNING │ │ │ │ │ │ │ │ │ │ │ │ Reseau de neurones │ │ │ │ │ │ CNN, RNN, Transformers │ │ │ │ │ │ ChatGPT, GPT-4, Claude... │ │ │ │ │ └───────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ Regression, Decision Trees, Random Forest │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ Regles explicites, if/then, systemes experts │ └─────────────────────────────────────────────────────────────────┘
Dans ce tutoriel, on va construire une IA de A a Z. Pas avec des boites noires, mais en comprenant chaque brique :
Tu vas voir, c'est pas de la magie. C'est des maths. Et les maths, ca se comprend.
Accessible Avant de coder, comprenons comment ca marche dans le cerveau.
Dans ton cerveau, y'a environ 86 milliards de neurones. Chaque neurone est une cellule qui traite l'information.
NEURONE BIOLOGIQUE
Dendrites (entrees) Corps cellulaire Axone (sortie)
│ │ │
──────────┼───────────┐ │ │
│ │ │ │
──────────┼───────────┤ │ │
│ │ ┌──────────┴──────────┐ │
──────────┼───────────┼───▶│ │───────────┼───▶
│ │ │ Traitement │ │
│ │ │ (activation) │ │
│ │ └─────────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ Noyau cell. │
│ │ │
│ └──────────────────────────────────────┘
│ Synapses (connexions)
│
└─── Signaux electriques provenant d'autres neurones
Comment ca marche :
En 1943, McCulloch et Pitts ont propose un modele mathematique du neurone. C'est la base de tout le deep learning moderne.
NEURONE ARTIFICIEL
x1 ────────┬──▶ w1 ┌─────────────────────┐
│ │ │ y
x2 ────────┼──▶ w2 ──▶ Σ ──▶│ Activation f() │───▶ (sortie)
│ │ │ │
x3 ────────┼──▶ w3 │ └─────────────────────┘
│ │
b ────────┴───────────┘ (biais)
x1, x2, x3 = entrees (nombres)
w1, w2, w3 = poids (importance de chaque entree)
b = biais (seuil d'activation)
Σ = somme ponderee
f = fonction d'activation
y = sortie
En anglais :
x sont les entrees (input)w sont les poids (weights) — c'est ce que le reseau apprendb est le biais (bias) — permet de decaler la decisionf est la fonction d'activation — decide si le neurone "s'active"y est la sortie (output)| Biologique | Artificiel | Description |
|---|---|---|
| Dendrites | Entrees x | Signal entrant |
| Force synaptique | Poids w | Importance de chaque connexion |
| Seuil d'activation | Biais b | Niveau necessaire pour s'activer |
| Corps cellulaire | Somme Σ + Activation f | Traitement |
| Axone | Sortie y | Signal sortant |
w sont ce que le reseau APPREND. Au debut, ils sont aleatoires. Pendant l'entrainement, on les ajuste pour que la sortie soit correcte. C'est ca, le "machine learning" : ajuster des nombres.
Debutant Construisons notre premier neurone.
Un perceptron est un neurone unique qui prend plusieurs entrees et produit une sortie binaire (0 ou 1).
On veut distinguer pommes et oranges avec deux caracteristiques :
Pomme : x1 = 150g, x2 = 1 (rouge) ──▶ y = 1 (pomme)
Orange: x1 = 200g, x2 = 0 (orange) ──▶ y = 0 (orange)
x1 (poids) ────▶ w1 = 0.1
│
├──▶ Σ ──▶ f(Σ) ──▶ y
│
x2 (couleur) ──▶ w2 = 0.2
b (biais) ─────▶ -15
Le neurone calcule :
Si Σ > 0, alors y = 1 (pomme). Sinon y = 0 (orange).
// Perceptron simple en JavaScript
// Entrees : [poids (g), couleur (1=rouge, 0=orange)]
const pomme1 = [150, 1];
const pomme2 = [130, 1];
const orange1 = [200, 0];
const orange2 = [180, 0];
// Poids et biais (au debut, on les choisit nous-memes)
const w1 = 0.1; // Poids pour le poids du fruit
const w2 = 0.2; // Poids pour la couleur
const b = -15; // Biais
// Fonction d'activation : step function
function activation(sum) {
return sum > 0 ? 1 : 0;
}
// Prediction d'un perceptron
function predict(x1, x2) {
const sum = x1 * w1 + x2 * w2 + b;
return activation(sum);
}
// Testons !
console.log('Pomme 1:', predict(pomme1[0], pomme1[1])); // 1 (pomme)
console.log('Pomme 2:', predict(pomme2[0], pomme2[1])); // 1 (pomme)
console.log('Orange 1:', predict(orange1[0], orange1[1])); // 0 (orange)
console.log('Orange 2:', predict(orange2[0], orange2[1])); // 0 (orange)
# Perceptron simple en Python
# Entrees : [poids (g), couleur (1=rouge, 0=orange)]
pomme1 = [150, 1]
pomme2 = [130, 1]
orange1 = [200, 0]
orange2 = [180, 0]
# Poids et biais
w1 = 0.1 # Poids pour le poids du fruit
w2 = 0.2 # Poids pour la couleur
b = -15 # Biais
# Fonction d'activation : step function
def activation(somme):
return 1 if somme > 0 else 0
# Prediction d'un perceptron
def predict(x1, x2):
somme = x1 * w1 + x2 * w2 + b
return activation(somme)
# Testons !
print('Pomme 1:', predict(*pomme1)) # 1 (pomme)
print('Pomme 2:', predict(*pomme2)) # 1 (pomme)
print('Orange 1:', predict(*orange1)) # 0 (orange)
print('Orange 2:', predict(*orange2)) # 0 (orange)
La fonction d'activation decide si le neurone "s'active" ou non. Voici les plus courantes :
| Fonction | Formule | Utilisation |
|---|---|---|
| Step | 1 si x > 0, sinon 0 | Perceptron binaire |
| Sigmoide | f(x) = 1 / (1 + e^(-x)) | Probabilites (0 a 1) |
| Tanh | f(x) = (e^x - e^(-x)) / (e^x + e^(-x)) | Probabilites (-1 a 1) |
| ReLU | f(x) = max(0, x) | Deep learning (le plus utilise) |
// JavaScript
function step(x) { return x > 0 ? 1 : 0; }
function sigmoid(x) { return 1 / (1 + Math.exp(-x)); }
function tanh(x) { return Math.tanh(x); }
function relu(x) { return Math.max(0, x); }
# Python
import math
def step(x): return 1 if x > 0 else 0
def sigmoid(x): return 1 / (1 + math.exp(-x))
def tanh_act(x): return math.tanh(x)
def relu(x): return max(0, x)
Debutant+ Comment le perceptron apprend tout seul.
Dans l'exemple precedent, on a choisi les poids nous-memes. Mais en pratique, on veut que la machine les trouve elle-meme.
C'est ca, l'entrainement.
┌─────────────────────────────────────────────────────────────┐
│ BOUCLE D'ENTRAINEMENT │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ │ Entree │ │Prediction│ │ Erreur │ │ Ajuster │
│ │ x │─────▶│ y_ │─────▶│ e = y-y_│─────▶│ poids │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │
│ Repondre pour chaque exemple │
│ │
└─────────────────────────────────────────────────────────────┘
│
▼
Repeter N fois
On veut mesurer a quel point notre prediction est mauvaise. La formule la plus simple :
Si la prediction est correcte, erreur = 0.
Si la prediction est fausse, erreur = 1 ou -1.
Pour ajuster les poids, on utilise cette formule :
Ou α (alpha) est le learning rate (taux d'apprentissage), un nombre entre 0 et 1.
// Entrainement d'un perceptron en JavaScript
// Donnees d'entrainement : [poids, couleur, label]
// label: 1 = pomme, 0 = orange
const data = [
{ x: [150, 1], y: 1 }, // pomme
{ x: [130, 1], y: 1 }, // pomme
{ x: [170, 1], y: 1 }, // pomme
{ x: [200, 0], y: 0 }, // orange
{ x: [180, 0], y: 0 }, // orange
{ x: [220, 0], y: 0 }, // orange
];
// Initialisation aleatoire des poids
let w = [Math.random(), Math.random()];
let b = Math.random();
const learningRate = 0.01;
// Fonction d'activation (step)
function activation(sum) {
return sum > 0 ? 1 : 0;
}
// Prediction
function predict(x) {
const sum = x[0] * w[0] + x[1] * w[1] + b;
return activation(sum);
}
// Entrainement sur une epoch
function trainEpoch() {
let totalError = 0;
for (const example of data) {
const x = example.x;
const target = example.y;
// Prediction
const sum = x[0] * w[0] + x[1] * w[1] + b;
const prediction = activation(sum);
// Erreur
const error = target - prediction;
totalError += Math.abs(error);
// Ajustement des poids
w[0] += learningRate * error * x[0];
w[1] += learningRate * error * x[1];
b += learningRate * error;
}
return totalError;
}
// Entrainement sur plusieurs epochs
console.log('Entrainement en cours...\n');
for (let epoch = 0; epoch < 100; epoch++) {
const error = trainEpoch();
if (epoch % 10 === 0) {
console.log(`Epoch ${epoch}: Erreur totale = ${error}`);
}
}
// Test final
console.log('\nResultats apres entrainement:');
console.log('Pomme 150g rouge:', predict([150, 1])); // 1
console.log('Orange 200g orange:', predict([200, 0])); // 0
console.log('Poids appris:', w, 'Biais:', b);
# Entrainement d'un perceptron en Python
import random
# Donnees d'entrainement : [poids, couleur, label]
data = [
{'x': [150, 1], 'y': 1}, # pomme
{'x': [130, 1], 'y': 1}, # pomme
{'x': [170, 1], 'y': 1}, # pomme
{'x': [200, 0], 'y': 0}, # orange
{'x': [180, 0], 'y': 0}, # orange
{'x': [220, 0], 'y': 0}, # orange
]
# Initialisation aleatoire des poids
w = [random.random(), random.random()]
b = random.random()
learning_rate = 0.01
# Fonction d'activation (step)
def activation(somme):
return 1 if somme > 0 else 0
# Prediction
def predict(x):
somme = x[0] * w[0] + x[1] * w[1] + b
return activation(somme)
# Entrainement sur une epoch
def train_epoch():
global w, b
total_error = 0
for example in data:
x = example['x']
target = example['y']
# Prediction
somme = x[0] * w[0] + x[1] * w[1] + b
prediction = activation(somme)
# Erreur
error = target - prediction
total_error += abs(error)
# Ajustement des poids
w[0] += learning_rate * error * x[0]
w[1] += learning_rate * error * x[1]
b += learning_rate * error
return total_error
# Entrainement sur plusieurs epochs
print('Entrainement en cours...\n')
for epoch in range(100):
error = train_epoch()
if epoch % 10 == 0:
print(f'Epoch {epoch}: Erreur totale = {error}')
# Test final
print('\nResultats apres entrainement:')
print('Pomme 150g rouge:', predict([150, 1])) # 1
print('Orange 200g orange:', predict([200, 0])) # 0
print(f'Poids appris: {w}, Biais: {b}')
Pendant l'entrainement, le perceptron calcule l'erreur pour chaque exemple, puis ajuste les poids pour reduire cette erreur. Au fil des epochs (iterations), les poids convergent vers des valeurs qui permettent de bien classifier les exemples.
Epoch 0 : Poids aleatoires ──▶ Erreur = 6 ──▶ Beaucoup de mauvaises predictions Epoch 10 : Poids s'ajustent ──▶ Erreur = 2 ──▶ Moins d'erreurs Epoch 50 : Poids convergent ──▶ Erreur = 0 ──▶ Toutes les predictions sont correctes !
Intermediaire Passons d'un neurone a un vrai reseau.
Un perceptron simple ne peut tracer qu'une ligne droite pour separer les classes. Pour des problemes plus complexes (comme XOR), il faut plusieurs neurones connectes en couches.
RESEAU MULTI-COUCHES (MLP)
COUCHE COUCHE COUCHE
D'ENTREE CACHEE DE SORTIE
x1 ───┬────▶ h1 ───┬────▶ y1
│ │ │
│ ┌──┴──┐ │
└───▶│ h2 │───┴───▶ y1
│ └──┬──┘
│ │
x2 ───┴────▶ h3 ───┘
x1, x2 = entrees (2 neurones d'entree)
h1, h2, h3 = neurones caches (3 neurones)
y1 = sortie (1 neurone de sortie)
Chaque fleche = un poids (w)
Ce reseau a: 2×3 + 3×1 = 9 poids + biais
Chaque couche transforme les donnees de maniere non-lineaire. Avec plusieurs couches, on peut approximer n'importe quelle fonction. C'est le theoreme d'approximation universelle.
XOR (ou exclusif) est un probleme mythique que le perceptron simple ne peut pas resoudre :
| x1 | x2 | XOR |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
Impossible de separer les 0 et les 1 avec une seule ligne ! Il faut un MLP.
// Reseau multi-couches (MLP) pour XOR en JavaScript
// Fonctions d'activation
function sigmoid(x) { return 1 / (1 + Math.exp(-x)); }
function sigmoidDerivative(x) { return x * (1 - x); }
// Donnees XOR
const X = [
[0, 0],
[0, 1],
[1, 0],
[1, 1]
];
const Y = [[0], [1], [1], [0]];
// Initialisation des poids (aleatoires)
const inputSize = 2;
const hiddenSize = 4;
const outputSize = 1;
// Poids entree -> cachee
const W1 = [];
for (let i = 0; i < inputSize; i++) {
W1.push([]);
for (let j = 0; j < hiddenSize; j++) {
W1[i].push(Math.random() * 2 - 1);
}
}
const b1 = Array(hiddenSize).fill(0).map(() => Math.random() * 2 - 1);
// Poids cachee -> sortie
const W2 = [];
for (let i = 0; i < hiddenSize; i++) {
W2.push([]);
for (let j = 0; j < outputSize; j++) {
W2[i].push(Math.random() * 2 - 1);
}
}
const b2 = Array(outputSize).fill(0).map(() => Math.random() * 2 - 1);
// Forward pass
function forward(x) {
// Couche cachee
const hidden = [];
for (let j = 0; j < hiddenSize; j++) {
let sum = b1[j];
for (let i = 0; i < inputSize; i++) {
sum += x[i] * W1[i][j];
}
hidden.push(sigmoid(sum));
}
// Couche de sortie
const output = [];
for (let j = 0; j < outputSize; j++) {
let sum = b2[j];
for (let i = 0; i < hiddenSize; i++) {
sum += hidden[i] * W2[i][j];
}
output.push(sigmoid(sum));
}
return { hidden, output };
}
// Entrainement
const learningRate = 0.5;
const epochs = 10000;
console.log('Entrainement du MLP pour XOR...\n');
for (let epoch = 0; epoch < epochs; epoch++) {
let totalError = 0;
for (let s = 0; s < X.length; s++) {
const x = X[s];
const y = Y[s];
// Forward
const { hidden, output } = forward(x);
// Erreur
const error = y[0] - output[0];
totalError += error * error;
// Backward (retropropagation simplifiee)
const deltaOutput = error * sigmoidDerivative(output[0]);
// Mise a jour des poids cachee -> sortie
for (let i = 0; i < hiddenSize; i++) {
W2[i][0] += learningRate * deltaOutput * hidden[i];
}
b2[0] += learningRate * deltaOutput;
// Erreur cachee
for (let i = 0; i < hiddenSize; i++) {
const deltaHidden = deltaOutput * W2[i][0] * sigmoidDerivative(hidden[i]);
for (let j = 0; j < inputSize; j++) {
W1[j][i] += learningRate * deltaHidden * x[j];
}
b1[i] += learningRate * deltaHidden;
}
}
if (epoch % 1000 === 0) {
console.log(`Epoch ${epoch}: Erreur = ${totalError.toFixed(6)}`);
}
}
// Test final
console.log('\nResultats apres entrainement:');
for (let s = 0; s < X.length; s++) {
const { output } = forward(X[s]);
console.log(`XOR(${X[s][0]}, ${X[s][1]}) = ${output[0].toFixed(4)} (attendu: ${Y[s][0]})`);
}
# Reseau multi-couches (MLP) pour XOR en Python
import random
import math
# Fonctions d'activation
def sigmoid(x):
return 1 / (1 + math.exp(-x))
def sigmoid_derivative(x):
return x * (1 - x)
# Donnees XOR
X = [[0, 0], [0, 1], [1, 0], [1, 1]]
Y = [[0], [1], [1], [0]]
# Dimensions
input_size = 2
hidden_size = 4
output_size = 1
# Initialisation des poids
W1 = [[random.uniform(-1, 1) for _ in range(hidden_size)] for _ in range(input_size)]
b1 = [random.uniform(-1, 1) for _ in range(hidden_size)]
W2 = [[random.uniform(-1, 1) for _ in range(output_size)] for _ in range(hidden_size)]
b2 = [random.uniform(-1, 1) for _ in range(output_size)]
# Forward pass
def forward(x):
# Couche cachee
hidden = []
for j in range(hidden_size):
s = b1[j]
for i in range(input_size):
s += x[i] * W1[i][j]
hidden.append(sigmoid(s))
# Couche de sortie
output = []
for j in range(output_size):
s = b2[j]
for i in range(hidden_size):
s += hidden[i] * W2[i][j]
output.append(sigmoid(s))
return hidden, output
# Entrainement
learning_rate = 0.5
epochs = 10000
print('Entrainement du MLP pour XOR...\n')
for epoch in range(epochs):
total_error = 0
for s in range(len(X)):
x = X[s]
y = Y[s]
# Forward
hidden, output = forward(x)
# Erreur
error = y[0] - output[0]
total_error += error * error
# Backward
delta_output = error * sigmoid_derivative(output[0])
# Mise a jour W2, b2
for i in range(hidden_size):
W2[i][0] += learning_rate * delta_output * hidden[i]
b2[0] += learning_rate * delta_output
# Erreur cachee
for i in range(hidden_size):
delta_hidden = delta_output * W2[i][0] * sigmoid_derivative(hidden[i])
for j in range(input_size):
W1[j][i] += learning_rate * delta_hidden * x[j]
b1[i] += learning_rate * delta_hidden
if epoch % 1000 == 0:
print(f'Epoch {epoch}: Erreur = {total_error:.6f}')
# Test final
print('\nResultats apres entrainement:')
for s in range(len(X)):
_, output = forward(X[s])
print(f'XOR({X[s][0]}, {X[s][1]}) = {output[0]:.4f} (attendu: {Y[s][0]})')
Apres 10000 epochs, le reseau devrait resoudre XOR correctement. C'est la base du deep learning : empiler des couches, entrainer avec la backpropagation, et le reseau apprend des representations complexes.
Avance L'algorithme fondamental du deep learning, avec les maths detaillees.
On a un reseau avec plusieurs couches. Chaque couche a des poids. Comment ajuster les poids des couches cachees ?
C'est la question qu'a resolu la backpropagation (retropropagation du gradient).
Imagine que tu veux minimiser l'erreur a la sortie. L'erreur depend des poids de la derniere couche, qui depend des poids de la couche precedente, etc.
Utilisant la regle de la chaine (chain rule) du calcul differentiel, on peut propager l'erreur vers l'arriere et calculer le gradient pour chaque poids.
Si y = f(g(x)), alors :
On peut decomposer une fonction complexe en fonctions simples et multiplier les derivees.
Pour un reseau simple (entree → cachee → sortie), on a :
On utilise souvent la MSE (Mean Squared Error) :
On veut calculer ∂L/∂W₁ et ∂L/∂W₂ pour mettre a jour les poids.
En appliquant la regle de la chaine :
Calculons chaque terme :
Donc :
Ou δ₂ = (ŷ - y) × ŷ(1 - ŷ) est l'erreur de la couche de sortie.
C'est plus complexe car l'erreur doit traverser la deuxieme couche :
Les nouveaux termes :
Donc :
Ou δ₁ = δ₂ × W₂ × h(1 - h) est l'erreur propagee vers la couche cachee.
La backpropagation suit ces etapes :
FORWARD: x ──▶ z1 ──▶ h ──▶ z2 ──▶ ŷ ──▶ L
│ │ │ │ │ │
│ │ │ │ │ │
BACKWARD: │◀─────│◀─────│◀─────│◀─────│◀─────│
│ │ │ │ │ │
∂L/∂W1 δ1 ∂L/∂W2 δ2 ∂L/∂ŷ (erreur)
// Backpropagation detaillee avec commentaires
// Reseau: entree(2) -> cachee(3) -> sortie(1)
const sigmoid = x => 1 / (1 + Math.exp(-x));
const sigmoidDeriv = y => y * (1 - y);
// Donnees
const X = [[0,0], [0,1], [1,0], [1,1]];
const Y = [[0], [1], [1], [0]];
// Dimensions
const n_input = 2, n_hidden = 3, n_output = 1;
// Poids (initialises aleatoirement)
let W1 = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]; // 2x3
let b1 = [0.1, 0.2, 0.3]; // 3
let W2 = [[0.7], [0.8], [0.9]]; // 3x1
let b2 = [0.1]; // 1
// FORWARD PASS pour un exemple
function forward(x) {
// z1 = W1·x + b1
const z1 = [];
for (let j = 0; j < n_hidden; j++) {
let sum = b1[j];
for (let i = 0; i < n_input; i++) {
sum += W1[i][j] * x[i];
}
z1.push(sum);
}
// h = sigmoid(z1)
const h = z1.map(sigmoid);
// z2 = W2·h + b2
const z2 = [];
for (let j = 0; j < n_output; j++) {
let sum = b2[j];
for (let i = 0; i < n_hidden; i++) {
sum += W2[i][j] * h[i];
}
z2.push(sum);
}
// y_pred = sigmoid(z2)
const y_pred = z2.map(sigmoid);
return { z1, h, z2, y_pred };
}
// BACKWARD PASS avec tout le detail
function backward(x, y_true, forward_result, lr = 0.5) {
const { z1, h, z2, y_pred } = forward_result;
// 1. Erreur a la sortie: delta2 = (y_pred - y_true) * sigmoid'(z2)
const delta2 = [];
for (let j = 0; j < n_output; j++) {
const error = y_pred[j] - y_true[j];
delta2.push(error * sigmoidDeriv(y_pred[j])); // delta2 = erreur * derivee
}
// 2. Gradient pour W2: dL/dW2 = delta2 * h
const dW2 = [];
for (let i = 0; i < n_hidden; i++) {
dW2.push([]);
for (let j = 0; j < n_output; j++) {
dW2[i].push(delta2[j] * h[i]); // gradient = erreur * activation precedente
}
}
// 3. Gradient pour b2: dL/db2 = delta2
const db2 = [...delta2];
// 4. Erreur de la couche cachee: delta1 = delta2 * W2 * sigmoid'(h)
const delta1 = [];
for (let i = 0; i < n_hidden; i++) {
let sum = 0;
for (let j = 0; j < n_output; j++) {
sum += delta2[j] * W2[i][j]; // propagation de l'erreur
}
delta1.push(sum * sigmoidDeriv(h[i])); // fois la derivee
}
// 5. Gradient pour W1: dL/dW1 = delta1 * x
const dW1 = [];
for (let i = 0; i < n_input; i++) {
dW1.push([]);
for (let j = 0; j < n_hidden; j++) {
dW1[i].push(delta1[j] * x[i]); // gradient = erreur * entree
}
}
// 6. Gradient pour b1: dL/db1 = delta1
const db1 = [...delta1];
// 7. Mise a jour des poids: W = W - lr * dW
for (let i = 0; i < n_hidden; i++) {
for (let j = 0; j < n_output; j++) {
W2[i][j] -= lr * dW2[i][j];
}
}
for (let j = 0; j < n_output; j++) {
b2[j] -= lr * db2[j];
}
for (let i = 0; i < n_input; i++) {
for (let j = 0; j < n_hidden; j++) {
W1[i][j] -= lr * dW1[i][j];
}
}
for (let j = 0; j < n_hidden; j++) {
b1[j] -= lr * db1[j];
}
}
// Entrainement
console.log('Entrainement avec backpropagation detaillee...\n');
for (let epoch = 0; epoch < 10000; epoch++) {
let totalLoss = 0;
for (let s = 0; s < X.length; s++) {
const result = forward(X[s]);
const loss = 0.5 * Math.pow(Y[s][0] - result.y_pred[0], 2);
totalLoss += loss;
backward(X[s], Y[s], result);
}
if (epoch % 1000 === 0) console.log(`Epoch ${epoch}: Loss = ${totalLoss.toFixed(6)}`);
}
// Test
console.log('\nPredictions:');
for (let s = 0; s < X.length; s++) {
const { y_pred } = forward(X[s]);
console.log(`XOR(${X[s][0]}, ${X[s][1]}) = ${y_pred[0].toFixed(4)}`);
}
En codant chaque etape nous-memes, tu as compris :
Maintenant, tu sais ce que fait PyTorch/TensorFlow sous le capot.
| From scratch | Framework (PyTorch/TensorFlow) |
|---|---|
| Comprendre | Etre productif |
| Implémenter chaque detail | Utiliser des fonctions optimisees |
| CPU uniquement (en general) | GPU acceleration automatique |
| Derivees a la main | Autograd (derivees automatiques) |
| Pas de parallelisation | Parallelisation sur GPU |
| Numeros de lignes de code | Quelques lignes |
Le meme reseau XOR en PyTorch:
import torch
import torch.nn as nn
# Definition du modele (3 lignes au lieu de 100+)
model = nn.Sequential(
nn.Linear(2, 4), # entree -> cachee
nn.Sigmoid(),
nn.Linear(4, 1), # cachee -> sortie
nn.Sigmoid()
)
# Fonction de perte et optimiseur
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.5)
# Donnees
X = torch.tensor([[0,0], [0,1], [1,0], [1,1]], dtype=torch.float32)
Y = torch.tensor([[0], [1], [1], [0]], dtype=torch.float32)
# Entrainement (la backprop est automatique !)
for epoch in range(10000):
optimizer.zero_grad()
outputs = model(X)
loss = criterion(outputs, Y)
loss.backward() # Backprop automatique
optimizer.step()
# Test
print(model(X))
Debutant+ Transformer du texte en vecteurs numeriques.
Un embedding est une representation numerique d'un texte (ou d'une image, d'un son...). C'est un vecteur de nombres qui capture le "sens" du texte.
Par exemple, les phrases "Le chat dort" et "Le chat fait une sieste" auront des embeddings similaires parce qu'elles ont le meme sens.
TEXTE EMBEDDING
"Le chat dort" ──▶ [0.12, -0.34, 0.56, 0.23, ...] (768 nombres)
"Le chat fait une" ──▶ [0.11, -0.32, 0.55, 0.22, ...] (tres similaire)
"sieste"
"La voiture roule" ──▶ [-0.45, 0.23, 0.12, -0.67, ...] (different)
Pour comparer deux embeddings, on utilise la similarite cosinus :
Le resultat est entre -1 et 1. Plus c'est proche de 1, plus les vecteurs sont similaires.
Ollama fournit une API d'embeddings. Tu peux l'utiliser localement ou avec ton abonnement cloud.
// Embeddings avec Ollama en JavaScript
const OLLAMA_URL = 'http://localhost:11434'; // Ou ton URL Ollama Cloud
const MODEL = 'nomic-embed-text'; // Modele d'embeddings
// Obtenir un embedding
async function getEmbedding(text) {
const response = await fetch(`${OLLAMA_URL}/api/embeddings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: MODEL,
prompt: text
})
});
const data = await response.json();
return data.embedding;
}
// Similarite cosinus
function cosineSimilarity(a, b) {
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
// Test
async function main() {
const texts = [
'Le chat dort sur le canape',
'Le chat fait une sieste',
'La voiture roule sur la route',
'JavaScript est un langage de programmation'
];
console.log('Generation des embeddings...\n');
const embeddings = [];
for (const text of texts) {
const emb = await getEmbedding(text);
embeddings.push(emb);
console.log(`"${text}" -> [${emb.length} valeurs]`);
}
console.log('\n Similarites cosinus:\n');
for (let i = 0; i < texts.length; i++) {
for (let j = i + 1; j < texts.length; j++) {
const sim = cosineSimilarity(embeddings[i], embeddings[j]);
console.log(`"${texts[i]}"`);
console.log(` vs "${texts[j]}"`);
console.log(` -> Similarite: ${(sim * 100).toFixed(1)}%\n`);
}
}
}
main();
# Embeddings avec Ollama en Python
import requests
import math
OLLAMA_URL = 'http://localhost:11434' # Ou ton URL Ollama Cloud
MODEL = 'nomic-embed-text' # Modele d'embeddings
# Obtenir un embedding
def get_embedding(text):
response = requests.post(f'{OLLAMA_URL}/api/embeddings', json={
'model': MODEL,
'prompt': text
})
return response.json()['embedding']
# Similarite cosinus
def cosine_similarity(a, b):
dot_product = sum(x * y for x, y in zip(a, b))
norm_a = math.sqrt(sum(x ** 2 for x in a))
norm_b = math.sqrt(sum(x ** 2 for x in b))
return dot_product / (norm_a * norm_b)
# Test
texts = [
'Le chat dort sur le canape',
'Le chat fait une sieste',
'La voiture roule sur la route',
'JavaScript est un langage de programmation'
]
print('Generation des embeddings...\n')
embeddings = []
for text in texts:
emb = get_embedding(text)
embeddings.append(emb)
print(f'"{text}" -> [{len(emb)} valeurs]')
print('\nSimilarites cosinus:\n')
for i in range(len(texts)):
for j in range(i + 1, len(texts)):
sim = cosine_similarity(embeddings[i], embeddings[j])
print(f'"{texts[i]}"')
print(f' vs "{texts[j]}"')
print(f' -> Similarite: {sim * 100:.1f}%\n')
Intermediaire Utiliser les embeddings pour classifier des textes.
Classifier automatiquement des avis clients en "positif" ou "negatif".
// Classification de texte avec embeddings
const OLLAMA_URL = 'http://localhost:11434';
const MODEL = 'nomic-embed-text';
// Donnees d'entrainement
const trainingData = [
{ text: 'Super produit, je recommande !', label: 'positif' },
{ text: 'Excellent, tres satisfaite', label: 'positif' },
{ text: 'Parfait, livraison rapide', label: 'positif' },
{ text: 'Tres deçu, mauvaise qualité', label: 'negatif' },
{ text: 'Nul, ne fonctionne pas', label: 'negatif' },
{ text: 'Arnaque, a eviter', label: 'negatif' },
];
async function getEmbedding(text) {
const response = await fetch(`${OLLAMA_URL}/api/embeddings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: MODEL, prompt: text })
});
return (await response.json()).embedding;
}
function cosineSimilarity(a, b) {
let dotProduct = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
// Calculer l'embedding moyen d'une classe
function meanEmbedding(embeddings) {
const n = embeddings[0].length;
const mean = new Array(n).fill(0);
for (const emb of embeddings) {
for (let i = 0; i < n; i++) {
mean[i] += emb[i];
}
}
return mean.map(x => x / embeddings.length);
}
async function trainClassifier(data) {
console.log('Entrainement du classificateur...\n');
const positifEmbeddings = [];
const negatifEmbeddings = [];
for (const item of data) {
const emb = await getEmbedding(item.text);
if (item.label === 'positif') {
positifEmbeddings.push(emb);
} else {
negatifEmbeddings.push(emb);
}
console.log(` "${item.text}" -> ${item.label}`);
}
return {
positif: meanEmbedding(positifEmbeddings),
negatif: meanEmbedding(negatifEmbeddings)
};
}
function predict(text, centroids) {
return getEmbedding(text).then(emb => {
const simPos = cosineSimilarity(emb, centroids.positif);
const simNeg = cosineSimilarity(emb, centroids.negatif);
return {
label: simPos > simNeg ? 'positif' : 'negatif',
confidence: Math.max(simPos, simNeg) / (simPos + simNeg)
};
});
}
// Main
async function main() {
const centroids = await trainClassifier(trainingData);
console.log('\nPredictions:');
const testTexts = [
'Genial, tres content',
'Horrible, perte d\'argent',
'Correct mais sans plus'
];
for (const text of testTexts) {
const result = await predict(text, centroids);
console.log(`"${text}"`);
console.log(` -> ${result.label} (confiance: ${(result.confidence * 100).toFixed(1)}%)\n`);
}
}
main();
# Classification de texte avec embeddings
import requests
import math
OLLAMA_URL = 'http://localhost:11434'
MODEL = 'nomic-embed-text'
# Donnees d'entrainement
training_data = [
{'text': 'Super produit, je recommande !', 'label': 'positif'},
{'text': 'Excellent, tres satisfaite', 'label': 'positif'},
{'text': 'Parfait, livraison rapide', 'label': 'positif'},
{'text': 'Tres decu, mauvaise qualite', 'label': 'negatif'},
{'text': 'Nul, ne fonctionne pas', 'label': 'negatif'},
{'text': 'Arnaque, a eviter', 'label': 'negatif'},
]
def get_embedding(text):
response = requests.post(f'{OLLAMA_URL}/api/embeddings', json={
'model': MODEL,
'prompt': text
})
return response.json()['embedding']
def cosine_similarity(a, b):
dot_product = sum(x * y for x, y in zip(a, b))
norm_a = math.sqrt(sum(x ** 2 for x in a))
norm_b = math.sqrt(sum(x ** 2 for x in b))
return dot_product / (norm_a * norm_b)
def mean_embedding(embeddings):
n = len(embeddings[0])
mean = [0] * n
for emb in embeddings:
for i in range(n):
mean[i] += emb[i]
return [x / len(embeddings) for x in mean]
def train_classifier(data):
print('Entrainement du classificateur...\n')
positif_embeddings = []
negatif_embeddings = []
for item in data:
emb = get_embedding(item['text'])
if item['label'] == 'positif':
positif_embeddings.append(emb)
else:
negatif_embeddings.append(emb)
print(f" \"{item['text']}\" -> {item['label']}")
return {
'positif': mean_embedding(positif_embeddings),
'negatif': mean_embedding(negatif_embeddings)
}
def predict(text, centroids):
emb = get_embedding(text)
sim_pos = cosine_similarity(emb, centroids['positif'])
sim_neg = cosine_similarity(emb, centroids['negatif'])
return {
'label': 'positif' if sim_pos > sim_neg else 'negatif',
'confidence': max(sim_pos, sim_neg) / (sim_pos + sim_neg)
}
# Main
centroids = train_classifier(training_data)
print('\nPredictions:')
test_texts = [
'Genial, tres content',
'Horrible, perte d\'argent',
'Correct mais sans plus'
]
for text in test_texts:
result = predict(text, centroids)
print(f'"{text}"')
print(f' -> {result["label"]} (confiance: {result["confidence"] * 100:.1f}%)\n')
Accessible Comment fonctionne un modele comme GPT ?
LLM = Large Language Model. C'est un modele de langage entraine sur des milliards de textes pour predire le mot suivant.
Un token est une unite de texte. Ca peut etre un mot, une partie de mot, ou meme un caractere.
TEXTE: "Bonjour le monde" TOKENISATION: "Bonjour" -> [1234] " le" -> [567] " monde" -> [890] EMBEDDINGS: [1234] -> [0.12, -0.34, ...] (768 nombres) [567] -> [0.45, 0.23, ...] [890] -> [-0.11, 0.67, ...]
L'attention permet au modele de "se souvenir" du contexte. Quand le modele predit un mot, il regarde tous les mots precedents et calcule leur importance.
Par exemple, dans "Le chat mange la souris car il a ..."
Le modele apprend ces relations pendant l'entrainement.
Un LLM predit le mot suivant en calculant une probabilite pour chaque mot du vocabulaire :
Le mot avec la plus haute probabilite est choisi (ou on echantillonne parmi les plus probables).
| Modele | Parametres | Taille |
|---|---|---|
| GPT-2 | 1.5 milliards | ~6 Go |
| GPT-3 | 175 milliards | ~350 Go |
| LLaMA-70B | 70 milliards | ~140 Go |
| GPT-4 | ~1 trillion (estimation) | ? |
Ces parametres sont les poids du reseau (comme les W1, W2 qu'on a vus), mais en beaucoup plus grand.
Voila un exemple simple d'API Node.js qui sert un modele d'embeddings :
// api-embeddings.js - API simple pour les embeddings
const http = require('http');
const url = require('url');
const OLLAMA_URL = 'http://localhost:11434';
const PORT = 3000;
async function getEmbedding(text) {
const response = await fetch(`${OLLAMA_URL}/api/embeddings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'nomic-embed-text',
prompt: text
})
});
return (await response.json()).embedding;
}
const server = http.createServer(async (req, res) => {
const parsedUrl = url.parse(req.url, true);
res.setHeader('Content-Type', 'application/json');
if (parsedUrl.pathname === '/embed' && req.method === 'POST') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', async () => {
try {
const { text } = JSON.parse(body);
const embedding = await getEmbedding(text);
res.writeHead(200);
res.end(JSON.stringify({ success: true, embedding }));
} catch (error) {
res.writeHead(400);
res.end(JSON.stringify({ success: false, error: error.message }));
}
});
} else if (parsedUrl.pathname === '/health') {
res.writeHead(200);
res.end(JSON.stringify({ status: 'ok' }));
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: 'Not found' }));
}
});
server.listen(PORT, () => {
console.log(`API d'embeddings sur http://localhost:${PORT}`);
console.log('POST /embed { "text": "votre texte" }');
});
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production || true
COPY . .
EXPOSE 3000
CMD ["node", "api-embeddings.js"]
services:
api-embeddings:
build: .
container_name: api-embeddings
restart: unless-stopped
ports:
- "3000:3000"
environment:
- OLLAMA_URL=http://host.docker.internal:11434
extra_hosts:
- "host.docker.internal:host-gateway"