一般在一个新的 trick 和 experience 开坑时,都会先暂时粗略地搬运一些其他地方的内容,或者简略描述。偶尔精进与专门研究时,会特别地丰富和细致化该内容。
# pytorch_mssim.ssim 的使用
以下面计算 ssim 的代码为例:
ssim_value = ssim(final, gt_batch, data_range=2.0, size_average=True)  | 
data_range 表示图像像素值的动态范围(最大值与最小值的差)。如果输入图像经过归一化处理(如 transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) ),则像素值范围会被映射到 [-1, 1] 。此时 data_range 应设为 2.0 (因为 1 - (-1) = 2 ),而不是 1.0 。
SSIM 默认假设输入范围是 [0, 1] (当 data_range=1 )或 [0, 255] (当 data_range=255 )。
size_average=True 会将 SSIM 值在所有图像和通道上取平均。 pytorch_msssim 的新版本使用 reduction 取代这个参数,重写为: reduction='mean' 。
有时候,应该确保输入给 ssim 做计算时的数据范围在 [-1, 1] 之间,不然,则要裁剪处理:
final_clamped = torch.clamp(final, 0.0, 1.0)  | |
ssim_value = ssim(final_clamped, gt_batch, data_range=1.0, size_average=True)  | 
# 损失函数下降、震荡、上升的原因
在训练包含多个损失函数的模型时,各子损失函数的变化趋势能够反映模型的学习动态和优化方向。以下是不同趋势的详细分析及应对策略:
# 一、损失函数整体持续下降
# 含义解析
- 良性学习信号:模型正在有效优化该任务目标,权重分配合理,数据质量良好。
 - 潜在风险:
 
- 过拟合倾向:若验证集对应指标未同步下降,可能过拟合训练数据。
 - 任务主导性:其他损失未充分优化,模型可能偏向该任务。
 # 典型案例
- 重建损失(如 L1/L2)持续下降,但对抗损失震荡 → 模型过度拟合像素级精度,忽视生成真实性。
 # 应对策略
- 验证泛化性:检查验证集对应指标是否同步改善
 - 调整权重:若其他损失停滞,适当降低该损失权重(如从 1.0→0.7)
 - 早停机制:当验证损失不再下降时停止训练
 # 二、损失函数震荡波动
# 含义解析
- 优化不稳定:学习率过高、批次过小或损失间存在冲突。
 - 数据问题:噪声数据或类别不均衡导致梯度方向不一致。
 - 对抗性博弈:典型于 GAN 的判别器与生成器损失交替上升。
 # 数值特征
- 高频震荡(如 ±5%):常由学习率过大引起
 - 低频震荡(如每 5 个 epoch 变化):多任务目标冲突
 # 典型案例
- 分类损失下降但正则化损失震荡 → L2 正则化强度过高导致参数更新不稳定
 # 应对策略
- 降低学习率:将初始学习率减少 3-5 倍(如 2e-4→5e-5)
 - 增大批次大小:从 32 提升至 128,稳定梯度估计
 - 梯度裁剪:设置
 max_grad_norm=1.0- 冲突分析:计算损失梯度余弦相似度,对负相关损失解耦训练
 # 三、损失函数持续上升
# 含义解析
- 严重警告信号:模型在该任务上性能退化,优化方向错误。
 - 常见诱因:
 
- 损失权重倒置:如误将权重设为负数
 - 任务本质冲突:如超分辨率任务中,L1 损失下降但感知损失上升
 - 数值不稳定:梯度爆炸导致损失进入病态区域
 # 典型案例
- 对抗损失上升而重建损失下降 → 判别器过强导致生成器无法有效学习
 # 应对策略
- 立即暂停训练:检查损失计算代码和权重符号
 - 损失权重热力图:可视化各损失对总损失的贡献比例
 - 渐进式训练:分阶段引入上升的损失项(如先用 L1 预训练,第 50epoch 加入对抗损失)
 - 架构改进:对于根本性冲突,修改网络结构(如增加多尺度特征融合模块)
 # 四、综合优化建议
- 动态权重调整:采用不确定性加权法(如《Multi-Task Learning Using Uncertainty to Weigh Losses》)
 
# 各损失自动加权示例 log_var = torch.nn.Parameter(torch.zeros(3)) # 3 个损失 loss = 0.5*(loss1/torch.exp(log_var[0]) + loss2/torch.exp(log_var[1]) + log_var.sum())- 损失相关性监控:计算各损失间的 Pearson 相关系数矩阵,识别冲突组合
 - 课程学习策略:早期侧重易优化损失(如 L1),后期加强高阶损失(如 SSIM、VGG 感知损失)
 - 可视化工具:使用 TensorBoard 的并行坐标视图对比超参数与损失关系
 # 五、调试检查清单
当出现异常损失趋势时,按以下顺序排查:
- 数值检查:
 
- 确认损失计算未出现 NaN/Inf
 - 检查梯度幅值(
 torch.nn.utils.clip_grad_norm_)- 数据流验证:
 
# 数据检查代码片段 for batch in val_loader: print(batch['image'].min(), batch['image'].max()) # 应为 [0,1] 或 [-1,1] visualize(batch['image'][0]) # 肉眼验证图像质量- 权重合理性:确保各损失量级匹配(如 L1≈0.1,对抗损失≈2.0 时,需调整权重平衡)
 - 模型容量测试:在小数据集(如 100 样本)上过拟合,验证能否达到预期损失
 通过系统分析损失动态,可精准定位模型优化瓶颈,实现多目标协同优化。
# transforms.Resize()
这个与 RandomCrop() 还不太一样, Resize() 是等比例的缩放原图。所以存在一定的信息损失,一般不使用这种操作。
# 不确定性损失加权法 —— 多任务损失均衡
以下是使用不确定性加权法改造后的损失函数实现:
import torch import torch.nn as nn import torch.nn.functional as F from pytorch_msssim import ssim class UncertaintyWeightedLoss(nn.Module): def __init__(self, trans, num_tasks=5):""" trans: HVI转换器实例 num_tasks: 需要加权的损失项数量(这里包含L1, SSIM, Res, Cons, HVI) """ super().__init__() self.trans = trans# 初始化可学习的不确定性参数(log 方差) self.log_vars = nn.Parameter(torch.zeros(num_tasks))# 初始化参数(可选) nn.init.uniform_(self.log_vars, -3, -1) # 初始方差在 0.05~0.37 之间 def compute_losses(self, final, gt, output, S1, P1, S2, P2):"""分解计算各个基础损失项"""# RGB 空间损失 l1_loss = F.l1_loss(final, gt) ssim_loss = 1 - ssim(final, gt, data_range=1.0, size_average=True)# 残差一致性损失 loss_res1 = F.mse_loss(S1, P1 + S2) / (S1.detach().var() + 1e-6) loss_res2 = F.mse_loss(S2, P2 + S1) / (S2.detach().var() + 1e-6) res_loss = loss_res1 + loss_res2# 下采样一致性损失 g1, g2 = pair_downsampler(output) cons_loss = F.l1_loss(S1, P1 + g1) + F.l1_loss(S2, P2 + g2)# HVI 空间损失 final_hvi = self.trans.HVIT(final) gt_hvi = self.trans.HVIT(gt) hvi_l1 = F.l1_loss(final_hvi, gt_hvi) hvi_edge = edge_loss(final_hvi, gt_hvi) return [l1_loss, ssim_loss, res_loss, cons_loss, hvi_l1, hvi_edge] def forward(self, final, gt, output, S1, P1, S2, P2):# 获取所有基础损失项 losses = self.compute_losses(final, gt, output, S1, P1, S2, P2)# 应用不确定性加权 total_loss = 0.0 for i, loss in enumerate(losses): precision = torch.exp(-self.log_vars[i]) total_loss += precision * loss + self.log_vars[i]# 返回总损失和详细损失项(用于监控) loss_details = { 'total': total_loss, 'l1': losses[0], 'ssim': losses[1], 'res': losses[2], 'cons': losses[3], 'hvi_l1': losses[4], 'hvi_edge': losses[5], 'log_vars': self.log_vars} return total_loss, loss_details主要改进点说明:
- 模块化设计:
 
# 初始化方式变化 trans = RGB_HVI().to(device) criterion = UncertaintyWeightedLoss(trans).to(device)# 前向计算变化 total_loss, loss_details = criterion(final, gt, output, S1, P1, S2, P2)
- 动态权重机制:
 
- 每个损失项自动获得权重:weight = exp (-log_var)
 - 包含正则项:log_var 防止方差无限增大
 - 初始权重范围:exp (-3)=0.05 ~ exp (-1)=0.37
 
- 训练监控增强:
 
# 在训练循环中添加监控 writer.add_scalars('Loss/Train', { 'total': loss_details['total'].item(), 'l1': loss_details['l1'].item(), 'ssim': loss_details['ssim'].item(), 'res': loss_details['res'].item(), 'cons': loss_details['cons'].item(), 'hvi_l1': loss_details['hvi_l1'].item(), 'hvi_edge': loss_details['hvi_edge'].item() }, total_iter)# 监控不确定性参数 for i, log_var in enumerate(loss_details['log_vars']): writer.add_scalar(f'Uncertainty/var_{i}', torch.exp(log_var).item(), epoch)
- 优化器调整:
 
# 需要将损失参数加入优化器 optimizer = optim.Adam( list(model.parameters()) + list(criterion.parameters()), lr=args.lr, betas=(0.9, 0.999))
- 梯度稳定性处理:
 
# 在训练步骤中添加梯度裁剪 torch.nn.utils.clip_grad_norm_( list(model.parameters()) + list(criterion.parameters()), max_norm=1.0)该实现通过以下方式解决原代码问题:
- 自动平衡机制:
 
- 难任务(高方差项)自动获得低权重
 - 简单任务(低方差项)获得高权重
 - 通过反向传播自动调整权重
 
- 数值稳定性改进:
 
- 使用 log 方差代替直接学习方差,避免除零错误
 - 初始值限制在合理范围
 - 梯度裁剪防止参数爆炸
 
- 可解释性增强:
 
- 通过 TensorBoard 可直接观察每个任务的:
 
- 损失值变化曲线
 - 自适应权重变化(exp (-log_var))
 - 方差参数演化
 实际使用时需要注意:
参数初始化:
# 如果某些任务需要强调,可以调整初始值 nn.init.constant_(self.log_vars[4], -2.0) # 强调 HVI_L1 损失学习率设置:
# 为损失参数设置更小的学习率 optimizer = optim.Adam([ {'params': model.parameters(), 'lr': 1e-4}, {'params': criterion.parameters(), 'lr': 1e-5} ])多阶段训练:
# 初始阶段固定部分损失权重 if epoch < warmup_epochs: criterion.log_vars.requires_grad_(False) else: criterion.log_vars.requires_grad_(True)这种实现方式比手动调整 lambda 参数更科学,尤其在处理类似 HVI 这类新增损失时,能自动平衡其与原有损失项的关系。
# 余弦退火学习率调度器
一代经典的学习率调度器。
模板代码参考如:
# ... 其他导入 ... | |
from torch.optim.lr_scheduler import CosineAnnealingLR  | |
def main():  | |
    # 参数解析 | |
parser = argparse.ArgumentParser("MAI_Denoised_Train")  | |
    # ... 原有参数 ... | |
parser.add_argument('--min_lr', type=float, default=1e-6, help='minimum learning rate for cosine annealing')  | |
args = parser.parse_args()  | |
    # ... 模型初始化 ... | |
    # 初始化优化器和调度器 | |
optimizer = optim.Adam(model.parameters(), lr=args.lr, betas=(0.9, 0.999), weight_decay=1e-6)  | |
scheduler = CosineAnnealingLR(optimizer, T_max=args.epochs, eta_min=args.min_lr)  | |
    # 训练循环 | |
for epoch in range(args.epochs):  | |
        # 训练过程... | |
        # 验证过程... | |
        # 更新学习率 | |
scheduler.step()  | |
        # 记录学习率 | |
current_lr = scheduler.get_last_lr()[0]  | |
writer.add_scalar('Learning Rate', current_lr, epoch)  | |
logging.info(f'Epoch [{epoch+1}/{args.epochs}] Learning Rate: {current_lr:.7f}')  | |
    # ... 后续代码 ... | 
# Optuna 自动化调参
为了使用 Optuna 进行超参数调优,我们需要对代码进行以下主要修改。这里展示关键修改步骤(完整代码见最后):
import optuna from optuna.trial import Trial def objective(trial):# 定义可调参数范围 args = argparse.Namespace( batch_size=trial.suggest_categorical('batch_size', [32, 64, 128]), lr=trial.suggest_float('lr', 1e-6, 1e-3, log=True), lambda_hvi1=trial.suggest_float('lambda_hvi1', 0.001, 0.1), lambda_hvi2=trial.suggest_float('lambda_hvi2', 0.001, 0.1), lambda_rgb1=trial.suggest_float('lambda_rgb1', 0.1, 2), lambda_rgb2=trial.suggest_float('lambda_rgb2', 0.1, 2),# 保留其他固定参数... cuda=True, gpu='cuda:0', epochs=100, # 调试时可减少 epoch save=f'./EXP/{trial.number}' # 为每个 trial 创建独立目录)# 运行训练并返回验证指标 final_psnr = train_model(args, trial)return final_psnr def train_model(args, trial=None):# [原 main () 函数内容在这里重构]# 在验证循环中报告中间结果 if trial: trial.report(val_psnr, epoch) if trial.should_prune(): raise optuna.exceptions.TrialPruned() return best_val_psnr # 返回需要优化的指标 if __name__ == "__main__":# 创建 Optuna study study = optuna.create_study( direction='maximize', sampler=optuna.samplers.TPESampler(), pruner=optuna.pruners.MedianPruner())# 运行优化 study.optimize(objective, n_trials=50, timeout=3600*24)# 输出结果 print("Best trial:") trial = study.best_trial print(f" Value: {trial.value}") print(" Params: ") for key, value in trial.params.items(): print(f" {key}: {value}")主要修改点说明:
参数建议:使用 trial.suggest_* 方法替代原 argparse 参数:
 trial.suggest_categorical('batch_size', [32, 64, 128]) trial.suggest_float('lr', 1e-6, 1e-3, log=True)目标函数:将训练过程封装到目标函数中,返回验证 PSNR 作为优化指标
中间报告:在训练过程中定期报告验证指标,支持提前终止:
 trial.report(val_psnr, epoch) if trial.should_prune(): raise optuna.exceptions.TrialPruned()独立目录:为每个 trial 创建独立的保存目录,避免文件冲突:
 args.save = f'./EXP/{trial.number}'Study 配置:创建优化 study 时指定优化方向(最大化 PSNR)和采样策略:
 study = optuna.create_study(direction='maximize')完整整合后的代码示例:
import optuna from optuna.trial import Trialimport argparseimport torch def main():# 原始训练代码... def objective(trial):# 定义超参数搜索空间 params = { 'batch_size': trial.suggest_categorical('batch_size', [32, 64, 128]), 'lr': trial.suggest_float('lr', 1e-6, 1e-3, log=True), 'lambda_hvi1': trial.suggest_float('lambda_hvi1', 0.001, 0.1), 'lambda_hvi2': trial.suggest_float('lambda_hvi2', 0.001, 0.1), 'lambda_rgb1': trial.suggest_float('lambda_rgb1', 0.5, 2.0), 'lambda_rgb2': trial.suggest_float('lambda_rgb2', 0.5, 2.0), 'lambda_res': trial.suggest_float('lambda_res', 0.5, 2.0), 'lambda_cons': trial.suggest_float('lambda_cons', 0.5, 2.0),}# 固定参数 fixed_params = { 'cuda': True, 'gpu': 'cuda:0', 'epochs': 100, # 调优时 epoch 可以适当减少 'data_dir': '/path/to/data', 'save': f'./EXP/trial_{trial.number}',}# 合并参数 args = argparse.Namespace(**{**params, **fixed_params})# 运行训练 best_psnr = train_with_args(args, trial)return best_psnr def train_with_args(args, trial=None):# [原 main () 函数内容重构到这里]# 初始化模型、数据加载等... best_psnr = 0 for epoch in range(args.epochs):# 训练循环...# 验证循环 with torch.no_grad(): val_psnr = evaluate(model, val_loader)# 向 Optuna 报告中间结果 if trial: trial.report(val_psnr, epoch)# 提前终止 if trial.should_prune(): raise optuna.exceptions.TrialPruned() if val_psnr > best_psnr:best_psnr = val_psnr torch.save(model.state_dict(), f"{args.save}/best_model.pth")return best_psnr if __name__ == "__main__":# 创建 Optuna study study = optuna.create_study( direction='maximize', sampler=optuna.samplers.TPESampler(), pruner=optuna.pruners.MedianPruner(n_startup_trials=5, n_warmup_steps=10))# 运行优化 study.optimize(objective, n_trials=50, timeout=3600*24)# 输出最佳结果 print("Best trial:") trial = study.best_trial print(f" PSNR: {trial.value}") print(" Params: ") for key, value in trial.params.items(): print(f" {key}: {value}")关键优化技巧:
参数范围设置:根据参数性质选择适当的范围:
 trial.suggest_float('lr', 1e-6, 1e-3, log=True) # 对数尺度更适合学习率 trial.suggest_categorical('batch_size', [32, 64, 128])提前终止:使用 MedianPruner 避免资源浪费:
 pruner=optuna.pruners.MedianPruner(n_startup_trials=5, n_warmup_steps=10)并行优化:通过指定 n_jobs 并行运行:
 study.optimize(objective, n_trials=100, n_jobs=4)持久化存储:使用数据库保存进度:
 study = optuna.create_study( storage='sqlite:///optuna.db', study_name='denoising_study', load_if_exists=True)注意事项:
资源管理:调优时适当减少 epoch 数量(如 50-100),最终训练时再用完整 epoch
参数空间:初始搜索使用较宽范围,后期可基于初步结果缩小范围
指标选择:建议使用验证集 PSNR 作为优化目标,而非训练损失
随机种子:为保持可比性,可在每个 trial 中固定随机种子:
 torch.manual_seed(trial.suggest_int('seed', 0, 1000))GPU 内存:注意 batch_size 与 GPU 显存的匹配,建议在 suggest_categorical 中包含可行值
这种集成方式可以在不破坏原有训练逻辑的基础上,系统性地探索超参数空间。最终可以通过 study.best_trial.params 获取最佳参数组合,用于最终模型的训练。
# 混合精度训练
# 梯度累积
通过多次小批量迭代累积梯度,模拟大 Batch Size 的效果,模板代码参考可见下方:
accumulation_steps = 4 # 累积 4 个 batch 的梯度  | |
for i, (inputs, labels) in enumerate(dataloader):  | |
outputs = model(inputs)  | |
loss = criterion(outputs, labels)  | |
loss = loss / accumulation_steps # 损失按累积步数缩放  | |
loss.backward()  | |
if (i + 1) % accumulation_steps == 0:  | |
optimizer.step()  | |
optimizer.zero_grad()  | 
# torchinfo 统计模型的显存占用
参考如下代码:
from torchinfo import summary  | |
model = MyModel().cuda()  | |
summary(model, input_size=(batch_size, 3, 256, 256))  | 
# 显存分析器 memory_profiler
这个工具可以可以统计每行代码的显存变化
from pytorch_memlab import LineProfiler  | |
@profile | |
def train_batch(inputs, labels):  | |
outputs = model(inputs)  | |
loss = criterion(outputs, labels)  | |
loss.backward()  | |
optimizer.step()  | |
# 运行后会打印每行代码的显存变化 | |
train_batch(inputs, labels)  | 
# Dataloader 的 num_workers 设置
num_workers 通常设置为 0, CPU 线程数的 75% , CPU 线程数, CPU 线程数的两倍。
CPU 的线程数计算公式为:线程数 = 逻辑核心数 = 物理核心数 * 单核线程数。
num_workers 很大程度上影响 GPU 的占用率。保持长时间的高 GPU 占用率是高效率训练深度学习的基础。
# 损失为 Nan 的分析
损失值出现 NaN(Not a Number)通常由数值不稳定引起,以下是可能原因及解决方案:
# 1. 输入数据问题
检查数据中的 NaN 或异常值:确保输入数据无缺失或无效值。
 import numpy as np print("NaN in data:", np.isnan(data).any()) print("数据范围:", data.min(), data.max())数据标准化 / 归一化:过大或过小的输入值可能导致梯度爆炸。
 data = (data - data.mean()) / data.std() # 标准化# 2. 学习率过高
- 降低学习率:过大的学习率会导致参数更新不稳定。
 
 optimizer = torch.optim.SGD(model.parameters(), lr=0.01) # 初始学习率设为 0.01 或更小# 3. 损失函数实现问题
避免对零取对数:在交叉熵损失中增加极小值 ε(如 1e-8)。
 loss = -tf.reduce_sum(y_true * tf.math.log(y_pred + 1e-8))使用框架内置函数:如 TensorFlow 的
CategoricalCrossentropy(from_logits=True),避免手动实现中的错误。# 4. 梯度爆炸(前提是你的其他代码得写对)
梯度裁剪:限制梯度最大范数。
# PyTorch 示例 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# TensorFlow 示例 gradients = tape.gradient(loss, model.trainable_variables) gradients, _ = tf.clip_by_global_norm(gradients, 1.0) optimizer.apply_gradients(zip(gradients, model.trainable_variables))# 5. 模型结构问题
- 激活函数与输出层匹配:分类任务最后一层需用 Softmax(或配合
 from_logits=True)。- 权重初始化:使用 He/Xavier 初始化避免初始值过大。
 
# PyTorch 示例 torch.nn.init.kaiming_normal_(layer.weight)# 6. 数值稳定性技巧
- 添加 Batch Normalization:稳定层间输出分布。
 
 model.add(tf.keras.layers.BatchNormalization())- 混合精度训练:使用 FP16 时,开启梯度缩放。
 
 optimizer = tf.keras.mixed_precision.LossScaleOptimizer(optimizer)# 7. 调试步骤
- 小数据集测试:用少量样本过拟合,快速复现问题。
 - 打印中间结果:检查前向传播输出和梯度。
 
# 检查输出层 print("模型输出:", outputs)# 检查梯度 for name, param in model.named_parameters(): if param.grad is not None: print(f"梯度 {name}: {param.grad.norm()}")# 8. 其他可能原因
- 正则化过强:降低 L2 正则化系数。
 - 数据预处理错误:检查标准化时是否除以零(如方差为零的特征)。
 # 总结流程
- 检查输入数据:确保无 NaN 且已标准化。
 - 降低学习率:尝试 0.001 或更低。
 - 验证损失函数:使用内置函数或添加 ε。
 - 梯度裁剪:限制梯度大小。
 - 检查模型结构:激活函数、初始化、添加 BatchNorm。
 - 逐步调试:缩小数据范围,打印中间变量。
 通过以上步骤逐步排查,通常可以定位并解决 NaN 损失问题。
# torchvision.utils.save_image
有个参数叫做 Normalize ,这个将数值映射到 [0,255] 的区间。相关使用说明如下:
当设置normalize=True时:  | |
- 会自动将张量的数值范围从[min, max]线性映射到[0, 255]  | |
- 例如:输入张量范围是[-1, 1],会被映射到0-255  | |
- 例如:输入张量范围是[0, 1],会被映射到0-255(相当于直接乘以255)  | 
torchvision.utils.save_image 保存图像要求图像的数值范围必须是指定范围,即在 [0,1] 或 [0,255] 。如果数据范围在其他区间,则需要保证数据范围符合 torchvision.utils.save_image 的要求,可通过设置 normalize 为 True 解决这个问题。
# 确保 Python 优先加载本地项目的代码而不是 Anaconda 环境中的库
情景: TinyNeuralNetwork 库代码在项目文件夹 Retinexformer 下面, Anaconda 也有一个 TinyNeuralNetwork 库。现在我们在本地更新了 TinyNeuralNetwork 库代码,想要运行更新后的库代码中的 convert.py 代码。这个时候 Python 有可能会在执行新库代码 convert.py 的时候,调用 Anaconda 环境的旧库代码。
一种方法是指定优先级,强制优先加载项目中的本地库:
import sys | |
import os | |
# 获取当前脚本所在目录(TinyNeuralNetwork 文件夹的路径) | |
TINYNN_DIR = os.path.dirname(os.path.abspath(__file__))  | |
# 获取项目根目录(假设 TinyNeuralNetwork 是 Retinexformer 的子目录) | |
PROJECT_ROOT = os.path.dirname(TINYNN_DIR)  | |
# 将本地库路径插入到 sys.path 的最前面 | |
sys.path.insert(0, TINYNN_DIR)  | |
sys.path.insert(0, PROJECT_ROOT)  | |
# 打印验证路径是否正确添加(可选) | |
print("当前 Python 路径:")  | |
for p in sys.path:  | |
print(p)  | 
另一种方式就是在终端执行 convert.py 而不是在 IDE 中运行:
> cd Retinexformer | |
> export PYTHONPATH="$PWD:$PYTHONPATH"  | |
> python convert.py  | 
