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