Backpropagation 없이 XOR 풀기: Genetic Algorithm 접근법 🧬
Source: Dev.to
소개
우리는 신경망을 훈련시킬 때 보통 역전파와 경사 하강법에 대해 듣습니다. 이것들은 업계 표준이며, “미적분학”적인 학습 방법입니다.
하지만 한 걸음 물러서서 자연이 어떻게 학습하는지를 살펴본다면 어떨까요? 이 글에서는 유전 알고리즘(GA)을 사용하여 고전적인 XOR 문제(단일 뉴런으로는 해결할 수 없는 문제)를 해결합니다. 기울기는 계산되지 않으며, 우리는 “생존자 경쟁”을 시뮬레이션합니다.
XOR 문제
XOR (exclusive OR) 게이트는 비선형 문제입니다. 단순한 선형 선으로는 그 출력들을 구분할 수 없으므로 신경망이 필요합니다.
Genetic Algorithm Overview
In a GA we treat neural networks as individuals in a population.
- Fitness – How well the network solves XOR (inverse of error).
- Selection – The best networks reproduce.
- Crossover – We mix the weights of two parents.
- Mutation – Random noise is added to maintain genetic diversity.
파이썬 구현
Note: 이 코드는 기본
NeuralNetwork클래스와.forward()메서드 및 가중치 행렬W1,W2등을 가지고 있다고 가정합니다.
적합도 계산
import numpy as np
from basic_neural_network import NeuralNetwork
def calculate_fitness(nn):
"""
Measures the XOR performance of the network.
The lower the error, the higher the fitness (score).
"""
X_xor = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y_target = np.array([[0], [1], [1], [0]])
predictions = nn.forward(X_xor)
mse = np.mean((y_target - predictions) ** 2) # Mean Squared Error
# Add a tiny constant to avoid division by zero
return 1 / (mse + 1e-9)
변이
def mutate(nn, mutation_rate=0.1, strength=0.5):
"""
Adds random noise to the network's weights.
"""
parameters = [nn.W1, nn.b1, nn.W2, nn.b2, nn.W3, nn.b3]
for param in parameters:
if np.random.rand() < mutation_rate:
# Add random Gaussian noise
noise = np.random.randn(*param.shape) * strength
param += noise
교차
def crossover(parent1, parent2):
"""
Produces a child by averaging two parents' weights.
"""
child = NeuralNetwork()
child.W1 = (parent1.W1 + parent2.W1) / 2
child.b1 = (parent1.b1 + parent2.b1) / 2
child.W2 = (parent1.W2 + parent2.W2) / 2
child.b2 = (parent1.b2 + parent2.b2) / 2
child.W3 = (parent1.W3 + parent2.W3) / 2
child.b3 = (parent1.b3 + parent2.b3) / 2
return child
메인 진화 루프
# --- MAIN LOOP ---
population_size = 50
population = [NeuralNetwork() for _ in range(population_size)]
generation_count = 1000
print("Evolution starting...")
for gen in range(generation_count):
# Evaluate fitness and sort (highest score first)
population = sorted(population, key=lambda x: calculate_fitness(x), reverse=True)
best_individual = population[0]
best_error = 1 / calculate_fitness(best_individual)
if gen % 100 == 0:
print(f"Gen {gen}: Lowest Error (MSE): {best_error:.5f}")
if best_error < 0.001:
print("Solution found!")
break
# Create new generation (elitism + crossover)
new_population = []
# Keep top 5 individuals unchanged (elitism)
new_population.extend(population[:5])
# Fill the rest by breeding top performers
while len(new_population) < population_size:
parent1 = population[np.random.randint(0, 10)] # top 20%
parent2 = population[np.random.randint(0, 10)]
child = crossover(parent1, parent2)
mutate(child, mutation_rate=0.3, strength=0.2)
new_population.append(child)
population = new_population
최종 테스트
print("\n--- Training Complete ---")
X_test = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
result = population[0].forward(X_test)
print("XOR Inputs:")
print(X_test)
print("Trained Network Predictions:")
print(np.round(result, 3))
백프로파게이션보다 유전 알고리즘을 사용하는 이유
- 희소 보상: 많은 문제에서 결과를 마지막에만 알 수 있습니다 (예: 게임 승리).
- 비미분 가능 영역: 이산 논리나 복잡한 물리 시뮬레이션은 사용 가능한 그래디언트를 제공하지 않을 수 있습니다.
결론
진화적 접근법은 gradient‑based 방법이 어려워하는 문제들을 해결할 수 있습니다. 진화적 코드를 실험해 본 적이 있나요? 댓글에 여러분의 경험을 공유해주세요! 즐거운 코딩 되세요! 🚀