1. 程式人生 > >影象分割必備知識點 | Unet詳解 理論+ 程式碼

影象分割必備知識點 | Unet詳解 理論+ 程式碼

文章轉自:微信公眾號【機器學習煉丹術】。文章轉載或者交流聯絡作者微信: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/