[譯]使用 U-Net 進行語義分割(第一部分)
- 原文地址:Semantic Segmentation — U-Net (Part 1)
- 原文作者:Kerem Turgutlu
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:JohnJiangLA
- 校對者:haiyang-tju leviding
寫給6個月前的我
我將主要關注語義分割這樣一種畫素級別的分類任務及其特定的一種演算法實現。另外我將提供一些近期一直在做的案例練習。
從定義上講,語義分割是將影象分割為連續部件的過程。例如,對屬於一個人、一輛車、一棵樹或資料集裡的任何其它實體的每個畫素進行分類。
語義分割 VS 例項分割
語義分割相比與它的老哥例項分割來說容易很多。
例項分割中,我們的目標不僅要對每個人,每輛車做出畫素級的預測,同時還要將實體區分為 person 1、person 2、tree 1、tree 2、car 1、car 2 等等。目前最優秀的分割演算法是 Mask-RCNN:一種使用 RPN(Region Proposal Network)、FPN(Feature Pyramid Network)和 FCN(Fully Convolutional Network)[5, 6, 7, 8] 多子網協作的兩階段方法。
圖 4. 語義分割
圖 5. 例項分割
研究案例:Data Science Bowl 2018
Data Science Bowl 2018 剛剛結束,在比賽中我學習到很多。其中最重要的一點可能就是,即使有了相較於傳統機器學習自動化程度更高的深度學習,預處理與後處理可能才是取得優異成績的關鍵。這些都是從業人員需要掌握的重要技能,它們決定了為問題搭建網路結構與模型化的方式。
因為在 Kaggle 上已經有大量對這個任務以及競賽過程中所用方法的討論和解釋,所以我不會詳盡的評述這次競賽中的每個細節。但由於冠軍方案和這篇博文的基礎有關聯,所以會簡要講解它。
Data Science Bowl 2018 和往屆比賽一樣都是由 Booz Allen Foundation 組織。今年的任務是在給定的顯微鏡影象中識別出細胞核,併為其繪製單獨的分割遮罩。
現在,先花一兩分鐘猜下這個任務需要哪種型別的分割:語義還是實體?
這是一個樣本遮罩圖片和原始顯微影象。
圖 6. 細胞核遮罩(左)和原始影象(右)
儘管這個任務起初聽起來像是個語義分割任務,但其實需要例項分割。我們需要獨立地處理影象中的每個核,並將它們識別為 nuclei 1、nuclei 2、nuclei 3 等等,這就類似於前面那個例項中的 Car 1、Car 2、Person 1 等等。也許這項任務的動機是跟蹤記錄細胞樣本中細胞核的大小、數量和特徵。這樣的自動化跟蹤記錄過程非常重要,有助於進一步加速各種疾病治療實驗的研究程序。
你現在可能想,如果這篇文章是關於語義分割的,但如果 Data Science Bowl 2018 是例項分割任務樣例,那麼我為什麼一直要討論這個特定的比賽。如果你在考慮這些,絕對是正確,這次比賽的最終目標並不能作為語義分割的樣例。但是,如何將這個例項分割問題轉化為多分類的語義分割任務。這是我嘗試過的方法,雖然在實踐過程中失敗了但是也對最後成功有一定幫助。
在這三個月的競賽中,在整個論壇中分享或至少明確討論的只有兩種模型(或它們的變體):Mask-RCNN 和 U-Net。正如前面所述,Mask-RCNN 是目前最優秀的物件檢測演算法,它同例項分割中一樣能檢測出單個物件並預測它們的遮罩。但由於 Mask-RCNN 使用了兩階段的方式,需要先優化一個 RPN(Region Proposal Network)然後同時預測邊界框、類別和遮罩,所以部署與訓練都會非常困難。
另一方面,U-Net 是種非常流行的用於語義分割的端到端編解碼網路[9]。最初它也是建立並應用在生物醫學影象分割領域,和這次 Data Science Bowl 非常類似的任務。在這種競賽中沒有銀彈,這兩種架構如果不做後處理或預處理亦或結構上細微的調整,都不能得到較好的預測值。我在這次比賽中並沒有機會嘗試 Mask-RCNN,所以我就圍繞著 U-Net 進行試驗,學習到很多東西。
另外,由於我們的主題是語義分割,Mask-RCNN 就留給其他部落格來解釋。但如果你想在自己的 CV 應用上嘗試它們,這裡有兩個已實現功能並受歡迎的 github 庫:Tensorflow 和 PyTorch。[10, 11]
現在,我們繼續講解 U-Net,並深入研究它的細節...
下面先以它的體系結構開始:
圖 7. 原生 U-Net
對於熟悉傳統卷積神經網路的朋友來說,第一部分(表示為下降)的結構非常眼熟。第一部分可以稱作下降或你可以認為它是編碼器部分,你在這裡用卷積模組處理,然後再使用最大池化下采樣,將輸入影象編碼為不同層級的特徵表示。
網路的第二部分則包括上取樣和級聯,然後是普通的卷積運算。對於一些讀者來說,在 CNN 中使用上取樣可能是個新概念,但其思路很簡單:擴充套件特徵維度,以達到與左側的相應級聯塊的相同大小。這裡的灰色和綠色的箭頭表示將兩個特徵對映在一起。與其他 FCN 分割網路相比,U-Net 在這方面的主要貢獻在於,在上取樣和深入網路過程中,我們將下采樣中的高解析度特徵與上取樣特徵連線起來以便在後續的卷積過程中更好地定位和學習實體的表徵。由於上取樣是稀疏操作,我們需要在早期處理過程中獲取良好的先驗,以更好的表示位置資訊。在 FPN(Feature Pyramidal Networks) 中也有類似的連線匹配分級的思路。
圖 7. 原生 U-Net 張量圖解
我們可以將在下降部分中的一個操作模組定義為“卷積 → 下采樣”。
# 一個取樣下降模組
def make_conv_bn_relu(in_channels, out_channels, kernel_size=3, stride=1, padding=1):
return [
nn.Conv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride, padding=padding, bias=False),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
]
self.down1 = nn.Sequential(
*make_conv_bn_relu(in_channels, 64, kernel_size=3, stride=1, padding=1 ),
*make_conv_bn_relu(64, 64, kernel_size=3, stride=1, padding=1 ),
)
# 卷積然後最大池化
down1 = self.down1(x)
out1 = F.max_pool2d(down1, kernel_size=2, stride=2)
複製程式碼
U-Net 下采樣模組
同樣我們可以在上升部分中定義一個操作模組:“上取樣 → 級聯 → 卷積”。
# 一個取樣上升模組
def make_conv_bn_relu(in_channels, out_channels, kernel_size=3, stride=1, padding=1):
return [
nn.Conv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride, padding=padding, bias=False),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
]
self.up4 = nn.Sequential(
*make_conv_bn_relu(128,64, kernel_size=3, stride=1, padding=1 ),
*make_conv_bn_relu(64,64, kernel_size=3, stride=1, padding=1 )
)
self.final_conv = nn.Conv2d(32, num_classes, kernel_size=1, stride=1, padding=0 )
# 對 out_last 上取樣,並與 down1 級聯,然後卷積
out = F.upsample(out_last, scale_factor=2, mode='bilinear')
out = torch.cat([down1, out], 1)
out = self.up4(out)
# 用於最後預測的 1 * 1 卷積
final_out = self.final_conv(out)
複製程式碼
U-Net 上取樣模組
仔細看下結構圖,你會發現輸出尺寸(388 * 388)與原始輸入(572 * 572)並不一致。如果你希望輸出保持一致的尺寸,你可以使用填充卷積來保持跨級聯的維度一致,就像我們在上面的示例程式碼中所做的那樣。
當提到這種上取樣時,您可能會遇到以下術語之一:轉置卷積、上卷積、反捲積或上取樣。很多人,包括我在內的很多人以及PyTorch技術文件都不喜歡反捲積這個術語,因為在上取樣階段,我們實際上是在做常規的卷積運算,並沒有字面上所謂的“反”。在進一步討論之前,如果你不熟悉基本卷積運算及其算術,我強烈建議你訪問檢視here.。[12]
我將解釋從簡單到複雜的上取樣方法。這裡有三種在 PyTorch 中對二維張量進行上取樣的方法:
最近鄰插值
這是在將張量調整(轉換)為更大張量(例如2x2到4x4、5x5或6x6)時尋找缺失畫素值的最簡單方法。
我們使用 Numpy 逐步實現這個基礎的計算機視覺演算法:
def nn_interpolate(A, new_size):
"""
逐步實現最近鄰插值
"""
# 獲取大小
old_size = A.shape
# 計算擴充後的行與列
row_ratio, col_ratio = new_size[0]/old_size[0], new_size[1]/old_size[1]
# 定義新的行與列位置
new_row_positions = np.array(range(new_size[0]))+1
new_col_positions = np.array(range(new_size[1]))+1
# 按照比例標準化新行與列的位置
new_row_positions = new_row_positions / row_ratio
new_col_positions = new_col_positions / col_ratio
# 對新行與列位置應用 ceil (計算大於等於該值的最小整數)
new_row_positions = np.ceil(new_row_positions)
new_col_positions = np.ceil(new_col_positions)
# 計算各點需要重複的次數
row_repeats = np.array(list(Counter(new_row_positions).values()))
col_repeats = np.array(list(Counter(new_col_positions).values()))
# 在矩陣的各列執行列向插值
row_matrix = np.dstack([np.repeat(A[:, i], row_repeats)
for i in range(old_size[1])])[0]
# 在矩陣的各列執行列向插值
nrow, ncol = row_matrix.shape
final_matrix = np.stack([np.repeat(row_matrix[i, :], col_repeats)
for i in range(nrow)])
return final_matrix
def nn_interpolate(A, new_size):
""向量化最近鄰插值"""
old_size = A.shape
row_ratio, col_ratio = np.array(new_size)/np.array(old_size)
# 行向插值
row_idx = (np.ceil(range(1, 1 + int(old_size[0]*row_ratio))/row_ratio) - 1).astype(int)
# 列向插值
col_idx = (np.ceil(range(1, 1 + int(old_size[1]*col_ratio))/col_ratio) - 1).astype(int)
final_matrix = A[:, row_idx][col_idx, :]
return final_matrix
複製程式碼
[PyTorch] F.upsample(…, mode = “nearest”)
>>> input = torch.arange(1, 5).view(1, 1, 2, 2)
>>> input
(0 ,0 ,.,.) =
1 2
3 4
[torch.FloatTensor of size (1,1,2,2)]
>>> m = nn.Upsample(scale_factor=2, mode='nearest')
>>> m(input)
(0 ,0 ,.,.) =
1 1 2 2
1 1 2 2
3 3 4 4
3 3 4 4
[torch.FloatTensor of size (1,1,4,4)]
複製程式碼
雙線性插值
雙線性插值雖然計算效率不如最近鄰插值,但它是一種更精確的近似演算法。單個畫素值被計算為基於距離的所有其它畫素值的加權平均值。
[PyTorch] F.upsample(…, mode = “bilinear”)
>>> input = torch.arange(1, 5).view(1, 1, 2, 2)
>>> input
(0 ,0 ,.,.) =
1 2
3 4
[torch.FloatTensor of size (1,1,2,2)]
>>> m = nn.Upsample(scale_factor=2, mode='bilinear')
>>> m(input)
(0 ,0 ,.,.) =
1.0000 1.2500 1.7500 2.0000
1.5000 1.7500 2.2500 2.5000
2.5000 2.7500 3.2500 3.5000
3.0000 3.2500 3.7500 4.0000
[torch.FloatTensor of size (1,1,4,4)]
複製程式碼
轉置卷積
在轉置卷積中,我們可以通過反向傳播來學習權重。在論文中,我嘗試了針對各種情況的所有上取樣方法,在實踐中,你可能會更改網路的體系結構,可以嘗試所有這些方法,以找到最適合問題的方法。我個人更喜歡轉置卷積,因為它更可控,但你可以直接使用簡單的雙線性插值或最近鄰插值。
[PyTorch] nn.ConvTranspose2D(…, stride=…, padding=…)
圖 8. 使用不同引數的轉置卷積樣例,轉自 github.com/vdumoulin/c… [12]
在這個 Data Science Bowl 的具體案例中,使用原生 U-Net 的主要缺點就是細胞核的重疊。如前圖所示,建立一個二元的遮罩作為目標輸出,U-Net 能夠準確做出類似的預測遮罩,這樣重疊或鄰近的細胞核就會產生聯合在一起的遮罩。
Fig 9. 重疊的細胞核遮罩
對於例項重疊問題,U-Net 論文的作者使用加權交叉熵來著重細胞邊界的學習。這種方法有助於分離重疊的例項。基本思路是對邊界進行更多的加權操作以使網路能夠學習相鄰例項間的間隔。
圖 10. 加權對映
圖 11. (a)原圖 (b)各例項新增不同底色 (c)生成分割遮罩 (d)畫素加權對映
解決這類問題的另一種方法是將二元的遮罩轉換成複合型別的目標,這是包括獲勝方案等許多競爭選手採用的一種方法。U-Net 的一個優點是可以通過在最後一層使用 1*1 卷積來構建網路以實現任意多個輸出來表示多個型別。
引用自 Data Science Bowl 獲勝方案:
目標為 2 通道遮罩使用 sigmod 啟用函式的網路,即(遮罩 - 邊界,邊界);目標為 3 通道遮罩使用 softmax 啟用函式的網路,即(遮罩 - 邊界,1 - 掩碼 - 邊界) 2 通道全遮罩,即(遮罩,邊界)
在這些預測操作後,就可以使用傳統的影象處理演算法例如 watershed 做後處理進一步分割出單個細胞核。[14]
圖 12. 視覺化分類:前景(綠色)輪廓(黃色)背景(黑色)
這是我第一次鼓起勇氣參與 Kaggle 上官方舉辦的 CV 比賽,而且還是 Data Science Bowl。儘管我最後只以前 20% 的成績完成了比賽(這通常是比賽的平均水準),但我還是很高興地參與了這次 Data Science Bowl 並且學習到了一些實際參與和嘗試才能學習到的東西。主動學習要遠比觀看和閱讀線上課程資源有成效。
作為一名剛開始參與 Fast.ai 課程學習的深度學習從業人員,這是我漫長學習旅程的重要一步,也能從中獲取寶貴的經驗。所以,我建議各位可以特意地去嘗試面對一些你從未見過或解決過的挑戰,以感受學習未知事物的巨大樂趣。
我在這場比賽中學習到的另一個寶貴教訓是,在計算機視覺(同樣適用於 NLP)比賽中,親眼檢查每一個預測結果以判斷哪些有效很重要。如果你的資料集足夠小,那麼應該檢查每個輸出。這可以讓你進一步找到更好的思路,或在程式碼出問題時除錯程式碼。
遷移學習以及其他
目前,我們已經講解了原生 U-Net 的架構模組並如何轉變目標以解決例項分割問題。現在我們來進一步的討論這些型別編解碼網路的靈活性。所謂靈活性,我是指在設計網路時能夠擁有的自由度以及創新性。
遷移學習是個非常給力的想法,所以使用深度學習的人都避不開它。簡單來說,遷移學習就是在缺乏大規模資料集時,使用在擁有大量資料的類似任務上預先訓練好的網路。即使資料足夠的情況下,遷移學習也能一定程度上提升效能,而且不僅可用於計算機視覺中,同時對 NLP 也有效。
遷移學習對類似 U-Net 的體系來說也是一種強力的技術。我們之前已經定義了 U-Net 中兩個重要的組成部分:上取樣和下采樣。這裡我們將它們理解為編碼器和解碼器。編碼器接受輸入並將其編碼到一個低維特徵空間,這就將輸入用更低維度表徵。那麼試想如果用你理想的 ImageNet 替代這個編碼器,比如: VGG, ResNet, Inception, NasNet 等任何你想要的。這些經過高度設計的網路都是在完成一件事:以儘可能優秀的方式對自然影象進行編碼,並且 ImageNet 上可以線上獲取它們的預訓練權值模型。
因此,為什麼不使用它們其中一種架構作為我們的編碼器,再構建一個解碼器,這將與原先的 U-Net 一樣可用,但更好,更生猛。
TernausNet 是 KaggleVagle Carvana 挑戰的獲勝方案的網路架構,它就使用相同的思路,以 VGG11 作為編碼器。[15、16]
Vladimir Iglovikov 和 Alexey Shvets 的 TernausNet
Fast.ai: 動態 U-Net
受到 TernausNet 論文以及其他許多優秀資料的啟發,我將這個想法簡述為,將預訓練或預設的編碼器應用於類似於 U-net 的架構。因此,我提出了一個通用體系結構:動態 U-Net。
動態 U-Net 就是這個想法的實現,它能夠完成所有的計算和匹配,自動地為任何給定的編碼器建立解碼器。編碼器既可以是現成的預訓練的網路,也可以是自定義的網路體系結構。
它使用 PyTorch 編寫,目前在 Fast.ai 庫中。可以參考這個 文件 來檢視實踐樣例或檢視原始碼。動態 U-Net 的主要目標是節省開發時間,以實現用盡可能少的程式碼更簡易地對不同的編碼器進行實驗。
在第 2 部分中,我將解釋針對三維資料的編碼器解碼器模型,例如 MRI(核磁共振成像)掃描影象,並給出我一直在研究的現實案例。
參考文獻
[5] Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks: arxiv.org/abs/1506.01…
[6] Mask R-CNN: https://arxiv.org/abs/1703.06870
[7] Feature Pyramid Networks for Object Detection: https://arxiv.org/abs/1612.03144
[8] Fully Convolutional Networks for Semantic Segmentation: https://people.eecs.berkeley.edu/~jonlong/long_shelhamer_fcn.pdf
[9] U-net: Convolutional Networks for Biomedical Image Segmentation: https://arxiv.org/abs/1505.04597
[10] Tensorflow Mask-RCNN: https://github.com/matterport/Mask_RCNN
[11] Pytorch Mask-RCNN: https://github.com/multimodallearning/pytorch-mask-rcnn
[12] Convolution Arithmetic: https://github.com/vdumoulin/conv_arithmetic
[13] Data Science Bowl 2018 Winning Solution, ods-ai: https://www.kaggle.com/c/data-science-bowl-2018/discussion/54741
[14] Watershed Algorithm docs.opencv.org/3.3.1/d3/db…
[15] Carvana Image Masking Challenge: www.kaggle.com/c/carvana-i…
[16] TernausNet: U-Net with VGG11 Encoder Pre-Trained on ImageNet for Image Segmentation: https://arxiv.org/abs/1801.05746
感謝 Prince Grover 和 Serdar Ozsoy。
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。