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