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