课程摘要
本节承接一维卷积,学习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官方的RNN、LSTM和GRU说明。
二十、迁移到真实力学仿真数据
真实时序输入可以包含多个通道:
\ 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参数
-
逐轮训练记录
