Skip to content

Commit c0c87ec

Browse files
tensorflower-gardenerfyangf
authored andcommitted
Internal change
PiperOrigin-RevId: 492000777
1 parent a3fc656 commit c0c87ec

File tree

2 files changed

+94
-32
lines changed

2 files changed

+94
-32
lines changed

official/vision/modeling/layers/edgetpu.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,8 @@ def shard_tensors(axis: int, block_size: int,
160160
def non_max_suppression_padded(boxes: tf.Tensor,
161161
scores: tf.Tensor,
162162
output_size: int,
163-
iou_threshold: float = 0.5) -> tf.Tensor:
163+
iou_threshold: float = 0.5,
164+
refinements: int = 0) -> tf.Tensor:
164165
"""Selects a subset of boxes which have highest score among IOU-similar boxes.
165166
166167
Prunes away boxes that have high intersection-over-union (IOU) overlap
@@ -190,8 +191,10 @@ def non_max_suppression_padded(boxes: tf.Tensor,
190191
representing a single score corresponding to each box (each row of boxes).
191192
output_size: A scalar integer `Tensor` representing the maximum number of
192193
boxes to be selected by non-max suppression.
193-
iou_threshold: A 0-D float tensor representing the threshold for deciding
194-
whether boxes overlap too much with respect to IOU.
194+
iou_threshold: A float representing the threshold for deciding whether boxes
195+
overlap too much with respect to IOU.
196+
refinements: A number of extra refinement steps to make result closer to
197+
original sequential NMS.
195198
196199
Returns:
197200
A 1-D+ integer `Tensor` of shape `[...batch_dims, output_size]` representing
@@ -211,7 +214,7 @@ def non_max_suppression_padded(boxes: tf.Tensor,
211214
for boxes_i, scores_i in shard_tensors(0, block, boxes, scores):
212215
indices.append(
213216
_non_max_suppression_as_is(boxes_i, scores_i, output_size,
214-
iou_threshold))
217+
iou_threshold, refinements))
215218
indices = tf.concat(indices, axis=0)
216219
return tf.reshape(indices, batch_shape + [output_size])
217220

@@ -266,7 +269,8 @@ def _refine_nms_graph_to_original_algorithm(better: tf.Tensor) -> tf.Tensor:
266269
def _non_max_suppression_as_is(boxes: tf.Tensor,
267270
scores: tf.Tensor,
268271
output_size: int,
269-
iou_threshold: float = 0.5) -> tf.Tensor:
272+
iou_threshold: float = 0.5,
273+
refinements: int = 0) -> tf.Tensor:
270274
"""Selects a subset of boxes which have highest score among IOU-similar boxes.
271275
272276
Args:
@@ -277,6 +281,8 @@ def _non_max_suppression_as_is(boxes: tf.Tensor,
277281
boxes to be selected by non-max suppression.
278282
iou_threshold: A 0-D float tensor representing the threshold for deciding
279283
whether boxes overlap too much with respect to IOU.
284+
refinements: A number of extra refinement steps to make result closer to
285+
original sequencial NMS.
280286
281287
Returns:
282288
A 1-D+ integer `Tensor` of shape `[...batch_dims, output_size]` representing
@@ -299,6 +305,9 @@ def _non_max_suppression_as_is(boxes: tf.Tensor,
299305
worse = _greater(relative_scores)
300306
same_later = _and(_same(relative_scores), _greater(relative_order))
301307
similar_worse_or_same_later = _and(similar, _or(worse, same_later))
308+
for _ in range(refinements):
309+
similar_worse_or_same_later = _refine_nms_graph_to_original_algorithm(
310+
similar_worse_or_same_later)
302311
prunable = _reduce_or(similar_worse_or_same_later, axis=-1)
303312
remaining = tf.constant(1, dtype=prunable.dtype) - prunable
304313
if scores.shape[0] is None:

official/vision/modeling/layers/edgetpu_test.py

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,75 @@ def _maximum_activation_size(model):
4444
return max_size
4545

4646

47+
def _deviation_and_margin(reference, valid, optimized):
48+
"""Returns deviation and margin between two batched sets of indices."""
49+
deviation_rate = 0
50+
min_union = reference.shape[1] + optimized.shape[1]
51+
runs = reference.shape[0]
52+
for run in range(runs):
53+
reference_slice = {*reference[run, :valid[run]].numpy().tolist()}
54+
optimized_slice = {*optimized[run].numpy().astype(int).tolist()} - {-1}
55+
union_size = len(optimized_slice | reference_slice)
56+
symdiff_size = len(optimized_slice ^ reference_slice)
57+
deviation_rate += symdiff_size / union_size
58+
min_union = min(min_union, union_size)
59+
deviation_rate = deviation_rate / runs
60+
# six sigma estimate via LLN theorem
61+
margin = 6 * (deviation_rate / np.sqrt(runs) + 1 / (runs * min_union))
62+
return deviation_rate, margin
63+
64+
4765
class NonMaxSuppressionTest(parameterized.TestCase, tf.test.TestCase):
4866

4967
def setUp(self):
5068
super().setUp()
5169
tf.random.set_seed(42)
5270

53-
@parameterized.parameters((16, 8, 200, 0.009), (31, 17, 100, 0.013),
54-
(71, 41, 100, 0.045), (150, 100, 100, 0.129),
55-
(300, 300, 100, 0.116), (600, 600, 50, 0.176))
56-
def test_reference_match(self, n, top, runs, max_deviation):
71+
def test_refinement_sample(self):
72+
"""Tests difference in NMS behaviours.
73+
74+
Runs on four boxes with following IOU table (only neighbours will qualify
75+
as similar boxes)
76+
77+
box | 0 | 1 | 2 | 3
78+
--- | ---- | ---- | ---- | ----
79+
0 | 1 | 7/13 | 1/4 | 1/19
80+
1 | 7/13 | 1 | 7/13 | 1/4
81+
2 | 1/4 | 7/13 | 1 | 7/13
82+
3 | 1/19 | 1/4 | 7/13 | 1
83+
84+
So 0 is best box, it eliminates 1, next is box 2 which is eleminated by 1
85+
if it is allowed (depending on number of refinements).
86+
"""
87+
boxes: tf.Tensor = tf.constant(
88+
[
89+
# y1, x1, y2, x2
90+
[0.0, 0.0, 1.0, 1.0],
91+
[0.0, 0.3, 1.0, 1.3],
92+
[0.0, 0.6, 1.0, 1.6],
93+
[0.0, 0.9, 1.0, 1.9],
94+
],
95+
dtype=tf.float32)
96+
scores: tf.Tensor = tf.constant([
97+
1.0,
98+
0.9,
99+
0.8,
100+
0.7,
101+
], dtype=tf.float32)
102+
self.assertAllEqual(
103+
edgetpu.non_max_suppression_padded(boxes, scores, 4, refinements=0),
104+
tf.constant([0.0, -1.0, -1.0, -1.0], dtype=tf.float32))
105+
self.assertAllEqual(
106+
edgetpu.non_max_suppression_padded(boxes, scores, 4, refinements=1),
107+
tf.constant([0.0, 2.0, -1.0, -1.0], dtype=tf.float32))
108+
109+
@parameterized.parameters((16, 8, 200, [0.009, 0.004, 0.004]),
110+
(31, 17, 100, [0.013, 0.004, 0.004]),
111+
(71, 41, 100, [0.045, 0.003, 0.002]),
112+
(150, 100, 100, [0.129, 0.010, 0.001]),
113+
(300, 300, 100, [0.116, 0.016, 0.002]),
114+
(600, 600, 50, [0.176, 0.032, 0.003]))
115+
def test_reference_match(self, n, top, runs, max_devs):
57116
"""Compares that new optimized method is close to reference method.
58117
59118
Runs two algorithms with same sets of input boxes and scores, and measures
@@ -71,32 +130,26 @@ def test_reference_match(self, n, top, runs, max_deviation):
71130
top: limit of output boxes count.
72131
runs: for the statistical testing number of runs to performs to avoid
73132
tests flakiness.
74-
max_deviation: mean limit on deviation between optimized and reference
75-
algorithms. Please read notes why this number may be set higher to avoid
76-
flaky testing.
133+
max_devs: series of mean limits on deviation between optimized and
134+
reference algorithms with different number of refinements. (Indexes of
135+
elements correspond to number of refinements) Please use margin based
136+
values proposed by failed test to avoid flaky testing.
77137
"""
78-
deviation_rate = 0
79-
min_union = 2*n
80138
boxes = random_boxes([runs, n])
81139
scores = tf.random.uniform(shape=[runs, n])
82-
test = edgetpu.non_max_suppression_padded(boxes, scores, top)
83-
for run in range(runs):
84-
reference = tf.image.non_max_suppression(boxes[run], scores[run], top)
85-
reference = {*reference.numpy().tolist()}
86-
optimized = {*test[run].numpy().astype(int).tolist()} - {-1}
87-
union_size = len(optimized | reference)
88-
deviation_rate += len(optimized ^ reference) / union_size
89-
min_union = min(min_union, union_size)
90-
deviation_rate = deviation_rate / runs
91-
# six sigma estimate via LLN theorem
92-
safe_margin = 6 * (deviation_rate / np.sqrt(runs) + 1/(runs*min_union))
93-
self.assertLess(
94-
deviation_rate,
95-
max_deviation,
96-
msg='Deviation rate between optimized and reference implementations is '
97-
'higher than expected. If you are tuning the test, recommended safe '
98-
'deviation rate is '
99-
f'{deviation_rate} + {safe_margin} = {deviation_rate + safe_margin}')
140+
reference, valid = tf.image.non_max_suppression_padded(
141+
boxes, scores, top, pad_to_max_output_size=True)
142+
for refinements, max_deviation in enumerate(max_devs):
143+
optimized = edgetpu.non_max_suppression_padded(
144+
boxes, scores, top, refinements=refinements)
145+
deviation, margin = _deviation_and_margin(reference, valid, optimized)
146+
self.assertLess(
147+
deviation,
148+
max_deviation,
149+
msg='Deviation rate between optimized and reference implementations is '
150+
'higher than expected. If you are tuning the test, recommended safe '
151+
'deviation rate is '
152+
f'{deviation} + {margin} = {deviation + margin}')
100153

101154
@parameterized.parameters(([16], 8), ([91, 150], 100), ([20, 20, 200], 10))
102155
def test_sharded_match(self, shape: list[int], top: int):

0 commit comments

Comments
 (0)