FCN源码解读之solve.py

转载自 https://blog.csdn.net/qq_21368481/article/details/80636932

solve.py是FCN中解决方案文件,即通过执行solve.py文件可以实现FCN模型的训练和测试过程。以下拿voc-fcn32s文件夹里的solve.py举例分析。

一、源码及分析如下:


   
     
     
     
     
  1. #coding=utf-8
  2. import caffe
  3. import surgery, score
  4. import numpy as np
  5. import os
  6. import sys
  7. try:
  8. import setproctitle
  9. setproctitle.setproctitle(os.path.basename(os.getcwd()))
  10. except:
  11. pass
  12. #加载网络模型的参数信息
  13. weights = '../ilsvrc-nets/vgg16-fcn.caffemodel'
  14. # init 初始化(选择要执行此代码训练的GPU)
  15. caffe.set_device(int(sys.argv[ 1]))
  16. caffe.set_mode_gpu()
  17. #使用caffe中的随机梯度下降法解决方案(SGDSolver,也即FCN模型训练时每次迭代只训练一张图片)
  18. solver = caffe.SGDSolver( 'solver.prototxt')
  19. solver.net.copy_from(weights) #直接从上述加载的模型中拷贝参数初始化网络
  20. # surgeries 由于Vgg16模型是没有上采样层的,所以遇到上采样层(即含有'up'字眼的层),需要利用surgery.py中的interp()函数进行
  21. #这些层的初始化,所采用的初始化方法是双线性插值,详见我的另一篇博客https://blog.csdn.net/qq_21368481/article/details/80289350
  22. interp_layers = [k for k in solver.net.params.keys() if 'up' in k]
  23. surgery.interp(solver.net, interp_layers)
  24. # scoring val中保存测试集的索引号
  25. val = np.loadtxt( '../data/segvalid11.txt', dtype=str)
  26. #总训练次数为25*4000=100000次(注:这里不修改的话,直接在solve.prototxt中修改max_iter是没有用的)
  27. for _ in range( 25):
  28. solver.step( 4000) #每训练4000次,进行一次测试,并保存一下模型参数(对应solve.prototxt中的snapshot: 4000)
  29. score.seg_tests(solver, False, val, layer= 'score')

但是直接执行上述代码是无法运行的,会报很多错误,需要进行修改才能运行,具体修改后的代码如下(读者可以和上述源码对比一下,做出相应的修改):


   
     
     
     
     
  1. #coding=utf-8
  2. import sys  
  3. sys.path.append( 'D:/caffe/caffe-master/python'#加载caffe中的python文件所在路径(以免报错),注意要加载自己的
  4. import caffe
  5. import surgery, score
  6. import numpy as np
  7. import os
  8. try:
  9.     import setproctitle
  10.     setproctitle.setproctitle(os.path.basename(os.getcwd()))
  11. except:
  12.     pass
  13. '''
  14. 加载Vgg16模型参数(原因在于FCN中FCN32s是在Vgg16模型上fine-tune来的,而
  15. FCN16s是在FCN32s的基础上训练的,所以在FCN16s的solve.py文件中要加载FCN32s
  16. 训练完后的模型(例如caffemodel-url中给的fcn32s-heavy-pascal.caffemodel,
  17. caffemodel-url文件在每个FCN模型文件夹都有,例如voc-fcn32s文件夹,直接
  18. 复制里面的网址下载即可),FCN8s的加载FCN16s训练好的模型参数。
  19. Vgg16模型参数和其deploy.prototxt文件可从我提供的百度云链接上下载:
  20. '''
  21. vgg_weights = 'vgg16-fcn.caffemodel'  
  22. vgg_proto = 'vgg16_deploy.prototxt'    #加载Vgg16模型的deploy.prototxt文件
  23. #weights = '../ilsvrc-nets/vgg16-fcn.caffemodel'
  24. # init 初始化
  25. #caffe.set_device(int(sys.argv[1]))
  26. caffe.set_device( 0)
  27. caffe.set_mode_gpu() #如果是CPU训练修改为caffe.set_mode_cpu(),但是好像CPU带不起FCN这个庞大模型(内存不够)
  28. solver = caffe.SGDSolver( 'solver.prototxt')
  29. #solver.net.copy_from(weights)
  30. vgg_net = caffe.Net(vgg_proto, vgg_weights, caffe.TRAIN)  #利用上述加载的vgg_weights和vgg_proto初始化Vgg16网络
  31. #将上述网络通过transplant()函数强行复制给FCN32s网络(具体复制方法仍可参见我的
  32. #另一篇博客https://blog.csdn.net/qq_21368481/article/details/80289350)
  33. surgery.transplant(solver.net, vgg_net)  
  34. del vgg_net  #删除Vgg16网络
  35. # surgeries
  36. interp_layers = [k for k in solver.net.params.keys() if 'up' in k]
  37. surgery.interp(solver.net, interp_layers)
  38. #此处训练集的路径需要换成自己的,其中的seg11valid.txt里存放的是训练集每张图片的文件名(不包含扩展名)
  39. #可以自行设置哪些图片作为训练集(个人建议直接拿Segmentation文件夹中的val.txt里的当做训练集)
  40. val = np.loadtxt( 'D:/VOC2012/ImageSets/Segmentation/seg11valid.txt', dtype=str)
  41. for _ in range( 25):
  42.     solver.step( 4000)
  43.     score.seg_tests(solver, False, val, layer= 'score')

1.1 运行

在修改完上述文件的情况下,如果是windows下(装了Anaconda的话直接打开Anaconda Prompt,如果没装的话直接cmd打开命令提示符窗口),直接在voc-fcn32s文件路径下输入以下语句即可进行训练。

python solve.py
   
     
     
     
     

如下图所示:

FCN源码解读之solve.py_第1张图片

如果在linux下,打开终端,也在voc-fcn32s文件路径下输入以下语句:

sudo python solve.py
   
     
     
     
     
如下图所示:


1.2 常见错误

(1)ImportError: No module named surgery

将与voc-fcn32s文件夹同一根目录下的surgery.py文件复制到voc-fcn32s文件夹中,并在开头添加如下代码:


   
     
     
     
     
  1. from __future__ import division
  2. import sys
  3. sys.path.append( 'D:/caffe/caffe-master/python') #添加自己caffe中的python文件夹路径
  4. import caffe
  5. import numpy as np

如果还报No module named score等,同理解决。

(2)路径错误

训练前,还需要修改一下train.prototxt和val.prototxt中的data层中数据集加载路径,拿train.prototxt为例,'sbdd_dir'后的路径修改为自己数据集的路径。


   
     
     
     
     
  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\': \'D:/VOC2012\', \'seed\': 1337, \'split\': \'train\', \'mean\': (104.00699, 116.66877, 122.67892)}"
  10. }
  11. }

同时也要修改一下voc_layers.py文件,因为这里面也有训练集和测试集的加载路径,且在训练过程中是会时时通过这个文件下的代码加载训练图片(原因在于上述train.prototxt中是data层中的module:"voc_layers",所代表的意思就是通过voc_layers.py自行定义的训练集和测试集加载方式来加载),测试图片加载同理。

主要修改其中的class SBDDSegDataLayer(caffe.Layer)类中的setup(self, bottom, top)函数中的路径和load_label(self, idx)函数中的数据集格式以及class VOCSegDataLayer(caffe.Layer)类中的setup(self, bottom, top)函数的路径,都修改为自己的。

详细可参见我的另一篇博客:https://blog.csdn.net/qq_21368481/article/details/80246028。

二、实时绘制loss-iter曲线

caffe中的loss分两种,一种是本次单次迭代输出的loss,一种是多次迭代的平均loss,具体可以参见caffe中的src文件夹下的solver.cpp中的Step(int iters) 函数和UpdateSmoothedLoss(Dtype loss, int start_iter, int average_loss) 函数。

如下所示,Iteration 20, loss = 390815中的loss为20次迭代输出的平均loss,而rain net output #0: loss = 151572为本次单次迭代输出的loss,也即为何两者是不同的(只有在Iteration 0时两者才会相同,因为平均loss是从0开始累加求平均计算出来,第0次迭代输出的平均loss就是第0次迭代输出的loss)。

平均loss的好处的更加平滑,当然不用loss直接用单次的loss都是一样的,两者都能反映训练过程中loss的变化趋势。


   
     
     
     
     
  1. I0611 22: 14: 55.219404 6556 solver.cpp: 228] Iteration 0, loss = 557193
  2. I0611 22: 14: 55.219404 6556 solver.cpp: 244] Train net output #0: loss = 557193 (* 1 = 557193 loss)
  3. I0611 22: 14: 55.233840 6556 sgd_solver.cpp: 106] Iteration 0, lr = 1e-010
  4. I0611 22: 15: 03.114138 6556 solver.cpp: 228] Iteration 20, loss = 390815
  5. I0611 22: 15: 03.114138 6556 solver.cpp: 244] Train net output #0: loss = 151572 (* 1 = 151572 loss)
  6. I0611 22: 15: 03.115141 6556 sgd_solver.cpp: 106] Iteration 20, lr = 1e-010
  7. I0611 22: 15: 11.631978 6556 solver.cpp: 228] Iteration 40, loss = 299086
  8. I0611 22: 15: 11.632982 6556 solver.cpp: 244] Train net output #0: loss = 22125.5 (* 1 = 22125.5 loss)
  9. I0611 22: 15: 11.634985 6556 sgd_solver.cpp: 106] Iteration 40, lr = 1e-010
  10. I0611 22: 15: 19.864284 6556 solver.cpp: 228] Iteration 60, loss = 238765
  11. I0611 22: 15: 19.865325 6556 solver.cpp: 244] Train net output #0: loss = 154152 (* 1 = 154152 loss)
  12. I0611 22: 15: 19.866287 6556 sgd_solver.cpp: 106] Iteration 60, lr = 1e-010

1.Step(int iters) 函数


   
     
     
     
     
  1. template < typename Dtype>
  2. void Solver::Step( int iters) {
  3. const int start_iter = iter_; //起始迭代点
  4. const int stop_iter = iter_ + iters; //终止迭代点
  5. int average_loss = this->param_.average_loss(); //平均loss=(前average_loss-1次迭代的loss总和+本次迭代的loss)/average_loss
  6. losses_.clear(); //清楚用来保存每次迭代所输出的loss
  7. smoothed_loss_ = 0; //smoothed_loss即平均loss(注:每次调用Step()函数平均loss就会清零,即从本次迭代开始重新开始累计求平均)
  8. while (iter_ < stop_iter) {
  9. // zero-init the params
  10. net_->ClearParamDiffs();
  11. if (param_.test_interval() && iter_ % param_.test_interval() == 0
  12. && (iter_ > 0 || param_.test_initialization())
  13. && Caffe::root_solver()) {
  14. TestAll();
  15. if (requested_early_exit_) {
  16. // Break out of the while loop because stop was requested while testing.
  17. break;
  18. }
  19. }
  20. for ( int i = 0; i < callbacks_.size(); ++i) {
  21. callbacks_[i]->on_start();
  22. }
  23. const bool display = param_.display() && iter_ % param_.display() == 0;
  24. net_->set_debug_info(display && param_.debug_info());
  25. // accumulate the loss and gradient
  26. Dtype loss = 0;
  27. for ( int i = 0; i < param_.iter_size(); ++i) { //这里的param_.iter_size()对于FCN来说,对应solver.prototxt中的iter_size这个参数
  28. loss += net_->ForwardBackward(); //前向计算+后向传播(从中可以看出每次迭代过程中其实是param_.iter_size()次前向计算和后向传播)
  29. }
  30. loss /= param_.iter_size(); //求平均
  31. // average the loss across iterations for smoothed reporting
  32. UpdateSmoothedLoss(loss, start_iter, average_loss); //更新平均loss
  33. if (display) { //一般而言都设置display=average_loss,这里可以看看FCN中是设置
  34. LOG_IF(INFO, Caffe::root_solver()) << "Iteration " << iter_
  35. << ", loss = " << smoothed_loss_; //输出平均loss
  36. const vector*>& result = net_->output_blobs();
  37. int score_index = 0;
  38. for ( int j = 0; j < result.size(); ++j) {
  39. const Dtype* result_vec = result[j]->cpu_data();
  40. const string& output_name =
  41. net_->blob_names()[net_->output_blob_indices()[j]];
  42. const Dtype loss_weight =
  43. net_->blob_loss_weights()[net_->output_blob_indices()[j]];
  44. for ( int k = 0; k < result[j]->count(); ++k) {
  45. ostringstream loss_msg_stream;
  46. if (loss_weight) { //只有loss层的loss_weight=1,其他层都是0
  47. loss_msg_stream << " (* " << loss_weight
  48. << " = " << loss_weight * result_vec[k] << " loss)";
  49. }
  50. LOG_IF(INFO, Caffe::root_solver()) << " Train net output #"
  51. << score_index++ << ": " << output_name << " = "
  52. << result_vec[k] << loss_msg_stream.str(); //输出单次迭代的loss值
  53. }
  54. }
  55. }
  56. for ( int i = 0; i < callbacks_.size(); ++i) {
  57. callbacks_[i]->on_gradients_ready();
  58. }
  59. ApplyUpdate();
  60. // Increment the internal iter_ counter -- its value should always indicate
  61. // the number of times the weights have been updated.
  62. ++iter_;
  63. SolverAction::Enum request = GetRequestedAction();
  64. // Save a snapshot if needed.
  65. if ((param_.snapshot()
  66. && iter_ % param_.snapshot() == 0
  67. && Caffe::root_solver()) ||
  68. (request == SolverAction::SNAPSHOT)) {
  69. Snapshot();
  70. }
  71. if (SolverAction::STOP == request) {
  72. requested_early_exit_ = true;
  73. // Break out of training loop.
  74. break;
  75. }
  76. }
  77. }
2.UpdateSmoothedLoss(Dtype loss, int start_iter, int average_loss) 函数

   
     
     
     
     
  1. void Solver::UpdateSmoothedLoss(Dtype loss, int start_iter,
  2. int average_loss) {
  3. if (losses_.size() < average_loss) { //如果losses_中还未存储完average_loss次迭代输出的loss,则直接求平均
  4. losses_.push_back(loss);
  5. int size = losses_.size();
  6. smoothed_loss_ = (smoothed_loss_ * (size - 1) + loss) / size; //求前size次迭代输出的loss的平均值
  7. } else { //如果losses_中已经存在average_loss次迭代输出的loss,则取离本次迭代最近的average_loss-1次迭代输出的loss与本次loss求平均
  8. int idx = (iter_ - start_iter) % average_loss;
  9. smoothed_loss_ += (loss - losses_[idx]) / average_loss;
  10. losses_[idx] = loss; //将本次迭代输出的loss存入losses_中的相应位置
  11. }
  12. }

3.修改后的solve.py文件如下(实现实时绘制loss-iter曲线)


   
     
     
     
     
  1. #coding=utf-8
  2. import sys
  3. sys.path.append( 'D:/caffe/caffe-master/python')
  4. import caffe
  5. import surgery, score
  6. import numpy as np
  7. import os
  8. import sys
  9. #plot 加载绘制图像所需要的python库
  10. import matplotlib.pyplot as plt
  11. from matplotlib.patches import Circle
  12. import math
  13. //根据上述UpdateSmoothedLoss()函数修改为python语言而来,目的就是更新平均loss
  14. def UpdateSmLoss(loss,losses_,iterval,average_loss,sm_loss):
  15. sizel = len(losses_)
  16. listloss=loss.tolist() #array转化为list
  17. if sizel < average_loss:
  18. losses_.append(listloss)
  19. sm_loss = (sm_loss*sizel+listloss)/(sizel+ 1)
  20. else:
  21. idx = iterval % average_loss
  22. sm_loss += (listloss-losses_[idx]) / average_loss
  23. losses_[idx] = listloss
  24. return sm_loss,losses_
  25. try:
  26. import setproctitle
  27. setproctitle.setproctitle(os.path.basename(os.getcwd()))
  28. except:
  29. pass
  30. vgg_weights = 'vgg16-fcn.caffemodel'
  31. vgg_proto = 'vgg16_deploy.prototxt'
  32. #weights = 'vgg16-fcn.caffemodel'
  33. # init
  34. #caffe.set_device(int(sys.argv[1]))
  35. caffe.set_device( 0)
  36. caffe.set_mode_gpu()
  37. #solver = caffe.SGDSolver('solver.prototxt')
  38. #solver.net.copy_from(weights)
  39. solver = caffe.SGDSolver( 'solver.prototxt')
  40. #parameter 实时绘制所需要的一些参数
  41. niter = 100000 #对应solver.prototxt中的max_iter: 100000
  42. display = 20 #对应solver.prototxt中的display: 20
  43. snapshotnum = 4000 #对应solver.prototxt中的snapshot: 4000
  44. ave_loss = 20 #对应solver.prototxt中的average_loss: 20
  45. #losses_用于存储当前迭代次数的前average_loss次迭代所产生的loss
  46. losses_ = []
  47. sm_loss = 0 #平均loss
  48. #train_loss 用于存储每次的sm_loss,以便画折线图
  49. train_loss = np.zeros(np.ceil(niter * 1.0 / display))
  50. vgg_net = caffe.Net(vgg_proto, vgg_weights, caffe.TRAIN)
  51. surgery.transplant(solver.net, vgg_net)
  52. del vgg_net
  53. # surgeries
  54. interp_layers = [k for k in solver.net.params.keys() if 'up' in k]
  55. surgery.interp(solver.net, interp_layers)
  56. # scoring
  57. val = np.loadtxt( 'D:/VOC2012/ImageSets/Segmentation/seg11valid.txt', dtype=str)
  58. #for _ in range(25):
  59. #solver.step(4000)
  60. #score.seg_tests(solver, False, val, layer='score')
  61. plt.close()
  62. fig=plt.figure()
  63. ax=fig.add_subplot( 1, 1, 1)
  64. plt.grid( True)
  65. plt.ion() #开启交互式绘图(实现实时绘图的关键语句)
  66. for it in range(niter):
  67. solver.step( 1) #python下的step()函数,对应于上述Step()函数
  68. _train_loss = solver.net.blobs[ 'loss'].data #取出每次迭代输出的loss
  69. [sm_loss,losses_] = UpdateSmLoss(_train_loss,losses_,it,ave_loss,sm_loss) #更新
  70. if it % display == 0 and it != 0: #满足条件时展示平均loss
  71. ax.scatter(it,sm_loss,c = 'r',marker = 'o') #绘制loss的散点图
  72. train_loss[it // display - 1] = sm_loss #存储平均loss
  73. if it > display:
  74. ax.plot([it -20,it],[train_loss[it // display - 2],train_loss[it // display - 1]], '-r') #绘制折线图
  75. plt.pause( 0.0001)
  76. if it % snapshotnum == 0 and it != 0: #对应原solve.py文件中的最后两句代码,每snapshotnum次迭代进行一次测试
  77. score.seg_tests(solver, False, val, layer= 'score')
  78. losses_ = [] #测试后需要清空losses_以及平均loss,对应于每次进入Step()函数都需要对这两者清空
  79. sm_loss = 0

效果图如下:

FCN源码解读之solve.py_第2张图片

注:可以发现在训练过程中输出的本次单次迭代loss和多次迭代的平均loss是一样的,原因在于每次进入到Step()函数都需要对losses_ 和smoothed_loss清空导致每次进入Step()函数后losses_的大小总为0,也就只会进入if (losses_.size() < average_loss)这个条件里,导致两种loss相同。


   
     
     
     
     
  1. I0611 22: 08: 17.291122 8428 solver.cpp: 228] Iteration 160, loss = 63695.3
  2. I0611 22: 08: 17.291122 8428 solver.cpp: 244] Train net output #0: loss = 63695.3 (* 1 = 63695.3 loss)
  3. I0611 22: 08: 17.292124 8428 sgd_solver.cpp: 106] Iteration 160, lr = 1e-010

你可能感兴趣的:(Deep,Learning,FCN,caffe)