Использование ансамбля сверточных нейронных сетей в fastai для прогнозирования covid на основе компьютерной томографии.

Прежде чем я объясню детали, вы можете посмотреть код здесь. Этот рабочий пример использует Набор данных КТ-сканирования SARS-COV-2 и пытается обнаружить covid на основе изображений компьютерной томографии. Вы можете скачать данные по этой ссылке или с моего диска.

Мотивация этой публикации — проанализировать производительность каждой модели с помощью interp.plot_top_losses. Я обнаружил, что при внесении небольших изменений в этап обучения (например, изменении количества эпох или добавлении /удаление элементов и пакетных преобразований) результаты interp.plot_top_lossesмного изменились. Под этим я подразумеваю, что, несмотря на то, что окончательные потери при проверке были примерно одинаковыми или менялись лишь незначительно, неправильно классифицированные изображения всегда были разными. Поэтому я подумал, а что, если я объединим две или более моделей и возьму среднее значение их прогнозов? Результаты были многообещающими, и в этой короткой статье я попытаюсь объяснить, как мне это удалось.

Но сначала класс Ensemble:

class Ensemble:
  def __init__(self, dls, models : dict, vocab : list=[0, 1]):
    self.models = models
    self.vocab = vocab
    self.dls = dls
    self.model_list = models.values()
    print(f'vocab: {self.vocab}')
    for name, model in models.items():
      print(f'loaded: {name}')


  def calc_probas(self, item):
    probas = []
    for _, model in self.models.items():
      _, _, p = model.predict(item)
      probas.append(p)
    
    probas = torch.stack(probas, dim=0)
    return probas

  def predict(self, item):
    probas = self.calc_probas(item)
    mean, std = probas.mean(axis=0), probas.std(axis=0)

    return self.vocab[mean.argmax()], mean.argmax(), std
  
  def get_preds(self, ds_idx=1, dl=None, with_input=False, with_decoded=False, with_loss=False, act=None, inner=False, reorder=True, cbs=None, **kwargs):
    
    if dl is None: dl = self.dls[1]

    predictions = []
    losses = []
    res = []

    for name, model in self.models.items():
      print(f'Getting predictions from {name} \n')
      inputs, preds, targs, decoded, loss = model.get_preds(dl=dls.valid, with_input=True, with_loss=True, with_decoded=True)
      predictions.append(preds)
      losses.append(loss)
    
    preds = torch.stack(predictions).mean(0)
    decoded = preds.argmax(1)
    
    if with_input:
      res.append(inputs)
    
    res.append(preds)
    res.append(targs)

    if with_decoded:
      res.append(decoded)
    
    if with_loss:
      res.append(torch.stack(losses, dim=1).mean(1))
    
    return tuple(res)

  def calc_metrics(self, metrics : dict):
    res = {}
    predictions, targs, decoded, losses = self.get_preds(dl=self.dls.valid, with_input=False, with_loss=True, with_decoded=True)
    for name, metric in metrics.items():
      res[name] = metric(decoded, targs)
    return res

Это не красиво, но это работает. Я разберу некоторые ключевые части кода, чтобы объяснить, как это работает.

Сначала я загружаю модели как набор пар ключ-значение. Это позволяет мне индексировать по имени модели, если я хочу получить доступ к определенной модели из ансамбля. Затем у меня есть predict, который берет один элемент и вызывает calc_probas. calc_probas в основном вызывает прогноз для каждой модели для переданного элемента, а затем складывает результаты каждого в тензор, чтобы мы могли усреднить по нулевому измерению.

Если есть три модели (и два класса), calc_probas возвращает что-то вроде этого:

[ [0.1 0.9
   0.2 0.8
   0.1 0.9] ] 

Где каждый столбец — это вероятность принадлежности к определенному классу, а каждая строка — результат модели. Если мы усредним по столбцам, мы получим:

[0.13 0.86]

Это то, что я делаю с результатом calc_probas. Затем я использую argmax, чтобы найти позицию максимума и индексировать список словарей по этой позиции.

Это все, что мне нужно, чтобы сделать вывод, но делать прогнозы на основе новых данных не очень полезно, если ансамбль хуже, чем отдельные модели, поэтому я нашел способ заставить класс Ensemble работать с ним. класс ClassificationInterpretationFastai. Это то, что делает get_preds. Как и прежде, я вызываю метод get_preds каждой модели, суммирую результаты и беру среднее значение, чтобы метод get_preds Ensemble возвращал что-то в та же форма, что и у Learner, get_preds.

Таким образом, я мог позвонить

interp = ClassificationInterpretation.from_learner(en, dl=dls.valid)

И посмотрите матрицу путаницы и максимальные потери.

Но так как я хотел знать фактическое значение метрики, я написал метод calc_metric. Он также вызывает get_preds и вызывает каждую переданную метрику, чтобы вычислить метрику с результатами и вернуть словарь, где ключами являются имя метрики и значение результатов.

en.calc_metrics({'F1Score': F1Score(), 'Recall': Recall()})

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

Если мы теперь посмотрим на результат ансамбля, мы увидим улучшение:

Мы видим, что ансамбль имеет наименьшее количество ложных отрицательных результатов, что эквивалентно утверждению, что у него самый высокий отзыв, очень важный показатель, когда речь идет о медицине. Но у него также немного лучший F1score.

Выводы

Достижения в области искусственного интеллекта и машинного обучения дают нам больше возможностей для решения сложных проблем. Объединение этих моделей для достижения лучших результатов не является чем-то новым, однако в fast.ai не было фреймворка для использования этих методов ансамбля. Я надеюсь, что это небольшое доказательство концепции поможет другим людям, использующим ансамбли с fast.ai, улучшить свои результаты.
Дайте мне знать, если вы найдете это полезным и хотели бы иметь возможность использовать его как отдельный пакет (например, : fast-ensemble), которые вы можете импортировать и использовать в своих проектах.