Skip to content

Commit ce46d70

Browse files
authored
memory efficient test (open-mmlab#330)
* memory efficient test * implement efficient test * merge * Add document and docstring * fix unit test * add memory usage report
1 parent 8ed47ab commit ce46d70

File tree

6 files changed

+187
-67
lines changed

6 files changed

+187
-67
lines changed

docs/inference.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Optional arguments:
2525
- `EVAL_METRICS`: Items to be evaluated on the results. Allowed values depend on the dataset, e.g., `mIoU` is available for all dataset. Cityscapes could be evaluated by `cityscapes` as well as standard `mIoU` metrics.
2626
- `--show`: If specified, segmentation results will be plotted on the images and shown in a new window. It is only applicable to single GPU testing and used for debugging and visualization. Please make sure that GUI is available in your environment, otherwise you may encounter the error like `cannot connect to X server`.
2727
- `--show-dir`: If specified, segmentation results will be plotted on the images and saved to the specified directory. It is only applicable to single GPU testing and used for debugging and visualization. You do NOT need a GUI available in your environment for using this option.
28+
- `--eval-options`: Optional parameters during evaluation. When `efficient_test=True`, it will save intermediate results to local files to save CPU memory. Make sure that you have enough local storage space (more than 20GB).
2829

2930
Examples:
3031

@@ -86,3 +87,15 @@ Assume that you have already downloaded the checkpoints to the directory `checkp
8687

8788
You will get png files under `./pspnet_test_results` directory.
8889
You may run `zip -r results.zip pspnet_test_results/` and submit the zip file to [evaluation server](https://www.cityscapes-dataset.com/submit/).
90+
91+
6. CPU memory efficient test DeeplabV3+ on Cityscapes (without saving the test results) and evaluate the mIoU.
92+
93+
```shell
94+
python tools/test.py \
95+
configs/deeplabv3plus/deeplabv3plus_r18-d8_512x1024_80k_cityscapes.py \
96+
deeplabv3plus_r18-d8_512x1024_80k_cityscapes_20201226_080942-cff257fe.pth \
97+
--eval-options efficient_test=True \
98+
--eval mIoU
99+
```
100+
101+
Using ```pmap``` to view CPU memory footprint, it used 2.25GB CPU memory with ```efficient_test=True``` and 11.06GB CPU memory with ```efficient_test=False``` . This optional parameter can save a lot of memory.

mmseg/apis/test.py

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,48 @@
44
import tempfile
55

66
import mmcv
7+
import numpy as np
78
import torch
89
import torch.distributed as dist
910
from mmcv.image import tensor2imgs
1011
from mmcv.runner import get_dist_info
1112

1213

13-
def single_gpu_test(model, data_loader, show=False, out_dir=None):
14+
def np2tmp(array, temp_file_name=None):
15+
"""Save ndarray to local numpy file.
16+
17+
Args:
18+
array (ndarray): Ndarray to save.
19+
temp_file_name (str): Numpy file name. If 'temp_file_name=None', this
20+
function will generate a file name with tempfile.NamedTemporaryFile
21+
to save ndarray. Default: None.
22+
23+
Returns:
24+
str: The numpy file name.
25+
"""
26+
27+
if temp_file_name is None:
28+
temp_file_name = tempfile.NamedTemporaryFile(
29+
suffix='.npy', delete=False).name
30+
np.save(temp_file_name, array)
31+
return temp_file_name
32+
33+
34+
def single_gpu_test(model,
35+
data_loader,
36+
show=False,
37+
out_dir=None,
38+
efficient_test=False):
1439
"""Test with single GPU.
1540
1641
Args:
1742
model (nn.Module): Model to be tested.
18-
data_loader (nn.Dataloader): Pytorch data loader.
43+
data_loader (utils.data.Dataloader): Pytorch data loader.
1944
show (bool): Whether show results during infernece. Default: False.
20-
out_dir (str, optional): If specified, the results will be dumped
21-
into the directory to save output results.
45+
out_dir (str, optional): If specified, the results will be dumped into
46+
the directory to save output results.
47+
efficient_test (bool): Whether save the results as local numpy files to
48+
save CPU memory during evaluation. Default: False.
2249
2350
Returns:
2451
list: The prediction results.
@@ -31,10 +58,6 @@ def single_gpu_test(model, data_loader, show=False, out_dir=None):
3158
for i, data in enumerate(data_loader):
3259
with torch.no_grad():
3360
result = model(return_loss=False, **data)
34-
if isinstance(result, list):
35-
results.extend(result)
36-
else:
37-
results.append(result)
3861

3962
if show or out_dir:
4063
img_tensor = data['img'][0]
@@ -61,13 +84,26 @@ def single_gpu_test(model, data_loader, show=False, out_dir=None):
6184
show=show,
6285
out_file=out_file)
6386

87+
if isinstance(result, list):
88+
if efficient_test:
89+
result = [np2tmp(_) for _ in result]
90+
results.extend(result)
91+
else:
92+
if efficient_test:
93+
result = np2tmp(result)
94+
results.append(result)
95+
6496
batch_size = data['img'][0].size(0)
6597
for _ in range(batch_size):
6698
prog_bar.update()
6799
return results
68100

69101

70-
def multi_gpu_test(model, data_loader, tmpdir=None, gpu_collect=False):
102+
def multi_gpu_test(model,
103+
data_loader,
104+
tmpdir=None,
105+
gpu_collect=False,
106+
efficient_test=False):
71107
"""Test model with multiple gpus.
72108
73109
This method tests model with multiple gpus and collects the results
@@ -78,10 +114,12 @@ def multi_gpu_test(model, data_loader, tmpdir=None, gpu_collect=False):
78114
79115
Args:
80116
model (nn.Module): Model to be tested.
81-
data_loader (nn.Dataloader): Pytorch data loader.
117+
data_loader (utils.data.Dataloader): Pytorch data loader.
82118
tmpdir (str): Path of directory to save the temporary results from
83119
different gpus under cpu mode.
84120
gpu_collect (bool): Option to use either gpu or cpu to collect results.
121+
efficient_test (bool): Whether save the results as local numpy files to
122+
save CPU memory during evaluation. Default: False.
85123
86124
Returns:
87125
list: The prediction results.
@@ -96,9 +134,14 @@ def multi_gpu_test(model, data_loader, tmpdir=None, gpu_collect=False):
96134
for i, data in enumerate(data_loader):
97135
with torch.no_grad():
98136
result = model(return_loss=False, rescale=True, **data)
137+
99138
if isinstance(result, list):
139+
if efficient_test:
140+
result = [np2tmp(_) for _ in result]
100141
results.extend(result)
101142
else:
143+
if efficient_test:
144+
result = np2tmp(result)
102145
results.append(result)
103146

104147
if rank == 0:

mmseg/core/evaluation/metrics.py

Lines changed: 88 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,49 @@
1+
import mmcv
12
import numpy as np
23

34

4-
def intersect_and_union(pred_label, label, num_classes, ignore_index):
5+
def intersect_and_union(pred_label,
6+
label,
7+
num_classes,
8+
ignore_index,
9+
label_map=dict(),
10+
reduce_zero_label=False):
511
"""Calculate intersection and Union.
612
713
Args:
8-
pred_label (ndarray): Prediction segmentation map
9-
label (ndarray): Ground truth segmentation map
10-
num_classes (int): Number of categories
14+
pred_label (ndarray): Prediction segmentation map.
15+
label (ndarray): Ground truth segmentation map.
16+
num_classes (int): Number of categories.
1117
ignore_index (int): Index that will be ignored in evaluation.
18+
label_map (dict): Mapping old labels to new labels. The parameter will
19+
work only when label is str. Default: dict().
20+
reduce_zero_label (bool): Wether ignore zero label. The parameter will
21+
work only when label is str. Default: False.
1222
1323
Returns:
1424
ndarray: The intersection of prediction and ground truth histogram
15-
on all classes
25+
on all classes.
1626
ndarray: The union of prediction and ground truth histogram on all
17-
classes
27+
classes.
1828
ndarray: The prediction histogram on all classes.
1929
ndarray: The ground truth histogram on all classes.
2030
"""
2131

32+
if isinstance(pred_label, str):
33+
pred_label = np.load(pred_label)
34+
35+
if isinstance(label, str):
36+
label = mmcv.imread(label, flag='unchanged', backend='pillow')
37+
# modify if custom classes
38+
if label_map is not None:
39+
for old_id, new_id in label_map.items():
40+
label[label == old_id] = new_id
41+
if reduce_zero_label:
42+
# avoid using underflow conversion
43+
label[label == 0] = 255
44+
label = label - 1
45+
label[label == 254] = 255
46+
2247
mask = (label != ignore_index)
2348
pred_label = pred_label[mask]
2449
label = label[mask]
@@ -34,20 +59,27 @@ def intersect_and_union(pred_label, label, num_classes, ignore_index):
3459
return area_intersect, area_union, area_pred_label, area_label
3560

3661

37-
def total_intersect_and_union(results, gt_seg_maps, num_classes, ignore_index):
62+
def total_intersect_and_union(results,
63+
gt_seg_maps,
64+
num_classes,
65+
ignore_index,
66+
label_map=dict(),
67+
reduce_zero_label=False):
3868
"""Calculate Total Intersection and Union.
3969
4070
Args:
41-
results (list[ndarray]): List of prediction segmentation maps
42-
gt_seg_maps (list[ndarray]): list of ground truth segmentation maps
43-
num_classes (int): Number of categories
71+
results (list[ndarray]): List of prediction segmentation maps.
72+
gt_seg_maps (list[ndarray]): list of ground truth segmentation maps.
73+
num_classes (int): Number of categories.
4474
ignore_index (int): Index that will be ignored in evaluation.
75+
label_map (dict): Mapping old labels to new labels. Default: dict().
76+
reduce_zero_label (bool): Wether ignore zero label. Default: False.
4577
4678
Returns:
4779
ndarray: The intersection of prediction and ground truth histogram
48-
on all classes
80+
on all classes.
4981
ndarray: The union of prediction and ground truth histogram on all
50-
classes
82+
classes.
5183
ndarray: The prediction histogram on all classes.
5284
ndarray: The ground truth histogram on all classes.
5385
"""
@@ -61,7 +93,7 @@ def total_intersect_and_union(results, gt_seg_maps, num_classes, ignore_index):
6193
for i in range(num_imgs):
6294
area_intersect, area_union, area_pred_label, area_label = \
6395
intersect_and_union(results[i], gt_seg_maps[i], num_classes,
64-
ignore_index=ignore_index)
96+
ignore_index, label_map, reduce_zero_label)
6597
total_area_intersect += area_intersect
6698
total_area_union += area_union
6799
total_area_pred_label += area_pred_label
@@ -70,21 +102,29 @@ def total_intersect_and_union(results, gt_seg_maps, num_classes, ignore_index):
70102
total_area_pred_label, total_area_label
71103

72104

73-
def mean_iou(results, gt_seg_maps, num_classes, ignore_index, nan_to_num=None):
105+
def mean_iou(results,
106+
gt_seg_maps,
107+
num_classes,
108+
ignore_index,
109+
nan_to_num=None,
110+
label_map=dict(),
111+
reduce_zero_label=False):
74112
"""Calculate Mean Intersection and Union (mIoU)
75113
76114
Args:
77-
results (list[ndarray]): List of prediction segmentation maps
78-
gt_seg_maps (list[ndarray]): list of ground truth segmentation maps
79-
num_classes (int): Number of categories
115+
results (list[ndarray]): List of prediction segmentation maps.
116+
gt_seg_maps (list[ndarray]): list of ground truth segmentation maps.
117+
num_classes (int): Number of categories.
80118
ignore_index (int): Index that will be ignored in evaluation.
81119
nan_to_num (int, optional): If specified, NaN values will be replaced
82120
by the numbers defined by the user. Default: None.
121+
label_map (dict): Mapping old labels to new labels. Default: dict().
122+
reduce_zero_label (bool): Wether ignore zero label. Default: False.
83123
84124
Returns:
85125
float: Overall accuracy on all images.
86-
ndarray: Per category accuracy, shape (num_classes, )
87-
ndarray: Per category IoU, shape (num_classes, )
126+
ndarray: Per category accuracy, shape (num_classes, ).
127+
ndarray: Per category IoU, shape (num_classes, ).
88128
"""
89129

90130
all_acc, acc, iou = eval_metrics(
@@ -93,29 +133,35 @@ def mean_iou(results, gt_seg_maps, num_classes, ignore_index, nan_to_num=None):
93133
num_classes=num_classes,
94134
ignore_index=ignore_index,
95135
metrics=['mIoU'],
96-
nan_to_num=nan_to_num)
136+
nan_to_num=nan_to_num,
137+
label_map=label_map,
138+
reduce_zero_label=reduce_zero_label)
97139
return all_acc, acc, iou
98140

99141

100142
def mean_dice(results,
101143
gt_seg_maps,
102144
num_classes,
103145
ignore_index,
104-
nan_to_num=None):
146+
nan_to_num=None,
147+
label_map=dict(),
148+
reduce_zero_label=False):
105149
"""Calculate Mean Dice (mDice)
106150
107151
Args:
108-
results (list[ndarray]): List of prediction segmentation maps
109-
gt_seg_maps (list[ndarray]): list of ground truth segmentation maps
110-
num_classes (int): Number of categories
152+
results (list[ndarray]): List of prediction segmentation maps.
153+
gt_seg_maps (list[ndarray]): list of ground truth segmentation maps.
154+
num_classes (int): Number of categories.
111155
ignore_index (int): Index that will be ignored in evaluation.
112156
nan_to_num (int, optional): If specified, NaN values will be replaced
113157
by the numbers defined by the user. Default: None.
158+
label_map (dict): Mapping old labels to new labels. Default: dict().
159+
reduce_zero_label (bool): Wether ignore zero label. Default: False.
114160
115161
Returns:
116162
float: Overall accuracy on all images.
117-
ndarray: Per category accuracy, shape (num_classes, )
118-
ndarray: Per category dice, shape (num_classes, )
163+
ndarray: Per category accuracy, shape (num_classes, ).
164+
ndarray: Per category dice, shape (num_classes, ).
119165
"""
120166

121167
all_acc, acc, dice = eval_metrics(
@@ -124,7 +170,9 @@ def mean_dice(results,
124170
num_classes=num_classes,
125171
ignore_index=ignore_index,
126172
metrics=['mDice'],
127-
nan_to_num=nan_to_num)
173+
nan_to_num=nan_to_num,
174+
label_map=label_map,
175+
reduce_zero_label=reduce_zero_label)
128176
return all_acc, acc, dice
129177

130178

@@ -133,20 +181,24 @@ def eval_metrics(results,
133181
num_classes,
134182
ignore_index,
135183
metrics=['mIoU'],
136-
nan_to_num=None):
184+
nan_to_num=None,
185+
label_map=dict(),
186+
reduce_zero_label=False):
137187
"""Calculate evaluation metrics
138188
Args:
139-
results (list[ndarray]): List of prediction segmentation maps
140-
gt_seg_maps (list[ndarray]): list of ground truth segmentation maps
141-
num_classes (int): Number of categories
189+
results (list[ndarray]): List of prediction segmentation maps.
190+
gt_seg_maps (list[ndarray]): list of ground truth segmentation maps.
191+
num_classes (int): Number of categories.
142192
ignore_index (int): Index that will be ignored in evaluation.
143193
metrics (list[str] | str): Metrics to be evaluated, 'mIoU' and 'mDice'.
144194
nan_to_num (int, optional): If specified, NaN values will be replaced
145195
by the numbers defined by the user. Default: None.
196+
label_map (dict): Mapping old labels to new labels. Default: dict().
197+
reduce_zero_label (bool): Wether ignore zero label. Default: False.
146198
Returns:
147199
float: Overall accuracy on all images.
148-
ndarray: Per category accuracy, shape (num_classes, )
149-
ndarray: Per category evalution metrics, shape (num_classes, )
200+
ndarray: Per category accuracy, shape (num_classes, ).
201+
ndarray: Per category evalution metrics, shape (num_classes, ).
150202
"""
151203

152204
if isinstance(metrics, str):
@@ -156,8 +208,9 @@ def eval_metrics(results,
156208
raise KeyError('metrics {} is not supported'.format(metrics))
157209
total_area_intersect, total_area_union, total_area_pred_label, \
158210
total_area_label = total_intersect_and_union(results, gt_seg_maps,
159-
num_classes,
160-
ignore_index=ignore_index)
211+
num_classes, ignore_index,
212+
label_map,
213+
reduce_zero_label)
161214
all_acc = total_area_intersect.sum() / total_area_label.sum()
162215
acc = total_area_intersect / total_area_label
163216
ret_metrics = [all_acc, acc]

0 commit comments

Comments
 (0)