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
) поддерживают побатчевое обучение. Для формирования батчей (а заодно и предварительной обработки данных в них) и подойдет наша библиотека.
В заголовке затронута важная тема. Но решение, на самом деле не предоставлено.
Поэтому пользуясь случаем, обращаюсь к сообществу. Просто мой крик души.
Пожалуйста, если вы занимаетесь машинным обучением и пишите код, который будут читать другие люди, следуйте простым и общеизвестным правилам. Особенно, если используете скриптовые языки без строгой типизации, такие как Python и Lua.
В частности,
1) не используйте магические константы (64 в статье).
2) используйте самоназывающие имена переменных (имя переменной 'ksi' и ссылка на статью, где эта переменная расшифровывается — не вариант).
3) не экономьте место, разделяйте блоки на логические части.
4) комментируйте код.
5) (самое важное) аннотируйте сигнатуры функций и методов — параметры и возврощаемые значения
Почему?
Я согласен, что код следует писать хорошо, и что часто академический код ужасен. Но делать код очень аккуратным в исследовательском проекте — пустая трата времени.
Почему магические константы (как в данном случае 64) — это плохо? Я согласен, что повторяющие захардкоженнные константы — это плохо. Потому, что поменяв ее в одном месте, можно забыть поменять в другом.
Конкретно тут она используется в одном месте. Лучше было ее сделать именованной константой и вынести в начало скрипта? Кажется, что нет.
А чем 'ksi' лучше/хуже имени 'loss'? Если человека реализовывает алгоритм по статье — лучше пусть он следует обозначениям статьи, нежели придумывает свои длинные длинные, но интерпретируемые названия. В оригинальных обозначениях можно хотя бы со статьей сверяться.
Ну тут ответ очень стандартный. Никаких предметно-специфических вещей не добавляется.
Главная причина — код пишут для людей, а не для компьютеров.
Рано или поздно ваш код прочитает ваш коллега. И если вы писали для людей, то получите меньше проклятий в свой адрес. Вы сами через пол года можете забыть что означала эта ksi
.
Вам или вашему коллеге может быть просто нужно внести небольшие изменения в код — добавить новые фичи. И тут выясняется что для этого вам нужно прочитать статью в 20 страниц, на которую у вас прямо сейчас просто нет времени. И все это только для того что бы понять что же на самом деле скрывается за именем пременной ksi
.
Если вы хардкодите константы в коде, то у того, кто будет читать код не может быть уверенности в том что таже самая константа не захардкожена в другом месте. И как её менять? Просматривать весь код на наличие константы 64
? А вдруг в другом месте другая 64
? А что это вообще за 64? Ах, да! нужно прочитать статью, которая прилагается в pdf файле к коду.
На хабре тема write-only кода поднималась тысячу раз. Мне даже как-то неловко поднимать её в 1001-й раз.
Потому что в коде написанном на 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, и ещё:
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
собираются все коэффициенты по всем обработанным сигналам. Если сигналов слишком много, то можно их побатчево записывать на диск.
Data science и качественный код