QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 11194|回复: 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
    " }. L5 r) _) k$ h* i  A/ \

    * X' l! X* o9 q5 q! _: }8 [Pytorch实战语义分割(VOC2012)& T6 p& N& y" Z: x5 u
    本文参照了《动手深度学习》的9.9、9.10章节,原书使用的是 mxnet 框架,本文改成了pytorch代码。
    2 {, W, W+ A+ Z' Q  D* @8 T语义分割(semantic segmentation)问题,它关注如何将图像分割成属于不同语义类别的区域。值得一提的是,这些语义区域的标注和预测都是像素级的。
    1 j; p6 J3 T6 l( g# W4 m
    8 {' @* A5 x: V/ \: |( q
    ( T2 S! I4 n" ?1 i  I语义分割中图像有关狗、猫和背景的标签/ ^7 S& b! j1 N8 {( g: W
    文章目录
    - k9 H3 ]( w! \2 l, F
    ( A% X5 `" h0 Y% d- x1 图像分割和实例分割2 L- b1 b* E  P8 i7 H
    2 Pascal VOC2012语义分割数据集
    0 [+ Y9 j$ k3 z2.1 导入模块
    0 c" H6 q* z* |5 ]. ~  M2.2 下载数据集6 d4 S5 [6 a9 S. E! V, Q9 i( Q6 @* }
    2.3 可视化数据
    8 N7 \! S; v0 f2.4 预处理数据
    6 C# U0 w1 N5 A& A9 m" N3 自定义数据集类
    . v0 \/ P/ v- z, w3.1 数据集类0 [; N8 g, k" p! E6 J! r5 Y
    3.2 读取数据集' c$ k: f6 R; Y* D
    4 构造模型& R+ @9 K2 `' z5 ^7 ?( Q, l9 C
    4.1 预训练模型
    + Z" F3 S6 y7 _$ k4.2 修改成FCN# Z% v4 _0 ^9 o" v
    4.3 初始化转置卷积层% a) m* m. c6 @( y
    5 训练模型' i. ^# c* F/ j
    6 测试模型3 r& M9 b  f% ?  N
    6.1 通用型$ k' }4 `7 T6 ~
    6.2 不通用
    % ?# [: a% T! O+ a7 结语
    . n9 ^1 k% h6 z& Y& k6 z: o1 图像分割和实例分割
    8 ^' ]" s% K7 S. ?4 l) j! m4 N9 E" P5 H) j8 M2 z
    计算机视觉领域还有2个与语义分割相似的重要问题,即图像分割(image segmentation)和实例分割(instance segmentation):, T9 _9 _7 F* \" b
    8 k- ?: S1 ^* \' |! c) ]0 ]
    图像分割将图像分割成若干组成区域。这类问题的方法通常利用图像中像素之间的相关性。它在训练时不需要有关图像像素的标签信息,在预测时也无法保证分割出的区域具有我们希望得到的语义。以上图的图像为输入,图像分割可能将狗分割成两个区域:一个覆盖以黑色为主的嘴巴和眼睛,而另一个覆盖以黄色为主的其余部分身体。
    / r) e& }& @* e2 P! j5 T实例分割又叫同时检测并分割(simultaneous detection and segmentation)。它研究如何识别图像中各个目标实例的像素级区域。与语义分割有所不同,实例分割不仅需要区分语义,还要区分不同的目标实例。如果图像中有两只狗,实例分割需要区分像素属于这两只狗中的哪一只。
    " W$ \% ~% U: |% l- e  x0 c% _5 `  L  t" i
    2 Pascal VOC2012语义分割数据集9 h) [! Q$ U# J

    - p7 X) e6 v" }% i2.1 导入模块
    9 x8 C# a0 K7 B4 y# |/ Fimport time& s+ F% I5 t8 ~
    import copy6 J: {) ~- p5 Q2 ?8 X) ?2 p5 O/ b$ e
    import torch0 |5 w* ^& Z/ r" s, V; U
    from torch import optim, nn" x( T. z; z/ G% s5 \
    import torch.nn.functional as F9 x' r; L2 N1 l  W
    import torchvision
    2 n4 v  P, U: @+ p& Xfrom torchvision import transforms
    - _9 k! }; A; j+ v0 N9 h2 Dfrom torchvision.models import resnet18
    * k  o+ O: ]" e& E8 {import numpy as np& t. o2 ]4 S' K4 ^- p( G( z; g' a
    from matplotlib import pyplot as plt
    1 c. T' L; T6 H9 ^from PIL import Image
    5 s) @$ R5 F0 v9 T+ R' qimport sys2 \7 w0 \* z9 j4 @6 {
    sys.path.append("..")
    7 M) n5 `9 f8 b: |from IPython import display* A7 I2 a0 A7 r$ @5 A4 g) Z2 L
    from tqdm import tqdm
    ) a1 O: w. |4 y( {0 ]import warnings
    : t% j1 @4 \# |3 Awarnings.filterwarnings("ignore")
    - Q0 w' g8 }  T: Z( o
    6 m9 E! a5 W- ~2 N! O! I2.2 下载数据集+ M2 }7 w/ s. r5 _
    9 Y* z1 N9 c* I7 |$ S/ S3 |  Y, o  s
    语义分割的一个重要数据集叫作Pascal VOC2012,点击下载这个数据集的压缩包,大小是2 GB左右,所以下载需要一定时间。下载后解压得到VOCdevkit/VOC2012文件夹,然后将其放置在data文件夹下,VOC2012文件目录是这样的:
    6 x5 r1 D3 F$ w& O0 P1 _) V3 I0 |! U/ t; s. }/ `3 d  x  z6 D
    + k# s7 T! }" S9 y
    ImageSets/Segmentation路径包含了指定训练和测试样本的文本文件; c) \6 |* y/ X% S# L  Z+ _1 O
    JPEGImages和SegmentationClass路径下分别包含了样本的输入图像和标签。这里的标签也是图像格式,其尺寸和它所标注的输入图像的尺寸相同。标签中颜色相同的像素属于同一个语义类别。& h% l9 A9 X9 D4 z6 |$ H
    2.3 可视化数据8 b0 |- X9 ]: K# W
    ( }: `/ X, l# W! a4 w7 |4 R
    定义read_voc_images函数将输入图像和标签读进内存。
    5 `" h9 R' Y$ P) V) v; F  [& i* [! M
    def read_voc_images(root="../../data/VOCdevkit/VOC2012", is_train=True, max_num=None):- a0 H% j1 ]3 o' P' B+ g  M* N
        txt_fname = '%s/ImageSets/Segmentation/%s' % (root, 'train.txt' if is_train else 'val.txt')
    6 b, e7 B% ]% W4 K7 x6 w    with open(txt_fname, 'r') as f:
    , m+ ]3 u# U) `6 [: Z+ T        images = f.read().split() # 拆分成一个个名字组成list
    " K9 _. `% b8 b$ y$ l# A! ^    if max_num is not None:" ?+ D' t% [2 q4 Z
            images = images[:min(max_num, len(images))]+ W/ P( a% Y3 Z5 n; [
        features, labels = [None] * len(images), [None] * len(images)  Y; A% X1 @" u) V: y/ a8 u& J! |
        for i, fname in tqdm(enumerate(images)):# n( |9 l' ]7 h4 E$ t
            # 读入数据并且转为RGB的 PIL image* u3 }) Z$ j  g8 [
            features = Image.open('%s/JPEGImages/%s.jpg' % (root, fname)).convert("RGB")9 w3 i3 }; W( J+ y+ D9 R
            labels = Image.open('%s/SegmentationClass/%s.png' % (root, fname)).convert("RGB")
    - ]: [. [6 ?3 B% c    return features, labels # PIL image 0-255
    . f' Y. p( Y0 l6 U% \9 _2 j& A' }/ O3 i3 D- z. w4 w
    定义可视化数据集的函数show_images
    2 z/ T1 }, Y0 W. g! W; [; ~0 o; t
    # 这个函数可以不需要
    # i* _6 `2 y" V: Wdef set_figsize(figsize=(3.5, 2.5)):
    + @; F8 m, @6 p1 c$ L+ W: i    """在jupyter使用svg显示"""
    + P- L4 x( Q4 f    display.set_matplotlib_formats('svg')+ A, w8 p0 |; O4 A
        # 设置图的尺寸
    - y8 r; W( P# z& p, w( `    plt.rcParams['figure.figsize'] = figsize" o7 w2 M  y& w" b- c

    % r2 H1 s) C+ @# @; s$ Tdef show_images(imgs, num_rows, num_cols, scale=2):
    2 g8 H. M- K/ Z5 f# ~0 l    # a_img = np.asarray(imgs)7 t) \+ C! x4 i6 z
        figsize = (num_cols * scale, num_rows * scale)
    # o! P& _' E, R, G( |* f    _, axes = plt.subplots(num_rows, num_cols, figsize=figsize)
    6 b! ~. ]# Q6 f) o; Q/ n4 j    for i in range(num_rows):
    " j2 P. s3 Y$ u  c: S: i        for j in range(num_cols):$ P- i: }! q, Y  u. u& G  K
                axes[j].imshow(imgs[i * num_cols + j])8 U0 o8 b, e; i- Z5 k& B
                axes[j].axes.get_xaxis().set_visible(False)/ H7 B/ Y7 V6 j* V/ u9 G* N
                axes[j].axes.get_yaxis().set_visible(False)7 n8 y0 i" J8 P! f/ _  s  K& ^  ?$ v3 |
        plt.show()
    + q# l; f2 l2 G    return axes* v6 ~% P7 g$ b$ O. m# V4 U
    ) r) U3 m3 U  d7 [  L! @
    定义可视化数据集的函数show_images+ L5 M% |+ Q& W3 w2 r6 c( @0 V) Z
    1 Y& Q6 Q& R" P' X- d! W
    # 这个函数可以不需要& `4 u( b3 ^7 W3 W5 A+ j/ a
    def set_figsize(figsize=(3.5, 2.5)):9 m) ]+ K9 ~) a* r
        """在jupyter使用svg显示"""
    + M& Z7 w* x- _, t. o# {    display.set_matplotlib_formats('svg')( V+ ]7 }' b8 ?
        # 设置图的尺寸3 R  ]: g3 Y3 T! H. W
        plt.rcParams['figure.figsize'] = figsize
    " Z* g# {. H( B4 j# x9 v, E3 z
    . J# \9 r' W) ^def show_images(imgs, num_rows, num_cols, scale=2):
    $ _/ x6 F+ X) _    # a_img = np.asarray(imgs)- @! N$ q' a7 Y$ J/ {& A( u
        figsize = (num_cols * scale, num_rows * scale)0 K, L3 A. n( q- L
        _, axes = plt.subplots(num_rows, num_cols, figsize=figsize)( w5 o* u- ]$ t* u, Y' N+ l
        for i in range(num_rows):
    0 b9 O) f4 [% h# Z. ~        for j in range(num_cols):; P' n1 G4 l5 q2 s  d7 @. [& ?& f
                axes[j].imshow(imgs[i * num_cols + j])+ b9 t$ z  s9 \3 u
                axes[j].axes.get_xaxis().set_visible(False)
    0 d6 M8 u2 y9 R7 ~* x2 z2 v            axes[j].axes.get_yaxis().set_visible(False)  R- g) y# F7 p, z' K5 ]
        plt.show(): G& ?  P( o$ M+ ]) }  H, b
        return axes
    ( L% W6 ]* s/ |7 t8 w3 e$ |/ Y画出前5张输入图像和它们的标签。在标签图像中,白色和黑色分别代表边框和背景,而其他不同的颜色则对应不同的类别。
    4 C4 _5 K& h0 q- t% _7 f% p7 m3 ~) c; W& R4 G& s3 Z
    # 根据自己存放数据集的路径修改voc_dir1 v6 V3 p$ c1 {: `
    voc_dir = r"[local]\VOCdevkit\VOC2012"6 J* i  V' \4 D4 Q: y
    train_features, train_labels = read_voc_images(voc_dir, max_num=10)& l9 n2 p! I9 |9 i) a6 i8 q
    n = 5 # 展示几张图像
    " z7 U1 I& u: j: l- Q, Z$ n: vimgs = train_features[0:n] + train_labels[0:n] # PIL image4 q' G7 H( b4 }' l5 t  e0 s
    show_images(imgs, 2, n)
    % `' m) z* H8 ^4 t7 r
    - D2 P$ }0 h, U2 y- \ 1.png
    ) m3 K5 p, E. n( K: v! ]7 n9 U' C$ _- {1 n) j* m3 j/ S( C
    列出标签中每个RGB颜色的值及其标注的类别。
    ' K7 e9 D) }+ u* e. J8 t# 标签中每个RGB颜色的值. e+ F* m/ a, r. X
    VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
    # R8 q! R$ f5 v; w8 M; ]: n                [0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128],$ T1 T3 l' J4 A
                    [64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0],
    * |* |6 `: s3 g- b                [64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128],7 }  S! W! ?; O& }) D) H; D
                    [0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0],
    " E2 c' c$ l1 M. f                [0, 64, 128]]7 [  K# g* {: P7 N$ y: [0 @7 W
    # 标签其标注的类别
    . Y9 z5 s7 t0 @+ x* B1 H, TVOC_CLASSES = ['background', 'aeroplane', 'bicycle', 'bird', 'boat'," m, Q( F$ D1 \6 U
                   'bottle', 'bus', 'car', 'cat', 'chair', 'cow',, |; {  j$ R. H  B1 O/ f
                   'diningtable', 'dog', 'horse', 'motorbike', 'person',
    " O* o- }  W: `6 U, |' O               'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor']
    7 z; N% A7 e: x1 A  w3 g1 R有了上面定义的两个常量以后,我们可以很容易地查找标签中每个像素的类别索引voc_label_indices是根据colormap2label把标签里的 rgb 颜色对应上面的VOC_COLORMAP中的下标给取出来,当作 label 。, M  y% P2 R2 b7 e5 b' l

    ! o1 x: k! o2 R% Y" [, _有了上面定义的两个常量以后,我们可以很容易地查找标签中每个像素的类别索引voc_label_indices是根据colormap2label把标签里的 rgb 颜色对应上面的VOC_COLORMAP中的下标给取出来,当作 label 。2 ~# m$ v  o+ o6 V
    colormap2label = torch.zeros(256**3, dtype=torch.uint8) # torch.Size([16777216])
    & t4 E) ]  p8 ?4 g- @5 D! o- kfor i, colormap in enumerate(VOC_COLORMAP):
    : {: o3 i9 D. e6 x& t9 Q    # 每个通道的进制是256,这样可以保证每个 rgb 对应一个下标 i
    * v6 b8 s' W& U4 K/ c    colormap2label[(colormap[0] * 256 + colormap[1]) * 256 + colormap[2]] = i% R4 }( S# b' e2 T- k8 i9 z) r  O
    % z& ~0 o' ?9 N% L* o0 ]7 f
    # 构造标签矩阵
    9 n" O* w: h4 Y# {% B0 E2 ~. mdef voc_label_indices(colormap, colormap2label):! V9 `$ B9 \0 x' g# @, w
        colormap = np.array(colormap.convert("RGB")).astype('int32')0 {2 d/ X0 ]. A$ C; L
        idx = ((colormap[:, :, 0] * 256 + colormap[:, :, 1]) * 256 + colormap[:, :, 2]) ) B4 \! @1 Z( M1 a
        return colormap2label[idx] # colormap 映射 到colormaplabel中计算的下标/ o, z: C& t4 ~0 n$ f/ i. c* g

    % z( P6 ]# j8 v& U9 o/ }可以打印一下结果
    # }) _5 H1 l1 Y: l& l
    8 W+ \7 Q1 r( L- b, h2 o; O2 g# py = voc_label_indices(train_labels[0], colormap2label)# j( |6 O3 {1 q. m
    print(y[100:110, 130:140]) #打印结果是一个int型tensor,tensor中的每个元素i表示该像素的类别是VOC_CLASSES
    " I! ^! X* u* t
    0 k) p6 s# c' g! |1 X2.4 预处理数据9 q' Y; H3 D) X, |) W9 b- {6 e

    % Q% [% W9 ~6 [; j$ I  x在语义分割里,如果使用缩放图像使其符合模型的输入形状的话,需要将预测的像素类别重新映射回原始尺寸的输入图像,这样的映射难以做到精确,尤其是在不同语义的分割区域。所以选择将图像裁剪成固定尺寸而不是缩放。具体来说,我们使用图像增广里的随机裁剪,并对输入图像和标签裁剪相同区域。
    - D5 M8 f( h! f% d7 @) c! @  C% r, ^$ m

    8 p8 B, d' f9 u  P; ndef voc_rand_crop(feature, label, height, width):" d) p0 A/ C+ ]
        """7 n/ U* v, W  F; z  d
        随机裁剪feature(PIL image) 和 label(PIL image).
    ; L7 j, s% [7 x$ y4 o    为了使裁剪的区域相同,不能直接使用RandomCrop,而要像下面这样做, ^* W, I3 D% G& J
        Get parameters for ``crop`` for a random crop.7 q* ?) I& }2 y: w5 f* ?
        Args:
    6 `* T: e* e8 |; ?5 i+ f        img (PIL Image): Image to be cropped.! j( {/ q- P1 B( _5 B
            output_size (tuple): Expected output size of the crop.
    % g' ~+ \6 |/ v$ O( w    Returns:# {1 p. Y; ~. L
            tuple: params (i, j, h, w) to be passed to ``crop`` for random crop.
    3 ^' X6 @5 C" A# W    """$ \2 l& v6 y  t, ^' o4 X/ {* P
        i,j,h,w = torchvision.transforms.RandomCrop.get_params(feature, output_size=(height, width))
    1 P9 ?8 p6 t3 d, K    feature = torchvision.transforms.functional.crop(feature, i, j, h, w)$ X2 k6 H! _( Z! q
        label = torchvision.transforms.functional.crop(label, i, j, h, w)2 }4 v8 f1 m& M# E. Z
        return feature, label
    & C) e5 }+ [1 L- n" |7 Y" M
    3 |6 U9 P* e1 m% i  r# 显示n张随机裁剪的图像和标签,前面的n是5
    : ?( C# S5 r2 P8 `0 Gimgs = []" I. x2 k+ a# l. N1 X$ |# a
    for _ in range(n):6 }& y: U. E3 c6 p! s! y  G
        imgs += voc_rand_crop(train_features[0], train_labels[0], 200, 300)
    + B& ?) c5 g1 m# |show_images(imgs[::2] + imgs[1::2], 2, n);
    ' t( ]8 t9 I4 u" F
      K0 x5 h, q- T5 ~$ E3 i
    2 N" V. k# `! A% D5 j4 Y# \" B 2.png / J  V8 F* V6 X7 m" a. J3 e2 u

      w5 C* M: ?; i7 ^$ Y' E3 O$ {3 D* F/ k

    % a: F7 |" W: c- S' f3 自定义数据集类1 N  T' ?+ z  v- ^" _
    ' g" r/ K9 g- n( }
    3.1 数据集类% E0 G% I3 i6 b$ H* S
    2 w) ?# |- \8 t: i5 w
    torch.utils.data.Dataset是表示数据集的抽象类,因此自定义数据集应继承Dataset并覆盖以下方法( h& e: a3 ^+ ]( _3 x+ w

    $ \0 ?* V' s9 f__len__ 实现 len(dataset) 返还数据集的尺寸。
    0 d2 i+ N& L, \0 m__getitem__用来获取一些索引数据,例如 dataset[idx] 中的(idx)。( f& L9 `: y6 v
    由于数据集中有些图像的尺寸可能小于随机裁剪所指定的输出尺寸,这些样本需要通过自定义的filter函数所移除。此外,因为之后会用到预训练模型来做特征提取器,所以我们还对输入图像的 RGB 三个通道的值分别做标准化。
    , d' }$ n1 u! h" k  _" Y+ w, |1 E+ U! c( L* G
    class VOCSegDataset(torch.utils.data.Dataset):1 y, j2 f7 C5 B/ e$ ?
        def __init__(self, is_train, crop_size, voc_dir, colormap2label, max_num=None):3 f6 P, @% j8 G- m
            """
    , s0 G2 r' i8 A" _5 l        crop_size: (h, w)
    * t( I- u& S0 ~& C# C        """
    2 `, }/ N9 T" H/ R7 {% D2 r        # 对输入图像的RGB三个通道的值分别做标准化) S" F5 L7 Y; c
            self.rgb_mean = np.array([0.485, 0.456, 0.406])
    ! U! G8 I2 i! U' q        self.rgb_std = np.array([0.229, 0.224, 0.225])
    & k* t4 w, X9 e& i9 P, m        self.tsf = torchvision.transforms.Compose([
      x7 C5 @# W' A/ ]            torchvision.transforms.ToTensor(),5 c( F9 a; W7 m4 _# o! `  g
                torchvision.transforms.Normalize(mean=self.rgb_mean, std=self.rgb_std)])9 N& b  ~6 c+ J1 v9 q! r
            self.crop_size = crop_size # (h, w)3 e* [" ?. g( _4 g/ p& w
            features, labels = read_voc_images(root=voc_dir, is_train=is_train,  max_num=max_num)4 h) d& x) ?1 a& _$ y
    # 由于数据集中有些图像的尺寸可能小于随机裁剪所指定的输出尺寸,这些样本需要通过自定义的filter函数所移除
    . ~- j6 d0 R+ A0 W( i, B        self.features = self.filter(features) # PIL image
    ! R7 y) X  ~. R+ w/ a. V        self.labels = self.filter(labels)     # PIL image
    " l. e3 [) I1 s! U8 a$ X+ X        self.colormap2label = colormap2label. ^4 R% E5 H/ J/ S+ U7 J+ }. P1 ^
            print('read ' + str(len(self.features)) + ' valid examples')
    - f2 U# i0 n7 w3 Z3 X. i7 G) \$ g' @# P# q
        def filter(self, imgs):
    , @3 @0 M& l* ]- v. X# q        return [img for img in imgs if (
    - p" p7 h  Q4 l" f8 y0 A            img.size[1] >= self.crop_size[0] and img.size[0] >= self.crop_size[1])]
    $ [' C/ A2 K$ I8 J  g5 ]
    8 `7 S* h) b6 s) C3 c0 L    def __getitem__(self, idx):* P% E( S7 f- G4 b" T7 t3 }& G  v
            feature, label = voc_rand_crop(self.features[idx], self.labels[idx], *self.crop_size)
    , Q4 d% [8 V5 e4 r; k( H                                # float32 tensor           uint8 tensor (b,h,w); i8 Z- a6 n) |- P; W5 F: q$ _
            return (self.tsf(feature), voc_label_indices(label, self.colormap2label))9 I/ D) x: S- b8 N

    ; Y+ c1 D& N1 J$ @' t7 d' |* [    def __len__(self):' V% h1 ?& L) h$ \
            return len(self.features)
    # W" M5 i6 v% F7 _3 p% z- D% f1 @1 G3.2 读取数据集4 l( J& y7 f) M3 r7 ^

    " _* f6 L4 t- p( {通过自定义的VOCSegDataset类来分别创建训练集和测试集的实例。因为待会用的是全卷积网络,所以随机裁剪的输出图像的形状可以自己指定,这里指定为320×480​ 320\times 480​320×480​。* Q9 B- p' n0 L& }7 H, w. l; s

    3 p* n/ O% M. a# P2 B' D6 M1 Ybatch_size = 32 # 实际上我的小笔记本不允许我这么做!哭了(大家根据自己电脑内存改吧)
    / J- n! }1 y) i& l% y4 ccrop_size = (320, 480) # 指定随机裁剪的输出图像的形状为(320,480)
    ( [+ @+ r8 A. L. Xmax_num = 20000 # 最多从本地读多少张图片,我指定的这个尺寸过滤完不合适的图像之后也就只有1175张~
    8 V" I: `. z! U6 e$ S" _: P' d/ i3 x7 X- m2 f
    # 创建训练集和测试集的实例
    - T5 O) p) E% a: u6 \voc_train = VOCSegDataset(True, crop_size, voc_dir, colormap2label, max_num)
    5 l6 t! _! L/ E7 W6 dvoc_test = VOCSegDataset(False, crop_size, voc_dir, colormap2label, max_num)! q# x& j8 d1 p7 B
    ( b% @1 n% O# ]( T5 j
    # 设批量大小为32,分别定义【训练集】和【测试集】的数据迭代器
    & B) j3 l! e( I: W, `3 T& wnum_workers = 0 if sys.platform.startswith('win32') else 4
    % T- c9 O# R2 _" B3 z. dtrain_iter = torch.utils.data.DataLoader(voc_train, batch_size, shuffle=True,
    2 P  V: g  n. K+ @- A, c6 H# s                              drop_last=True, num_workers=num_workers)
    3 R4 u7 z- b' K" ^) M- ytest_iter = torch.utils.data.DataLoader(voc_test, batch_size, drop_last=True,
    3 G3 \9 ~2 x" d                             num_workers=num_workers)
    2 ?' ]/ @: Y  q
    " R9 J  s6 i& q! w- c- v; f. y# 方便封装,把训练集和验证集保存在dict里
    * G* ^# r5 [( S' d+ p! Ydataloaders = {'train':train_iter, 'val':test_iter}
    ) n/ A( C( z5 H1 x8 Gdataset_sizes = {'train':len(voc_train), 'val':len(voc_test)}  q5 U' G5 }. e# \2 Y

    9 P/ e) P9 ^( |) i, b4 构造模型4.1 预训练模型

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

    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')/ h* Y0 L$ x$ w' s5 ?# Z1 y

    , Q$ N! S$ w8 a$ `6 U/ k/ Enum_classes = 21 # 21分类,1个背景,20个物体' D8 l  Y6 x; E, o. }# l! _% @
    model_ft = resnet18(pretrained=True) # 设置True,表明要加载使用训练好的参数
    5 O4 f, f8 {2 `' K! L8 P1 ?$ ?: o3 U' S1 q; M& d3 S! r0 f- w
    # 特征提取器
    ! t9 f9 x1 z2 u7 W' Z- T6 pfor param in model_ft.parameters():
    ! R# R# G: z0 M- U8 e5 N+ E# i" Q    param.requires_grad = False
    6 z- K, O' S1 E; o8 d) _1 l, `' C! F4.2 修改成FCN
    : o- E/ y! U: d; k
    $ S& Z8 f) ?' C- K+ c全卷积⽹络(顾名思义全部都是卷积层)先使⽤卷积神经⽹络抽取图像特征,然后通过 1×1​ 1\times 1​1×1​ 卷积层将通道数变换为类别个数,最后通过转置卷积层将特征图的⾼和宽变换为输⼊图像的尺⼨。模型输出与输⼊图像的⾼和宽相同,并在空间位置上⼀⼀对应:( a) Q+ |2 z7 J3 ]* m( N
    最终输出的通道包含了该空间位置像素的类别预测。. ~1 \* E8 Z; ]; o5 N8 e* I
    % ^. V5 o, F$ t
    对于转置卷积层,如果步幅为 S​ S​S​、填充为 S/2​ S/2​S/2​ (假设为整数)、卷积核的⾼和宽为 2S​ 2S​2S​,转置卷积核将输⼊的⾼和宽分别放⼤ S​ S​S​ 倍。
    7 {$ G4 R( e8 \, n2 k" V# E, a7 P. `2 P, X. M- z
    可以先打印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 倍即可。/ V7 x* `! j% ]; I
    " U  a' P6 A6 v  o7 ?" y* {% x" p
    model_ft = nn.Sequential(*list(model_ft.children())[:-2], # 去掉最后两层
    9 ~) {) K* E! f& P% ~1 q7 J7 G              nn.Conv2d(512,num_classes,kernel_size=1), # 用大小为1的卷积层改变输出通道为num_class- |$ d; n/ B. U  v- @6 G' T
                  nn.ConvTranspose2d(num_classes,num_classes, kernel_size=64, padding=16, stride=32)).to(device) # 转置卷积层使图像变为输入图像的大小
    3 p" B0 ]! A* O1 P  H" `' `
    : W9 ^! r% R. N1 W2 ?7 k* }' [# 对model_ft做一个测试
    ! F$ T( V# Q! `x = torch.rand((2,3,320,480), device=device) # 构造随机的输入数据( M* P/ d- I/ d9 Z, I0 R) O
    print(net(x).shape) # 输出依然是 torch.Size([2, 21, 320, 480])   q. y" e6 G: W, h
    6 P3 ?6 S( C% [  t7 G7 ]. n& D4 t% f
    # 打印第一个小批量的类型和形状。不同于图像分类和目标识别,这里的标签是一个三维数组
    ; s  Q% {* o9 x  C! [# E5 e# for X, Y in train_iter:
    + X0 x$ j0 {  a4 [- B% f#     print(X.dtype, X.shape)
    ( P8 Y1 o. Y& Y/ s+ v0 `" D. B#     print(Y.dtype, Y.shape)6 Y$ [; y1 j( Z! q2 K) }
    #     break0 E/ D* g! l( ?' c% o% ~5 z
    # Q- f! B  H1 k' ^  A: U" w- ^; z/ b% u
    - n0 L/ Q( f! G$ t2 A- r" g
    4.3 初始化转置卷积层4 P9 C, a$ r2 ]9 G! e
    $ U7 R6 l  O1 F( e6 c  y
    在图像处理中,我们有时需要将图像放⼤,即上采样(upsample)。上采样的⽅法有很多,常⽤的有双线性插值。简单来说,为了得到输出图像- N3 m* {4 e, s
    在坐标 (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函数构造的卷积核的转置卷积层来实现。
    4 X& F# C  w# a) K9 F$ @, y" y+ x5 y6 l& A0 V( T% U8 U' ~; s
    6 z  D) J" R2 w
    # 双线性插值的上采样,用来初始化转置卷积层的卷积核9 B1 \- h/ H1 x0 n7 K
    def bilinear_kernel(in_channels, out_channels, kernel_size):- l6 W% O/ G/ B2 s, Q
        factor = (kernel_size+1)//2
    % p! W: ?8 k1 J6 o2 \    if kernel_size%2 == 1:
    % S) M; g$ l( `. b        center = factor-1
    " ~+ n8 {7 h2 J    else:
    2 E/ s7 o; M7 f4 E, z, ~. q        center = factor-0.5& ?1 u1 e/ @- u, g' g
        og = np.ogrid[:kernel_size, :kernel_size]5 ~, I# ~$ d! `8 K2 K
        filt = (1-abs(og[0]-center)/factor) * (1-abs(og[1]-center)/factor)
    ! t2 d$ A8 ~! ^: G5 _0 H+ `    weight = np.zeros((in_channels,out_channels, kernel_size,kernel_size), dtype='float32')( z4 {- D5 U# I2 `
        weight[range(in_channels), range(out_channels), :, :] = filt
    % S3 Z9 X* ]% s7 P0 S$ s& i. ?- i$ D    weight = torch.Tensor(weight)3 H, H, b' b# t: E( M6 k
        weight.requires_grad = True. }) Z" k  m" {$ L$ g
        return weight8 F% r2 Z" O& b) b
    * g2 |# Y# M) ]9 r
    ) b8 _; g7 B% b( y( K# p
    在全卷积⽹络中,将转置卷积层初始化为双线性插值的上采样。对于1×1 1\times 11×1卷积层,采⽤Xavier XavierXavier随机初始化。  c& n, X) p+ s
    9 v* S8 x( f1 T8 Q6 S3 T$ X# {
    nn.init.xavier_normal_(model_ft[-2].weight.data, gain=1)# o+ |. G+ ]9 y
    model_ft[-1].weight.data = bilinear_kernel(num_classes, num_classes, 64).to(device)
    6 E0 g. U( C$ w1 f* p3 L- i5 J6 @, @  c# L, E% f4 |! q, u6 a" x, e
    4 d4 _* ^7 P" L; P+ j; D& c7 G- b

    # c+ R, m6 t2 J% M1 d8 T5 S5 训练模型

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

      V3 ]' ]4 b- N& b8 J) E, M# n
    def train_model(model:nn.Module, criterion, optimizer, scheduler, num_epochs=20):
    $ |4 ]! `/ l; u) ]& G( E    since = time.time()+ s' w) N9 ~$ h9 S% b# B
        best_model_wts = copy.deepcopy(model.state_dict())) v  e5 Z: a. X
        best_acc = 0.0, @* e! ~: n; Y# w2 D$ ?0 O3 J% s' z8 x
        # 每个epoch都有一个训练和验证阶段
    * V0 r2 Y/ [; o$ I! C5 F    for epoch in range(num_epochs):; V) ?3 k) F& j6 u' \
            print('Epoch {}/{}'.format(epoch, num_epochs-1))0 F6 x) X) x1 r) l
            print('-'*10)# H' W5 @. i+ p- I
            for phase in ['train', 'val']:
    5 i; Z# n. J( m, C5 F            if phase == 'train':
      B( c+ f$ |4 o$ H" g2 N- U% L                scheduler.step(), [/ O8 S9 Q- U! N% i' r
                    model.train()7 Q' y& \- q6 j: a
                else:
    + |( v! K4 g# ?. C. X                model.eval()+ I# n  P8 \( o/ D& C
                runing_loss = 0.0
    ; m- q/ l, f6 ]  _, [9 s            runing_corrects = 0.0
    * o3 l. M* u7 V- a. B" o, c            # 迭代一个epoch
    * j6 \" p! [7 Y' B            for inputs, labels in dataloaders[phase]:4 h% K5 ^2 Z3 _; f) o6 K
                    inputs, labels = inputs.to(device), labels.to(device)/ @& J; b4 y( w! |& H" c# `- t+ O6 s
                    optimizer.zero_grad() # 零参数梯度( H$ s, C( B+ j6 i
                                    # 前向,只在训练时跟踪参数
    ! [5 J+ {7 |! u                with torch.set_grad_enabled(phase=='train'):8 s+ M* w1 T/ z2 M+ I
                        logits = model(inputs)  # [5, 21, 320, 480]
    $ C; H4 l0 W5 u& b                    loss = criteon(logits, labels.long())
    / j; ^& W) N7 a. j# @/ Z                    # 后向,只在训练阶段进行优化
    ' p& J# ~4 H* n6 j' n( n                    if phase=='train':) }0 e! n# T. m5 X
                            loss.backward()
    8 I. _5 q. H3 I                        optimizer.step()
    ) c7 t8 ^: x! z, _9 D: P                                # 统计loss和correct
    6 ^$ a4 _3 T4 D                runing_loss += loss.item()*inputs.size(0)
    $ a' T( d) c6 K& \( T                runing_corrects += torch.sum((torch.argmax(logits.data,1))==labels.data)/(480*320). s4 _3 T9 j* D! z" ]. c

    1 a5 B9 h# A, I8 I$ ~5 _            epoch_loss = runing_loss / dataset_sizes[phase]
      ?6 Y  c* G& N0 @            epoch_acc = runing_corrects.double() / dataset_sizes[phase]5 }( s7 p3 i- l0 Z2 U" P4 c$ W# v
                print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))) D# K* y3 S2 G5 l( O; F. [+ K
                            # 深度复制model参数
    9 y! U% _3 j" u            if phase=='val' and epoch_acc>best_acc:
    8 ~5 v0 v3 p1 F. M' G                best_acc = epoch_acc
    / y9 w! W! ^4 k, b                best_model_wts = copy.deepcopy(model.state_dict())7 _( j8 C6 N6 j6 f0 G
            print()( `7 v* y) z6 D& }" Z3 y1 M3 z
        time_elapsed = time.time() - since;- u2 R0 _, Y9 l! q. L; j2 k
        print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed//60, time_elapsed%60)). C" {% L- |& s/ P3 Z0 N
        # 加载最佳模型权重
    , [3 Q' {. K: c0 v& y/ q7 J    model.load_state_dict(best_model_wts)
    # q) l. m- p4 H. {    return model, s. o0 b0 b; G) C

    + e3 Y' a+ p& o' p! b下面定义train_model要用到的参数,开始训练
    " P2 [6 r  V+ G  l: a' S6 O$ @& C: v2 D* W9 W) a/ j
    epochs = 5 # 训练5个epoch" y( H- h- e6 a+ `0 J5 L' y6 Q! u
    criteon = nn.CrossEntropyLoss()
    8 n6 d8 K* w4 goptimizer = optim.SGD(model_ft.parameters(), lr=0.001, weight_decay=1e-4, momentum=0.9)% r" C1 V( J; A( f9 _
    # 每3个epochs衰减LR通过设置gamma=0.1; \7 H& G; N: F* m( S
    exp_lr_scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)! H2 O" C' c& K. u

    + V8 z; c4 p5 k# 开始训练
    / ~0 `6 `- G# _$ j' x5 b; w8 D1 H! r+ \8 kmodel_ft = train_model(model_ft, criteon, optimizer, exp_lr_scheduler, num_epochs=epochs)& P& f7 F- f7 O" g( ^
    $ R8 h! N/ l9 _& p$ A$ G
    6 测试模型

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

    def label2image(pred):
    + e0 R- C: n- d5 o( ]5 [8 Z, d& B2 F    # pred: [320,480]" L- O( N' z, |1 O: s
        colormap = torch.tensor(VOC_COLORMAP,device=device,dtype=int)
    7 |" y% A3 Q, P& D8 @    x = pred.long()$ A" o' L; X8 K: E0 \6 q
        return (colormap[x,:]).data.cpu().numpy()2 i' ?  E+ N+ l) [

    0 Z0 J, \/ y' B$ s

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

    6.1 通用型

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

    mean=torch.tensor([0.485, 0.456, 0.406]).reshape(3,1,1).to(device)& ~1 m  y( _7 Q+ C  T1 Z! j( a$ [4 t
    std=torch.tensor([0.229, 0.224, 0.225]).reshape(3,1,1).to(device)
    ; V; v3 [; }7 w) O4 Q$ E0 Jdef visualize_model(model:nn.Module, num_images=4):
    * X/ G" l2 o5 e9 I  y( v* g    was_training = model.training+ r  W% P6 _# j- `( o" Q! X& J, t
        model.eval()! K  j! q( \. H/ l
        images_so_far = 08 ]' X- @" I: Z8 j
        n, imgs = num_images, []6 a! t) p6 [6 I7 x- s: p) `% n
        with torch.no_grad():$ F0 h* c2 m! Q5 m9 A$ F" _
            for i, (inputs, labels) in enumerate(dataloaders['val']):+ C1 D! a2 ]" E. l" s
                inputs, labels = inputs.to(device), labels.to(device) # [b,3,320,480]$ ~; q' d' u5 |& x% ^0 Y
                outputs = model(inputs). j" M' B% E% h- C8 P& Q! ~% ^$ n
                pred = torch.argmax(outputs, dim=1) # [b,320,480]
    4 D* b* a+ a* U) a; V            inputs_nd = (inputs*std+mean).permute(0,2,3,1)*255 # 记得要变回去哦
    . ?& E0 f4 t( _1 ]9 z& s1 u+ j8 y2 R/ q& {
                for j in range(num_images):* t1 Q# f% b) l$ n1 o; A( Y$ A8 K$ L
                    images_so_far += 1
    2 z6 _) [/ a' R                pred1 = label2image(pred[j]) # numpy.ndarray (320, 480, 3)6 v) g: o' m( t3 k& y
                    imgs += [inputs_nd[j].data.int().cpu().numpy(), pred1, label2image(labels[j])]
    # J7 Z7 G0 X) [) z                if images_so_far == num_images:3 a: E. {3 J: [
                        model.train(mode=was_training)6 v6 X8 P6 m3 L6 R2 Y
                        # 我已经固定了每次只显示4张图了,大家可以自己修改
    # g1 ?% k% h& e& M                    show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n)
    % w7 E: H$ ?2 W  i1 `; _                    return model.train(mode=was_training)* I0 p! f  G0 J4 N
    . l6 }( `) v; M
    # 开始验证
    $ @: o7 T2 y( N3 @! hvisualize_model(model_ft)7 [7 d" {! L# A$ }1 }4 }1 S  h
    6.2 不通用

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

    # 预测前将图像标准化,并转换成(b,c,h,w)的tensor
    & h+ J2 L/ S6 B" c* ~1 ~- P: [def predict(img, model):1 @6 V8 @/ F. i, P; ^
        tsf = transforms.Compose([
    ! `0 x. E. z. r1 t8 Y# y3 T            transforms.ToTensor(), # 好像会自动转换channel( _# n7 G) L0 v0 k+ A
                transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])])
    6 i0 {- Y' H) _9 T    x = tsf(img).unsqueeze(0).to(device) # (3,320,480) -> (1,3,320,480)( [" O) I8 I5 S0 ~/ [
        pred = torch.argmax(model(x), dim=1) # 每个通道选择概率最大的那个像素点 -> (1,320,480)* v% u% s# m4 Z2 i: f! _  d
        return pred.reshape(pred.shape[1],pred.shape[2]) # reshape成(320,480): W! [4 D6 g8 }! `

    1 h! o6 K9 ?( Gdef evaluate(model:nn.Module):, f% e) E; a" S' e
        model.eval()
    # [' d( |, X( r! e' D6 q* ?    test_images, test_labels = read_voc_images(voc_dir, is_train=False, max_num=10)
    1 c- c0 h7 A" d- s    n, imgs = 4, []
    / ?5 t7 P) F" W% B* Q    for i in range(n):
    ) f4 G1 l% P/ g' k) y, X4 B; v        xi, yi = voc_rand_crop(test_images, test_labels, 320, 480) # Image; M  l+ ?  d# c9 o* l
            pred = label2image(predict(xi, model))+ f8 x# h0 i& d7 a4 V
            imgs += [xi, pred, yi]
    , |2 e9 K; ]$ i+ Y( I5 J    show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n)
    9 G; s, ]# ?5 }: l6 E$ K4 V! S6 d! A
    # 开始测试- \* e) x6 N' _6 p
    evaluate(model_ft)
    " z; f) a# B4 |" B) K! ?- `; K- R) }& D  U+ ]% J+ [
    7 结语

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

    7 a/ ^  z( U8 i; t( X# u
    Epoch 0/2* w# E& i: Q( k8 e  a5 b2 k4 Q
    ----------
    , |1 X7 i. M+ G& t  v% V" a$ Dtrain Loss: 1.7844 Acc: 0.5835& y1 @/ \5 k7 u/ w
    val Loss: 1.1669 Acc: 0.6456. Y: ^' {$ V& x
    - Q* f/ \; W* V
    Epoch 1/2
    - s6 n9 S0 D" F. i; N) A* H* p----------/ k, \2 j9 j9 Y# `2 H5 |, g5 X- ~
    train Loss: 1.1288 Acc: 0.65352 m% |' I. g' ~* K' m
    val Loss: 0.9012 Acc: 0.6929( f' k7 [& s3 s& P* k& N

    # X* w  u' H8 o; iEpoch 2/2: C) y7 g4 a( f
    ----------0 Y/ v5 n7 v) I* l: U% f3 O
    train Loss: 0.9578 Acc: 0.6706+ b0 U+ ?6 `6 _2 _  E
    val Loss: 0.8088 Acc: 0.6948" r* D" ?/ y0 p! r6 n' T* E
    6 U+ B( L) o/ \$ d# r( [3 f; M1 U
    Training complete in 6m 37s, s7 h$ w% m8 R6 U/ S0 g

    ; B3 X. I6 J: D/ P5 H% _. Q/ b& Z7 @0 o
    2.jpg , q7 |8 @0 o# m4 Z

    : ], E+ c& k( S" F+ v当 epochs = 5 时,训练集的精度在 89 8989% 左右,测试集的精度可以达到 86​ 86​86​ %。
    ; C* a; u9 {6 V( E0 u! m
      W; ^! t% L7 n4 L% b. _( L$ g对于这个模型用 ResNet-50 作特征提取器会有更好的效果,不过训练的时间也会更长。还有超参数lr, weight_decay, momentum, step_size, gamma 以及1×1 1×11×1卷积层和转置卷积层的初始化方式也可以继续调。
    ' R! d; j  A1 X# i/ Y
    " h$ o; ~, N, t: K. z4 B/ R  F! v/ U# e4 V
    语义分割还有很多可用的模型,本文用的是 FCN,在其它一些模型上会有更好的表现:
      D+ P! }( i! S( o; O* e( X6 d+ Y4 {! W9 S: C) T. a
    Deeplab V3+ 具有可分离卷积的编码器/解码器,用于语义图像分割[论文]
    2 }5 R4 N' A# t# a% j" L) YGCN 通过全局卷积网络改进语义分割[论文]0 i6 w( B) F9 }  A7 n/ p0 w
    UperNet 统一感知解析" X& Y9 t9 y. [' S+ z, b: {
    ENet 用于实时语义分割的深度神经网络体系结构[论文]
    7 j4 B2 v% n1 _+ i7 HU-Net 用于生物医学图像分割的卷积网络
    0 s$ Y/ y" n1 x8 K/ p1 B3 wSegNet 用于图像分段的深度卷积编码器-解码器架构。
    6 ?! c9 Z0 R) H' r: w- k还有(DUC,HDC)、PSPNet等。: w' c" c, E0 j3 P, ?. A

    % t' A; L7 M1 Z( C6 c+ I常用的语义分割数据集也有很多:Pascal VOC、CityScapes、ADE20K、COCO Stuff等。; U3 n* ~2 J* H9 O2 r4 G
    " N7 E5 D: G( w( ]: A7 a- J
    对于损失函数,除了交叉熵误差,也可以用这些:9 ~1 G, P2 a+ Z) C
      X' @2 X- n3 D* u
    Dice-Loss 可以测试两个样本之间的重叠度量,可以更好地反映训练目标,但该损失函数具有很强的非凸性,很难优化。+ ^0 L) X1 W$ X% W5 T  p
    CE Dice loss Dice 损失与 CE 的总和,CE 提供了平滑的优化,而 Dice 损失则很好地表明了分割结果的质量。9 n+ ~& N: m3 E4 X! e( I
    Focal Loss CE 的另一种版本,用于避免类别不平衡而降低了置信度的情况。! U; ]# K* P/ x( R9 {$ D
    Lovasz Softmax 查看论文:Lovasz - softmax损失。
    - ?& E* r' ^9 n
    % K; u$ k. h% k
    - _/ Q& b2 f5 b4 ^& x3 D% A# S0 J/ [' l0 b. u
    : _) g& q. z. e( @. m) q
    1 l# |3 O' V' o: }1 `
    ————————————————7 @! V/ S! t& r5 W
    版权声明:本文为CSDN博主「小红不吃糖」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。: N( J7 |1 g, O3 _
    原文链接:https://blog.csdn.net/qq_43280818/article/details/1059165070 N1 [; {' M: D7 c3 i. s
    % X- z6 n1 o) s3 ~$ N4 \
    / |9 U% _" ^7 Z# ?7 p" G
    5 F! `9 l1 A* t
    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-9-2 03:58 , Processed in 0.427041 second(s), 53 queries .

    回顶部