From 88d068b05d4a330cbcfd4af22740bc9fb4f3b2c8 Mon Sep 17 00:00:00 2001 From: co63oc Date: Mon, 13 Nov 2023 16:30:05 +0800 Subject: [PATCH] Add epnn model (#606) --- docs/zh/api/loss.md | 1 + docs/zh/examples/epnn.md | 210 ++++++++++ docs/zh/examples/epnn_images/loss_trend.png | Bin 0 -> 79028 bytes examples/epnn/conf/epnn.yaml | 56 +++ examples/epnn/epnn.py | 205 ++++++++++ examples/epnn/functions.py | 424 ++++++++++++++++++++ mkdocs.yml | 1 + ppsci/arch/__init__.py | 2 + ppsci/arch/epnn.py | 122 ++++++ ppsci/loss/__init__.py | 2 + ppsci/loss/mae.py | 82 ++++ ppsci/optimizer/lr_scheduler.py | 41 ++ ppsci/utils/reader.py | 51 ++- 13 files changed, 1194 insertions(+), 3 deletions(-) create mode 100644 docs/zh/examples/epnn.md create mode 100644 docs/zh/examples/epnn_images/loss_trend.png create mode 100644 examples/epnn/conf/epnn.yaml create mode 100755 examples/epnn/epnn.py create mode 100644 examples/epnn/functions.py create mode 100644 ppsci/arch/epnn.py create mode 100644 ppsci/loss/mae.py diff --git a/docs/zh/api/loss.md b/docs/zh/api/loss.md index 1a593f921..caa04f6df 100644 --- a/docs/zh/api/loss.md +++ b/docs/zh/api/loss.md @@ -9,6 +9,7 @@ - L1Loss - L2Loss - L2RelLoss + - MAELoss - MSELoss - MSELossWithL2Decay - IntegralLoss diff --git a/docs/zh/examples/epnn.md b/docs/zh/examples/epnn.md new file mode 100644 index 000000000..74c851988 --- /dev/null +++ b/docs/zh/examples/epnn.md @@ -0,0 +1,210 @@ +# EPNN + +=== "模型训练命令" + + ``` sh + # linux + wget https://paddle-org.bj.bcebos.com/paddlescience/datasets/dstate-16-plas.dat -O datasets/dstate-16-plas.dat + wget https://paddle-org.bj.bcebos.com/paddlescience/datasets/dstress-16-plas.dat -O datasets/dstress-16-plas.dat + # windows + # curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/dstate-16-plas.dat --output datasets/dstate-16-plas.dat + # curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/dstress-16-plas.dat --output datasets/dstress-16-plas.dat + python epnn.py + ``` + +=== "模型评估命令" + + ``` sh + # linux + wget https://paddle-org.bj.bcebos.com/paddlescience/datasets/dstate-16-plas.dat -O datasets/dstate-16-plas.dat + wget https://paddle-org.bj.bcebos.com/paddlescience/datasets/dstress-16-plas.dat -O datasets/dstress-16-plas.dat + # windows + # curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/dstate-16-plas.dat --output datasets/dstate-16-plas.dat + # curl https://paddle-org.bj.bcebos.com/paddlescience/datasets/dstress-16-plas.dat --output datasets/dstress-16-plas.dat + python epnn.py mode=eval EVAL.pretrained_model_path=https://paddle-org.bj.bcebos.com/paddlescience/models/epnn/epnn_pretrained.pdparams + ``` + +## 1. 背景简介 + +这里主要为复现 Elasto-Plastic Neural Network (EPNN) 的 Physics-Informed Neural Network (PINN) 代理模型。将这些物理嵌入神经网络的架构中,可以更有效地训练网络,同时使用更少的数据进行训练,同时增强对训练数据外加载制度的推断能力。EPNN 的架构是模型和材料无关的,即它可以适应各种弹塑性材料类型,包括地质材料和金属;并且实验数据可以直接用于训练网络。为了证明所提出架构的稳健性,我们将其一般框架应用于砂土的弹塑性行为。EPNN 在预测不同初始密度砂土的未观测应变控制加载路径方面优于常规神经网络架构。 + +## 2. 问题定义 + +在神经网络中,信息通过连接的神经元流动。神经网络中每个链接的“强度”是由一个可变的权重决定的: + +$$ +z_l^{\mathrm{i}}=W_{k l}^{\mathrm{i}-1, \mathrm{i}} a_k^{\mathrm{i}-1}+b^{\mathrm{i}-1}, \quad k=1: N^{\mathrm{i}-1} \quad \text { or } \quad \mathbf{z}^{\mathrm{i}}=\mathbf{a}^{\mathrm{i}-1} \mathbf{W}^{\mathrm{i}-1, \mathrm{i}}+b^{\mathrm{i}-1} \mathbf{I} +$$ + +其中 $b$ 是偏置项;$N$ 为不同层中神经元数量;$I$ 指的是所有元素都为 1 的单位向量。 + +## 3. 问题求解 + +接下来开始讲解如何将问题一步一步地转化为 PaddleScience 代码,用深度学习的方法求解该问题。 +为了快速理解 PaddleScience,接下来仅对模型构建、方程构建、计算域构建等关键步骤进行阐述,而其余细节请参考 [API文档](../api/arch.md)。 + +### 3.1 模型构建 + +在 EPNN 问题中,建立网络,用 PaddleScience 代码表示如下 + +``` py linenums="370" +--8<-- +examples/epnn/functions.py:370:390 +--8<-- +``` + +Epnn 参数 input_keys 是输入字段名,output_keys 是输出字段名,node_sizes 是节点大小列表,activations 是激活函数字符串列表,drop_p 是节点丢弃概率。 + +### 3.2 数据生成 + +本案例涉及读取数据生成,如下所示 + +``` py linenums="36" +--8<-- +examples/epnn/epnn.py:36:41 +--8<-- +``` + +``` py linenums="305" +--8<-- +examples/epnn/functions.py:305:320 +--8<-- +``` +这里使用 Data 读取文件构造数据类,然后使用 get_shuffled_data 混淆数据,然后计算需要获取的混淆数据数量 itrain,最后使用 get 获取每组 itrain 数量的 10 组数据。 + +### 3.3 约束构建 + +设置训练数据集和损失计算函数,返回字段,代码如下所示: + +``` py linenums="63" +--8<-- +examples/epnn/epnn.py:63:86 +--8<-- +``` + +`SupervisedConstraint` 的第一个参数是监督约束的读取配置,配置中 `“dataset”` 字段表示使用的训练数据集信息,其各个字段分别表示: + +1. `name`: 数据集类型,此处 `"NamedArrayDataset"` 表示顺序读取的数据集; +2. `input`: 输入数据集; +3. `label`: 标签数据集; + +第二个参数是损失函数,此处使用自定义函数 `train_loss_func`。 + +第三个参数是方程表达式,用于描述如何计算约束目标,计算后的值将会按照指定名称存入输出列表中,从而保证 loss 计算时可以使用这些值。 + +第四个参数是约束条件的名字,我们需要给每一个约束条件命名,方便后续对其索引。 + +在约束构建完毕之后,以我们刚才的命名为关键字,封装到一个字典中,方便后续访问。 + +### 3.4 评估器构建 + +与约束同理,本问题使用 `ppsci.validate.SupervisedValidator` 构建评估器,参数含义也与[约束构建](#33)类似,唯一的区别是评价指标 `metric`。代码如下所示: + +``` py linenums="88" +--8<-- +examples/epnn/epnn.py:88:103 +--8<-- +``` + +### 3.5 超参数设定 + +接下来我们需要指定训练轮数,此处我们按实验经验,使用 10000 轮训练轮数。iters_per_epoch 为 1。 + +``` yaml linenums="40" +--8<-- +examples/epnn/conf/epnn.yaml:40:41 +--8<-- +``` + +### 3.6 优化器构建 + +训练过程会调用优化器来更新模型参数,此处选择较为常用的 `Adam` 优化器,并配合使用机器学习中常用的 ExponentialDecay 学习率调整策略。 + +由于使用多个模型,需要设置多个优化器,对 Epnn 网络部分,需要设置 `Adam` 优化器。 + +``` py linenums="395" +--8<-- +examples/epnn/functions.py:395:403 +--8<-- +``` + +然后对增加的 gkratio 参数,需要再设置优化器。 + +``` py linenums="405" +--8<-- +examples/epnn/functions.py:405:412 +--8<-- +``` + +优化器按顺序优化,代码汇总为: + +``` py linenums="395" +--8<-- +examples/epnn/functions.py:395:413 +--8<-- +``` + +### 3.7 自定义 loss + +由于本问题包含无监督学习,数据中不存在标签数据,loss 根据模型返回数据计算得到,因此需要自定义 loss。方法为先定义相关函数,再将函数名作为参数传给 `FunctionalLoss` 和 `FunctionalMetric`。 + +需要注意自定义 loss 函数的输入输出参数需要与 PaddleScience 中如 `MSE` 等其他函数保持一致,即输入为模型输出 `output_dict` 等字典变量,loss 函数输出为 loss 值 `paddle.Tensor`。 + +相关的自定义 loss 函数使用 `MAELoss` 计算,代码为 + +``` py linenums="113" +--8<-- +examples/epnn/functions.py:113:125 +--8<-- +``` + +### 3.8 模型训练与评估 + +完成上述设置之后,只需要将上述实例化的对象按顺序传递给 `ppsci.solver.Solver`。 + +``` py linenums="106" +--8<-- +examples/epnn/epnn.py:106:118 +--8<-- +``` + +模型训练时设置 eval_during_train 为 True,将在每次训练后评估。 + +``` yaml linenums="43" +--8<-- +examples/epnn/conf/epnn.yaml:43:43 +--8<-- +``` + +最后启动训练即可: + +``` py linenums="121" +--8<-- +examples/epnn/epnn.py:121:121 +--8<-- +``` + +## 4. 完整代码 + +``` py linenums="1" title="epnn.py" +--8<-- +examples/epnn/epnn.py +--8<-- +``` + +## 5. 结果展示 + +EPNN 案例针对 epoch=10000 的参数配置进行了实验,结果返回 Loss 为 0.00471。 + +下图分别为不同 epoch 的 Loss, Training error, Cross validation error 图形: + +
+ ![loss_trend](epnn_images/loss_trend.png){ loading=lazy } +
训练 loss 图形
+
+ +## 6. 参考资料 + +- [A physics-informed deep neural network for surrogate +modeling in classical elasto-plasticity](https://arxiv.org/abs/2204.12088) +- diff --git a/docs/zh/examples/epnn_images/loss_trend.png b/docs/zh/examples/epnn_images/loss_trend.png new file mode 100644 index 0000000000000000000000000000000000000000..a6ad4fbbee0e8e067156b662601d046b1a8bb6b7 GIT binary patch literal 79028 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�R0W>-6r@EpvIncHJ6Ghe! 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 literal 0 HcmV?d00001 diff --git a/examples/epnn/conf/epnn.yaml b/examples/epnn/conf/epnn.yaml new file mode 100644 index 000000000..4d22bffda --- /dev/null +++ b/examples/epnn/conf/epnn.yaml @@ -0,0 +1,56 @@ +hydra: + run: + # dynamic output directory according to running time and override name + dir: outputs_epnn/${now:%Y-%m-%d}/${now:%H-%M-%S}/${hydra.job.override_dirname} + job: + name: ${mode} # name of logfile + chdir: false # keep current working direcotry unchaned + config: + override_dirname: + exclude_keys: + - TRAIN.checkpoint_path + - TRAIN.pretrained_model_path + - EVAL.pretrained_model_path + - mode + - output_dir + - log_freq + sweep: + # output directory for multirun + dir: ${hydra.run.dir} + subdir: ./ + +# general settings +mode: train # running mode: train/eval +seed: 42 +output_dir: ${hydra:run.dir} +log_freq: 20 + +# set working condition +DATASET_STATE: datasets/dstate-16-plas.dat +DATASET_STRESS: datasets/dstress-16-plas.dat +NTRAIN_SIZE: 40 + +# model settings +MODEL: + ihlayers: 3 + ineurons: 60 + +# training settings +TRAIN: + epochs: 10000 + iters_per_epoch: 1 + save_freq: 50 + eval_during_train: true + eval_with_no_grad: true + lr_scheduler: + epochs: ${TRAIN.epochs} + iters_per_epoch: ${TRAIN.iters_per_epoch} + gamma: 0.97 + decay_steps: 1 + pretrained_model_path: null + checkpoint_path: null + +# evaluation settings +EVAL: + pretrained_model_path: null + eval_with_no_grad: true diff --git a/examples/epnn/epnn.py b/examples/epnn/epnn.py new file mode 100755 index 000000000..8b28e89ce --- /dev/null +++ b/examples/epnn/epnn.py @@ -0,0 +1,205 @@ +# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Reference: https://github.com/meghbali/ANNElastoplasticity +""" + +from os import path as osp + +import functions +import hydra +from omegaconf import DictConfig + +import ppsci +from ppsci.utils import logger + + +def train(cfg: DictConfig): + # set random seed for reproducibility + ppsci.utils.misc.set_random_seed(cfg.seed) + + # initialize logger + logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info") + + ( + input_dict_train, + label_dict_train, + input_dict_val, + label_dict_val, + ) = functions.get_data(cfg.DATASET_STATE, cfg.DATASET_STRESS, cfg.NTRAIN_SIZE) + model_list = functions.get_model_list( + cfg.MODEL.ihlayers, + cfg.MODEL.ineurons, + input_dict_train["state_x"][0].shape[1], + input_dict_train["state_y"][0].shape[1], + input_dict_train["stress_x"][0].shape[1], + ) + optimizer_list = functions.get_optimizer_list(model_list, cfg) + model_state_elasto, model_state_plastic, model_stress = model_list + model_list_obj = ppsci.arch.ModelList(model_list) + + def _transform_in_stress(_in): + return functions.transform_in_stress( + _in, model_state_elasto, "out_state_elasto" + ) + + model_state_elasto.register_input_transform(functions.transform_in) + model_state_plastic.register_input_transform(functions.transform_in) + model_stress.register_input_transform(_transform_in_stress) + model_stress.register_output_transform(functions.transform_out) + + output_keys = [ + "state_x", + "state_y", + "stress_x", + "stress_y", + "out_state_elasto", + "out_state_plastic", + "out_stress", + ] + sup_constraint_pde = ppsci.constraint.SupervisedConstraint( + { + "dataset": { + "name": "NamedArrayDataset", + "input": input_dict_train, + "label": label_dict_train, + }, + "batch_size": 1, + "num_workers": 0, + }, + ppsci.loss.FunctionalLoss(functions.train_loss_func), + {key: (lambda out, k=key: out[k]) for key in output_keys}, + name="sup_train", + ) + constraint_pde = {sup_constraint_pde.name: sup_constraint_pde} + + sup_validator_pde = ppsci.validate.SupervisedValidator( + { + "dataset": { + "name": "NamedArrayDataset", + "input": input_dict_val, + "label": label_dict_val, + }, + "batch_size": 1, + "num_workers": 0, + }, + ppsci.loss.FunctionalLoss(functions.eval_loss_func), + {key: (lambda out, k=key: out[k]) for key in output_keys}, + metric={"metric": ppsci.metric.FunctionalMetric(functions.metric_expr)}, + name="sup_valid", + ) + validator_pde = {sup_validator_pde.name: sup_validator_pde} + + # initialize solver + solver = ppsci.solver.Solver( + model_list_obj, + constraint_pde, + cfg.output_dir, + optimizer_list, + None, + cfg.TRAIN.epochs, + cfg.TRAIN.iters_per_epoch, + save_freq=cfg.TRAIN.save_freq, + eval_during_train=cfg.TRAIN.eval_during_train, + validator=validator_pde, + eval_with_no_grad=cfg.TRAIN.eval_with_no_grad, + ) + + # train model + solver.train() + functions.plotting(cfg.output_dir) + + +def evaluate(cfg: DictConfig): + # set random seed for reproducibility + ppsci.utils.misc.set_random_seed(cfg.seed) + # initialize logger + logger.init_logger("ppsci", osp.join(cfg.output_dir, f"{cfg.mode}.log"), "info") + + ( + input_dict_train, + _, + input_dict_val, + label_dict_val, + ) = functions.get_data(cfg.DATASET_STATE, cfg.DATASET_STRESS, cfg.NTRAIN_SIZE) + model_list = functions.get_model_list( + cfg.MODEL.ihlayers, + cfg.MODEL.ineurons, + input_dict_train["state_x"][0].shape[1], + input_dict_train["state_y"][0].shape[1], + input_dict_train["stress_x"][0].shape[1], + ) + model_state_elasto, model_state_plastic, model_stress = model_list + model_list_obj = ppsci.arch.ModelList(model_list) + + def transform_f_stress(_in): + return functions.transform_f(_in, model_state_elasto, "out_state_elasto") + + model_state_elasto.register_input_transform(functions.transform_in) + model_state_plastic.register_input_transform(functions.transform_in) + model_stress.register_input_transform(transform_f_stress) + model_stress.register_output_transform(functions.transform_out) + + output_keys = [ + "state_x", + "state_y", + "stress_x", + "stress_y", + "out_state_elasto", + "out_state_plastic", + "out_stress", + ] + sup_validator_pde = ppsci.validate.SupervisedValidator( + { + "dataset": { + "name": "NamedArrayDataset", + "input": input_dict_val, + "label": label_dict_val, + }, + "batch_size": 1, + "num_workers": 0, + }, + ppsci.loss.FunctionalLoss(functions.eval_loss_func), + {key: (lambda out, k=key: out[k]) for key in output_keys}, + metric={"metric": ppsci.metric.FunctionalMetric(functions.metric_expr)}, + name="sup_valid", + ) + validator_pde = {sup_validator_pde.name: sup_validator_pde} + functions.OUTPUT_DIR = cfg.output_dir + + # initialize solver + solver = ppsci.solver.Solver( + model_list_obj, + output_dir=cfg.output_dir, + validator=validator_pde, + pretrained_model_path=cfg.EVAL.pretrained_model_path, + eval_with_no_grad=cfg.EVAL.eval_with_no_grad, + ) + # evaluate after finished training + solver.eval() + + +@hydra.main(version_base=None, config_path="./conf", config_name="epnn.yaml") +def main(cfg: DictConfig): + if cfg.mode == "train": + train(cfg) + elif cfg.mode == "eval": + evaluate(cfg) + else: + raise ValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'") + + +if __name__ == "__main__": + main() diff --git a/examples/epnn/functions.py b/examples/epnn/functions.py new file mode 100644 index 000000000..04fd8865b --- /dev/null +++ b/examples/epnn/functions.py @@ -0,0 +1,424 @@ +# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Elasto-Plastic Neural Network (EPNN) + +DEVELOPED AT: + COMPUTATIONAL GEOMECHANICS LABORATORY + DEPARTMENT OF CIVIL ENGINEERING + UNIVERSITY OF CALGARY, AB, CANADA + DIRECTOR: Prof. Richard Wan + +DEVELOPED BY: + MAHDAD EGHBALIAN + +MIT License + +Copyright (c) 2022 Mahdad Eghbalian + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import math +from typing import Dict + +import numpy as np +import paddle + +import ppsci +from ppsci.utils import logger + +# log for loss(total, state_elasto, state_plastic, stress), eval error(total, state_elasto, state_plastic, stress) +loss_log = {} # for plotting +eval_log = {} +plot_keys = {"total", "state_elasto", "state_plastic", "stress"} +for key in plot_keys: + loss_log[key] = [] + eval_log[key] = [] + +# transform +def transform_in(input): + input_transformed = {} + for key in input: + input_transformed[key] = paddle.squeeze(input[key], axis=0) + return input_transformed + + +def transform_out(input, out): + # Add transformed input for computing loss + out.update(input) + return out + + +def transform_in_stress(input, model, out_key): + input_elasto = model(input)[out_key] + input_elasto = input_elasto.detach().clone() + input_transformed = {} + for key in input: + input_transformed[key] = paddle.squeeze(input[key], axis=0) + input_state_m = paddle.concat( + x=( + input_elasto, + paddle.index_select( + input_transformed["state_x"], + paddle.to_tensor([0, 1, 2, 3, 7, 8, 9, 10, 11, 12]), + axis=1, + ), + ), + axis=1, + ) + input_transformed["state_x_f"] = input_state_m + return input_transformed + + +common_param = [] +gkratio = paddle.to_tensor( + data=[[0.45]], dtype=paddle.get_default_dtype(), stop_gradient=False +) + + +def val_loss_criterion(x, y): + return 100.0 * ( + paddle.linalg.norm(x=x["input"] - y["input"]) / paddle.linalg.norm(x=y["input"]) + ) + + +def train_loss_func(output_dict, *args) -> paddle.Tensor: + """For model calculation of loss in model.train(). + + Args: + output_dict (Dict[str, paddle.Tensor]): The output dict. + + Returns: + paddle.Tensor: Loss value. + """ + # Use ppsci.loss.MAELoss to replace paddle.nn.L1Loss + loss, loss_elasto, loss_plastic, loss_stress = loss_func( + output_dict, ppsci.loss.MAELoss() + ) + loss_log["total"].append(float(loss)) + loss_log["state_elasto"].append(float(loss_elasto)) + loss_log["state_plastic"].append(float(loss_plastic)) + loss_log["stress"].append(float(loss_stress)) + return loss + + +def eval_loss_func(output_dict, *args) -> paddle.Tensor: + """For model calculation of loss in model.eval(). + + Args: + output_dict (Dict[str, paddle.Tensor]): The output dict. + + Returns: + paddle.Tensor: Loss value. + """ + error, error_elasto, error_plastic, error_stress = loss_func( + output_dict, val_loss_criterion + ) + eval_log["total"].append(float(error)) + eval_log["state_elasto"].append(float(error_elasto)) + eval_log["state_plastic"].append(float(error_plastic)) + eval_log["stress"].append(float(error_stress)) + logger.message( + f"Error: {float(error)},{float(error_elasto)},{float(error_plastic)},{float(error_stress)}" + ) + return error + + +def metric_expr(output_dict, *args) -> Dict[str, paddle.Tensor]: + return {"dummy_loss": paddle.to_tensor(0.0)} + + +def loss_func(output_dict, criterion) -> paddle.Tensor: + ( + min_elasto, + min_plastic, + range_elasto, + range_plastic, + min_stress, + range_stress, + ) = common_param + + coeff1 = 2.0 + coeff2 = 1.0 + input_elasto = output_dict["out_state_elasto"] + input_plastic = output_dict["out_state_plastic"] + input_stress = output_dict["out_stress"] + target_elasto = output_dict["state_y"][:, 0:1] + target_plastic = output_dict["state_y"][:, 1:4] + loss_elasto = criterion({"input": input_elasto}, {"input": target_elasto}) + loss_plastic = criterion({"input": input_plastic}, {"input": target_plastic}) + oneten_state = paddle.ones(shape=[3, 1], dtype=paddle.get_default_dtype()) + oneten_stress = paddle.ones( + shape=[output_dict["stress_y"].shape[0], output_dict["stress_y"].shape[1]], + dtype=paddle.get_default_dtype(), + ) + dstrain = output_dict["state_x"][:, 10:] + dstrain_real = ( + paddle.multiply(x=dstrain + coeff2, y=paddle.to_tensor(range_stress)) / coeff1 + + min_stress + ) + # predict label + dstrainpl = target_plastic + dstrainpl_real = ( + paddle.multiply(x=dstrainpl + coeff2, y=paddle.to_tensor(range_elasto[1:4])) + / coeff1 + + min_elasto[1:4] + ) + # evaluate label + dstrainel = dstrain_real - dstrainpl_real + mu = paddle.multiply(x=gkratio, y=paddle.to_tensor(input_stress[:, 0:1])) + mu_dstrainel = 2.0 * paddle.multiply(x=mu, y=paddle.to_tensor(dstrainel)) + stress_dstrainel = paddle.multiply( + x=input_stress[:, 0:1] - 2.0 / 3.0 * mu, + y=paddle.to_tensor( + paddle.multiply( + x=paddle.matmul(x=dstrainel, y=oneten_state), + y=paddle.to_tensor(oneten_stress), + ) + ), + ) + input_stress = ( + coeff1 + * paddle.divide( + x=mu_dstrainel + stress_dstrainel - min_plastic, + y=paddle.to_tensor(range_plastic), + ) + - coeff2 + ) + target_stress = output_dict["stress_y"] + loss_stress = criterion({"input": input_stress}, {"input": target_stress}) + loss = loss_elasto + loss_plastic + loss_stress + return loss, loss_elasto, loss_plastic, loss_stress + + +class Dataset: + def __init__(self, data_state, data_stress, itrain): + self.data_state = data_state + self.data_stress = data_stress + self.itrain = itrain + + def get(self, epochs=1): + # Slow if using BatchSampler to obtain data + input_dict_train = { + "state_x": [], + "state_y": [], + "stress_x": [], + "stress_y": [], + } + input_dict_val = { + "state_x": [], + "state_y": [], + "stress_x": [], + "stress_y": [], + } + label_dict_train = {"dummy_loss": []} + label_dict_val = {"dummy_loss": []} + for i in range(epochs): + shuffled_indices = paddle.randperm(n=self.data_state.x_train.shape[0]) + input_dict_train["state_x"].append( + self.data_state.x_train[shuffled_indices[0 : self.itrain]] + ) + input_dict_train["state_y"].append( + self.data_state.y_train[shuffled_indices[0 : self.itrain]] + ) + input_dict_train["stress_x"].append( + self.data_stress.x_train[shuffled_indices[0 : self.itrain]] + ) + input_dict_train["stress_y"].append( + self.data_stress.y_train[shuffled_indices[0 : self.itrain]] + ) + label_dict_train["dummy_loss"].append(paddle.to_tensor(0.0)) + + shuffled_indices = paddle.randperm(n=self.data_state.x_valid.shape[0]) + input_dict_val["state_x"].append( + self.data_state.x_valid[shuffled_indices[0 : self.itrain]] + ) + input_dict_val["state_y"].append( + self.data_state.y_valid[shuffled_indices[0 : self.itrain]] + ) + input_dict_val["stress_x"].append( + self.data_stress.x_valid[shuffled_indices[0 : self.itrain]] + ) + input_dict_val["stress_y"].append( + self.data_stress.y_valid[shuffled_indices[0 : self.itrain]] + ) + label_dict_val["dummy_loss"].append(paddle.to_tensor(0.0)) + return input_dict_train, label_dict_train, input_dict_val, label_dict_val + + +class Data: + def __init__(self, dataset_path, train_p=0.6, cross_valid_p=0.2, test_p=0.2): + data = ppsci.utils.reader.load_dat_file(dataset_path) + self.x = data["X"] + self.y = data["y"] + self.train_p = train_p + self.cross_valid_p = cross_valid_p + self.test_p = test_p + + def get_shuffled_data(self): + # Need to set the seed, otherwise the loss will not match the precision + ppsci.utils.misc.set_random_seed(seed=10) + shuffled_indices = paddle.randperm(n=self.x.shape[0]) + n_train = math.floor(self.train_p * self.x.shape[0]) + n_cross_valid = math.floor(self.cross_valid_p * self.x.shape[0]) + n_test = math.floor(self.test_p * self.x.shape[0]) + self.x_train = self.x[shuffled_indices[0:n_train]] + self.y_train = self.y[shuffled_indices[0:n_train]] + self.x_valid = self.x[shuffled_indices[n_train : n_train + n_cross_valid]] + self.y_valid = self.y[shuffled_indices[n_train : n_train + n_cross_valid]] + self.x_test = self.x[ + shuffled_indices[n_train + n_cross_valid : n_train + n_cross_valid + n_test] + ] + self.y_test = self.y[ + shuffled_indices[n_train + n_cross_valid : n_train + n_cross_valid + n_test] + ] + + +def get_data(dataset_state, dataset_stress, ntrain_size): + set_common_param(dataset_state, dataset_stress) + + data_state = Data(dataset_state) + data_stress = Data(dataset_stress) + data_state.get_shuffled_data() + data_stress.get_shuffled_data() + + train_size_log10 = np.linspace( + 1, np.log10(data_state.x_train.shape[0]), num=ntrain_size + ) + train_size_float = 10**train_size_log10 + train_size = train_size_float.astype(int) + itrain = train_size[ntrain_size - 1] + + return Dataset(data_state, data_stress, itrain).get(10) + + +def set_common_param(dataset_state, dataset_stress): + get_data = ppsci.utils.reader.load_dat_file(dataset_state) + min_state = paddle.to_tensor(data=get_data["miny"]) + range_state = paddle.to_tensor(data=get_data["rangey"]) + min_dstrain = paddle.to_tensor(data=get_data["minx"][10:]) + range_dstrain = paddle.to_tensor(data=get_data["rangex"][10:]) + get_data = ppsci.utils.reader.load_dat_file(dataset_stress) + min_stress = paddle.to_tensor(data=get_data["miny"]) + range_stress = paddle.to_tensor(data=get_data["rangey"]) + common_param.extend( + [ + min_state, + min_stress, + range_state, + range_stress, + min_dstrain, + range_dstrain, + ] + ) + + +def get_model_list( + nhlayers, nneurons, state_x_output_size, state_y_output_size, stress_x_output_size +): + NHLAYERS_PLASTIC = 4 + NNEURONS_PLASTIC = 75 + hl_nodes_elasto = [nneurons] * nhlayers + hl_nodes_plastic = [NNEURONS_PLASTIC] * NHLAYERS_PLASTIC + node_sizes_state_elasto = [state_x_output_size] + node_sizes_state_plastic = [state_x_output_size] + node_sizes_stress = [stress_x_output_size + state_y_output_size - 6] + node_sizes_state_elasto.extend(hl_nodes_elasto) + node_sizes_state_plastic.extend(hl_nodes_plastic) + node_sizes_stress.extend(hl_nodes_elasto) + node_sizes_state_elasto.extend([state_y_output_size - 3]) + node_sizes_state_plastic.extend([state_y_output_size - 1]) + node_sizes_stress.extend([1]) + + activation_elasto = "leaky_relu" + activation_plastic = "leaky_relu" + activations_elasto = [activation_elasto] + activations_plastic = [activation_plastic] + activations_elasto.extend([activation_elasto for ii in range(nhlayers)]) + activations_plastic.extend([activation_plastic for ii in range(NHLAYERS_PLASTIC)]) + activations_elasto.extend([activation_elasto]) + activations_plastic.extend([activation_plastic]) + drop_p = 0.0 + n_state_elasto = ppsci.arch.Epnn( + ("state_x",), + ("out_state_elasto",), + tuple(node_sizes_state_elasto), + tuple(activations_elasto), + drop_p, + ) + n_state_plastic = ppsci.arch.Epnn( + ("state_x",), + ("out_state_plastic",), + tuple(node_sizes_state_plastic), + tuple(activations_plastic), + drop_p, + ) + n_stress = ppsci.arch.Epnn( + ("state_x_f",), + ("out_stress",), + tuple(node_sizes_stress), + tuple(activations_elasto), + drop_p, + ) + return (n_state_elasto, n_state_plastic, n_stress) + + +def get_optimizer_list(model_list, cfg): + optimizer_list = [] + lr_list = [0.001, 0.001, 0.01] + for i, model in enumerate(model_list): + scheduler = ppsci.optimizer.lr_scheduler.ExponentialDecay( + **cfg.TRAIN.lr_scheduler, learning_rate=lr_list[i] + )() + optimizer_list.append( + ppsci.optimizer.Adam(learning_rate=scheduler, weight_decay=0.0)(model) + ) + + scheduler_ratio = ppsci.optimizer.lr_scheduler.ExponentialDecay( + **cfg.TRAIN.lr_scheduler, learning_rate=0.001 + )() + optimizer_list.append( + paddle.optimizer.Adam( + parameters=[gkratio], learning_rate=scheduler_ratio, weight_decay=0.0 + ) + ) + return ppsci.optimizer.OptimizerList(optimizer_list) + + +def plotting(output_dir): + ppsci.utils.misc.plot_curve( + data=eval_log, + xlabel="Epoch", + ylabel="Training Eval", + output_dir=output_dir, + smooth_step=1, + use_semilogy=True, + ) diff --git a/mkdocs.yml b/mkdocs.yml index 9802993af..ba2b6afd1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -65,6 +65,7 @@ nav: - Phy-LSTM: zh/examples/phylstm.md - 材料科学(AI for Material): - hPINNs: zh/examples/hpinns.md + - EPNN: zh/examples/epnn.md - 地球科学(AI for Earth Science): - FourCastNet: zh/examples/fourcastnet.md - API文档: diff --git a/ppsci/arch/__init__.py b/ppsci/arch/__init__.py index f418e225f..baa15a892 100644 --- a/ppsci/arch/__init__.py +++ b/ppsci/arch/__init__.py @@ -31,6 +31,7 @@ from ppsci.arch.afno import AFNONet # isort:skip from ppsci.arch.afno import PrecipNet # isort:skip from ppsci.arch.unetex import UNetEx # isort:skip +from ppsci.arch.epnn import Epnn # isort:skip from ppsci.utils import logger # isort:skip @@ -50,6 +51,7 @@ "AFNONet", "PrecipNet", "UNetEx", + "Epnn", "build_model", ] diff --git a/ppsci/arch/epnn.py b/ppsci/arch/epnn.py new file mode 100644 index 000000000..b158415d8 --- /dev/null +++ b/ppsci/arch/epnn.py @@ -0,0 +1,122 @@ +# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Elasto-Plastic Neural Network (EPNN) + +DEVELOPED AT: + COMPUTATIONAL GEOMECHANICS LABORATORY + DEPARTMENT OF CIVIL ENGINEERING + UNIVERSITY OF CALGARY, AB, CANADA + DIRECTOR: Prof. Richard Wan + +DEVELOPED BY: + MAHDAD EGHBALIAN + +MIT License + +Copyright (c) 2022 Mahdad Eghbalian + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from typing import Tuple + +import paddle.nn as nn + +from ppsci.arch import activation as act_mod +from ppsci.arch import base + + +class Epnn(base.Arch): + """Builds a feedforward network with arbitrary layers. + + Args: + input_keys (Tuple[str, ...]): Name of input keys, such as ("x", "y", "z"). + output_keys (Tuple[str, ...]): Name of output keys, such as ("u", "v", "w"). + node_sizes (Tuple[int, ...]): The tuple of node size. + activations (Tuple[str, ...]): Name of activation functions. + drop_p (float): The parameter p of nn.Dropout. + + Examples: + >>> import ppsci + >>> ann_node_sizes_state = [1] + >>> model = ppsci.arch.Epnn(("x",), ("y",), node_sizes=node_sizes_state, + activations=("leaky_relu"), + drop_p=0.0 + ) + """ + + def __init__( + self, + input_keys: Tuple[str, ...], + output_keys: Tuple[str, ...], + node_sizes: Tuple[int, ...], + activations: Tuple[str, ...], + drop_p: float, + ): + super().__init__() + self.active_func = [act_mod.get_activation(i) for i in activations] + self.node_sizes = node_sizes + self.drop_p = drop_p + self.layers = [] + self.layers.append( + nn.Linear(in_features=node_sizes[0], out_features=node_sizes[1]) + ) + layer_sizes = zip(node_sizes[1:-2], node_sizes[2:-1]) + self.layers.extend( + [nn.Linear(in_features=h1, out_features=h2) for h1, h2 in layer_sizes] + ) + self.layers.append( + nn.Linear( + in_features=node_sizes[-2], out_features=node_sizes[-1], bias_attr=False + ) + ) + + self.layers = nn.LayerList(self.layers) + self.dropout = nn.Dropout(p=drop_p) + self.input_keys = input_keys + self.output_keys = output_keys + + def forward(self, x): + if self._input_transform is not None: + x = self._input_transform(x) + + y = x[self.input_keys[0]] + for ilayer in range(len(self.layers)): + y = self.layers[ilayer](y) + if ilayer != len(self.layers) - 1: + y = self.active_func[ilayer + 1](y) + if ilayer != len(self.layers) - 1: + y = self.dropout(y) + y = self.split_to_dict(y, self.output_keys, axis=-1) + + if self._output_transform is not None: + y = self._output_transform(x, y) + return y diff --git a/ppsci/loss/__init__.py b/ppsci/loss/__init__.py index ee83ca5e9..4e7d6242c 100644 --- a/ppsci/loss/__init__.py +++ b/ppsci/loss/__init__.py @@ -22,6 +22,7 @@ from ppsci.loss.l2 import L2Loss from ppsci.loss.l2 import L2RelLoss from ppsci.loss.l2 import PeriodicL2Loss +from ppsci.loss.mae import MAELoss from ppsci.loss.mse import MSELoss from ppsci.loss.mse import MSELossWithL2Decay from ppsci.loss.mse import PeriodicMSELoss @@ -35,6 +36,7 @@ "L2Loss", "L2RelLoss", "PeriodicL2Loss", + "MAELoss", "MSELoss", "MSELossWithL2Decay", "PeriodicMSELoss", diff --git a/ppsci/loss/mae.py b/ppsci/loss/mae.py new file mode 100644 index 000000000..75b39fe5c --- /dev/null +++ b/ppsci/loss/mae.py @@ -0,0 +1,82 @@ +# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Dict +from typing import Optional +from typing import Union + +import paddle.nn.functional as F +from typing_extensions import Literal + +from ppsci.loss import base + + +class MAELoss(base.Loss): + r"""Class for mean absolute error loss. + + $$ + L = + \begin{cases} + \dfrac{1}{N} \Vert {\mathbf{x}-\mathbf{y}} \Vert_1, & \text{if reduction='mean'} \\ + \Vert {\mathbf{x}-\mathbf{y}} \Vert_1, & \text{if reduction='sum'} + \end{cases} + $$ + + $$ + \mathbf{x}, \mathbf{y} \in \mathcal{R}^{N} + $$ + + Args: + reduction (Literal["mean", "sum"], optional): Reduction method. Defaults to "mean". + weight (Optional[Union[float, Dict[str, float]]]): Weight for loss. Defaults to None. + + Examples: + >>> import ppsci + >>> loss = ppsci.loss.MAELoss("mean") + """ + + def __init__( + self, + reduction: Literal["mean", "sum"] = "mean", + weight: Optional[Union[float, Dict[str, float]]] = None, + ): + if reduction not in ["mean", "sum"]: + raise ValueError( + f"reduction should be 'mean' or 'sum', but got {reduction}" + ) + super().__init__(reduction, weight) + + def forward(self, output_dict, label_dict, weight_dict=None): + losses = 0.0 + for key in label_dict: + loss = F.l1_loss(output_dict[key], label_dict[key], "none") + if weight_dict: + loss *= weight_dict[key] + + if "area" in output_dict: + loss *= output_dict["area"] + + if self.reduction == "sum": + loss = loss.sum() + elif self.reduction == "mean": + loss = loss.mean() + if isinstance(self.weight, (float, int)): + loss *= self.weight + elif isinstance(self.weight, dict) and key in self.weight: + loss *= self.weight[key] + + losses += loss + return losses diff --git a/ppsci/optimizer/lr_scheduler.py b/ppsci/optimizer/lr_scheduler.py index 4c7cc44fe..697aac48c 100644 --- a/ppsci/optimizer/lr_scheduler.py +++ b/ppsci/optimizer/lr_scheduler.py @@ -16,6 +16,7 @@ import abc import math +from typing import List from typing import Tuple from typing import Union @@ -732,3 +733,43 @@ def __call__(self): setattr(learning_rate, "by_epoch", self.by_epoch) return learning_rate + + +class SchedulerList: + """SchedulerList which wrap more than one scheduler. + Args: + scheduler_list (Tuple[lr.LRScheduler, ...]): Schedulers listed in a tuple. + by_epoch (bool, optional): Learning rate decays by epoch when by_epoch is True, else by iter. Defaults to False. + Examples: + >>> import ppsci + >>> sch1 = ppsci.optimizer.lr_scheduler.Linear(10, 2, 0.001)() + >>> sch2 = ppsci.optimizer.lr_scheduler.ExponentialDecay(10, 2, 1e-3, 0.95, 3)() + >>> sch = ppsci.optimizer.lr_scheduler.SchedulerList((sch1, sch2)) + """ + + def __init__( + self, scheduler_list: Tuple[lr.LRScheduler, ...], by_epoch: bool = False + ): + super().__init__() + self._sch_list = scheduler_list + self.by_epoch = by_epoch + + def step(self): + for sch in self._sch_list: + sch.step() + + def get_lr(self) -> float: + """Return learning rate of first scheduler""" + return self._sch_list[0].get_lr() + + def _state_keys(self) -> List[str]: + return ["last_epoch", "last_lr"] + + def __len__(self) -> int: + return len(self._sch_list) + + def __getitem__(self, idx): + return self._sch_list[idx] + + def __setitem__(self, idx, sch): + raise NotImplementedError("Can not modify any item in SchedulerList.") diff --git a/ppsci/utils/reader.py b/ppsci/utils/reader.py index f6738dec6..3236c3173 100644 --- a/ppsci/utils/reader.py +++ b/ppsci/utils/reader.py @@ -16,6 +16,7 @@ import collections import csv +import pickle from typing import Dict from typing import Optional from typing import Tuple @@ -31,6 +32,7 @@ "load_npz_file", "load_vtk_file", "load_vtk_with_time_file", + "load_dat_file", ] @@ -179,14 +181,18 @@ def load_vtk_file( i = 0 for key in input_dict: if key == "t": - input_dict[key].append(np.full((n, 1), index * time_step, "float32")) + input_dict[key].append( + np.full((n, 1), index * time_step, paddle.get_default_dtype()) + ) else: input_dict[key].append( - mesh.points[:, i].reshape(n, 1).astype("float32") + mesh.points[:, i].reshape(n, 1).astype(paddle.get_default_dtype()) ) i += 1 for i, key in enumerate(label_dict): - label_dict[key].append(np.array(mesh.point_data[key], "float32")) + label_dict[key].append( + np.array(mesh.point_data[key], paddle.get_default_dtype()) + ) for key in input_dict: input_dict[key] = np.concatenate(input_dict[key]) for key in label_dict: @@ -212,3 +218,42 @@ def load_vtk_with_time_file(file: str) -> Dict[str, np.ndarray]: z = mesh.points[:, 2].reshape(n, 1) input_dict = {"t": t, "x": x, "y": y, "z": z} return input_dict + + +def load_dat_file( + file_path: str, + keys: Tuple[str, ...] = None, + alias_dict: Optional[Dict[str, str]] = None, +) -> Dict[str, np.ndarray]: + """Load *.dat file and fetch data as given keys. + + Args: + file_path (str): Dat file path. + keys (Tuple[str, ...]): Required fetching keys. + alias_dict (Optional[Dict[str, str]]): Alias for keys, + i.e. {original_key: original_key}. Defaults to None. + + Returns: + Dict[str, np.ndarray]: Loaded data in dict. + """ + + if alias_dict is None: + alias_dict = {} + + try: + # read all data from .dat file + raw_data = pickle.load(open(file_path, "rb")) + except FileNotFoundError as e: + raise e + + # convert to numpy array + data_dict = {} + if keys is None: + keys = raw_data.keys() + for key in keys: + fetch_key = alias_dict[key] if key in alias_dict else key + if fetch_key not in raw_data: + raise KeyError(f"fetch_key({fetch_key}) do not exist in raw_data.") + data_dict[key] = np.asarray(raw_data[fetch_key], paddle.get_default_dtype()) + + return data_dict