本文译自作者的韩语原文。 阅读原文(한국어)
[论文综述] 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)
- 设定: 雷诺数(惯性力与粘性力之比)100、马赫数 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:
"""一维现象学翼型-尾流环境。
状态 : 离散化的升力振荡幅度 (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,涡交替脱落, 振幅很大。拉到 15–20 m/s,涡变淡,尾流安静下来。输出里的 12 m/s 不是这根滑块的右端。暂停后把滑块停在 12 附近,就能比较奖励想要的安静尾流,和表停住的那一格是不是同一个地方。
输出里的3.25是代码坐进平衡后的数。动作12的控制比例是12/20 = 0.6,振幅停在。升力是,阻力是。奖励,对上第300回合那一行。浪费项只在动作超过12时打开。12 m/s不是升力公式的顶点,而是浪费为零的最后一格。
升到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 与 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 的升力吗?
相关文章
如果对您有帮助,请分享。