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