Propagacja wsteczna

Propagacja wsteczna (ang. Backpropagation lub Backward Propagation of Errors) – podstawowy algorytm uczenia maszynowego wielowarstwowych sieci neuronowych. Podaje on przepis na zmianę wag dowolnych połączeń elementów przetwarzających rozmieszczonych w sąsiednich warstwach sieci. Oparty jest on na minimalizacji sumy kwadratów błędów (lub innej funkcji kosztu) uczenia z wykorzystaniem optymalizacyjnej metody największego spadku. Dzięki zastosowaniu specyficznego sposobu propagowania błędów uczenia sieci powstałych na jej wyjściu, tj. przesyłania ich od warstwy wyjściowej do wejściowej, algorytm propagacji wstecznej stał się jednym z najskuteczniejszych algorytmów uczenia sieci.

Algorytm uczenia wyznacza kierunek, w którym w danej iteracji należy zmodyfikować wagi w celu zmniejszenia błędu popełnianego przez sieć. Natomiast wielkość modyfikacji wag zależy od współczynnika uczenia[1] – im większy jest ten współczynnik, tym bardziej modyfikuje się wagi w każdej iteracji.

Pomimo iż pojedynczy perceptron (sztuczny neuron) nie może rozwiązać problemów, które nie są liniowo separowalne, to połączenie większej liczby neuronów już wykazuje tę zdolność. Np. pojedynczy neuron nie może nauczyć się bramki XOR, ale już trzy neurony mają tę zdolność (por. przykład podany dalej). Jest to bardzo wyrazisty przykład emergencji cech czyli pojawienie się nowych zdolności w układzie, złożonym z elementów, które same takich cech nie wykazują.

Uczenie sieci neuronowej funkcji XOR oraz funkcji parzystości

Marvin Minsky i Seymour Papert w książce Perceptrons[2] analizowali problem uczenia perceptronu funkcji XOR(2) oraz funkcji parzystości (parity function) dla większej liczby wejść jako uogólnienie problemu. W „Perceptrons” pokazali, że:

  • Pojedynczy perceptron nie jest w stanie rozróżnić przypadków o różnej parzystości (czyli np. wejść z parzystą liczbą jedynek vs nieparzystą), gdyż funkcja parzystości nie jest liniowo separowalna (tj. nie da się jej punktów wejściowych o różnej parzystości podzielić jedną hiperpłaszczyzną, wykreśloną w przestrzeni wejść; w szczególności dla przypadku 2-wymiarowego nie istnieje prosta, która separuje punkty na płaszczyźnie x1-x2 o różnej parzystości - por. dalej).
  • Ich wniosek został błędnie zinterpretowany jako dowód, że sieci neuronowe w ogóle nie mogą się uczyć takich funkcji (co doprowadziło do tzw. zimy AI).

W tamtym czasie nie znano metody uczenia sieci wielowarstwowych - problem znalazł rozwiązanie w sieciach wielowarstwowych, które uczy się metodą backpropagation.

Uczenie sieci XOR metodą propagacji wstecznej

Poniżej pokazano kod programu w języku Python, który uczy małą sieć neuronową (2 wejścia, 2 neurony w warstwie ukrytej, 1 wyjście) działania jako bramka logiczna XOR.

1. Struktura sieci

Granica decyzyjna sieci neuronowej XOR po 50 epokach treningu metodą backpropagation - widać, że granica jest nieliniowa, sieć już prawidłowo klasyfikuje punkty klasy , oddzielając je od punktów klasy ; funkcja straty jest jeszcze dość duża.

Wejścia: x1, x2

Warstwa ukryta: 2 neurony, funkcja aktywacji: funkcja sigmoidalna

Wyjście: 1 neuron, funkcja aktywacji: funkcja sigmoidalna

Funkcja straty: entropia krzyżowa

Granica decyzyjna sieci neuronowej XOR po 500 epokach treningu metodą backpropagation; funkcja straty jest znacznie mniejsza - punkty są bardzo dobrze odseparowane granicą decyzyjną.

2. Kroki uczenia w jednej epoce:

2.1 Forward pass (przejście do przodu)

Obliczenie macierzy aktywacji warstwy ukrytej:

gdzie:

– macierz danych uczących (4x2 dla XOR)
– macierz wag warstwy ukrytej (2x2)
– macierz transponowana wag warstwy ukrytej (2x2)
– macierz biasów warstwy ukrytej (1x2)
– funkcja sigmoidalna

Obliczenie wyjścia sieci:

Wykres funkcji straty obliczonej dla sieci neuronowej XOR w zależności od epok treningu; dla 3000 epok funkcja straty jest bliska zeru ( ).

gdzie:

– macierz aktywacji warstwy ukrytej
– macierz wag wyjścia (1x2)
– macierz transponowana wag wyjścia (2x1)
– bias wyjścia (1x1)

przy czym dla XOR(2):

, , ,

, , ,

2.2 Obliczenie błędu (loss)

Metoda spadku gradientowego może znaleźć minimum lokalne zamiast minimum globalnego.

Bramka XOR dokonuje klasyfikacji binarnej. Jako funkcję kosztu do jej treningu stosuje się entropię krzyżową (co jest typowe dla treningu sieci neuronowych będących klasyfikatorami binarnymi):

gdzie:

– wartości prawdziwe (próbki uczące)
– przewidywania sieci
– liczba próbek uczących (4 dla XOR)

przy czym oznacza sumowanie po wszystkich próbkach uczących i odpowiadających im przewidywaniach sieci ;

- wektor oczekiwanych wyników z sieci, odpowiadających macierzy danych uczących .

2.3 Propagacja wsteczna

Gradienty dla warstwy wyjściowej:

Gradienty dla warstwy ukrytej:

gdzie:

– pochodna funkcji sigmoidalnej

Aktualizacja wag i biasów:

gdzie:

– współczynnik uczenia (learning rate)

3. Wizualizacja granicy decyzyjnej

Granica decyzyjna rysowana co plot_interval = 500 epok na siatce punktów. Kolor wskazuje przewidywaną klasę, tytuł wykresu pokazuje numer epoki i wartość funkcji straty Loss.

4. Jak przebiega uczenie sieci?

Ponieważ metoda spadku gradientowego może znaleźć minimum lokalne zamiast minimum globalnego (por. rysunek), to uczenie podzielono na dwa etapy:

Etap 1: Wybiera się początkowo losowe wartości wag i basów, a następnie uczy się sieć metodą backpropagation przez 50 kolejnych epok; jeśli sieć nauczy się poprawnie klasyfikować punkty, to przechodzi się do drugiego etapu; jeśli nie, to ponownie losuje się wartości początkowe wag i biasów; w ten sposób zwiększa się szansę na wybranie takich wag i biasów początkowych, które kierują sieć w stronę globalnego minimum, co oznacza, że sieć nauczyła się przewidywać wyniki zgodnie ze wzorcem uczenia; etap ten powtarza się nie więcej niż ustaloną liczbę razy, w razie braku sukcesu kończy się program.

Etap 2: Kontynuuje się uczenie, jeśli został znaleziony zestaw wag i biasów, które gwarantują sukces; w kolejnych epokach uczenia zmniejsza się funkcja straty Loss, a granica decyzyjna (klasyfikacyjna) coraz lepiej oddziela punkty klasy (parzyste) od punktów klasy (nieparzyste).

Po zakończeniu uczenia sieci program robi test poprawności działania sieci - wypisuje wyniki klasyfikacji ; są w 100% zgodne z wartościami prawdziwymi (oczekiwanymi) :

import numpy as np
import matplotlib.pyplot as plt

# --- Funkcje aktywacji ---
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(x):
    s = sigmoid(x)
    return s * (1 - s)

# --- Strata entropii krzyżowej ---
def cross_entropy(y_true, y_pred):
    y_pred = np.clip(y_pred, 1e-10, 1-1e-10)
    return -np.mean(y_true*np.log(y_pred) + (1-y_true)*np.log(1-y_pred))

# --- Forward pass ---
def xor_network(x1, x2, W1, b1, W2, b2):
    H = sigmoid(np.column_stack([x1, x2]).dot(W1.T) + b1)
    Y = sigmoid(H.dot(W2.T) + b2)
    Y_bin = np.where(Y >= 0.5, 1, 0)
    return Y, H, Y_bin

# --- Jedna iteracja treningu ---
def train_step(X, y, W1, b1, W2, b2, lr):
    A1 = sigmoid(X.dot(W1.T) + b1)
    A2 = sigmoid(A1.dot(W2.T) + b2)
    dZ2 = A2 - y
    W2 -= lr * (dZ2.T.dot(A1) / X.shape[0])
    b2 -= lr * np.mean(dZ2, axis=0)
    dZ1 = dZ2.dot(W2) * sigmoid_derivative(X.dot(W1.T) + b1)
    W1 -= lr * (dZ1.T.dot(X) / X.shape[0])
    b1 -= np.mean(dZ1, axis=0)
    return W1, b1, W2, b2, cross_entropy(y, A2)

# --- Rysowanie granicy decyzyjnej (ze straty) ---
def plot_decision_boundary(X, y, W1, b1, W2, b2, epoch=None, loss=None):
    xx, yy = np.meshgrid(np.linspace(-0.5,1.5,200), np.linspace(-0.5,1.5,200))
    Z, _, _ = xor_network(xx.ravel(), yy.ravel(), W1, b1, W2, b2)
    Z = Z.reshape(xx.shape)
    plt.figure(figsize=(5,4))
    plt.contourf(xx, yy, Z, levels=[0,0.5,1], alpha=0.4, cmap='coolwarm')
    plt.scatter(X[:,0], X[:,1], c=y.flatten(), s=100, edgecolors='k', cmap='coolwarm')
    if epoch is not None and loss is not None:
        plt.title(f"Epoch {epoch}, Loss={loss:.4f}")
    else:
        plt.title("Decision Boundary")
    plt.xlabel("x1")
    plt.ylabel("x2")
    plt.grid(True)
    plt.show()

# --- Dane XOR ---
X = np.array([[0,0],[0,1],[1,0],[1,1]])
y_true = np.array([[0],[1],[1],[0]])

# --- Parametry ---
input_size = 2
hidden_size = 2
output_size = 1
lr = 0.5
initial_epochs = 50
epochs = 3000
plot_interval = 500
max_trials = 1000

# --- Etap 1: Szukanie dobrego startu ---
np.random.seed()
trial = 0
while trial < max_trials:
    trial += 1
    W1, b1 = np.random.randn(hidden_size, input_size), np.random.randn(hidden_size)
    W2, b2 = np.random.randn(output_size, hidden_size), np.random.randn(output_size)

    # Wstępny trening
    for _ in range(initial_epochs):
        W1, b1, W2, b2, loss = train_step(X, y_true, W1, b1, W2, b2, lr)

    _, _, y_bin = xor_network(X[:,0], X[:,1], W1, b1, W2, b2)
    if np.array_equal(y_bin, y_true):
        print(f"Found good start in trial {trial}")
        # Obliczamy loss po initial_epochs
        loss_initial = cross_entropy(y_true, sigmoid(sigmoid(X.dot(W1.T)+b1).dot(W2.T)+b2))
        plot_decision_boundary(X, y_true, W1, b1, W2, b2, epoch=initial_epochs, loss=loss_initial)
        break
else:
    print(f"No good start found in {max_trials} trials. Exiting.")
    exit()

# --- Lista do zapisywania straty w pełnym treningu ---
loss_history = []

# --- Etap 2: Pełny trening ---
for epoch in range(initial_epochs, epochs + initial_epochs):
    W1, b1, W2, b2, loss = train_step(X, y_true, W1, b1, W2, b2, lr)
    loss_history.append(loss)  # zapisujemy loss

    if epoch % plot_interval == 0 or epoch == epochs + initial_epochs - 1:
        plot_decision_boundary(X, y_true, W1, b1, W2, b2, epoch=epoch, loss=loss)

# --- Rysowanie wykresu zmian straty ---
plt.figure(figsize=(6,4))
plt.plot(range(initial_epochs, epochs + initial_epochs), loss_history, marker='o', markersize=3)
plt.xlabel("Epoch")
plt.ylabel("Loss (Cross-Entropy)")
plt.title("Loss vs Epochs")
plt.grid(True)
plt.show()

# --- Test po treningu ---
_, _, y_pred = xor_network(X[:,0], X[:,1], W1, b1, W2, b2)
print("=== TEST SIECI XOR PO NAUCE ===")
for xi, yt, yp in zip(X, y_true, y_pred):
    print(f"X={xi}, y_true={yt[0]}, y_pred={int(yp[0])}")

Uwaga 1: W programie kończy się uczenie sieci, gdy minie ustalona z góry liczba epok uczenia. Można zoptymalizować warunki zakończenia uczenia, wprowadzając dodatkowe kryteria mierzenia dokładności nauczenie się sieci.

Uwaga 2: W liniach 53-56 są dane prawdziwe (treningowe) dla sieci XOR, a w liniach 57-65 dane definiujące architekturę sieci (oraz parametry treningowe). Zmieniając dane treningowe można łatwo zmienić program na uczenie innej funkcji, np. AND, NOT, XOR(3), XOR(4), itd.

Uwaga 3: W programie zastosowano metodę uczenia, w której w każdym pojedynczym kroku iteracyjnym wykonuje się jednocześnie obliczenia na całym zestawie 4 par danych uczących. Takie podejście wymaga rozszerzenia wektora danych do postaci macierzy; przyjęto postać macierzy 4x2, tj. poszczególne wiersze macierzy oznaczają 4 kolejne pary danych wejściowych. Implikuje to postać równań macierzowych, w których np. mamy mnożenie macierzy przez macierz transponowaną , tj. , zamiast , jak to jest tradycyjnie w opisie działania neuronów, gdy w jednym kroku iteracji wykonuje się obliczenia na pojedynczych danych wejściowych (por. przykład kodu programu w artykule perceptron). Dalej, rozszerzenia wektorów do postaci macierzy wymagają działania na biasach neuronów; w programie nie dokonuje się tego rozszerzenia, ale korzysta się z implementowanego w języku Python domyślnego rozszerzenia wektorów do postaci macierzy, tzw. broadcasting, dlatego nie pokazano tutaj jawnie tego rozszerzenia. Także sumaryczne sygnały w dwóch neuronach warstwy ukrytej sieci są zapisywane teraz nie w wektorze 2-elementowym, ale w macierzy o wymiarze 4x2, w której poszczególnym wierszom odpowiadają sumaryczne sygnały w neuronach wywołane przez kolejne pary danych wejściowych. W końcu sygnał wyjściowy z sieci ma postać macierzy wymiaru 4x1 - poszczególne 4 wiersze tej macierzy oznaczają wartości wyjściowe z sieci, w odpowiedzi na 4 poszczególne pary danych wejściowych.

Uczenie sieci neuronowej funkcji parzystości

Definicja funkcji parzystości

Dla wejść binarnych

funkcja (czyli funkcja parzystości) ma postać:

czyli:

  • wynik = , jeśli liczba jedynek wśród wejść jest nieparzysta
  • wynik = , jeśli liczba jedynek jest parzysta

Sieć neuronowa licząca funkcję parzystości XOR(3)

Poniżej podano kod programu uczącego sieć neuronową funkcji parzystości . Sieć neuronowa składa się teraz z 3 wejść, 4 neuronów ukrytych i jednego wyjścia. (Wizualizacja granicy decyzyjnej wymagałaby rysowania powierzchni w przestrzeni 3D - zostało to tu pominięte)

import numpy as np

# --- Funkcje pomocnicze ---
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(x):
    s = sigmoid(x)
    return s * (1 - s)

# --- Dane: wszystkie kombinacje 3-bitowe ---
X = np.array([
    [0,0,0],
    [0,0,1],
    [0,1,0],
    [0,1,1],
    [1,0,0],
    [1,0,1],
    [1,1,0],
    [1,1,1]
])

# --- Odpowiedzi: XOR(3) ---
y = np.array([[np.sum(x) % 2] for x in X])  # 1 gdy liczba jedynek nieparzysta

# --- Inicjalizacja wag ---
np.random.seed()
w1 = np.random.randn(3, 4)   # 3 wejścia, 4 neurony ukryte
b1 = np.zeros((1, 4))
w2 = np.random.randn(4, 1)   # 4 neurony ukryte -> 1 wyjście
b2 = np.zeros((1, 1))

# --- Trening ---
eta = 0.5
for epoch in range(10000):
    # Forward
    z1 = X.dot(w1) + b1
    a1 = sigmoid(z1)
    z2 = a1.dot(w2) + b2
    y_pred = sigmoid(z2)

    # Strata
    loss = np.mean((y - y_pred)**2)

    # Backpropagation
    d2 = (y_pred - y) * sigmoid_derivative(z2)
    dw2 = a1.T.dot(d2)
    db2 = np.sum(d2, axis=0, keepdims=True)

    d1 = d2.dot(w2.T) * sigmoid_derivative(z1)
    dw1 = X.T.dot(d1)
    db1 = np.sum(d1, axis=0, keepdims=True)

    # Aktualizacja wag
    w1 -= eta * dw1
    b1 -= eta * db1
    w2 -= eta * dw2
    b2 -= eta * db2

    if epoch % 1000 == 0:
        print(f"Epoka {epoch}, strata = {loss:.4f}")

# --- Wyniki ---
print("\nWyniki końcowe:")
for i in range(len(X)):
    print(f"{X[i]} -> {y_pred[i][0]:.3f} (oczekiwane {y[i][0]})")

Przypisy

  1. ↑ Ryszard Tadeusiewicz, Maciej Szaleniec: Leksykon sieci neuronowych. s. 123. ISBN 978-83-63270-10-0.
  2. ↑ Marvin Lee Minsky, Seymour Papert, Perceptrons : an introduction to computational geometry, Cambridge, Mass. : MIT Press, 1988, ISBN 978-0-262-63111-2 [dostęp 2025-10-26].

Zobacz też

Bibliografia

  • Tadeusiewicz Ryszard, Sieci neuronowe, Akademicka Oficyna Wydaw. RM, Warszawa 1993

Linki zewnętrzne