Skip to content
cfd-lab:~/ja/posts/2026-06-07-drl-synthetic…● online

この記事は著者が韓国語で書いた原文の翻訳です。 原文(한국어)を読む

NOTE #065DAY SUN 논문리뷰DATE 2026.06.07UPDATED 2026.10.19READ 5 min read#DRL#Reinforcement-Learning#Flow-Control#Synthetic-Jet#Airfoil#Paper-Review

[論文レビュー] DQNが合成ジェットで翼の渦を手なずける — 強化学習による能動流れ制御

論文の揚力1.79から2.0は、オフセットR1=3の横で報酬を0.042しか動かしません。12 m/sはその目標ではありません。

迎え角の大きい翼の後ろでは、渦が交互に剥がれます。揚力は周期ごとに揺れ、翼は振動します。普通は形状か、固定した周期強制(periodic forcing)で抑えます。Hammouda ら(2026)は噴流速度を固定しませんでした。前縁近くの合成ジェット(吹いては吸い込む、正味流量ゼロのアクチュエータ)の速度を、0から20 m/sまでの21段からDQNに選ばせています。

εスライダーを0の近くに置くと、棒は最初に高く出た速度へ固まります。0.2にすると黄色い探索が減り、12 m/s付近へ水色の活用が集まります。

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

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): 合成ジェット速度 UaU_a。0から20 m/sまで1 m/s刻みの21個の整数。DQNが離散行動を要求するからです。
  • 報酬(reward): 抗力を削り揚力を上げる一行。
r=R1−⟨CD⟩ac+R2 ⟨CL⟩acr = R_1 - \langle C_D \rangle_{ac} + R_2\,\langle C_L \rangle_{ac}

⟨CD⟩ac\langle C_D \rangle_{ac}と⟨CL⟩ac\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+γmax⁡a′Q(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は将来の報酬を削る割引率、max⁡a′Q(s′,a′)\max_{a'}Q(s',a')は次の状態で期待できる最良の価値です。

まだ値を知らない行動をどう試すかが残ります。ε-greedyは確率1−ϵ1-\epsilonで今まで最も良く見える行動(活用)を、確率ϵ\epsilonで無作為(探索)を選びます。ϵ\epsilonが大きければより迷い、小さければ早く一つの枠に座ります。前の棒で0付近が詰まり、0.2が12 m/sへ向かう理由がこの確率です。

行動としての合成ジェット

合成ジェットは膜を震わせ、同じ穴から空気を出してまた吸い込みます。正味の質量は0です。運動量だけが境界層に入ります。その量を無次元にしたのが運動量係数です。

Cμ=ρj Ua2 dj12 ρ∞ U∞2 cC_\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、U∞U_\infty、ccは一様流の密度・速度とコード長です。穴は前縁近くの負圧面x/c=0.1x/c=0.1、直径0.2 mmです。境界層に運動量が加わると剥離(boundary layer separation)が遅れ、渦放出が弱くなります。

Mach 0.2では、エージェントが見る後流より先に音波が時間刻みを締めます。その音波を消さずに低Machを解く側は、漸近保存のIMEX波動スキームが扱います。後流の図は、報酬の数字を出したあとに置きます。

手を動かす: Q学習でジェットを入れる#

論文のDQNをそのまま移しません。状態の枠と離散速度だけを残した表形式のQ学習で、同じ制御を回します。状態は揚力振動の幅を分けた枠、行動はジェット速度です。

import numpy as np
 
class SyntheticJetEnv:
    """1次元 現象論的な翼-後流環境。
 
    状態  : 揚力振動幅を離散化したマス (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に置くと渦が交互に落ち、CLC_Lの振幅が大きくなります。15–20 m/sへ上げると渦が薄れ、後流が静まります。出力の12 m/sは、そのスライダーの右端ではありません。再生を止めて12の近くに置くと、報酬が狙う静かな後流と、表が止まった枠が同じ場所かを比べられます。

出力の3.25は、コードが平衡に座った値です。行動12では制御割合が12/20 = 0.6で、振動幅は1−0.8×0.6=0.521 - 0.8 \times 0.6 = 0.52に残ります。揚力は1.8+0.2×0.6−0.4×0.52=1.711.8 + 0.2 \times 0.6 - 0.4 \times 0.52 = 1.71、抗力は0.085−0.006×0.6+0.02×0.52=0.0920.085 - 0.006 \times 0.6 + 0.02 \times 0.52 = 0.092です。報酬3−0.092+0.2×1.71=3.253 - 0.092 + 0.2 \times 1.71 = 3.25が300回の行と一致します。無駄項は行動が12を超えたときだけ点きます。12 m/sは揚力式の頂点ではなく、無駄が0の最後の枠です。

20 m/sに上げると割合は1、幅は0.2、揚力は1.92、抗力は0.083です。無駄0.01×(20−12)=0.080.01 \times (20 - 12) = 0.08が引かれて報酬は3.22へ下がります。ジェットを切ると幅1、揚力1.4、抗力0.105、報酬3.18です。学習が持ち上げた幅は3.18から3.25、差は0.07です。最初の行の3.12は平衡ではなく、無作為と雑音です。

論文の揚力は1.79から約2.0です。R2=0.2R_2 = 0.2を掛けると報酬は0.042しか動きません。R1=3R_1 = 3が印刷された数字の大半です。玩具の12 m/sでの揚力は1.71で論文の2.0に届かず、20 m/sでも1.92です。出力の噴流速度は揚力の目標ではなく、ペナルティが始まる枠を指しています。

DQNの兄弟: Doubleと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)−1∣A∣∑a′A(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 m/sと13 m/sのように行動が似ていると、状態価値は一度学べば足ります。論文でDueling DQNの学習曲線が最も揺れなかった理由です。

5層、各128ニューロンが300エピソード以内に収束しました。能動制御を入れるとCLC_Lは1.79から約2.0へ上がり、後流は静まります。上の表学習が12 m/sで出す揚力1.71とは別の数字です。

12 m/sが揚力の最適値ではない理由#

300エピソードの0.13は、ほとんどがR1R_1の横で起きた小さな移動です。論文の揚力増が報酬に入れる分は0.042です。その差を噴流速度の一枠、1 m/sとして信じてよいかは別の問題です。

本物のCFDで11 m/sと13 m/sを見分けるには、エピソードごとに渦の周期を25回積分しなければなりません。時間刻みを大きく取ってもL∞L_\inftyが崩れる場所は、1001倍ステップの請求書にあります。その請求書の上で、12 m/sの無駄項と論文の揚力2.0を分けられるでしょうか。

役に立ったらシェアしてください。