Skip to content

필사 모드: [심층 강화학습] 16. Black-Box 최적화: 진화 전략과 유전 알고리즘

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

개요

지금까지 다룬 강화학습 알고리즘들은 모두 **그래디언트 기반**이었다. 정책 네트워크의 파라미터를 역전파(backpropagation)로 업데이트했다. 하지만 보상 함수가 미분 불가능하거나, 환경이 블랙박스인 경우에는 어떻게 할까?

**Black-Box 최적화**는 목적 함수의 내부 구조를 모르는 채로, 입력-출력 관계만으로 최적화하는 방법이다. 진화 전략(Evolution Strategies, ES)과 유전 알고리즘(Genetic Algorithms, GA)이 대표적이다.

Black-Box 방법의 특징

그래디언트 기반 vs Black-Box

| 항목 | 그래디언트 기반 | Black-Box |

| ----------- | ---------------------------- | ------------------ |

| 미분 필요성 | 목적 함수가 미분 가능해야 함 | 불필요 |

| 정보 활용 | 그래디언트 (방향 + 크기) | 함수값만 사용 |

| 확장성 | 수백만 파라미터 가능 | 파라미터 수에 민감 |

| 병렬화 | 제한적 (동기화 필요) | 자연스러운 병렬화 |

| 탐색 성격 | 로컬 탐색 | 글로벌 탐색 가능 |

장점

- 보상 함수의 미분 불가능해도 적용 가능

- 환경의 내부 구조를 알 필요 없음

- 대규모 병렬화가 자연스러움

- 지역 최적에 빠지기 어려움

단점

- 샘플 효율성이 낮음

- 고차원 파라미터 공간에서 느림

- 그래디언트 정보를 활용하지 못함

진화 전략 (Evolution Strategies)

기본 원리

ES는 파라미터 공간에서 **무작위 변이(perturbation)** 를 만들고, 좋은 성능을 보인 변이 방향으로 파라미터를 업데이트한다. 놀랍게도 이 과정이 그래디언트 추정과 수학적으로 동치라는 것이 밝혀졌다.

ES의 수학적 직관

파라미터 theta에 가우시안 노이즈 epsilon을 더한 theta + sigma \* epsilon에서의 기대 보상을 최대화한다. 이때 보상의 기대값에 대한 theta의 그래디언트를 몬테카를로 추정하면, 결국 보상으로 가중된 노이즈 방향의 평균이 된다.

CartPole에서의 ES 구현

class ESPolicy(nn.Module):

"""ES용 정책 네트워크"""

def __init__(self, obs_size, act_size):

super().__init__()

self.net = nn.Sequential(

nn.Linear(obs_size, 64),

nn.Tanh(),

nn.Linear(64, act_size),

)

def forward(self, obs):

return self.net(obs)

def get_action(self, obs):

with torch.no_grad():

logits = self.forward(torch.FloatTensor(obs))

return logits.argmax().item()

def evaluate_policy(env, policy, num_episodes=5):

"""정책의 평균 보상 계산"""

total_reward = 0.0

for _ in range(num_episodes):

obs, _ = env.reset()

done = False

while not done:

action = policy.get_action(obs)

obs, reward, terminated, truncated, _ = env.step(action)

total_reward += reward

done = terminated or truncated

return total_reward / num_episodes

def train_es_cartpole(pop_size=50, num_generations=100,

sigma=0.1, lr=0.01):

"""CartPole에서 ES 학습"""

env = gym.make('CartPole-v1')

obs_size = env.observation_space.shape[0]

act_size = env.action_space.n

기본 정책

policy = ESPolicy(obs_size, act_size)

base_params = nn.utils.parameters_to_vector(

policy.parameters()

).detach()

for gen in range(num_generations):

1. 무작위 변이 생성

noise = torch.randn(pop_size, len(base_params))

2. 각 변이에 대해 보상 평가

rewards = []

for i in range(pop_size):

양방향 변이 (미러링)

for sign in [1, -1]:

perturbed = base_params + sign * sigma * noise[i]

nn.utils.vector_to_parameters(perturbed, policy.parameters())

reward = evaluate_policy(env, policy, num_episodes=3)

rewards.append((reward, sign, i))

3. 보상으로 가중된 업데이트 방향 계산

rewards_array = np.array([r[0] for r in rewards])

보상 정규화 (rank-based)

ranked = np.argsort(np.argsort(-rewards_array)).astype(np.float32)

ranked = (ranked - ranked.mean()) / (ranked.std() + 1e-8)

그래디언트 추정

grad = torch.zeros_like(base_params)

for idx, (_, sign, noise_idx) in enumerate(rewards):

grad += ranked[idx] * sign * noise[noise_idx]

grad /= (2 * pop_size * sigma)

4. 파라미터 업데이트

base_params += lr * grad

성능 확인

nn.utils.vector_to_parameters(base_params, policy.parameters())

avg_reward = evaluate_policy(env, policy, num_episodes=10)

if gen % 10 == 0:

print(f"세대 {gen}: 평균 보상 = {avg_reward:.1f}")

if avg_reward >= 495:

print(f"세대 {gen}에서 해결!")

break

return policy

HalfCheetah에서의 ES

연속 행동 공간에서의 ES는 더 많은 인구(population)와 세대(generation)가 필요하다:

class ESContinuousPolicy(nn.Module):

def __init__(self, obs_size, act_size):

super().__init__()

self.net = nn.Sequential(

nn.Linear(obs_size, 256),

nn.Tanh(),

nn.Linear(256, 256),

nn.Tanh(),

nn.Linear(256, act_size),

nn.Tanh(),

)

def forward(self, obs):

return self.net(obs)

def get_action(self, obs):

with torch.no_grad():

return self.forward(torch.FloatTensor(obs)).numpy()

def train_es_continuous(env_name='HalfCheetah-v4', pop_size=200,

num_generations=500, sigma=0.02, lr=0.01):

"""연속 환경에서 ES 학습 (병렬화 가능)"""

env = gym.make(env_name)

obs_size = env.observation_space.shape[0]

act_size = env.action_space.shape[0]

policy = ESContinuousPolicy(obs_size, act_size)

base_params = nn.utils.parameters_to_vector(

policy.parameters()

).detach()

Adam 모멘텀 추적

m = torch.zeros_like(base_params) # 1차 모멘트

v = torch.zeros_like(base_params) # 2차 모멘트

beta1, beta2 = 0.9, 0.999

for gen in range(num_generations):

noise = torch.randn(pop_size, len(base_params))

rewards_pos = np.zeros(pop_size)

rewards_neg = np.zeros(pop_size)

병렬화 가능한 평가 루프

for i in range(pop_size):

양방향 변이 평가

params_pos = base_params + sigma * noise[i]

nn.utils.vector_to_parameters(params_pos, policy.parameters())

rewards_pos[i] = evaluate_policy(env, policy, num_episodes=1)

params_neg = base_params - sigma * noise[i]

nn.utils.vector_to_parameters(params_neg, policy.parameters())

rewards_neg[i] = evaluate_policy(env, policy, num_episodes=1)

보상 차이로 그래디언트 추정

all_rewards = np.concatenate([rewards_pos, rewards_neg])

reward_mean = all_rewards.mean()

reward_std = all_rewards.std() + 1e-8

grad = torch.zeros_like(base_params)

for i in range(pop_size):

normalized = (rewards_pos[i] - rewards_neg[i]) / reward_std

grad += normalized * noise[i]

grad /= (2 * pop_size * sigma)

Adam 업데이트

m = beta1 * m + (1 - beta1) * grad

v = beta2 * v + (1 - beta2) * grad ** 2

m_hat = m / (1 - beta1 ** (gen + 1))

v_hat = v / (1 - beta2 ** (gen + 1))

base_params += lr * m_hat / (torch.sqrt(v_hat) + 1e-8)

nn.utils.vector_to_parameters(base_params, policy.parameters())

if gen % 10 == 0:

avg = evaluate_policy(env, policy, num_episodes=5)

print(f"세대 {gen}: 평균 보상 = {avg:.1f}")

return policy

유전 알고리즘 (Genetic Algorithm)

GA 기본 원리

유전 알고리즘은 생물학적 진화를 모방한다:

1. **인구(Population)**: 여러 개의 정책(개체)을 유지

2. **적합도 평가(Fitness)**: 각 개체의 보상을 측정

3. **선택(Selection)**: 적합도가 높은 개체를 선택

4. **교차(Crossover)**: 선택된 개체들의 파라미터를 혼합

5. **변이(Mutation)**: 소량의 무작위 변화 추가

CartPole에서의 GA 구현

def train_ga_cartpole(pop_size=100, num_generations=50,

elite_ratio=0.1, mutation_rate=0.1,

mutation_sigma=0.1):

"""유전 알고리즘으로 CartPole 학습"""

env = gym.make('CartPole-v1')

obs_size = env.observation_space.shape[0]

act_size = env.action_space.n

초기 인구 생성

population = [ESPolicy(obs_size, act_size) for _ in range(pop_size)]

num_elite = max(int(pop_size * elite_ratio), 1)

for gen in range(num_generations):

1. 적합도 평가

fitness = []

for individual in population:

reward = evaluate_policy(env, individual, num_episodes=5)

fitness.append(reward)

정렬 (적합도 내림차순)

sorted_indices = np.argsort(fitness)[::-1]

best_fitness = fitness[sorted_indices[0]]

if gen % 5 == 0:

print(f"세대 {gen}: 최고 적합도 = {best_fitness:.1f}, "

f"평균 = {np.mean(fitness):.1f}")

if best_fitness >= 495:

print(f"세대 {gen}에서 해결!")

return population[sorted_indices[0]]

2. 엘리트 선택

elites = [copy.deepcopy(population[i]) for i in sorted_indices[:num_elite]]

3. 다음 세대 생성

new_population = list(elites) # 엘리트는 그대로 유지

while len(new_population) < pop_size:

토너먼트 선택

parent1 = tournament_select(population, fitness)

parent2 = tournament_select(population, fitness)

교차

child = crossover(parent1, parent2)

변이

mutate(child, mutation_rate, mutation_sigma)

new_population.append(child)

population = new_population

return population[sorted_indices[0]]

def tournament_select(population, fitness, k=3):

"""토너먼트 선택: k개 중 최고 적합도 선택"""

indices = np.random.choice(len(population), k, replace=False)

best_idx = indices[np.argmax([fitness[i] for i in indices])]

return copy.deepcopy(population[best_idx])

def crossover(parent1, parent2):

"""균일 교차: 각 파라미터를 50% 확률로 선택"""

child = copy.deepcopy(parent1)

for c_param, p2_param in zip(child.parameters(), parent2.parameters()):

mask = torch.rand_like(c_param) > 0.5

c_param.data[mask] = p2_param.data[mask]

return child

def mutate(individual, rate=0.1, sigma=0.1):

"""가우시안 변이"""

for param in individual.parameters():

mask = torch.rand_like(param) < rate

noise = torch.randn_like(param) * sigma

param.data += mask.float() * noise

GA 개선 기법

Deep GA

Uber AI Labs의 Deep GA는 대규모 신경망에 GA를 적용하기 위한 기법이다:

class DeepGA:

"""Deep GA: 파라미터 인코딩으로 메모리 효율적 GA"""

def __init__(self, policy_fn, pop_size=1000, seed_size=5):

self.pop_size = pop_size

self.policy_fn = policy_fn

각 개체를 시드 시퀀스로 인코딩 (메모리 절약)

self.population = []

for _ in range(pop_size):

seeds = [np.random.randint(0, 2**31) for _ in range(seed_size)]

self.population.append(seeds)

def decode(self, seed_sequence):

"""시드 시퀀스에서 정책 파라미터 복원"""

policy = self.policy_fn()

base_params = nn.utils.parameters_to_vector(

policy.parameters()

).detach()

params = torch.zeros_like(base_params)

for seed in seed_sequence:

rng = np.random.RandomState(seed)

noise = torch.FloatTensor(

rng.randn(len(base_params))

) * 0.01

params += noise

nn.utils.vector_to_parameters(params, policy.parameters())

return policy

def evolve(self, fitness_fn, num_generations=100):

for gen in range(num_generations):

적합도 평가

fitness = []

for seeds in self.population:

policy = self.decode(seeds)

fitness.append(fitness_fn(policy))

변이: 시드 하나 추가

sorted_idx = np.argsort(fitness)[::-1]

elites = [self.population[i] for i in sorted_idx[:10]]

new_pop = [list(e) for e in elites]

while len(new_pop) < self.pop_size:

parent = elites[np.random.randint(len(elites))]

child = list(parent) + [np.random.randint(0, 2**31)]

new_pop.append(child)

self.population = new_pop

if gen % 10 == 0:

print(f"세대 {gen}: 최고 = {max(fitness):.1f}")

신규성 탐색 (Novelty Search)

적합도 대신 **행동의 신규성**을 기준으로 선택하는 방법이다. 새로운 행동 패턴을 보이는 개체가 선호되어, 다양한 전략을 탐색한다:

class NoveltySearch:

"""신규성 탐색: 행동 다양성으로 개체 선택"""

def __init__(self, archive_size=1000, k_nearest=15):

self.archive = []

self.archive_size = archive_size

self.k = k_nearest

def behavior_characterization(self, trajectory):

"""에이전트의 행동을 특성 벡터로 요약"""

예: 최종 위치, 방문한 상태의 통계 등

if len(trajectory) == 0:

return np.zeros(4)

states = np.array(trajectory)

return np.array([

states[:, 0].mean(), # 평균 x 위치

states[:, 1].mean(), # 평균 y 위치

states[:, 0].std(), # x 분산

states[:, 1].std(), # y 분산

])

def novelty_score(self, behavior):

"""k-최근접 이웃과의 평균 거리 = 신규성 점수"""

if len(self.archive) < self.k:

return float('inf')

distances = [np.linalg.norm(behavior - b)

for b in self.archive]

distances.sort()

return np.mean(distances[:self.k])

def add_to_archive(self, behavior):

self.archive.append(behavior)

if len(self.archive) > self.archive_size:

랜덤하게 하나 제거

idx = np.random.randint(len(self.archive))

self.archive.pop(idx)

ES vs GA 비교

| 항목 | 진화 전략 (ES) | 유전 알고리즘 (GA) |

| --------------- | ------------------------ | --------------------------- |

| 업데이트 방식 | 그래디언트 추정 (연속적) | 선택 + 교차 + 변이 (이산적) |

| 수학적 기반 | 정규 분포 기반 최적화 | 생물학적 진화 모방 |

| 인구 다양성 | 단일 분포에서 샘플링 | 여러 개체가 독립적으로 존재 |

| 병렬화 | 매우 쉬움 | 쉬움 |

| 대규모 파라미터 | 비교적 잘 확장 | 교차 연산이 비효율적 |

핵심 요약

- Black-Box 최적화는 그래디언트 없이 보상 신호만으로 정책을 최적화한다

- ES는 무작위 변이의 보상으로 그래디언트를 추정하며, 대규모 병렬화에 유리하다

- GA는 선택, 교차, 변이를 통한 인구 기반 최적화를 수행한다

- Deep GA와 신규성 탐색으로 GA의 한계를 극복할 수 있다

- 그래디언트 기반 방법보다 샘플 효율은 낮지만, 특수한 환경에서 유용하다

다음 글에서는 환경 모델을 학습하여 활용하는 **모델 기반 강화학습**을 알아보겠다.

현재 단락 (1/280)

지금까지 다룬 강화학습 알고리즘들은 모두 **그래디언트 기반**이었다. 정책 네트워크의 파라미터를 역전파(backpropagation)로 업데이트했다. 하지만 보상 함수가 미분 불...

작성 글자: 0원문 글자: 9,285작성 단락: 0/280