【零基础学智能仿真-15】卷积神经网络(CNN)——从有限元场中提取空间特征
2026/9/12 20:47:20 网站建设 项目流程

课程摘要

本节承接神经网络基础,学习卷积神经网络处理有限元场数据的基本方法。课程从局部感受野和权重共享出发,讲解卷积核、通道、步长、填充、池化及残差连接,并通过带圆孔板教学应力场演示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个样本,每个样本输出一个标量。

Conv2dMaxPool2d的参数定义可参考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下载

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询