本文主要介紹如何使用自己的數據集訓練DeepLabv3+分割算法,代碼使用的是官方源碼。
1、代碼簡介
當前使用TensorFlow版本的官方源碼,選擇它的原因是因爲代碼中的內容比較全面,除了代碼實現以外,還提供了許多文檔幫助理解與使用,同時還提供了模型轉換的代碼實現。
代碼地址:
【github】models/research/deeplab at master · tensorflow/models
接下來,先對這個代碼倉庫進行一下簡單的介紹,因爲自己在使用該代碼倉庫的時候只關心訓練代碼的實現,而忽略的其他的內容,走了不少彎路,到後面才發現我想要的內容,倉庫裏面早有(==)。
在當前的實現中,我們支持採用以下網絡主幹:
MobileNetv2
和MobileNetv3
:一個爲移動設備設計的快速網絡結構Xception
:用於服務器端部署的強大網絡結構ResNet-v1-{50, 101}
:我們提供原始的ResNet-v1
及其“ beta”
變體,其中對“ stem”
進行了修改以進行語義分割。PNASNet
: 一個通過神經體系結構搜索發現的強大網絡結構。Auto-Deeplab
(代碼中叫做HNASNet
):通過神經體系結構搜索找到的特定於細分的網絡主幹。
該目錄包含TensorFlow 實現。我們提供的代碼使用戶可以訓練模型,根據mIOU
(平均交叉點求和)評估結果以及可視化細分結果。我們以PASCAL VOC 2012
和Cityscapes
語義分割基準爲例。
代碼中幾個重要文件:
datasets/
:該文件夾下包含對於訓練數據集的處理代碼,主要針對PASCAL VOC 2012
和Cityscapes
數據集的處理。g3doc/
:該文件夾下包含多個Markdown
文件,非常有用,如何安裝,常見問題FAQ等。deeplab_demo.ipynb
:該文件中給出瞭如果對一張圖像進行語義分割並顯示結果的Demo。export_model.py
:該文件提供了將訓練的checkpoint
模型轉爲.pb
文件的代碼實現。train.py
:訓練代碼文件,訓練時,需要指定提供的訓練參數。eval.py
:驗證代碼,輸出mIOU,用來評估模型的好壞。vis.py
:可視化代碼。
2、安裝
Deeplab
依賴的庫有:
- Numpy
- Pillow 1.0
- tf Slim (which is included in the “tensorflow/models/research/” checkout)
- Jupyter notebook
- Matplotlib
- Tensorflow
2.1 添加庫到PYTHONPATH
本地運行的時候,tensorflow/models/research/
目錄應該追加到PYTHONPATH
中,如下:
# From tensorflow/models/research/
export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/slim
# [Optional] for panoptic evaluation, you might need panopticapi:
# https://github.com/cocodataset/panopticapi
# Please clone it to a local directory ${PANOPTICAPI_DIR}
touch ${PANOPTICAPI_DIR}/panopticapi/__init__.py
export PYTHONPATH=$PYTHONPATH:${PANOPTICAPI_DIR}/panopticapi
注意:此命令需要在您啓動的每個新終端上運行。如果希望避免手動運行此命令,可以將它作爲新行添加到
〜/ .bashrc
文件的末尾。
2.2 測試是否安裝成功
通過運行model_test.py
快速測試:
# From tensorflow/models/research/
python deeplab/model_test.py
在PASCAL VOC 2012
數據集上快速運行所有代碼:
# From tensorflow/models/research/deeplab
sh local_test.sh
3、數據集準備
最終目標: 生成TFRecord
格式的數據
數據集目錄結構如下:
+dataset #數據集名稱
+image
+mask
+index
- train.txt
- trainval.txt
- val.txt
+tfrecord
- image: 原圖圖像,RGB彩色圖像
- mask:像素值爲類別標籤的mask圖像,單通道,與原圖的名稱一致,後綴爲
.jpg
和.png
都可以,只要在代碼中讀取一致即可。VOC數據集默認原圖是.jpg
,mask圖像爲.png
。 - index:存放圖像文件名的
txt
文件(不加後綴) - tfrecord:存放轉爲
tfrecord
格式的圖像數據
數據集製作流程:
- 標註數據,製作符合要求的
mask
圖像 - 將數據集分割爲訓練集、驗證集和測試集
- 生成
TFRecord
格式的數據集
3.1 標註數據
訓練集數據包含兩部分,一是原圖,二是對應分類的標註值(本文中稱爲mask
圖像)。
mask圖像的值是如何設置的?
根據圖像分割的分類個數來製作原圖對應的mask圖像。假如一共有N個類別(背景作爲一類),則mask圖像的值的範圍是[0~N)
。0
值作爲背景值,其他分割類別的值依次設置爲1, 2, ..., N-1
。
注意:
ignore_label
:從字面意思來講是忽略的標籤,即ignore_label
是指沒有做標註的像素,即不需進行預測的像素值,因此,它不參與loss
值的計算,在mask
圖像中將其值記爲255
。mask
圖像是單通道的灰度圖像。mask
圖像的格式沒有限定,但所有的mask圖像採用同一種圖像格式,方便數據讀取。
小總結
mask
圖像的值分爲三類:
- 背景:用
0
表示 - 分類類別:使用
1, 2, ....., N-1
表示 ignore_label
值:用255
表示
如果分割的類別較少,則生成的
mask
圖像看上去是一片黑,因爲分類的值都較小,在0~255
的範圍內不容易顯示出來。
3.2 分割數據集
這部分就是將準備的數據集進行分割,分爲訓練集、驗證集、測試集。
無需將具體的圖像文件分到三個文件夾中,只需要建立圖像的索引文件即可,通過添加相應的路徑+文件名即可獲取到具體的圖像。
假設原圖像和mask圖像的存放路徑如下:
- 原圖:
./dataset/images
mask
圖像:./dataset/mask
:此處存放的是2.1小節要求格式
原圖與
mask
圖像是一一對應的,包括圖像尺寸,圖像名(後綴可以不同)
索引文件存放路徑:./dataset/index
,該路徑下生成:
train.txt
trainval.txt
val.txt
索引文件中,只需記錄文件名(不加後綴),這取決於代碼中數據集加載的方式。
目前爲止,數據集目錄結構如下:
#./dataset
+image
+mask
+index
- train.txt
- trainval.txt
- val.txt
3.3 將數據打包爲TFRecord
格式
TFRecord
是谷歌推薦的一種二進制文件格式,理論上它可以保存任何格式的信息。
TFRecord
內部使用了“Protocol Buffer”
二進制數據編碼方案,它只佔用一個內存塊,只需要一次性加載一個二進制文件的方式即可,簡單,快速,尤其對大型訓練數據很友好。而且當我們的訓練數據量比較大的時候,可以將數據分成多個TFRecord文件,來提高處理效率。
那麼,如何將數據生成TFRecord
格式呢?
在此,我們可以藉助 項目代碼中./datasets/build_voc2012_data.py
文件來實現。給文件是VOC2012數據集處理的代碼,我們只需修改一下輸入參數即可。
參數:
image_folder
:原圖文件夾名稱,./dataset/image
semantic_segmentation_folder
:分割文件夾名稱,./dataset/mask
list_folder
:索引文件夾名稱,./dataset/index
output_dir
:輸出路徑,即生成的tfrecord
文件所在位置,./dataset/tfrecord
運行命令:
python ./datasets/build_voc2012_data.py --image_folder=./dataset/image
--semantic_segmentation_folder=./dataset/mask
--list_folder=./dataset/index
--output_dir=./dataset/tfrecord
生成的文件如下:
注意: 可在代碼中調節參數_NUM_SHARDS
(默認爲4
),改變數據分塊的數目。(一些文件系統有最大單個文件大小的限制,如果數據集非常大,增加_NUM_SHARDS
可減小單個文件的大小)
該文件的核心代碼如下:
# dataset_split指的是train.txt, val.txt等
dataset = os.path.basename(dataset_split)[:-4]
filenames = [x.strip('\n') for x in open(dataset_split, 'r')] # 文件名列表
# 輸出tfrecord文件名
output_filename = os.path.join(
FLAGS.output_dir,
'%s-%05d-of-%05d.tfrecord' % (dataset, shard_id, _NUM_SHARDS))
with tf.python_io.TFRecordWriter(output_filename) as tfrecord_writer:
for i in range(start_idx, end_idx):
image_filename = os.path.join(iamge_folder, filenames[i]+'.'+image_format)# 原圖路徑
image_data = tf.gfile.GFile(image_filename, 'rb').read() #讀取原圖文件
height, width = image_reader.read_image_dims(image_data)
seg_filename = os.path.join(semantic_segmentation_folder,
filenames[i] + '.' + label_format) # mask圖像路徑
seg_data = tf.gfile.GFile(seg_filename, 'rb').read() # 讀取分割圖像
seg_height, seg_width = label_reader.read_image_dims(seg_data)
# 判斷原圖與mask圖像尺寸是否匹配
if height != seg_height or width != seg_width:
raise RuntimeError('Shape mismatched between image and label.')
# Convert to tf example.
example = build_data.image_seg_to_tfexample(
image_data, filenames[i], height, width, seg_data)
tfrecord_writer.write(example.SerializeToString())
至此,數據集的製作部分已經完成!!!
4、訓練
4.1 代碼修改
爲了訓練自己的數據集,需要修改以下幾處文件:
1 datasets/data_generator.py
:增加數據集的註冊
該文件提供語義分割數據的包裝器
在該文件中,可以看到PASCAL_VOC
, CITYSCAPES
以及ADE20K
數據集的數據描述,如下:
_PASCAL_VOC_SEG_INFORMATION = DatasetDescriptor(
splits_to_sizes={
'train': 1464,
'train_aug': 10582,
'trainval': 2913,
'val': 1449,
},
num_classes=21,
ignore_label=255,
)
en,比着葫蘆畫瓢,增加我們自己數據集的描述信息,如下:
_PORTRAIT_INFORMATION = DatasetDescriptor(
splits_to_sizes={
'train': 17116,
'trainval': 21395,
'val': 4279,
},
num_classes=2, # 類別數目,包括背景
ignore_label=255, # 忽略像素值
)
以人像分割任務爲例,只有兩類,即前景(人像)和背景(非人像)。
添加完描述信息後,需要將該數據集信息進行註冊,如下:
_DATASETS_INFORMATION = {
'cityscapes': _CITYSCAPES_INFORMATION,
'pascal_voc_seg': _PASCAL_VOC_SEG_INFORMATION,
'ade20k': _ADE20K_INFORMATION,
'portrait_seg': _PORTRAIT_INFORMATION, #增加此句
}
注意:此處的數據集名稱要與前面對應!
2 ./utils/train_utils.py
修改
在函數get_model_init_fn
中,修改爲如下代碼,增加logits
層不加載預訓練模型權重:
# Variables that will not be restored.
exclude_list = ['global_step', 'logits']
if not initialize_last_layer:
exclude_list.extend(last_layers)
4.2 主要訓練參數
訓練文件train.py
和common.py
文件中包含了訓練分割網絡所需要的所有參數。
model_variant
:Deeplab
模型變量,可選值可見core/feature_extractor.py
。- 當使用
mobilenet_v2
時,設置變量strous_rates=decoder_output_stride=None
; - 當使用
xception_65
或resnet_v1
時,設置strous_rates=[6,12,18](output stride 16), decoder_output_stride=4
。
- 當使用
label_weights
:此變量可以設置標籤的權重值,當數據集中出現類別不均衡時,可通過此變量來指定每個類別標籤的權重值,如label_weights=[0.1, 0.5]
意味着標籤0的權重是0.1, 標籤1的權重是0.5。如果該值爲None
,則所有的標籤具有相同的權重1.0
。train_logdir
:存放checkpoint
和logs
的路徑。log_steps
:該值表示每隔多少步輸出日誌信息。save_interval_secs
:該值表示以秒爲單位,每隔多長時間保存一次模型文件到硬盤。optimizer
:優化器,可選值['momentum', 'adam']
。learning_policy
:學習率策略,可選值['poly', 'step']
。base_learning_rate
:基礎學習率,默認值0.0001
。training_number_of_steps
:模型訓練的迭代次數。train_batch_size
:模型訓練的批處理圖像數量。train_crop_size
:模型訓練時所使用的圖像尺寸,默認'513, 513'
。tf_initial_checkpoint
:預訓練模型。initialize_last_layer
:是否初始化最後一層。last_layers_contain_logits_only
:是否只考慮邏輯層作爲最後一層。fine_tune_batch_norm
:是否微調batch norm
參數。atrous_rates
:默認值[6, 12, 18]
。output_stride
:默認值16
,輸入和輸出空間分辨率的比值- 對於
xception_65
, 如果output_stride=8
,則使用atrous_rates=[12, 24, 36]
- 如果
output_stride=16
,則atrous_rates=[6, 12, 18]
- 對於
mobilenet_v2
,使用None
- 注意:在訓練和驗證階段可以使用不同的
strous_rates
和output_stride
。
- 對於
dataset
:所使用的分割數據集,此處與數據集註冊時的名稱一致。train_split
:使用哪個數據集來訓練,可選值即數據集註冊時的值,如train
,trainval
。dataset_dir
:數據集存放的路徑。
針對訓練參數,下面幾點需要重點注意:
-
關於是否加載預訓練網絡的權重問題
如果要在其他數據集上微調該網絡,需要關注以下幾個參數:- 使用預訓練網絡的權重,設置
initialize_last_layer=True
- 只使用網絡的
backbone
,設置initialize_last_layer=False
和last_layers_contain_logits_only=False
- 使用所有的預訓練權重,除了
logits
,設置initialize_last_layer=False
和last_layers_contain_logits_only=True
由於我的數據集分類與默認類別數不同,因此採取的參數值是:
--initialize_last_layer=false --last_layers_contain_logits_only=true
- 使用預訓練網絡的權重,設置
-
如果資源有限,想要訓練自己數據集的幾條建議:
- 設置
output_stride=16
或者甚至32
(同時需要修改atrous_rates
變量,例如,對於output_stride=32
,atrous_rates=[3, 6, 9]
) - 儘可能多的使用
GPU
,更改num_clone
標誌,並將train_batch_size
設置的儘可能大 - 調整
train_crop_size
,可以將它設置的更小一些,例如513x513
(甚至321x321
),這樣就可以使用更大的batch_size
- 使用較小的網絡主幹,如
mobilenet_v2
- 設置
-
關於是否微調
batch_norm
當訓練使用的批處理大小train_batch_size
大於12(最好大於16)時,設置fine_tune_batch_norm=True
。否則,設置fine_tune_batch_norm=False
。
4.3 預訓練模型
模型鏈接具體可見:models/model_zoo.md at master · tensorflow/models
提供了在幾個數據集上的預訓練模型,包括(1) PASCAL VOC 2012
, (2) Cityscapes
, (3) ADE20K
未解壓的目下包括:
- 一個
frozen inference graph
(forzen_inference_graph.pb
)。默認情況下,所有凍結推理圖的輸出步長爲8,單個eval scale爲1.0,沒有左右翻轉,除非另外指定。基於MobileNet-v2
的模型不包括解碼器模塊。 - 一個
checkpoint
(model.ckpt.data-00000-of-00001
,model.ckpt.index
)
還提供了在ImageNet
預訓練的checkpoints
未解壓文件包括:
一個model checkpoint (model.ckpt.data-00000-of-00001, model.ckpt.index)
根據自己的情況進行下載
4.4 訓練模型
python train.py \
--logtostderr \
--training_number_of_steps=20000 \
--train_split="train" \
--model_variant="xception_65" \
--train_crop_size="513,513" \
--atrous_rates=6 \
--atrous_rates=12 \
--atrous_rates=18 \
--output_stride=16 \
--decoder_output_stride=4 \
--train_batch_size=2 \
--save_interval_secs=240 \
--optimizer="momentum" \
--leraning_policy="poly" \
--fine_tune_batch_norm=false \
--initialize_last_layer=false \
--last_layers_contain_logits_only=true \
--dataset="portrait_seg" \
--tf_initial_checkpoint="./checkpoint/deeplabv3_pascal_trainval/model.ckpt" \
--train_logdir="./train_logs" \
--dataset_dir="./dataset/tfrecord"
4.5 驗證模型
驗證代碼: ./eval.py
# From tensorflow/models/research/
python deeplab/eval.py \
--logtostderr \
--eval_split="val" \
--model_variant="xception_65" \
--atrous_rates=6 \
--atrous_rates=12 \
--atrous_rates=18 \
--output_stride=16 \
--decoder_output_stride=4 \
--eval_crop_size="513,513" \
--dataset="portrait_seg" \ # 數據集名稱
--checkpoint_dir=${PATH_TO_CHECKPOINT} \ # 預訓練模型
--eval_logdir=${PATH_TO_EVAL_DIR} \
--dataset_dir="./dataset/tfrecord" # 數據集路徑
得到的結果如下:
4.6 訓練過程可視化
可以使用Tensorboard
檢查培訓和評估工作的進展。如果使用推薦的目錄結構,Tensorboard可以使用以下命令運行:
tensorboard --logdir=${PATH_TO_LOG_DIRECTORY}
# 文中log地址
tensorboard --logdir="./train_logs"
5、推理
5.1 模型導出
在訓練過程中,會保存模型文件到硬盤,如下:
其形式是TensorFlow
的checkpoint
格式,代碼中提供了一個腳本(export_model.py
)可以將checkpoint
轉換爲.pb
格式。
export_model.py
主要參數:
checkpoint_path
:訓練保存的檢查點文件export_path
:模型導出路徑num_classes
:分類類別crop_size
:圖像尺寸,[513, 513]
atrous_rates
:12, 24, 36
output_stride
:8
生成的.pb
文件如下:
5.2 單張圖像上推理
class DeepLabModel(object):
"""class to load deeplab model and run inference"""
INPUT_TENSOR_NAME = 'ImageTensor:0'
OUTPUT_TENSOR_NAME='SemanticPredictions:0'
INPUT_SIZE = 513
FROZEN_GRAPH_NAME= 'frozen_inference_graph'
def __init__(self, pretrained_weights):
"""Creates and loads pretrained deeplab model."""
self.graph = tf.Graph()
graph_def = None
# Extract frozen graph from tar archive
if pretrained_weights.endswith('.tar.gz'):
tar_file = tarfile.open(pretrained_weights)
for tar_info in tar_file.getmembers():
if self.FROZEN_GRAPH_NAME in os.path.basename(tar_info.name):
file_handle = tar_file.extractfile(tar_info)
graph_def = tf.GraphDef.FromString(file_handle.read())
break
tar_file.close()
else:
with open(pretrained_weights, 'rb') as fd:
graph_def = tf.GraphDef.FromString(fd.read())
if graph_def is None:
raise RuntimeError('Cannot find inference graph in tar archive.')
with self.graph.as_default():
tf.import_graph_def(graph_def, name='')
gpu_options = tf.GPUOptions(allow_growth=True)
config = tf.ConfigProto(gpu_options=gpu_options, log_device_placement=False)
self.sess = tf.Session(graph=self.graph, config=config)
def run(self, image):
"""Runs inference on a single image.
Args:
image: A PIL.Image object, raw input image.
Returns:
resized_image:RGB image resized from original input image.
seg_map:Segmentation map of 'resized_iamge'.
"""
width, height = image.size
resize_ratio = 1.0 * self.INPUT_SIZE/max(width, height)
target_size = (int(resize_ratio*width), int(resize_ratio * height))
resized_image = image.convert('RGB').resize(target_size, Image.ANTIALIAS)
batch_seg_map = self.sess.run(
self.OUTPUT_TENSOR_NAME,
feed_dict={self.INPUT_TENSOR_NAME:[np.asarray(resized_image)]}
)
seg_map = batch_seg_map[0]
return resized_image, seg_map
if __name__ == '__main__':
pretrained_weights = './train_logs/frozen_inference_graph_20000.pb'
MODEL = DeepLabModel(pretrained_weights) # 加載模型
img_name = 'test.jpg'
img = Image.open(img_name)
resized_im, seg_map = MODEL.run(original_im) #獲取結果
seg_map[seg_map==1]=255 #將人像的像素值置爲255
seg_map.save('output.jpg') # 保存mask結果圖像
至此,整個訓練過程就結束了!!!