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