☰
Unet眼底血管分割实战:数据预处理、模型训练与界面封装
2026/9/28 2:23:37 网站建设 项目流程

简介:面向眼底图像分割初学者和医学影像方向课题/毕设人群的UNet综合实训包,配套视频与图文教程,覆盖网络原理讲解、数据准备、训练测试到界面封装全流程。视网膜血管的形态变化能反映多种疾病的早期病理,而传统人工分割费时且主观性强,使用该实训包可将自动分割方案快速落地。资源共259个文件,总大小约123.23MB:188张png眼底图像构成训练与测试样本,py脚本负责训练和预测逻辑,pth为训练好的模型权重,mp4教学视频与pdf图文资料辅助理解,txt/json/xml存放标签、参数配置及项目说明。压缩包内置可视化预测系统,上传眼底图像即可得到血管分割结果,同时提供混淆矩阵csv等评估文件,便于检查性能。已有105人学习下载,适合想系统掌握UNet原理并快速搭建可演示项目的开发者。

1. 为什么眼底血管分割要靠 Unet:这套资源里到底有什么

做眼科影像相关的课题或课程设计时,最劝退的不是模型原理,而是“数据根本没有对齐”。眼底血管图像分割这种任务,公开数据集要自己下载、自己裁剪、自己转 mask,等把数据折腾干净,一周时间就没了。这套 Unet 眼底血管图像分割资源,把数据、代码、模型、界面、视频教程打包在一起,核心解决的是“从零到能演示”的问题:训练集和测试集是处理好的,Unet 训练和测试代码是完整的,还有一个训练好的模型可以直接加载,最后封装成了图形界面,点按钮上传一张眼底图就能出血管分割结果。

这套资源适合三类人:做医学影像相关课程设计的学生,想快速跑通 Unet 并在眼底数据上验证效果的算法工程师,以及需要一套完整 baseline 做论文对比实验的研究生。它包含 Unet 原理讲解、处理好的训练集和测试集、训练测试代码、训练好的模型权重、混淆矩阵结果文件,以及配套的图文和视频教程。下面我从数据组织、训练参数、测试评估、界面封装到避坑,按实际使用顺序拆一遍。

2. Unet 原理与数据处理:为什么它能抠出眼底细血管

2.1 Unet 的编码-解码与跳跃连接:血管分割的关键在于细节保留

Unet 能成为医学图像分割的事实标准,不是因为它结构花哨,而是它的“编码-解码 + 跳跃连接”设计恰好踩中了血管分割的两个痛点。眼底血管是细长结构,一根血管可能只占几个像素宽,传统的 FCN 连续下采样会把细血管直接“吞掉”;而 Unet 在每次下采样后都把对应分辨率的特征图通过跳跃连接传给解码器,让上采样过程中能“找回”边缘细节。

编码器部分就是常见的卷积 + 池化组合,每经过一次下采样,特征图尺寸减半、通道数翻倍,网络看到的是越来越宏观的语义信息;解码器用转置卷积把特征图逐步还原回原图尺寸,同时通过torch.cat把同尺度的编码器特征拼接回来。这种拼接操作等于告诉解码器“宏观位置信息和微观边缘信息我都要”,所以最后输出的分割图里,主血管和末梢毛细血管都能保留。

2.2 数据集目录与 mask 标注:训练前先确认三件事

拿到这套资源后,不要急着跑训练脚本,先花十分钟确认数据组织方式。眼底血管分割数据集的常规组织方式是images目录放眼底原图,masks目录放对应的二值标注图,文件名一一对应。标注图里血管区域是白色(像素值 255),背景是黑色(像素值 0),训练时一般把 mask 转成单通道二值图,白色归一化为 1。

资源里还有一个confusion_matrix.csv文件,这是模型在测试集上统计出的混淆矩阵结果,包含 TP、FP、FN、TN 四个数值。这四个数不是摆设,后面算 Accuracy、Sensitivity、Specificity、Dice、IoU 全靠它,也是你判断模型“能不能用”的第一手证据。

打开数据目录后重点确认三件事:第一,原图和 mask 是否一一对应,文件名是否完全一致;第二,图像尺寸是否统一,如果不统一,代码里有没有 resize 逻辑;第三,mask 是单通道灰度图还是三通道 RGB 图,这决定了训练时读取的方式。我见过太多人在这三个地方翻车——名字对不上导致训练时图跟 mask 错位,训练了一天才发现,血泪经验。

2.3 数据加载与预处理:灰度化、归一化与数据增强怎么写

眼底图像分割的数据预处理比自然图像分割要“轻”,因为眼底图结构相对固定,不需要做复杂的随机裁剪和色彩抖动。常见做法是转灰度图、归一化到[0, 1]、统一 resize 到模型输入尺寸,然后做少量几何增强。

import cv2 import numpy as np from torch.utils.data import Dataset class FundusDataset(Dataset): def __init__(self, img_dir, mask_dir, img_size=(512, 512), augment=False): self.img_paths = sorted(os.listdir(img_dir)) self.mask_dir = mask_dir self.img_size = img_size self.augment = augment def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img_name = self.img_paths[idx] img = cv2.imread(os.path.join(self.img_dir, img_name), cv2.IMREAD_GRAYSCALE) mask = cv2.imread(os.path.join(self.mask_dir, img_name), cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, self.img_size) mask = cv2.resize(mask, self.img_size, interpolation=cv2.INTER_NEAREST) img = img.astype(np.float32) / 255.0 mask = (mask > 127).astype(np.float32) if self.augment: if np.random.rand() > 0.5: img = cv2.flip(img, 1) mask = cv2.flip(mask, 1) if np.random.rand() > 0.5: img = cv2.flip(img, 0) mask = cv2.flip(mask, 0) img = torch.from_numpy(img).unsqueeze(0) mask = torch.from_numpy(mask).unsqueeze(0) return img, mask

这段代码有几个关键参数要注意。img_size我建议设成(512, 512),这是显存占用和分割精度之间的平衡点;INTER_NEAREST是 resize mask 时必须用的插值方式,如果改成线性插值,mask 边缘会出现介于 0 和 1 之间的过渡值,训练时损失函数会混乱;mask > 127是为了兜底处理那些不是纯 0 和纯 255 的标注图。数据增强只用了水平翻转和垂直翻转,因为眼底图的血管方向本来就是随机的,旋转类增强容易把视盘位置搞乱,不建议开。灰度归一化到[0, 1]这一步必须和后面测试、界面预测时保持一致,否则会出现“训练效果好、部署效果差”的玄学问题。

3. 跑通训练脚本:模型定义、损失函数与输出文件

3.1 Unet 模型定义:卷积块、下采样与上采样怎么拼

这套资源里的 Unet 是标准医疗影像版本:四个下采样块、四个上采样块,输入单通道灰度图,输出单通道概率图。核心模块是两个卷积加 BatchNorm 的DoubleConv,下采样用MaxPool2d,上采样用ConvTranspose2d。

import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv = nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_channels=1, out_channels=1): super().__init__() self.enc1 = DoubleConv(in_channels, 64) self.enc2 = DoubleConv(64, 128) self.enc3 = DoubleConv(128, 256) self.enc4 = DoubleConv(256, 512) self.pool = nn.MaxPool2d(2) self.center = DoubleConv(512, 1024) self.up4 = nn.ConvTranspose2d(1024, 512, 2, stride=2) self.dec4 = DoubleConv(1024, 512) self.up3 = nn.ConvTranspose2d(512, 256, 2, stride=2) self.dec3 = DoubleConv(512, 256) self.up2 = nn.ConvTranspose2d(256, 128, 2, stride=2) self.dec2 = DoubleConv(256, 128) self.up1 = nn.ConvTranspose2d(128, 64, 2, stride=2) self.dec1 = DoubleConv(128, 64) self.out = nn.Conv2d(64, out_channels, 1) def forward(self, x): e1 = self.enc1(x) e2 = self.enc2(self.pool(e1)) e3 = self.enc3(self.pool(e2)) e4 = self.enc4(self.pool(e3)) c = self.center(self.pool(e4)) d4 = self.dec4(torch.cat([self.up4(c), e4], dim=1)) d3 = self.dec3(torch.cat([self.up3(d4), e3], dim=1)) d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=1)) d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1)) return self.out(d1)

这段结构有两点值得注意。第一,in_channels=1是因为眼底图转成了灰度图;如果你想用彩色眼底图,改成 3 即可,但建议先按灰度跑通再改。第二,最后一层是nn.Conv2d(64, 1, 1),输出没有接 Sigmoid,因为训练时用BCEWithLogitsLoss,这个损失函数内部已经包含了 Sigmoid 计算,直接在输出上算损失数值更稳定。跳跃连接的位置在torch.cat,把下采样第 N 层的特征和上采样第 N 层的特征拼接,这是 Unet 的核心机制,不要删。

3.2 训练参数推荐:损失函数、学习率与低显存配置

训练眼底血管分割,最典型的坑是没有综合考虑显存和分割精度。根据这套资源的实际场景,我给出以下参数配置。

参数推荐值说明
输入尺寸512 × 512低于 384 会明显丢失细血管
Batch Size4 ~ 8显存不够就降到 2,配合梯度累积
损失函数BCEWithLogitsLoss + Dice Loss单独用 BCE 会导致血管区域被背景淹没
优化器Adamlr 初始 1e-4,比 SGD 更稳
学习率策略CosineAnnealingLR避免后期 loss 震荡
Epoch60 ~ 100小数据集 60 轮足够收敛
混合精度AMP显存不够时开启,能省约 40% 显存

我看到资源包的说明里提到训练集已经“处理好”,如果数据集规模是几百张级别,这个配置够用。很多人喜欢把 Batch Size 拉到 16,结果显存直接爆掉,然后怪模型太大——其实 512 分辨率下 Unet 的显存占用大头在特征图,把 Batch Size 设为 4,训练一个 epoch 并不会慢太多,但显存压力小一个量级。

如果有人显卡只有 6GB,我一般建议把img_size从 512 降到 448,同时开启 AMP。448 对眼底血管来说损失可以接受,但能保住 Batch Size 4 的训练稳定性。低显存运行模型的核心思路就是“降分辨率优先于降通道数”,因为 Unet 的通道数是结构决定的,轻易改动会破坏跳跃连接的维度匹配。

3.3 训练主循环:从 loss 曲线到混淆矩阵输出

训练循环本身不复杂,套路是前向计算、算损失、反向传播、更新参数,每个 epoch 结束做一次验证并保存最优模型。但为了后续能出混淆矩阵,验证阶段要做额外几步:把预测概率图二值化,和二值 mask 逐像素比对,统计 TP、FP、FN、TN,最后写入confusion_matrix.csv。

for epoch in range(epochs): model.train() total_loss = 0 for imgs, masks in train_loader: imgs, masks = imgs.to(device), masks.to(device) preds = model(imgs) loss = criterion(preds, masks) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() # 验证阶段 model.eval() tp = fp = fn = tn = 0 with torch.no_grad(): for imgs, masks in val_loader: imgs, masks = imgs.to(device), masks.to(device) preds = torch.sigmoid(model(imgs)) binary = (preds > 0.5).float() tp += ((binary == 1) & (masks == 1)).sum().item() fp += ((binary == 1) & (masks == 0)).sum().item() fn += ((binary == 0) & (masks == 1)).sum().item() tn += ((binary == 0) & (masks == 0)).sum().item() torch.save(model.state_dict(), f"unet_epoch_{epoch}.pth")

这里有两个容易被忽视的细节。第一个是torch.sigmoid必须在验证阶段手动调用,因为训练时 BCEWithLogitsLoss 内部做了 Sigmoid,但验证时模型直接输出 logits,不经过 Sigmoid 就阈值得不到正确的二值图。第二个是混淆矩阵的统计单位,用.sum().item()统计每个 batch 的累加值,而不是用torchmetrics这类库——不是不能用,而是手写统计能让你清楚地知道这四个数到底怎么来的,后面解释模型效果时更有底。

训练结束后,资源里的confusion_matrix.csv就是用类似逻辑统计出来的。训练时每个 epoch 保存一份权重文件会占用大量硬盘空间,建议只保存验证集 Dice 最高的那一个,命名成best_model.pth,逻辑是:当前 epoch 的 Dice 大于历史最佳时覆盖保存,否则跳过。

4. 模型测试与界面封装:从概率图到上传即预测

4.1 加载训练好的权重做推理:输出概率图转二值图

训练好的模型如果要复现效果,第一步是加载权重,第二步是保证预处理和训练时完全一致。资源里带了一个训练好的模型文件,这就是“后悔药”——即使你没显卡重新训练,也能直接加载它跑测试和界面。

import torch import torch.nn.functional as F device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = UNet(in_channels=1, out_channels=1).to(device) model.load_state_dict(torch.load("best_model.pth", map_location=device)) model.eval() def predict_single(img_path, size=(512, 512)): img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, size) img = img.astype(np.float32) / 255.0 img_tensor = torch.from_numpy(img).unsqueeze(0).unsqueeze(0).to(device) with torch.no_grad(): logits = model(img_tensor) prob = torch.sigmoid(logits) binary = (prob > 0.5).float() return binary.squeeze().cpu().numpy(), prob.squeeze().cpu().numpy()

map_location=device这行要写,因为很多人用 GPU 训练、用 CPU 加载,不指定的话 PyTorch 会尝试按保存时的设备加载,CPU 环境直接报错。unsqueeze(0).unsqueeze(0)是把 H×W 的二维数组变成 N×C×H×W 的四维张量,N 是 batch 维,C 是通道维,顺序不能乱。阈值0.5在绝大多数情况下可用,但如果你的模型预测概率普遍偏低或者偏高,可以看验证集的 PR 曲线来定,不过那是后话。

推理时一个显眼的技巧是处理大图——如果测试图比 512×512 大很多,直接 resize 会丢失细血管,我一般会切成 512×512 的 patch 分别预测再拼回去,patch 之间重叠 32 像素,重叠区取平均值,能有效避免拼缝处的血管断裂。

4.2 评估指标:从 confusion_matrix.csv 解读 Acc、Dice、IoU

拿到confusion_matrix.csv后,里面四个数字怎么变成论文里的指标,这是有固定公式的。医学图像分割领域最看重的是 Dice 和 Sensitivity,而不是 Accuracy,因为血管像素占比不到 10%,就算把所有像素都预测为背景,Accuracy 也能到 90% 以上,但那没有任何临床意义。

import pandas as pd cm = pd.read_csv("confusion_matrix.csv", index_col=0) tn = cm.loc["TN", "value"] # 实际是读取数值,按文件结构调整 fp = cm.loc["FP", "value"] fn = cm.loc["FN", "value"] tp = cm.loc["TP", "value"] acc = (tp + tn) / (tp + tn + fp + fn) sensitivity = tp / (tp + fn) # 召回率:血管被找出来的比例 specificity = tn / (tn + fp) # 背景被正确排除的比例 dice = 2 * tp / (2 * tp + fp + fn) iou = tp / (tp + fp + fn) print(f"Dice: {dice:.4f}, IoU: {iou:.4f}, Sen: {sensitivity:.4f}, Spe: {specificity:.4f}")

Dice的取值范围是 0 到 1,眼底血管分割任务里做到 0.75 以上就算不错的结果,公开数据集上的 SOTA 大概在 0.82 左右。如果你复现出来的 Dice 明显偏低,不要先怀疑模型,大概率是预处理不一致或者测试集和训练集分布差异大。Sensitivity是临床最关心的指标,因为血管漏检的代价比误检背景高得多,如果 Sensitivity 低于 0.7,说明模型对细血管的捕捉能力不够,可以考虑把输入分辨率提高或者换用带空洞卷积的变体。

4.3 图形界面封装:PyQt5 上传图片一键预测

资源里说的“系统界面”,本质是把推理逻辑包一层 GUI。技术选型上常见做法是 PyQt5 或 Tkinter,PyQt5 更美观,配合QLabel显示图片,QPushButton触发上传和预测。

import sys from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QFileDialog, QVBoxLayout from PyQt5.QtGui import QPixmap, QImage import numpy as np class SegmentationApp(QWidget): def __init__(self): super().__init__() self.model = load_model() # 初始化时加载一次 self.init_ui() def init_ui(self): self.btn_open = QPushButton("上传眼底图像") self.btn_save = QPushButton("保存分割结果") self.label_orig = QLabel("原图") self.label_result = QLabel("分割结果") layout = QVBoxLayout() layout.addWidget(self.btn_open) layout.addWidget(self.label_orig) layout.addWidget(self.label_result) layout.addWidget(self.btn_save) self.setLayout(layout) self.btn_open.clicked.connect(self.open_image) def open_image(self): path, _ = QFileDialog.getOpenFileName(self, "选择图像", "", "图像文件 (*.png *.jpg *.jpeg)") if path: binary, prob = predict_single(path) # 保存到当前目录,并在界面上显示 cv2.imwrite("result.png", binary * 255) qimg = QImage(binary * 255, binary.shape[1], binary.shape[0], QImage.Format_Grayscale8) self.label_result.setPixmap(QPixmap.fromImage(qimg))

这段代码有三个工程细节值得注意。第一,模型初始化要在__init__里一次性加载,不要放在按钮点击事件里,否则每次预测都要重新读权重文件,界面会卡顿一两秒。第二,QFileDialog.getOpenFileName返回的文件路径可能是中文路径,PyQt5 对中文字符串处理偶尔有编码问题,保险起见可以用os.path.abspath转一下。第三,binary * 255是因为 QImage 显示灰度图要求像素值范围是 0~255,直接用 0/1 的二值数组显示出来会是全黑。这个坑在界面调试时非常常见,属于“看起来像是模型预测失败,其实是显示格式问题”。

5. 常见问题与避坑:五个必踩的坑和对应处理

5.1 预测结果全黑或全白

现象:训练时 loss 正常下降,但加载模型后对任意输入图预测,输出图要么全黑要么全白。

原因:全黑通常是输入没做归一化,模型训练时输入范围是[0, 1],预测时直接喂了 0~255 的原始像素值,分布完全错位,网络输出的 logits 全部偏向负区间,Sigmoid 后全部接近 0。全白则相反,可能是训练时 mask 预处理出了问题,比如把背景标注为 1、血管标注为 0,模型学反了。

解决:把预测代码里的img.astype(np.float32) / 255.0加上,检查数据增强代码里有没有把 mask 的 0/1 翻转。我见过最隐蔽的一次,是数据读取时用了cv2.imread默认的 BGR 通道,而训练时某段代码又转了 RGB,导致 mask 错位——这个问题在灰度图上不会出现,但只要你切到彩图就必炸。

5.2 显存不足(CUDA out of memory)

现象:训练脚本一跑就在第一个 batch 报CUDA out of memory,6GB 显卡尤其常见。

原因:输入尺寸 512×512 加 Batch Size 8,Unet 的特征图占用本身就很大,显存峰值出现在编码器最深层的 1024 通道特征图上,那是一张 32×32×1024 的浮点张量,一个就占 4MB,反向传播还要存梯度,翻好几倍。

解决:优先把 Batch Size 降到 2 或 4,开启 PyTorch 的 AMP 混合精度;如果还不行,把img_size降到 448。另外注意检查有没有torch.no_grad()包裹验证阶段,很多人验证时忘了关梯度,显存直接双倍占用。AMP 的开启方式是torch.cuda.amp.autocast()包住前向和 loss 计算,再配合GradScaler,注意scaler.scale(loss).backward()和scaler.step(optimizer)的顺序不要写反。

5.3 血管断裂、细血管丢失

现象:分割结果里主干血管完整,但末梢毛细血管出现大量断裂,视觉上像“虚线血管”。

原因:本质是下采样次数过多导致细节丢失。Unet 默认下采样 4 次,512 输入在底层特征图只有 32×32,一条只有 2 像素宽的毛细血管在这个尺度上已经不可分辨。另外一个常见因素是阈值设太高,概率图里细血管的预测值普遍偏低,0.5 的阈值把它们滤掉了。

解决:如果血管断裂发生在细末梢,先看概率图,如果细血管区域概率在 0.3~0.5 之间,可以尝试把阈值降到 0.3 再二值化;如果概率图上细血管区域本来就是黑的一块,那是模型本身没学到,需要提高输入分辨率或增加数据集中细血管样本的权重。形态学后处理也能改善断裂,用cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)做一个闭运算,可以把间隔 1~2 像素的断裂点连上。

5.4 loss 不降或震荡

现象:训练前几个 epoch loss 还在降,后面开始上下剧烈震荡,或者从一开始就完全不降。

原因:震荡通常是学习率过大。Adam 虽然自带自适应学习率,但 1e-3 起步对 Unet 来说往往太高,后期会出现 loss 在某个值附近来回跳。完全不降则大概率是数据问题,最常见的是 mask 和原图没有对齐,或者 mask 里全是 0(标注文件读取路径错了)。

解决:把初始学习率设为 1e-4,配合 CosineAnnealingLR,后面 200 个 epoch 内学习率会平滑衰减到接近 0,震荡基本消失。完全不降时,在训练循环里加一行print(img_path, mask_path, mask.max()),打印第一个 batch 的 mask 最大值,如果是 0 说明 mask 读取失败,不要再调模型,先修数据路径。

5.5 界面预测效果和训练效果不一致

现象:在训练脚本测试集上 Dice 有 0.75,但通过界面上传同一张图,结果明显变差。

原因:这是最典型的“预处理不一致”问题。训练时走了灰度化、resize、归一化、翻转增强,界面预测时如果漏掉其中任何一步,模型输入的分布就和训练时不同。比如漏了灰度化,输入变成三通道但模型只有单通道卷积,PyTorch 虽然会报错,但报错在深层卷积计算时,界面程序直接把异常吞了,显示的还是上一张的结果图。

解决:把预处理封装成一个函数,训练、测试、界面三处共同调用同一个preprocess(img_path),不要在界面里重新写一遍。我现在的习惯是train.py、test.py、app.py三个文件都from preprocess import preprocess,保证三处逻辑完全一致,这个习惯是从一次界面翻车后养成的。

6. 进阶:把代码迁移到自己的眼底数据集

这套资源最值钱的地方在于“可以直接替换数据”。如果你有自己采集的眼底图像,或者是其他公开数据集,只需要把图像和 mask 按images和masks两个目录放好,文件名一一对应,其他代码基本不用动。但迁移时有一个容易被忽略的检查:原图和 mask 的尺寸、数量、文件名必须完全对应,缺失任何一个文件,训练时不会报错,而是会错位。

我强烈建议在训练前跑一遍数据对应性检查脚本,这一步能省下你一整天的调试时间。

import os from PIL import Image img_dir = "images" mask_dir = "masks" count = 0 for name in sorted(os.listdir(img_dir)): mask_path = os.path.join(mask_dir, name) if not os.path.exists(mask_path): print(f"[缺失] {name} 没有对应的 mask") count += 1 continue img = Image.open(os.path.join(img_dir, name)) mask = Image.open(mask_path) if img.size != mask.size: print(f"[尺寸不一致] {name} 图 {img.size} mask {mask.size}") print(f"检查完成,共发现 {count} 个问题")

这段脚本会在训练前把“文件名缺失”和“尺寸不一致”这两类最常见的错误全部揪出来。文件名缺失的原因很多——有些数据集是.jpg和.png混用,有些是大小写不一致,有些是从不同来源下载后没有重命名。尺寸不一致的问题在公开数据集里很常见,比如原始眼底图是 2048×2048,而标注图是 512×512,预处理里必须有 resize 逻辑,否则训练时torch.cat会因为尺寸匹配不上直接报错。

迁移时另一个注意点是num_workers和内存的平衡。眼底图像普遍偏大,如果原图是 2048 分辨率,建议在 Dataset 的__getitem__里先 resize 到 512 再返回,不要等collate阶段处理。数据加载的num_workers在 Windows 上建议设为 2,Linux 可以设 4,设太高反而会因为进程切换开销拖慢训练。

从数据处理到界面封装,这套资源把眼底血管分割的完整链路都串起来了。我拿到资源后的习惯是先在 CPU 上用小数据集跑 10 个 epoch 验证全流程通不通,再上 GPU 完整训练,这个习惯让我避免了至少三次“训练两小时后才发现数据路径写错”的惨剧。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询