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

Когда я научился программировать, это расширило мой кругозор и изменило мой взгляд на мир. Изучение машинного обучения за последние несколько недель сделало то же самое. Это даже изменило мой взгляд на код. Что, если мы посмотрим на проектирование программной системы так же, как мы могли бы смотреть на проблему машинного обучения? Что, если требования, данные нам во время разработки, были нашими обучающими данными; дизайн, который мы придумываем, — это модель, которую мы обучаем. Любые новые требования будут новыми точками данных, и то, насколько легко наш дизайн может быть адаптирован к этим новым требованиям, является мерой того, насколько хорошо наш дизайн программного обеспечения обобщает. В этом контексте переоснащение дизайна программного обеспечения — это создание проекта программного обеспечения, который очень хорошо соответствует существующим требованиям, за счет того, что в будущем будет очень сложно добавлять новые функции.

Аналогия не идеальна (аналогии нет), но я видел (и был виновен в этом) множество проектов программного обеспечения, которые пересоответствовали требованиям исходной задачи. Часто, по иронии судьбы, такое переоснащение было сделано из-за ошибочного убеждения, что это облегчит добавление новых функций в будущем.

Лучшим примером этого, который я видел в последнее время, является распространение новых фреймворков. Вместо реализации функций, которые мы призваны реализовывать, мы, разработчики программного обеспечения, попадаем в ловушку: сначала изобретаем структуру, которая затем упрощает реализацию этих функций. Это часто требует гораздо больше работы, чем просто создание программы, которую нас просят сделать, но (мы рассуждаем), если мы сможем решить проблему в общем случае, это избавит нас от всей этой воображаемой будущей работы. (Иногда я на самом деле подозреваю, что это на самом деле результат поиска способа сделать нашу работу по разработке CRUD-приложений менее скучной, но я также чувствую, что должен быть лучший способ быть продуктивным и получать от этого удовольствие).

Более крайний пример — создание DSL. На предыдущей работе я создал свой собственный язык запросов для JSON RESTful API, который позволял запросам вписываться в URL-адрес. Синтаксис языка запросов был (что неудивительно) вдохновлен LISP (в то время я читал SICP). Сначала это оказалось здорово, потому что нам больше не нужно было создавать новую конечную точку на сервере всякий раз, когда мы писали новое представление на клиенте. Проблемы с этим подходом проявились намного позже, когда мы столкнулись с ограничениями самого языка запросов. В этот момент я мог бы обновить язык запросов или просто начать создавать новые конечные точки сервера по старинке. Мы выбрали последнее, потому что код, который сопоставлял запросы со структурой модели JSON, а затем обратно в SQL, уже был достаточно сложным. Теперь у нас был и этот язык запросов, и набор специальных конечных точек для запроса данных, а код, реализующий язык запросов (который был встроен в DAL всего приложения), был самой сложной частью всего приложения.

Реализация этого языка запросов и соответствующего DAL была одной из самых интересных инженерных задач, которые мне когда-либо приходилось решать на этой работе (у него почти не было накладных расходов по сравнению с необработанным SQL, он был защищен от атак путем внедрения и превосходил любую ORM, которую я пробовал в то время). время). Это также одно из проектных решений, о которых я сожалел больше всего, потому что время шло, а требования менялись, было очень сложно развивать дизайн DAL. Дизайн очень хорошо соответствовал первоначальным требованиям, и он даже хорошо подходил для добавления тех же функций, но он не мог обобщать.

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

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

Дизайн часто заключается в балансировании многих важных конкурирующих принципов. Простота важна, но насколько важна? Вот что я утверждаю из своего опыта: это важнее, чем SOLID. Это важнее, чем СУХОЙ. Это важнее, чем покрытие юнит-тестами. Это настолько важно, что почти любая иерархия наследования глубиной более одного уровня не стоит той сложности, которую она вносит. (Если эти идеи оскорбляют вас, я надеюсь, что они оскорбят вас достаточно, чтобы подумать об этом в следующий раз, когда вы попытаетесь использовать наследование).

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

- МАШИНА. Хор

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

Как мы можем сделать то же самое для проектов программного обеспечения? Одна вещь, которую я сделал при оценке предполагаемого проекта, — это попытка подумать о новых требованиях, которые могут быть предъявлены к нему в будущем. Затем я планирую, как добавить эти новые функции в существующий дизайн. Это часто приводило меня к настройке дизайна, чтобы удовлетворить эти новые воображаемые требования. Помимо того факта, что эти требования полностью составлены, у этого подхода есть еще одна проблема: поскольку новые требования становятся частью проекта, они не могут служить способом проверки проекта на соответствие будущим (неизвестным) требованиям. На практике мы не можем перепроектировать (или реорганизовать) всю кодовую базу каждый раз, когда нам приходит новое требование.

(Я еще не пробовал, но было бы очень интересно разделить требования к новой программной системе на «обучающий» и «тестовый» наборы. Затем вы бы разработали решение для требований в «обучающем» наборе. Если получившийся проект сложно изменить, чтобы он соответствовал требованиям «тестового» набора, вам придется отказаться от всего проекта и начать с нуля (возможно, с новым рандомизированным набором требований к обучению и тестированию). подскажите как я с этим справляюсь)

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

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