隨機2D形狀周圍層流預測!基於飛槳實現圖形神經網絡

項目背景

近年來,快速流場預測領域一直由基於像素的卷積神經網絡(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 這個領域很多優秀論文。在復現實踐的過程中,我們深入研究了這些論文的方法和技術,加深了我們對這個領域的理解,瞭解到了學術界的最新進展和應用。最後,我要衷心感謝百度飛槳團隊所有組織者和工作人員。他們的辛勤付出和專業支持使得這次比賽得以順利進行。也要特別感謝陸林、汪璐、孔德天這些一起參加比賽的師兄弟,感謝我們團隊中每一位成員的努力和奉獻。未來,我們將繼續保持學習的態度,不斷探索和創新,爭取爲推動該領域的發展做出貢獻。

往期推薦

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章