先交代下这套深度学习环境的硬件配置:

  • 显卡:技嘉5090纯血版(国外背回来的)
  • CPU:AMD 9950X
  • 主板:华硕 X870E-PLUS WIFI7
  • 固态:三星990pro 2TB
  • 机械:西部数据紫盘 2TB
  • 机箱:安钛克FLUX SE
  • 电源:安钛克NE 1300金
  • 风冷散热:九州风神 阿萨辛4
  • 内存:美商海盗船 DDR5 5200 32G✖️2

最近在跑代码时,接连遇到了三次不同表现的桌面显示问题。起初我以为在 BIOS 里禁用核显就能解决,但实际测试下来发现并非如此。为了方便日后查阅,也希望能给遇到类似问题的朋友一些参考,这里把这三种情况及对应的解决思路记录下来。

情况一:直接掉盘,找不到 GPU 节点

这是比较棘手的一种情况。当时正在跑训练,中途桌面突然消失,机箱风扇维持高转速。通过终端 SSH 连进去后,输入 nvidia-smi 提示找不到显卡设备,表现为典型的 GPU 掉盘。

初步分析,这多半是在负载突变或者供电出现瞬时波动时,触发了主板或者电源的某种保护机制,导致 PCIe 设备被系统断开。针对这种硬件级别的保护掉线,目前最直接的办法只能是硬重启机器。

情况二:能识别显卡,但无信号输入且无桌面

这种情况表现为:开机后 nvidia-smi 能正常找到显卡并输出状态,但是显示器提示无信号输入,使用 ToDesk 远程连接时也显示没有桌面环境。

遇到这种状况,我的处理方式是彻底断电重置:关机,拔掉主机电源线,静置等待一分钟左右,然后再插上电源开机。通常这样操作一次就能恢复正常。推测可能是主板静电或某些电容的残余电荷干扰了显示输出,拔电静置可以帮助主板彻底放电。

情况三:远程有桌面,但本地显示器黑屏

第三种情况有些特殊:本地显示器依然毫无反应(无输入信号),但是通过 ToDesk 远程连接,居然可以正常看到并操作 Ubuntu 桌面。

经过摸索,这主要是 Ubuntu 下的显示服务器配置出现异常,或者显卡驱动与 Wayland 发生了冲突。可以在 ToDesk 里打开终端,尝试重置相关配置:

  1. 清理旧的 xorg 配置文件:
    1
    sudo rm /etc/X11/xorg.conf
  2. 重新生成 NVIDIA 的 xconfig:
    1
    sudo nvidia-xconfig
  3. 关闭 Wayland,强制系统使用 X11 协议。编辑 gdm3 的配置文件:
    1
    sudo nano /etc/gdm3/custom.conf
    找到里面被注释掉的 #WaylandEnable=false 这一行,去掉前面的 # 号,变成 WaylandEnable=false,然后保存退出。
  4. 重启系统:
    1
    sudo reboot
    补充说明:重启的时候,建议给显示器换一个 DP 接口重新插拔一次,通常就能正常点亮了。

总结与后续优化

一开始我猜测是核显和独显在抢占输出通道,所以在 BIOS 里把主板核显禁用了。但事实证明,即使禁用了核显,上述黑屏和掉线问题依然会不定期出现。

综合来看,我觉得 5090 这张卡的瞬态功耗和供电要求确实非常高,即便搭配了 1300W 的金牌电源,依然会在某些特定高负载工况下出现掉线。另外,这种情况我也会更多地考虑散热方面的影响,毕竟高功耗伴随着高发热。后续我打算加装机箱风扇、定期清理灰尘,并把机箱移动到更加通风的位置,因为 1300W 电源在理论上是不可能出现余量不足的。

前言

Git 是开发中绕不开的版本控制工具,但其包含的命令和底层概念非常多。在实际的日常代码管理中,很多时候我们只需要用到它的一小部分核心功能。

为了提高工作效率,我在这里整理了一份自己平时最常用到的 Git 操作清单,主要涵盖了本地代码的暂存、提交、历史回退以及与远程仓库的交互。这份记录以实用为主,方便在忘记具体指令时随时查阅。


一、 初始化与基础配置

在开始管理代码前,第一步需要配置用户信息,这有助于追踪每次提交的代码作者。

1. 配置全局信息

打开终端,运行以下命令:

1
2
git config --global user.name "你的名字"
git config --global user.email "你的邮箱@example.com"

2. 建立本地代码库

让一个目录受到 Git 管理,通常有两种情况:

情况 A:初始化本地新项目
在项目根目录下执行初始化,这会生成一个 .git 隐藏文件夹来管理版本记录:

1
2
cd your_project_folder
git init

情况 B:克隆远程仓库
如果项目已经存在于远端(如 GitHub),直接克隆下来即可,不需要再执行 git init

1
git clone https://github.com/用户名/项目名.git

二、 代码提交流程

写好代码后,需要将其保存到本地仓库的版本记录中。这是一个两步走的过程:先添加到暂存区,再正式提交。

1. 暂存代码更改

将当前目录下的所有文件变更放入暂存区:

1
2
# 点号代表当前目录所有更改
git add .

2. 生成提交版本

为这次变更附上说明,并生成一个固定的版本记录:

1
git commit -m "新增了烟雾分割的数据预处理脚本"

💡 个人体会:养成写清晰 Commit Message 的习惯在后期回顾代码或与他人协作时能省下很多沟通成本。


三、 版本查看与回退

在开发过程中经常会遇到代码改错或者需要查看过往修改逻辑的情况,这时候就需要用到版本回退功能。

1. 查看提交历史

获取简略的提交记录列表(主要为了获取版本对应的哈希值):

1
git log --oneline

2. 执行回退操作

根据不同情况选择不同的回退指令:

  • 撤销当前工作区尚未 commit 的更改(放弃最近的修改,恢复到上一次提交时的状态):

    1
    2
    3
    4
    # 危险操作,未提交的代码将直接覆盖
    git checkout .
    # 或者使用新版本推荐的命令
    git restore .
  • 彻底回退到历史的某个特定版本

    1
    2
    # 回退到指定的 commit_id,并抛弃该版本之后的所有记录(需谨慎使用)
    git reset --hard <commit_id>

四、 分支管理与代码合并

当开发一个独立的新功能,或者需要修复一个特定的 Bug 时,新建分支可以有效防止未经验证的代码污染主线环境。

1. 创建并切换分支

1
2
3
4
5
# 新建 dev-feature 分支并立刻切换
git checkout -b dev-feature

# 在较新的 Git 版本中,更推荐使用 switch 语义
git switch -c dev-feature

2. 在分支中提交代码

在独立分支中的操作和主分支一致:

1
2
git add .
git commit -m "完成了验证集代码的编写"

3. 将分支合并到主线

新功能开发并测试稳定后,需要将其合并回主线分支(通常为 mainmaster):

1
2
3
4
5
# 切换回主分支
git checkout main

# 将指定开发分支的代码合并到当前分支
git merge dev-feature

五、 与远程云端同步

本地代码完成迭代后,通常需要推送到远程仓库进行备份或协作。

1. 关联远程仓库 (针对 git init 的项目)

如果是手动初始化的项目,需要先跟 GitHub 等平台的空仓库绑定关系:

1
git remote add origin https://github.com/用户名/你的仓库名.git

2. 推送本地代码到远端

初次推送一个新分支时,需要加上 -u 参数来建立本地和远程分支的关联:

1
git push -u origin main

绑定完成后,后续的常规推送就变得非常简单了:

1
2
git commit -m "日常代码更新"
git push

掌握了这套基础操作,基本就能应对单人开发以及小规模团队协作的大部分版本控制需求了。找到你想回去的那个版本的“哈希值(一串黄色字符)”:

1
git log --oneline

2. 代码回退

通常有两种回退需求:

  • 撤销还没有 commit 的修改(让代码回到上一次提交的状态):

    1
    2
    3
    4
    # 丢弃工作区所有的修改(危险操作,未保存的代码将丢失!)
    git checkout .
    # 或者更现代的命令
    git restore .
  • 彻底回退到历史的某个 commit

    1
    2
    # 回退到指定的 commit_id,并且丢弃这之后的所有修改(极度危险,确认无误再用)
    git reset --hard <commit_id>

四、 平行宇宙:分支与合并

什么时候需要新分支?
当你正在开发一个新功能(或者修复一个 Bug),但不希望影响现有的主线稳定代码时,就需要切出一个新分支。在这个独立的分支里,你可以随便改,改坏了也没事。

1. 创建并切换到新分支

1
2
3
4
5
# 创建一个名为 dev-feature 的新分支,并立刻切换过去
git checkout -b dev-feature

# (注:较新的 Git 版本更推荐使用 switch 命令)
git switch -c dev-feature

2. 在分支上开发与提交

在新分支上,一切操作和之前一样:

1
2
git add .
git commit -m "完成了新功能开发"

3. 合并分支

功能开发完毕且测试通过后,你需要把它合并回主分支(通常叫 mainmaster):

1
2
3
4
5
# 第一步:先切换回主分支
git checkout main

# 第二步:把开发分支合并进当前所在的(主)分支
git merge dev-feature

五、 云端同步:推送到 GitHub

本地的代码玩得再溜,最终也是要推送到远程(如 GitHub)备份或与他人共享的。

1. 关联远程仓库(仅针对 git init 初始化的项目)

如果是 git clone 下来的项目,它已经默认关联了远程仓库,这一步可以跳过。如果是你自己 git init 创建的本地项目,需要先跟 GitHub 上的空仓库建立关联:

1
git remote add origin https://github.com/用户名/你的仓库名.git

2. 推送本地代码

把本地的 main 分支推送到远程仓库的 main 分支(第一次推送通常需要加 -u 参数绑定):

1
git push -u origin main

之后日常开发中,只要分支绑定好了,推送只需极其简单的两步走:

1
2
git commit -m "日常代码更新"
git push

最近在处理烟雾分割任务时,发现现有的语义分割评估指标在应对特定分布(如目标极其微小、类别极度不平衡)时存在一些统计上的盲区。为了更客观地衡量模型性能,我对常用的评价指标进行了梳理,并记录了在 MMSegmentation 框架下实现自定义烟雾分割评价指标的过程。

一、通用语义分割评价指标复习

在语义分割中,预测结果通常被视为逐像素的分类问题。因此,评价指标主要围绕混淆矩阵展开:

  • TP (True Positive):正确预测为正样本的像素数。
  • TN (True Negative):正确预测为负样本的像素数。
  • FP (False Positive):错误预测为正样本的像素数。
  • FN (False Negative):错误预测为负样本的像素数。

1. 像素准确率 (Pixel Accuracy, PA)

这是最直观的指标,表示预测正确的像素在总像素中的占比。

PA=i=0kpiii=0kj=0kpij=TP+TNTP+TN+FP+FNPA = \frac{\sum_{i=0}^{k} p_{ii}}{\sum_{i=0}^{k} \sum_{j=0}^{k} p_{ij}} = \frac{TP + TN}{TP + TN + FP + FN}

2. 平均像素准确率 (mean Pixel Accuracy, mPA)

分别计算每个类别的像素准确率,然后再求取所有类别的平均值。

mPA=1k+1i=0kpiij=0kpijmPA = \frac{1}{k+1} \sum_{i=0}^{k} \frac{p_{ii}}{\sum_{j=0}^{k} p_{ij}}

3. 交并比 (Intersection over Union, IoU)

语义分割中最常用的核心指标,用于衡量特定类别的预测结果与真实标签(Ground Truth)交集和并集的比值。

IoU=TPTP+FP+FNIoU = \frac{TP}{TP + FP + FN}

4. 平均交并比 (mean Intersection over Union, mIoU)

计算所有类别的 IoU,并取综合平均值,以兼顾各类别目标区域的精准度与完整度。

mIoU=1k+1i=0kTPiTPi+FPi+FNimIoU = \frac{1}{k+1} \sum_{i=0}^{k} \frac{TP_i}{TP_i + FP_i + FN_i}


二、面向特定任务(烟雾分割)的评价指标

在烟雾分割等特定场景下,评价指标的计算逻辑通常会有两点区别:
第一,评价指标主要只针对“烟雾”这一个类别进行考察;
第二,为了应对数据分布的不平衡,往往会采用 Image-level(图像级别)的平均机制,而不是全局像素累加。

参考《Deep Smoke Segmentation》和《FoSp: Focus and Separation Network for Early Smoke Segmentation》等论文,我们通常使用以下五个核心指标:mIoU, mFbeta, mPrecision, mRecall, mMse

💡 概念澄清:前缀 “m” 的特殊含义
在此场景下,m 通常代表 mean(所有图像分数的均值),这与通用指标中代表的“所有类别的均值”有所区别。
其计算流程为:先在测试集的每一张图像上独立计算得出指标数值,最后将所有图像的得分加和并除以图像总数 nn

1. 图像级别核心指标的计算

对于单张图像,我们对比预测结果与 GT,得出单图级别的 TPTPFPFPFNFN

  • Precision (精确率): Precision=TPTP+FPPrecision = \frac{TP}{TP + FP}
  • Recall (召回率): Recall=TPTP+FNRecall = \frac{TP}{TP + FN}
  • IoU (交并比): IoU=TPTP+FP+FNIoU = \frac{TP}{TP + FP + FN}
  • Fbeta (FβF_\beta): 结合精确率和召回率,通常定义为 Fβ=(1+β2)×Precision×Recallβ2×Precision+RecallF_\beta = \frac{(1 + \beta^2) \times Precision \times Recall}{\beta^2 \times Precision + Recall}

计算完单图分数后,对数据集中的 nn 张图像求算术平均:

mIoU=1ni=1nIoUimIoU = \frac{1}{n} \sum_{i=1}^{n} IoU_i

2. 均方误差 (mMse) 指标

在对比网络输出的概率图时,mMse (mean Mean Squared Error) 可以直观反映概率分布的偏离程度。具体定义在不同研究中略有差异:

  • 常规设定:将网络输出经过 Sigmoid 激活并通过阈值处理后的二值化图,与二值化标签进行均方误差计算。
  • 变体使用:也有直接利用 [0,1][0,1] 连续概率图进行均方差计算,或者采用均方根误差 (RMSE)。

无论细节如何,其整体逻辑均是先计算单图均方误差,再求全集的均值

mMse=1ni=1nMseimMse = \frac{1}{n} \sum_{i=1}^{n} Mse_i


三、Global IoU 与 Image-level IoU 的差异分析

在学习过程中,我重点对比了全局平均 (Global Average) 与图像平均 (Image-level) 这两种统计方式的差异。在烟雾检测(特别是存在大量极小面积烟雾图像的数据集)中,如果使用默认的 Global IoU 计算方式,可能会掩盖模型在小目标上的真实表现。

统计方式的区别

  1. 全局计算 (Global):将整个数据集中所有的交集像素和并集像素进行统一累加,最后进行一次比值计算。

    IoUglobal=i=1nii=1niIoU_{global} = \frac{\sum_{i=1}^{n} 交集_i}{\sum_{i=1}^{n} 并集_i}

  2. 图像级别计算 (Image-level):如前文所述,先计算每张图像的 IoU 分数,再求取这些分数的平均值。

    IoUimagelevel=1ni=1n(ii)IoU_{image-level} = \frac{1}{n} \sum_{i=1}^{n} \left( \frac{交集_i}{并集_i} \right)

一个极端的测试用例

假设一个微型测试集只包含 2 张照片:

  • 图片 A (大面积目标):真实像素 10000。预测交集 9000,并集 11000。单图 IoUA81.8%IoU_A \approx 81.8\%
  • 图片 B (极小面积目标):真实像素 100。模型漏检,交集 0,并集 100。单图 IoUB=0%IoU_B = 0\%

对比结算结果:

  • Global 计算法9000+011000+10081.08%\frac{9000 + 0}{11000 + 100} \approx 81.08\%
  • Image-level 计算法81.8%+0%2=40.9%\frac{81.8\% + 0\%}{2} = 40.9\%

可以看到,Global 方法下图片 B 的漏检误差被图片 A 庞大的像素基数所掩盖。而 Image-level 方法能更直观地体现出模型在图片 B 上的检测失效,这正是为什么在类别极度不平衡或目标大小差异巨大的任务中,Image-level 评估能更客观地反映模型的平均处理能力。


四、MMSegmentation 自定义评估代码编写

由于 MMSegmentation 1.x 默认采用像素级全局累计的评价方式,为了引入 Image-level 的 Macro-Average 以及 RMSE/MSE 误差记录,需要自定义一套评价体系。eep Smoke Segmentation):将网络输出经过 Sigmoid 函数激活并通过阈值化处理后的二值化预测图**,与二值化的 GT 标签之间进行均方误差计算。

  • 变体 1(非阈值化):部分论文直接将仅经过 Sigmoid 激活后、值域在 [0,1][0,1] 之间的连续预测概率图,直接与二值化 GT 之间计算平方差。
  • 变体 2(均方根误差):也有一些论文将此指标定义为 RMSE,即在均方误差的基础上多开了一个根号。

但无论哪种变体,计算逻辑一律是先在每一张图片上独立求出单图误差(Mse),然后再除以 nn 取所有图像的均值

mMse=1ni=1nMseimMse = \frac{1}{n} \sum_{i=1}^{n} Mse_i


三、避坑:全局平均 (Global IoU) vs 图像平均 (Image-level IoU) 的致命差别

在处理烟雾(尤其是早期微小烟雾)这种占比呈现极度长尾分布的场景时,为什么必须强调使用前面提到的 Image-level 平均机制 呢?

因为如果沿用很多代码库(如 MMSegmentation 原生逻辑)默认的“全局平均精度(Global Average)”计算方式,会造成性能指标“造假虚高”。

数学逻辑的本质差异

  1. 全局平均 (Global 计算):先将整个测试集所有图片上的交集所有图片上的并集各自揉在一起累加,最后做唯一一次除法。

    IoUglobal=i=1nii=1niIoU_{global} = \frac{\sum_{i=1}^{n} 交集_i}{\sum_{i=1}^{n} 并集_i}

  2. 单图平均 (Image-level 计算):如前文所述,先算出单图分数,再把 nn 张图的分数相加除以 nn

    IoUimagelevel=1ni=1n(ii)IoU_{image-level} = \frac{1}{n} \sum_{i=1}^{n} \left( \frac{交集_i}{并集_i} \right)

实例说明

假设测试集只有极端的 2 张照片:

  • 图片 A (大片浓烟):真实像素 10,000。模型预测交集 9,000 像素,并集 11,000 像素。单图 IoUA=90001100081.8%IoU_A = \frac{9000}{11000} \approx \textbf{81.8\%}
  • 图片 B (极小烟雾):真实像素 100 像素。网络完全漏检预测全黑,交集 0,并集 100 像素。单图 IoUB=0100=0%IoU_B = \frac{0}{100} = \textbf{0\%}

如果用这两种方式结算,结果截然不同:

  • 全局模式:把面积数值直接合并 9000+011000+10081.08%\frac{9000 + 0}{11000 + 100} \approx \textbf{81.08\%}
  • 单图模式:百分比分数算术平均 81.8%+0%2=40.9%\frac{81.8\% + 0\%}{2} = \textbf{40.9\%}

直观结论
全局模式下,模型在图片 B 上的致命漏检,被图片 A 庞大的像素绝对数量给“稀释和掩盖”了,制造出高达 81% 的幻象。而 Image-level 模式断崖式坠落到 40.9%,反而更为客观、真实地反映了模型在全数据集上的平均检测性能。这也就说明了烟雾分割领域为何必须严格遵从对单张图进行独立评价再求平均。


四、MMSegmentation 1.x 烟雾分割评价指标代码实现

在 MMSegmentation 1.x 版本中,为了实现上述的 Image-level 评价机制 以及加入 mMse 等指标,我们需要自定义一个评价指标类。它继承自官方的 IoUMetric,在保留全局统计功能的同时,通过覆写 processcompute_metrics,额外计算单图平均(Macro-Average)以及基于概率图的 RMSE/MSE 误差。

操作步骤

  1. 在工程目录下创建 mmseg/evaluation/metrics/smoke_seg_metric.py 文件。
  2. 将以下代码填入文件中,不要忘记添加模块注册功能。
  3. 在你的网络主配置文件的 val_evaluatortest_evaluator 字段中指向 SmokeSegMetric 以启用它。

具体实现代码如下

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
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
from collections import OrderedDict
from typing import Dict, List, Optional, Sequence

import numpy as np
import torch
from mmengine.dist import is_main_process
from mmengine.logging import MMLogger, print_log
from mmengine.utils import mkdir_or_exist
from PIL import Image
from prettytable import PrettyTable

from mmseg.registry import METRICS
from .iou_metric import IoUMetric


@METRICS.register_module()
class SmokeSegMetric(IoUMetric):
"""烟雾分割评价指标。

在官方 IoUMetric(pixel-level 全局累积)的基础上,新增 image-level
Macro-Average 指标:逐图计算 IoU、Fβ、Precision、Recall,然后对
所有图片取均值。两种指标同时输出。

Image-level 指标会**分别计算每个类**(如 _background_ 和 smoke),
最终输出每个类的 Img_IoU、Img_Fbeta、Img_Precision、Img_Recall。

Args:
ignore_index (int): 评估时忽略的标签索引。默认: 255。
iou_metrics (list[str] | str): 官方 pixel-level 指标类型,
支持 'mIoU'、'mDice'、'mFscore'。默认: ['mIoU']。
nan_to_num (int, optional): 如果指定,NaN 值将被替换为该数值。
默认: None。
beta (int): Fβ 的 β 值,决定 Recall 在综合得分中的权重。
β=1 时为 F1-Score,β=2 时更侧重 Recall。默认: 1。
collect_device (str): 分布式训练时收集结果的设备。默认: 'cpu'。
output_dir (str): 输出预测结果的目录。默认: None。
format_only (bool): 仅格式化结果不进行评估。默认: False。
prefix (str, optional): 指标名称前缀。默认: None。
"""

def __init__(self,
ignore_index: int = 255,
iou_metrics: List[str] = ['mIoU'],
nan_to_num: Optional[int] = None,
beta: int = 1,
collect_device: str = 'cpu',
output_dir: Optional[str] = None,
format_only: bool = False,
prefix: Optional[str] = None,
**kwargs) -> None:
super().__init__(
ignore_index=ignore_index,
iou_metrics=iou_metrics,
nan_to_num=nan_to_num,
beta=beta,
collect_device=collect_device,
output_dir=output_dir,
format_only=format_only,
prefix=prefix,
**kwargs)

def process(self, data_batch: dict,
data_samples: Sequence[dict]) -> None:
"""处理一个 batch 的数据。

在父类 IoUMetric.process() 的基础上,额外从 seg_logits(sigmoid
后的概率图)计算每张图的 RMSE,一并存入 self.results。

每张图的结果为 8 元组:
(area_intersect, area_union, area_pred_label, area_label, rmse_prob, mse_prob, rmse_binary, mse_binary)

Args:
data_batch (dict): 一个 batch 的输入数据。
data_samples (Sequence[dict]): 一个 batch 的模型输出。
"""
num_classes = len(self.dataset_meta['classes'])
for data_sample in data_samples:
pred_label = data_sample['pred_sem_seg']['data'].squeeze()
# format_only 模式不计算指标
if not self.format_only:
label = data_sample['gt_sem_seg']['data'].squeeze().to(
pred_label)
# 计算 intersect_and_union(与父类一致)
iau = self.intersect_and_union(
pred_label, label, num_classes, self.ignore_index)

# ========== 1. 从概率图计算 RMSE_Prob 和 MSE_Prob ==========
seg_logits = data_sample['seg_logits']['data'].squeeze()
gt_float = label.float()
# seg_logits 在 out_channels==1 时已经是 sigmoid 后的概率图
mse_prob = torch.mean((seg_logits - gt_float) ** 2).item()
rmse_prob = torch.sqrt(torch.tensor(mse_prob)).item()

# ========== 2. 从二值化预测图计算 RMSE_Binary 和 MSE_Binary ==========
# 按照 deep-smoke-segmentation 等计算二值化后的误差
# 注意:直接使用已经根据 threshold(如0.4) 生成好的二值图 pred_label
pred_binary = pred_label.float()
mse_binary = torch.mean((pred_binary - gt_float) ** 2).item()
rmse_binary = torch.sqrt(torch.tensor(mse_binary)).item()

# 存储 8 元组
self.results.append((*iau, rmse_prob, mse_prob, rmse_binary, mse_binary))

# 保存预测结果到文件(与父类一致)
if self.output_dir is not None:
basename = osp.splitext(osp.basename(
data_sample['img_path']))[0]
png_filename = osp.abspath(
osp.join(self.output_dir, f'{basename}.png'))
output_mask = pred_label.cpu().numpy()
if data_sample.get('reduce_zero_label', False):
output_mask = output_mask + 1
output = Image.fromarray(output_mask.astype(np.uint8))
output.save(png_filename)

def compute_metrics(self, results: list) -> Dict[str, float]:
"""计算评价指标。

同时输出:
1. 官方 pixel-level 指标(通过父类逻辑)
2. image-level Macro-Average 指标(逐图计算后取均值)
3. RMSE 指标(逐图计算后取均值)

Args:
results (list): 每张图片的处理结果,每项为
(area_intersect, area_union, area_pred_label, area_label, rmse_prob, mse_prob, rmse_binary, mse_binary)
的 8 元组。

Returns:
Dict[str, float]: 所有评价指标的字典。
"""
logger: MMLogger = MMLogger.get_current_instance()
if self.format_only:
logger.info('results are saved to '
f'{self.output_dir}')
return OrderedDict()

# ========== 第一部分:官方 pixel-level 指标 ==========
# 转换数据格式:list of 8-tuples → tuple of lists
results_tuple = tuple(zip(*results))
assert len(results_tuple) == 8

total_area_intersect = sum(results_tuple[0])
total_area_union = sum(results_tuple[1])
total_area_pred_label = sum(results_tuple[2])
total_area_label = sum(results_tuple[3])
rmse_prob_list = list(results_tuple[4])
mse_prob_list = list(results_tuple[5])
rmse_binary_list = list(results_tuple[6])
mse_binary_list = list(results_tuple[7])

# 使用父类的静态方法计算 pixel-level 指标
ret_metrics = self.total_area_to_metrics(
total_area_intersect, total_area_union, total_area_pred_label,
total_area_label, self.metrics, self.nan_to_num, self.beta)

class_names = self.dataset_meta['classes']
num_classes = len(class_names)

# 汇总表(pixel-level)
ret_metrics_summary = OrderedDict({
ret_metric:
np.round(np.nanmean(ret_metric_value) * 100, 2)
for ret_metric, ret_metric_value in ret_metrics.items()
})
metrics = dict()
for key, val in ret_metrics_summary.items():
if key == 'aAcc':
metrics[key] = val
else:
metrics['m' + key] = val

# 每类表(pixel-level)
ret_metrics.pop('aAcc', None)
ret_metrics_class = OrderedDict({
ret_metric:
np.round(ret_metric_value * 100, 2)
for ret_metric, ret_metric_value in ret_metrics.items()
})
ret_metrics_class.update({'Class': class_names})
ret_metrics_class.move_to_end('Class', last=False)
class_table_data = PrettyTable()
for key, val in ret_metrics_class.items():
class_table_data.add_column(key, val)

print_log('per class results (pixel-level):', logger)
print_log('\n' + class_table_data.get_string(), logger=logger)

# ========== 第二部分:image-level Macro-Average 指标 ==========
# 提取前 4 项(intersect_and_union 结果)用于 image-level 指标
iau_results = [(r[0], r[1], r[2], r[3]) for r in results]
img_metrics = self._compute_image_level_metrics(
iau_results, num_classes)

# RMSE 和 MSE(逐图均值)
mean_rmse_prob = np.round(np.mean(rmse_prob_list), 4)
mean_mse_prob = np.round(np.mean(mse_prob_list), 4)
mean_rmse_binary = np.round(np.mean(rmse_binary_list), 4)
mean_mse_binary = np.round(np.mean(mse_binary_list), 4)

# 打印 image-level 表格
img_table_data = PrettyTable()
img_table_data.add_column('Class', list(class_names))
for key in ['Img_IoU', 'Img_Fbeta', 'Img_Prec', 'Img_Rec']:
img_table_data.add_column(
key,
[np.round(img_metrics[key][c] * 100, 2)
for c in range(num_classes)])

print_log('per class results (image-level macro-average):', logger)
print_log('\n' + img_table_data.get_string(), logger=logger)
print_log(f'RMSE_Prob: {mean_rmse_prob}', logger)
print_log(f'MSE_Prob: {mean_mse_prob}', logger)
print_log(f'RMSE_Binary: {mean_rmse_binary}', logger)
print_log(f'MSE_Binary: {mean_mse_binary}', logger)

# 汇总 image-level 指标到返回字典(按类分别输出)
for key in ['Img_IoU', 'Img_Fbeta', 'Img_Prec', 'Img_Rec']:
for c in range(num_classes):
class_key = f'{class_names[c]}.{key}'
metrics[class_key] = np.round(
img_metrics[key][c] * 100, 2)
metrics['RMSE_Prob'] = mean_rmse_prob
metrics['MSE_Prob'] = mean_mse_prob
metrics['RMSE_Binary'] = mean_rmse_binary
metrics['MSE_Binary'] = mean_mse_binary

return metrics

def _compute_image_level_metrics(
self,
results: list,
num_classes: int) -> Dict[str, np.ndarray]:
"""逐图计算 IoU、Fβ、Precision、Recall,然后取均值。

模仿 FoSp 的处理方式:
- 所有图片都参与均值计算,不跳过任何图
- 分母为 0 时用 max(1, x) 保护,使该项结果为 0
- 最终除以总图片数取算术平均

Args:
results (list): 每张图的 4 元组列表。
num_classes (int): 类别数量。

Returns:
Dict[str, np.ndarray]: 每个类的 image-level 均值指标,
shape = (num_classes,)。
"""
n_images = len(results)
beta = self.beta

# 累积每个类的指标和,shape = (num_classes,)
iou_sum = np.zeros(num_classes)
precision_sum = np.zeros(num_classes)
recall_sum = np.zeros(num_classes)
fbeta_sum = np.zeros(num_classes)

for area_intersect, area_union, \
area_pred_label, area_label in results:
for c in range(num_classes):
intersect = area_intersect[c].item()
union = area_union[c].item()
pred = area_pred_label[c].item()
label = area_label[c].item()

# IoU:交集 / 并集(union 不可能为 0,因为每张图中
# 每个类在 GT 或预测中至少有一方存在像素)
iou_sum[c] += intersect / union

# Precision:交集 / max(1, 预测面积)
p = intersect / max(1, pred)
precision_sum[c] += p

# Recall:交集 / max(1, GT面积)
r = intersect / max(1, label)
recall_sum[c] += r

# Fβ = (1+β²) * P * R / max(β²*P + R, 1e-3)
denom = (beta ** 2) * p + r
fbeta_sum[c] += (1 + beta ** 2) * p * r / max(denom, 1e-3)

# 除以总图片数取均值
img_metrics = {
'Img_IoU': iou_sum / n_images,
'Img_Fbeta': fbeta_sum / n_images,
'Img_Prec': precision_sum / n_images,
'Img_Rec': recall_sum / n_images,
}

return img_metrics

在配置基于 RTX 5090 的深度学习环境时,由于底层硬件架构较新,框架版本的匹配需要特别注意。本文主要记录了在 RTX 5090 上配置 MMSegmentation 环境的过程,以及如何使用自定义数据集跑通基础的训练和测试流程。

一、基础环境配置

RTX 5090 对 CUDA 版本有一定要求,经过测试,以下版本组合运行较为稳定:

  • CUDA: 12.8 (5090 最低支持 12.8 版本的 CUDA)
  • PyTorch: 2.8.0
  • Python: 3.10 (建议 3.10.x 或 3.12.x)

💡 个人习惯:建立基础克隆环境
由于目前部分国内镜像源可能还没有完全同步最新的 GPU 版 PyTorch,我通常会先建一个只包含上述核心组件的“基础环境”。后续新建项目时直接克隆这个基础环境,可以避免重复下载和安装。

1
conda create -n new_env_name --clone base_5090_env

二、框架选择:使用 OneDL-MMSegmentation

官方的 OpenMMLab 库 mmsegmentation 已经有较长一段时间未更新,最新的 1.x 版本在适配最新的硬件环境(如 CUDA 12.8)和 PyTorch 2.x 时会遇到不少兼容性问题。虽然可以通过手动修改配置、从源码编译 mmcv 来强行适配,但维护成本较高。

在调研后,我选择了第三方维护的复刻版本:onedl-mmsegmentation。该库对最新的 PyTorch 2.x 提供了较好的支持。

*图 1:官方主页中的相关兼容性说明。*

三、完整的安装与配置流程

1. 创建环境与安装 PyTorch

1
2
3
4
5
conda create -n mmseg python=3.10 -y
conda activate mmseg

# 注意:需通过官方源安装指定 CUDA 12.8 版本的 PyTorch
pip install torch==2.8.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128

2. 安装基础依赖包

1
2
pip install -U onedl-mim
mim install onedl-mmengine

3. 安装 MMCV

在整个配置过程中,mmcv 的安装是比较容易出错的一步。主要有两种方案:

方案 A:使用预编译 Wheel 包安装 (推荐)

最理想的方式是使用 mim 自动安装预编译包:

1
mim install onedl-mmcv==2.3.2

⚠️ 编译提示
如果在终端日志中看到 Building wheel from source,说明系统未能匹配到合适的预构建包,正在尝试本地编译。这种情况往往由于缺少 C++ 扩展编译环境,导致后续调用 mmcv._ext 时报错。

为了避免编译错误,可以根据 OneDL-MMCV 官方文档 寻找对应的预编译包(如 Python 3.10 + CUDA 12.8 + PyTorch 2.8.0)。如果自动匹配失败,建议直接通过获取到的 .whl 链接进行安装:

*图 2:官方发布的预编译包列表。*

*图 3:复制对应版本的下载链接。*
1
2
# 示例:通过复制的 .whl 文件绝对链接直接安装
pip install https://mmwheels-bucket.onedl.ai/cu128-torch280/onedl-mmcv/onedl_mmcv-2.3.3-cp310-cp310-manylinux_2_34_x86_64.whl

方案 B:从源代码编译

如果无法使用预编译包,也可以从源码编译,建议将代码克隆到工程的同级目录:

1
2
3
4
5
6
7
8
9
10
11
git clone https://github.com/open-mmlab/mmcv.git
cd mmcv
git checkout v2.1.0 # 切换到所需的分支

# 配置环境变量以编译 C++ 算子
export FORCE_CUDA=1
export MMCV_WITH_OPS=1

pip install -r requirements.txt
python setup.py build_ext --inplace
pip install -e .

4. 从源码安装 OneDL-MMSegmentation

确认 mmcv 正确安装并能导入 mmcv._ext 后,即可安装主框架:

1
2
3
git clone -b main https://github.com/vbti-development/onedl-mmsegmentation.git
cd onedl-mmsegmentation
pip install -v -e .

四、测试与验证环境

完成上述步骤后,可以使用官方提供的 Demo 进行推理测试,以验证环境是否正常配置:

1
2
3
4
5
6
7
8
9
10
cd onedl-mmsegmentation

# 获取测试用的配置和模型权重
mim download mmsegmentation --config pspnet_r50-d8_4xb2-40k_cityscapes-512x1024 --dest .

# 运行单张图片推理
python demo/image_demo.py demo/demo.png \
configs/pspnet/pspnet_r50-d8_4xb2-40k_cityscapes-512x1024.py \
pspnet_r50-d8_512x1024_40k_cityscapes_20200605_003338-2966598c.pth \
--device cuda:0 --out-file result.jpg

如果当前目录生成了带有分割蒙版的 result.jpg,说明环境的安装配置已经初步完成。


五、在内置数据集 (ADE20K) 上的训练尝试

在切入自定义数据前,我先尝试用内置的 ADE20K 数据集走了一遍完整的训练和测试流程,以熟悉框架的调用逻辑。

  1. 准备数据:按照官方文档指引,解压 ADE20K 并放至 data/ade 目录。
  2. 启动训练:尝试使用 Segformer 模型。得益于 5090 充足的显存,可以将 batch size 适当调大以加速验证。
1
2
3
4
5
6
conda activate mmseg

# 覆盖默认超参数配置
python tools/train.py configs/segformer/segformer_mit-b0_8xb2-160k_ade20k-512x512.py \
--work-dir work_dirs/segformer_mit-b0_ade20k \
--cfg-options train_dataloader.batch_size=16 val_dataloader.batch_size=1 train_dataloader.num_workers=8

也可以通过修改迭代参数进行一次快速测试:

1
2
3
python tools/train.py configs/segformer/segformer_mit-b0_8xb2-160k_ade20k-512x512.py \
--work-dir work_dirs/segformer_mit-b0_ade20k_fast \
--cfg-options train_dataloader.batch_size=16 val_dataloader.batch_size=1 train_dataloader.num_workers=8 train_cfg.max_iters=40000 train_cfg.val_interval=2000

显卡状态监控

为了观察高负载下的硬件表现,可以在训练期间监控显卡状态:

1
2
3
4
5
6
7
# 方法 1:系统自带
watch -n 1 nvidia-smi

# 方法 2:使用第三方库 nvitop
conda activate base
pip install nvitop
nvitop

模型验证

训练完成后,评估精度并输出预测结果:

1
2
3
4
python tools/test.py configs/segformer/segformer_mit-b0_8xb2-160k_ade20k-512x512.py \
work_dirs/segformer_mit-b0_ade20k/best_mIoU_iter_xxx.pth \
--out work_dirs/segformer_mit-b0_ade20k/results \
--show-dir work_dirs/segformer_mit-b0_ade20k/vis_results

六、跑通自定义数据集:以 SmokeSeg 为例

结合自己课题,这里以开源的烟雾分割数据集 SmokeSeg 为例,记录了如何将其接入 MMSegmentation 框架。

假设数据已转为标准 VOC 格式并存放在项目目录的 data/SmokeSeg 中。

1. 注册新数据集

在 MMSegmentation 1.x 规范中,自定义数据集需继承 BaseSegDataset

  • mmseg/datasets/ 下创建 smoke_voc.py,编写数据集读取逻辑。
  • mmseg/datasets/__init__.py 中导入并暴露该模块以便系统调用。

2. 编写训练配置文件

configs/segformer/ 目录下新建对应 SmokeSeg 的配置文件,例如 segformer_mit-b5_8xb4-160k_smokeseg-515x512.py
(注: 可以直接利用框架的 _base_ 机制继承官方的 SegFormer-B5 配置文件,然后在此基础上仅重写类别数、数据路径等参数。)

3. 执行训练与可视化

调用新配置开始模型训练:

1
python tools/train.py configs/segformer/segformer_mit-b5_8xb4-160k_smokeseg-515x512.py

训练结束后进行测试并可视化:

1
2
3
4
python tools/test.py \
configs/segformer/segformer_mit-b5_8xb4-160k_smokeseg-515x512.py \
你的最优权重文件路径.pth \
--show-dir vis_results_smokeseg/

到这里,整套分割库的本地化配置和运行流程基本走通了。接下来就可以在此框架上开展进一步的调参和对比实验。

最近抽时间把个人博客重新搭建了一遍,依然选择的是 Hexo 配合 GitHub Pages 的经典方案。在搭建过程中,发现很多以前的教程随着软件版本的更迭已经不再适用了。这里将整个流程和遇到的一些报错记录下来,方便以后查阅。

阶段一:基础框架搭建 (Hexo + GitHub Pages)

博客的基础骨架搭建,我主要参考了这篇知乎经验贴:
🔗 参考教程《Hexo + GitHub Pages 搭建个人博客》

💡 操作记录
虽然这篇文章的整体逻辑很清晰,但因为发布时间较早,很多细节已经发生了变化。例如 Node.js 的版本要求、Hexo 包管理的变动,以及目前 GitHub 默认主分支已经从 master 变成了 main。这些差异很容易导致按照原教程执行时出现报错。

在实际操作中,如果遇到版本不兼容导致的报错,建议直接将终端报错信息交给大语言模型去分析,通常能很快定位并解决问题,能省下不少查阅旧资料的时间。


阶段二:配置专属独立域名

默认部署到 GitHub Pages 上的域名格式通常是 用户名.github.io。为了方便记忆和后续的扩展,我为博客绑定了一个专属的独立域名。

🔗 参考教程《GitHub Pages 绑定自定义域名》

核心操作步骤

  1. 域名购买:我在阿里云购买了一个 .top 后缀的域名,相对来说性价比比较高。
  2. DNS 解析配置:进入阿里云控制台,添加一条 CNAME 记录,将其解析指向自己的 GitHub Pages 地址。
  3. GitHub 仓库配置:在 GitHub 博客仓库的 Settings -> Pages -> Custom domain 中填入刚购买的域名,并勾选开启 Enforce HTTPS 以保证访问安全。配置完成后,稍微等待 DNS 解析生效,就可以使用独立域名访问博客了。

阶段三:博客主题美化与自定义

原生的 Hexo 界面比较基础,为了提升页面的可读性和排版效果,通常需要对主题进行一定的自定义。

🔗 参考教程《Hexo 博客主题深度美化指南》

我主要进行的配置和调整包括

  • 更换第三方主题(这里我尝试了经典的 NexT 等几款主流主题)。
  • 配置侧边栏的个人信息、头像以及社交平台链接。
  • 微调了博客的背景和字体配色,以改善正文内容的阅读体验。
  • 补充了专属的网站标签页小图标(Favicon)。

整个流程走下来,算是一次比较完整的前端部署实践。一个功能完善、排版清晰的博客不仅能作为知识库,也是记录学习日常的好工具。

0%