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