[논문 리뷰] DQN이 합성 제트로 날개의 와류를 길들이다 — 강화학습 기반 능동 유동 제어
논문의 양력 1.79에서 2.0은 오프셋 R1=3 옆에서 보상 0.042만 움직인다. 12 m/s는 그 목표의 자리가 아니다.
받음각이 큰 날개 뒤로 와류가 번갈아 떨어진다. 양력은 주기마다 출렁이고 날개는 떨린다. 보통은 형상이나 고정된 주기 강제(periodic forcing)로 누른다. Hammouda 등(2026)은 앞전 근처 합성 제트(synthetic jet, 불었다 들이마시는 무토출 액추에이터)의 속도를 잠그지 않았다. 0부터 20 m/s까지 21칸 중 하나를 DQN이 고르게 했다.
ε 슬라이더를 0 근처에 두면 막대가 처음 높게 잡힌 속도에 고정된다. 0.2로 두면 노란 탐험이 줄고, 12 m/s 근처로 청록 활용이 모인다.
Train을 누르는 동안 막대 높이가 갱신되고 노란 선택이 줄어드는지 보자. ε를 0.8로 올리면 좋은 칸을 알고도 계속 다른 속도를 찍는다.
이 논문의 좌표#
- 제목: Application of deep reinforcement learning for aerodynamic control around an angled airfoil via synthetic jet
- 저자: N. Ghezaiel Hammouda, R. Khan, L. Mostafa 외 (Scientific Reports, 2026)
- 세팅: Reynolds 수(관성력/점성력 비) 100, Mach 수 0.2의 약압축성 층류. 받음각이 큰 날개와 앞전 근처 합성 제트.
- 결과: Dueling DQN이 가장 안정적으로 수렴해 와류 흘림을 줄이고, 양력을 키우며 항력을 낮췄다.
Re 100이면 흐름은 층류다. 받음각이 크면 그래도 날개 뒤에서 와류가 주기적으로 떨어진다. 그 흘림이 양력과 항력을 흔든다.
와류를 강화학습 문제로 번역하기#
강화학습(시행착오로 보상을 키우는 정책을 배우는 기법)은 세 가지만 정하면 출발한다.
- 상태(state): 날개 주위와 후류의 가상 센서가 읽는 압력·속도. 논문은 압력만 쓸 때보다 속도를 같이 넣을 때 학습이 빨라졌다고 적는다.
- 행동(action): 합성 제트 속도 . 0부터 20 m/s까지 1 m/s 간격, 21개 정수. DQN이 이산 행동을 요구해서다.
- 보상(reward): 항력은 깎고 양력은 키우는 한 줄.
, 는 한 행동 구간에서 평균낸 항력·양력 계수다. , 는 보상을 양수로 두고 양력과 항력의 비중을 맞추는 상수다. 논문은 , 를 썼다. 행동 한 번은 와류 흘림 한 주기고, 학습은 에피소드당 25주기, 전부 300 에피소드다.
ε-greedy: 탐험과 활용 사이#
에이전트는 행동 가치를 로 둔다. 갱신은 Bellman 식이다.
는 학습률, 는 미래 보상을 깎는 할인율, 는 다음 상태에서 기대하는 최선의 가치다.
아직 값을 모르는 행동을 어떻게 찍느냐가 남는다. ε-greedy는 확률 로 지금껏 가장 좋아 보이는 행동(활용)을, 확률 로 무작위(탐험)를 고른다. 이 크면 더 헤매고, 작으면 빨리 한 칸에 앉는다. 앞의 막대에서 0 근처가 막히고 0.2가 12 m/s로 가는 이유가 이 확률이다.
합성 제트라는 행동#
합성 제트는 막을 흔들어 같은 구멍으로 공기를 내뿜고 다시 빨아들인다. 알짜 질량은 0이다. 운동량만 경계층에 들어간다. 그 양을 무차원으로 쓴 것이 운동량 계수다.
, , 는 제트의 밀도·속도·구멍 지름이다. , , 는 자유류 밀도·속도와 코드 길이다. 구멍은 앞전 근처 흡입면 , 지름 0.2 mm다. 경계층에 운동량이 더해지면 박리(boundary layer separation)가 늦어지고 와류 흘림이 약해진다.
마하 0.2면 에이전트가 보는 후류보다 음파가 시간스텝을 먼저 조인다. 그 음파를 지우지 않고 저마하를 푸는 쪽은 점근 보존 IMEX 파동 스킴이 다룬다. 후류 그림은 보상 숫자를 찍은 뒤에 둔다.
직접 구현: Q-학습으로 제트 켜기#
논문의 DQN을 통째로 옮기지 않는다. 상태 칸과 이산 속도만 남긴 표 기반 Q-학습으로 같은 제어를 돌린다. 상태는 양력 진동 폭을 나눈 칸이고, 행동은 제트 속도다.
import numpy as np
class SyntheticJetEnv:
"""1D 현상학적 날개-후류 환경.
상태 : 양력 진동 폭을 이산화한 칸 (0..n_bins-1)
행동 : 제트 속도 레벨 {0,1,...,20} m/s
보상 : R1 - <Cd> + R2*<Cl> (논문 식 4)
"""
def __init__(self, n_bins=6, peak=12, R1=3.0, R2=0.2, seed=0):
self.n_bins, self.peak = n_bins, peak
self.R1, self.R2 = R1, R2
self.rng = np.random.default_rng(seed)
self.amp = 1.0 # 정규화된 와류 진동 폭 (1 = 무제어)
def reset(self):
self.amp = 1.0
return self._bin()
def _bin(self):
return min(self.n_bins - 1, int(self.amp * self.n_bins))
def step(self, action):
ctrl = action / 20.0 # 제어 권한 0..1
target = max(0.05, 1.0 - 0.8 * ctrl) # 제트가 진동 폭을 누른다
self.amp += 0.5 * (target - self.amp) # 1차 완화
cl = 1.8 + 0.2 * ctrl - 0.4 * self.amp # 양력 계수
cd = 0.085 - 0.006 * ctrl + 0.02 * self.amp # 항력 계수
waste = 0.01 * max(0, action - self.peak) # 과도한 분사 페널티
reward = self.R1 - cd + self.R2 * cl - waste
reward += self.rng.normal(0, 0.05)
return self._bin(), reward
def epsilon_greedy(q_row, eps, rng):
if rng.random() < eps:
return int(rng.integers(len(q_row))) # 탐험
return int(np.argmax(q_row)) # 활용
def train_jet_controller(episodes=300, steps=25, alpha=0.1, gamma=0.9, eps0=0.3):
env = SyntheticJetEnv()
n_actions = 21
Q = np.zeros((env.n_bins, n_actions))
rng = np.random.default_rng(1)
history = []
for ep in range(episodes):
s = env.reset()
eps = eps0 * (1 - ep / episodes) # 선형 감쇠
total = 0.0
for _ in range(steps):
a = epsilon_greedy(Q[s], eps, rng)
s2, r = env.step(a)
Q[s, a] += alpha * (r + gamma * Q[s2].max() - Q[s, a])
s, total = s2, total + r
history.append(total / steps)
best = int(np.argmax(Q.sum(axis=0)))
return Q, history, best
if __name__ == "__main__":
Q, hist, best = train_jet_controller()
print(f"episode 1 avg reward = {hist[0]:.3f}")
print(f"episode 300 avg reward = {hist[-1]:.3f}")
print(f"learned jet velocity = {best} m/s")출력은 다음과 같다.
episode 1 avg reward = 3.12
episode 300 avg reward = 3.25
learned jet velocity = 12 m/s제트 속도 슬라이더를 0에 두면 와류가 번갈아 떨어지고 진폭이 크다. 15–20 m/s로 올리면 와류가 옅어지고 후류가 잠잠해진다. 출력의 12 m/s는 그 슬라이더의 오른쪽 끝이 아니다. Play를 끄고 12 근처에 두면, 보상 식이 노리던 잔잔한 후류와 코드가 멈춘 칸이 같은 자리인지 비교할 수 있다.
출력의 3.25는 코드가 균형에 앉은 값이다. 행동 12에서 제어 비율은 12/20 = 0.6이고, 진동 폭은 로 남는다. 양력은 , 항력은 다. 보상 로 300회 줄과 맞는다. 낭비 항은 행동이 12를 넘을 때만 켜진다. 12 m/s는 양력 식의 꼭대기가 아니라, 낭비가 0인 마지막 칸이다.
20 m/s로 올리면 제어 비율은 1, 폭은 0.2, 양력은 1.92, 항력은 0.083이다. 낭비 이 빠져 보상은 3.22로 내려간다. 제트를 끄면 폭 1, 양력 1.4, 항력 0.105, 보상 3.18이다. 학습이 올린 폭은 3.18에서 3.25, 차이 0.07이다. 첫 줄의 3.12는 평형이 아니라 무작위와 잡음이다.
논문의 양력은 1.79에서 약 2.0이다. 를 곱하면 보상은 0.042밖에 안 움직인다. 이 인쇄된 숫자의 대부분이다. 장난감의 12 m/s에서 양력은 1.71이라 논문의 2.0에 닿지 않고, 20 m/s에서도 1.92다. 출력의 제트 속도는 양력 목표가 아니라 페널티가 시작되는 칸을 가리킨다.
DQN의 형제들: Double vs Dueling#
논문은 DQN 세 가지를 나란히 뒀다.
- 전통 DQN: 가 가치를 과대평가하는 쪽으로 기운다.
- Double DQN: 행동을 고르는 망과 가치를 매기는 망을 갈라 그 과대평가를 줄인다.
- Dueling DQN: 를 상태가치 와 어드밴티지 로 나눈다.
는 이 상태가 얼마나 좋은가, 는 그 안에서 이 행동이 평균보다 얼마나 나은가다. 제트 속도 11 m/s와 13 m/s처럼 행동이 비슷하면 상태가치는 한 번만 배우면 된다. 논문에서 Dueling DQN의 학습 곡선이 가장 덜 흔들린 이유다.
5층, 층당 128뉴런이 300 에피소드 안에 수렴했다. 능동 제어를 켜면 이 1.79에서 약 2.0으로 오르고 후류가 잠잠해졌다. 위의 표 학습이 12 m/s에서 내는 양력 1.71과는 다른 숫자다.
12 m/s가 양력 최적값이 아닌 이유#
300 에피소드의 0.13은 대부분 옆에서 일어난 작은 이동이다. 논문의 양력 증가가 보상에 넣는 몫은 0.042다. 그 차이를 제트 속도 한 칸, 1 m/s로 믿을지는 다른 문제다.
진짜 CFD로 11 m/s와 13 m/s를 가리려면 에피소드마다 와류 주기 25번을 적분해야 한다. 시간스텝을 크게 벌려도 가 무너지는 자리는 1001배 스텝의 청구서에 있다. 그 청구서 위에서 12 m/s의 낭비 항과 논문의 양력 2.0을 구분할 수 있는가?
관련
도움이 됐다면 공유해주세요.