Первоначально опубликовано на http://blog.simiacryptus.com/2017/12/unit-testing-and-neural-networks.html

Важнейшей частью любого хорошего программного обеспечения является тестовый код. Это преуменьшение того, что тесты улучшают качество; они улучшают масштабируемость всего процесса разработки программного обеспечения. Тесты позволяют писать больше кода, быстрее, лучше. Одной из ведущих методологий тестирования является модульное тестирование: философия разбиения программного обеспечения на отдельные компоненты и тестирования каждого по отдельности. Оказывается, отличный кейс по проектированию юнит-тестов также оказался одной из актуальных тем сегодняшнего дня — искусственные нейронные сети.

Нейронные сети, известные под множеством названий и сокращений, представляют собой модульную структуру обработки данных, форма которой вдохновлена ​​биологическими нейронными сетями. Основным используемым алгоритмом/свойством обучения является обратное распространение; он позволяет итеративно улучшать (известное как градиентный спуск) множества свободных переменных в произвольно сложной сети для улучшения скалярного результата «пригодности» — процесс, также известный как машинное обучение.

Таким образом, ключевыми компонентами разработки программного обеспечения для нейронных сетей являются:

Сегодня речь пойдет о №1.

Требования к тестированию

Обратное распространение использует цепное правило для обеспечения композитивности, что позволяет строить произвольно сложные сети. Это означает, что каждый компонент должен предоставлять исполняемые реализации как многомерной функции, так и ее производной. Хотя на практике эта функция должна быть достаточно гладкой, в принципе функция произвольная и даже может иметь изломы. Однако большое требование состоит в том, чтобы производная согласовывалась с функционалом. Мы назовем это числовая согласованность.

Оказывается, эта библиотека компонентов может вырасти довольно большой. Только в моем личном исследовании у меня есть 70 слоев в текущей версии MindsEye. С такой большой библиотекой мне было не только трудно поддерживать качество, мне даже было трудно вспомнить, что должен делать тот или иной слой! Я стараюсь, чтобы названия не говорили сами за себя, но ясно, что нам нужна документация в дополнение к обеспечению качества.

Есть веские причины, по которым на момент написания этой статьи универсальный ИИ еще не захватил мир. Одна из основных причин заключается в том, что он еще недостаточно быстр — поэтому скорость часто определяет скорость разработки. Из этого следует, что нас интересуют контрольные показатели производительности, и их следует протестировать и задокументировать.

Чтобы получить более высокую производительность, особенно в области компонентов с ускорением на GPU, мы можем использовать более низкую точность, например 32 или даже 16-битные числа с плавающей запятой, или могут быть другие мелкие детали в том, как реализована функция. Обычно это нормально, но мы, безусловно, хотим следить за этим. Поэтому, когда мы проверяем поведение этой функции, мы ожидаем некоторую небольшую, но приемлемую ошибку от того, что мы считаем аналитически идеальным значением. В некоторых случаях эта ошибка может быть значительной, и мы хотели бы задокументировать эту точность чисел.

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

Реализация

Таким образом, мы хотим сделать следующее для каждого компонента:

  1. Документация
  2. Документируйте, что он делает
  3. Задокументируйте формат сериализации
  4. Тестирование того, что функция «ведет себя»
  5. Убедитесь, что это численно согласованная функция
  6. Убедитесь, что это точная функция
  7. Количественные ориентиры для постепенного улучшения
  8. Сравните производительность
  9. Сравните точность

Существует отличный программный шаблон, который можно использовать для выполнения требований к тестированию и документации с помощью одного и того же общего кода: блокнот. По сути, идея состоит в том, чтобы однопоточный скрипт выдавал отчет, в котором код чередуется с результатами оценки этого кода. Результатом является документ, созданный тестовым примером, содержащий демонстрацию самой современной реализации данного компонента. Например, он может просто оценить network.toJson() и задокументировать формат сериализации. В MindsEye мы используем специальную библиотеку блокнотов для создания отчетов в GitHub Markdown.

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

Так как же нам продемонстрировать, что делает компонент? Один очевидный ответ — просто распечатать некоторые примеры входных и выходных данных — это стандартная настройка нашего отчета. Если компонент сам по себе является сетью подкомпонентов, мы можем визуализировать сеть с помощью визуализации графа. Если компонент представляет собой простую одномерную функцию активации, график был бы удобен. Другой способ продемонстрировать, что делает компонент, — это сравнение; продемонстрировать, что он делает то же самое, что и другой компонент (т. е. эталонная реализация). Это особенно полезно в критически важных для производительности компонентах, которые могут иметь несколько реализаций, таких как уровень свертки.

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

Еще одна деталь, которую важно проверить, по крайней мере, в том, как реализован MindsEye, — это пакетная инвариантность. Благодаря программным оптимизациям оценки сети выполняются быстрее, если они выполняются в пакетном режиме. При обучении сеть обычно не будет обрабатывать одну строку данных за раз; он обрабатывается кусками некоторого удобоваримого, но большого размера партии. За некоторыми исключениями, компоненты должны вести себя одинаково независимо от того, было ли выполнение пакетным или по одному за раз.

Наконец, нам нужны количественные показатели, указывающие на спектр непрерывных улучшений, которые должны быть отражены в наших отчетах. Очевидно, что показатели производительности, которые мы хотели бы видеть, стремятся к нулю или к бесконечности; чтобы получить хорошие показатели производительности, мы просто измеряем, сколько времени занимает N выполнений. Другая цифра, которая часто описывает снижение производительности, — это числовая точность.

Заключение

Короче говоря, именно так я обеспечил качественную и масштабируемую разработку в MindsEye. Благодаря четко определенным и тестируемым функциональным контрактам огромная библиотека компонентов может поддерживаться общим тестовым кодом, требующим лишь минимальной реализации для каждого компонента. Это одно из скрытых требований к тестовому коду — он достаточно универсален, чтобы 1) быть очень простым в использовании и 2) не устаревать.

Спасибо за чтение, и хорошего дня!