Skip to content
cfd-lab:~/ko/posts/2026-06-07-drl-synthetic…online
NOTE #067DAY SUN 논문리뷰DATE 2026.06.07READ 4 min readWORDS 2,210#DRL#Reinforcement-Learning#Flow-Control#Synthetic-Jet#Airfoil#논문리뷰

[논문 리뷰] DQN이 합성 제트로 날개의 와류를 길들이다 — 강화학습 기반 능동 유동 제어

DQN·Dueling DQN이 합성 제트를 조절해 양력을 키우고 항력을 줄인다

받음각이 큰 날개 뒤로 와류가 번갈아 떨어진다. 양력은 매 주기 출렁이고, 날개는 떨린다. 엔지니어는 보통 이런 진동을 형상 설계나 고정된 주기 강제(periodic forcing)로 누른다. Hammouda 등(2026)은 다른 길을 택했다. 날개에 작은 합성 제트(synthetic jet, 주기적으로 불고 빨아들이는 무토출 액추에이터) 구멍을 뚫고, 그 분사 속도를 강화학습 에이전트가 스스로 정하게 했다. 오늘은 이 논문이 와류 흘림(vortex shedding)을 어떻게 강화학습 문제로 번역했는지 보고, ε-greedy Q-학습으로 같은 아이디어를 직접 돌려본다.

이 논문의 좌표#

  • 제목: 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): 합성 제트의 분사 속도 UaU_a. 0부터 20 m/s까지 1 m/s 간격의 21개 정수 레벨로 이산화했다. DQN이 이산 행동을 요구하기 때문이다.
  • 보상(reward): 항력은 깎고 양력은 키우는 한 줄짜리 함수.
r=R1CDac+R2CLacr = R_1 - \langle C_D \rangle_{ac} + R_2\,\langle C_L \rangle_{ac}

여기서 CDac\langle C_D \rangle_{ac}, CLac\langle C_L \rangle_{ac}는 한 행동 구간 동안 평균낸 항력·양력 계수다. R1R_1, R2R_2는 보상을 양수로 유지하고 양력 대 항력의 비중을 맞추는 상수로, 논문은 R1=3R_1=3, R2=0.2R_2=0.2를 썼다. 한 번의 행동은 와류 흘림 한 주기에 대응하고, 학습은 에피소드당 25주기씩 총 300 에피소드를 돈다.

ε-greedy: 탐험과 활용 사이#

에이전트는 각 행동의 가치를 행동가치함수 Q(s,a)Q(s,a)로 추정한다. 핵심은 Bellman 갱신이다.

Q(s,a)Q(s,a)+α[r+γmaxaQ(s,a)Q(s,a)]Q(s,a) \leftarrow Q(s,a) + \alpha\left[\, r + \gamma \max_{a'} Q(s',a') - Q(s,a) \,\right]

α\alpha는 학습률, γ\gamma는 미래 보상을 깎는 할인율, maxaQ(s,a)\max_{a'}Q(s',a')는 다음 상태에서 기대할 수 있는 최선의 가치다.

문제는 아직 가치를 모르는 행동을 어떻게 시도하느냐다. ε-greedy 정책이 답이다. 확률 1ϵ1-\epsilon로는 지금까지 가장 좋아 보이는 행동(활용)을, 확률 ϵ\epsilon로는 무작위 행동(탐험)을 고른다. ϵ\epsilon이 크면 더 많이 탐험하고, 작으면 빨리 한 곳에 정착한다.

아래 시뮬레이션에서 직접 조작해보자. 막대는 21개 제트 속도 각각의 추정 가치 QQ이고, 노란 막대는 탐험, 청록 막대는 활용으로 방금 고른 행동이다.

0jet velocity action (m/s)20
steps: 0 · best action: 0 m/s · avg reward: 0.00■ explore■ exploit

ϵ\epsilon을 0에 가깝게 두면 에이전트가 우연히 처음 높게 잡힌 행동에 갇히는 걸 볼 수 있다. 0.2 부근에서는 12 m/s 근처의 진짜 최적값을 빠르게 찾아간다. 탐험이 너무 많으면(0.8) 좋은 값을 알면서도 계속 엉뚱한 곳을 찔러본다.

합성 제트라는 행동#

합성 제트는 막을 진동시켜 같은 구멍으로 공기를 내뿜고 다시 빨아들인다. 알짜 질량 토출은 0이지만, 운동량은 경계층에 주입된다. 이 주입량을 무차원으로 나타낸 것이 운동량 계수다.

Cμ=ρjUa2dj12ρU2cC_\mu = \frac{\rho_j\,U_a^2\,d_j}{\tfrac{1}{2}\,\rho_\infty\,U_\infty^2\,c}

ρj\rho_j, UaU_a, djd_j는 제트의 밀도·속도·구멍 지름이고, ρ\rho_\infty, UU_\infty, cc는 자유류 밀도·속도와 코드 길이다. 논문에서 구멍은 앞전 근처 흡입면 x/c=0.1x/c=0.1에 있고 지름 0.2 mm다. 제트가 경계층에 운동량을 더하면 박리(boundary layer separation)가 늦춰지고, 와류 흘림이 약해진다.

아래 시뮬레이션에서 직접 조작해보자. 제트 속도를 올리면 후류의 와류가 어떻게 변하는지 본다.

UaU_a가 0이면 강한 와류가 번갈아 떨어지고 CLC_L 진동 폭이 크다. 속도를 15~20 m/s로 올리면 와류가 옅어지고 후류가 안정되며, 양력 진동이 눈에 띄게 줄어든다. 이것이 보상 함수가 보상하려는 바로 그 상태다.

직접 구현: Q-학습으로 제트 켜기#

논문의 DQN을 그대로 옮기는 대신, 핵심 아이디어만 남긴 표(table) 기반 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

에이전트는 처음엔 무작위로 헤매다, 300 에피소드 뒤 12 m/s 근처가 양력 이득과 분사 낭비 사이의 균형점임을 스스로 알아낸다. 논문의 보상 형태와 행동 공간을 그대로 따른 결과다.

DQN의 형제들: Double vs Dueling#

논문은 세 가지 DQN 변종을 비교했다.

  • 전통 DQN: max\max 연산으로 가치를 과대평가하는 경향이 있다.
  • Double DQN: 행동 선택과 가치 평가에 다른 네트워크를 써 과대평가를 줄인다.
  • Dueling DQN: QQ를 상태가치 V(s)V(s)와 어드밴티지 A(s,a)A(s,a)로 분리한다.
Q(s,a)=V(s)+(A(s,a)1AaA(s,a))Q(s,a) = V(s) + \left( A(s,a) - \frac{1}{|\mathcal{A}|}\sum_{a'} A(s,a') \right)

V(s)V(s)는 "이 상태가 얼마나 좋은가", A(s,a)A(s,a)는 "그 안에서 이 행동이 평균보다 얼마나 나은가"를 따로 학습한다. 많은 행동이 비슷한 가치를 가질 때 — 제트 속도 11과 13 m/s가 거의 같을 때처럼 — 상태가치를 한 번만 배우면 되니 학습이 안정된다. 논문에서 Dueling DQN이 가장 일관된 학습 곡선과 최고 성능을 보인 이유다.

5층 × 128 뉴런 네트워크가 300 에피소드 만에 수렴했고, 능동 제어를 켠 경우 CLC_L이 1.79에서 약 2.0으로 오르면서 후류가 안정됐다.

기억할 점#

  • 유동 제어를 RL로 번역하는 레시피: 상태 = 센서 압력·속도, 행동 = 제트 속도(이산), 보상 = R1CD+R2CLR_1 - \langle C_D\rangle + R_2\langle C_L\rangle.
  • 합성 제트는 알짜 질량 0으로 운동량만 경계층에 주입해 박리를 늦추고 와류 흘림을 약화시킨다.
  • Dueling DQNQ=V+AQ = V + A 분리 덕에 비슷한 행동이 많은 유동 제어 문제에서 가장 안정적으로 수렴한다.

도움이 됐다면 공유해주세요.