Предположение
Вы спрашиваете конкретно о одномерном случае, поэтому мы решим одномерный случай здесь, но метод для 2D в основном такой же.
Допустим, у вас есть два основанияограничивающие истину блоки: блок 1 и блок 2.
Далее, давайте предположим, что наша модель не так уж велика и предсказывает более 2 блоков (возможно, она нашла что-то новое, возможно, она разбила одну коробку на две).
Для этой демонстрации давайте рассмотрим, с чем мы работаем:
# labels
# box 1: x----y
# box 2: x++++y
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# x--------y x+++++++++++++++++++++++++++++y TRUTH
# a-----------b PRED 1, BOX 1
# a+++++++++++++++++b PRED 2, BOX 2
# a++++++++++++++++++++++++++++++++b PRED 3, BOX 2
Основная проблема
То, что вы хотите, в действительности означает оценку выравниванияваши прогнозы к целям .... но о нет!какие цели принадлежат каким прогнозам?
Выберите функцию выбора расстояния и соедините каждое предсказание с целью, основанной на этой функции.В этом случае я буду использовать модифицированное пересечение через объединение (IOU) для одномерного случая.Я выбрал эту функцию, так как хотел, чтобы PRED 2 и 3 из вышеприведенной диаграммы были выровнены в рамке 2.
Получив оценку для каждого прогноза, сопоставьте ее с целью, которая дала наилучший результат.
Теперь с парой предсказание-цель-один-к-одному вычислите все, что вы хотите.
Демонстрация с вышеприведенным предположением
из приведенных выше предположений:
pred_boxes = [
[4, 8],
[6, 12],
[5, 16]
]
true_boxes = [
[4, 7],
[10, 20]
]
1d версия пересечения по объединению:
def iou_1d(predicted_boundary, target_boundary):
'''Calculates the intersection over union (IOU) based on a span.
Notes:
boundaries are provided in the the form of [start, stop].
boundaries where start = stop are accepted
boundaries are assumed to be only in range [0, int < inf)
Args:
predicted_boundary (list): the [start, stop] of the predicted boundary
target_boundary (list): the ground truth [start, stop] for which to compare
Returns:
iou (float): the IOU bounded in [0, 1]
'''
p_lower, p_upper = predicted_boundary
t_lower, t_upper = target_boundary
# boundaries are in form [start, stop] and 0<= start <= stop
assert 0<= p_lower <= p_upper
assert 0<= t_lower <= t_upper
# no overlap, pred is too far left or pred is too far right
if p_upper < t_lower or p_lower > t_upper:
return 0
if predicted_boundary == target_boundary:
return 1
intersection_lower_bound = max(p_lower, t_lower)
intersection_upper_bound = min(p_upper, t_upper)
intersection = intersection_upper_bound - intersection_lower_bound
union = max(t_upper, p_upper) - min(t_lower, p_lower)
union = union if union != 0 else 1
return min(intersection / union, 1)
несколько простых помощников:
from math import sqrt
def euclidean(u, v):
return sqrt((u[0]-v[0])**2 + (u[1]-v[1])**2)
def mean(arr):
return sum(arr) / len(arr)
как мы выравниваем наши границы:
def align_1d(predicted_boundary, target_boundaries, alignment_scoring_fn=iou_1d, take=max):
'''Aligns predicted_bondary to the closest target_boundary based on the
alignment_scoring_fn
Args:
predicted_boundary (list): the predicted boundary in form of [start, stop]
target_boundaries (list): a list of all valid target boundaries each having
form [start, stop]
alignment_scoring_fn (function): a function taking two arguments each of
which is a list of two elements, the first assumed to be the predicted
boundary and the latter the target boundary. Should return a single number.
take (function): should either be min or max. Selects either the highest or
lower score according to the alignment_scoring_fn
Returns:
aligned_boundary (list): the aligned boundary in form [start, stop]
'''
scores = [
alignment_scoring_fn(predicted_boundary, target_boundary)
for target_boundary in target_boundaries
]
# boundary did not align to any boxes, use fallback scoring mechanism to break
# tie
if not any(scores):
scores = [
1 / euclidean(predicted_boundary, target_boundary)
for target_boundary in target_boundaries
]
aligned_index = scores.index(take(scores))
aligned = target_boundaries[aligned_index]
return aligned
как мырассчитать разницу:
def diff(u, v):
return [u[0] - v[0], u[1] - v[1]]
объединить все в одно:
def aligned_distance_1d(predicted_boundaries, target_boundaries, alignment_scoring_fn=iou_1d, take=max, distance_fn=diff, aggregate_fn=mean):
'''Returns the aggregated distance of predicted boundings boxes to their aligned bounding box based on alignment_scoring_fn and distance_fn
Args:
predicted_boundaries (list): a list of all valid target boundaries each
having form [start, stop]
target_boundaries (list): a list of all valid target boundaries each having
form [start, stop]
alignment_scoring_fn (function): a function taking two arguments each of
which is a list of two elements, the first assumed to be the predicted
boundary and the latter the target boundary. Should return a single number.
take (function): should either be min or max. Selects either the highest or
lower score according to the alignment_scoring_fn
distance_fn (function): a function taking two lists and should return a
single value.
aggregate_fn (function): a function taking a list of numbers (distances
calculated by distance_fn) and returns a single value (the aggregated
distance)
Returns:
aggregated_distnace (float): return the aggregated distance of the
aligned predicted_boundaries
aggregated_fn([distance_fn(pair) for pair in paired_boundaries(predicted_boundaries, target_boundaries)])
'''
paired = [
(predicted_boundary, align_1d(predicted_boundary, target_boundaries, alignment_scoring_fn))
for predicted_boundary in predicted_boundaries
]
distances = [distance_fn(*pair) for pair in paired]
aggregated = [aggregate_fn(error) for error in zip(*distances)]
return aggregated
run:
aligned_distance_1d(pred_boxes, true_boxes)
# [-3.0, -3.6666666666666665]
Обратите внимание, что для многих прогнозов и многих целей существует множество способовоптимизировать код.Здесь я разбил основные функциональные блоки, чтобы было понятно, что происходит.
Теперь это имеет смысл?Ну, так как я хотел, чтобы pred 2 и 3 были приведены в соответствие со вставкой 2, да, оба начала предшествуют истине и оба заканчиваются преждевременно.
Решение поставленного вопроса
копия вставила ваши примеры:
# "detected" objects
p_obj = [
[[2, 3], [8, 8]], # class 1
[[4, 4], [6, 7]], # class 2
[[0, 0]] # class 3
]
# true objects
t_obj = [
[[1, 3], [6, 9]], # class 1
[[4, 7]], # class 2
[[0, 0]] # class 3
]
, так как вы знаете блоки для каждого класса, это легко:
[
aligned_distance_1d(p_obj[cls_no], t_obj[cls_no])
for cls_no in range(len(t_obj))
]
# [[1.5, -0.5], [1.0, -1.5], [0.0, 0.0]]
Имеет ли смысл этот вывод?
Начиная с проверки работоспособности, давайте посмотрим на класс3. Среднее расстояние [start, stop] равно 0. Имеет смысл.
Как насчет класса 1?оба прогноза начинаются слишком поздно (2> 1, 8> 6), но только одно заканчивается слишком рано (8 <9).Это имеет смысл. </p>
Теперь давайте посмотрим на класс 2, поэтому кажется, что вы задали вопрос (больше прогнозов, чем целей).
Если бы мы нарисовали то, что подсказывает оценкабыло бы:
# 0 1 2 3 4 5 6 7 8 9
# ---------- # truth [4, 7]
# ++ # pred [4 + 1, 7 - 1.5]
Это не выглядит так здорово, но это всего лишь пример ...
Имеет ли это смысл?Да нет.Да, с точки зрения того, как мы рассчитали метрику.Один остановил 3 значения слишком рано, другой начал 2 слишком поздно.Нет в том смысле, что ни одно из ваших предсказаний на самом деле не охватывает значение 5, и все же эта метрика заставляет вас поверить, что это так ...
Заключение
Является ли это ошибочной метрикой?
Зависит от того, что вы используете для / пытаетесь показать.Однако, поскольку вы используете бинарную маску для генерации предсказанных вами границ, это немаловажный корень этой проблемы.Возможно, есть лучшая стратегия, чтобы получить границы от вероятностей вашего ярлыка.