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