Метрика для оценки прогнозируемых ограничивающих рамок из семантической сегментации на уровне объекта вне обучения - PullRequest
0 голосов
/ 20 ноября 2018

Контекст

Для простоты представим, что мы выполняем семантическую сегментацию для серии изображений высотой в один пиксель шириной w с тремя каналами (r, g, b) с n метка классов.

Другими словами, одно изображение может выглядеть следующим образом:

img = [
    [r1, r2, ..., rw], # channel r
    [g1, g2, ..., gw], # channel g
    [b1, b2, ..., bw], # channel b
]

и иметь размеры [3, w].

, а затем для данного изображения с w=10 и n=3 его истинная метка может быть:

# ground "truth"
target = np.array([
  #0     1     2     3     4     5     6     7     8     9      # position
  [0,    1,    1,    1,    0,    0,    1,    1,    1,    1],    # class 1
  [0,    0,    0,    0,    1,    1,    1,    1,    0,    0],    # class 2
  [1,    0,    0,    0,    0,    0,    0,    0,    0,    0],    # class 3
])

, и наша модель может предсказать как вывод:

# prediction
output = np.array([
  #0     1     2     3     4     5     6     7     8     9      # position
  [0.11, 0.71, 0.98, 0.95, 0.20, 0.15, 0.81, 0.82, 0.95, 0.86], # class 1
  [0.13, 0.17, 0.05, 0.42, 0.92, 0.89, 0.93, 0.93, 0.67, 0.21], # class 2
  [0.99, 0.33, 0.20, 0.12, 0.15, 0.15, 0.20, 0.01, 0.02, 0.13], # class 3
])

для дальнейшей простоты, давайте преобразуем вывод нашей модели, преобразовав его в двоичную форму с помощьюотсечение 0.9

# binary mask with cutoff 0.9
b_mask = np.array([
  #0     1     2     3     4     5     6     7     8     9      # position
  [0,    0,    1,    1,    0,    0,    0,    0,    1,    0],    # class 1
  [0,    0,    0,    0,    1,    0,    1,    1,    0,    0],    # class 2
  [1,    0,    0,    0,    0,    0,    0,    0,    0,    0],    # class 3
])

Тогда, если бы мы смотрели на «объекты» каждого класса ограничивающие рамки (или в данном случае только границы, то есть [start, stop] пикселей) наши предсказанные объекты из двоичногомаска "вводит" объект:

# "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
] 

Вопрос

Если бы я хотел, чтобы метрика описывала точностьграниц в среднем по объекту, какой будет соответствующая метрика?

Я понимаю IOU в контексте обучения модели, которая предсказывает ограничивающие рамки, например, это объект длясравнение объектов, но что делать, когда одинобъект может быть разбит на несколько частей?

Цель

Мне бы хотелось, чтобы показатель, который в каждом классе давал мне что-то вроде этого:

class 1: [-1, 2]  # bounding boxes for class one, on average start one
                  # pixel before they should and end two pixels after 
                  # they should

class 2: [ 0, 3]  # bounding boxes for class two, on average start 
                  # exactly where they should and end three pixels  
                  # after they should

class 3: [ 3, -1] # bounding boxes for class three, on average start 
                  # three pixels after where they begin and end one 
                  # pixels too soon

, но я не уверен, как лучше всего подойти к этому, когдаодин объект разбит на несколько частей ...

1 Ответ

0 голосов
/ 21 ноября 2018

Предположение

Вы спрашиваете конкретно о одномерном случае, поэтому мы решим одномерный случай здесь, но метод для 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, и все же эта метрика заставляет вас поверить, что это так ...

Заключение

Является ли это ошибочной метрикой?

Зависит от того, что вы используете для / пытаетесь показать.Однако, поскольку вы используете бинарную маску для генерации предсказанных вами границ, это немаловажный корень этой проблемы.Возможно, есть лучшая стратегия, чтобы получить границы от вероятностей вашего ярлыка.

...