跳转至

psc_NN(Machine Learning for Perovskite Solar Cells: An Open-Source Pipeline)

注意事项

  1. 开始训练前,请确保数据集已正确放置在 data/cleaned/ 目录下。
  2. 训练和评估需要安装额外的依赖包,请使用 pip install -r requirements.txt 安装。
  3. 为获得最佳性能,建议使用 GPU 进行训练。
python psc_nn.py
# 使用本地预训练模型
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/psc/data.zip
unzip data.zip
python psc_nn.py mode=eval eval.pretrained_model_path="Your pdparams path"
# 或使用提供的预训练模型
wget -c https://paddle-org.bj.bcebos.com/paddlescience/datasets/psc/data.zip
unzip data.zip
python psc_nn.py mode=eval eval.pretrained_model_path="https://paddle-org.bj.bcebos.com/paddlescience/models/PerovskiteSolarCells/solar_cell_pretrained.pdparams"
预训练模型 指标
solar_cell_pretrained.pdparams RMSE: 3.91798

1. 背景简介

太阳能电池是一种通过光电效应将光能直接转换为电能的关键能源器件,其性能预测是优化和设计太阳能电池的重要环节。然而,传统的性能预测方法往往依赖于复杂的物理模拟和大量的实验测试,不仅成本高昂,且耗时较长,制约了研究与开发的效率。

近年来,深度学习和机器学习技术的快速发展,为太阳能电池性能预测提供了创新的方法。通过机器学习技术,可以显著加快开发速度,同时实现与实验结果相当的预测精度。特别是在钙钛矿太阳能电池研究中,材料的化学组成和结构多样性为模型训练带来了新的挑战。为了解决这一问题,研究者们通常将材料的特性转换为固定长度的特征向量,以适配机器学习模型。尽管如此,不同性能指标的特征表示设计仍需不断优化,同时对模型预测结果的可解释性要求也更为严格。

本研究中,通过利用包含钙钛矿太阳能电池特性信息的全面数据库(PDP),我们构建并评估了包括 XGBoost、psc_nn 在内的多种机器学习模型,专注于预测短路电流密度(Jsc)。研究结果表明,结合深度学习与超参数优化工具(如 Optuna)能够显著提升太阳能电池设计的效率,为新型太阳能电池研发提供了更精确且高效的解决方案。

2. 模型原理

本章节仅对太阳能电池性能预测模型的原理进行简单地介绍,详细的理论推导请阅读 Machine Learning for Perovskite Solar Cells: An Open-Source Pipeline

该方法的主要思想是通过人工神经网络建立光谱响应数据与短路电流密度(Jsc)之间的非线性映射关系。人工神经网络模型的总体结构如下图所示:

psc_nn_overview

本案例采用多层感知机(MLP)作为基础模型架构,主要包括以下几个部分:

  1. 输入层:接收 2808 维的光谱响应数据
  2. 隐藏层:4-6 层全连接层,每层的神经元数量通过 Optuna 优化
  3. 激活函数:使用 ReLU 激活函数引入非线性特性
  4. 输出层:输出预测的 Jsc 值

通过这种方式,我们可以自动找到最适合当前任务的模型配置,提高模型的预测性能。

3. 模型实现

本章节我们讲解如何基于 PaddleScience 代码实现钙钛矿太阳能电池性能预测模型。本案例结合 Optuna 框架进行超参数优化,并使用 PaddleScience 内置的各种功能模块。为了快速理解 PaddleScience,接下来仅对模型构建、约束构建、评估器构建等关键步骤进行阐述,而其余细节请参考 API文档

3.1 数据集介绍

本案例使用的数据集包含 Perovskite Database Project(PDP) 数据。数据集分为以下几个部分:

  1. 训练集:
  2. 特征数据:data/cleaned/training.csv
  3. 标签数据:data/cleaned/training_labels.csv
  4. 验证集:
  5. 特征数据:data/cleaned/validation.csv
  6. 标签数据:data/cleaned/validation_labels.csv

为了方便数据处理,我们实现了一个辅助函数 create_tensor_dict 来创建输入和标签的 tensor 字典:

examples/perovskite_solar_cells/psc_nn.py
def create_tensor_dict(X, y):
    """Create Tensor Dictionary for Input and Labels"""
    return {
        "input": paddle.to_tensor(X.values, dtype="float32"),
        "label": {"target": paddle.to_tensor(y.values, dtype="float32")},
    }

数据集的读取和预处理代码如下:

examples/perovskite_solar_cells/psc_nn.py
# Read and preprocess data
X_train = pd.read_csv(cfg.data.train_features_path)
y_train = pd.read_csv(cfg.data.train_labels_path)
X_val = pd.read_csv(cfg.data.val_features_path)
y_val = pd.read_csv(cfg.data.val_labels_path)

for col in X_train.columns:
    if "[" in col or "]" in col:
        old_name = col
        new_name = col.replace("[", "(").replace("]", ")")
        X_train = X_train.rename(columns={old_name: new_name})
        X_val = X_val.rename(columns={old_name: new_name})

X_train, X_verif, y_train, y_verif = train_test_split(
    X_train, y_train, test_size=0.1, random_state=42
)

for df in [X_train, y_train, X_verif, y_verif, X_val, y_val]:
    df.reset_index(drop=True, inplace=True)

为了进行超参数优化,我们将训练集进一步划分为训练集和验证集:

examples/perovskite_solar_cells/psc_nn.py
X_train, X_verif, y_train, y_verif = train_test_split(
    X_train, y_train, test_size=0.1, random_state=42
)

3.2 模型构建

本案例使用 PaddleScience 内置的 ppsci.arch.MLP 构建多层感知机模型。模型的超参数通过 Optuna 框架进行优化,主要包括:

  1. 网络层数:4-6层
  2. 每层神经元数量:10-input_dim/2
  3. 激活函数:ReLU
  4. 输入维度:2808(光谱响应数据维度)
  5. 输出维度:1(Jsc 预测值)

模型定义代码如下:

examples/perovskite_solar_cells/psc_nn.py
def define_model(trial, input_dim, output_dim):
    n_layers = trial.suggest_int("n_layers", 4, 6)
    hidden_sizes = []
    for i in range(n_layers):
        out_features = trial.suggest_int(f"n_units_l{i}", 10, input_dim // 2)
        hidden_sizes.append(out_features)

    model = ppsci.arch.MLP(
        input_keys=("input",),
        output_keys=("target",),
        num_layers=None,
        hidden_size=hidden_sizes,
        activation="relu",
        input_dim=input_dim,
        output_dim=output_dim,
    )
    return model

3.3 损失函数设计

考虑到数据集中不同样本的重要性可能不同,我们设计了一个加权均方误差损失函数。该函数对较大的 Jsc 值赋予更高的权重,以提高模型在高性能太阳能电池上的预测准确性:

examples/perovskite_solar_cells/psc_nn.py
def weighted_loss(output_dict, target_dict, weight_dict=None):
    pred = output_dict["target"]
    true = target_dict["target"]
    epsilon = 1e-06
    n = len(true)
    weights = true / (paddle.sum(x=true) + epsilon)
    squared = (true - pred) ** 2
    weighted = squared * weights
    loss = paddle.sum(x=weighted) / n
    return {"weighted_mse": loss}

3.4 约束构建

本案例基于数据驱动的方法求解问题,因此使用 PaddleScience 内置的 SupervisedConstraint 构建监督约束。为了减少代码重复,我们实现了 create_constraint 函数来创建监督约束:

examples/perovskite_solar_cells/psc_nn.py
def create_constraint(input_dict, batch_size, shuffle=True):
    """Create supervision constraints"""
    return SupervisedConstraint(
        dataloader_cfg={
            "dataset": {
                "name": "NamedArrayDataset",
                "input": {"input": input_dict["input"]},
                "label": input_dict["label"],
            },
            "batch_size": batch_size,
            "sampler": {
                "name": "BatchSampler",
                "drop_last": False,
                "shuffle": shuffle,
            },
        },
        loss=weighted_loss,
        output_expr={"target": lambda out: out["target"]},
        name="train_constraint",
    )

3.5 评估器构建

为了实时监测模型的训练情况,我们实现了 create_validator 函数来创建评估器:

examples/perovskite_solar_cells/psc_nn.py
def create_validator(input_dict, batch_size, name="validator"):
    """Create an evaluator"""
    return SupervisedValidator(
        dataloader_cfg={
            "dataset": {
                "name": "NamedArrayDataset",
                "input": {"input": input_dict["input"]},
                "label": input_dict["label"],
            },
            "batch_size": batch_size,
        },
        loss=weighted_loss,
        output_expr={"target": lambda out: out["target"]},
        metric={"RMSE": ppsci.metric.RMSE(), "MAE": ppsci.metric.MAE()},
        name=name,
    )

3.6 优化器构建

为了统一管理优化器和学习率调度器的创建,我们实现了 create_optimizer 函数:

examples/perovskite_solar_cells/psc_nn.py
def create_optimizer(model, optimizer_name, lr, train_cfg, data_size):
    """Create optimizer and learning rate scheduler"""
    schedule = lr_scheduler.ExponentialDecay(
        epochs=train_cfg.epochs,
        iters_per_epoch=data_size // train_cfg.batch_size,
        learning_rate=lr,
        gamma=train_cfg.lr_scheduler.gamma,
        decay_steps=train_cfg.lr_scheduler.decay_steps,
        warmup_epoch=train_cfg.lr_scheduler.warmup_epoch,
        warmup_start_lr=train_cfg.lr_scheduler.warmup_start_lr,
    )()

    if optimizer_name == "Adam":
        return optimizer.Adam(learning_rate=schedule)(model)
    elif optimizer_name == "RMSProp":
        return optimizer.RMSProp(learning_rate=schedule)(model)
    else:
        return optimizer.SGD(learning_rate=schedule)(model)

3.7 模型训练与评估

在训练过程中,我们使用上述封装的函数来创建数据字典、约束、评估器和优化器:

examples/perovskite_solar_cells/psc_nn.py
train_dict = create_tensor_dict(X_train, y_train)
val_dict = create_tensor_dict(X_val, y_val)

train_constraint = create_constraint(train_dict, cfg.TRAIN.batch_size)
val_validator = create_validator(val_dict, cfg.EVAL.batch_size, "val_validator")

4. 完整代码

examples/perovskite_solar_cells/psc_nn.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
import os
import tempfile
import urllib.request
from os import path as osp

import hydra
import numpy as np
import optuna
import paddle
import pandas as pd
from matplotlib import pyplot as plt
from omegaconf import DictConfig
from sklearn.metrics import mean_absolute_percentage_error
from sklearn.metrics import mean_squared_error
from sklearn.metrics import r2_score
from sklearn.model_selection import train_test_split

import ppsci
from ppsci.constraint import SupervisedConstraint
from ppsci.optimizer import lr_scheduler
from ppsci.optimizer import optimizer
from ppsci.solver import Solver
from ppsci.validate import SupervisedValidator


def download_model_from_url(url, local_path=None):
    """
    Download model from URL to local path.

    Args:
        url (str): URL to download the model from
        local_path (str, optional): Local path to save the model.
                                   If None, saves to a temporary file.

    Returns:
        str: Path to the downloaded model file
    """
    if local_path is None:
        # Create a temporary file
        temp_dir = tempfile.gettempdir()
        local_path = os.path.join(temp_dir, "downloaded_model.pdparams")

    print(f"Downloading model from {url} to {local_path}")

    try:
        urllib.request.urlretrieve(url, local_path)
        print(f"Successfully downloaded model to {local_path}")
        return local_path
    except Exception as e:
        raise RuntimeError(f"Failed to download model from {url}: {str(e)}")


def load_model_from_path_or_url(model_path):
    """
    Load model from local path or URL.

    Args:
        model_path (str): Local path or URL to the model

    Returns:
        dict: Loaded model dictionary
    """
    if model_path.startswith(("http://", "https://")):
        # It's a URL, download first
        local_path = download_model_from_url(model_path)
        return paddle.load(local_path)
    else:
        # It's a local path
        return paddle.load(model_path)


def weighted_loss(output_dict, target_dict, weight_dict=None):
    pred = output_dict["target"]
    true = target_dict["target"]
    epsilon = 1e-06
    n = len(true)
    weights = true / (paddle.sum(x=true) + epsilon)
    squared = (true - pred) ** 2
    weighted = squared * weights
    loss = paddle.sum(x=weighted) / n
    return {"weighted_mse": loss}


def create_tensor_dict(X, y):
    """Create Tensor Dictionary for Input and Labels"""
    return {
        "input": paddle.to_tensor(X.values, dtype="float32"),
        "label": {"target": paddle.to_tensor(y.values, dtype="float32")},
    }


def create_constraint(input_dict, batch_size, shuffle=True):
    """Create supervision constraints"""
    return SupervisedConstraint(
        dataloader_cfg={
            "dataset": {
                "name": "NamedArrayDataset",
                "input": {"input": input_dict["input"]},
                "label": input_dict["label"],
            },
            "batch_size": batch_size,
            "sampler": {
                "name": "BatchSampler",
                "drop_last": False,
                "shuffle": shuffle,
            },
        },
        loss=weighted_loss,
        output_expr={"target": lambda out: out["target"]},
        name="train_constraint",
    )


def create_validator(input_dict, batch_size, name="validator"):
    """Create an evaluator"""
    return SupervisedValidator(
        dataloader_cfg={
            "dataset": {
                "name": "NamedArrayDataset",
                "input": {"input": input_dict["input"]},
                "label": input_dict["label"],
            },
            "batch_size": batch_size,
        },
        loss=weighted_loss,
        output_expr={"target": lambda out: out["target"]},
        metric={"RMSE": ppsci.metric.RMSE(), "MAE": ppsci.metric.MAE()},
        name=name,
    )


def create_optimizer(model, optimizer_name, lr, train_cfg, data_size):
    """Create optimizer and learning rate scheduler"""
    schedule = lr_scheduler.ExponentialDecay(
        epochs=train_cfg.epochs,
        iters_per_epoch=data_size // train_cfg.batch_size,
        learning_rate=lr,
        gamma=train_cfg.lr_scheduler.gamma,
        decay_steps=train_cfg.lr_scheduler.decay_steps,
        warmup_epoch=train_cfg.lr_scheduler.warmup_epoch,
        warmup_start_lr=train_cfg.lr_scheduler.warmup_start_lr,
    )()

    if optimizer_name == "Adam":
        return optimizer.Adam(learning_rate=schedule)(model)
    elif optimizer_name == "RMSProp":
        return optimizer.RMSProp(learning_rate=schedule)(model)
    else:
        return optimizer.SGD(learning_rate=schedule)(model)


def define_model(trial, input_dim, output_dim):
    n_layers = trial.suggest_int("n_layers", 4, 6)
    hidden_sizes = []
    for i in range(n_layers):
        out_features = trial.suggest_int(f"n_units_l{i}", 10, input_dim // 2)
        hidden_sizes.append(out_features)

    model = ppsci.arch.MLP(
        input_keys=("input",),
        output_keys=("target",),
        num_layers=None,
        hidden_size=hidden_sizes,
        activation="relu",
        input_dim=input_dim,
        output_dim=output_dim,
    )
    return model


def train(cfg: DictConfig):
    # Read and preprocess data
    X_train = pd.read_csv(cfg.data.train_features_path)
    y_train = pd.read_csv(cfg.data.train_labels_path)
    X_val = pd.read_csv(cfg.data.val_features_path)
    y_val = pd.read_csv(cfg.data.val_labels_path)

    for col in X_train.columns:
        if "[" in col or "]" in col:
            old_name = col
            new_name = col.replace("[", "(").replace("]", ")")
            X_train = X_train.rename(columns={old_name: new_name})
            X_val = X_val.rename(columns={old_name: new_name})

    X_train, X_verif, y_train, y_verif = train_test_split(
        X_train, y_train, test_size=0.1, random_state=42
    )

    for df in [X_train, y_train, X_verif, y_verif, X_val, y_val]:
        df.reset_index(drop=True, inplace=True)

    def objective(trial):
        model = define_model(trial, cfg.model.input_dim, cfg.model.output_dim)

        optimizer_name = trial.suggest_categorical(
            "optimizer", ["Adam", "RMSProp", "SGD"]
        )
        lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True)

        train_dict = create_tensor_dict(X_train, y_train)
        verif_dict = create_tensor_dict(X_verif, y_verif)

        opt = create_optimizer(model, optimizer_name, lr, cfg.TRAIN, len(X_train))

        train_constraint = create_constraint(train_dict, cfg.TRAIN.batch_size)
        verif_validator = create_validator(
            verif_dict, cfg.EVAL.batch_size, "verif_validator"
        )

        solver = Solver(
            model=model,
            constraint={"train": train_constraint},
            optimizer=opt,
            validator={"verif": verif_validator},
            output_dir=cfg.output_dir,
            epochs=cfg.TRAIN.search_epochs,
            iters_per_epoch=len(X_train) // cfg.TRAIN.batch_size,
            eval_during_train=cfg.TRAIN.eval_during_train,
            eval_freq=cfg.TRAIN.eval_freq,
            save_freq=cfg.TRAIN.save_freq,
            eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
            log_freq=cfg.TRAIN.log_freq,
        )

        solver.train()

        verif_preds = solver.predict({"input": verif_dict["input"]}, return_numpy=True)[
            "target"
        ]

        verif_rmse = np.sqrt(mean_squared_error(y_verif.values, verif_preds))

        return verif_rmse

    study = optuna.create_study()
    study.optimize(objective, n_trials=50)

    best_params = study.best_trial.params
    print("\nBest hyperparameters: " + str(best_params))

    # Save the optimal model structure
    hidden_sizes = []
    for i in range(best_params["n_layers"]):
        hidden_sizes.append(best_params[f"n_units_l{i}"])

    # Create and train the final model
    final_model = define_model(
        study.best_trial, cfg.model.input_dim, cfg.model.output_dim
    )
    opt = create_optimizer(
        final_model,
        best_params["optimizer"],
        best_params["lr"],
        cfg.TRAIN,
        len(X_train),
    )

    train_dict = create_tensor_dict(X_train, y_train)
    val_dict = create_tensor_dict(X_val, y_val)

    train_constraint = create_constraint(train_dict, cfg.TRAIN.batch_size)
    val_validator = create_validator(val_dict, cfg.EVAL.batch_size, "val_validator")

    solver = Solver(
        model=final_model,
        constraint={"train": train_constraint},
        optimizer=opt,
        validator={"valid": val_validator},
        output_dir=cfg.output_dir,
        epochs=cfg.TRAIN.epochs,
        iters_per_epoch=len(X_train) // cfg.TRAIN.batch_size,
        eval_during_train=cfg.TRAIN.eval_during_train,
        eval_freq=cfg.TRAIN.eval_freq,
        save_freq=cfg.TRAIN.save_freq,
        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
        log_freq=cfg.TRAIN.log_freq,
    )

    solver.train()

    # Save model structure and weights
    model_dict = {
        "state_dict": final_model.state_dict(),
        "hidden_size": hidden_sizes,
        "n_layers": best_params["n_layers"],
        "optimizer": best_params["optimizer"],
        "lr": best_params["lr"],
    }
    paddle.save(
        model_dict, os.path.join(cfg.output_dir, "checkpoints", "best_model.pdparams")
    )
    print(
        "Saved model structure and weights to "
        + os.path.join(cfg.output_dir, "checkpoints", "best_model.pdparams")
    )

    solver.plot_loss_history(by_epoch=True, smooth_step=1)
    solver.eval()

    visualize_results(solver, X_val, y_val, cfg.output_dir)


def evaluate(cfg: DictConfig):
    # Read and preprocess data
    X_val = pd.read_csv(cfg.data.val_features_path)
    y_val = pd.read_csv(cfg.data.val_labels_path)

    for col in X_val.columns:
        if "[" in col or "]" in col:
            old_name = col
            new_name = col.replace("[", "(").replace("]", ")")
            X_val = X_val.rename(columns={old_name: new_name})

    # Loading model structure and weights
    print(f"Loading model from {cfg.EVAL.pretrained_model_path}")
    model_dict = load_model_from_path_or_url(cfg.EVAL.pretrained_model_path)
    hidden_size = model_dict["hidden_size"]
    print(f"Loaded model structure with hidden sizes: {hidden_size}")

    model = ppsci.arch.MLP(
        input_keys=("input",),
        output_keys=("target",),
        num_layers=None,
        hidden_size=hidden_size,
        activation="relu",
        input_dim=cfg.model.input_dim,
        output_dim=cfg.model.output_dim,
    )

    # Load model weights
    model.set_state_dict(model_dict["state_dict"])
    print("Successfully loaded model weights")

    valid_dict = create_tensor_dict(X_val, y_val)
    valid_validator = create_validator(
        valid_dict, cfg.EVAL.batch_size, "valid_validator"
    )

    solver = Solver(
        model=model,
        output_dir=cfg.output_dir,
        validator={"valid": valid_validator},
        eval_with_no_grad=cfg.EVAL.eval_with_no_grad,
    )

    # evaluation model
    print("Evaluating model...")
    solver.eval()

    # Generate prediction results
    predictions = solver.predict({"input": valid_dict["input"]}, return_numpy=True)[
        "target"
    ]

    # Calculate multiple evaluation indicators
    rmse = np.sqrt(mean_squared_error(y_val.values, predictions))
    r2 = r2_score(y_val.values, predictions)
    mape = mean_absolute_percentage_error(y_val.values, predictions)

    print("Evaluation metrics:")
    print(f"RMSE: {rmse:.5f}")
    print(f"R2 Score: {r2:.5f}")
    print(f"MAPE: {mape:.5f}")

    # Visualization results
    print("Generating visualization...")
    visualize_results(solver, X_val, y_val, cfg.output_dir)
    print("Evaluation completed.")


def visualize_results(solver, X_val, y_val, output_dir):
    pred_dict = solver.predict(
        {"input": paddle.to_tensor(X_val.values, dtype="float32")}, return_numpy=True
    )
    val_preds = pred_dict["target"]
    val_true = y_val.values

    plt.figure(figsize=(10, 6))
    plt.grid(True, linestyle="--", alpha=0.7)
    plt.hist(val_true, bins=30, alpha=0.6, label="True Jsc", color="tab:blue")
    plt.hist(val_preds, bins=30, alpha=0.6, label="Predicted Jsc", color="orange")

    pred_mean = np.mean(val_preds)
    pred_std = np.std(val_preds)
    plt.axvline(pred_mean, color="black", linestyle="--")
    plt.axvline(pred_mean + pred_std, color="red", linestyle="--")
    plt.axvline(pred_mean - pred_std, color="red", linestyle="--")

    val_rmse = np.sqrt(mean_squared_error(val_true, val_preds))
    plt.title(f"Distribution of True Jsc vs Pred Jsc: RMSE {val_rmse:.5f}", pad=20)
    plt.xlabel("Jsc (mA/cm²)")
    plt.ylabel("Counts")
    plt.legend(fontsize=10)
    plt.tight_layout()
    plt.savefig(
        osp.join(output_dir, "jsc_distribution.png"), dpi=300, bbox_inches="tight"
    )
    plt.close()


@hydra.main(version_base=None, config_path="./conf", config_name="psc_nn.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()

5. 参考文献