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

本文译自作者的韩语原文。 阅读原文(한국어)

NOTE #065DAY SUN 논문리뷰DATE 2026.06.07UPDATED 2026.10.19READ 4 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)没有把射流速度锁住。前缘附近的合成射流(synthetic jet,吹完再吸、净质量为零的作动器)从 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)
  • 设定: 雷诺数(惯性力与粘性力之比)100、马赫数 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 大就多晃,ϵ\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)就推迟,涡脱落变弱。

马赫数 0.2 时,智能体还没看清尾流,声波已经先把时间步收紧。不抹掉这些声波来解低马赫问题,是渐近保持的 IMEX 波动格式在处理的事。尾流那张图放在奖励数字打印之后。

动手:用 Q 学习打开射流#

论文的 DQN 不整网搬过来。只留下状态格子和离散速度的表格式 Q 学习,跑同一套控制。状态是升力振荡幅度的分档,动作是射流速度。

import numpy as np
 
class SyntheticJetEnv:
    """一维现象学翼型-尾流环境。
 
    状态  : 离散化的升力振荡幅度 (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)      # 一阶松弛
        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不是升力公式的顶点,而是浪费为零的最后一格。

升到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 的升力吗?

如果对您有帮助,请分享。