QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 11181|回复: 0
打印 上一主题 下一主题

Pytorch实战语义分割(VOC2012)

[复制链接]
字体大小: 正常 放大
杨利霞        

5273

主题

82

听众

17万

积分

  • TA的每日心情
    开心
    2021-8-11 17:59
  • 签到天数: 17 天

    [LV.4]偶尔看看III

    网络挑战赛参赛者

    网络挑战赛参赛者

    自我介绍
    本人女,毕业于内蒙古科技大学,担任文职专业,毕业专业英语。

    群组2018美赛大象算法课程

    群组2018美赛护航培训课程

    群组2019年 数学中国站长建

    群组2019年数据分析师课程

    群组2018年大象老师国赛优

    跳转到指定楼层
    1#
    发表于 2020-5-4 15:03 |只看该作者 |倒序浏览
    |招呼Ta 关注Ta

    & r1 m' \( ~& t, c
    # Q- ~& g, e- W) JPytorch实战语义分割(VOC2012)$ {  h; Y; ~3 Z- k
    本文参照了《动手深度学习》的9.9、9.10章节,原书使用的是 mxnet 框架,本文改成了pytorch代码。
    4 y6 m0 Z# P( e, j  l语义分割(semantic segmentation)问题,它关注如何将图像分割成属于不同语义类别的区域。值得一提的是,这些语义区域的标注和预测都是像素级的。3 k, y: A% G) t. F

    ; i( B) |' f% h5 b4 J3 z1 p: V, u' y% A* I
    语义分割中图像有关狗、猫和背景的标签
    + T8 F) i7 M8 b8 y文章目录
    6 L! I* m) {* M' _- i6 v; k; P4 w9 l
    1 图像分割和实例分割2 e. G: @% X3 h6 F
    2 Pascal VOC2012语义分割数据集3 D. M% Y7 b* T7 r8 z4 \
    2.1 导入模块
    ! w5 z: ~; y9 E; ?$ e; l2.2 下载数据集
    % `8 N3 y3 K4 r% [$ Z2.3 可视化数据& X0 L; O' h! |- k2 h7 U. P
    2.4 预处理数据& T6 @% B& O- y/ m
    3 自定义数据集类, p9 K. H% u% v
    3.1 数据集类- P% Z  Y* }! C
    3.2 读取数据集- I" ~6 I3 v6 ~" h5 F
    4 构造模型
    ' S0 r7 r5 E3 T9 }4.1 预训练模型
    # R) _$ N8 [' o1 j4 M+ I5 I( T& I: R4.2 修改成FCN* [/ `, q) w% t
    4.3 初始化转置卷积层
    9 J/ G  e7 U1 e; q6 y- c/ o5 训练模型8 F% i$ v2 W% j9 ^4 p
    6 测试模型: L' W- [, V7 `+ e" n: X  s  ]
    6.1 通用型; T4 k4 ], p/ b+ o* o6 p
    6.2 不通用8 _6 e' N. j  U
    7 结语& p/ a1 ]2 P. E9 |0 ?6 ^
    1 图像分割和实例分割
    5 K* ]0 ?: N5 J
    9 a2 X/ e6 B' Y/ z5 |$ a计算机视觉领域还有2个与语义分割相似的重要问题,即图像分割(image segmentation)和实例分割(instance segmentation):4 A6 a8 ^3 u) x

    + p2 R( }* H' X8 Z' o图像分割将图像分割成若干组成区域。这类问题的方法通常利用图像中像素之间的相关性。它在训练时不需要有关图像像素的标签信息,在预测时也无法保证分割出的区域具有我们希望得到的语义。以上图的图像为输入,图像分割可能将狗分割成两个区域:一个覆盖以黑色为主的嘴巴和眼睛,而另一个覆盖以黄色为主的其余部分身体。# J7 F; {3 ]: R( P
    实例分割又叫同时检测并分割(simultaneous detection and segmentation)。它研究如何识别图像中各个目标实例的像素级区域。与语义分割有所不同,实例分割不仅需要区分语义,还要区分不同的目标实例。如果图像中有两只狗,实例分割需要区分像素属于这两只狗中的哪一只。
    " [! D' f. Z0 q* ?. @
    ( a2 o9 w9 ~; K- y2 Pascal VOC2012语义分割数据集
    1 B, h/ h' {4 d2 ]6 |
    6 g( Q# ?/ B4 L; t( b5 y2.1 导入模块+ P1 j( H' g, ~
    import time
    ( z3 L+ d  a) h% n- Limport copy
    # v: U+ Z) M; {7 _* t/ J: vimport torch( x0 v: f' b, M* q- }" U
    from torch import optim, nn/ J3 q# J2 b# _) ?( c8 j8 \
    import torch.nn.functional as F* R& N2 m4 o/ |7 y
    import torchvision
    . K( H- Q0 F0 W) F7 M4 T% sfrom torchvision import transforms& g# o! l! J, E1 \; j4 u( a
    from torchvision.models import resnet18- W( \  \0 ?! d: H
    import numpy as np
    / I- w2 d! q  H! X; Kfrom matplotlib import pyplot as plt
    * v% V  p, k, l! efrom PIL import Image& F" G4 M# Y: R$ Q$ e% x% n
    import sys! y6 B( z& q% m
    sys.path.append("..")
    ; q! X' _' y  W9 wfrom IPython import display
    $ N  F" _5 a' h2 I0 M- t0 Hfrom tqdm import tqdm2 x2 X: }1 t3 E" j! x" a
    import warnings
    , T8 ^( ~4 _# X! p8 p: g  Y# D# Xwarnings.filterwarnings("ignore")
    6 C* I/ }, Z4 g5 {
    + u( J/ f' w; |4 U2.2 下载数据集
    & i1 e% \/ O4 W4 x- t8 r, M) B% L' Y1 ^8 N$ C
    语义分割的一个重要数据集叫作Pascal VOC2012,点击下载这个数据集的压缩包,大小是2 GB左右,所以下载需要一定时间。下载后解压得到VOCdevkit/VOC2012文件夹,然后将其放置在data文件夹下,VOC2012文件目录是这样的:/ y5 J9 B0 n& X
    . U- o% [3 `  {. y: a7 a
      [( P: J1 ~1 Y3 \6 Q* V6 y) U& E
    ImageSets/Segmentation路径包含了指定训练和测试样本的文本文件# l6 d6 U6 F" @
    JPEGImages和SegmentationClass路径下分别包含了样本的输入图像和标签。这里的标签也是图像格式,其尺寸和它所标注的输入图像的尺寸相同。标签中颜色相同的像素属于同一个语义类别。
    2 k$ g% M( `& D3 ~2.3 可视化数据0 I# w! W' w! K0 H9 W' t# b* Y
    / y0 Y% K  T1 U) {+ W
    定义read_voc_images函数将输入图像和标签读进内存。
      @; A! u, f1 ]6 u
    7 S- L: k! ~" Y% k7 ndef read_voc_images(root="../../data/VOCdevkit/VOC2012", is_train=True, max_num=None):
    1 w2 S; p# Y/ X: ]/ c1 b! G# l  o    txt_fname = '%s/ImageSets/Segmentation/%s' % (root, 'train.txt' if is_train else 'val.txt')
    4 W" `/ ]3 T6 K. n    with open(txt_fname, 'r') as f:2 I7 n- v% |/ U9 c
            images = f.read().split() # 拆分成一个个名字组成list. V* D2 L/ W$ V9 d# c" E7 ^
        if max_num is not None:
    $ Q6 e2 V4 a. H9 x        images = images[:min(max_num, len(images))]/ N" J0 g0 J. v9 r
        features, labels = [None] * len(images), [None] * len(images)$ X8 Q% {( ]. M. E7 Q) v
        for i, fname in tqdm(enumerate(images)):
    3 w4 h+ S) C! m# ^2 E        # 读入数据并且转为RGB的 PIL image
    ' }' J8 s$ v' Y. H( M8 B$ F' @        features = Image.open('%s/JPEGImages/%s.jpg' % (root, fname)).convert("RGB")
    . z0 U$ H  w; K8 Y9 C- M        labels = Image.open('%s/SegmentationClass/%s.png' % (root, fname)).convert("RGB")& u' a5 N. l7 o5 |: t1 n% f. _
        return features, labels # PIL image 0-2557 K, o/ c! E  q, S) M
    ' D2 k! B( U) I1 l" V3 d
    定义可视化数据集的函数show_images6 w$ }, t" G1 k4 t( ^2 J3 d

    7 L( I' @7 S0 u' H+ X; ~# 这个函数可以不需要
    2 g+ x% V* G/ B5 Kdef set_figsize(figsize=(3.5, 2.5)):  n! v% g& @; W  v$ u4 V5 F
        """在jupyter使用svg显示"""
    : C5 o9 U" H8 ~    display.set_matplotlib_formats('svg')
    / p) x8 Q; O* ?: n  s    # 设置图的尺寸3 {2 M0 N4 a8 P- H* c0 t; m9 b
        plt.rcParams['figure.figsize'] = figsize
    - e1 w( b# Z% _* s, @) A
    * u# i. A$ D6 }5 b$ t; h; Cdef show_images(imgs, num_rows, num_cols, scale=2):
    * A( K1 c8 m; ^6 |/ Y- Z8 V" R    # a_img = np.asarray(imgs)3 F- ^6 K' b2 ~2 A- ]# n6 D
        figsize = (num_cols * scale, num_rows * scale)
      F: e) m5 S7 J6 a! K+ S3 X    _, axes = plt.subplots(num_rows, num_cols, figsize=figsize)# h  [% f" l: a9 ~5 E0 ?' p* f
        for i in range(num_rows):8 S* _! n- G4 [3 p. a+ T! r
            for j in range(num_cols):
    , b/ @" Q6 e& ^            axes[j].imshow(imgs[i * num_cols + j])( O6 Q; r0 u3 R1 F' ~: @2 t: s
                axes[j].axes.get_xaxis().set_visible(False)
    ' ~  ?3 k: [, w' k- F/ Q5 u            axes[j].axes.get_yaxis().set_visible(False)
    / a0 ~# F! Q2 D" {9 R- H8 ], w    plt.show()
    ( R' c, d; {# c% A    return axes; H1 ?. M- M% s

    5 f+ \5 ~4 Q' R7 w4 W- v定义可视化数据集的函数show_images
    . z5 I# \, @6 ~* u8 X7 j3 ~/ F
    ( d7 ]0 t& v+ a0 q/ H$ @9 b# 这个函数可以不需要  b) D( {7 N- N& e
    def set_figsize(figsize=(3.5, 2.5)):
    % U2 U+ t# q( x8 p    """在jupyter使用svg显示"""
      o" g" s$ E+ q2 L5 S7 d    display.set_matplotlib_formats('svg')) u! |7 Q" d8 w3 Z) `- D- D; j$ p
        # 设置图的尺寸
    2 f$ D0 T# o0 Y# y% n6 w    plt.rcParams['figure.figsize'] = figsize5 [' J6 ?9 \3 K) g; P' c- F3 y! J
      H* P1 i+ t  X8 b( r) A; N) h+ K, {
    def show_images(imgs, num_rows, num_cols, scale=2):- E7 f# ~; A$ U2 J* q5 ~0 X/ @
        # a_img = np.asarray(imgs)5 s7 B: z$ q  \: [" S# r# u, F
        figsize = (num_cols * scale, num_rows * scale), p" P+ n) N0 ?( R
        _, axes = plt.subplots(num_rows, num_cols, figsize=figsize)
    + |' U) u* e2 k# ?. D    for i in range(num_rows):0 k! @4 A& ?# ?+ |8 B/ I* i
            for j in range(num_cols):
    7 }9 s% P/ p' \4 o9 z            axes[j].imshow(imgs[i * num_cols + j])
    7 F& D5 h7 p  ~' m; j            axes[j].axes.get_xaxis().set_visible(False)
    " |1 k. @' X& u$ D3 q1 B: v( u            axes[j].axes.get_yaxis().set_visible(False)
    & ?2 l/ J9 |% {8 T/ P3 |    plt.show()1 n8 ~+ ~8 X" [7 M$ ?
        return axes  H' ~9 b% D7 p/ f; _# l* B" l
    画出前5张输入图像和它们的标签。在标签图像中,白色和黑色分别代表边框和背景,而其他不同的颜色则对应不同的类别。+ [. r9 b0 j$ k$ P9 j, \2 m
    $ e! I' A3 O% m1 ~/ B, E7 D
    # 根据自己存放数据集的路径修改voc_dir4 u: B7 b: W% _! L& s( L: k/ i
    voc_dir = r"[local]\VOCdevkit\VOC2012"  ?! N# U# h0 B9 g" }' v: _6 T
    train_features, train_labels = read_voc_images(voc_dir, max_num=10)) z; q$ p0 Y& a% V; u" e
    n = 5 # 展示几张图像  X/ o' \+ C# U$ g
    imgs = train_features[0:n] + train_labels[0:n] # PIL image* k( y& J) D4 F2 ]
    show_images(imgs, 2, n)
    0 I8 i! O2 }2 N. @( `2 f# b3 J0 J1 u6 W. E4 ^2 e$ e
    1.png ! D: c* @1 c! }

      a2 C/ N; V; ^$ k" Y, V3 O列出标签中每个RGB颜色的值及其标注的类别。
    : r% \- k( l# T; {# 标签中每个RGB颜色的值4 \% [! ]$ X9 `
    VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],% q0 |/ s( m8 e. w, ]# ~' f; ~
                    [0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128],4 D: J9 D  V0 v9 [
                    [64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0],
    # P9 C+ ]/ i0 k                [64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128],
    & m6 T+ I& ?9 Y4 U3 d                [0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0],
    5 L* P! a, f0 D2 E) I% S; L  c& |                [0, 64, 128]]
    ' d7 {# u$ S# c, f0 v# o1 T# 标签其标注的类别
    ! c4 F2 G+ S4 ?) v0 f; lVOC_CLASSES = ['background', 'aeroplane', 'bicycle', 'bird', 'boat',! m2 [0 \- c2 @
                   'bottle', 'bus', 'car', 'cat', 'chair', 'cow',6 c6 A  x( K2 ~. \2 c# @
                   'diningtable', 'dog', 'horse', 'motorbike', 'person',. U; G2 k  Y( V. _- S  H5 D
                   'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor']
    ; T1 e! O) m+ ]  ]有了上面定义的两个常量以后,我们可以很容易地查找标签中每个像素的类别索引voc_label_indices是根据colormap2label把标签里的 rgb 颜色对应上面的VOC_COLORMAP中的下标给取出来,当作 label 。2 f& u2 L' X2 b
    5 b  ]/ }2 S  f6 M) f' R& R
    有了上面定义的两个常量以后,我们可以很容易地查找标签中每个像素的类别索引voc_label_indices是根据colormap2label把标签里的 rgb 颜色对应上面的VOC_COLORMAP中的下标给取出来,当作 label 。
    9 r& U4 [$ U9 zcolormap2label = torch.zeros(256**3, dtype=torch.uint8) # torch.Size([16777216])
    0 f# h) A$ g. c7 K: a+ Q8 m& Ffor i, colormap in enumerate(VOC_COLORMAP):
    1 a3 e3 ^  k" P) ~    # 每个通道的进制是256,这样可以保证每个 rgb 对应一个下标 i
    / K- e6 `! A) e% s+ C1 Q    colormap2label[(colormap[0] * 256 + colormap[1]) * 256 + colormap[2]] = i6 h) O; ~+ X& a) {& F

    % U; p+ A7 Y4 T# 构造标签矩阵
    " m' ?4 G5 B& P& O: }; ~0 ydef voc_label_indices(colormap, colormap2label):
    ! Q1 [) U. p# m. a; F) H; x. p$ a    colormap = np.array(colormap.convert("RGB")).astype('int32')
    7 i  V" J  i; w' }7 K& ~* W) o5 d    idx = ((colormap[:, :, 0] * 256 + colormap[:, :, 1]) * 256 + colormap[:, :, 2]) 7 A& e, g! I' r  Q$ W% G
        return colormap2label[idx] # colormap 映射 到colormaplabel中计算的下标
    $ P% a! Y4 J4 O! Z4 }: R0 n; i  o
    / V: v! I2 _$ O. T) e7 B9 y可以打印一下结果  r1 }. M- I# S5 O' w
    % e$ ~0 t% i5 V4 R' l0 e
    y = voc_label_indices(train_labels[0], colormap2label)) I: h( p8 P5 d! f2 c7 Z$ |3 y
    print(y[100:110, 130:140]) #打印结果是一个int型tensor,tensor中的每个元素i表示该像素的类别是VOC_CLASSES
    . M7 G- e, d# z" z) R, c9 A3 l4 d3 n- C& Y9 P8 W; o
    2.4 预处理数据# Z# N9 B; a, k
    7 _- q& r0 i; `3 p& ?5 D
    在语义分割里,如果使用缩放图像使其符合模型的输入形状的话,需要将预测的像素类别重新映射回原始尺寸的输入图像,这样的映射难以做到精确,尤其是在不同语义的分割区域。所以选择将图像裁剪成固定尺寸而不是缩放。具体来说,我们使用图像增广里的随机裁剪,并对输入图像和标签裁剪相同区域。5 w% _1 i. U. q- ]" _1 w+ A

    3 Y8 ^1 ?% P* J+ l4 D2 C
      U+ s/ r1 l+ Y0 c2 t, fdef voc_rand_crop(feature, label, height, width):
    + H, B5 N- B; F0 a    """$ L; F- Q* K5 ~' W# i9 Q9 `
        随机裁剪feature(PIL image) 和 label(PIL image).
    / ]8 d; X5 _# E( ]& j% W    为了使裁剪的区域相同,不能直接使用RandomCrop,而要像下面这样做* `8 }+ s0 c: N2 f8 j! \2 \
        Get parameters for ``crop`` for a random crop.5 ^! }- w& i. `1 R
        Args:
      ~$ b/ s: Q6 G  N/ X' C        img (PIL Image): Image to be cropped.5 g) z) y0 q" g6 t
            output_size (tuple): Expected output size of the crop.8 [. K8 n, S( F& T
        Returns:
    & o& ~1 R% r9 X, f        tuple: params (i, j, h, w) to be passed to ``crop`` for random crop.
    - |0 L; V: j& ~& G: `5 _$ I1 L0 V    """+ t! C4 z9 h8 `8 V' Y* Z
        i,j,h,w = torchvision.transforms.RandomCrop.get_params(feature, output_size=(height, width))
    ! b2 G+ I3 N; K0 D' ?    feature = torchvision.transforms.functional.crop(feature, i, j, h, w)/ v8 S4 s% B6 {4 ~' g% Z
        label = torchvision.transforms.functional.crop(label, i, j, h, w)3 M2 t* O8 n* h6 \
        return feature, label
    " O$ \% @1 j7 L( d; t* A/ ~4 |2 n6 S3 `' x7 n
    # 显示n张随机裁剪的图像和标签,前面的n是5! v  {/ R9 |* M7 @
    imgs = []
    : J" W& w+ a* [( pfor _ in range(n):, [9 o" @( D% U0 m4 ?+ T
        imgs += voc_rand_crop(train_features[0], train_labels[0], 200, 300)$ j4 R6 A& X- j4 T: `
    show_images(imgs[::2] + imgs[1::2], 2, n);
    5 G, e' I$ q" D" t) Z7 P( }& `  g( S8 J  w* U. V% y
    * w- j  ]9 v; y$ y% A% e: p3 N0 r
    2.png
    5 v- D# X! T3 k' A3 O/ h  z4 Z! z  Q2 E! i: o* @
      q# M, z8 u$ n8 N

    $ B! S: S- ~: d3 自定义数据集类
    6 V# f2 o- t, J
    $ |1 q: ]7 ?6 v! ]# j! i4 ^3.1 数据集类
    8 a* V7 @5 [4 h0 ^6 }5 o
    ( b, I9 o( W: x6 d6 |0 ^; Htorch.utils.data.Dataset是表示数据集的抽象类,因此自定义数据集应继承Dataset并覆盖以下方法
    % H% V6 L9 w. o2 N, X2 t1 B7 ~3 j" k* V; J1 _$ ?
    __len__ 实现 len(dataset) 返还数据集的尺寸。
    6 q; ?8 u& |$ N__getitem__用来获取一些索引数据,例如 dataset[idx] 中的(idx)。" G! j7 V3 E; q: ~% l
    由于数据集中有些图像的尺寸可能小于随机裁剪所指定的输出尺寸,这些样本需要通过自定义的filter函数所移除。此外,因为之后会用到预训练模型来做特征提取器,所以我们还对输入图像的 RGB 三个通道的值分别做标准化。
    ( ^) p6 L/ f3 q% C* e: O6 f) D8 C. c9 ~- \/ @- t4 Q
    class VOCSegDataset(torch.utils.data.Dataset):, s5 d7 q" ]% `* I" h
        def __init__(self, is_train, crop_size, voc_dir, colormap2label, max_num=None):
    ! A' G; [: m' u1 {' }' M8 U        """
    - f( T; D/ S" }  M& y* x7 o        crop_size: (h, w)' C7 H$ r+ ^- B' a( [
            """* S  E! |3 a1 g! I+ Y
            # 对输入图像的RGB三个通道的值分别做标准化4 b# k* t( a3 @/ s
            self.rgb_mean = np.array([0.485, 0.456, 0.406])
    3 c8 S: _  }7 B( h* o: T        self.rgb_std = np.array([0.229, 0.224, 0.225])
    ; ]  l4 u7 f- ], T3 i- r# s        self.tsf = torchvision.transforms.Compose([
    # y+ ], i/ ]$ e6 B& p            torchvision.transforms.ToTensor(),: ~8 ]$ {% H1 }7 W4 t" L) E
                torchvision.transforms.Normalize(mean=self.rgb_mean, std=self.rgb_std)]). r% b3 }' ^3 c% B
            self.crop_size = crop_size # (h, w)
    6 N  [" _8 p9 o$ n        features, labels = read_voc_images(root=voc_dir, is_train=is_train,  max_num=max_num)% Y1 B8 w7 H( i" a$ T" [
    # 由于数据集中有些图像的尺寸可能小于随机裁剪所指定的输出尺寸,这些样本需要通过自定义的filter函数所移除
    ; R: G( j$ U# ^) x& v$ g' g; R" `        self.features = self.filter(features) # PIL image# d/ {' J8 z* g/ h: B
            self.labels = self.filter(labels)     # PIL image
    - l" U9 H7 V' {8 Z. L        self.colormap2label = colormap2label4 t! |& L6 N3 ]
            print('read ' + str(len(self.features)) + ' valid examples')
    7 W3 H/ L6 X+ V8 C
    % G$ K; W, _, k    def filter(self, imgs):* u; i1 h6 Z6 N. }6 N1 Z+ U
            return [img for img in imgs if (' o6 s1 S. U1 f5 `1 Q7 x: ^
                img.size[1] >= self.crop_size[0] and img.size[0] >= self.crop_size[1])]% [6 e/ N) a2 ?* o! a8 `6 z. v

    2 I0 a3 c! @% |/ x6 V. q  n/ y: \+ b    def __getitem__(self, idx):8 g; Z7 g7 B6 W) x; y& Y
            feature, label = voc_rand_crop(self.features[idx], self.labels[idx], *self.crop_size)8 s* Y, _  \0 ^: j; V
                                    # float32 tensor           uint8 tensor (b,h,w)6 ~, x- P. c8 T, Z- u
            return (self.tsf(feature), voc_label_indices(label, self.colormap2label))9 f: }" O; _" Z; M0 @! k, R

    / x- m( r; g" d2 x' u    def __len__(self):
    . k. _9 e" }6 S: V+ f/ u" B        return len(self.features)1 h! K% a3 M/ j9 d7 V7 [/ M
    3.2 读取数据集
    ' Q( h* b8 ~  F* {0 j6 H+ C& E/ Z) J4 I/ m
    通过自定义的VOCSegDataset类来分别创建训练集和测试集的实例。因为待会用的是全卷积网络,所以随机裁剪的输出图像的形状可以自己指定,这里指定为320×480​ 320\times 480​320×480​。$ Q/ x4 z" u6 q7 b' P: a, t

    % c5 `& x1 h1 v8 o/ Fbatch_size = 32 # 实际上我的小笔记本不允许我这么做!哭了(大家根据自己电脑内存改吧)# `! V5 w6 W2 J* Y' q
    crop_size = (320, 480) # 指定随机裁剪的输出图像的形状为(320,480)5 J- {% e! X; ?; q: l7 p) S
    max_num = 20000 # 最多从本地读多少张图片,我指定的这个尺寸过滤完不合适的图像之后也就只有1175张~1 b1 s' T. i5 Q. N' p+ z4 `+ P
    / S0 ]0 i8 o9 u) y0 {9 y
    # 创建训练集和测试集的实例
    " q. j$ s$ R8 t3 svoc_train = VOCSegDataset(True, crop_size, voc_dir, colormap2label, max_num)
    ! V2 e- ^3 I" E# |4 ^6 \8 Uvoc_test = VOCSegDataset(False, crop_size, voc_dir, colormap2label, max_num)* H) h8 N3 ]$ g, V0 T
    5 }9 j% f( U; l  y4 T* j* N5 S1 k
    # 设批量大小为32,分别定义【训练集】和【测试集】的数据迭代器, ?$ E5 H4 X3 Q9 @
    num_workers = 0 if sys.platform.startswith('win32') else 45 l. U$ }; ?4 ]4 k- _
    train_iter = torch.utils.data.DataLoader(voc_train, batch_size, shuffle=True," A. b1 o! x+ ~) t6 ]
                                  drop_last=True, num_workers=num_workers)0 w) b; @: R* Q0 G
    test_iter = torch.utils.data.DataLoader(voc_test, batch_size, drop_last=True,
    ! C* s. J, Q, u% `7 r; c0 i. k1 n                             num_workers=num_workers)" }9 I% P" E/ f6 Q7 ~

    3 |! j! o4 P8 P/ f9 z! u% J# 方便封装,把训练集和验证集保存在dict里
    * k7 W5 o2 v+ s7 E' edataloaders = {'train':train_iter, 'val':test_iter}( o# i+ u& H4 f8 @1 P
    dataset_sizes = {'train':len(voc_train), 'val':len(voc_test)}" K9 I9 t0 |5 e7 d: b
    - R% B, ^* o. u
    4 构造模型4.1 预训练模型

    下⾯我们使⽤⼀个基于 ImageNet 数据集预训练的 ResNet-18 模型来抽取图像特征。

    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    8 O& h" z2 f3 d8 K2 d; k
    ! K  l0 p3 f; F& n- @. Fnum_classes = 21 # 21分类,1个背景,20个物体
    " Q) ^; m" r- z7 `' J2 G% `0 ?model_ft = resnet18(pretrained=True) # 设置True,表明要加载使用训练好的参数
    ! \. b* b0 D" V" Y" w! N+ p. ~" H6 C3 B' ]! e, R4 G  Y! c3 y
    # 特征提取器
    ; Z* e6 U) S5 X3 d* @# u) Afor param in model_ft.parameters():
    4 B1 K3 F" [: `% s7 ?    param.requires_grad = False# }5 I: s" B) b1 g' B
    4.2 修改成FCN" z  u8 s9 t6 I4 R
    , L0 J! _8 \, a/ @1 T8 o
    全卷积⽹络(顾名思义全部都是卷积层)先使⽤卷积神经⽹络抽取图像特征,然后通过 1×1​ 1\times 1​1×1​ 卷积层将通道数变换为类别个数,最后通过转置卷积层将特征图的⾼和宽变换为输⼊图像的尺⼨。模型输出与输⼊图像的⾼和宽相同,并在空间位置上⼀⼀对应:# _' }" z* {5 o8 g" M  u: [
    最终输出的通道包含了该空间位置像素的类别预测。1 ~  o/ a6 }* {! ~

    4 z4 X' z2 L( Q' j- B  W对于转置卷积层,如果步幅为 S​ S​S​、填充为 S/2​ S/2​S/2​ (假设为整数)、卷积核的⾼和宽为 2S​ 2S​2S​,转置卷积核将输⼊的⾼和宽分别放⼤ S​ S​S​ 倍。1 z, t, t: T8 r8 i" I* R( E  ]

    ' U5 n; L" E) l, k可以先打印model_ft,可见 ResNet-18 的最后两层分别是全局最⼤池化层GlobalAvgPool2D 和 全连接层。全卷积⽹络不需要使⽤这些层。通过测试,当输入图像的 size 是(batch,3,320,480) (batch,3,320,480)(batch,3,320,480) 时,通过除最后两层的预训练网络后输出的大小是 (batch,512,10,15) (batch,512,10,15)(batch,512,10,15),也就是 feature featurefeature 的宽高比输入缩小了 32 3232 倍,只需要用转置卷积层将其放大 32 3232 倍即可。
    ( p7 K% P- Q; Z* ~. i4 a/ v* ?' v* b5 ^4 P# Z* l( z* q
    model_ft = nn.Sequential(*list(model_ft.children())[:-2], # 去掉最后两层: o! ?9 k  l# X# O% }5 @# ~
                  nn.Conv2d(512,num_classes,kernel_size=1), # 用大小为1的卷积层改变输出通道为num_class* u/ u' U& t+ Y% X
                  nn.ConvTranspose2d(num_classes,num_classes, kernel_size=64, padding=16, stride=32)).to(device) # 转置卷积层使图像变为输入图像的大小  _6 }8 u/ N7 F
    5 R% R/ ?" L8 J
    # 对model_ft做一个测试) m9 x( R3 e- n: v8 n8 t$ }
    x = torch.rand((2,3,320,480), device=device) # 构造随机的输入数据  T) l/ U* k) o% [
    print(net(x).shape) # 输出依然是 torch.Size([2, 21, 320, 480])
      b+ j+ v5 l1 S6 J2 k4 H5 `( C# B, E) b; p# q
    # 打印第一个小批量的类型和形状。不同于图像分类和目标识别,这里的标签是一个三维数组( a5 e3 G  y" s! v
    # for X, Y in train_iter:
    % M. W. k8 W2 G2 A4 q* B% @#     print(X.dtype, X.shape)5 |0 Z6 T. m( f+ w$ |$ p
    #     print(Y.dtype, Y.shape)
    ' l1 L! N" O+ C3 b( }2 N% h" \#     break
    5 @% @) |" B' Y5 r6 o
    . q7 }; M0 q% K  X* h
    , ?/ @# u- M' V1 p4.3 初始化转置卷积层
    ) @  Y( Y) g) H9 G  ]- u
    & m! R2 I% [( p. S  v& ]; \在图像处理中,我们有时需要将图像放⼤,即上采样(upsample)。上采样的⽅法有很多,常⽤的有双线性插值。简单来说,为了得到输出图像- n5 b8 _1 b# I% g0 @& R
    在坐标 (x,y) (x, y)(x,y)上的像素,先将该坐标映射到输⼊图像的坐标 (x',y') (x′, y′ )(x′,y′)。例如,根据输⼊与输出的尺⼨之⽐来映射。映射后的 x' x′x′ 和 y' y′y′ 通常是实数。然后,在输⼊图像上找到与坐标 (x',y') (x′, y′ )(x′,y′)最近的 4 44 个像素。最后,输出图像在坐标 (x,y) (x, y)(x,y)上的像素依据输⼊图像上这4 44个像素及其与 (x',y') (x′, y′ )(x′,y′)的相对距离来计算。双线性插值的上采样可以通过由以下bilinear_kernel函数构造的卷积核的转置卷积层来实现。
    ) ]9 S/ o) U6 e! R: y) t
    8 x2 c$ ^- |0 D
    8 R* p8 p; ]- {6 b& S' ?# 双线性插值的上采样,用来初始化转置卷积层的卷积核
    5 A! l1 p' Z- G8 @6 J: pdef bilinear_kernel(in_channels, out_channels, kernel_size):- C( g+ s% E% \' k9 Y. k! |9 F
        factor = (kernel_size+1)//2
    . I% O3 w# z  ]2 g- O4 h5 O    if kernel_size%2 == 1:
    6 H& {- Y' R, W- o. F3 F        center = factor-1
      p$ w; K& W8 v  Z! k    else:
    & E, u6 R2 h4 k2 e8 W% }( j; e4 b( ]8 `        center = factor-0.5
    % Z' H' ]. X7 b8 |6 q. R    og = np.ogrid[:kernel_size, :kernel_size]
    # a# {7 i, T1 r0 j  A    filt = (1-abs(og[0]-center)/factor) * (1-abs(og[1]-center)/factor). P; I+ x6 @) m  I1 n* E, z. D
        weight = np.zeros((in_channels,out_channels, kernel_size,kernel_size), dtype='float32')+ k0 V+ s  c/ q. H" d  r
        weight[range(in_channels), range(out_channels), :, :] = filt2 e# U- p+ d) o( a
        weight = torch.Tensor(weight)
    ! u5 C3 d; O/ A9 [: F! H. X& K! @3 _    weight.requires_grad = True: g, X7 C; t8 P6 B
        return weight8 K( G9 |* ]7 a. t3 P

    ; m" N: ~! T; Y
    9 g0 ~: k7 I5 V在全卷积⽹络中,将转置卷积层初始化为双线性插值的上采样。对于1×1 1\times 11×1卷积层,采⽤Xavier XavierXavier随机初始化。5 w3 W0 e* E% A4 {4 Z0 I* k! T
    : x4 h4 v, ]' Z
    nn.init.xavier_normal_(model_ft[-2].weight.data, gain=1)% }0 b. ?, V; s/ F3 m
    model_ft[-1].weight.data = bilinear_kernel(num_classes, num_classes, 64).to(device)" U! f3 l& G! |* }. N/ F
    # H. |/ j/ `7 _

    & @, E- D* n" K9 v# A7 P5 A% e. r* \
    5 训练模型

    现在可以开始训练模型了。这⾥的损失函数和准确率计算与图像分类中的并没有本质上的不同。有一个 blog 我认为说的很详细,图也画得很好:https://blog.csdn.net/Fcc_bd_stars/article/details/105158215


    5 l* I/ O3 L* j7 i5 m/ N$ K1 Ndef train_model(model:nn.Module, criterion, optimizer, scheduler, num_epochs=20):- Z- A9 W2 e) j; q; L4 l
        since = time.time()& \! Z8 V- I4 v2 H; c
        best_model_wts = copy.deepcopy(model.state_dict())
    / p/ N& Z2 K, N# v5 N% o    best_acc = 0.0
    7 C% I3 f  T" F0 q4 j( p    # 每个epoch都有一个训练和验证阶段% z5 D8 {- `1 }
        for epoch in range(num_epochs):- S; C; j: H8 ]3 @
            print('Epoch {}/{}'.format(epoch, num_epochs-1))
    8 ~9 f) \* r$ E& d3 \$ k( o        print('-'*10)6 F  j, C* a- n: c0 C
            for phase in ['train', 'val']:
    % p. p+ B. T/ j, c: ?& C$ z3 T% j: {            if phase == 'train':: w3 B* ?+ W$ f0 ~* o
                    scheduler.step()3 T  `2 M+ g2 J  ]
                    model.train()
    3 n  [* i9 p" Q) j7 B/ [8 ?6 Y            else:
    ' z& h" H) X. e3 R. t, {                model.eval()
    3 Q! A! ~( _2 ~6 B# x; I' r            runing_loss = 0.0
    + b; c5 ]  Z8 z3 l6 V            runing_corrects = 0.0* ~3 n1 _  ?' J4 @$ {" |: g4 G8 M
                # 迭代一个epoch
    ) Z5 Y1 Z- T4 t- s) D4 Z" O            for inputs, labels in dataloaders[phase]:
      ]! m; z7 q% T3 U& t8 w  W" F  I                inputs, labels = inputs.to(device), labels.to(device)
    . J: h: R4 l' M                optimizer.zero_grad() # 零参数梯度+ t+ f$ e4 P7 e# H- B
                                    # 前向,只在训练时跟踪参数; e7 A" L  R( z7 W( Z( X# ]
                    with torch.set_grad_enabled(phase=='train'):, v2 F! j; B: l, r$ O( N, a
                        logits = model(inputs)  # [5, 21, 320, 480]) c1 Q  L. X0 A# R& n+ b
                        loss = criteon(logits, labels.long())
    " i$ Y- ?! Z6 X. L( [                    # 后向,只在训练阶段进行优化8 A( |: K, @6 l0 Z
                        if phase=='train':
    ' w( v0 o7 ]' R8 B& ?" l" G- l( j                        loss.backward()
    6 F' c4 U! k( I5 h8 [                        optimizer.step()
    3 `- k9 J  i/ b" j/ R                                # 统计loss和correct
    # h4 B1 X5 _" ~! }( M2 P                runing_loss += loss.item()*inputs.size(0)
    4 L3 n: r& ]$ j- ^! N- B( [                runing_corrects += torch.sum((torch.argmax(logits.data,1))==labels.data)/(480*320)
    ' x1 B6 J# Z& O$ I& R6 k& D. x" i
                epoch_loss = runing_loss / dataset_sizes[phase]
      o& g8 j9 d  n% z# [. q            epoch_acc = runing_corrects.double() / dataset_sizes[phase]5 w# w+ `2 O& g/ l5 z
                print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
    ! l& d$ e  B# D% Q; ^$ Y' j& H                        # 深度复制model参数: a0 x: i" L5 [9 `. B$ r
                if phase=='val' and epoch_acc>best_acc:
    / c0 b; N0 q9 C                best_acc = epoch_acc
      W$ O7 F) r9 M, o7 j. D, i& I                best_model_wts = copy.deepcopy(model.state_dict())
    $ J( M, i) [: X, f* g! m" h5 Y! O        print()( k: V  A+ a8 o
        time_elapsed = time.time() - since;
    2 |4 L) J4 v& S. Z    print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed//60, time_elapsed%60))
    3 `$ P9 y* }4 W8 q& [    # 加载最佳模型权重, b3 B, k  z9 o$ `' d
        model.load_state_dict(best_model_wts)
    & p  }5 Z: m2 O) z5 w) e    return model  ~  [# t- L7 }  }1 y. c1 B
    + o( x$ o/ D" e' i6 k$ g' Q" \7 f3 c
    下面定义train_model要用到的参数,开始训练
    , ]! \+ t3 D% S  P+ ~
      u  i4 w) B( \! M4 y! H- \+ A. Kepochs = 5 # 训练5个epoch7 g. |+ b) C/ a5 J' m
    criteon = nn.CrossEntropyLoss()
    2 H: Z9 [/ m- u; H+ Voptimizer = optim.SGD(model_ft.parameters(), lr=0.001, weight_decay=1e-4, momentum=0.9)2 C/ \% w/ Q4 ^2 M+ n" j
    # 每3个epochs衰减LR通过设置gamma=0.1
    3 f' |% A$ Q$ texp_lr_scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)
    " [! c3 W: q8 e% y7 D
    7 e3 n9 m) X+ d# 开始训练& C: A3 \5 {" p1 C# S* k. @
    model_ft = train_model(model_ft, criteon, optimizer, exp_lr_scheduler, num_epochs=epochs)
    ( W2 k& w7 m9 K* t2 h8 R) Z: {
    , Y% \8 i! B, g. P8 R6 测试模型

    为了可视化每个像素的预测类别,我们将预测类别映射回它们在数据集中的标注颜⾊。

    def label2image(pred):' [0 a  ^+ _' _8 @6 f$ \) d) F9 s; O
        # pred: [320,480]7 i& r6 E9 `; _' f0 \
        colormap = torch.tensor(VOC_COLORMAP,device=device,dtype=int)% T7 U2 p( [9 s
        x = pred.long()- Y% \6 f4 K/ H3 r, M' t9 S6 e/ c
        return (colormap[x,:]).data.cpu().numpy()! f. X! V" c- L2 y

    1 j( w( k. z% [) `5 M1 D. X

    下面这里提供了两种测试形式

    6.1 通用型

    其实如果要用于测试其它数据集,也是要改动一下的 : ) 😃

    mean=torch.tensor([0.485, 0.456, 0.406]).reshape(3,1,1).to(device)( G1 E9 |5 e) Y, \, f* r" x
    std=torch.tensor([0.229, 0.224, 0.225]).reshape(3,1,1).to(device)
    # g. N- I; S% I- y0 ]- edef visualize_model(model:nn.Module, num_images=4):6 ^( X3 p' ~2 d* e
        was_training = model.training
    0 o, s- ~& S# V8 y$ h3 W: x    model.eval()9 D3 h0 v" D( ]' g8 K
        images_so_far = 0$ ]* u- f( J  X9 l& D
        n, imgs = num_images, []
    6 U% I; o* X8 w, h; K. G    with torch.no_grad():
    . n' o* Y# s8 C& I        for i, (inputs, labels) in enumerate(dataloaders['val']):
    4 y: c4 l# T3 ]3 f/ \) O3 e            inputs, labels = inputs.to(device), labels.to(device) # [b,3,320,480]
    # Y5 L0 @5 F  M2 ]% f* ~            outputs = model(inputs)* u1 o+ N, U# q1 ~
                pred = torch.argmax(outputs, dim=1) # [b,320,480]' Z3 p- J# }  F% D
                inputs_nd = (inputs*std+mean).permute(0,2,3,1)*255 # 记得要变回去哦
    # C" G2 P: z/ Z4 A
    ! S1 [% |' N( E0 Y" g2 {2 J            for j in range(num_images):' z3 a6 y- g! j* }+ Y" E5 E
                    images_so_far += 1
    : L8 K9 r6 V- e                pred1 = label2image(pred[j]) # numpy.ndarray (320, 480, 3)
    7 M" ~7 [/ n0 z) O% ~, s. u                imgs += [inputs_nd[j].data.int().cpu().numpy(), pred1, label2image(labels[j])]& `' M  \7 b0 Y, Z: `6 r& N( H( @
                    if images_so_far == num_images:, j/ b! ?7 a2 d$ t" F3 F' e0 Y8 K/ e
                        model.train(mode=was_training)
    ( }5 S' l+ h! O, K2 O/ w1 f                    # 我已经固定了每次只显示4张图了,大家可以自己修改/ S; W5 g6 B7 |; o% {3 Y7 X, H) R
                        show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n). T; X9 q; p! c; P* v7 s/ R
                        return model.train(mode=was_training)
    # m( r5 @! n, c% }9 S# X* s5 E* ~% X, B. b' ]! ]: B$ X
    # 开始验证+ v: `- Q2 x) c+ P  {8 P& C. @$ |
    visualize_model(model_ft). N3 C/ h3 j! `5 U* ?  ^
    6.2 不通用

    在预测时,我们需要将输⼊图像在各个通道做标准化,并转成卷积神经⽹络所需要的四维输⼊格式。

    # 预测前将图像标准化,并转换成(b,c,h,w)的tensor) J8 H3 |0 O5 j# |% i3 x: K1 j3 W+ W
    def predict(img, model):9 T, v& ^1 G8 c8 v% ?( e( S% U8 f' x
        tsf = transforms.Compose([/ n! T& M9 k- R  X2 T2 @
                transforms.ToTensor(), # 好像会自动转换channel
    3 ]; l! B2 Q: Q4 l- u0 M            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])])  m0 Q5 c3 g4 l
        x = tsf(img).unsqueeze(0).to(device) # (3,320,480) -> (1,3,320,480)
    : }1 x: e. y/ }    pred = torch.argmax(model(x), dim=1) # 每个通道选择概率最大的那个像素点 -> (1,320,480)1 R8 j& a7 c$ V& d' p$ k
        return pred.reshape(pred.shape[1],pred.shape[2]) # reshape成(320,480)" a' m" G0 C2 R& d% Y
      x8 K$ _  ?# h* K2 r& F
    def evaluate(model:nn.Module):5 j7 P# V8 |- L' A1 t7 i: `
        model.eval()9 U$ u3 ~( H$ W4 p/ Y6 u
        test_images, test_labels = read_voc_images(voc_dir, is_train=False, max_num=10)
    ) [  `  R. v: T1 A3 N( q9 e0 X    n, imgs = 4, []# ^  _* P0 j, I. W; {5 l
        for i in range(n):
    0 _, `, S* W$ S        xi, yi = voc_rand_crop(test_images, test_labels, 320, 480) # Image+ K1 o8 F% F8 k9 b
            pred = label2image(predict(xi, model))  G2 p# y/ u3 A+ H$ X, |( A; I
            imgs += [xi, pred, yi]
    8 d, q4 D- w6 |5 n" T- O    show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n)
    6 k4 r9 }; r2 Q6 D
    ' z/ ~3 n* R5 y9 |# 开始测试
    / ?  P0 ~% A; m9 ]5 ~% Gevaluate(model_ft)
    0 m5 }" N' v$ g# O( u8 L$ B- M' {
    1 q# X) d# p+ N0 D1 Z+ O! h7 结语

    我只训练了3个epoch,下面是训练输出

    ( \5 N1 q; ?6 a  }: S4 I
    Epoch 0/2: v2 q! m/ [: n: ?; R8 J
    ----------
    # U* W+ K: h5 e* \) Utrain Loss: 1.7844 Acc: 0.5835
    1 Z( U* t  Z. v8 K$ ?% q. @val Loss: 1.1669 Acc: 0.6456
    % l9 l. T: b. o" `& Z: t, h7 w- N0 l' q( w8 U+ |3 a
    Epoch 1/2
    0 l& {, G' j2 A) D1 w" k& W----------, I4 Y8 g1 j( z1 v/ B) B
    train Loss: 1.1288 Acc: 0.6535
    + L1 Z! _1 @7 a; }# D) ^val Loss: 0.9012 Acc: 0.69294 T2 O, g/ o5 }* j

    $ X- p/ h8 I% H: H: A: j; \+ u# m- ]Epoch 2/2
      A3 d6 }7 ?. Z: N----------& @2 a3 a- \: w) j1 h
    train Loss: 0.9578 Acc: 0.6706' v9 o/ I; K3 |) y* J
    val Loss: 0.8088 Acc: 0.6948
    . n7 q4 v+ f: `* o2 O
    . ~' `5 b# V4 p4 J* P/ d* F4 {/ DTraining complete in 6m 37s; o; Z) B# @, E0 D! s! l! D

    0 s3 \" ~! M9 @& H- E7 @. V1 m7 n( x' {8 R7 _: `
    2.jpg ! K% M4 d2 _9 }  @2 H

    , G  W; R1 R. z( E4 {( L/ j当 epochs = 5 时,训练集的精度在 89 8989% 左右,测试集的精度可以达到 86​ 86​86​ %。8 S: @* n1 t5 |% k3 t

    , K% a& d+ `3 p) x对于这个模型用 ResNet-50 作特征提取器会有更好的效果,不过训练的时间也会更长。还有超参数lr, weight_decay, momentum, step_size, gamma 以及1×1 1×11×1卷积层和转置卷积层的初始化方式也可以继续调。
    ) ?0 W" m4 }# `1 c' M6 ^5 h5 Y# Q  b" {% J/ |

    $ t- q3 z" A! Q' q8 k1 `语义分割还有很多可用的模型,本文用的是 FCN,在其它一些模型上会有更好的表现:
    ! Z* c  U3 Z4 b3 T
    - G' g; l) p- G6 m  ], @# bDeeplab V3+ 具有可分离卷积的编码器/解码器,用于语义图像分割[论文]3 u" q6 Q& r2 _. t7 p2 E
    GCN 通过全局卷积网络改进语义分割[论文]
    # _% r/ G# w, L0 qUperNet 统一感知解析
    & f: O" a: x) f8 MENet 用于实时语义分割的深度神经网络体系结构[论文]; v7 c, n/ @. J$ p7 _  u! O1 v
    U-Net 用于生物医学图像分割的卷积网络
    ( {, ?& g( Y: p" VSegNet 用于图像分段的深度卷积编码器-解码器架构。
    ; _4 G" i9 \; B$ d还有(DUC,HDC)、PSPNet等。/ M6 T! [" }# j( U7 }- H4 e
    : X; G9 _" O  |( {2 ~. k  k
    常用的语义分割数据集也有很多:Pascal VOC、CityScapes、ADE20K、COCO Stuff等。0 j, k7 r: g+ Q4 e+ {' U

    ! u0 k- j! D0 i$ D% j& n对于损失函数,除了交叉熵误差,也可以用这些:
    ( X3 p' Z/ j8 K/ B( O3 u, M# H% a" v4 R$ e) n
    5 S+ K! o* D0 nDice-Loss 可以测试两个样本之间的重叠度量,可以更好地反映训练目标,但该损失函数具有很强的非凸性,很难优化。( [7 ~5 ?, f3 P% E0 p( Q, D
    CE Dice loss Dice 损失与 CE 的总和,CE 提供了平滑的优化,而 Dice 损失则很好地表明了分割结果的质量。4 C9 C. m2 P. g* I2 R: Y6 }
    Focal Loss CE 的另一种版本,用于避免类别不平衡而降低了置信度的情况。
    . C) ]: c7 A. X7 a4 x) I) q! GLovasz Softmax 查看论文:Lovasz - softmax损失。( q+ @1 K( {" Q; A- l- F  @

    8 X: z0 p9 r- B; L5 y2 x& o( C' K( [* R% F9 ?9 B
      S- [0 A  K- A9 m
    9 c* R7 W" M3 Z

    7 X: T; ~6 u) v- {' X————————————————5 {  ^7 ]( v2 `
    版权声明:本文为CSDN博主「小红不吃糖」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    , p( v; `7 `6 k8 g- s5 w原文链接:https://blog.csdn.net/qq_43280818/article/details/105916507
    % @* Y6 I6 {6 G" i: l
    - D6 @* C* z8 y& ]! ]1 J  R5 i1 l9 z( P
    ! s. C9 W$ Z0 }  x" w( W
    zan
    转播转播0 分享淘帖0 分享分享0 收藏收藏0 支持支持0 反对反对0 微信微信
    您需要登录后才可以回帖 登录 | 注册地址

    qq
    收缩
    • 电话咨询

    • 04714969085
    fastpost

    关于我们| 联系我们| 诚征英才| 对外合作| 产品服务| QQ

    手机版|Archiver| |繁體中文 手机客户端  

    蒙公网安备 15010502000194号

    Powered by Discuz! X2.5   © 2001-2013 数学建模网-数学中国 ( 蒙ICP备14002410号-3 蒙BBS备-0002号 )     论坛法律顾问:王兆丰

    GMT+8, 2026-7-18 13:36 , Processed in 0.479281 second(s), 53 queries .

    回顶部