Зачем тестировать код к статье в машинном обучении. Разбираем пример

В этой статье я расскажу на примере, как невнимание к коду может привести к неверному результату в исследовании данных. 

Есть один замечательный курс [1], где студенты учатся проводить исследования по машинному обучению. К завершению семестра студенты готовят статью. На лекциях им параллельно рассказывают, как это делать. Обычно подготовка статьи включает проведение эксперимента с данными. По результатам семестра несколько работ уже готовы к подаче в журнал.

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

Пока я просматривал работы за весну 2020 года, меня заинтересовал необычный график в работе “Анализ свойств ансамбля локально аппроксимирующих моделей” [2]. На графике с результатами эксперимента четко виден разрыв в значениях представленной зависимости. Но исходя из выбора исходных данных и свойств этой зависимости, разрыва в этом месте быть не должно.

Любопытно проверить, почему в работе получился такой неожиданный результат.

Повторение эксперимента

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

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

В работе много сделано для воспроизводимости результатов. Опубликован исходный код, доступен сам текст статьи, рецензия к нему и исходные данные [2]. Вот упомянутый ранее график с необычным разрывом. Аннотации мои.

 Для повторения на Google Colab берем из проекта тетрадку. Тетрадка помимо стандартных использует библиотеку MixtureLib из отдельного репозитория на GitHub [3]. MixtureLib как раз решает задачу подбора параметров нескольких функций. Библиотека документирована и снабжена автоматическими тестами. Код приводим частями, которые важны для понимания сути эксперимента.

!git clone https://github.com/andriygav/MixtureLib.git
!python3 -m pip install MixtureLib/src/.
from mixturelib.localmodels import EachModelLinear
from mixturelib.hypermodels import HyperExpertNN, HyperModelDirichlet
from mixturelib.mixture import MixtureEM

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

correlations = []
sigmas = [0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,
         1.0,0.03,0.04,0.05,0.06,0.07,0.08,0.09,
         0.15,0.25,0.35,0.45,0.55,0.65,0.75,0.85,
         0.95,0.11,0.12,0.13,0.14,0.15,0.22,0.27,
         0.32,0.37,0.42,0.47,0.52,0.57,0.62,0.67,
         0.72,0.77,0.82,0.87,0.92,0.97]

Смотрим дальше, вот этот код отвечает за создание экземпляров моделей EachModelLinear, параметры которых подбираем. Эти модели линейные. Еще здесь создается экземпляр класса MixtureEM и нейронная сеть HyperNetEM, которая и подбирает параметры моделей алгоритмом максимизации правдоподобия. Этот алгоритм итерационный, начинает со случайно выбранных исходных значений параметров и понемногу их уточняет. В таком случае, значения параметров моделей будут наиболее вероятными для заданных входных данных.

torch.random.manualseed(42)
first_model = EachModelLinear(input_dim=2)
second_model = EachModelLinear(input_dim=2)
list_of_models = [firstmodel, second_model]

HpMd = HyperExpertNN(inputdim=2, hiddendim=5,outputdim=2, epochs=1000)
mixture = MixtureEM(HyperParameters={'beta': 1.},
                    HyperModel=HpMd,
                    ListOfModels=listofmodels,
                    modeltype='sample')

Далее идет код расчета данных для нужного нам графика. Здесь цикл по сигмам. 

for i, sigma in enumerate(sigmas):
    #порождение данных
    x1 = np.random.normal(0, 1, (200, 1))
    x2 = np.random.normal(0, 1, (200, 1))
    y1 = np.array([f1(x) for x in x1])
    y2 = np.array([f2(x) for x in x2])
    s1 = np.random.normal(0, sigma, 200).reshape((200, 1))
    s2 = np.random.normal(0, sigma, 200).reshape((200, 1))
    X1 = np.hstack([x1, s1]) 
    X2 = np.hstack([s2, x2])
    X = np.vstack([X1, X2])
    Y = np.hstack([y1, y2])
    realsecondw = np.array([[10.], [0.]])
    realfirstw = np.array([[0.], [50.]])
    Xtr = torch.FloatTensor(X)
    Ytr = torch.FloatTensor(Y).view([-1,1])

    #обучение модели
    # …

На нашем графике показано, насколько похожи предсказываемые моделями значения в зависимости от величины шума сигма. Для расчета этого сходства в статье использована корреляция Пирсона. Далее в этом же цикле она и рассчитывается.

for i, sigma in enumerate(sigmas):
    #порождение данных
    # ... 
    
    #обучение модели
    torch.random.manual_seed(42)
    mixture.fit(X_tr, Y_tr)
    predicted_first_w = mixture.ListOfModels[0].W.numpy()
    predicted_second_w = mixture.ListOfModels[1].W.numpy()
    weights = []
    weights.append([predicted_first_w[0][0], predicted_first_w[1][0]])
    weights.append([predicted_second_w[0][0], predicted_second_w[1][0]])
    #расчет расстояния между оптимальными локальными моделями, ошибка ансамбля
    Y1 = X.dot(weights[0])
    Y2 = X.dot(weights[1])
    correlations.append(cor_Pearson(Y1, Y2))

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

Расчет без изменений в прямом направлении по списку сигм
Расчет без изменений в прямом направлении по списку сигм
Расчет в обратном направлении по списку сигм
Расчет в обратном направлении по списку сигм

Проведем еще один расчет, будем изменять сигму не от нуля к единице, а наоборот. Его результаты совсем другие. И это странно, так как результаты на графике не должны зависеть от порядка расчета при такой постановке эксперимента.

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

Что произошло

В методе fit() класса MixtureEM не очищаются параметры модели. Для новых значений сигмы параметры моделей начинают подбираться не заново, как должно быть в итеративном алгоритме, а начиная с предыдущих значений параметров модели. Алгоритм же итеративный и итерации можно продолжить. Это и происходит.

Почему же автор кода эксперимента этого не заметил? В документации к fit() не сказано про очищение параметров моделей. Среди автоматических тестов библиотеки MixtureLib нет теста, который проверяет fit() при нескольких вызовах. Вот ссылка на примеры тестов.

Однако, fit() очень напоминает аналогичную функцию fit() из scikit-learn, которая параметры модели задает заново. Для случая продолжения обучения итеративным алгоритмом есть partial_fit()(см. здесь).

Исправляем, создаем новые модели для каждой сигма, запускаем. Результаты не картинке ниже. Скачка нет, есть постепенное изменение.

Стоит отметить, что найденная ошибка не делает всю работу неверной. Но все случаи использования fit() следовало бы пересмотреть.

Как исправить и избежать

Сформулируем, какую пользу можно извлечь из рассмотрения данного случая.

  • Проводя расчеты с разными значениями параметров, проверьте, что запуски независимы. Скажем, перебираем список значений параметров в случайном порядке и сравниваем результаты.

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

  • Привлекайте интуицию и здравый смысл еще при планировании расчетов. Продумайте, какие результаты ожидаются от эксперимента, а какие явно будут указывать на ошибку?

  • Проверяйте процедуры проведения расчетов, циклы, итерации на корректность в известных и простых случаях. В условиях, когда заранее известен результат.

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

Ссылки и упоминаемые материалы

[1] Вот ссылка на курс “Моя первая научная статья”, учебная работа из которого исследована – https://m1p.org

[2] Здесь ссылка на репозиторий со статьей и кодом – https://github.com/Intelligent-Systems-Phystech/2020_Project-51

[3] Репозиторий библиотеки MixtureLib https://github.com/andriygav/MixtureLib

[4] Вот здесь исправленная тетрадка для повторения результатов из этой статьи – https://colab.research.google.com/drive/1DZoJN32EpTZVSi2N3BduRCRf-ZST8snP#scrollTo=1JopTLX4eMnX

P.S. Конечно, связались и обсудили найденное с авторами статьи и библиотеки MixtureLib. Ошибка подтвердилась. Статья сделана еще в марте, так что в декабре уже сложно восстановить как именно был получен исходный график и как проводился эксперимент. Дальше можно гадать, проводился ли эксперимент по частям. Особенно, если обратить внимание на отсутствие графиков в оригинальной тетрадке и вот такой список сигм:

sigmas = [0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,
          1.0,0.03,0.04,0.05,0.06,0.07,0.08,0.09,
          0.15,0.25,0.35,0.45,0.55,0.65,0.75,0.85,
          0.95,0.11,0.12,0.13,0.14,0.15,0.22,0.27,
          0.32,0.37,0.42,0.47,0.52,0.57,0.62,0.67,
          0.72,0.77,0.82,0.87,0.92,0.97]

Let’s block ads! (Why?)

Read More

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *