CAFFE -FCN训练配置过程

转载自 http://blog.csdn.net/jiongnima/article/details/78549326?locationNum=3&fps=1

   在2015年发表于计算机视觉顶会CVPR上的Fully Convolutional Networks for Semantic Segmentation 论文(下文中简称FCN)开创了图像语义分割的新流派。在后来的科研工作者发表学术论文做实验的时候,还常常把自己的实验结果与FCN相比较。笔者在做实验的时候,也去改动并跑了跑FCN的代码,可是问题出现了,笔者的训练并不收敛

   下面是笔者最初的训练prototxt文件:

[python] view plain copy
  1. name: "fcn8snet"  
  2. layer {  
  3.   name: "data"  
  4.   type: "ImageData"  
  5.   top: "data"  
  6.   top: "fake-dlabel"  
  7.   include {  
  8.     phase: TRAIN  
  9.   }  
  10.   transform_param {  
  11.     mean_file: "fcn8s_cityscapes/fcn_mean.binaryproto"  
  12.     #scale: 0.00390625  
  13.   }  
  14.   image_data_param {  
  15.     source: "fcn8s_cityscapes/data/train_img.txt"  
  16.     batch_size: 1  
  17.     root_folder: "fcn8s_cityscapes/data/train/image/"  
  18.   }  
  19. }  
  20. layer {  
  21.   name: "label"  
  22.   type: "ImageData"  
  23.   top: "label"  
  24.   top: "fake_llabel"  
  25.   include {  
  26.     phase: TRAIN  
  27.   }  
  28.   image_data_param {  
  29.     source: "fcn8s_cityscapes/data/train_label.txt"  
  30.     batch_size: 1  
  31.     root_folder: "fcn8s_cityscapes/data/train/label/"  
  32.     is_color: false  
  33.   }  
  34. }  
  35. layer {  
  36.   name: "conv1_1"  
  37.   type: "Convolution"  
  38.   bottom: "data"  
  39.   top: "conv1_1"  
  40.   param {  
  41.     lr_mult: 1  
  42.     decay_mult: 1  
  43.   }  
  44.   param {  
  45.     lr_mult: 2  
  46.     decay_mult: 0  
  47.   }  
  48.   convolution_param {  
  49.     num_output: 64  
  50.     pad: 100  
  51.     kernel_size: 3  
  52.     stride: 1  
  53.   }  
  54. }  
  55. layer {  
  56.   name: "relu1_1"  
  57.   type: "ReLU"  
  58.   bottom: "conv1_1"  
  59.   top: "conv1_1"  
  60. }  
  61. layer {  
  62.   name: "conv1_2"  
  63.   type: "Convolution"  
  64.   bottom: "conv1_1"  
  65.   top: "conv1_2"  
  66.   param {  
  67.     lr_mult: 1  
  68.     decay_mult: 1  
  69.   }  
  70.   param {  
  71.     lr_mult: 2  
  72.     decay_mult: 0  
  73.   }  
  74.   convolution_param {  
  75.     num_output: 64  
  76.     pad: 1  
  77.     kernel_size: 3  
  78.     stride: 1  
  79.   }  
  80. }  
  81. layer {  
  82.   name: "relu1_2"  
  83.   type: "ReLU"  
  84.   bottom: "conv1_2"  
  85.   top: "conv1_2"  
  86. }  
  87. layer {  
  88.   name: "pool1"  
  89.   type: "Pooling"  
  90.   bottom: "conv1_2"  
  91.   top: "pool1"  
  92.   pooling_param {  
  93.     pool: MAX  
  94.     kernel_size: 2  
  95.     stride: 2  
  96.   }  
  97. }  
  98. layer {  
  99.   name: "conv2_1"  
  100.   type: "Convolution"  
  101.   bottom: "pool1"  
  102.   top: "conv2_1"  
  103.   param {  
  104.     lr_mult: 1  
  105.     decay_mult: 1  
  106.   }  
  107.   param {  
  108.     lr_mult: 2  
  109.     decay_mult: 0  
  110.   }  
  111.   convolution_param {  
  112.     num_output: 128  
  113.     pad: 1  
  114.     kernel_size: 3  
  115.     stride: 1  
  116.   }  
  117. }  
  118. layer {  
  119.   name: "relu2_1"  
  120.   type: "ReLU"  
  121.   bottom: "conv2_1"  
  122.   top: "conv2_1"  
  123. }  
  124. layer {  
  125.   name: "conv2_2"  
  126.   type: "Convolution"  
  127.   bottom: "conv2_1"  
  128.   top: "conv2_2"  
  129.   param {  
  130.     lr_mult: 1  
  131.     decay_mult: 1  
  132.   }  
  133.   param {  
  134.     lr_mult: 2  
  135.     decay_mult: 0  
  136.   }  
  137.   convolution_param {  
  138.     num_output: 128  
  139.     pad: 1  
  140.     kernel_size: 3  
  141.     stride: 1  
  142.   }  
  143. }  
  144. layer {  
  145.   name: "relu2_2"  
  146.   type: "ReLU"  
  147.   bottom: "conv2_2"  
  148.   top: "conv2_2"  
  149. }  
  150. layer {  
  151.   name: "pool2"  
  152.   type: "Pooling"  
  153.   bottom: "conv2_2"  
  154.   top: "pool2"  
  155.   pooling_param {  
  156.     pool: MAX  
  157.     kernel_size: 2  
  158.     stride: 2  
  159.   }  
  160. }  
  161. layer {  
  162.   name: "conv3_1"  
  163.   type: "Convolution"  
  164.   bottom: "pool2"  
  165.   top: "conv3_1"  
  166.   param {  
  167.     lr_mult: 1  
  168.     decay_mult: 1  
  169.   }  
  170.   param {  
  171.     lr_mult: 2  
  172.     decay_mult: 0  
  173.   }  
  174.   convolution_param {  
  175.     num_output: 256  
  176.     pad: 1  
  177.     kernel_size: 3  
  178.     stride: 1  
  179.   }  
  180. }  
  181. layer {  
  182.   name: "relu3_1"  
  183.   type: "ReLU"  
  184.   bottom: "conv3_1"  
  185.   top: "conv3_1"  
  186. }  
  187. layer {  
  188.   name: "conv3_2"  
  189.   type: "Convolution"  
  190.   bottom: "conv3_1"  
  191.   top: "conv3_2"  
  192.   param {  
  193.     lr_mult: 1  
  194.     decay_mult: 1  
  195.   }  
  196.   param {  
  197.     lr_mult: 2  
  198.     decay_mult: 0  
  199.   }  
  200.   convolution_param {  
  201.     num_output: 256  
  202.     pad: 1  
  203.     kernel_size: 3  
  204.     stride: 1  
  205.   }  
  206. }  
  207. layer {  
  208.   name: "relu3_2"  
  209.   type: "ReLU"  
  210.   bottom: "conv3_2"  
  211.   top: "conv3_2"  
  212. }  
  213. layer {  
  214.   name: "conv3_3"  
  215.   type: "Convolution"  
  216.   bottom: "conv3_2"  
  217.   top: "conv3_3"  
  218.   param {  
  219.     lr_mult: 1  
  220.     decay_mult: 1  
  221.   }  
  222.   param {  
  223.     lr_mult: 2  
  224.     decay_mult: 0  
  225.   }  
  226.   convolution_param {  
  227.     num_output: 256  
  228.     pad: 1  
  229.     kernel_size: 3  
  230.     stride: 1  
  231.   }  
  232. }  
  233. layer {  
  234.   name: "relu3_3"  
  235.   type: "ReLU"  
  236.   bottom: "conv3_3"  
  237.   top: "conv3_3"  
  238. }  
  239. layer {  
  240.   name: "pool3"  
  241.   type: "Pooling"  
  242.   bottom: "conv3_3"  
  243.   top: "pool3"  
  244.   pooling_param {  
  245.     pool: MAX  
  246.     kernel_size: 2  
  247.     stride: 2  
  248.   }  
  249. }  
  250. layer {  
  251.   name: "conv4_1"  
  252.   type: "Convolution"  
  253.   bottom: "pool3"  
  254.   top: "conv4_1"  
  255.   param {  
  256.     lr_mult: 1  
  257.     decay_mult: 1  
  258.   }  
  259.   param {  
  260.     lr_mult: 2  
  261.     decay_mult: 0  
  262.   }  
  263.   convolution_param {  
  264.     num_output: 512  
  265.     pad: 1  
  266.     kernel_size: 3  
  267.     stride: 1  
  268.   }  
  269. }  
  270. layer {  
  271.   name: "relu4_1"  
  272.   type: "ReLU"  
  273.   bottom: "conv4_1"  
  274.   top: "conv4_1"  
  275. }  
  276. layer {  
  277.   name: "conv4_2"  
  278.   type: "Convolution"  
  279.   bottom: "conv4_1"  
  280.   top: "conv4_2"  
  281.   param {  
  282.     lr_mult: 1  
  283.     decay_mult: 1  
  284.   }  
  285.   param {  
  286.     lr_mult: 2  
  287.     decay_mult: 0  
  288.   }  
  289.   convolution_param {  
  290.     num_output: 512  
  291.     pad: 1  
  292.     kernel_size: 3  
  293.     stride: 1  
  294.   }  
  295. }  
  296. layer {  
  297.   name: "relu4_2"  
  298.   type: "ReLU"  
  299.   bottom: "conv4_2"  
  300.   top: "conv4_2"  
  301. }  
  302. layer {  
  303.   name: "conv4_3"  
  304.   type: "Convolution"  
  305.   bottom: "conv4_2"  
  306.   top: "conv4_3"  
  307.   param {  
  308.     lr_mult: 1  
  309.     decay_mult: 1  
  310.   }  
  311.   param {  
  312.     lr_mult: 2  
  313.     decay_mult: 0  
  314.   }  
  315.   convolution_param {  
  316.     num_output: 512  
  317.     pad: 1  
  318.     kernel_size: 3  
  319.     stride: 1  
  320.   }  
  321. }  
  322. layer {  
  323.   name: "relu4_3"  
  324.   type: "ReLU"  
  325.   bottom: "conv4_3"  
  326.   top: "conv4_3"  
  327. }  
  328. layer {  
  329.   name: "pool4"  
  330.   type: "Pooling"  
  331.   bottom: "conv4_3"  
  332.   top: "pool4"  
  333.   pooling_param {  
  334.     pool: MAX  
  335.     kernel_size: 2  
  336.     stride: 2  
  337.   }  
  338. }  
  339. layer {  
  340.   name: "conv5_1"  
  341.   type: "Convolution"  
  342.   bottom: "pool4"  
  343.   top: "conv5_1"  
  344.   param {  
  345.     lr_mult: 1  
  346.     decay_mult: 1  
  347.   }  
  348.   param {  
  349.     lr_mult: 2  
  350.     decay_mult: 0  
  351.   }  
  352.   convolution_param {  
  353.     num_output: 512  
  354.     pad: 1  
  355.     kernel_size: 3  
  356.     stride: 1  
  357.   }  
  358. }  
  359. layer {  
  360.   name: "relu5_1"  
  361.   type: "ReLU"  
  362.   bottom: "conv5_1"  
  363.   top: "conv5_1"  
  364. }  
  365. layer {  
  366.   name: "conv5_2"  
  367.   type: "Convolution"  
  368.   bottom: "conv5_1"  
  369.   top: "conv5_2"  
  370.   param {  
  371.     lr_mult: 1  
  372.     decay_mult: 1  
  373.   }  
  374.   param {  
  375.     lr_mult: 2  
  376.     decay_mult: 0  
  377.   }  
  378.   convolution_param {  
  379.     num_output: 512  
  380.     pad: 1  
  381.     kernel_size: 3  
  382.     stride: 1  
  383.   }  
  384. }  
  385. layer {  
  386.   name: "relu5_2"  
  387.   type: "ReLU"  
  388.   bottom: "conv5_2"  
  389.   top: "conv5_2"  
  390. }  
  391. layer {  
  392.   name: "conv5_3"  
  393.   type: "Convolution"  
  394.   bottom: "conv5_2"  
  395.   top: "conv5_3"  
  396.   param {  
  397.     lr_mult: 1  
  398.     decay_mult: 1  
  399.   }  
  400.   param {  
  401.     lr_mult: 2  
  402.     decay_mult: 0  
  403.   }  
  404.   convolution_param {  
  405.     num_output: 512  
  406.     pad: 1  
  407.     kernel_size: 3  
  408.     stride: 1  
  409.   }  
  410. }  
  411. layer {  
  412.   name: "relu5_3"  
  413.   type: "ReLU"  
  414.   bottom: "conv5_3"  
  415.   top: "conv5_3"  
  416. }  
  417. layer {  
  418.   name: "pool5"  
  419.   type: "Pooling"  
  420.   bottom: "conv5_3"  
  421.   top: "pool5"  
  422.   pooling_param {  
  423.     pool: MAX  
  424.     kernel_size: 2  
  425.     stride: 2  
  426.   }  
  427. }  
  428. layer {  
  429.   name: "fc6"  
  430.   type: "Convolution"  
  431.   bottom: "pool5"  
  432.   top: "fc6"  
  433.   param {  
  434.     lr_mult: 1  
  435.     decay_mult: 1  
  436.   }  
  437.   param {  
  438.     lr_mult: 2  
  439.     decay_mult: 0  
  440.   }  
  441.   convolution_param {  
  442.     num_output: 4096  
  443.     pad: 0  
  444.     kernel_size: 7  
  445.     stride: 1  
  446.   }  
  447. }  
  448. layer {  
  449.   name: "relu6"  
  450.   type: "ReLU"  
  451.   bottom: "fc6"  
  452.   top: "fc6"  
  453. }  
  454. layer {  
  455.   name: "drop6"  
  456.   type: "Dropout"  
  457.   bottom: "fc6"  
  458.   top: "fc6"  
  459.   dropout_param {  
  460.     dropout_ratio: 0.5  
  461.   }  
  462. }  
  463. layer {  
  464.   name: "fc7"  
  465.   type: "Convolution"  
  466.   bottom: "fc6"  
  467.   top: "fc7"  
  468.   param {  
  469.     lr_mult: 1  
  470.     decay_mult: 1  
  471.   }  
  472.   param {  
  473.     lr_mult: 2  
  474.     decay_mult: 0  
  475.   }  
  476.   convolution_param {  
  477.     num_output: 4096  
  478.     pad: 0  
  479.     kernel_size: 1  
  480.     stride: 1  
  481.   }  
  482. }  
  483. layer {  
  484.   name: "relu7"  
  485.   type: "ReLU"  
  486.   bottom: "fc7"  
  487.   top: "fc7"  
  488. }  
  489. layer {  
  490.   name: "drop7"  
  491.   type: "Dropout"  
  492.   bottom: "fc7"  
  493.   top: "fc7"  
  494.   dropout_param {  
  495.     dropout_ratio: 0.5  
  496.   }  
  497. }  
  498. layer {  
  499.   name: "score_fr_cityscapes"  
  500.   type: "Convolution"  
  501.   bottom: "fc7"  
  502.   top: "score_fr"  
  503.   param {  
  504.     lr_mult: 1  
  505.     decay_mult: 1  
  506.   }  
  507.   param {  
  508.     lr_mult: 2  
  509.     decay_mult: 0  
  510.   }  
  511.   convolution_param {  
  512.     num_output: 2  
  513.     pad: 0  
  514.     kernel_size: 1  
  515.     weight_filler { type: "gaussian" std: 0.01 }  
  516.     bias_filler { type: "constant" value: 0 }  
  517.   }  
  518. }  
  519. layer {  
  520.   name: "upscore2_cityscapes"  
  521.   type: "Deconvolution"  
  522.   bottom: "score_fr"  
  523.   top: "upscore2"  
  524.   param {  
  525.     lr_mult: 0  
  526.   }  
  527.   convolution_param {  
  528.     num_output: 2  
  529.     bias_term: false  
  530.     kernel_size: 4  
  531.     stride: 2  
  532.     weight_filler { type: "gaussian" std: 0.01 }  
  533.     bias_filler { type: "constant" value: 0 }  
  534.   }  
  535. }  
  536. layer {  
  537.   name: "score_pool4_cityscapes"  
  538.   type: "Convolution"  
  539.   bottom: "pool4"  
  540.   top: "score_pool4"  
  541.   param {  
  542.     lr_mult: 1  
  543.     decay_mult: 1  
  544.   }  
  545.   param {  
  546.     lr_mult: 2  
  547.     decay_mult: 0  
  548.   }  
  549.   convolution_param {  
  550.     num_output: 2  
  551.     pad: 0  
  552.     kernel_size: 1  
  553.     weight_filler { type: "gaussian" std: 0.01 }  
  554.     bias_filler { type: "constant" value: 0 }  
  555.   }  
  556. }  
  557. layer {  
  558.   name: "score_pool4c"  
  559.   type: "Crop"  
  560.   bottom: "score_pool4"  
  561.   bottom: "upscore2"  
  562.   top: "score_pool4c"  
  563.   crop_param {  
  564.     axis: 2  
  565.     offset: 5  
  566.   }  
  567. }  
  568. layer {  
  569.   name: "fuse_pool4"  
  570.   type: "Eltwise"  
  571.   bottom: "upscore2"  
  572.   bottom: "score_pool4c"  
  573.   top: "fuse_pool4"  
  574.   eltwise_param {  
  575.     operation: SUM  
  576.   }  
  577. }  
  578. layer {  
  579.   name: "upscore_pool4_cityscapes"  
  580.   type: "Deconvolution"  
  581.   bottom: "fuse_pool4"  
  582.   top: "upscore_pool4"  
  583.   param {  
  584.     lr_mult: 0  
  585.   }  
  586.   convolution_param {  
  587.     num_output: 2  
  588.     bias_term: false  
  589.     kernel_size: 4  
  590.     stride: 2  
  591.     weight_filler { type: "gaussian" std: 0.01 }  
  592.     bias_filler { type: "constant" value: 0 }  
  593.   }  
  594. }  
  595. layer {  
  596.   name: "score_pool3_cityscapes"  
  597.   type: "Convolution"  
  598.   bottom: "pool3"  
  599.   top: "score_pool3"  
  600.   param {  
  601.     lr_mult: 1  
  602.     decay_mult: 1  
  603.   }  
  604.   param {  
  605.     lr_mult: 2  
  606.     decay_mult: 0  
  607.   }  
  608.   convolution_param {  
  609.     num_output: 2  
  610.     pad: 0  
  611.     kernel_size: 1  
  612.     weight_filler { type: "gaussian" std: 0.01 }  
  613.     bias_filler { type: "constant" value: 0 }  
  614.   }  
  615. }  
  616. layer {  
  617.   name: "score_pool3c"  
  618.   type: "Crop"  
  619.   bottom: "score_pool3"  
  620.   bottom: "upscore_pool4"  
  621.   top: "score_pool3c"  
  622.   crop_param {  
  623.     axis: 2  
  624.     offset: 9  
  625.   }  
  626. }  
  627. layer {  
  628.   name: "fuse_pool3"  
  629.   type: "Eltwise"  
  630.   bottom: "upscore_pool4"  
  631.   bottom: "score_pool3c"  
  632.   top: "fuse_pool3"  
  633.   eltwise_param {  
  634.     operation: SUM  
  635.   }  
  636. }  
  637. layer {  
  638.   name: "upscore8_cityscapes"  
  639.   type: "Deconvolution"  
  640.   bottom: "fuse_pool3"  
  641.   top: "upscore8"  
  642.   param {  
  643.     lr_mult: 0  
  644.   }  
  645.   convolution_param {  
  646.     num_output: 2  
  647.     bias_term: false  
  648.     kernel_size: 16  
  649.     stride: 8  
  650.     weight_filler { type: "gaussian" std: 0.01 }  
  651.     bias_filler { type: "constant" value: 0 }  
  652.   }  
  653. }  
  654. layer {  
  655.   name: "score"  
  656.   type: "Crop"  
  657.   bottom: "upscore8"  
  658.   bottom: "data"  
  659.   top: "score"  
  660.   crop_param {  
  661.     axis: 2  
  662.     offset: 31  
  663.   }  
  664. }  
  665. layer {  
  666.   name: "loss"  
  667.   type: "SoftmaxWithLoss"  
  668.   bottom: "score"  
  669.   bottom: "label"  
  670.   top: "loss"  
  671.   loss_param {  
  672.     ignore_label: 255  
  673.     #normalize: false  
  674.   }  
  675. }  

   细心的读者朋友们可以发现,笔者换掉了数据层(换成了自己生成的lmdb文件),并且输出对应的图片以及标签,然后,由于笔者是二分类,因此改掉了原有的带有num_output: 21的卷积层与反卷积层的名字。这样在fine-tune参数模型的时候不会出错(笔者跑的是经过处理的cityscapes数据集)。然后在启动训练的时候,笔者运行了以下的脚本:

[python] view plain copy
  1. #!/usr/bin/env sh  
  2. set -e  
  3.   
  4. echo "begin:"  
  5. GLOG_logtostderr=0 GLOG_log_dir=./fcn8s_cityscapes/logs/ \  
  6. ./build/tools/caffe train \  
  7. --solver="fcn8s_cityscapes/solver.prototxt" \  
  8. --weights="fcn8s_cityscapes/fcn8s-heavy-pascal.caffemodel" \  
  9. --gpu=1  
  10. echo "end"  

   可是,训练过程的输出却由下图所示:


   笔者训练了10w次,像素二分类loss一直等于0.693147,很明显不正常。那么,到底是哪里出了问题呢?笔者不由得陷入了冷静分析:

   笔者训练使用的prototxt文件(如上所示)是按照官方提供的修改的,对结果有绝对影响的一共有两个地方:

(1) 按照我自己的输出分类数改了6个卷积与反卷积层的名字,这应该是正确的操作,因为不这样做会出错。

(2) 改掉了网络的数据层,当时为了方便,笔者甚至都没有去编译pycaffe,然后就直接使用了预先生成的lmdb文件 并使用data_layer进行数据读取。笔者严重怀疑猫腻出在此处。

   于是,笔者进入了fcn代码中自带的solve.py进行查看:

[python] view plain copy
  1. # surgeries  
  2. interp_layers = [k for k in solver.net.params.keys() if 'up' in k]  
  3. surgery.interp(solver.net, interp_layers)  
   在上面的代码中,原作者对层名称中有"up"字样的层做了操作,这恰好是训练文件中的反卷积层。因此笔者进入源码中的surgery.py文件看看这个操作到底是什么:

[python] view plain copy
  1. def interp(net, layers):  
  2.     """ 
  3.     Set weights of each layer in layers to bilinear kernels for interpolation. 
  4.     """  
  5.     for l in layers:  
  6.         m, k, h, w = net.params[l][0].data.shape  
  7.         if m != k and k != 1:  
  8.             print 'input + output channels need to be the same or |output| == 1'  
  9.             raise  
  10.         if h != w:  
  11.             print 'filters need to be square'  
  12.             raise  
  13.         filt = upsample_filt(h)  
  14.         net.params[l][0].data[range(m), range(k), :, :] = filt  
  15.   
  16.   
  17. def upsample_filt(size):  
  18.     """ 
  19.     Make a 2D bilinear kernel suitable for upsampling of the given (h, w) size. 
  20.     """  
  21.     factor = (size + 1) // 2  
  22.     if size % 2 == 1:  
  23.         center = factor - 1  
  24.     else:  
  25.         center = factor - 0.5  
  26.     og = np.ogrid[:size, :size]  
  27.     return (1 - abs(og[0] - center) / factor) * \  
  28.            (1 - abs(og[1] - center) / factor)  

   看到这里笔者才恍然大悟,原来,官方自带的solve.py文件中的interp函数中的upsample_filt函数已经对反卷积层参数进行了双线性插值初始化,而在最上面笔者最初使用的.prototxt文件中,笔者只是自己在反卷积层中对参数做了高斯初始化,这是不对的。


   到这里,FCN训练不收敛的原因已经完全暴露:在于没有对反卷积层参数做正确的初始化操作。解决方案是,按照官方提供的代码配置程序并使用solve.py运行程序。

   那么,该怎么进行FCN训练程序的配置呢?笔者下面以fcn8s配置为例来讲解一下:

   在讲解之前先说一句,在配置FCN源码的过程中,博主Darlewo的FCN训练自己的数据集及测试这篇博客对我很有启发,在此对他表示最诚挚的感谢。但是这篇博客笔者认为不是太详细与系统,因此笔者将在本篇博客中详解,下面开始干货。

(0) 配置好caffe并编译完成pycaffe

第0步相信绝大多数读者都已经完成,笔者不再赘述。


(1) 下载FCN源代码并解压。

https://github.com/shelhamer/fcn.berkeleyvision.org


(2) 进入上一步解压后的fcn.berkeleyvision.org-master文件夹,再进入voc-fcn8s文件夹,查看里面的caffemodel-url文件中的链接,并下载训练时需要fine-tune的模型fcn8s-heavy-pascal.caffemodel。

在这里,如果有读者朋友对以上的链接下载不方便的话,也可以下载笔者的百度网盘中的文件,包含源码与fine-tune所需模型。链接:http://pan.baidu.com/s/1dEWT8FR


(3) 针对训练所需的训练集与测试集,准备图像与标签。并且写训练与测试文件txt。

在这一步,笔者想要说的是,大家要准备四样东西:

   训练图像与标签

   测试图像与标签

   训练txt文件,姑且称为train.txt

   测试txt文件,姑且称为test.txt

   准备的时候要注意以下几点:

   第一,训练图片与训练标签的名称要保持一致格式没有必要保持一致。比如,笔者的训练/测试图像就是train(num).jpg/test(num).jpg,训练的标签就是相应的train(num).png/test(num).png。

   笔者的训练图像:


   笔者训练图像对应的标签:


    第二,对于train.txt文件与test.txt文件,里面只需要记录训练/测试的图像的名称,不要记录后缀格式。这就是为什么图像与标签要在名称上保持一致,因为voc_layers.py文件会根据从txt文件中阅读的文件名去同时读取训练/测试图片与标签。另外,训练/测试图像名顺序不重要。

   下面是笔者的train.txt:


顺便分享给大家一个写train.txt和test.txt的脚本文件:

[python] view plain copy
  1. import numpy as np  
  2. import glob  
  3. import os  
  4. import random  
  5.   
  6. def main():  
  7.     train_dir = "./train/image/"  
  8.     test_dir = "./test/image/"  
  9.     train_path = "./train.txt"  
  10.     test_path = "./test.txt"  
  11.     train_images = glob.glob(os.path.join(train_dir, "*.jpg"))  
  12.     test_images = glob.glob(os.path.join(test_dir, "*.jpg"))  
  13.     train_file = open(train_path, 'a')  
  14.     test_file = open(test_path, 'a')  
  15.     print(len(train_images))  
  16.     print(len(test_images))  
  17.     for idx in range(len(train_images)):  
  18.         train_name, _ = os.path.splitext(os.path.basename(train_images[idx]))  
  19.         train_content = train_name + "\n"  
  20.         train_file.write(train_content)  
  21.     train_file.close()  
  22.     for idx in range(len(test_images)):  
  23.         test_name, _ = os.path.splitext(os.path.basename(test_images[idx]))  
  24.         test_content = test_name + "\n"  
  25.         test_file.write(test_content)  
  26.     test_file.close()  
  27.   
  28. if __name__ == '__main__':  
  29.     main()  

   在data文件夹下新建好train.txt和test.txt空白文件后直接新建并运行上述python脚本即可。

   第三,对于train.txt文件和test.txt文件,训练/测试的图像与标签,统一放在一个文件夹下面,而在该文件夹下的路径可以随意配置。只是需要按需修改一下voc_layers.py文件。比如说,笔者的数据文件就是如下所示安排的。

[python] view plain copy
  1. |-voc-fcn8s  
  2.   |-data  
  3.     test.txt  
  4.     train.txt  
  5.   |-test  
  6.     |-image  
  7.       test_images  
  8.     |-label  
  9.       test_labels  
  10.   |-train  
  11.     |-image  
  12.       train_images  
  13.     |-label  
  14.       train_labels  
   在voc-fcn8s文件夹下面笔者新建了data文件夹,data文件夹中有train.txt和test.txt,同时还有两个文件夹train和test,在train和test文件夹下面各自有一个image和label文件夹,里面分别记录了训练/测试的图像与标签。

(4)在准备完毕训练与测试的数据和标签之后,我们就可以修改训练文件开始训练了,下面阐述训练文件配置。

   1) 首先将fcn.berkeleyvision.org-master文件夹中的surgery.py文件与score.py文件移动到voc-fcn8s文件夹中。

   2) 打开voc-fcn8s文件夹下的solve.py文件,按照注释进行修改。

[python] view plain copy
  1. import sys  
  2. sys.path.append('/home/cvlab/caffe-master/python')#引入sys库并且增加需要的caffe的python路径  
  3. import caffe  
  4. import surgery, score  
  5.   
  6. import numpy as np  
  7. import os  
  8. import sys  
  9.   
  10. try:  
  11.     import setproctitle  
  12.     setproctitle.setproctitle(os.path.basename(os.getcwd()))  
  13. except:  
  14.     pass  
  15.   
  16. weights = './fcn8s-heavy-pascal.caffemodel'#置下载好的用来finetune的模型  
  17.   
  18. # init  
  19. caffe.set_device(int(1))#设置gpu号,笔者使用的1号gpu跑的,如果只有一块显卡,就是(int(0))  
  20. caffe.set_mode_gpu()  
  21.   
  22. solver = caffe.SGDSolver('solver.prototxt')#设置solver.prototxt  
  23. solver.net.copy_from(weights)  
  24.   
  25. # surgeries  
  26. interp_layers = [k for k in solver.net.params.keys() if 'up' in k]#此处埋下伏笔,在修改层名称的时候不要把反卷积层的name中的"up"去掉!  
  27. surgery.interp(solver.net, interp_layers)  
  28.   
  29. # scoring  
  30. val = np.loadtxt('./data/test.txt', dtype=str)#在这里按照实际路径传入第(3)步写好的test.txt  
  31.   
  32. for _ in range(25):#可选修改,在这两行按需修改训练的epoch数和每个epoch中的step数,笔者并没有进行修改。  
  33.     solver.step(4000)  
  34.     score.seg_tests(solver, False, val, layer='score')  

   在修改solve.py文件的时候,首先就是要在文件的最上端引入caffe的python路径,然后分别设置fine-tune的模型,gpu号和使用的solver.prototxt文件路径。笔者直接将fine-tune模型和solver.prototxt文件放置在voc-fcn8s目录下。然后修改在上一步中写好的test.txt文件路径。注意是传入的测试集对应的test.txt,不是训练集对应的train.txt。最后,可以修改一下训练进行的迭代epoch数和每个epoch中的step数。

   3) 修改solver.prototxt文件

[python] view plain copy
  1. train_net: "/home/cvlab/caffe-master/fcn.berkeleyvision.org-master/voc-fcn8s/train.prototxt"#修改训练prototxt文件路径  
  2. test_net: "/home/cvlab/caffe-master/fcn.berkeleyvision.org-master/voc-fcn8s/val.prototxt"#修改测试prototxt文件路径  
  3. test_iter: 500  
  4. # make test net, but don't invoke it from the solver itself  
  5. test_interval: 999999999  
  6. display: 20  
  7. average_loss: 20  
  8. lr_policy: "fixed"  
  9. # lr for unnormalized softmax  
  10. base_lr: 1e-14  
  11. # high momentum  
  12. momentum: 0.99  
  13. # no gradient accumulation  
  14. iter_size: 1  
  15. max_iter: 100000  
  16. weight_decay: 0.0005  
  17. snapshot: 4000  
  18. snapshot_prefix: "/home/cvlab/caffe-master/fcn.berkeleyvision.org-master/voc-fcn8s/snapshot/train"#修改模型保存路径  
  19. test_initialization: false  
   在此处的修改,主要是为了调整一下训练以及测试时使用的prototxt文件,再修改模型的保存路径。
   4) 修改train.prototxt文件与val.prototxt文件

   对每个文件,修改的部分一共有2类地方,第一类就是数据层,下面是笔者的train.prototxt数据层:

[python] view plain copy
  1. layer {  
  2.   name: "data"  
  3.   type: "Python"  
  4.   top: "data"  
  5.   top: "label"  
  6.   python_param {  
  7.     module: "voc_layers"  
  8.     layer: "SBDDSegDataLayer"  
  9.     param_str: "{\'sbdd_dir\': \'./data\', \'seed\': 1337, \'split\': \'train\', \'mean\': (72.5249, 82.9668, 73.1944)}"#mean: BGR  
  10.   }  
  11. }  
   里面的sbdd_dir参数就是放数据的主文件夹,如上文所说,笔者是将数据放在了voc-fcn8s/data文件夹下,因此直接传入./data,seed是一个随机数种子,笔者没动。split参数就是之前为训练和测试写的txt文件名(不带后缀格式),笔者的训练数据文件名为train.txt,因此传入train。最后是数据集的均值文件,笔者直接在训练集上求取了三个通道的均值,那么,是按照BGR的顺序写进去还是RGB的顺序写进去呢?答案是应该按照BGR的顺序传进去。因为在voc_layers.py文件中有说明,详见下文voc_layers.py代码注释分解。

   顺便附带val.prototxt中的数据层:

[python] view plain copy
  1. layer {  
  2.   name: "data"  
  3.   type: "Python"  
  4.   top: "data"  
  5.   top: "label"  
  6.   python_param {  
  7.     module: "voc_layers"  
  8.     layer: "VOCSegDataLayer"  
  9.     param_str: "{\'voc_dir\': \'./data\', \'seed\': 1337, \'split\': \'test\', \'mean\': (72.5249, 82.9668, 73.1944)}"  
  10.   }  
  11. }  

   第二类就是由于我们需要更改最后的分类数,因此需要改动带有num_output: 21的层的name属性。在更改的时候,尤其需要注意一点,因为在上文中提到,数据层需要对反卷积层参数进行初始化,因此在更改反卷积层的名称的时候,不要把"up"字样去掉,比如说笔者就直接在后面加了个"_n",原理详见上文中solve.py代码解析。

[python] view plain copy
  1. layer {  
  2.   name: "upscore2_n"  
  3.   type: "Deconvolution"  
  4.   bottom: "score_fr"  
  5.   top: "upscore2"  
  6.   param {  
  7.     lr_mult: 0  
  8.   }  
  9.   convolution_param {  
  10.     num_output: 2  
  11.     bias_term: false  
  12.     kernel_size: 4  
  13.     stride: 2  
  14.   }  
  15. }  
   5) 修改voc_layers.py文件

   voc_layers.py文件是数据层,该文件协定了训练/测试的时候的数据读取。里面有两个类,一个叫VOCSegDataLayer,用于测试时的数据读取,一个叫SBDDSegDataLayer,用于训练时的数据读取。下面是笔者的voc_layers.py文件:

[python] view plain copy
  1. import caffe  
  2.   
  3. import numpy as np  
  4. from PIL import Image  
  5.   
  6. import random  
  7.   
  8. class VOCSegDataLayer(caffe.Layer):  
  9.     """ 
  10.     Load (input image, label image) pairs from PASCAL VOC 
  11.     one-at-a-time while reshaping the net to preserve dimensions. 
  12.  
  13.     Use this to feed data to a fully convolutional network. 
  14.     """  
  15.   
  16.     def setup(self, bottom, top):  
  17.         """ 
  18.         Setup data layer according to parameters: 
  19.  
  20.         - voc_dir: path to PASCAL VOC year dir 
  21.         - split: train / val / test 
  22.         - mean: tuple of mean values to subtract 
  23.         - randomize: load in random order (default: True) 
  24.         - seed: seed for randomization (default: None / current time) 
  25.  
  26.         for PASCAL VOC semantic segmentation. 
  27.  
  28.         example 
  29.  
  30.         params = dict(voc_dir="/path/to/PASCAL/VOC2011", 
  31.             mean=(104.00698793, 116.66876762, 122.67891434), 
  32.             split="val") 
  33.         """  
  34.         # config  
  35.         params = eval(self.param_str)  
  36.         self.voc_dir = params['voc_dir']  
  37.         self.split = params['split']  
  38.         self.mean = np.array(params['mean'])  
  39.         self.random = params.get('randomize'True)  
  40.         self.seed = params.get('seed'None)  
  41.   
  42.         # two tops: data and label  
  43.         if len(top) != 2:  
  44.             raise Exception("Need to define two tops: data and label.")  
  45.         # data layers have no bottoms  
  46.         if len(bottom) != 0:  
  47.             raise Exception("Do not define a bottom.")  
  48.   
  49.         # load indices for images and labels  
  50.         split_f  = '{}/{}.txt'.format(self.voc_dir,  
  51.                 self.split)#读入val.prototxt中数据层的voc_dir参数与split参数,笔者将test.txt直接放在了voc_dir下面,这样直接就能读取到test.txt文件中的内容  
  52.         self.indices = open(split_f, 'r').read().splitlines()#在这里获取test.txt中每一行的值(不带后缀的测试图像/标签名称)  
  53.         self.idx = 0  
  54.   
  55.         # make eval deterministic  
  56.         if 'train' not in self.split:  
  57.             self.random = False  
  58.   
  59.         # randomization: seed and pick  
  60.         if self.random:  
  61.             random.seed(self.seed)  
  62.             self.idx = random.randint(0, len(self.indices)-1)  
  63.   
  64.   
  65.     def reshape(self, bottom, top):  
  66.         # load image + label image pair  
  67.         self.data = self.load_image(self.indices[self.idx])  
  68.         self.label = self.load_label(self.indices[self.idx])  
  69.         # reshape tops to fit (leading 1 is for batch dimension)  
  70.         top[0].reshape(1, *self.data.shape)  
  71.         top[1].reshape(1, *self.label.shape)  
  72.   
  73.   
  74.     def forward(self, bottom, top):  
  75.         # assign output  
  76.         top[0].data[...] = self.data  
  77.         top[1].data[...] = self.label  
  78.   
  79.         # pick next input  
  80.         if self.random:  
  81.             self.idx = random.randint(0, len(self.indices)-1)  
  82.         else:  
  83.             self.idx += 1  
  84.             if self.idx == len(self.indices):  
  85.                 self.idx = 0  
  86.   
  87.   
  88.     def backward(self, top, propagate_down, bottom):  
  89.         pass  
  90.   
  91.   
  92.     def load_image(self, idx):  
  93.         """ 
  94.         Load input image and preprocess for Caffe: 
  95.         - cast to float 
  96.         - switch channels RGB -> BGR 
  97.         - subtract mean 
  98.         - transpose to channel x height x width order 
  99.         """  
  100.         im = Image.open('{}/test/image/{}.jpg'.format(self.voc_dir, idx))#读入val.prototxt中数据层的voc_dir参数与test.txt中的某行内容,结合图片路径和图片格式,然后直接就能读取到一张图片,请大家按照自己的测试图片路径配置。  
  101.         in_ = np.array(im, dtype=np.float32)  
  102.         in_ = in_[:,:,::-1]  
  103.         in_ -= self.mean#印证了数据层中mean是按照BGR的顺序从前到后排列的  
  104.         in_ = in_.transpose((2,0,1))  
  105.         return in_  
  106.   
  107.   
  108.     def load_label(self, idx):  
  109.         """ 
  110.         Load label image as 1 x height x width integer array of label indices. 
  111.         The leading singleton dimension is required by the loss. 
  112.         """  
  113.         im = Image.open('{}/test/label/{}.png'.format(self.voc_dir, idx))#读入val.prototxt中数据层的voc_dir参数与test.txt中的同一行内容,结合标签路径和标签格式,然后直接就能读取到一张同上文中测试图片对应的标签,请大家按照自己的测试标签路径配置。  
  114.         label = np.array(im, dtype=np.uint8)  
  115.         label = label[np.newaxis, ...]  
  116.         return label  
  117.   
  118.   
  119. class SBDDSegDataLayer(caffe.Layer):  
  120.     """ 
  121.     Load (input image, label image) pairs from the SBDD extended labeling 
  122.     of PASCAL VOC for semantic segmentation 
  123.     one-at-a-time while reshaping the net to preserve dimensions. 
  124.  
  125.     Use this to feed data to a fully convolutional network. 
  126.     """  
  127.   
  128.     def setup(self, bottom, top):  
  129.         """ 
  130.         Setup data layer according to parameters: 
  131.  
  132.         - sbdd_dir: path to SBDD `dataset` dir 
  133.         - split: train / seg11valid 
  134.         - mean: tuple of mean values to subtract 
  135.         - randomize: load in random order (default: True) 
  136.         - seed: seed for randomization (default: None / current time) 
  137.  
  138.         for SBDD semantic segmentation. 
  139.  
  140.         N.B.segv11alid is the set of segval11 that does not intersect with SBDD. 
  141.         Find it here: https://gist.github.com/shelhamer/edb330760338892d511e. 
  142.  
  143.         example 
  144.  
  145.         params = dict(sbdd_dir="/path/to/SBDD/dataset", 
  146.             mean=(104.00698793, 116.66876762, 122.67891434), 
  147.             split="valid") 
  148.         """  
  149.         # config  
  150.         params = eval(self.param_str)  
  151.         self.sbdd_dir = params['sbdd_dir']  
  152.         self.split = params['split']  
  153.         self.mean = np.array(params['mean'])  
  154.         self.random = params.get('randomize'True)  
  155.         self.seed = params.get('seed'None)  
  156.   
  157.         # two tops: data and label  
  158.         if len(top) != 2:  
  159.             raise Exception("Need to define two tops: data and label.")  
  160.         # data layers have no bottoms  
  161.         if len(bottom) != 0:  
  162.             raise Exception("Do not define a bottom.")  
  163.   
  164.         # load indices for images and labels  
  165.         split_f  = '{}/{}.txt'.format(self.sbdd_dir,  
  166.                 self.split)#读入train.prototxt中数据层的sbdd_dir参数与split参数,笔者将train.txt直接放在了voc_dir下面,这样直接就能读取到train.txt文件中的内容  
  167.         self.indices = open(split_f, 'r').read().splitlines()#在这里获取train.txt中每一行的值(不带后缀的训练图像/标签名称)  
  168.         self.idx = 0  
  169.   
  170.         # make eval deterministic  
  171.         if 'train' not in self.split:  
  172.             self.random = False  
  173.   
  174.         # randomization: seed and pick  
  175.         if self.random:  
  176.             random.seed(self.seed)  
  177.             self.idx = random.randint(0, len(self.indices)-1)  
  178.   
  179.   
  180.     def reshape(self, bottom, top):  
  181.         # load image + label image pair  
  182.         self.data = self.load_image(self.indices[self.idx])  
  183.         self.label = self.load_label(self.indices[self.idx])  
  184.         # reshape tops to fit (leading 1 is for batch dimension)  
  185.         top[0].reshape(1, *self.data.shape)  
  186.         top[1].reshape(1, *self.label.shape)  
  187.   
  188.   
  189.     def forward(self, bottom, top):  
  190.         # assign output  
  191.         top[0].data[...] = self.data  
  192.         top[1].data[...] = self.label  
  193.   
  194.         # pick next input  
  195.         if self.random:  
  196.             self.idx = random.randint(0, len(self.indices)-1)  
  197.         else:  
  198.             self.idx += 1  
  199.             if self.idx == len(self.indices):  
  200.                 self.idx = 0  
  201.   
  202.   
  203.     def backward(self, top, propagate_down, bottom):  
  204.         pass  
  205.   
  206.   
  207.     def load_image(self, idx):  
  208.         """ 
  209.         Load input image and preprocess for Caffe: 
  210.         - cast to float 
  211.         - switch channels RGB -> BGR 
  212.         - subtract mean 
  213.         - transpose to channel x height x width order 
  214.         """  
  215.         im = Image.open('{}/train/image/{}.jpg'.format(self.sbdd_dir, idx))#读入train.prototxt中数据层的sdbb_dir参数与train.txt中的某行内容,结合图片路径和图片格式,然后直接就能读取到一张图片,请大家按照自己的训练图片路径配置。  
  216.         in_ = np.array(im, dtype=np.float32)  
  217.         in_ = in_[:,:,::-1]  
  218.         in_ -= self.mean#印证了数据层中mean是按照BGR的顺序从前到后排列的  
  219.         in_ = in_.transpose((2,0,1))  
  220.         return in_  
  221.   
  222. #由于笔者的标签是.png格式,因此笔者重写了load_label函数,将.mat换成.png,并将format中的self.voc_dir换成了self.sbdd_dir,从这里可以看到,标签与图片是什么格式的都可以,只要能读取到即可。  
  223.     def load_label(self, idx):  
  224.         """ 
  225.         Load label image as 1 x height x width integer array of label indices. 
  226.         The leading singleton dimension is required by the loss. 
  227.         """  
  228.         im = Image.open('{}/train/label/{}.png'.format(self.sbdd_dir, idx))#读入train.prototxt中数据层的sbdd_dir参数与train.txt中的同一行内容,结合标签路径和标签格式,然后直接就能读取到一张同上文中训练图片对应的标签,请大家按照自己的训练标签路径配置。  
  229.         label = np.array(im, dtype=np.uint8)  
  230.         label = label[np.newaxis, ...]  
  231.         return label  
  232.   
  233. #笔者将原来读取mat的函数注释掉了  
  234. """ 
  235.     def load_label(self, idx): 
  236.         import scipy.io 
  237.         mat = scipy.io.loadmat('{}/cls/{}.mat'.format(self.sbdd_dir, idx)) 
  238.         label = mat['GTcls'][0]['Segmentation'][0].astype(np.uint8) 
  239.         label = label[np.newaxis, ...] 
  240.         return label 
  241. """  

   在上面的代码中,读者朋友们可以看到,在数据层中我们写的sbdd_dir/voc_dir只是存放数据的根文件夹名称,split参数是训练/测试的txt文件名。而在voc_layers.py文件中,请读者朋友们注意上面的注释,这两个参数只是为了被当成字符串传入去读取数据集txt文件中的内容或者读取一张图片。这充分说明了,数据存放的格式是可以灵活多变的,只要在voc_layers.py文件中进行相应的配置,满足能读到相应的内容就可以了。

   另外,就是在数据层中传入的mean参数是按照BGR的顺序排列的。这可以在voc_layers.py中得到印证,图像被Image.open接口读进来,是RGB格式,然后通道换了顺序,换成了BGR格式,再减去mean,最后,将通道转换回来,又变成了RGB的顺序。因此,在数据层中写均值的时候,应该按照BGR的顺序写。

   6) 将修改好的voc_layer.py文件复制到caffe路径下的python文件夹中。


(5) 在voc-fcn8s文件夹下运行solve.py文件,训练开始,很明显地看到loss在慢慢减小,模型训练收敛!



   FCN配置训练大功告成!


(6) 对训练生成的模型进行测试

   大家最初下载的代码中有一个infer.py文件,该文件可以用来测试我们训练成功的模型,笔者训练了10w次。我们将infer.py复制进voc-fcn8s文件夹中,修改小部分代码参数:

[python] view plain copy
  1. import numpy as np  
  2. from PIL import Image  
  3.   
  4. import caffe  
  5. import cv2   #引入cv2库进行模型保存  
  6.   
  7. # load image, switch to BGR, subtract mean, and make dims C x H x W for Caffe  
  8. im = Image.open('./data/test/image/test912.jpg'#导入测试图片  
  9. in_ = np.array(im, dtype=np.float32)  
  10. in_ = in_[:,:,::-1]  
  11. in_ -= np.array((72.524982.966873.1944))  #修改均值参数  
  12. in_ = in_.transpose((2,0,1))  
  13.   
  14. # load net  
  15. net = caffe.Net('./deploy_cityscapes.prototxt''./snapshot/train_iter_100000.caffemodel', caffe.TEST)  #传入训练获得的模型  
  16. # shape for input (data blob is N x C x H x W), set data  
  17. net.blobs['data'].reshape(1, *in_.shape)  
  18. net.blobs['data'].data[...] = in_  
  19. # run net and take argmax for prediction  
  20. net.forward()  
  21. out = net.blobs['score'].data[0].argmax(axis=0)  
  22.   
  23.   
  24. result = np.expand_dims(np.array((-255.) * (out-1.)).astype(np.float32), axis = 2)    #加了两行代码保存图片  
  25. cv2.imwrite("result.png", result)  

   我们的测试图片是:


   笔者训练的2分类程序,然后运行infer.py:

   可见在文件夹下生成了测试结果图result.png:

   result.png:

   测试程序运行无误。


   到这里,FCN配置说明就接近尾声了,笔者写得比较详尽,希望能对各位读者朋友有帮助。遇到困难,还是那句老话:多读源码,冷静分析,笔者相信绝大多数技术问题会迎刃而解。


欢迎阅读笔者后续博客,各位读者朋友的支持与鼓励是我最大的动力!


written by jiong

沉舟侧畔千帆过,病树前头万木春

没有更多推荐了,返回首页

私密
私密原因:
请选择设置私密原因
  • 广告
  • 抄袭
  • 版权
  • 政治
  • 色情
  • 无意义
  • 其他
其他原因:
120
出错啦
系统繁忙,请稍后再试