Pull to refresh

Comments 13

Из текста понятно, как это работает с датасетами.
А как с моделями?
Основная проблема таких подходов — они хорошо работают в накатанном сценарии использования. Шаг в сторону — и либо самому переписывать, либо обвешиваться callback'ами.

К слову, примерно так, как описано в тексте, работает класс Dataset в torch(torchnet)/pytorch.

Когда вам нужен новый функционал, то все равно придется писать новую функцию.
Только в данном случае это будет не callback, а еще один action-метод в том же самом классе (или в новом, если вам так удобнее).


Гибкость подхода заключается в том, что по цепочке action-методов текут батчи, а что уж с ними делать, решаете вы в каждом отдельном методе. А потом легко можете менять цепочку, не переписывая вообще ничего.
Даже обучение модели тоже можно запихнуть в action-метод.


some_dataset.pipeline()
      .load('/some/path', 'some-format')
      .normalize_whatever()
      .crop_anything_you_want()
      .train_neural_network(sess, opt)

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


some_dataset.pipeline()
      .load('/some/path', 'some-format')
      .normalize_whatever()
      .crop_anything_you_want()
      .encode_with_AE(ae)
      .train_neural_network(sess, opt)

А если вам потребуется отладочная инфа, то можно снова расширить цепочку:


some_dataset.pipeline()
      .load('/some/path', 'some-format')
      .normalize_whatever()
      .crop_anything_you_want()
      .encode_with_AE(ae)
      .print_debug_info(after_every=10)
      .train_neural_network(sess, opt)

Естественно, вам придется самостоятельно написать эти методы encode_with_AE и print_debug_info, потому что только вы знаете, что вы здесь хотите сделать.


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

Не совсем понятно, какие у этой библиотеки преимущества по сравнению с scikit-learn?

Она эффектно дополняет.
В scikit-learn есть набор моделей, и некоторые (например, SGDRegressor, MiniBatchKMeans или IncrementalPCA) поддерживают побатчевое обучение. Для формирования батчей (а заодно и предварительной обработки данных в них) и подойдет наша библиотека.

Согласен. Чего только не придумают что бы не пользоваться Pipeline'ами…

В заголовке затронута важная тема. Но решение, на самом деле не предоставлено.
Поэтому пользуясь случаем, обращаюсь к сообществу. Просто мой крик души.
Пожалуйста, если вы занимаетесь машинным обучением и пишите код, который будут читать другие люди, следуйте простым и общеизвестным правилам. Особенно, если используете скриптовые языки без строгой типизации, такие как Python и Lua.
В частности,
1) не используйте магические константы (64 в статье).
2) используйте самоназывающие имена переменных (имя переменной 'ksi' и ссылка на статью, где эта переменная расшифровывается — не вариант).
3) не экономьте место, разделяйте блоки на логические части.
4) комментируйте код.
5) (самое важное) аннотируйте сигнатуры функций и методов — параметры и возврощаемые значения

Нарываюсь на минуса, но все же.
Почему?
Я согласен, что код следует писать хорошо, и что часто академический код ужасен. Но делать код очень аккуратным в исследовательском проекте — пустая трата времени.

Почему магические константы (как в данном случае 64) — это плохо? Я согласен, что повторяющие захардкоженнные константы — это плохо. Потому, что поменяв ее в одном месте, можно забыть поменять в другом.
Конкретно тут она используется в одном месте. Лучше было ее сделать именованной константой и вынести в начало скрипта? Кажется, что нет.

А чем 'ksi' лучше/хуже имени 'loss'? Если человека реализовывает алгоритм по статье — лучше пусть он следует обозначениям статьи, нежели придумывает свои длинные длинные, но интерпретируемые названия. В оригинальных обозначениях можно хотя бы со статьей сверяться.

Ну тут ответ очень стандартный. Никаких предметно-специфических вещей не добавляется.
Главная причина — код пишут для людей, а не для компьютеров.
Рано или поздно ваш код прочитает ваш коллега. И если вы писали для людей, то получите меньше проклятий в свой адрес. Вы сами через пол года можете забыть что означала эта ksi.
Вам или вашему коллеге может быть просто нужно внести небольшие изменения в код — добавить новые фичи. И тут выясняется что для этого вам нужно прочитать статью в 20 страниц, на которую у вас прямо сейчас просто нет времени. И все это только для того что бы понять что же на самом деле скрывается за именем пременной ksi.
Если вы хардкодите константы в коде, то у того, кто будет читать код не может быть уверенности в том что таже самая константа не захардкожена в другом месте. И как её менять? Просматривать весь код на наличие константы 64? А вдруг в другом месте другая 64? А что это вообще за 64? Ах, да! нужно прочитать статью, которая прилагается в pdf файле к коду.


На хабре тема write-only кода поднималась тысячу раз. Мне даже как-то неловко поднимать её в 1001-й раз.

Потому что 99% кода написанного на Python это самые натуральные спагетти, такие, шо даже сам автор через пару месяцев не разберётся в своей лапше.
Потому что в коде написанном на Python постоянно происходит куча неявных преобразований и другой фигни, о которых автор кода даже не догадывается, ну а про тонны зарытых ошибок я вообще молчу.
Потому что этот код будут сопровождать и работать с ним другие люди, я, конечно, понимаю шо мне ничего не надо, лишь бы у соседа ничего не было, но какая-то культура должна быть…
Ну и потому что код на Питоне, это даже хуже кода на Матлабе (там хотя бы хоть какая-то проверка корректности тех же матричных операций присутствует).

Так что если у человека полностью отсутствует самодисциплина и аккуратность, то его даже близко нельзя подпускать к таким языкам, как Python и прочие ЖабаСкрипты.

На самом деле как были самыми главным сайнтифик языками C/C++ и Fortran, так они ими и остаются. Ну может еще Golang когда-нибудь взлетит.

Теперь по порядку. Магических и не общеупотребимых циферок в теле программы быть не должно в принципе. Например, вместо какой-то там 3.14..., должна быть константа Pi, а вместо циферки 64, которая постоянно встречается в этой статье в роли аргумента (а еще 256 и вообще, кто все эти цифры?), должна быть какая-то переменная с осмысленным именем, ибо я еще пока ниразу не встречал программиста-экстрасенса. Ну а если программист этого не понимает, ну я хз, наверное его стоит отправить в ссылку программировать на Паскале… Пока не научится.

Что касается имен, слышал я легенды, о том, что когда-то в древние-древние времена было ограничение на число символов в имени файла или в имени функции, но вроде бы к 21-ому веку эту проблему побороли. Так шо можно и больше 3-х букв использовать в именах: loss — норм, наверное это как-то связано с функцией потерь, а вот шо такое ksi — я даже хз, похоже на матерное слово… А может это маскировка под греческую букву, неизвестного назначения? Может угол какой-то? Конечно, доля смысла в использовании букв прям как в статье есть, только тогда потрудились описывать такие переменные комментариями в коде, как это принято в приличных статьях: where ksi — is random value, phi — is phase of oscillation. Я еще понимаю когда опять-таки используют общеупотребимых буквы, типа epsilon — диэлектрическая проницаемость, c-скорость света, h — постоянная планка… хотя код может быть посвящен термодинамике, где c уже окажется теплоемкостью…

От того, что вы явно объявите константу WIDTH = 64 или даже IMAGE_WIDTH = 64, не станет ни капли понятнее. Зато станет неудобнее, потому что константа объявлена в другом месте. И если нужно что-то изменить, то придется скакать по всему коду, а может и по разным файлам.


Если нужно пояснить, почему выбрано именно такое значение, то лучше написать комментарий в этом самом месте ("На GPU с памятью 12ГБ вмещаются батчи размером не более 32х256х256").


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


Точно также data science код — это не совсем обычный программистский код и требует специальных знаний.


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


mu = Normal(mu=tf.zeros([K, D]), sigma=tf.ones([K, D]))
sigma = InverseGamma(alpha=tf.ones([K, D]), beta=tf.ones([K, D]))
c = Categorical(logits=tf.zeros([N, K]))
x = Normal(mu=tf.gather(mu, c), sigma=tf.gather(sigma, c))

Точно также надо специально знать, что порядок осей в массивах запросто может быть [z, x, y] и это не нужно отдельно комментировать, потому что придется это комментировать в 100500 местах.


В общем, есть у дата-сайентологического программирования своя специфика. И поэтому "просто из принципа" переносить сюда правила программирования из других областей не получится.

Полностью поддерживаю комментарии rotor. Даже если вы делаете код не для production, думаете, что пишете только для себя, не считаете себя software developer, то, пожалуйста, следуйте общеизвестным правилам. Это просто элемент культуры, как например, обозначения по осям графика писать и выводить только значащие цифры.
В частности, следуйте пяти правилам, которые перечислил выше rotor, и ещё:
6) Пишите тесты.
Буквально несколько дней назад с коллегой сидели перед Jupyter тетрадкой, обсуждали графики и выдвигали гипотезы. Оказалось, что теряли время, так как в обработке была ошибка. Для реальных данных этой ошибки не видно, а на модельных/искусственных видно. Если бы заранее написали тест, то не потеряли бы время. А могли ошибку и не заметить. Так бы и отдали неправильные результаты.
Почему магические константы (как в данном случае 64) — это плохо?

Как правило, без констант в программах не обойтись. Плохо, когда эти константы не объяснены/ не обоснованы, когда они превращаются в "(black) magic numbers". Как понять, из каких соображений взято это число? Это результат экспериментов над какими-то данными, оно основано на априорной информации, на интуиции или, просто, случайное? Если данные/задача изменятся, из каких соображений надо менять это число? И так далее.
Кстати, у рецензентов научных статей словосочетание «magic numbers» является одним из самых «ругательных».

Спасибо за Ваши труды, библиотека интересная, хотелось бы её уже пощупать.
А как может выглядеть код, если нужно обрабатывать временные ряды? Например, нужно провести через фильтры поступающие данные и дообучить модель.

Если ряды одинаковой длины, то удобнее всего хранить их в батче в виде матрицы [длина батча, длина ряда] и тогда к вашим услугам все скоростные матричные операции и векторизация.
Если разной, то можно хранить в виде массива массивов (например, мы так ЭКГ храним: в одном исследовании сигнал может быть длиной 1000, а в другом 9000). И затем распараллеливаем с помощью numba.


Выглядит это примерно так:


class EcgBatch(Batch):
...
    @action
    @inbatch_parallel(post="make_batch", target='nogil')
    def fft(self, item, *args, **kwargs):
        # call fast numba implementation
        # ...
        return ecg_fft_array_for_1_signal

    def make_batch(self, list_of_arrs):
        return FFTBatch.from_array(np.concatenate(list_of_arrs))

# ...

ecg_res = ecg_dataset.pipeline()
               .load(None, 'wfdb')
               .low_filter()
               .fft()
               .dump(fft_arr, 'ndarray')

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

Sign up to leave a comment.

Articles