. X" ^( `. R# x& t" v
( s) T8 T/ z6 x! u2 P* g$ }7 H$ Q) x
Pytorch实战语义分割(VOC2012)
) u d+ R/ Y! r& {本文参照了《动手深度学习》的9.9、9.10章节,原书使用的是 mxnet 框架,本文改成了pytorch代码。' l1 K4 M4 j/ ]9 u$ N3 D4 r
语义分割(semantic segmentation)问题,它关注如何将图像分割成属于不同语义类别的区域。值得一提的是,这些语义区域的标注和预测都是像素级的。
" ]- C+ a- [! ?
2 c5 p: W3 k+ {. L+ B* ]; d( e& z: j u; V1 D9 D" j. f0 W
语义分割中图像有关狗、猫和背景的标签5 |: `' T6 [0 p
文章目录# X3 e" G6 [1 s- r2 T2 w
' `; Y$ {6 P1 y; L% g9 {: F
1 图像分割和实例分割
- ~- F2 s: X4 w( B5 q) l. L2 Pascal VOC2012语义分割数据集
* k2 y4 {& N0 V5 h- q; Y3 l2.1 导入模块
0 H( j2 D& G- x+ o8 N2 a4 |2.2 下载数据集% x0 i, n3 Q# w+ b1 w- p) I7 t
2.3 可视化数据! u2 o! A; W0 C+ F! K( q# S
2.4 预处理数据
6 D% E) D; o! u5 J. j4 }3 自定义数据集类$ k8 |% u& _: I
3.1 数据集类# L4 _' W6 }: G4 {# o
3.2 读取数据集4 j- [' W; e7 g: z
4 构造模型
' _+ y: [$ t) ]5 K) X K$ B4.1 预训练模型
9 Q# G1 v: ~! ` y3 |) x4.2 修改成FCN2 z1 Q' x* j# Y( y$ G
4.3 初始化转置卷积层
( e4 K- h8 \) ~5 训练模型
. i1 h$ h& E, B7 r- x6 测试模型
9 k& I6 }1 \0 S$ ^$ l6.1 通用型8 E) [1 L* O) m0 G! F* [: v
6.2 不通用
; F" D) v# W6 D1 X* V8 {7 结语
+ F4 z3 q) }5 c+ k% a; j1 图像分割和实例分割- |4 ]! C! ^, b' s) b3 K
. c% z0 f. Z3 e( A计算机视觉领域还有2个与语义分割相似的重要问题,即图像分割(image segmentation)和实例分割(instance segmentation):3 s( ]: v9 K5 D c/ R( o! a6 {
4 ~1 o( B4 {3 B' Y. t' o! a& K* N
图像分割将图像分割成若干组成区域。这类问题的方法通常利用图像中像素之间的相关性。它在训练时不需要有关图像像素的标签信息,在预测时也无法保证分割出的区域具有我们希望得到的语义。以上图的图像为输入,图像分割可能将狗分割成两个区域:一个覆盖以黑色为主的嘴巴和眼睛,而另一个覆盖以黄色为主的其余部分身体。
; ~- i/ d9 c9 ?7 I实例分割又叫同时检测并分割(simultaneous detection and segmentation)。它研究如何识别图像中各个目标实例的像素级区域。与语义分割有所不同,实例分割不仅需要区分语义,还要区分不同的目标实例。如果图像中有两只狗,实例分割需要区分像素属于这两只狗中的哪一只。: Q0 s% L& A1 V3 m4 m
1 E0 j) v' f" W, h6 V) Z0 P0 s$ X2 Pascal VOC2012语义分割数据集
' w q8 A( N$ s f7 I3 q" C- D+ X3 g3 E
2.1 导入模块0 y* K* K' |) ]5 ]- \! e5 a V3 S
import time
/ j/ E w. j) z+ E7 kimport copy* U# N: T# i/ a: C3 f9 h( R
import torch
0 L7 Z1 D9 t S* C# O) S' b6 a9 sfrom torch import optim, nn( T+ ^1 Y$ x2 ~
import torch.nn.functional as F
( z$ k, _6 k$ T$ I: m8 q8 kimport torchvision1 p) W4 }2 G d7 k
from torchvision import transforms
* H7 I5 y3 E+ j3 Gfrom torchvision.models import resnet182 s1 p# Y9 S( F/ R, h
import numpy as np! `. @$ ]9 q2 g: }4 t
from matplotlib import pyplot as plt
* F- J/ W' U0 H( r& O n8 Rfrom PIL import Image
0 @! O8 i; R% \import sys
# _) L; O+ }+ [0 |# rsys.path.append("..")$ |# U/ u; T! i8 P8 F e
from IPython import display
. g2 U9 Y3 W! i5 J, I+ Ofrom tqdm import tqdm
9 J; E/ b4 l5 Z8 p5 A8 Vimport warnings0 |8 z! ?, D8 O8 `' O" f
warnings.filterwarnings("ignore")2 J+ v: n5 ?! e2 Y6 _
) w5 W% H0 I, I' |# b7 ]% ]2.2 下载数据集! t; [9 B, _. c% J- u' g1 O, T
! a* Y+ Y9 j* ]/ [
语义分割的一个重要数据集叫作Pascal VOC2012,点击下载这个数据集的压缩包,大小是2 GB左右,所以下载需要一定时间。下载后解压得到VOCdevkit/VOC2012文件夹,然后将其放置在data文件夹下,VOC2012文件目录是这样的:
B2 k; c+ l {. P& z1 \, A$ m P* n0 l9 y0 a3 r
, s! r q1 z7 B3 i) h3 T
ImageSets/Segmentation路径包含了指定训练和测试样本的文本文件
. W* ]( ]- d Y2 r& XJPEGImages和SegmentationClass路径下分别包含了样本的输入图像和标签。这里的标签也是图像格式,其尺寸和它所标注的输入图像的尺寸相同。标签中颜色相同的像素属于同一个语义类别。
- U& V/ q8 l# W+ p# @2.3 可视化数据
0 X$ x- S4 Y/ O/ g$ O. Q# i e( ?% M" C9 ?; w, g
定义read_voc_images函数将输入图像和标签读进内存。8 p: ^( W- e+ X1 B) Z1 b
: o0 B! k' S& l+ e' Q
def read_voc_images(root="../../data/VOCdevkit/VOC2012", is_train=True, max_num=None):" ?2 r- p, @/ n7 ]
txt_fname = '%s/ImageSets/Segmentation/%s' % (root, 'train.txt' if is_train else 'val.txt'). g' g5 Z1 l( ]
with open(txt_fname, 'r') as f:5 |8 D6 R+ z( Z3 x9 U5 J* z) z1 k
images = f.read().split() # 拆分成一个个名字组成list
: C& d8 E! ]& _9 [4 F/ e5 k if max_num is not None:
4 d+ _, O/ x% y2 A9 Y9 d images = images[:min(max_num, len(images))]! Z0 E |3 Y3 p% S$ ~8 H' v& n
features, labels = [None] * len(images), [None] * len(images)
; e) Q$ X& j7 X% c( A; [' z- O for i, fname in tqdm(enumerate(images)):! B' I; a' L! ^% n% J7 c
# 读入数据并且转为RGB的 PIL image
3 ]1 B8 {: D% f1 l5 v, u9 G* N/ ~# I features = Image.open('%s/JPEGImages/%s.jpg' % (root, fname)).convert("RGB")" U) K* T5 w. V
labels = Image.open('%s/SegmentationClass/%s.png' % (root, fname)).convert("RGB")* ?& o" W( p+ _ E; _* j2 n+ _
return features, labels # PIL image 0-255
/ o+ |3 R) C5 J! C) Y8 ^/ ~5 e3 q$ E* v8 l3 k* X; I" K/ V: t8 \
定义可视化数据集的函数show_images# Z. V! E* m; Q0 B+ ` u9 Z
2 Q ?% Q4 K( J; _) g a8 K1 B
# 这个函数可以不需要
" E- c' o$ C% G! W/ f( l# ?def set_figsize(figsize=(3.5, 2.5)):
8 |# _7 [# p9 P """在jupyter使用svg显示"""3 A* q, k' u: }4 k" u. f
display.set_matplotlib_formats('svg')
- `: U0 L2 y4 L3 A( b # 设置图的尺寸
; t+ Y% ^0 d7 X plt.rcParams['figure.figsize'] = figsize
0 L/ h" n! \- e+ s5 n4 \/ d7 C+ k( _" L l v$ ]
def show_images(imgs, num_rows, num_cols, scale=2):, l1 j; b$ C# q" k9 T. j1 K+ @
# a_img = np.asarray(imgs)
! }" J, D9 }& l figsize = (num_cols * scale, num_rows * scale): x7 f$ s, R% f( s
_, axes = plt.subplots(num_rows, num_cols, figsize=figsize)7 `# {. B. U, I# d7 O, |
for i in range(num_rows):8 u4 T) w( M, w
for j in range(num_cols):, E7 D" {( U- Q3 u
axes[j].imshow(imgs[i * num_cols + j])
" A$ o1 h/ J/ E" f axes[j].axes.get_xaxis().set_visible(False)
8 S0 |7 l. p2 G% Q5 _8 F axes[j].axes.get_yaxis().set_visible(False)
: E$ ^( P1 p# J% ^7 y5 B plt.show()
; `& A' D Z( y% o return axes% Z2 t" g3 U/ H6 \$ ?1 H
3 t0 ~4 V8 o& Q
定义可视化数据集的函数show_images。
$ X+ ]. V& z, {( ^* V( B/ k: o' u+ E# i+ G2 k) x$ b- G3 S) W
# 这个函数可以不需要
9 R9 Z& ?1 y8 h, J9 T% r8 r& ?def set_figsize(figsize=(3.5, 2.5)):
0 |) ]$ T5 _& b2 w k7 j3 u """在jupyter使用svg显示"""& q+ B0 z$ x2 v" l+ Q- b
display.set_matplotlib_formats('svg')8 d/ c( `3 d4 j# R- X$ n
# 设置图的尺寸2 f J& a% G% x0 x; K+ y
plt.rcParams['figure.figsize'] = figsize$ M: C9 z0 l6 ~
' Z+ R0 A+ B3 H4 z0 E9 D' J& J7 N3 A
def show_images(imgs, num_rows, num_cols, scale=2):7 C7 B7 _! {& P2 |* A, w9 l
# a_img = np.asarray(imgs)
7 h; {% ~" P2 Z figsize = (num_cols * scale, num_rows * scale)
7 I; r. g H; G/ h _, axes = plt.subplots(num_rows, num_cols, figsize=figsize)( s3 F- l! |0 S: n$ z, z
for i in range(num_rows):+ C. b9 u/ d! V& {; S
for j in range(num_cols):
( ] Z {! u7 A+ I7 ~ axes[j].imshow(imgs[i * num_cols + j])
5 I. M& u- B6 L9 o$ Y axes[j].axes.get_xaxis().set_visible(False)( f& d6 l. J3 ]; G6 d
axes[j].axes.get_yaxis().set_visible(False); J! }$ ` i7 k% i
plt.show(): l R% A$ ]7 L _! R: Z' T q
return axes
5 M3 }% o4 v( y画出前5张输入图像和它们的标签。在标签图像中,白色和黑色分别代表边框和背景,而其他不同的颜色则对应不同的类别。! z3 P3 F# A2 @& I4 n
6 u, Q1 T7 t! s: A5 z# 根据自己存放数据集的路径修改voc_dir
4 K& f6 F4 h+ }0 f( O" e0 z) ^voc_dir = r"[local]\VOCdevkit\VOC2012"3 a9 I, s" E0 i% {+ P5 m
train_features, train_labels = read_voc_images(voc_dir, max_num=10)5 d; G; g2 F- L6 T- v5 a
n = 5 # 展示几张图像
, t, Q/ Z' z# N' g9 Q) t Aimgs = train_features[0:n] + train_labels[0:n] # PIL image
: e2 F. O3 y+ X- D, vshow_images(imgs, 2, n)
) R5 ~1 S2 W O/ [/ Y
- h# \. U- R w. z1 j+ E
3 d0 l1 i! @8 T& f. C t
. t7 l% \ A* A& a2 _* _列出标签中每个RGB颜色的值及其标注的类别。0 Q5 l( c5 f2 F4 g* w( H8 K* W
# 标签中每个RGB颜色的值, Z( i5 h, q. V+ V! ~: ?% I
VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],% g& c1 ^; u* C7 Y1 j3 y. C4 D
[0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128],/ ~) b& c) _3 C: G0 a
[64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0],- D+ O' f; v, i9 O; m' J/ ^+ l, ]
[64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128],7 O% ^5 A/ d1 j, |
[0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0],) ?' \) c% B& r0 B" n) k
[0, 64, 128]]
8 j, W! m2 U4 F& E! p$ }) n6 b* p1 g# 标签其标注的类别, h: A$ \" t4 O5 r$ w$ K, p
VOC_CLASSES = ['background', 'aeroplane', 'bicycle', 'bird', 'boat',6 I- y$ }: j* ~. J7 y b
'bottle', 'bus', 'car', 'cat', 'chair', 'cow',1 x& U0 ]8 f5 Z+ Y
'diningtable', 'dog', 'horse', 'motorbike', 'person',; v5 k) ?) I5 w) @# Z0 u* f2 g
'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor']
. R1 G5 R8 y, y- r y6 ` \2 A; O有了上面定义的两个常量以后,我们可以很容易地查找标签中每个像素的类别索引,voc_label_indices是根据colormap2label把标签里的 rgb 颜色对应上面的VOC_COLORMAP中的下标给取出来,当作 label 。
' v) p, n6 \& c; N8 a* m3 V1 G# t$ H- o" T' W# | ^
有了上面定义的两个常量以后,我们可以很容易地查找标签中每个像素的类别索引,voc_label_indices是根据colormap2label把标签里的 rgb 颜色对应上面的VOC_COLORMAP中的下标给取出来,当作 label 。
$ d t4 n! C3 ^# O! Hcolormap2label = torch.zeros(256**3, dtype=torch.uint8) # torch.Size([16777216])
7 b% i+ U4 j5 c" k3 ufor i, colormap in enumerate(VOC_COLORMAP):* `7 Z4 m% q% a7 l0 F" j. m' E/ X
# 每个通道的进制是256,这样可以保证每个 rgb 对应一个下标 i
2 m$ b4 ^6 u0 Z+ A& I/ P9 |4 n colormap2label[(colormap[0] * 256 + colormap[1]) * 256 + colormap[2]] = i
, G$ @* F; u1 v1 Z' d4 c# l* g: Z; @. E! s$ \; q- U6 b& I! C
# 构造标签矩阵
0 d/ Q( z8 p& T0 H( Ddef voc_label_indices(colormap, colormap2label):" J4 v( M3 L. D: O4 Y
colormap = np.array(colormap.convert("RGB")).astype('int32')
/ T& Y; C9 J& l1 m0 S i7 j! V idx = ((colormap[:, :, 0] * 256 + colormap[:, :, 1]) * 256 + colormap[:, :, 2])
6 o9 }+ x1 ?1 `$ F6 z9 | return colormap2label[idx] # colormap 映射 到colormaplabel中计算的下标
6 y1 ~, Y" L- V, \! D
7 d- x7 G4 P8 N可以打印一下结果
, z1 }- o$ w8 C2 [: D' T4 Z$ e
& }# V! A8 P% E Z) X: e! G. cy = voc_label_indices(train_labels[0], colormap2label)
2 Z ~: N! a) ~9 F/ P5 mprint(y[100:110, 130:140]) #打印结果是一个int型tensor,tensor中的每个元素i表示该像素的类别是VOC_CLASSES
. O: n( Q5 |( f! {9 ^+ d7 M' l% y( X: E; ~# b! @
2.4 预处理数据, ^# Q: \9 S- F8 r( h( o
# X# L" }3 z: J8 W' ?, b. f在语义分割里,如果使用缩放图像使其符合模型的输入形状的话,需要将预测的像素类别重新映射回原始尺寸的输入图像,这样的映射难以做到精确,尤其是在不同语义的分割区域。所以选择将图像裁剪成固定尺寸而不是缩放。具体来说,我们使用图像增广里的随机裁剪,并对输入图像和标签裁剪相同区域。* T' c! ]1 ^3 T2 T1 e* }+ v: ~( q4 E
8 r) o Z% w. a O. }% i0 u# c) B" p! M
def voc_rand_crop(feature, label, height, width):, b& ?/ o5 M- \' v$ G( }& W) I
"""
. X9 |0 ?0 r5 Z! B5 p4 w 随机裁剪feature(PIL image) 和 label(PIL image).+ X/ L/ m& V1 ]/ W: r
为了使裁剪的区域相同,不能直接使用RandomCrop,而要像下面这样做1 W9 d4 l. J* U
Get parameters for ``crop`` for a random crop.
" T7 V7 ?9 }1 _" R# M) _ Args:$ A( T+ m) k1 w h
img (PIL Image): Image to be cropped.+ h4 H+ ~7 U2 g- _, E( D+ I0 s( A
output_size (tuple): Expected output size of the crop.9 M! R4 ^/ B7 M/ _
Returns:4 u1 z" ]7 p0 M! L
tuple: params (i, j, h, w) to be passed to ``crop`` for random crop.. N- {' M( I4 e: e
"""
# A! M& C; ^. b t# g i,j,h,w = torchvision.transforms.RandomCrop.get_params(feature, output_size=(height, width))) R& W0 ^/ G& b4 R' ^- \# \7 L
feature = torchvision.transforms.functional.crop(feature, i, j, h, w)
" f2 E: v0 ~3 D) v label = torchvision.transforms.functional.crop(label, i, j, h, w)
' F+ t/ _$ t- S return feature, label% O# V! N3 a$ V: A) M: h, j
/ C1 h7 j ^2 t- b: ?) p# F# 显示n张随机裁剪的图像和标签,前面的n是5# W* ~- C( x$ c1 o9 I' p+ u
imgs = []! G% W! @$ H$ D
for _ in range(n):
7 W$ x7 u8 W( [ J imgs += voc_rand_crop(train_features[0], train_labels[0], 200, 300)
) h8 j: e/ m) `, e- A$ _show_images(imgs[::2] + imgs[1::2], 2, n);
$ f2 S9 z* e/ B0 \5 Y, `
2 i( E8 ] W% b" G a/ A
2 l, R" J, m9 G* k9 c6 Q/ u
, V, r. p) X; U) p
?# A+ [7 P$ a% L, Z
( R+ J. y- H9 Y3 d/ e) P0 Z6 W
' ^4 t+ P( D: S$ p+ o
3 自定义数据集类! Z0 r" [6 u3 A- x8 x
+ r" d, }/ ^1 J. U2 q5 Y& @3.1 数据集类" M! O. B2 p, J" M
! @6 s. x2 i; w# p" Q3 d. d9 etorch.utils.data.Dataset是表示数据集的抽象类,因此自定义数据集应继承Dataset并覆盖以下方法
6 | a Z4 Q8 O! o" w* K; \' |# }6 O; W
__len__ 实现 len(dataset) 返还数据集的尺寸。$ D- { C. ^. s- o& C4 l
__getitem__用来获取一些索引数据,例如 dataset[idx] 中的(idx)。7 h# ~( s# x e% X' Z% P5 V' a' X
由于数据集中有些图像的尺寸可能小于随机裁剪所指定的输出尺寸,这些样本需要通过自定义的filter函数所移除。此外,因为之后会用到预训练模型来做特征提取器,所以我们还对输入图像的 RGB 三个通道的值分别做标准化。
" s5 O& V3 P* v/ e4 O, p8 y/ E8 o. ?
) x) k% U$ Q) N- r: @3 n, M$ s* l2 Yclass VOCSegDataset(torch.utils.data.Dataset):5 M; _ @1 Z. [% j9 X
def __init__(self, is_train, crop_size, voc_dir, colormap2label, max_num=None):
' y' K% ?! \6 B/ |, q """
# N% L l4 j! x& S! R crop_size: (h, w)
* u8 `7 Q, [+ Y """0 V+ Q) }) N( C, }- R
# 对输入图像的RGB三个通道的值分别做标准化4 J0 R) J& X) {
self.rgb_mean = np.array([0.485, 0.456, 0.406])
. _0 Z5 ?1 Y2 O# Z" K2 \, K8 } self.rgb_std = np.array([0.229, 0.224, 0.225]), Y2 E y4 z7 ?& O) w9 V; A
self.tsf = torchvision.transforms.Compose([2 T9 o+ n9 @5 C z0 _
torchvision.transforms.ToTensor(),, x8 _# `% t3 z; T$ O
torchvision.transforms.Normalize(mean=self.rgb_mean, std=self.rgb_std)])
5 [. o3 n- E/ p+ @6 O3 y% R0 |7 u self.crop_size = crop_size # (h, w)* N: s' o1 j. J& J, S* l
features, labels = read_voc_images(root=voc_dir, is_train=is_train, max_num=max_num)
+ {! H; k2 d: G0 |2 h& I; r# 由于数据集中有些图像的尺寸可能小于随机裁剪所指定的输出尺寸,这些样本需要通过自定义的filter函数所移除6 M6 M6 _2 F* p0 m1 w* U: N4 K
self.features = self.filter(features) # PIL image- M0 e; V: s4 H0 l7 x* W9 S
self.labels = self.filter(labels) # PIL image
/ C3 n& _+ M. [8 d" w; F1 j9 z self.colormap2label = colormap2label. \2 x' b7 R; {/ B; c; j' u
print('read ' + str(len(self.features)) + ' valid examples')! [( x0 j- t8 q
! _8 U0 C. F: C. {) p; ~4 _5 O. O def filter(self, imgs):
$ s6 W0 ?/ X+ Z$ r return [img for img in imgs if (
* S$ R: {3 h0 ~. a5 L) m img.size[1] >= self.crop_size[0] and img.size[0] >= self.crop_size[1])]6 r1 M0 b& ~8 J/ V. u. c
& G& M8 m8 \2 d7 U/ g$ Q def __getitem__(self, idx):
2 b1 q, O, u* S* b$ A: _" l feature, label = voc_rand_crop(self.features[idx], self.labels[idx], *self.crop_size)
4 @5 M- r3 A. A5 m8 W; c( E # float32 tensor uint8 tensor (b,h,w)8 X5 s7 g% G) ]0 t! u9 d6 w! t+ g3 Z
return (self.tsf(feature), voc_label_indices(label, self.colormap2label))# I4 a; f0 r# f9 R8 E
, C5 P9 Q( r% ]7 m, T def __len__(self):
7 _/ Z1 y6 m2 C6 H6 |$ x return len(self.features)( K" a1 m2 O/ s# Z; v
3.2 读取数据集% V" n& M7 n& R3 P G) {! G& k; B
( m- e" n, o1 ~$ j5 M+ @* \. y1 `
通过自定义的VOCSegDataset类来分别创建训练集和测试集的实例。因为待会用的是全卷积网络,所以随机裁剪的输出图像的形状可以自己指定,这里指定为320×480 320\times 480320×480。
1 w6 ^/ [1 D) e3 h
1 ^+ _% t+ m1 Z+ O5 ?, b1 V2 Wbatch_size = 32 # 实际上我的小笔记本不允许我这么做!哭了(大家根据自己电脑内存改吧). q6 N2 E! B$ N5 r
crop_size = (320, 480) # 指定随机裁剪的输出图像的形状为(320,480), }8 h, f& e* p# d. R) o+ h h
max_num = 20000 # 最多从本地读多少张图片,我指定的这个尺寸过滤完不合适的图像之后也就只有1175张~, L2 d. A* H0 J3 W, k
% r" a3 S7 S: D
# 创建训练集和测试集的实例; Q$ T. h7 K, I+ _3 c; Y8 R' ~
voc_train = VOCSegDataset(True, crop_size, voc_dir, colormap2label, max_num)
/ F" Z5 D+ e$ a @$ P0 avoc_test = VOCSegDataset(False, crop_size, voc_dir, colormap2label, max_num)1 ?$ Q8 c1 Z e. ]( x- Q/ r/ n
1 f: b+ r4 I6 x7 [& \
# 设批量大小为32,分别定义【训练集】和【测试集】的数据迭代器
! R: W6 a1 N4 unum_workers = 0 if sys.platform.startswith('win32') else 4
) I6 ~1 b: W6 F3 O) y! B5 E% Utrain_iter = torch.utils.data.DataLoader(voc_train, batch_size, shuffle=True,/ [6 {6 O j" V
drop_last=True, num_workers=num_workers)' @. a* X1 p+ w3 E b
test_iter = torch.utils.data.DataLoader(voc_test, batch_size, drop_last=True,
- m0 E* {* y7 R1 v5 V num_workers=num_workers)+ |6 f9 K4 O3 j0 ^+ s/ |; n
$ J! i# o( A- E, W" a# 方便封装,把训练集和验证集保存在dict里6 N6 Y, @. h' s
dataloaders = {'train':train_iter, 'val':test_iter}
9 O8 Y+ B/ C) Tdataset_sizes = {'train':len(voc_train), 'val':len(voc_test)}) p+ b( E9 I8 G; h2 K5 u
3 y# s. r# m% d+ }7 B
4 构造模型4.1 预训练模型下⾯我们使⽤⼀个基于 ImageNet 数据集预训练的 ResNet-18 模型来抽取图像特征。 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')# S( m6 O# A/ W) N: ^+ e7 w
2 \2 C% d1 j5 j5 S/ s* i8 p$ N. W% rnum_classes = 21 # 21分类,1个背景,20个物体' O n u+ c9 P ~: Y
model_ft = resnet18(pretrained=True) # 设置True,表明要加载使用训练好的参数$ Y( }2 n6 ?& V$ I0 u
2 w; c/ t; B# D6 S4 m1 u, y
# 特征提取器
5 J$ y" S$ X5 m ]# f5 v% jfor param in model_ft.parameters():: `5 h" P" E3 I
param.requires_grad = False0 P# T/ {3 n7 S5 w; T
4.2 修改成FCN3 w2 h2 l# d( G) k1 H
1 `8 i& j% h+ z% N6 G
全卷积⽹络(顾名思义全部都是卷积层)先使⽤卷积神经⽹络抽取图像特征,然后通过 1×1 1\times 11×1 卷积层将通道数变换为类别个数,最后通过转置卷积层将特征图的⾼和宽变换为输⼊图像的尺⼨。模型输出与输⼊图像的⾼和宽相同,并在空间位置上⼀⼀对应:. O+ n, ?5 R0 W# X
最终输出的通道包含了该空间位置像素的类别预测。
+ z& F9 |" H% }6 }, q3 P' e! X$ v& u) ]& m& d
对于转置卷积层,如果步幅为 S SS、填充为 S/2 S/2S/2 (假设为整数)、卷积核的⾼和宽为 2S 2S2S,转置卷积核将输⼊的⾼和宽分别放⼤ S SS 倍。
$ P d* J2 h9 e/ v4 t. g* [( O3 n0 y( } w
可以先打印model_ft,可见 ResNet-18 的最后两层分别是全局最⼤池化层GlobalAvgPool2D 和 全连接层。全卷积⽹络不需要使⽤这些层。通过测试,当输入图像的 size 是(batch,3,320,480) (batch,3,320,480)(batch,3,320,480) 时,通过除最后两层的预训练网络后输出的大小是 (batch,512,10,15) (batch,512,10,15)(batch,512,10,15),也就是 feature featurefeature 的宽高比输入缩小了 32 3232 倍,只需要用转置卷积层将其放大 32 3232 倍即可。
( L' ~) E+ h2 ?. {* `% j
r Z0 X' N( d' ~3 ^9 J8 omodel_ft = nn.Sequential(*list(model_ft.children())[:-2], # 去掉最后两层
0 h! ?' U! G4 h nn.Conv2d(512,num_classes,kernel_size=1), # 用大小为1的卷积层改变输出通道为num_class
" g4 A8 h! u$ x3 j/ z: g nn.ConvTranspose2d(num_classes,num_classes, kernel_size=64, padding=16, stride=32)).to(device) # 转置卷积层使图像变为输入图像的大小
2 I( `: H7 D2 n+ H. A+ z' Q$ u7 \8 }$ k0 l) O( f" S3 K5 |
# 对model_ft做一个测试/ s6 n+ o9 K( `. t$ o5 h- a
x = torch.rand((2,3,320,480), device=device) # 构造随机的输入数据
: I8 n8 x6 e+ s# `0 B/ d% ]print(net(x).shape) # 输出依然是 torch.Size([2, 21, 320, 480]) . l6 x2 ^* G# e+ C7 w7 p6 |
/ `; _% m+ v8 c8 o/ c. n
# 打印第一个小批量的类型和形状。不同于图像分类和目标识别,这里的标签是一个三维数组
' r& \. V' t: P# for X, Y in train_iter:
: Y1 X+ p# H; m7 h. K$ e. V# print(X.dtype, X.shape)+ v# R& o( }% z, c& A
# print(Y.dtype, Y.shape), ?/ i) x7 @! B3 E: ?* l
# break; C' g/ O, s, `+ I @7 _
6 l' q% l+ v( O5 X5 X2 V+ t' _
5 [: P) h5 g6 T6 w, ]' T! p4.3 初始化转置卷积层
9 `2 R3 E* @! n3 B7 U3 f
5 ^+ H( I. [0 o) Y在图像处理中,我们有时需要将图像放⼤,即上采样(upsample)。上采样的⽅法有很多,常⽤的有双线性插值。简单来说,为了得到输出图像
6 n2 C& o* p% O( p# t; T# y在坐标 (x,y) (x, y)(x,y)上的像素,先将该坐标映射到输⼊图像的坐标 (x',y') (x′, y′ )(x′,y′)。例如,根据输⼊与输出的尺⼨之⽐来映射。映射后的 x' x′x′ 和 y' y′y′ 通常是实数。然后,在输⼊图像上找到与坐标 (x',y') (x′, y′ )(x′,y′)最近的 4 44 个像素。最后,输出图像在坐标 (x,y) (x, y)(x,y)上的像素依据输⼊图像上这4 44个像素及其与 (x',y') (x′, y′ )(x′,y′)的相对距离来计算。双线性插值的上采样可以通过由以下bilinear_kernel函数构造的卷积核的转置卷积层来实现。
N9 J1 c7 y* R, P4 `9 G1 S/ M2 i1 ^3 k" ]* L
: w6 H1 g( A. @; R0 v& \
# 双线性插值的上采样,用来初始化转置卷积层的卷积核/ F7 Z+ u/ l/ v9 @
def bilinear_kernel(in_channels, out_channels, kernel_size):
- ^6 W" ]4 `( N! K) t h- A3 l factor = (kernel_size+1)//2- n+ d, I! K8 D. m7 ]( e) G6 W2 k$ W
if kernel_size%2 == 1:
0 \8 A* E, B6 r; q3 W center = factor-14 ]1 n8 R4 Q0 H/ }+ f8 ^8 S: j! L
else:6 J' u; I: ^# V; \: Q
center = factor-0.5# o7 E. T& O3 e5 p3 z
og = np.ogrid[:kernel_size, :kernel_size]: B% m: o0 R% w3 w8 H+ X8 X ^2 \! F
filt = (1-abs(og[0]-center)/factor) * (1-abs(og[1]-center)/factor). _! T( g; N1 p3 O5 @8 Q
weight = np.zeros((in_channels,out_channels, kernel_size,kernel_size), dtype='float32')
$ v1 v. W4 x# x# I, {4 R weight[range(in_channels), range(out_channels), :, :] = filt
) S; W- ]! y5 Y0 M weight = torch.Tensor(weight)* K0 ?+ D# G* ^& `' p
weight.requires_grad = True
+ g, j. l7 M7 N return weight
% i, e% Z% D* W/ }7 M) N4 `4 K/ ?" _! L
2 u* t- G/ e, K; X7 ]
# |. x! G+ G0 k+ y在全卷积⽹络中,将转置卷积层初始化为双线性插值的上采样。对于1×1 1\times 11×1卷积层,采⽤Xavier XavierXavier随机初始化。; m9 u; G* o5 S1 ^2 g. j+ b
' j1 b+ [# M+ N; r5 [" L, g
nn.init.xavier_normal_(model_ft[-2].weight.data, gain=1); a% I2 D' K7 W; U q/ F: G
model_ft[-1].weight.data = bilinear_kernel(num_classes, num_classes, 64).to(device)! }; }0 Q, `$ L& O
, ^/ u8 y" O' l
, u3 Z! W `4 T: E6 C3 k) }, g
4 Y5 U7 o8 ]. Q1 X, i3 p5 训练模型现在可以开始训练模型了。这⾥的损失函数和准确率计算与图像分类中的并没有本质上的不同。有一个 blog 我认为说的很详细,图也画得很好:https://blog.csdn.net/Fcc_bd_stars/article/details/105158215 6 e; Z1 w! G4 X
def train_model(model:nn.Module, criterion, optimizer, scheduler, num_epochs=20):
0 Z, e" j8 H) t: z since = time.time()* K) O B3 E0 z4 C1 I
best_model_wts = copy.deepcopy(model.state_dict())
- l; F5 f2 X4 `4 e! t8 Q best_acc = 0.01 ?, C0 q$ \0 e
# 每个epoch都有一个训练和验证阶段9 P4 C' T4 N9 Q9 z% j' p- u
for epoch in range(num_epochs):
& A' ]) `( s% G& ~% G! b; E print('Epoch {}/{}'.format(epoch, num_epochs-1))
. x0 A% e$ ~* R& e% q% t$ k print('-'*10)9 n. y5 P4 V6 S7 E. E0 R7 \
for phase in ['train', 'val']:
5 Z; D7 ~) D8 Q3 {: m4 n# J if phase == 'train':9 q5 Z# L; u8 ^& ^, n, o+ |9 X
scheduler.step()
4 z2 H2 M$ E2 N% m model.train()) X' ~/ [& k7 i
else:
g& U& `" m0 N5 _3 \: t model.eval()+ ^3 b, d# G, L9 T6 F# X" d5 e
runing_loss = 0.0" ^+ h; V" n) \ R9 M$ P( p8 @5 y
runing_corrects = 0.09 {$ J4 e9 k/ _. [5 L" f6 _! f& L
# 迭代一个epoch0 R. f2 C; x; _5 R" @6 t1 O
for inputs, labels in dataloaders[phase]:( a" {( q) o1 A- q
inputs, labels = inputs.to(device), labels.to(device)) E: h3 G# m9 [8 q e
optimizer.zero_grad() # 零参数梯度
1 A: t7 l7 b! `6 h9 P # 前向,只在训练时跟踪参数 V* Y# M9 b5 S
with torch.set_grad_enabled(phase=='train'):
# y0 y- J) L' [; `8 |/ t logits = model(inputs) # [5, 21, 320, 480]/ T3 [7 N$ h1 F) x/ s2 P) D# H
loss = criteon(logits, labels.long())
& Z5 P# |' M) e4 @% u& C # 后向,只在训练阶段进行优化! Z" x4 i6 |7 J% M$ ` h) J6 H
if phase=='train':4 T3 i0 Y. F: N1 k9 c% z
loss.backward()) J- w6 R9 g Q; N R
optimizer.step()& M4 Q3 t4 P, S5 f5 ~/ N2 x
# 统计loss和correct* o+ ~2 U; u6 }( P
runing_loss += loss.item()*inputs.size(0)
4 W+ j5 y+ b0 y! S8 d. p runing_corrects += torch.sum((torch.argmax(logits.data,1))==labels.data)/(480*320)
7 h$ g, P9 J$ o- C$ H
, X0 T8 l4 n+ ]8 q; k$ q epoch_loss = runing_loss / dataset_sizes[phase]- V& F7 c; ]5 k1 r; j3 \
epoch_acc = runing_corrects.double() / dataset_sizes[phase]8 \5 K9 w6 ?8 n( _8 I! ]
print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))2 @2 O2 q& I0 W( Q
# 深度复制model参数6 l7 p1 l( e5 L4 e, T
if phase=='val' and epoch_acc>best_acc:( T B3 ^1 ^4 f) k- j
best_acc = epoch_acc1 R4 \8 C- C% R1 u8 X% N8 s! b0 p% m
best_model_wts = copy.deepcopy(model.state_dict())
' d6 e# D! U" m$ P$ \ print()
: `# v$ f( m9 {) G time_elapsed = time.time() - since;
0 h8 g- [0 z- t+ J- i2 d print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed//60, time_elapsed%60))
1 H Z$ [2 g5 m$ o* n # 加载最佳模型权重4 R8 I& H1 S! {* c! h, e3 t/ F# E2 w3 q
model.load_state_dict(best_model_wts)% J) U' _' V" r& a
return model
+ A# I$ _9 q6 W9 y: P; E/ Y1 ]" I6 e J/ ~- \; H* I* Y$ C8 L
下面定义train_model要用到的参数,开始训练3 l) W; u% f( L+ r. r" Q! u
9 x, b, n$ ~8 M m/ V+ r5 }7 Tepochs = 5 # 训练5个epoch7 `8 j# Q$ @* S* |1 N
criteon = nn.CrossEntropyLoss()
9 X9 @; m0 c4 f b+ Joptimizer = optim.SGD(model_ft.parameters(), lr=0.001, weight_decay=1e-4, momentum=0.9)
4 K( S1 e6 a7 e- G3 `8 \# 每3个epochs衰减LR通过设置gamma=0.1
8 h8 a9 [' O+ J Bexp_lr_scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)' x. C" m: }# y* T8 G! u0 l
; C4 q P( `; P! @# 开始训练
% L }1 W* L1 d' C9 N. d. V" ?model_ft = train_model(model_ft, criteon, optimizer, exp_lr_scheduler, num_epochs=epochs)
6 N8 ]& f3 k, j" I, ]
6 h" _ J+ x9 {6 y/ B' j' q6 测试模型为了可视化每个像素的预测类别,我们将预测类别映射回它们在数据集中的标注颜⾊。 def label2image(pred): A) {8 ]1 X& b
# pred: [320,480]6 Y; v4 x5 W6 W5 U
colormap = torch.tensor(VOC_COLORMAP,device=device,dtype=int)
: r0 i4 I6 Y. f# |+ O; X# @9 f' b x = pred.long()' [0 S8 L- O1 v2 j
return (colormap[x,:]).data.cpu().numpy()
% n3 \. l4 U [, Z1 b
3 X, ]- S- \ N0 u$ @下面这里提供了两种测试形式 6.1 通用型其实如果要用于测试其它数据集,也是要改动一下的 : ) 😃 mean=torch.tensor([0.485, 0.456, 0.406]).reshape(3,1,1).to(device)
& m/ W6 {8 m" R y( I" Estd=torch.tensor([0.229, 0.224, 0.225]).reshape(3,1,1).to(device)6 T0 u! O/ u' X7 c
def visualize_model(model:nn.Module, num_images=4):- x3 c3 n4 r- a* I4 E3 {
was_training = model.training
6 u* u+ R8 i! B: c+ i; U* @ model.eval()1 M$ ~1 ~' ] z( ^ G$ S5 I
images_so_far = 0
5 j$ A; M2 ], i! I% X( ] n, imgs = num_images, []
4 F; n: t3 Y& @( l, Q& i- U with torch.no_grad():
- ]& H. O! a0 l, o4 h( _" Y for i, (inputs, labels) in enumerate(dataloaders['val']):1 u% R1 O/ c" U' q- W" o
inputs, labels = inputs.to(device), labels.to(device) # [b,3,320,480]
& f9 c- F& X* u+ N outputs = model(inputs)3 T8 A8 N( _' k3 Y# r
pred = torch.argmax(outputs, dim=1) # [b,320,480]
8 C1 u0 d7 W' W3 n9 ]9 n0 P inputs_nd = (inputs*std+mean).permute(0,2,3,1)*255 # 记得要变回去哦
2 S& X2 S' Q) G+ a7 J9 Y2 A6 l. y' M, N/ j# ~4 I
for j in range(num_images):
' e+ ^. K" _& m" c: j* S3 Y+ T images_so_far += 1
6 A( b( x7 c A7 O F pred1 = label2image(pred[j]) # numpy.ndarray (320, 480, 3)! j! n7 `) e# y. j- ?
imgs += [inputs_nd[j].data.int().cpu().numpy(), pred1, label2image(labels[j])]
7 K$ u5 R+ E v9 p9 O if images_so_far == num_images:
+ P3 y0 W N ~/ f0 m! X9 A model.train(mode=was_training)( v$ u3 Y. G; B! y
# 我已经固定了每次只显示4张图了,大家可以自己修改
. T+ x8 l* ^6 |9 ^( A show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n)
6 i$ B% c( o0 ?9 v6 u return model.train(mode=was_training)( E4 h/ q+ H8 _0 R
s: P8 F8 X6 N; ~# j/ w' L3 F# 开始验证
" P4 h4 b4 g4 Gvisualize_model(model_ft)9 l( G M5 V/ g2 F- E
6.2 不通用在预测时,我们需要将输⼊图像在各个通道做标准化,并转成卷积神经⽹络所需要的四维输⼊格式。 # 预测前将图像标准化,并转换成(b,c,h,w)的tensor
( w( m* b# N' t% |) c6 ~def predict(img, model):7 {3 \+ \/ T: a1 w
tsf = transforms.Compose([
: m6 o: [7 v% T( I, L8 a transforms.ToTensor(), # 好像会自动转换channel; n, J8 ~- i; }
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])])" @ F$ T8 \ G: Z2 n+ |
x = tsf(img).unsqueeze(0).to(device) # (3,320,480) -> (1,3,320,480). B+ g5 H: }8 j. d
pred = torch.argmax(model(x), dim=1) # 每个通道选择概率最大的那个像素点 -> (1,320,480); q) r" C w$ _2 v; |' k) S
return pred.reshape(pred.shape[1],pred.shape[2]) # reshape成(320,480)+ _" g9 p! `& w2 l+ M( s) {' p) f) F
- I/ Y2 o8 T- f+ Rdef evaluate(model:nn.Module):
3 j7 i4 k" n- q j5 M: b5 P model.eval()
$ b! x9 u6 H% w0 L& j( Q test_images, test_labels = read_voc_images(voc_dir, is_train=False, max_num=10)
& k9 D4 a8 n, V0 ^/ `3 o n, imgs = 4, []( y# A) |# s! S) a8 i" F4 c
for i in range(n):
: h/ a, R [0 z/ w- P5 x/ Y- N$ ] xi, yi = voc_rand_crop(test_images, test_labels, 320, 480) # Image
( Z& T$ [, \: n pred = label2image(predict(xi, model))
0 ^2 |7 V/ R) O* A( w/ h imgs += [xi, pred, yi]
0 k) K# H! }/ Z8 `! T* ^ show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n)7 A) G5 F. S3 k2 S, m
) O+ V# k5 v8 d8 z; ?# 开始测试
+ k, a2 k7 l5 r* y/ _evaluate(model_ft)/ ]: \( J/ c" r4 K) O+ v$ G0 B
! ]8 @9 r# e* u a) g2 a- `; a7 结语我只训练了3个epoch,下面是训练输出
7 b9 ~4 y3 Z' n# C8 ?Epoch 0/2
' H, Y1 H' P0 \, l7 O4 @----------
3 c3 T% o7 g! {) q+ x5 Q7 L6 Qtrain Loss: 1.7844 Acc: 0.5835
( }9 J) u! ?7 N" K; E) Hval Loss: 1.1669 Acc: 0.6456
, [$ M, X! T1 z: s5 a7 {9 y- x5 t
_2 s+ ^2 y3 @$ _Epoch 1/2) t& Q, w h }8 d
----------
0 V& v! w; s( B3 c( strain Loss: 1.1288 Acc: 0.6535% ^+ z& v; O9 c: ?( {+ u3 Z% K! ^
val Loss: 0.9012 Acc: 0.6929) P5 D) ?8 X! J w( h4 h- {
7 B) t* U) Q% J* A, a3 z
Epoch 2/2
+ R/ `3 D6 C4 C8 n. v" \----------* u4 u$ |* S n
train Loss: 0.9578 Acc: 0.6706
, l0 X# s( m) n9 B5 V/ `. uval Loss: 0.8088 Acc: 0.6948
3 @4 ] W6 c( L& R) q4 _% d+ }2 Q l5 F4 C) p& d
Training complete in 6m 37s4 B) `& p- b; x9 m: w! I6 R
5 h. e9 {4 _3 _$ _( a
. Y5 r( }) C, @* x
) A- ?$ }+ n: V2 d/ a3 N$ j/ X+ c( V9 g' |
当 epochs = 5 时,训练集的精度在 89 8989% 左右,测试集的精度可以达到 86 8686 %。2 m& b! G# o0 p- M$ P1 P
3 z7 X2 C2 Q/ B( O0 i对于这个模型用 ResNet-50 作特征提取器会有更好的效果,不过训练的时间也会更长。还有超参数lr, weight_decay, momentum, step_size, gamma 以及1×1 1×11×1卷积层和转置卷积层的初始化方式也可以继续调。
! M# `2 }. d5 ^8 z( e7 }4 M! K0 L, Y- w% L' d8 [1 M
) o% w& _/ I' T3 H+ C5 E1 V
语义分割还有很多可用的模型,本文用的是 FCN,在其它一些模型上会有更好的表现:
0 I' u* \9 a: i6 u: y4 J$ `) t: N( s4 u0 ^. `7 k1 y
Deeplab V3+ 具有可分离卷积的编码器/解码器,用于语义图像分割[论文]
! O! Y+ N: c. EGCN 通过全局卷积网络改进语义分割[论文]
9 C2 I+ k& r8 j! f2 eUperNet 统一感知解析
3 ~( o; V# ]. [: `9 i) YENet 用于实时语义分割的深度神经网络体系结构[论文]5 Z \$ P; q- `6 N; u, W
U-Net 用于生物医学图像分割的卷积网络
$ i: s: [# `# u; a7 j3 eSegNet 用于图像分段的深度卷积编码器-解码器架构。5 g9 t- I: _. f! s, D7 d' L
还有(DUC,HDC)、PSPNet等。/ U; p1 d' {8 a1 q M+ P% p0 I
7 h; l9 d3 ~7 R* q9 b
常用的语义分割数据集也有很多:Pascal VOC、CityScapes、ADE20K、COCO Stuff等。
$ Y. ` A+ ]/ e8 g' x! A# Z
0 w6 P) e! ?8 f) R+ j2 _对于损失函数,除了交叉熵误差,也可以用这些:9 g6 B/ e% s: D
, t. S. J! F7 }+ t8 k' nDice-Loss 可以测试两个样本之间的重叠度量,可以更好地反映训练目标,但该损失函数具有很强的非凸性,很难优化。6 s: y* E8 X' b" F, O* t8 e
CE Dice loss Dice 损失与 CE 的总和,CE 提供了平滑的优化,而 Dice 损失则很好地表明了分割结果的质量。: [0 ~ U- Z) S! U- v1 z: z
Focal Loss CE 的另一种版本,用于避免类别不平衡而降低了置信度的情况。8 c$ l3 m3 k+ r: s7 N& {
Lovasz Softmax 查看论文:Lovasz - softmax损失。
+ B8 W6 ~# {# D
# _# g L0 g* A& q; q8 _- H$ @! H! b( p
! R; q* T' Z2 G' ~
4 @! w# B5 ^$ E9 k+ O3 j$ d
4 C( ^% J% n- D4 o) H————————————————$ t& E! N4 y4 \2 I2 C: I
版权声明:本文为CSDN博主「小红不吃糖」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。1 S% P/ E; }4 _7 }
原文链接:https://blog.csdn.net/qq_43280818/article/details/105916507, @/ T' e7 Y ]* ?
# J, T# L! c( |" I3 C
: K+ c7 v4 F: h5 n
! y$ I9 \: r/ l& U1 m |