课程摘要
本节承接神经网络基础,学习卷积神经网络处理有限元场数据的基本方法。课程从局部感受野和权重共享出发,讲解卷积核、通道、步长、填充、池化及残差连接,并通过带圆孔板教学应力场演示CNN如何识别应力集中区域。学习者将掌握卷积输出尺寸和参数量的计算方法,理解标量预测、全场预测与一维时序卷积的区别,为后续CNN代理模型、LSTM和U-Net课程建立基础。
一、承接第十四节:为什么MLP不适合直接处理应力场?
第十四节使用多层感知机建立了映射:
\[ (F,L,b,h) \longrightarrow \sigma_{\max} \]
它的输入只有四个参数,使用MLP非常自然。
如果现在要预测整个有限元应力场,输出可能是:
\[ \boldsymbol{\sigma} \in \mathbb{R}^{64\times64} \]
输入也可能包含几何、材料、载荷和边界条件组成的多通道场:
\[ \boldsymbol{X} \in \mathbb{R}^{C\times64\times64} \]
一种直接方法是把二维场展开成一维向量:
\[ 64\times64 \longrightarrow 4096 \]
然后送入全连接层。但这样会产生两个问题。
第一,展开操作削弱了空间邻接关系。模型不再直接知道两个数值来自相邻节点还是模型两端。
第二,参数量迅速增加。若将4096个输入连接到32个神经元,需要:
\[ 4096\times32+32 = 131104 \]
个参数。
CNN保留二维结构,让同一个小卷积核在整个场上滑动,从而识别局部模式。
二、CNN观察有限元结果的方式
工程师查看云图时,通常会注意:
- 孔洞和缺口附近的应力集中;
- 裂纹尖端附近的高梯度区域;
- 载荷作用点附近的局部响应;
- 固定边界附近的约束效应;
- 场变量是否连续、对称或发生突变。
CNN的思路与此类似:使用较小的局部窗口观察场数据,并在不同位置重复使用同一组权重。
其核心可以概括为:
\[ \boxed{ \text{局部感受野} + \text{权重共享} + \text{分层特征提取} } \]
三、卷积核如何工作?
3.1 二维卷积计算
设输入场为 \(X\),卷积核为 \(K\)。输出特征图中第 \((i,j)\) 个位置为:
\[ Y_{i,j} = \sum_{m=0}^{k_h-1} \sum_{n=0}^{k_w-1} K_{m,n} X_{i+m,j+n} +c \]
其中:
- \(k_h,k_w\) 为卷积核高度和宽度;
- \(c\) 为偏置;
- \(Y\) 为卷积得到的特征图。
深度学习框架中的Conv2d通常执行互相关运算,即不翻转卷积核,但行业中仍习惯称其为卷积。
3.2 一个可以手算的卷积示例
输入场为:
\[ X= \begin{bmatrix} 0&0&0&0&0\\ 0&1&1&1&0\\ 0&1&3&1&0\\ 0&1&1&1&0\\ 0&0&0&0&0 \end{bmatrix} \]
使用卷积核:
\[ K= \begin{bmatrix} -1&-1&-1\\ -1&8&-1\\ -1&-1&-1 \end{bmatrix} \]
计算输出左上角时,取输入左上方的 \(3\times3\) 窗口:
\[ X_{\mathrm{window}} = \begin{bmatrix} 0&0&0\\ 0&1&1\\ 0&1&3 \end{bmatrix} \]
逐元素相乘并求和:
\[ \begin{aligned} Y_{1,1} &= 0(-1)+0(-1)+0(-1)\\ &\quad+0(-1)+1(8)+1(-1)\\ &\quad+0(-1)+1(-1)+3(-1)\\ &=3 \end{aligned} \]
卷积核继续向右、向下滑动,最终得到:
\[ Y= \begin{bmatrix} 3&1&3\\ 1&16&1\\ 3&1&3 \end{bmatrix} \]
中心区域的输出值16明显较大,说明该卷积核对局部突变较为敏感。
【图1:卷积核滑动与输出计算】
四、使用NumPy实现二维卷积
下面的代码不依赖PyTorch,可直接运行并验证上述计算。
import numpy as np from numpy.lib.stride_tricks import sliding_window_view def conv2d_single(field, kernel, padding=0, stride=1): if padding > 0: field = np.pad( field, padding, mode="constant" ) windows = sliding_window_view( field, kernel.shape )[::stride, ::stride] return np.einsum( "ijkl,kl->ij", windows, kernel ) field = np.array([ [0, 0, 0, 0, 0], [0, 1, 1, 1, 0], [0, 1, 3, 1, 0], [0, 1, 1, 1, 0], [0, 0, 0, 0, 0] ], dtype=float) kernel = np.array([ [-1, -1, -1], [-1, 8, -1], [-1, -1, -1] ], dtype=float) feature_map = conv2d_single( field, kernel ) print(feature_map.astype(int)) print("输出形状:", feature_map.shape)实际输出:
[[ 3 1 3] [ 1 16 1] [ 3 1 3]] 输出形状: (3, 3)五、卷积输出尺寸怎样计算?
设输入尺寸为 \(H\times W\),卷积核尺寸为 \(K_h\times K_w\),填充为 \(P\),步长为 \(S\)。
忽略空洞卷积时,输出高度为:
\[ H_{\mathrm{out}} = \left\lfloor \frac{H+2P-K_h}{S} \right\rfloor+1 \]
输出宽度为:
\[ W_{\mathrm{out}} = \left\lfloor \frac{W+2P-K_w}{S} \right\rfloor+1 \]
示例一:不填充
输入为 \(64\times64\),卷积核为 \(3\times3\),步长为1,填充为0:
\[ H_{\mathrm{out}} = \frac{64-3}{1}+1 = 62 \]
所以:
\[ 64\times64 \longrightarrow 62\times62 \]
示例二:保持尺寸不变
设置:
\[ K=3,\qquad P=1,\qquad S=1 \]
则:
\[ H_{\mathrm{out}} = \frac{64+2-3}{1}+1 = 64 \]
所以:
\[ 64\times64 \longrightarrow 64\times64 \]
这种设置通常称为相同尺寸填充。
示例三:使用步长2降采样
设置:
\[ K=3,\qquad P=1,\qquad S=2 \]
则:
\[ H_{\mathrm{out}} = \left\lfloor \frac{64+2-3}{2} \right\rfloor+1 = 32 \]
所以:
\[ 64\times64 \longrightarrow 32\times32 \]
六、步长、填充和边界效应
6.1 步长Stride
步长表示卷积核每次移动的距离。
- \(S=1\):逐个位置扫描,空间信息保留较多;
- \(S=2\):每次移动两个位置,同时完成降采样;
- 步长越大,输出尺寸通常越小。
6.2 填充Padding
填充是在输入边界外增加数值。
零填充表示:
\[ X_{\mathrm{outside}}=0 \]
它可以保持输出尺寸,但也可能在边界处产生人为的数值跳变。
对于有限元问题,零填充不一定具有正确物理意义。例如,应力场边界外的零值并不代表真实材料或真实边界条件。因此,边界附近的预测应单独检查。
七、通道是什么?
普通灰度图像只有一个通道:
\[ X\in\mathbb{R}^{1\times H\times W} \]
有限元代理模型可以把不同信息放入不同通道。例如:
| 通道 | 可以表示的内容 |
|---|---|
| 通道1 | 几何区域掩码 |
| 通道2 | 材料弹性模量场 |
| 通道3 | 泊松比场 |
| 通道4 | 水平载荷场 |
| 通道5 | 竖直载荷场 |
| 通道6 | 水平位移约束 |
| 通道7 | 竖直位移约束 |
于是输入可以写成:
\[ X\in\mathbb{R}^{7\times64\times64} \]
输出同样可以有多个通道:
\[ Y= \begin{bmatrix} U_1\\ U_2\\ S_{11}\\ S_{22}\\ S_{12}\\ S_{\mathrm{Mises}} \end{bmatrix} \]
在PyTorch中,二维场张量通常采用:
\[ (N,C,H,W) \]
其中:
- \(N\):批量中的样本数量;
- \(C\):通道数量;
- \(H\):场的高度;
- \(W\):场的宽度。
八、权重共享为什么能减少参数?
假设输入为单通道 \(64\times64\) 场,需要得到32个输出特征。
如果使用全连接层:
\[ 4096\longrightarrow32 \]
参数量为:
\[ 4096\times32+32 = 131104 \]
如果使用32个 \(3\times3\) 卷积核,参数量为:
\[ 3\times3\times1\times32+32 = 320 \]
参数量缩小倍数为:
\[ \frac{131104}{320} = 409.7 \]
【图2:全连接层与卷积层参数量比较】
卷积核在所有空间位置共享同一组参数,所以参数数量与输入场的宽度、高度没有直接关系。
需要区分“参数少”和“计算量小”。卷积核仍需在大量位置执行运算,因此参数减少不代表计算量一定同比例下降。
九、池化层:压缩空间信息
9.1 最大池化
对于一个 \(2\times2\) 区域:
\[ \begin{bmatrix} 1.2&2.5\\ 1.8&1.4 \end{bmatrix} \]
最大池化输出为:
\[ \max(1.2,2.5,1.8,1.4)=2.5 \]
最大池化倾向于保留局部最显著响应,因此适合提取:
- 应力集中;
- 裂纹尖端高响应;
- 局部缺陷特征;
- 场变量突变区域。
9.2 平均池化
同一区域的平均池化为:
\[ \frac{1.2+2.5+1.8+1.4}{4} = 1.725 \]
平均池化更强调区域整体水平。
对于高应力集中问题,最大池化可能更容易保留峰值;但它不能替代严格的峰值误差评价。
9.3 NumPy池化代码
def max_pool2d(field, pool_size=2, stride=2): windows = sliding_window_view( field, (pool_size, pool_size) )[::stride, ::stride] return windows.max(axis=(-2, -1)) field = np.arange(64 * 64).reshape(64, 64) pooled = max_pool2d( field, pool_size=2, stride=2 ) print("输入形状:", field.shape) print("池化后形状:", pooled.shape)实际输出:
输入形状: (64, 64) 池化后形状: (32, 32)十、有限元场特征提取示例
下面构造一个带圆孔板的教学场,用来演示局部应力集中。
需要明确:该场是根据解析形式人为构造的可视化数据,不是有限元求解结果,也不能替代Kirsch解或Abaqus计算。
输入场尺寸为:
\[ 64\times64 \]
使用Sobel卷积核提取水平方向变化:
\[ K_x= \frac{1}{8} \begin{bmatrix} -1&0&1\\ -2&0&2\\ -1&0&1 \end{bmatrix} \]
竖直方向卷积核为:
\[ K_y=K_x^{\mathrm T} \]
局部变化强度为:
\[ G= \sqrt{ G_x^2+G_y^2 } \]
【图3:应力场、卷积特征与池化结果】
图中可以看到:
- 原始场在圆孔两侧形成集中区域;
- Sobel卷积突出孔边界和场梯度较大的位置;
- 最大池化将尺寸从 \(64\times64\) 降至 \(32\times32\);
- 池化后主要集中区域仍被保留,但空间细节有所损失。
真正训练CNN时,卷积核不是预先指定的Sobel算子,而是通过反向传播从训练数据中学习。
十一、一个基础CNN的尺寸变化
下面建立两层卷积特征提取器:
\[ 64\times64\times1 \]
经过第一层卷积后:
\[ 64\times64\times8 \]
第一次最大池化后:
\[ 32\times32\times8 \]
第二层卷积后:
\[ 32\times32\times16 \]
第二次最大池化后:
\[ 16\times16\times16 \]
【图4:CNN特征提取结构】
尺寸变化可以理解为:
\[ \boxed{ \text{空间分辨率逐渐下降,特征通道逐渐增加} } \]
浅层卷积可能学习边界、梯度和局部集中;深层卷积会组合这些基础特征,形成更大范围的空间模式。
十二、使用NumPy检查CNN张量尺寸
以下代码使用8个随机卷积核完成第一层前向计算。随机卷积核没有经过训练,代码目的只是验证通道和空间尺寸。
rng = np.random.default_rng(42) # 一个样本、一个通道、64×64场 input_tensor = np.zeros( (1, 1, 64, 64) ) # 8个输出通道、1个输入通道、3×3卷积核 conv1_weights = rng.normal( 0, 0.1, size=(8, 1, 3, 3) ) conv1_bias = np.zeros(8) feature_maps = np.stack([ conv2d_single( input_tensor[0, 0], conv1_weights[channel, 0], padding=1 ) + conv1_bias[channel] for channel in range(8) ])[None, ...] # ReLU激活 feature_maps = np.maximum( feature_maps, 0 ) pooled_maps = np.stack([ max_pool2d(feature_maps[0, channel]) for channel in range(8) ])[None, ...] print("输入:", input_tensor.shape) print("卷积核:", conv1_weights.shape) print("卷积输出:", feature_maps.shape) print("池化输出:", pooled_maps.shape)实际输出:
输入: (1, 1, 64, 64) 卷积核: (8, 1, 3, 3) 卷积输出: (1, 8, 64, 64) 池化输出: (1, 8, 32, 32)卷积输出中的8表示网络使用8个不同卷积核,产生8张特征图。
十三、使用PyTorch定义基础CNN
安装PyTorch后,可以将同样的结构写成:
import torch import torch.nn as nn class StressCNN(nn.Module): def __init__(self): super().__init__() self.features = nn.Sequential( nn.Conv2d( in_channels=1, out_channels=8, kernel_size=3, padding=1 ), nn.ReLU(), nn.MaxPool2d( kernel_size=2, stride=2 ), nn.Conv2d( in_channels=8, out_channels=16, kernel_size=3, padding=1 ), nn.ReLU(), nn.MaxPool2d( kernel_size=2, stride=2 ) ) self.pool = nn.AdaptiveMaxPool2d((1, 1)) self.output = nn.Linear(16, 1) def forward(self, x): x = self.features(x) x = self.pool(x) x = torch.flatten(x, start_dim=1) return self.output(x) model = StressCNN() sample = torch.zeros( 4, 1, 64, 64 ) prediction = model(sample) print("输入形状:", sample.shape) print("输出形状:", prediction.shape)预期输出:
输入形状: torch.Size([4, 1, 64, 64]) 输出形状: torch.Size([4, 1])这里的4表示一个批次中有4个样本,每个样本输出一个标量。
Conv2d和MaxPool2d的参数定义可参考PyTorch Conv2d文档和PyTorch MaxPool2d文档。
十四、CNN可以完成哪些力学任务?
14.1 标量回归
输入几何和载荷场,输出:
\[ \sigma_{\max} \]
或者:
\[ U_{\max},\quad J,\quad K_I,\quad N_f \]
这类任务的输出是一个或少量数值,可以在卷积特征提取器后连接池化层和全连接层。
14.2 缺陷分类
输入应力场、位移场或检测图像,输出:
\[ \text{无缺陷} \]\[ \text{孔隙} \]\[ \text{脱层} \]\[ \text{裂纹} \]
分类任务通常在最后使用Softmax或Sigmoid,并采用交叉熵损失。
14.3 全场预测
输入:
\[ \text{几何} + \text{材料} + \text{载荷} + \text{边界条件} \]
输出:
\[ \hat{\boldsymbol{\sigma}} \in \mathbb{R}^{64\times64} \]
此时不能把全部空间信息压缩成一个标量。通常需要编码器—解码器结构:
\[ \text{输入场} \rightarrow \text{压缩特征} \rightarrow \text{上采样重建} \rightarrow \text{预测场} \]
后续U-Net课程将专门讲解这种全场预测方法。
十五、残差连接
当CNN层数增加时,网络可能出现训练困难。残差连接将输入直接加到变换结果上:
\[ \boldsymbol{y} = \mathcal{F}(\boldsymbol{x}) + \boldsymbol{x} \]
如果当前层不需要改变输入,网络只需让:
\[ \mathcal{F}(\boldsymbol{x})\approx0 \]
便可以近似保留原始信息。
一个基本残差块可以写为:
class ResidualBlock(nn.Module): def __init__(self, channels): super().__init__() self.layers = nn.Sequential( nn.Conv2d( channels, channels, kernel_size=3, padding=1 ), nn.ReLU(), nn.Conv2d( channels, channels, kernel_size=3, padding=1 ) ) self.activation = nn.ReLU() def forward(self, x): return self.activation( self.layers(x) + x )直接相加要求两个张量尺寸一致:
\[ \operatorname{shape} \left( \mathcal{F}(\boldsymbol{x}) \right) = \operatorname{shape} (\boldsymbol{x}) \]
若通道数或空间尺寸不同,需要使用 \(1\times1\) 卷积等方法调整尺寸。
十六、一维卷积与时序力学数据
卷积并不只用于二维云图。
对于蠕变、疲劳和动力响应,可以将数据写成:
\[ X\in\mathbb{R}^{N\times C\times T} \]
其中 \(T\) 是时间步数量。
一维卷积为:
\[ y_t = \sum_{k=0}^{K-1} w_kx_{t+k} +c \]
它可以提取:
- 短时间振动模式;
- 局部峰值;
- 载荷突变;
- 响应上升或下降趋势;
- 周期性疲劳特征。
【图5:一维卷积提取时序局部变化】
一维CNN擅长提取局部时序模式。若需要描述很长时间范围内的历史依赖,可以进一步使用下一阶段将学习的LSTM或Transformer。
十七、有限元数据进入CNN前的处理
真实有限元数据通常位于非结构化网格上,而普通二维CNN要求规则数组。因此常见处理流程为:
Abaqus ODB场输出 ↓ 节点或积分点数据提取 ↓ 统一坐标与单位 ↓ 插值到规则网格 ↓ 构造几何掩码与边界条件通道 ↓ 输入CNN插值过程中需要注意:
- 不要跨越孔洞或裂纹进行错误插值;
- 区分节点值和积分点值;
- 保留模型外部区域的掩码;
- 固定输入和输出坐标范围;
- 训练集与测试集使用相同的插值规则;
- 记录Pa、MPa、m和mm等单位;
- 网格插值误差与CNN预测误差应分别评估。
如果模型使用非结构化网格并且不希望插值,可以考虑图神经网络,而不是强行将所有有限元网格转换为图像。
十八、场预测的损失函数
普通像素均方误差可以写为:
\[ \mathcal{L}_{\mathrm{field}} = \frac{1}{N_{\mathrm{valid}}} \sum_{i,j} M_{i,j} \left( \hat{\sigma}_{i,j} - \sigma_{i,j} \right)^2 \]
其中 \(M_{i,j}\) 是有效区域掩码:
\[ M_{i,j} = \begin{cases} 1,&\text{材料区域}\\ 0,&\text{孔洞或模型外部} \end{cases} \]
如果不使用掩码,大量模型外部的零值可能使总体损失看起来很小,而材料区域的应力预测仍然很差。
应力集中区域还可以增加权重:
\[ \mathcal{L}_{\mathrm{weighted}} = \frac{ \sum_{i,j} M_{i,j}w_{i,j} \left( \hat{\sigma}_{i,j}-\sigma_{i,j} \right)^2 }{ \sum_{i,j}M_{i,j}w_{i,j} } \]
但权重设置必须提前确定,不能根据测试集结果反复修改。
十九、评价全场预测不能只看一个RMSE
至少应同时检查以下指标:
全场均方根误差
\[ \mathrm{RMSE} = \sqrt{ \frac{1}{N_{\mathrm{valid}}} \sum M_{i,j} \left( \hat{\sigma}_{i,j}-\sigma_{i,j} \right)^2 } \]
峰值相对误差
\[ e_{\mathrm{peak}} = \frac{ \left| \hat{\sigma}_{\max}-\sigma_{\max} \right| }{ \left|\sigma_{\max}\right|+\epsilon } \times100\% \]
高应力区域重合程度
例如比较预测场和真实场中:
\[ \sigma\geq0.9\sigma_{\max} \]
区域的位置是否一致。
工程中,全场平均误差很小,并不意味着裂纹尖端或孔边的峰值预测准确。
二十、常见错误
错误一:把云图截图直接作为训练数据
截图可能带有图例、文字、坐标轴和不同色标。CNN可能学习颜色设置,而不是实际场变量。
更合适的方法是从ODB中提取数值场,再统一映射到规则网格。
错误二:每张云图使用不同色标
若必须使用图像,应固定色标范围。否则相同颜色可能对应不同应力值。
错误三:把空白区域当成真实零应力
应为孔洞和模型外部建立独立掩码。
错误四:忽略通道的单位和顺序
模型训练时使用:
[几何掩码, 载荷X, 载荷Y, 约束X, 约束Y]预测时也必须保持完全相同的顺序和单位。
错误五:使用随机节点划分数据
同一有限元模型中的相邻节点高度相关。训练集和测试集应按照完整几何、完整工况或完整模型划分。
错误六:认为CNN自动满足力学规律
普通CNN不会自动满足平衡方程、边界条件和本构关系。这些约束需要通过数据设计、结构设计、物理损失或后处理检查引入。
二十一、课后练习
练习一:改变卷积核
将示例卷积核改为:
\[ K= \begin{bmatrix} -1&0&1\\ -2&0&2\\ -1&0&1 \end{bmatrix} \]
观察它对水平方向变化的响应。
练习二:改变步长和填充
分别计算:
conv2d_single( field, kernel, padding=0, stride=1 )conv2d_single( field, kernel, padding=1, stride=1 )conv2d_single( field, kernel, padding=1, stride=2 )记录三种输出尺寸。
练习三:比较池化方法
对同一应力场分别进行最大池化和平均池化,比较孔边高应力区域的保留程度。
练习四:设计有限元输入通道
针对“带中心孔板的应力场预测”,设计至少五个输入通道,并说明每个通道的单位和物理含义。
练习五:物理合理性检查
对于相同几何和材料,逐渐增加载荷。如果问题处于线弹性阶段,应检查CNN预测是否近似满足:
\[ \boldsymbol{\sigma}(\lambda F) \approx \lambda\boldsymbol{\sigma}(F) \]
二十二、本节小结
CNN相较MLP的关键变化是保留空间结构。
\[ \boxed{ \text{卷积层} = \text{局部连接} + \text{权重共享} } \]
卷积负责提取局部模式,激活函数提供非线性表达能力,池化压缩空间尺寸,残差连接帮助较深网络传递信息。
在有限元代理建模中,CNN特别适合规则网格上的应力场、位移场、温度场和损伤场。但模型能否可靠应用,还取决于数据通道设计、网格映射、区域掩码、损失函数和物理验证。
课程附件
本节完整程序只依赖NumPy和Matplotlib,已经实际运行。建议将其放在正文之后的“附录:完整可运行代码”中。
- [第十五节完整可运行代码]
- [完整运行结果]
- [教学场数据]
【零基础学智能仿真-15】卷积神经网络(CNN)-从有限元场中提取空间特征资源-CSDN下载