【零基础学智能仿真-16】循环神经网络与LSTM/GRU——学习力学响应的历史记忆

课程摘要

本节承接一维卷积,学习RNN、LSTM、GRU和双向循环网络处理力学时序数据的方法。课程从"当前载荷相同,结构响应为什么仍然不同"出发,讲解隐藏状态、时间展开、反向传播、梯度消失及门控记忆机制,并用纯NumPy训练一个循环神经网络,预测具有历史效应的位移响应。学习者将掌握时序数据组织、整序列划分、训练评估与因果性判断,为蠕变、动力响应、疲劳及裂纹扩展预测奠定基础。


一、承接第十五节:为什么还要学习RNN?

第十五节学习了一维卷积。它使用固定宽度的局部窗口提取时序特征:

\ y_t = \\sum_{k=0}\^{K-1}w_kx_{t+k}+c \\

如果卷积核长度为5,它主要观察相邻的5个时刻。增加卷积层数可以扩大感受野,但对于明显依赖长期历史的力学问题,我们还需要一种能够沿时间传递状态的模型。

例如:

  • 黏弹性材料的当前应变与过去载荷有关;
  • 塑性材料的当前响应与加载路径有关;
  • 疲劳损伤与历史循环次数和载荷幅值有关;
  • 裂纹扩展速度与先前裂纹长度和载荷历史有关;
  • 动力响应与前一时刻的位移和速度有关。

这类问题通常不能写成:

\ y_t=f(x_t) \\

更合理的表达为:

\ y_t = f(x_t,x_{t-1},x_{t-2},\\ldots) \\

循环神经网络通过隐藏状态压缩过去的信息:

\ y_t=f(x_t,h_{t-1}) \\


二、为什么相同载荷可能产生不同响应?

考虑两个加载历史:

  • 历史A:先施加 \(8\ \mathrm{kN}\),随后降到 \(4\ \mathrm{kN}\);
  • 历史B:先保持零载荷,随后升到 \(4\ \mathrm{kN}\)。

切换后,两种工况的当前载荷完全相同:

\ F_t\^{A}=F_t\^{B}=4\\ \\mathrm{kN} \\

但如果结构具有黏弹性或其他历史效应,则可能出现:

\ u_t\^{A}\\neq u_t\^{B} \\

本节教学模型在切换时刻得到:

\ u_t\^{A}=3.8713\\ \\mathrm{mm} \\\ u_t\^{B}=0.6304\\ \\mathrm{mm} \\

【图1:相同载荷、不同历史产生不同响应】

如果模型只接收当前载荷 \(F_t\),它无法区分两种工况。RNN需要通过隐藏状态记住先前发生过什么。


三、力学中的三类时序预测任务

3.1 多输入、多输出

输入完整载荷历史,输出完整位移历史:

\ \\{F_1,F_2,\\ldots,F_T\\} \\longrightarrow \\{u_1,u_2,\\ldots,u_T\\} \\

适用场景:

  • 瞬态动力响应;
  • 蠕变变形预测;
  • 裂纹长度随时间演化;
  • 温度---位移耦合响应。

本节案例属于这一类型。

3.2 多输入、单输出

输入一段历史,输出一个结果:

\ \\{F_1,F_2,\\ldots,F_T\\} \\longrightarrow N_f \\

适用场景包括疲劳寿命、最终损伤值和最大位移预测。

3.3 滚动预测

使用过去一段数据预测下一时刻:

\ \\{u_{t-p+1},\\ldots,u_t\\} \\longrightarrow u_{t+1} \\

将预测结果继续作为输入,可以得到多步预测。但误差也会逐步累积。


四、RNN的基本结构

普通神经网络中,每个样本之间相互独立。RNN在处理第 \(t\) 个时刻时,还会接收前一时刻的隐藏状态:

\ h_t = \\tanh \\left( W_{xh}x_t+ W_{hh}h_{t-1}+ b_h \\right) \\

随后计算输出:

\ \\hat{y}_t = W_{hy}h_t+b_y \\

其中:

  • \(x_t\):当前时刻输入;
  • \(h_{t-1}\):上一时刻隐藏状态;
  • \(h_t\):更新后的隐藏状态;
  • \(\hat{y}_t\):当前预测;
  • \(W_{xh}\):输入到隐藏层的权重;
  • \(W_{hh}\):隐藏状态之间的循环权重;
  • \(W_{hy}\):隐藏状态到输出层的权重。

【图2:RNN沿时间展开】

图中的三个RNN单元并不是三个互不相关的模型。它们共享同一组参数:

\ W_{xh},\\quad W_{hh},\\quad W_{hy} \\

不同之处只是处理的时间位置不同。


五、隐藏状态应该怎样理解?

隐藏状态可以看成网络对历史信息的压缩表示:

\ h_t = \\operatorname{Memory} (x_1,x_2,\\ldots,x_t) \\

它可能包含:

  • 过去载荷的整体水平;
  • 最近是否发生载荷突变;
  • 响应处于增长阶段还是恢复阶段;
  • 振动的相位与幅值;
  • 损伤或裂纹演化趋势。

但普通数据驱动训练不保证某个隐藏神经元对应明确的物理量。因此,不能直接把某个 \(h_t\) 分量解释为塑性应变或损伤变量。

如果需要明确的物理内部变量,应通过网络结构、状态方程或物理损失进行约束。


六、一个RNN单元的手算过程

设输入和隐藏状态均为标量:

\ x_t=0.8 \\\ h_{t-1}=0.3 \\

参数为:

\ W_{xh}=0.5,\\quad W_{hh}=0.7,\\quad b_h=0.1 \\

则:

\ \\begin{aligned} h_t \&= \\tanh \\left( 0.5\\times0.8+ 0.7\\times0.3+ 0.1 \\right)\\\\ \&= \\tanh(0.71)\\\\ \&\\approx0.6107 \\end{aligned} \\

如果输出层参数为:

\ W_{hy}=1.2,\\qquad b_y=0.05 \\

则:

\ \\hat{y}_t = 1.2\\times0.6107+0.05 \\approx0.7828 \\

下一时刻计算时,\(0.6107\) 将作为新的历史状态继续传递。


七、RNN怎样训练?

对于长度为 \(T\) 的位移序列,均方误差可以写为:

\ \\mathcal{L} = \\frac{1}{T} \\sum_{t=1}\^{T} \\left( \\hat{u}_t-u_t \\right)\^2 \\

因为较早时刻的隐藏状态会影响后续预测,所以反向传播需要沿时间展开,这一过程称为BPTT:

\ \\text{Backpropagation Through Time} \\

某个循环权重的梯度可能包含连续相乘:

\ \\frac{\\partial h_T}{\\partial h_t} = \\prod_{k=t+1}\^{T} \\frac{\\partial h_k}{\\partial h_{k-1}} \\

这就产生了梯度消失与梯度爆炸问题。


八、梯度为什么会消失或爆炸?

将复杂的局部梯度简化为一个倍率 \(\lambda\),跨越 \(k\) 个时间步后:

\ G_k\\approx\\lambda\^kG_0 \\

如果:

\ \|\\lambda\|\<1 \\

梯度会逐渐趋近于零。

例如:

\ 0.5\^{40} \\approx9.09\\times10\^{-13} \\

如果:

\ \|\\lambda\|\>1 \\

梯度会迅速增大。例如:

\ 1.1\^{40} \\approx45.26 \\

【图3:梯度消失与梯度爆炸】

常用处理方法包括:

  • 使用LSTM或GRU;
  • 对梯度进行裁剪;
  • 合理初始化循环权重;
  • 对输入和输出进行标准化;
  • 控制序列长度;
  • 使用截断时间反向传播;
  • 调整学习率。

本节NumPy程序将梯度范数限制在5以内:

复制代码
total_norm = np.sqrt(
    sum(
        np.sum(gradient**2)
        for gradient in gradients.values()
    )
)

if total_norm > 5.0:
    scale = 5.0 / total_norm

    gradients = {
        name: gradient * scale
        for name, gradient in gradients.items()
    }

九、LSTM:使用门控制长期记忆

普通RNN只有隐藏状态 \(h_t\)。LSTM增加了记忆单元 \(c_t\),并使用三个门控制信息流动。

9.1 遗忘门

\ f_t = \\sigma \\left( W_f\[x_t,h_{t-1}+b_f \right) \]

它决定保留多少过去记忆:

\ f_t\\odot c_{t-1} \\

当某一分量接近1时,相关记忆被大量保留;接近0时,相关记忆被大量遗忘。

9.2 输入门

\ i_t = \\sigma \\left( W_i\[x_t,h_{t-1}+b_i \right) \]

候选记忆为:

\ \\tilde{c}_t = \\tanh \\left( W_c\[x_t,h_{t-1}+b_c \right) \]

写入的新信息为:

\ i_t\\odot\\tilde{c}_t \\

9.3 更新记忆

\ c_t = f_t\\odot c_{t-1} + i_t\\odot\\tilde{c}_t \\

9.4 输出门

\ o_t = \\sigma \\left( W_o\[x_t,h_{t-1}+b_o \right) \]

最终隐藏状态为:

\ h_t = o_t\\odot\\tanh(c_t) \\

【图4:LSTM门控记忆机制】

LSTM的记忆通道使信息能够较平稳地跨越多个时间步,因此更适合长时间依赖问题。


十、GRU:更轻量的门控网络

GRU将LSTM的结构进行简化,主要使用更新门和重置门。

更新门:

\ z_t = \\sigma \\left( W_zx_t+U_zh_{t-1}+b_z \\right) \\

重置门:

\ r_t = \\sigma \\left( W_rx_t+U_rh_{t-1}+b_r \\right) \\

候选隐藏状态:

\ \\tilde{h}_t = \\tanh \\left( W_hx_t+ U_h(r_t\\odot h_{t-1})+ b_h \\right) \\

最终状态:

\ h_t = (1-z_t)\\odot h_{t-1} + z_t\\odot\\tilde{h}_t \\

RNN、GRU和LSTM的特点可以概括为:

模型 状态结构 参数量 适用情况
普通RNN 隐藏状态 较少 较短、较简单的历史关系
GRU 两个主要门 中等 需要记忆且希望模型较轻量
LSTM 三个门和独立记忆单元 较多 长期依赖和复杂加载历史

模型选择仍应依据验证结果,不应默认LSTM一定优于GRU。


十一、双向RNN能否用于实时预测?

双向RNN同时处理正向序列和反向序列:

\ \\overrightarrow{h_t} = f(x_t,\\overrightarrow{h_{t-1}}) \\\ \\overleftarrow{h_t} = f(x_t,\\overleftarrow{h_{t+1}}) \\

最终特征由两部分组成:

\ h_t = \\begin{bmatrix} \\overrightarrow{h_t}\\\\ \\overleftarrow{h_t} \\end{bmatrix} \\

它能够同时利用过去和未来的信息,适合:

  • 已完成试验数据的离线分析;
  • 整段信号的异常识别;
  • 已知完整载荷历史时的响应重建;
  • 离线疲劳序列分类。

如果目标是在时刻 \(t\) 实时预测,而未来的 \(x_{t+1}\) 尚未发生,则不能使用反向信息。否则会造成未来信息泄漏。


十二、实战案例:具有历史效应的位移预测

本节生成1000条载荷序列,每条包含60个时间步:

\ X\\in\\mathbb{R}\^{1000\\times60\\times1} \\

其中:

  • 1000:仿真工况数量;
  • 60:每个工况的时间步数;
  • 1:载荷通道数量。

教学响应使用以下离散记忆模型生成:

\ m_t = 0.88m_{t-1} + 0.12F_t \\

位移为:

\ u_t = 0.10F_t + 0.48m_t + \\varepsilon_t \\

其中:

\ \\varepsilon_t \\sim \\mathcal{N}(0,0.01\^2) \\

这个方程用于演示历史记忆,不是经过材料试验标定的黏弹性本构模型,也不是有限元求解结果。


十三、生成时序数据

复制代码
import numpy as np


def response_from_load(
    load,
    memory_factor=0.88,
    noise=None
):
    memory = np.zeros(
        load.shape[:-2] + (1,)
    )

    outputs = []

    for time_index in range(load.shape[-2]):
        current_load = load[
            ..., time_index, :
        ]

        memory = (
            memory_factor * memory
            + (1.0 - memory_factor)
            * current_load
        )

        displacement = (
            0.10 * current_load
            + 0.48 * memory
        )

        outputs.append(displacement)

    response = np.stack(
        outputs,
        axis=-2
    )

    if noise is not None:
        response = response + noise

    return response

生成1000条载荷序列后:

复制代码
print("输入数据形状:", load_sequences.shape)
print("输出数据形状:", displacement_sequences.shape)

实际输出:

复制代码
输入数据形状: (1000, 60, 1)
输出数据形状: (1000, 60, 1)

十四、正确划分时序数据

本节按照完整序列划分:

数据 序列数量 用途
训练集 640 更新RNN参数
验证集 160 选择模型
测试集 200 最终评价

不能把一条序列的前40个时间步放入训练集,后20个时间步放入测试集,再宣称它们相互独立。

正确原则是:

\ \\boxed{ \\text{一个完整工况只能属于一个数据子集} } \\

如果使用滑动窗口,相邻窗口可能包含大量重复时间步,也应按照原始试验或原始仿真编号进行分组。


十五、用NumPy实现一个简单RNN

15.1 初始化参数

复制代码
class SimpleRNN:
    def __init__(
        self,
        input_size=1,
        hidden_size=12,
        output_size=1,
        seed=42
    ):
        rng = np.random.default_rng(seed)

        self.hidden_size = hidden_size

        self.params = {
            "Wxh": rng.normal(
                0,
                np.sqrt(1 / input_size),
                (input_size, hidden_size)
            ),
            "Whh": rng.normal(
                0,
                np.sqrt(1 / hidden_size),
                (hidden_size, hidden_size)
            ),
            "Why": rng.normal(
                0,
                np.sqrt(1 / hidden_size),
                (hidden_size, output_size)
            ),
            "bh": np.zeros(hidden_size),
            "by": np.zeros(output_size)
        }

模型参数量为:

\ 1\\times12 + 12\\times12 + 12\\times1 + 12 + 1 = 181 \\


15.2 前向传播

复制代码
def forward(self, x):
    batch_size, time_steps, _ = x.shape

    hidden_states = [
        np.zeros(
            (batch_size, self.hidden_size)
        )
    ]

    predictions = []

    for time_index in range(time_steps):
        hidden = np.tanh(
            x[:, time_index, :]
            @ self.params["Wxh"]
            + hidden_states[-1]
            @ self.params["Whh"]
            + self.params["bh"]
        )

        prediction = (
            hidden @ self.params["Why"]
            + self.params["by"]
        )

        hidden_states.append(hidden)
        predictions.append(prediction)

    return (
        np.stack(predictions, axis=1),
        hidden_states
    )

输出形状仍然为:

\ (N,T,1) \\

因为本节需要为每一个时间步预测位移。


十六、标准化时序数据

训练集载荷的均值和标准差为:

\ \\mu_F,\\qquad s_F \\

训练集位移的均值和标准差为:

\ \\mu_u,\\qquad s_u \\

标准化为:

\ \\tilde{F} = \\frac{F-\\mu_F}{s_F} \\\ \\tilde{u} = \\frac{u-\\mu_u}{s_u} \\

代码如下:

复制代码
x_mean = x_train.mean()
x_std = x_train.std()

y_mean = y_train.mean()
y_std = y_train.std()

x_train_scaled = (
    x_train - x_mean
) / x_std

x_validation_scaled = (
    x_validation - x_mean
) / x_std

x_test_scaled = (
    x_test - x_mean
) / x_std

y_train_scaled = (
    y_train - y_mean
) / y_std

均值和标准差只能根据训练序列计算。


十七、实际训练结果

本节设置:

  • 隐藏状态维度:12;
  • 批量大小:64;
  • 最大训练轮数:500;
  • Adam学习率:0.0025;
  • 梯度裁剪阈值:5。

实际运行结果:

复制代码
数据形状:1000 × 60 × 1
训练序列:640
验证序列:160
测试序列:200
隐藏状态维度:12
模型参数量:181
实际训练轮数:500
最佳模型轮数:500
最佳验证RMSE:0.015388 mm

【图5:RNN训练曲线】

本次最低验证误差出现在第500轮,也就是训练预算的最后一轮。这说明验证误差仍在缓慢改善,早停条件没有触发。

因此,不能把第500轮解释成已经找到理论上的最优训练轮数。它只是本次最大训练轮数范围内的最佳结果。


十八、独立测试结果

测试集指标为:

\ \\mathrm{RMSE} = 0.015855\\ \\mathrm{mm} \\\ \\mathrm{MAE} = 0.011412\\ \\mathrm{mm} \\

平均峰值绝对误差为:

\ e_{\\mathrm{peak}} = 0.010398\\ \\mathrm{mm} \\

【图6:独立测试序列预测】

图中:

  • 第一部分为载荷历史;
  • 第二部分为真实位移与RNN预测;
  • 第三部分为逐时间步残差。

RNN总体能够跟随加载、卸载和趋势改变,但个别长时间增长序列的末端仍存在小幅系统偏差。时序评价不能只报告全局RMSE,还应检查长时间漂移和关键峰值。


十九、使用PyTorch建立RNN、LSTM和GRU

安装PyTorch后,可以使用统一的输入形状:

\ (N,T,C) \\

复制代码
import torch
import torch.nn as nn

x = torch.zeros(
    32, 60, 1
)

其中32为批量大小,60为时间步数,1为输入通道数。

普通RNN

复制代码
rnn = nn.RNN(
    input_size=1,
    hidden_size=16,
    batch_first=True
)

output, hidden = rnn(x)

print(output.shape)
print(hidden.shape)

预期输出:

复制代码
torch.Size([32, 60, 16])
torch.Size([1, 32, 16])

LSTM

复制代码
lstm = nn.LSTM(
    input_size=1,
    hidden_size=16,
    batch_first=True
)

output, (hidden, cell) = lstm(x)

print(output.shape)
print(hidden.shape)
print(cell.shape)

预期输出:

复制代码
torch.Size([32, 60, 16])
torch.Size([1, 32, 16])
torch.Size([1, 32, 16])

GRU

复制代码
gru = nn.GRU(
    input_size=1,
    hidden_size=16,
    batch_first=True
)

output, hidden = gru(x)

print(output.shape)
print(hidden.shape)

预期输出:

复制代码
torch.Size([32, 60, 16])
torch.Size([1, 32, 16])

接口与张量定义可参考PyTorch官方的RNNLSTMGRU说明。


二十、迁移到真实力学仿真数据

真实时序输入可以包含多个通道:

\ x_t= \\begin{bmatrix} F_t\& T_t\& p_t\& u_{t-1}\& \\sigma_{t-1} \\end{bmatrix} \\

例如:

输入通道 含义
\(F_t\) 当前载荷
\(T_t\) 当前温度
\(p_t\) 当前压力
\(u_{t-1}\) 上一步位移
\(\sigma_{t-1}\) 上一步应力

输出可以是:

\ y_t= \\begin{bmatrix} u_t\& v_t\& \\sigma_t\& d_t \\end{bmatrix} \\

其中 \(d_t\) 可以表示损伤变量或裂纹长度。

从Abaqus或其他求解器生成训练数据时,应记录:

  • 时间步或增量步;
  • 载荷幅值;
  • 场变量或历史变量;
  • 材料和几何参数;
  • 分析步是否收敛;
  • 序列是否被提前终止;
  • 每个通道的单位。

不同工况的时间采样间隔如果不一致,需要统一重采样,或者将时间间隔 \(\Delta t\) 作为额外输入。


二十一、时序模型的评价指标

全序列RMSE

\ \\mathrm{RMSE} = \\sqrt{ \\frac{1}{NT} \\sum_{i=1}\^{N} \\sum_{t=1}\^{T} \\left( \\hat{u}_{i,t}-u_{i,t} \\right)\^2 } \\

峰值误差

\ e_{\\mathrm{peak}} = \\left\| \\max_t\\hat{u}_t - \\max_tu_t \\right\| \\

最终时刻误差

\ e_{\\mathrm{final}} = \\left\| \\hat{u}_T-u_T \\right\| \\

相位误差

对于振动问题,应检查预测峰值与真实峰值的时间差:

\ e_{\\mathrm{phase}} = \\left\| t_{\\mathrm{peak,pred}} - t_{\\mathrm{peak,true}} \\right\| \\

对于疲劳和裂纹扩展,还应检查累计损伤、裂纹起始时刻和失效时刻,而不能只看逐点平均误差。


二十二、常见错误

错误一:随机拆分时间步

相邻时间步高度相关。应按照完整仿真、完整试件或完整载荷历史划分数据。

错误二:在实时预测中使用双向RNN

双向模型会读取未来数据,不适合严格的在线因果预测。

错误三:忽略初始状态

如果不同工况具有不同初始塑性应变、温度或损伤状态,应把它们作为输入或用于初始化隐藏状态。

错误四:把缺失时间步直接补零

零可能具有真实物理含义。应使用掩码,并区分"实际为零"和"数据缺失"。

错误五:忽略时间间隔

相同的60个时间步可能对应6秒,也可能对应600秒。材料的蠕变和动力响应会完全不同。

错误六:只检查短期预测

滚动预测可能在前几步很准确,随后逐渐漂移。必须检查完整预测区间。


二十三、课后练习

练习一:改变记忆系数

将:

\ m_t=0.88m_{t-1}+0.12F_t \\

改为:

\ m_t=0.97m_{t-1}+0.03F_t \\

观察响应记忆时间如何变化,并重新训练RNN。

练习二:改变隐藏状态维度

分别设置:

\ H=4,\\quad H=12,\\quad H=32 \\

比较参数量、验证误差和训练时间。

练习三:缩短输入历史

只允许模型观察最近5个时间步,比较它与循环模型在卸载阶段的误差。

练习四:检查因果性

构建双向RNN用于离线序列重建,并说明为什么它不能直接用于实时数字孪生。

练习五:加入第二个输入通道

在载荷之外加入温度:

\ x_t= \\begin{bmatrix} F_t\&T_t \\end{bmatrix} \\

设计一个同时具有载荷记忆和温度影响的教学响应模型。


二十四、本节小结

RNN通过隐藏状态将过去信息传递到当前时刻:

\ \\boxed{ h_t=f(x_t,h_{t-1}) } \\

普通RNN结构简单,但长序列训练容易出现梯度消失或爆炸。LSTM通过遗忘门、输入门和输出门管理长期记忆,GRU以更简化的门控结构实现相似目的。

对于力学时序预测,模型选择只是其中一部分。完整序列划分、时间尺度、初始状态、因果性和长时间漂移同样决定结果是否可靠。


课程附件

完整程序使用NumPy和Matplotlib,已经完成实际训练并生成正文插图。建议将其放在课程末尾的"附录:完整可运行代码"中。

  • 完整可运行代码

  • 实际运行结果

  • 完整时序数据集

  • 训练完成的RNN参数

  • 逐轮训练记录

【零基础学智能仿真-16】循环神经网络与LSTM/GRU-学习力学响应的历史记忆资源-CSDN下载

相关推荐
愚公搬代码1 小时前
【愚公系列】《造浪者:AI创业实战地图》002-AI创业的六个本质差异
人工智能
Dawson Zhu1 小时前
从“会回答“到“能执行“:AI Agent 的系统构成、运行机制与工程实践
人工智能·语言模型·架构·aigc·agi
七夜zippoe1 小时前
AI Agent 的核心能力循环:感知→规划→执行→反思→记
人工智能·ai·agent·循环·核心能力
loser.with.m1 小时前
【AgentScope 2.0】8-MCP 集成:Agent 的「USB-C 接口」是怎么接的
人工智能·spring boot·agentscope
果壳science1 小时前
张祥前统一场论研讨会在香港理工大学举办
人工智能·算法
xsd202411181 小时前
电梯轿厢事件识别
人工智能
kaixin_啊啊1 小时前
Codex论文辅助全流程
人工智能·笔记·学习·ai·大模型
科技每日热闻2 小时前
AWS Activate云积分可以用于哪些云服务和AI开发场景?
人工智能·ai·云计算·aws
世岩清上2 小时前
文旅历史展厅纪录片式视频,怎样弱化说教感提升自主观看欲?
人工智能·音视频·宣传片·展厅改造