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