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