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