语义分割实战-地表建筑物识别-阿里天池比赛分享
1、知识补充
1-1 语义分割的概念
如上图所示 :语义分割是将图片中的每个像素分类到对应的类别 是视觉意义上的"语义"分割,而不是自然语言上的语义。
语义分割区别于目标检测,目标检测的框太“粗糙”了,无法精细处理到像素。
语义分割区别于普通的分割在于语义分割的训练集,我们能确定每个像素的label是什么,也就是有监督学习,而普通的分割我们可以看作是聚类问题,也就是无监督学习
1-2 应用
视频会议,人像模式,无人驾驶-路面分割等等
总而言之 语义分割是一种分类问题 目标是要精确到每一个像素的label到底是什么
2、赛题背景
赛题以计算机视觉为背景,要求选手使用给定的航拍图像训练模型并完成地表建筑物识别任务。
2-1 赛题描述及数据说明
遥感技术已成为获取地表覆盖信息最为行之有效的手段,遥感技术已经成功应用于地表覆盖检测、植被面积检测和建筑物检测任务。本赛题使用航拍数据,需要参赛选手完成地表建筑物识别。
将地表航拍图像素划分为有建筑物和无建筑物两类。
解题思路
本次赛题是一个典型的语义分割任务:
- 步骤1:理解赛题,理解数据
- 步骤2:对数据进行解码并进行数据扩增方法,并划分验证集以增加监督模型精度;
- 步骤3:使用FCN 模型模型跑通具体模型训练过程,并对结果进行预测提交;
- 步骤4:使用更加强大模型结构(如Unet 和PSPNet)或尺寸更大的输入完成训练;
- 步骤5:集成学习,参数调优,其他trick;
如下图,左边为原始航拍图,右边为对应的建筑物标注。
平台:Jupyter Notebook、Pycharm、AutoDL AI算力云
工具:Anaconda Pytorch12.0 Python 3.11
赛题数据为航拍图,需要参赛选手识别图片中的地表建筑具体像素位置。
train_mask.csv:存储图片的标注的rle编码;
train和test文件夹:存储训练集和测试集图片;
赛题提供了如下数据集:
其中test_a和train均为带有编号的图片集,如下:
train_mask中存放着训练集图片不同编号的图片经RLE编码后的字符串。所以需要构造标签跟图片联系起来的训练集,然后再传入模型进行训练,构造结果如下:name列存图片地址,mask列存图片经编码后的字符串。
测试提交样例如下:
即第一列为图片的名称,第二列为图片经模型预测后分割好的图片再经过RLE编码后的字符串,如下图:
流程即:利用训练集与验证集在深度学习网络上训练模型,然后利用测试集得出预测结果。
3-2 评测标准
计算Dice系数
Dice系数(Dice Coefficient)可以通过以下公式计算:
D
i
c
e
(
A
,
B
)
=
2
⋅
∣
A
∩
B
∣
∣
A
∣
+
∣
B
∣
Dice(A, B) = \frac{2 \cdot |A \cap B|}{|A| + |B|}
Dice(A,B)=∣A∣+∣B∣2⋅∣A∩B∣
Dice系数来度量它们的相似性。
其中,A是模型预测的二值掩码、B是真实的二值掩码、|A∩B|表示预测结果与真实标签中同时为正类别的像素数量、|A|表示预测结果中的正类别像素数量、|B|表示真实标签中的正类别像素数量。
3、数据预处理
3-1 导入相关包
import numpy as np
import pandas as pd
import pathlib, sys, os, random, time
import numba, cv2, gc
from tqdm import tqdm_notebook
import matplotlib.pyplot as plt
%matplotlib inline
import warnings
warnings.filterwarnings('ignore')
from tqdm.notebook import tqdm
import albumentations as A
import rasterio
from rasterio.windows import Window
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.data as D
import torchvision
from torchvision import transforms as T
import segmentation_models_pytorch as smp
3-2 定义转码函数
赛题为语义分割任务,因此具体的标签为图像像素类别。在赛题数据中像素属于2类(无建筑物和有建筑物),因此标签为有建筑物的像素。赛题原始图片为jpg格式,标签为RLE编码的字符串。
RLE全称(run-length encoding),翻译为游程编码或行程长度编码,对连续的黑、白像素数以不同的码字进行编码。RLE是一种简单的非破坏性资料压缩法,经常用在在语义分割比赛中对标签进行编码
# 将图片编码为rle格式
def rle_encode(im):
'''
im: numpy数组, 1 - 掩模, 0 - 背景
返回以字符串格式表示的运行长度
'''
pixels = im.flatten(order='F') # 将数组展平为一维数组(按列顺序)
pixels = np.concatenate([[0], pixels, [0]]) # 在数组两端添加0,确保首尾都是0
runs = np.where(pixels[1:] != pixels[:-1])[0] + 1 # 找到变化点的索引并加1
runs[1::2] -= runs[::2] # 计算长度(差值)
return ' '.join(str(x) for x in runs)
# 将rle格式进行解码为图片
def rle_decode(mask_rle, shape=(512, 512)):
'''
mask_rle: 字符串格式的运行长度(起始长度)
shape: 返回数组的形状 (高度,宽度)
返回NumPy数组,1 - 掩模, 0 - 背景
'''
s = mask_rle.split() # 拆分字符串
starts, lengths = [np.asarray(x, dtype=int) for x in (s[0:][::2], s[1:][::2])] # 分别获取起始位置和长度
starts -= 1 # 起始位置减1
ends = starts + lengths # 计算结束位置
img = np.zeros(shape[0] * shape[1], dtype=np.uint8) # 创建全零数组
for lo, hi in zip(starts, ends):
img[lo:hi] = 1 # 根据起始和结束位置将相应区域设为1
return img.reshape(shape, order='F') # 将一维数组重新整形为指定形状
3-3 定义全局参数
## 定义批处理大小
BATCH_SIZE = 8
## 定义目标大小
IMAGE_SIZE = 256
## 选择GPU
DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
## 查看是否正常使用GPU
DEVICE
3-4 设置种子
# 定义一个名为set_seeds的函数,该函数接受一个可选的参数seed,默认值为2356。
def set_seeds(seed=2356):
# 使用指定的种子值设置Python的随机数生成器的种子,以确保随机数生成的确定性。
random.seed(seed)
# 设置环境变量'PYTHONHASHSEED'为种子值,这会影响Python字典和集合的哈希算法。
os.environ['PYTHONHASHSEED'] = str(seed)
# 使用指定的种子值设置NumPy的随机数生成器的种子。
np.random.seed(seed)
# 使用指定的种子值设置PyTorch的随机数生成器的种子。
torch.manual_seed(seed)
# 使用指定的种子值设置PyTorch的CUDA随机数生成器的种子。
torch.cuda.manual_seed(seed)
# 使用指定的种子值设置PyTorch的所有CUDA设备的随机数生成器的种子。
torch.cuda.manual_seed_all(seed)
# 设置PyTorch的cudnn后端为确定性模式,这意味着使用确定的算法来计算卷积,从而避免使用任何随机性。
torch.backends.cudnn.deterministic = True
# 设置PyTorch的cudnn后端不使用任何性能优化,这可能会略微降低运行速度,但确保了算法的确定性。
torch.backends.cudnn.benchmark = False
# 调用set_seeds函数,不传入任何参数将使用默认的种子值2356。
set_seeds()
3-5 数据增强
增强数据:从原始图像派生而来,并进行某种较小的几何变换(例如翻转、平移、旋转或添加噪声等)或者色彩变换(例如亮度、对比度、饱和度或通道混洗等),以此来增加训练集的多样性。
数据增强的作用:
1、省钱省时省力:
在实际的应用场景中,数据集的采集、清洗和标注在大多数情况下都是一个非常昂贵且费时费力且乏味的事情。有了数据增强技术,一方面可以减轻相关人员的工作量,另一方面也可以帮助公司削减运营开支。此外,有些数据由于涉及到各种隐私问题可能用钱都买不到,又或者一些异常场景的数据几乎是极小概率时间,这时候数据增强的优势便充分的体现出来了。
2、提升模型性能:
众所周知,卷积神经网络对平移、视点、大小或光照均具有不变性。因此,CNN 能够准确地对不同方向的物体进行分类。在深度学习中,CNN 通过对输入图像进行卷积运算来学习图像中的不同特征,从而在计算机视觉任务上表现非常出色。随着 ViT 的提出,一系列 Vision Transformer 模型被提出并被广泛地应用。然而,无论是 CNN 还是 Transformer,均离不开数据的支持。特别是,当数据量较小时 CNN 容易过拟合,Transformer 则无法学习到良好的表征
更多详细可见:https://zhuanlan.zhihu.com/p/598985864。
# 定义数据增强的变换
train_trfm = A.Compose([
# 调整图像的大小为IMAGE_SIZE x IMAGE_SIZE
A.Resize(IMAGE_SIZE, IMAGE_SIZE),
# 以50%的概率对图像进行水平翻转
A.HorizontalFlip(p=0.5),
# 以50%的概率对图像进行垂直翻转
A.VerticalFlip(p=0.5),
# 以90度的间隔随机旋转图像
A.RandomRotate90(),
# 从给定的变换列表中随机选择一个变换,变换被选择的概率为0.3
A.OneOf([
# 随机调整图像的对比度
A.RandomContrast(),
# 随机调整图像的伽马值
A.RandomGamma(),
# 随机调整图像的亮度
A.RandomBrightness(),
# 随机调整图像的亮度、对比度、饱和度和色调
A.ColorJitter(brightness=0.07, contrast=0.07, saturation=0.1, hue=0.1, always_apply=False, p=0.3),
], p=0.3),
])
如下图便是对原图进行添加噪声、模糊等数据增强处理后的对比:
3-7 定义my_Dataset
class my_Dataset(D.Dataset):
def __init__(self, paths, rles, transform, test_mode=False):
self.paths = paths # 图像文件路径列表
self.rles = rles # 对应的运行长度编码掩码列表
self.transform = transform # 数据增强的变换
self.test_mode = test_mode # 是否为测试模式
self.len = len(paths) # 数据集大小
self.as_tensor = T.Compose([
T.ToPILImage(), # 将图像转换为PIL图像
T.Resize(IMAGE_SIZE), # 调整图像大小为指定尺寸
T.ToTensor(), # 将PIL图像转换为PyTorch张量
T.Normalize([0.625, 0.448, 0.688], [0.131, 0.177, 0.101]), # 归一化
])
# 获取数据的操作
def __getitem__(self, index):
img = cv2.imread(self.paths[index]) # 读取图像文件
if not self.test_mode:
mask = rle_decode(self.rles[index]) # 解码运行长度编码的掩码
augments = self.transform(image=img, mask=mask) # 应用数据增强
return self.as_tensor(augments['image']), augments['mask'][None] # 返回处理后的图像和掩码
else:
return self.as_tensor(img), '' # 对于测试模式,只返回处理后的图像
def __len__(self):
"""
数据集的总样本数
"""
return self.len
注意:
路径中包含中文字符,cv2无法识别中文路径,基于以上这点,结果会是None
train_mask = pd.read_csv("E:/DeepLearning/data/train_mask.csv", sep='\t', names=['name', 'mask'])
train_mask['name'] = train_mask['name'].apply(lambda x: "E:/DeepLearning//data/train/" + x)
img = cv2.imread(train_mask['name'].iloc[0])
#调用rle_decode的函数来解码'mask'列中的值。这个解码后的值被存储在变量mask中。
mask = rle_decode(train_mask['mask'].iloc[0])
#验证解码的正确性:使用一个名为rle_encode的函数来对解码后的mask进行编码
#比较编码后的值与原始的'mask'值是否相同。如果相同,则打印True,否则打印False。这主要用于验证解码过程是否正确
print(rle_encode(mask) == train_mask['mask'].iloc[0])
print("Image Path:", train_mask['name'].iloc[0])
file_path = train_mask['name'].iloc[0]
if os.access(file_path, os.R_OK):
print("可读取")
else:
print("不可读取")
构造数据集
dataset = my_Dataset(train_mask['name'].values,train_mask['mask'].fillna('').values,train_trfm, False)
看看效果
image, mask = dataset[0]
plt.figure(figsize=(16,8))
plt.subplot(121)
plt.imshow(mask[0], cmap='gray')
plt.subplot(122)
plt.imshow(image[0]);
3-8 划分数据集
import torch.utils.data as D
valid_idx, train_idx = [], []
# 这里只选用1/7的数据为验证集,6/7的数据为训练集
"""
for i in range(len(dataset)):
if i % 7 == 0:
valid_idx.append(i)
elif i % 7 != 0:
train_idx.append(i)
"""
for i in range(len(dataset)):
if i % 7 == 0:
valid_idx.append(i)
elif i % 7 == 1:
train_idx.append(i)
# 创建训练集和验证集的 Subset
train_ds = D.Subset(dataset, train_idx)
valid_ds = D.Subset(dataset, valid_idx)
# 定义训练和验证集的数据加载器
# 使用 num_workers 设置为适当的值,以加速数据加载
loader = D.DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=0)
vloader = D.DataLoader(valid_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=0)
4、模型建立
4-1模型选择
4-1-1 U-Net
- model1:Unet
- model2:UnetPlusPlus
语义分割模型:语义分割(Semantic Segmentation)是图像处理和机器视觉一个重要分支。与分类任务不同,语义分割需要判断图像每个像素点的类别,进行精确分割。
如上图为自动驾驶中的移动分割任务的分割结果,可以从一张图片中有效的识别出汽车(深蓝色),行人(红色),红绿灯(黄色),道路(浅紫色)等。
-
编码器(Encoder):
编码器负责对输入图像进行层层的特征提取,通过卷积和池化等操作逐渐减小空间分辨率并增加通道数量。在 U-Net 中,通常使用预训练的卷积神经网络(CNN)作为编码器。这些网络在大规模图像分类任务上进行了训练,因此能够学习通用的、高级别的图像特征。
特征提取: 卷积层用于检测图像中的低级特征,如边缘、纹理等。随着网络的深入,卷积层逐渐捕捉到更抽象的语义特征,例如形状、物体部分等。
下采样(Pooling): 池化操作用于减小特征图的空间分辨率,同时保留主要的特征。这使得网络能够在保持语义信息的同时减小计算量。 -
解码器(Decoder):
解码器负责将编码器提取的抽象特征映射回输入图像的分辨率,并通过跳跃连接传递更多的细节信息。
上采样(Upsampling): 解码器使用上采样层或反卷积层,将编码器中降采样的特征图恢复到原始输入图像的分辨率。这有助于保留空间细节。
跳跃连接(Skip Connections): U-Net 引入了跳跃连接,通过将编码器中对应的层的特征图与解码器相应层的特征图连接在一起。这允许网络在解码的过程中直接访问底层的高分辨率特征,有助于更好地还原细节信息。
特征融合: 在解码器中进行特征融合,将来自编码器和跳跃连接的特征结合起来。这有助于网络学习如何在保留语义信息的同时还原细节。
3、网络结构:
U-Net 将编码器和解码器连接在一起,形成一个完整的 U 字形结构。这种结构允许网络在整个过程中同时保持全局信息和局部信息。
输出层: 最终的输出通常是一个特征图,其分辨率与输入图像相同,每个像素对应于输入图像的相应位置。通常,输出层使用适当的激活函数(如 Sigmoid 或 Softmax)生成分割掩码。
4、选择编码器:
我们在Kaggle比赛中看各大CV比赛前排方案分享时,发现几乎每个人都在使用EfficientNet作为他们的骨干,而在此之前我还没有听说过。于是借助本次赛题任务重新学习并认识。
EfficientNet是由谷歌人工智能提出,他们试图提出一种如其名字所暗示的更有效的方法,同时改进现有的技术成果。一般来说,模型做得太宽,太深,或者分辨率很高。增加这些特征最初有助于模型的建立,但很快就会饱和,所建立的模型只是有更多的参数,因此效率不高。在EfficientNet中,这些参数都以一种更加有效的方式逐渐增加。
我们先来认识一下EfficientNet。
共有的结构:
任何网络的最关键的都是它的stem,确定了之后才会进行后面的实验,这个结构在所有八个模型和最后一层都是共同的。
EfficientnetB0~B7都包含这7个区块。这些块还有不同数量的子块,当我们从EfficientNetB0到EfficientNetB7时,子块的数量会增加。
下面代码可以看清楚模型结构:
本次我们选择的编码器为efficientnet-b4/ efficientnet-b7
①U-net模型
其结构图如下:
简略图如下:
U-net模型解读:
该模型先对图片进行卷积和池化,在U-net论文中是池化4次,比方说一开始的图片是224224的,那么就会变成112112、5656、2828、1414四个不同尺寸的特征。然后我们对1414的特征图做上采样或者反卷积,得到2828的特征图,这个2828的特征图与之前的2828的特征图进行通道上的拼接concat,然后再对拼接之后的特征图做卷积和上采样,得到5656的特征图,再与之前的5656的特征拼接,卷积,再上采样,经过四次上采样可以得到一个与输入图像尺寸相同的224224的预测结果。
Unet优点:
网络层越深得到的特征图有着更大的视野域,浅层卷积关注纹理特征,深层网络关注本质的那种特征,所以深层浅层特征都是有各自的意义的。另外一点是通过反卷积得到的更大的尺寸的特征图的边缘,是缺少信息的,毕竟每一次下采样提炼特征的同时,也必然会损失一些边缘特征,而失去的特征并不能从上采样中找回,因此通过特征的拼接,来实现边缘特征的一个找回。
4-1-2 U-Net++
②U-net++模型
由于不同深度的U-net的表现并不是越深越好,它背后的传达的信息就是,不同层次特征的重要性对于不同的数据集是不一样的,并不是说设计一个原论文给出的那个结构,就一定对所有数据集的分割问题都最优,所以为了解决这个问题提出了U-net++。U-net++是在U-net的基础上,引入了更多的上采样节点和跳跃连接,简而言之就是在上采样时抓取不同层次的特征,将它们通过特征叠加的方式整合,加入更浅的U-net结构,使得融合时的特征图尺度差异更小,架构图如下:
简略图如下:
U-net++的好处也比较明显了,就是不需要我们对于下采样的深度进行探索,通过叠加融合的特征就能得到较好的结果。
def get_model(num):
if num == 1:
# 创建基于 U-Net 架构的模型
model_name="Unet"
model = smp.Unet(
encoder_name="efficientnet-b4", # 选择编码器,例如 mobilenet_v2 或 efficientnet-b7
encoder_weights='imagenet',
in_channels=3, # 模型输入通道数(1 表示灰度图像,3 表示 RGB,等等)
classes=1, # 模型输出通道数(数据集中的类别数)
)
elif num == 2:
# 创建基于 U-Net++ 架构的模型
model_name="Unet_PlusPlus"
model = smp.UnetPlusPlus(
encoder_name="efficientnet-b4", # 选择编码器,例如 mobilenet_v2 或 efficientnet-b7
encoder_weights='imagenet', # 使用 `imagenet` 预训练权重进行编码器初始化
in_channels=3, # 模型输入通道数(1 表示灰度图像,3 表示 RGB,等等)
classes=1, # 模型输出通道数(数据集中的类别数)
)
return model,model_name
# 构造模型验证
@torch.no_grad()
def validation(model, loader, loss_fn):
losses = []
model.eval()
for image, target in loader:
# 将输入图像和目标移动到指定的设备上
image, target = image.to(DEVICE), target.float().to(DEVICE)
# 前向传播获取模型的输出
output = model(image)
# 计算损失
loss = loss_fn(output, target)
# 记录损失值
losses.append(loss.item())
# 计算平均损失
return np.array(losses).mean()
4-2 实例化模型
## 模型1 Unet
model1,model_name = get_model(1)
model1.to(DEVICE)
## 模型2 Unet++
model2,model_name = get_model(2)
model2.to(DEVICE)
### 4-2 配置优化器、损失函数
model=model1
我们的方案中使用了两个主要的损失函数,分别是二分类交叉熵损失函数(BCEWithLogitsLoss)和Soft Dice损失函数。为了提高效果,我们不单单是使用单一的损失函数,而是将两个损失函数按照一定比列融合使用
# 定义优化器,这里使用了 AdamW 优化器
optimizer = torch.optim.AdamW(model.parameters(), lr=2e-4, weight_decay=1e-3)
scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=5, T_mult=1, eta_min=1e-6, last_epoch=-1)
# 定义 Soft Dice 损失函数
class SoftDiceLoss(nn.Module):
def __init__(self, smooth=1., dims=(-2, -1)):
super(SoftDiceLoss, self).__init__()
self.smooth = smooth
self.dims = dims
def forward(self, x, y):
tp = (x * y).sum(self.dims)
fp = (x * (1 - y)).sum(self.dims)
fn = ((1 - x) * y).sum(self.dims)
dc = (2 * tp + self.smooth) / (2 * tp + fp + fn + self.smooth)
dc = dc.mean()
return 1 - dc
# 使用 BCEWithLogitsLoss 作为二分类交叉熵损失
bce_fn = nn.BCEWithLogitsLoss()
# 使用 SoftDiceLoss 作为 Soft Dice 损失
dice_fn = SoftDiceLoss()
# 定义组合损失函数,结合了二分类交叉熵和 Soft Dice 损失
def loss_fn(y_pred, y_true, ratio=0.8, hard=False):
bce = bce_fn(y_pred, y_true)
if hard:
dice = dice_fn((y_pred.sigmoid()).float() > 0.5, y_true)
else:
dice = dice_fn(y_pred.sigmoid(), y_true)
# 将二者按一定比例组合
return ratio*bce + (1-ratio)*dice
5、模型训练
#清除缓存
torch.cuda.empty_cache()
# 定义表头
header = r'''
Epoch | Train | Valid | Time, h
'''
# 定义原始行的格式字符串
raw_line = '{:6d} | {:<7.3f} | {:<7.3f} | {:<6.2f}'
print(header)
EPOCHES = 2 # 训练周期数
best_loss = 10 # 初始最佳损失值
for epoch in range(1, EPOCHES + 1): # 遍历每个训练周期
losses = [] # 初始化损失列表
start_time = time.time() # 记录当前时间作为训练开始的时间
model.train() # 将模型设置为训练模式
for image, target in tqdm_notebook(loader): # 遍历训练数据集中的每个样本
image, target = image.to(DEVICE), target.float().to(DEVICE) # 将图像和目标数据转移到指定的设备(如GPU)上
optimizer.zero_grad() # 清零优化器中的梯度
output = model(image) # 通过模型获取预测输出
loss = loss_fn(output, target) # 计算损失函数值
loss.backward() # 反向传播,计算梯度
optimizer.step() # 根据梯度更新模型参数
losses.append(loss.item()) # 将当前损失添加到损失列表中
vloss = validation(model, vloader, loss_fn) # 在验证集上验证模型,并返回损失值
print(raw_line.format(epoch, np.array(losses).mean(), vloss, (time.time() - start_time) / 60 ** 1)) # 打印训练信息,包括训练周期数、平均损失、验证损失和训练时间(单位为分钟)
losses = [] # 清空损失列表,为下一个训练周期做准备
if vloss < best_loss:
best_loss = vloss # 如果验证损失比当前最佳损失小,则更新最佳损失值
torch.save(model.state_dict(), model_name + 'model_best.pth') # 并保存当前模型的权重到文件
6、模型验证
6-1 训练集(Train Set)
模型用于训练和调整模型参数。由赛题给出,我们将之与各自的RLE编码联系起来后即可使用。
6-2 验证集(Validation Set)
因为训练集和验证集是分开的,所以模型在验证集上面的精度在一定程度上可以反映模型的泛化能力。在划分验证集的时候,需要注意验证集的分布应该与测试集尽量保持一致,不然模型在验证集上的精度就失去了指导意义。
训练集和测试集赛题方已经给出,我们需要从训练集中划分出验证集。我们的训练集数据量较大,由于算力的限制且为了更快的得出验证集,我们使用留出法得到验证集。即直接将训练集划分成两部分,成为新的训练集和验证集。这种划分方式的优点是最为直接简单;缺点是只得到了一份验证集,有可能导致模型在验证集上过拟合。其应用场景主要是数据量比较大的情况。
留出法划分出的验集可能对评估训练集的泛化能力有一定的影响。不过整体上来看,效果还是可以的。这里选用了1/7的数据作为验证集,6/7的数据作为训练集。
6-3 测试集(Test Set)
验证模型的泛化能力。利用赛题提供的测试集,在进行测试的时候我们采用了TTA(Test time augmention)方法来提分,本质就是对测试集做一个数据加强然后利用加强后的图片得到的预测值和本身得到的预测值做一个算术平均。
这里的Sigmoid的值0.35也是在经过试验后得出的一个比较好的值,后期也可以利用算法来计算最优值。
至此,我们就完成了模型的训练跟预测。
7、模型融合
在尝试单模跑分后,U-net和U-net++效果差的并不大,所以我们尝试了一下模型融合。
通过对不同的模型进行权值分配,以此来达到更好的效果,这个权值分布我们也是根据试验来确定的,并没有采用一些优化的算法,这也是可以提升的地方,结果也有所提升。
8、结果分析
读取测试集图片并查看分割结果: