1. 程式人生 > >通過加權解決Detectron訓練object detection模型時的類間不平衡問題

通過加權解決Detectron訓練object detection模型時的類間不平衡問題

使用深度學習解決分類問題時,類間不平衡是一個常見的問題,我們也有很多常用的方法去解決這一問題。比如,對類別少的樣本進行augment,或者重取樣;對類別多的樣本進行降取樣;根據不同類別的樣本數目對損失函式進行加權;或者簡單粗暴地對較少的樣本在資料集內進行復制;等等。

不過這些方法中,考慮到資料對深度學習的重要性,一般不會使用對資料進行降取樣的方法;而augment、重取樣以及複製資料,都需要對資料進行直接操作,也並不如修改損失函式進行加權的方法簡單優雅。這種對損失函式進行加權的方法如下:

Loss = class_of_less_samples_loss * weights + class_of_more_samples_loss

不過,這種方法雖然簡單優雅,對分類問題進行處理也簡單方便,但如果是目標檢測問題,則可能需要考慮更多東西;如果我們使用的是像Detectron這樣包裝嚴密的框架,處理起來可能難上加難。本文,我們就以Detectron裡的Faster-RCNN來詳細描述一下這一問題,以期提供一些經驗參考。

1.Faster-RCNN的loss

Faster-RCNN一共有4個loss,包括前面的RPN的classification loss和regression loss,以及後面的classification loss和regression loss。但RPN部分除了前景、背景的分類之外,一般不按具體的類別進行區分,所以,我們無法對這一部分進行加權。而且由於這一部分僅僅進行region proposal,與具體的類別關係不大,所以對其進行加權也意義不大。至於後面的Fast-RCNN部分,對邊框進行迴歸的regression loss同樣意義不大,所以我們進行加權,主要加權的部分就是後面的Fast-RCNN部分

的classification loss。

當然,除了對不同類別的分類進行加權之外,我們還考慮到這樣一個情況,即有時候分類錯誤的損失影響較大,而只要檢測出來,檢測到的物體的邊框並不一定要求特別精確,所以,通常會針對具體問題,對這個地方進行修改,改變兩者的權重佔比。

2.Detectron的caffe2

caffe2是Facebook推出的主要面向產品部署等場景的深度學習框架,而針對於研究等方向,Facebook則推出的是簡單易用的PyTorch;因此,caffe2過於關注了效能,其易用性並不好。所以,基於caffe2的Detectron用起來雖然訓練速度很好,但想對其進行修改,卻並非易事。

與caffe類似,caffe2也是使用Blob來管理資料的。所以,在caffe2中,傳進傳出的引數,通常只有一個名字,這讓很多人摸不著頭腦,也使得除錯十分困難。

在caffe2中,新建一個Blob可以使用如下程式碼:

  1. import numpy as np
  2. from caffe2.python import core, workspace
  3. labels_array = np.array(...)
  4. labels = workspace.FeedBlob('labels', labels_array)

這樣,我們就新建了一個名為labels的Blob。

然後,我們可以在其他地方使用,使用時,直接把資料fetch出來即可:

  1. from caffe2.python import core, workspace
  2. labels = workspace.FetchBlob('labels')
  3. # labels = workspace.FetchBlob(core.ScopedName('labels'))

如果labels是一個全域性名,可以直接進行fetch;如果它是定義在一個scope裡面的Blob,則需要首先對名字進行一下處理,否則可能提示找不到相應的Blob。

在Detectron中,有很多使用Blob的例子,如這個,以及這個

注意:這裡給的連結都是老版本的連結,需要在github上搜一下,找到相應的檔案

3.修改Detectron中Faster-RCNN的loss_cls和loss_bbox的比例

Faster-RCNN中,建立這兩個loss是在fast_rcnn_heads.py中的add_fast_rcnn_losses函式,該函式中,分別使用model.net.SoftmaxWithLoss和model.net.SmoothL1Loss得到了相應的loss值。而SoftmaxWithLoss這個函式中的引數scale=model.GetLossScale(),即是控制比例的地方。函式中,這個比例根據使用GPU的數量進行調節,我們為了最小修改原來的內容,直接在這個函式後面加上修改的比例即可,比如,將其修改為:scale=model.GetLossScale()*2,即將loss_cls的比重擴大到原來的2倍。如果與此同時,對SmoothL1Loss的scale不作調整,則相當於將classification的loss權重提高到了regression loss的2倍。

4.根據不同類別的Object,修改其在loss中的權重

我們首先來看3中的loss_cls,這個loss是使用model.net.SoftmaxWithLoss來生成的。查閱這個函式的文件,可以看到,其輸入值,除了logits、labels,還可以輸入一個Blob作為weights。這就是我們需要修改的地方。

minibatch.py裡的get_minibatch函式中,可以看到,讀取RPN的Blob是通過roi_data.rpn.add_rpn_blobs來完成的,而讀取Fast-RCNN的資料,則是通過roi_data.fast_rcnn.add_fast_rcnn_blobs來完成的。因此,如上分析,我們需要修改進行加權的操作,是需要在後面這個函式中進行的。

我們跳轉到_sample_rois這個函式裡,我們找到labels生成的地方,然後根據labels,來生成我們需要的weights即可。

如下:在背景後面新增權重值

    # Label is the class each RoI has max overlap with
    sampled_labels = roidb['max_classes'][keep_inds]
    sampled_labels[fg_rois_per_this_image:] = 0  # Label bg RoIs with class 0
    #add weights,refer to the web blog
    weights=(np.where(sampled_labels==9,20,1)).astype(np.float32)

在這裡,我們將label為9的類別的權重設定為了20,而其他類別保持了不變。然後,在下面要返回的blob_dict裡新增上這個weights即可,如下:

  1. blob_dict = dict(
  2. labels_int32=sampled_labels.astype(np.int32, copy=False),
  3. #add this line
  4. weights=weights,
  5. rois=sampled_rois,
  6. bbox_targets=bbox_targets,
  7. bbox_inside_weights=bbox_inside_weights,
  8. bbox_outside_weights=bbox_outside_weights
  9. )

不過,在此之前,需要新增一個名為weights的Blob。

  1. # Add weights
  2. if is_training:
  3. blob_names += ['weights']

如此,即可為指定類別的Object進行加權,從而突出其在object detection時的重要性,對其進行特殊對待。

5.對某些圖片進行加權

有時候,我們不僅想要對這些類別進行加權,我們還希望對出現了這個類別的圖片進行加權,這時候應該這麼處理呢?

這個比較簡單,直接在最開始讀資料roidb時進行處理即可。在train.py裡的combined_roidb_for_training函式,是建立資料roidb用於訓練的。該函式定義在roidb.py檔案中,在該函式中,我們可以看到一個過濾roidb中資料的函式,該函式用來“Remove roidb entries that have no usable RoIs based on config settings”。我們可以仿照這個函式,構建一個augment_roidb_for_training函式。如下:

  1. def augment_for_training(roidb):
  2. """Augment roidb entries that have some RoIs.
  3. """
  4. num = len(roidb)
  5. auged_roidb = []
  6. for db in roidb:
  7. if np.sum(db['gt_classes']==2)>=1:
  8. auged_roidb.append(db)
  9. auged_roidb.append(db)
  10. else:
  11. auged_roidb.append(db)
  12. num_after = len(auged_roidb)
  13. logger.info('Augment {} roidb entries: {} -> {}'.
  14. format(num_after - num, num, num_after))
  15. return auged_roidb

然後在在combined_roidb_for_training(dataset_names, proposal_files)函式中,呼叫該函式

    roidb = filter_for_training(roidb)     roidb = augment_for_training(roidb)

這樣,我們將含有label為2的圖片複製成了兩份,即相當於對其完成了加權。