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