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