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