影象分割必備知識點 | Unet詳解 理論+ 程式碼
阿新 • • 發佈:2020-11-26
文章轉自:微信公眾號【機器學習煉丹術】。文章轉載或者交流聯絡作者微信:cyx645016617
**喜歡的話可以參與文中的討論、在文章末尾點贊、在看點一下唄。**
## 0 概述
語義分割(Semantic Segmentation)是影象處理和機器視覺一個重要分支。與分類任務不同,語義分割需要判斷影象每個畫素點的類別,進行精確分割。語義分割目前在自動駕駛、自動摳圖、醫療影像等領域有著比較廣泛的應用。
![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/be35ddc2874f48cdbbffd6a391a21280~tplv-k3u1fbpfcp-watermark.image)
**上圖為自動駕駛中的移動分割任務的分割結果,可以從一張圖片中有效的識別出汽車(深藍色),行人(紅色),紅綠燈(黃色),道路(淺紫色)等**
Unet可以說是**最常用、最簡單**的一種分割模型了,它簡單、高效、易懂、容易構建、可以從小資料集中訓練。
Unet已經是非常老的分割模型了,是2015年《U-Net: Convolutional Networks for Biomedical Image Segmentation》提出的模型
> 論文連線:https://arxiv.org/abs/1505.04597
在Unet之前,則是更老的FCN網路,FCN是Fully Convolutional Netowkrs的碎屑,不過這個**基本上是一個框架,到現在的分割網路,誰敢說用不到卷積層呢。** 不過FCN網路的準確度較低,不比Unet好用。現在還有Segnet,Mask RCNN,DeepLabv3+等網路,不過今天我先介紹Unet,畢竟一口吃不成胖子。
## 1 Unet
Unet其實挺簡單的,所以今天的文章並不會很長。
### 1.1 提出初衷(不重要)
1. Unet提出的初衷是為了解決醫學影象分割的問題;
2. 一種U型的網路結構來獲取上下文的資訊和位置資訊;
3. 在2015年的ISBI cell tracking比賽中獲得了多個第一,**一開始這是為了解決細胞層面的分割的任務的**
### 1.2 網路結構
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/872c56a9fa2d45fe8eab030c57b3a2f3~tplv-k3u1fbpfcp-watermark.image)
這個結構就是先對圖片進行卷積和池化,在Unet論文中是池化4次,比方說一開始的圖片是224x224的,那麼就會變成112x112,56x56,28x28,14x14四個不同尺寸的特徵。**然後我們對14x14的特徵圖做上取樣或者反捲積,得到28x28的特徵圖,這個28x28的特徵圖與之前的28x28的特徵圖進行通道傷的拼接concat,然後再對拼接之後的特徵圖做卷積和上取樣,得到56x56的特徵圖,再與之前的56x56的特徵拼接,卷積,再上取樣,經過四次上取樣可以得到一個與輸入影象尺寸相同的224x224的預測結果。**
其實整體來看,這個也是一個Encoder-Decoder的結構:
![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/16aa4d6044d3400091235103458a2baa~tplv-k3u1fbpfcp-watermark.image)
Unet網路非常的簡單,前半部分就是特徵提取,後半部分是上取樣。在一些文獻中把這種結構叫做**編碼器-解碼器結構**,由於網路的整體結構是一個大些的英文字母U,所以叫做U-net。
- Encoder:左半部分,由兩個3x3的卷積層(RELU)再加上一個2x2的maxpooling層組成一個下采樣的模組(後面程式碼可以看出);
- Decoder:有半部分,由一個上取樣的卷積層(去卷積層)+特徵拼接concat+兩個3x3的卷積層(ReLU)反覆構成(程式碼中可以看出來);
在當時,Unet相比更早提出的FCN網路,使用**拼接**來作為特徵圖的融合方式。
- FCN是通過特徵圖對應畫素值的相加來融合特徵的;
- U-net通過通道數的拼接,這樣可以形成更厚的特徵,當然這樣會更佳消耗視訊記憶體;
**Unet的好處我感覺是:網路層越深得到的特徵圖,有著更大的視野域,淺層卷積關注紋理特徵,深層網路關注本質的那種特徵,所以深層淺層特徵都是有格子的意義的;另外一點是通過反捲積得到的更大的尺寸的特徵圖的邊緣,是缺少資訊的,畢竟每一次下采樣提煉特徵的同時,也必然會損失一些邊緣特徵,而失去的特徵並不能從上取樣中找回,因此通過特徵的拼接,來實現邊緣特徵的一個找回。**
## 2 為什麼Unet在醫療影象分割種表現好
這是一個開放性的問題,大家如果有什麼看法歡迎回復討論。
大多數醫療影像語義分割任務都會首先用Unet作為baseline,當然上一章節講解的Unet的優點肯定是可以當作這個問題的答案,這裡談一談**醫療影像的特點**
根據網友的討論,得到的結果:
1. 醫療影像語義較為簡單、結構固定。因此語義資訊相比自動駕駛等較為單一,因此並不需要去篩選過濾無用的資訊。**醫療影像的所有特徵都很重要,因此低階特徵和高階語義特徵都很重要,所以U型結構的skip connection結構(特徵拼接)更好派上用場**
2. 醫學影像的資料較少,獲取難度大,資料量可能只有幾百甚至不到100,因此如果使用大型的網路例如DeepLabv3+等模型,很容易過擬合。大型網路的優點是更強的影象表述能力,而較為簡單、數量少的醫學影像並沒有那麼多的內容需要表述,因此也有人發現**在小數量級中,分割的SOTA模型與輕量的Unet並沒有神惡魔優勢**
3. 醫學影像往往是多模態的。比方說ISLES腦梗競賽中,官方提供了CBF,MTT,CBV等多中模態的資料(這一點聽不懂也無妨)。因此**醫學影像任務中,往往需要自己設計網路去提取不同的模態特徵,因此輕量結構簡單的Unet可以有更大的操作空間。**
## 3 Pytorch模型程式碼
這個是我自己寫的程式碼,所以並不是很精簡,但是應該很好理解,和我之前講解的完全一致,(有任何問題都可以和我交流:cyx645016617):
```python
import torch
import torch.nn as nn
import torch.nn.functional as F
class double_conv2d_bn(nn.Module):
def __init__(self,in_channels,out_channels,kernel_size=3,strides=1,padding=1):
super(double_conv2d_bn,self).__init__()
self.conv1 = nn.Conv2d(in_channels,out_channels,
kernel_size=kernel_size,
stride = strides,padding=padding,bias=True)
self.conv2 = nn.Conv2d(out_channels,out_channels,
kernel_size = kernel_size,
stride = strides,padding=padding,bias=True)
self.bn1 = nn.BatchNorm2d(out_channels)
self.bn2 = nn.BatchNorm2d(out_channels)
def forward(self,x):
out = F.relu(self.bn1(self.conv1(x)))
out = F.relu(self.bn2(self.conv2(out)))
return out
class deconv2d_bn(nn.Module):
def __init__(self,in_channels,out_channels,kernel_size=2,strides=2):
super(deconv2d_bn,self).__init__()
self.conv1 = nn.ConvTranspose2d(in_channels,out_channels,
kernel_size = kernel_size,
stride = strides,bias=True)
self.bn1 = nn.BatchNorm2d(out_channels)
def forward(self,x):
out = F.relu(self.bn1(self.conv1(x)))
return out
class Unet(nn.Module):
def __init__(self):
super(Unet,self).__init__()
self.layer1_conv = double_conv2d_bn(1,8)
self.layer2_conv = double_conv2d_bn(8,16)
self.layer3_conv = double_conv2d_bn(16,32)
self.layer4_conv = double_conv2d_bn(32,64)
self.layer5_conv = double_conv2d_bn(64,128)
self.layer6_conv = double_conv2d_bn(128,64)
self.layer7_conv = double_conv2d_bn(64,32)
self.layer8_conv = double_conv2d_bn(32,16)
self.layer9_conv = double_conv2d_bn(16,8)
self.layer10_conv = nn.Conv2d(8,1,kernel_size=3,
stride=1,padding=1,bias=True)
self.deconv1 = deconv2d_bn(128,64)
self.deconv2 = deconv2d_bn(64,32)
self.deconv3 = deconv2d_bn(32,16)
self.deconv4 = deconv2d_bn(16,8)
self.sigmoid = nn.Sigmoid()
def forward(self,x):
conv1 = self.layer1_conv(x)
pool1 = F.max_pool2d(conv1,2)
conv2 = self.layer2_conv(pool1)
pool2 = F.max_pool2d(conv2,2)
conv3 = self.layer3_conv(pool2)
pool3 = F.max_pool2d(conv3,2)
conv4 = self.layer4_conv(pool3)
pool4 = F.max_pool2d(conv4,2)
conv5 = self.layer5_conv(pool4)
convt1 = self.deconv1(conv5)
concat1 = torch.cat([convt1,conv4],dim=1)
conv6 = self.layer6_conv(concat1)
convt2 = self.deconv2(conv6)
concat2 = torch.cat([convt2,conv3],dim=1)
conv7 = self.layer7_conv(concat2)
convt3 = self.deconv3(conv7)
concat3 = torch.cat([convt3,conv2],dim=1)
conv8 = self.layer8_conv(concat3)
convt4 = self.deconv4(conv8)
concat4 = torch.cat([convt4,conv1],dim=1)
conv9 = self.layer9_conv(concat4)
outp = self.layer10_conv(conv9)
outp = self.sigmoid(outp)
return outp
model = Unet()
inp = torch.rand(10,1,224,224)
outp = model(inp)
print(outp.shape)
==> torch.Size([10, 1, 224, 224])
```
先把上取樣和兩個卷積層分別構建好,供Unet模型構建中重複使用。然後模型的輸出和輸入是相同的尺寸,說明模型可以執行。
參考部落格:
1. https://blog.csdn.net/wangdongwei0/article/details/82393275
2. https://www.zhihu.com/question/269914775?sort=created
3. https://zhuanlan.zhihu.com/p/