随机2D形状周围层流预测!基于飞桨实现图形神经网络

原创
2023/06/19 10:37
阅读数 105
AI总结

项目背景

近年来,快速流场预测领域一直由基于像素的卷积神经网络(Convolution Neural Network,CNN)主导。当 CFD 与基于 CNN 的神经网络模型耦合时,来自网格的数据必须在笛卡尔网格上进行插值,然后再投影回网格。然而均匀笛卡尔网格的内在几何表示较差,相关的计算成本很大,并不适合快速流场预测。与 CNN 不同,图卷积神经网络(Graph Convolution Neural Network,GCNN)可以直接应用于实体拟合的三角网格,从而与 CFD 求解器轻松耦合,解决上述问题。本项目选择复现基于 GCNN 结构的论文《Graph neural networks for laminar flow prediction around random two-dimensional shapes》,验证飞桨框架能够基于 GCNN 模型实现 2D 障碍物周围层流预测的能力。

论文原文

https://hal.archives-ouvertes.fr/hal-03432662/document

原文代码

https://github.com/cfl-minds/gnn_laminar_flow

开发环境与实现过程

开发环境

本文依托于飞桨框架2.4版本实现 2D 障碍物周围层流预测的图卷积神经网络。可以通过访问飞桨官网的安装文档完成安装。详情见链接:https://www.paddlepaddle.org.cn/install/quick?docurl=/documentation/docs/zh/install/conda/macos-conda.html

实现过程

图卷积神经网络的基本组成部分是卷积块。卷积块由一个两步图卷积层和一个两步平滑层组成。在论文中,节点和边上的特征矩阵分别用表示,其中,Nv 和 NE 是节点和边的数量,dV 和 dE 是节点和边上特征向量的维度。卷积层将节点级消息传播到边缘,然后聚合新的边缘特征并根据以下规则更新节点特征。 
其中,v1 和 v2 是由边 e 连接的两个节点,N(v)是围绕节点 v 的相邻边的集合。卷积核 fe 和 fv 为单隐藏层的全连接神经网络,隐藏层中的神经元数量设置为128。
节点特征更新代码如下:

def update_node_features(node_features, grad_P1, num_filters, initializer, message_fn):
    message_input = paddle.concat([node_features, grad_P1], axis=1)
    updated = message_fn(message_input)
    return updated

边特征更新代码如下:

def update_symmetry_edge_features(node_features, edges, edge_features, edge_feat_dim, initializer, message_fn):
    n_nodes = paddle.to_tensor(node_features.shape[0])
    n_features = paddle.to_tensor(node_features.shape[1])
    reshaped = paddle.reshape(node_features[edges], shape=[-1, 2 * n_features])
    symmetric = 0.5 * paddle.add(reshaped[:, 0:n_features], reshaped[:, n_features:2 * n_features])
    asymmetric = 0.5 * paddle.abs(paddle.subtract(reshaped[:, 0:n_features],
    reshaped[:, n_features:2 * n_features]))
    inputs = paddle.concat([symmetric, asymmetric, edge_features], axis=1)
    messages = message_fn(inputs)  # n_edges, n_output
    n_edges = edges.shape[0]
    updates = paddle.slice(
        paddle.add(paddle.index_add_(paddle.zeros([n_edges, messages.shape[1]]), edges[:, 0], 0, messages),
        paddle.index_add_(paddle.zeros([n_edges, messages.shape[1]]), edges[:, 1], 0, messages)),
        axes=[0, 1], starts=[0, 0], ends=[n_nodes, messages.shape[1]])
    return messages, updates

在边缘卷积步骤中,对称节点特征
优先于 x1 和 x2,以保持排列不变性。节点卷积步骤中的求和对于相邻边的排列也是不变的。平滑图层对输出图形执行平均操作。在三角网格上实现的平均内核分为两个步骤:

添加此层的动机不是为了消息传播,而是为了减少节点要素的空间变异性。它通过相邻节点要素的补偿来压低卷积层的特征图。平滑层代码如下:

class EdgeSmoothing(nn.Layer):
    def __init__(self):
        super(EdgeSmoothing, self).__init__()
    def forward(self, to_concat, node_features, edges, count):
        n_nodes = paddle.to_tensor(node_features.shape[0])
        flow_on_edge = paddle.mean(node_features[edges], axis=1)
        aggre_flow = paddle.add(paddle.index_add_(paddle.zeros([edges.shape[0], flow_on_edge.shape[1]]), edges[:, 0], 0,flow_on_edge[:, :]),
        paddle.index_add_(paddle.zeros([edges.shape[0], flow_on_edge.shape[1]]), edges[:, 1], 0,flow_on_edge[:, :]))
        return paddle.concat([to_concat, paddle.divide(aggre_flow[:n_nodes, :], count)], axis=1)

下图为论文中采用的网络架构图,该架构图由图卷积层和平滑层组成。输入由三个图像组成,8个卷积块/平滑层堆叠形成图卷积神经网络,然后以1×1卷积作为输出层。架构中一个重要组成部分是从输入图到卷积块的跳过连接。在每个平滑图层之后,结点的坐标将连接到结点要素。这些跳跃连接为公式中的边缘卷积步骤提供空间信息。

图1 网络架构图

网络架构代码如下:

class InvariantEdgeModel(nn.Layer):
    def __init__(self, edge_feature_dims, num_filters, initializer):
        super(InvariantEdgeModel, self).__init__()
        self.edge_feat_dims = edge_feature_dims
        self.num_filters = num_filters
        self.initializer = initializer
        self.layer0 = InvariantEdgeConv(self.edge_feat_dims[0], self.num_filters[0], self.initializer)
        self.layer1 = InvariantEdgeConv(self.edge_feat_dims[1], self.num_filters[1], self.initializer)
        self.layer2 = InvariantEdgeConv(self.edge_feat_dims[2], self.num_filters[2], self.initializer)
        self.layer3 = InvariantEdgeConv(self.edge_feat_dims[3], self.num_filters[3], self.initializer)
        self.layer4 = InvariantEdgeConv(self.edge_feat_dims[4], self.num_filters[4], self.initializer)
        self.layer5 = InvariantEdgeConv(self.edge_feat_dims[5], self.num_filters[5], self.initializer)
        self.layer6 = InvariantEdgeConv(self.edge_feat_dims[6], self.num_filters[6], self.initializer)
        self.layer7 = InvariantEdgeConv(self.edge_feat_dims[7], self.num_filters[7], self.initializer)
        self.layer8 = nn.Linear(10, 3)
        self.smoothLayer = EdgeSmoothing()
    def forward(self, node_input, edges, edge_input, smoothing_weights):
        new_node_features_0, new_edge_features_0 = self.layer0(node_input, edge_input, edges)
        smoothed_0 = self.smoothLayer(node_input[:, 0:2], new_node_features_0, edges, smoothing_weights)
        new_node_features_1, new_edge_features_1 = self.layer1(smoothed_0, new_edge_features_0, edges)
        smoothed_1 = self.smoothLayer(node_input[:, 0:2], new_node_features_1, edges, smoothing_weights)
        new_node_features_2, new_edge_features_2 = self.layer2(smoothed_1, new_edge_features_1, edges)
        smoothed_2 = self.smoothLayer(node_input[:, 0:2], new_node_features_2, edges, smoothing_weights)
        new_node_features_3, new_edge_features_3 = self.layer3(smoothed_2, new_edge_features_2, edges)
        smoothed_3 = self.smoothLayer(node_input[:, 0:2], new_node_features_3, edges, smoothing_weights)
        new_node_features_4, new_edge_features_4 = self.layer4(smoothed_3, new_edge_features_3, edges)
        smoothed_4 = self.smoothLayer(node_input[:, 0:2], new_node_features_4, edges, smoothing_weights)
        new_node_features_5, new_edge_features_5 = self.layer5(smoothed_4, new_edge_features_4, edges)
        smoothed_5 = self.smoothLayer(node_input[:, 0:2], new_node_features_5, edges, smoothing_weights)
        new_node_features_6, new_edge_features_6 = self.layer6(smoothed_5, new_edge_features_5, edges)
        smoothed_6 = self.smoothLayer(node_input[:, 0:2], new_node_features_6, edges, smoothing_weights)
        new_node_features_7, new_edge_features_7 = self.layer7(smoothed_6, new_edge_features_6, edges)
        smoothed_7 = self.smoothLayer(node_input[:, 0:2], new_node_features_7, edges, smoothing_weights)
        node_outputs = self.layer8(smoothed_7[:, 0:])
        return node_outputs

项目结果


为了展示复现的效果,我们使用复现模型对圆柱流场进行预测,结果如下:

图2 预测效果对比图

其中左侧为论文原文中采用的真实流场,右侧为我们复现的模型所预测的流场。可见我们得到的预测值(右边)与真实值(左边)基本一致,模型精度很好。我们复现的模型在实验结果中的 MAE 为0.0046,与原论文的结果0.0043也非常接近,验证了飞桨框架能够基于该模型实现 2D 障碍物周围层流预测的能力。

心得体会

百度飞桨的论文复现比赛为我们团队提供了宝贵的学习和成长机会。这个比赛不仅让我们深入了解流场预测这个细分领域,还锻炼了我们团队合作和解决问题的能力。现在回顾这次比赛,值得称赞的地方有很多。第一,飞桨官方强大的赛事组织能力,将比赛组织的规范和有序。从项目前期宣传、队伍报名、赛前讲解、赛中答疑以及结果提交一环扣一环,项目安排有序,每支队伍都清楚每个阶段该干什么。第二,比赛中,飞桨科学计算团队的技术人员提供细致答疑。比赛要求我们仔细阅读论文,并根据论文提供的参考代码使用飞桨进行复现。这个过程不仅需要我们对深度学习模型有深入的理解,也需要我们熟悉飞桨框架。作为一个新手,难免遇到各种各样的技术问题,每次找飞桨技术人员,总能得到耐心细致的解答。除此之外,官方还会定期跟踪复现的进展情况,有问题立即为选手解决问题。第三,参加飞桨的论文复现比赛也为我们打开了更广阔的视野。通过此次比赛,我们有机会接触到 AI for Science 这个领域很多优秀论文。在复现实践的过程中,我们深入研究了这些论文的方法和技术,加深了我们对这个领域的理解,了解到了学术界的最新进展和应用。最后,我要衷心感谢百度飞桨团队所有组织者和工作人员。他们的辛勤付出和专业支持使得这次比赛得以顺利进行。也要特别感谢陆林、汪璐、孔德天这些一起参加比赛的师兄弟,感谢我们团队中每一位成员的努力和奉献。未来,我们将继续保持学习的态度,不断探索和创新,争取为推动该领域的发展做出贡献。

往期推荐

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
0 收藏
0
分享
AI总结
返回顶部
顶部