Faza 03 · lecția 01

Perceptronul

Scopul lecției: Perceptronul este atomul rețelelor neurale. Desfaceți-l și găsiți ponderi, un bias și o decizie.

Versiunea curentă AlexBred.com: primele 100 de lecții ale programului în limba română.

Curs
AI Engineering from Scratch
Fază
Fundamentele învățării profunde
Lectură
16 min.
Verificat
Cuprinsul lecției
  1. Obiective de învățare
  2. Problema
  3. Conceptul
  4. Un neuron, o decizie
  5. Frontiera de decizie
  6. Regula de învățare
  7. Problema XOR
  8. Construiți
  9. Pasul 1: clasa Perceptron
  10. Pasul 2: antrenați pe porți logice
  11. Pasul 3: observați eșecul XOR
  12. Pasul 4: rezolvați XOR cu două straturi
  13. Pasul 5: antrenați o rețea cu două straturi
  14. Folosiți
  15. Livrați
  16. Exerciții
  17. Termeni-cheie
  18. Lecturi suplimentare

Perceptronul este atomul rețelelor neurale. Desfaceți-l și găsiți ponderi, un bias și o decizie.

Tip: Construire Limbaje: Python Cerințe preliminare: Faza 1 — Intuiția algebrei liniare Durată: ~60 de minute

Obiective de învățare

  • Să implementați un perceptron de la zero în Python, inclusiv regula de actualizare a ponderilor și funcția de activare treaptă.
  • Să explicați de ce un singur perceptron poate rezolva numai probleme separabile liniar și să demonstrați eșecul în cazul XOR.
  • Să construiți un perceptron multistrat prin compunerea porților OR, NAND și AND pentru a rezolva XOR.
  • Să antrenați o rețea cu două straturi, activare sigmoidă și retropropagare pentru a învăța automat XOR.

Problema

Cunoașteți vectorii și produsele scalare. Știți că o matrice transformă intrări în ieșiri. Dar cum învață o mașină ce transformare să folosească?

Perceptronul răspunde la această întrebare. Este cea mai simplă mașină de învățare posibilă: primește câteva intrări, le înmulțește cu ponderi, adaugă un bias și ia o decizie binară. Apoi se ajustează. Atât. Fiecare rețea neurală construită vreodată constă din straturi ale acestei idei așezate unul peste altul.

A înțelege perceptronul înseamnă a înțelege ce înseamnă de fapt „învățarea” în cod: ajustarea unor numere până când ieșirea corespunde realității.

Notă tehnică a traducerii: Rețelele moderne nu sunt literalmente stive de perceptroni clasici cu funcție treaptă. Ele folosesc de regulă transformări afine, activări diferențiabile aproape peste tot și componente precum atenția, normalizarea sau conexiunile reziduale. Perceptronul rămâne un model didactic fundamental, nu o descriere completă a fiecărei arhitecturi.

Conceptul

Un neuron, o decizie

Un perceptron primește n intrări, o înmulțește pe fiecare cu o pondere, le însumează, adaugă un bias și trece rezultatul printr-o funcție de activare.

Диаграмма к уроку «Perceptronul»

Funcția treaptă este necruțătoare: dacă suma ponderată plus biasul este >= 0, ieșirea este 1. Altfel, ieșirea este 0.

treaptă(z) = 1  dacă z >= 0
             0  dacă z < 0

Acesta este un clasificator liniar. Ponderile și biasul definesc o dreaptă — sau un hiperplan în dimensiuni mai mari — care împarte spațiul intrărilor în două regiuni.

Frontiera de decizie

Pentru două intrări, perceptronul trasează o dreaptă în spațiul bidimensional:

  x2

  │  Clasa 1        /
  │    (0)          /
  │                /
  │               / w1·x1 + w2·x2 + b = 0
  │              /
  │             /     Clasa 2
  │            /        (1)
  ┼───────────/──────────── x1

Tot ce se află de o parte a dreptei produce ieșirea 0. Tot ce se află de cealaltă parte produce 1. Antrenarea deplasează această dreaptă până când separă corect clasele.

Regula de învățare

Regula de învățare a perceptronului este simplă:

Pentru fiecare exemplu de antrenare (x, y_real):
    y_prezis = prezice(x)
    eroare = y_real - y_prezis

    Pentru fiecare pondere:
        w_i = w_i + rata_de_învățare * eroare * x_i
    bias = bias + rata_de_învățare * eroare

Dacă predicția este corectă, eroare = 0 și nimic nu se schimbă. Dacă prezice 0, dar ar trebui să fie 1, ponderile cresc. Dacă prezice 1, dar ar trebui să fie 0, ponderile scad. Rata de învățare controlează mărimea fiecărei ajustări.

Notă tehnică a traducerii: Afirmațiile „ponderile cresc” sau „scad” presupun intrări nenegative, ca în porțile logice de aici; semnul actualizării fiecărei ponderi depinde și de x_i. Pentru date separabile liniar și o margine pozitivă, teorema convergenței perceptronului garantează un număr finit de greșeli în condițiile algoritmului, dar nu pentru XOR.

Problema XOR

Aici se rupe mecanismul. Priviți aceste porți logice:

Poarta AND:         Poarta OR:          Poarta XOR:
x1  x2  ieșire      x1  x2  ieșire      x1  x2  ieșire
0   0   0           0   0   0           0   0   0
0   1   0           0   1   1           0   1   1
1   0   0           1   0   1           1   0   1
1   1   1           1   1   1           1   1   0

AND și OR sunt separabile liniar: puteți trasa o singură dreaptă pentru a separa zerourile de unități. XOR nu este. Nicio dreaptă nu poate separa [0,1] și [1,0] de [0,0] și [1,1].

AND — separabil:        XOR — neseparabil:

  x2                      x2
  1 ┤  0     1            1 ┤  1     0
    │     /                 │
  0 ┤  0 / 0              0 ┤  0     1
    ┼──/──────── x1         ┼──────────── x1
       dreapta merge!       nicio dreaptă nu merge!

Aceasta este o limită fundamentală. Un singur perceptron poate rezolva numai probleme separabile liniar. Minsky și Papert au demonstrat aceasta în 1969, iar rezultatul aproape a ucis cercetarea rețelelor neurale timp de un deceniu.

Soluția: așezați perceptronii în straturi. Un perceptron multistrat poate rezolva XOR combinând două decizii liniare într-una neliniară.

Notă tehnică a traducerii: Neseparabilitatea liniară a XOR este o consecință geometrică elementară; cartea Perceptrons din 1969 a analizat mai amplu limitele anumitor clase de perceptroni. Afirmația că lucrarea a „ucis” aproape singură cercetarea timp de un deceniu este o simplificare istorică și nu trebuie tratată drept rezultat tehnic al cărții.

perceptron-boundary

Construiți

Pasul 1: clasa Perceptron

class Perceptron:
    def __init__(self, n_inputs, learning_rate=0.1):
        self.weights = [0.0] * n_inputs
        self.bias = 0.0
        self.lr = learning_rate

    def predict(self, inputs):
        total = sum(w * x for w, x in zip(self.weights, inputs))
        total += self.bias
        return 1 if total >= 0 else 0

    def train(self, training_data, epochs=100):
        for epoch in range(epochs):
            errors = 0
            for inputs, target in training_data:
                prediction = self.predict(inputs)
                error = target - prediction
                if error != 0:
                    errors += 1
                    for i in range(len(self.weights)):
                        self.weights[i] += self.lr * error * inputs[i]
                    self.bias += self.lr * error
            if errors == 0:
                print(f"Converged at epoch {epoch + 1}")
                return
        print(f"Did not converge after {epochs} epochs")

Pasul 2: antrenați pe porți logice

and_data = [
    ([0, 0], 0),
    ([0, 1], 0),
    ([1, 0], 0),
    ([1, 1], 1),
]

or_data = [
    ([0, 0], 0),
    ([0, 1], 1),
    ([1, 0], 1),
    ([1, 1], 1),
]

not_data = [
    ([0], 1),
    ([1], 0),
]

print("=== AND Gate ===")
p_and = Perceptron(2)
p_and.train(and_data)
for inputs, _ in and_data:
    print(f"  {inputs} -> {p_and.predict(inputs)}")

print("\n=== OR Gate ===")
p_or = Perceptron(2)
p_or.train(or_data)
for inputs, _ in or_data:
    print(f"  {inputs} -> {p_or.predict(inputs)}")

print("\n=== NOT Gate ===")
p_not = Perceptron(1)
p_not.train(not_data)
for inputs, _ in not_data:
    print(f"  {inputs} -> {p_not.predict(inputs)}")

Pasul 3: observați eșecul XOR

xor_data = [
    ([0, 0], 0),
    ([0, 1], 1),
    ([1, 0], 1),
    ([1, 1], 0),
]

print("\n=== XOR Gate (single perceptron) ===")
p_xor = Perceptron(2)
p_xor.train(xor_data, epochs=1000)
for inputs, expected in xor_data:
    result = p_xor.predict(inputs)
    status = "OK" if result == expected else "WRONG"
    print(f"  {inputs} -> {result} (expected {expected}) {status}")

Nu va converge niciodată. Aceasta este demonstrația incontestabilă că un singur perceptron nu poate învăța XOR.

Notă tehnică a traducerii: Neconvergența unei singure rulări finite nu constituie singură o demonstrație. Imposibilitatea rezultă din neseparabilitatea liniară a celor patru puncte XOR; rularea doar o ilustrează. Pentru orice număr finit de epoci, mesajul „nu a convergat” poate apărea și pe date separabile dacă limita este prea mică sau setările sunt nepotrivite.

Pasul 4: rezolvați XOR cu două straturi

Trucul: XOR = (x1 OR x2) AND NOT (x1 AND x2). Combinați trei perceptroni:

Диаграмма к уроку «Perceptronul»

def xor_network(x1, x2):
    or_neuron = Perceptron(2)
    or_neuron.weights = [1.0, 1.0]
    or_neuron.bias = -0.5

    nand_neuron = Perceptron(2)
    nand_neuron.weights = [-1.0, -1.0]
    nand_neuron.bias = 1.5

    and_neuron = Perceptron(2)
    and_neuron.weights = [1.0, 1.0]
    and_neuron.bias = -1.5

    hidden1 = or_neuron.predict([x1, x2])
    hidden2 = nand_neuron.predict([x1, x2])
    output = and_neuron.predict([hidden1, hidden2])
    return output


print("\n=== XOR Gate (multi-layer network) ===")
for inputs, expected in xor_data:
    result = xor_network(inputs[0], inputs[1])
    print(f"  {inputs} -> {result} (expected {expected})")

Toate cele patru cazuri sunt corecte. Așezarea perceptronilor în straturi creează frontiere de decizie pe care niciun perceptron individual nu le poate produce.

Pasul 5: antrenați o rețea cu două straturi

La pasul 4, am conectat manual ponderile. Aceasta funcționează pentru XOR, dar nu și pentru probleme reale în care nu cunoașteți dinainte ponderile corecte. Soluția: înlocuiți funcția treaptă cu sigmoida și învățați automat ponderile prin retropropagare.

class TwoLayerNetwork:
    def __init__(self, learning_rate=0.5):
        import random
        random.seed(0)
        self.w_hidden = [[random.uniform(-1, 1), random.uniform(-1, 1)] for _ in range(2)]
        self.b_hidden = [random.uniform(-1, 1), random.uniform(-1, 1)]
        self.w_output = [random.uniform(-1, 1), random.uniform(-1, 1)]
        self.b_output = random.uniform(-1, 1)
        self.lr = learning_rate

    def sigmoid(self, x):
        import math
        x = max(-500, min(500, x))
        return 1.0 / (1.0 + math.exp(-x))

    def forward(self, inputs):
        self.inputs = inputs
        self.hidden_outputs = []
        for i in range(2):
            z = sum(w * x for w, x in zip(self.w_hidden[i], inputs)) + self.b_hidden[i]
            self.hidden_outputs.append(self.sigmoid(z))
        z_out = sum(w * h for w, h in zip(self.w_output, self.hidden_outputs)) + self.b_output
        self.output = self.sigmoid(z_out)
        return self.output

    def train(self, training_data, epochs=10000):
        for epoch in range(epochs):
            total_error = 0
            for inputs, target in training_data:
                output = self.forward(inputs)
                error = target - output
                total_error += error ** 2

                d_output = error * output * (1 - output)

                saved_w_output = self.w_output[:]
                hidden_deltas = []
                for i in range(2):
                    h = self.hidden_outputs[i]
                    hd = d_output * saved_w_output[i] * h * (1 - h)
                    hidden_deltas.append(hd)

                for i in range(2):
                    self.w_output[i] += self.lr * d_output * self.hidden_outputs[i]
                self.b_output += self.lr * d_output

                for i in range(2):
                    for j in range(len(inputs)):
                        self.w_hidden[i][j] += self.lr * hidden_deltas[i] * inputs[j]
                    self.b_hidden[i] += self.lr * hidden_deltas[i]
net = TwoLayerNetwork(learning_rate=2.0)
net.train(xor_data, epochs=10000)
for inputs, expected in xor_data:
    result = net.forward(inputs)
    predicted = 1 if result >= 0.5 else 0
    print(f"  {inputs} -> {result:.4f} (rounded: {predicted}, expected {expected})")

Există două diferențe esențiale față de pasul 4. Mai întâi, sigmoida înlocuiește funcția treaptă — este netedă, deci există gradienți. Apoi, metoda train propagă eroarea înapoi de la ieșire la stratul ascuns și ajustează fiecare pondere proporțional cu contribuția sa la eroare. Aceasta este retropropagarea în 20 de linii.

Aceasta este puntea către lecția 03. Matematica din spatele lui d_output și hidden_deltas este regula lanțului aplicată grafului rețelei. O vom deriva riguros acolo.

Notă tehnică a traducerii: Codul aplică actualizări online pentru eroarea pătratică și sigmoide, nu o implementare generală a retropropagării. Sigmoida este diferențiabilă, dar poate satura și produce gradienți foarte mici. ReLU, menționată mai jos drept activare „netedă”, este continuă, dar nu este diferențiabilă în zero; bibliotecile aleg acolo o subderivată convențională.

Folosiți

Tot ce tocmai ați construit de la zero există într-un singur import:

from sklearn.linear_model import Perceptron as SkPerceptron
import numpy as np

X = np.array([[0,0],[0,1],[1,0],[1,1]])
y = np.array([0, 0, 0, 1])

clf = SkPerceptron(max_iter=100, tol=1e-3)
clf.fit(X, y)
print([clf.predict([x])[0] for x in X])

Cinci linii. Clasa dumneavoastră Perceptron cu 30 de linii face același lucru. Versiunea scikit-learn adaugă verificări de convergență, mai multe funcții de pierdere și suport pentru intrări rare — dar bucla de bază este identică: sumă ponderată, funcție treaptă și actualizarea ponderilor la eroare.

Notă tehnică a traducerii: sklearn.linear_model.Perceptron fixează SGDClassifier(loss="perceptron", learning_rate="constant"); nu expune mai multe funcții de pierdere. Oferă însă regularizare, ponderi de clasă, amestecare, oprire timpurie și intrări rare. Detaliile de cod și convențiile etichetelor diferă de clasa didactică, chiar dacă regula conceptuală de actualizare este aceeași.

Diferența reală apare la scară. Ce se schimbă în rețelele de producție:

  • Funcția treaptă devine sigmoidă, ReLU sau o altă activare netedă.
  • Ponderile sunt învățate automat prin retropropagare — lecția 03.
  • Straturile devin mai profunde: 3, 10, peste 100 de straturi.
  • Principiul rămâne același: fiecare strat creează caracteristici noi din ieșirile stratului precedent.

Un singur perceptron poate trasa numai drepte. Așezați mai mulți în straturi și puteți trasa orice formă.

Notă tehnică a traducerii: Teoremele de aproximare universală afirmă, în condiții precise, că rețele cu suficientă lățime și activări neliniare potrivite pot aproxima arbitrar de bine anumite funcții continue pe domenii compacte. Ele nu spun că orice rețea finită, de orice dimensiune, va învăța exact „orice formă” și nici nu garantează optimizarea sau generalizarea.

Livrați

Această lecție produce:

  • outputs/skill-perceptron.md — o abilitate despre situațiile în care sunt necesare arhitecturi cu un singur strat sau cu mai multe straturi.

Exerciții

  1. Antrenați un perceptron pe o poartă NAND — poarta universală din care poate fi construit orice circuit logic. Verificați dacă ponderile și biasul formează o frontieră de decizie validă.
  2. Modificați clasa Perceptron pentru a urmări frontiera de decizie — w1*x1 + w2*x2 + b = 0 — în fiecare epocă. Afișați cum se deplasează dreapta în timpul antrenării pe poarta AND.
  3. Construiți un perceptron cu 3 intrări care emite 1 numai când cel puțin 2 dintre cele 3 intrări sunt 1 — o funcție de vot majoritar. Este separabilă liniar? De ce?

Termeni-cheie

Termen Ce spun oamenii Ce înseamnă de fapt
Perceptron „Un neuron fals” Clasificator liniar: produsul scalar dintre intrări și ponderi, plus bias, trecut printr-o funcție treaptă
Pondere „Cât de importantă este o intrare” Multiplicator care scalează contribuția fiecărei intrări la decizie
Bias „Pragul” Constantă care deplasează frontiera de decizie și permite activarea perceptronului chiar și cu intrări zero
Funcție de activare „Lucrul care comprimă valorile” Funcție aplicată după suma ponderată — funcție treaptă pentru perceptroni, sigmoidă/ReLU pentru rețele moderne
Separabil liniar „Puteți trasa o dreaptă între ele” Set de date în care un singur hiperplan poate separa perfect clasele
Problema XOR „Lucrul pe care perceptronii nu îl pot face” Dovada că rețelele cu un singur strat nu pot învăța funcții neseparabile liniar
Frontieră de decizie „Locul unde clasificatorul comută” Hiperplanul w*x + b = 0 care împarte spațiul intrărilor în două clase
Perceptron multistrat „O rețea neurală adevărată” Perceptroni așezați în straturi, unde ieșirea fiecărui strat alimentează intrarea următorului

Lecturi suplimentare

  • Frank Rosenblatt, „The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain” (1958) — lucrarea originală care a început totul.
  • Minsky și Papert, „Perceptrons” (1969) — analiză influentă a limitelor unor clase de perceptroni; neseparabilitatea liniară a XOR nu este un rezultat istoric atribuit exclusiv cărții, iar impactul asupra cercetării a avut cauze multiple.
  • Michael Nielsen, „Neural Networks and Deep Learning”, capitolul 1 — explicație vizuală gratuită despre compunerea perceptronilor în rețele.

Sursă: Originalul în limba engleză

Navigare: Faza 3 — Fundamentele învățării profunde · Catalog complet · în continuare: 03.02 — Rețele multistrat și propagarea înainte