Содержание
Грань возможного
Выступая заказчиками какой-либо программы для компьютера, которая должна каким-то образом заменить или облегчить человеческий труд, люди часто не могут понять заранее насколько это в принципе целесообразно. Можно ли вообще хотеть от компьютера то или иное "умение"? Насколько затратным будет создание такой программы? Приведет ли результат к реальной экономии на практике? Каков, так сказать, профит? Хорошо если заказчик вообще подумал над этими вопросами прежде чем составлять техническое задание. Зачастую бывает так, что этими вопросами приходится задаваться в процессе, когда уже выполнена какая-то работа, изъят ресурс, уплачен аванс в конце концов. А бывает что и после приемки продукта оказывается что затея не стоила и ломаного гроша, поскольку применить продукт на практике оказывается совершенно невозможно.
Приведу пару довольно типичных примеров из своей практики. Поступает задача составить некий отчет. В техническом задании строго указаны поля, условия фильтрации, упорядочения, предоставлены входные данные и шаблоны вывода. С технической точки зрения все совершенно законно и, на первый взгляд, однозначно. Вопросы начинают возникать относительно затрат. Скажем, на выполнение такого задания у программиста уйдет два дня. Это не считая процедур подготовки ТЗ, проверки, тестирования, сдачи-приемки и прочих так или иначе связанных с этим действий. В сумме, ну, пусть будет грубо четыре человекодня на все про все. Готовая программа передается, наконец, конечному пользователю, тот запускает отчет, сдает его и... всё. Буквально один раз по однократной просьбе. Стоило ли это столько усилий более квалифицированных кадров, когда сам пользователь прекрасно мог справиться с этим отчетом вручную за, не знаю, день?
Более сложный пример. Нужно ежемесячно рассылать письма ряду клиентов примерно одинакового содержания, в которых по сути меняется только адресат. Из каких-нибудь маркетинговых соображений предлагается, помимо вставки в письма имен и адресов получателей, склонять слово "Уважаемый" на "Уважаемая" в зависимости от пола, а не писать уродливое "Уважаемый(-ая)". Какое же это уважение в конце концов? Опять же, желание кажется законным и обоснованным. Проблема возникает когда в исходных данных отсутствует прямое указание на пол или есть основания полагать, что данные в какой-то степени некорректны. Написать окончание с явной ошибкой будет уже чуть похуже, чем проявить относительное пренебрежение. А если этот человек действительно важен и уважаем? Появляется необходимость человеческого контроля за результатом работы программы, что сводит на нет практическую пользу от такой автоматизации труда. Проще будет переформулировать задачу таким образом, что бы раз и навсегда проверить исходные данные и откорректировать их вручную.
Если бы такие примеры возникали редко, или, хотя бы, не постоянно, то и не нужны были бы межотраслевые специалисты - программисты понимающие задачи бизнеса и профильные специалисты разбирающиеся в программировании. Все довольны: программисту незачем знать насколько часто будет использоваться его программа, пользователь тоже решил свою проблему и всем доволен. Беда в том, что это случается гораздо чаще чем этого бы хотелось и в проигрыше оказываются те, которые платят за работу и тем и другим. Возникает необходимость предварительной оценки трудоемкости того или иного пожелания и эффективности использования результата. Для решения этой проблемы, как правило, выделяется дополнительное звено - специалист аналитик, выступающий посредником между программистом и заказчиком. Но, мало того что это повышает и без того высокую стоимость конечного продукта, такого специалиста может просто не оказаться. Поэтому иногда полезно самому заказчику понимать программиста что бы наладить прямой диалог без дополнительных звеньев - чувствовать ту самую грань возможного и целесообразного.
Диалог человек-машина
Чем же отличается программист от специалиста из другой области знаний и почему бывает так сложно наладить взаимопонимание? Дело в том, что программист среди прочего понимает как устроена машина и как она работает - он может мыслить как машина - он сам является эдаким посредником между машиной и человеком. И некоторые вещи очевидные программисту могут показаться чем-то совсем непонятным гуманитарию, медику, экономисту или даже физику-теоретику. Мыслить в категориях алгоритмов это тот навык, который на самом деле и отличает настоящего программиста от того, кто просто выучил какую-нибудь умную аббревиатуру или даже знает какой-то язык программирования. На курсах по программированию вам постараются привить этот навык, но постольку поскольку. Во главе угла всегда останется умение проходить определенный тест на технический минимум, который в общем можно и вызубрить. Понимание сути остается в качестве дополнительной нагрузки ко всему прочему и зависит от самого слушателя и преследуемых им целей.
До данного момента мы пытались вникнуть в суть работы собственной головы на примере машины. Теперь пора попробовать разобраться как работает сама машина. С тем что из себя представляет компьютер в классическом фон Неймановом понимании мы знакомы еще с уроков информатики. Все что важно знать для нас это то, что такой компьютер предназначен конкретно для исполнения последовательности инструкций. Причем одной единственной последовательности. Об этом уже было упомянуто в предыдущей части в отношении так называемых тактов. Важно понимать, что машина может сделать только то, что можно выразить в виде последовательности отдельных операций помещения в и изъятия из памяти кусочков информации называемых байтами., а так же нескольких видов операций над ячейками памяти, и только над ними. Такие инструкции называются машинным кодом. (Разрядность 32, 64, 128 бит и т.д. вычислительного устройства или операционной системы это как раз количество в байтов - 4, 8, 16 соответственно - в одной операции).
Знание таких деталей может показаться слегка отвлеченным от реальности, но по большому счету именно понимание этого принципа помогает определить "трудоемкость" для компьютера определенного действия. Отображение на экране одной буквы, например, это десятки последовательных инструкций управления отдельными точками (пикселями). Отображение сотни символов - тысячи операций. Каждое действие выполняемое вами при помощи компьютера имеет свою стоимость - количественное выражение в виде порядка объема машинного кода требуемого на его исполнение. Программисты, по крайней мере не те, которые непосредственно работают с оборудованием, конечно не переводят в уме каждый раз все в машинный код и обратно, но они представляют, хорошо или плохо, стоимость одного действия или их комплекса. Как-раз таки, если сложно сходу представить сколько же потребуется операций для машины что бы выполнить то или иное действие - это однозначный признак того что оно плохо выполнимо или невыполнимо в принципе. Напротив, если заранее известен порядок количества операций, то задача хорошо поддается алгоритмизации и эффективно выполнима при помощи компьютера.
Класс относительно хорошо алгоритмизируемых задач мы знаем со школы. Это в первую очередь перебор и арифметика - то что, как говорится, доктор прописал. Чуть сложнее с манипуляциями над не числовыми данными - их надо сперва перевести в числа. Еще чуть больше сноровки от компьютера требуют операции над вещественными числами и геометрия. Для решения этих задач даже придумали такие специализированные устройства как математический сопроцессор и графический ускоритель, которые забирают эти операции на себя. Для решения ряда задач по математике, физике, экономике существуют специальные численные методы решения, которые хорошо адаптируются для компьютера. Эти методы подразумевают под собой приближенное вычисление тех или иных величин с указанной погрешностью, которой на практике можно пренебречь.
Самыми трудоемкими и плохо адаптируемыми для машин являются задачи анализа сложных открытых систем. Такие задачи появляются в прикладной физике, метеорологии, климатологии, макроэкономике, социологии, биологии, медицине, лингвистике и множестве других дисциплин. В эпидемиологии. Для решения таких задач строятся математические модели поведения этих систем - аналитические, эвристические, статистические. Математические модели затем реализуются в виде программ, которые предназначены лишь для того что бы с какой-то степенью достоверности предсказывать результаты в зависимости от изменяющихся параметров. А часто вовсе и просто для того что бы показать, что некая выбранная модель ошибочна. Впрочем, это вотчина скорее ученых в данной области знаний, чем программистов. Тут программисты как-раз нужны лишь на завершающем этапе конкретной реализации, чем при создании самих моделей.
Однако, как вы уже догадываетесь, существует еще один прием. Что если смоделировать не изучаемый процесс, а работу мозга? А модель этого мозга уже сама будет строить алгоритм пригодный для решения задачи.
Вычислительная сложность
Применительно к нашим пациентам была намеренно выбрана задача, которая как-раз очевидно некорректна с точки зрения эффективности. Сложность алгоритма находящего в последовательности чисел ближайшее к среднему подсчитать очень просто. Для вычисления среднего нам понадобится два цикла размером с длину последовательности с операциями счета и сложения, плюс одна операция деления. Затем остается перебором установить минимальную разность между результатом и каждым элементом, таким образом вычислив конкретный элемент. Считаем: допустим, последовательность состоит из N чисел, тогда количество условных атомарных операций сопоставимо с N*4+1. Если перевести на человеческий язык: сложность решения задачи зависит от количества входящих данных линейно. Обозначается это как O(n) - вычислительная сложность, иногда можно встретить такое обозначение в технической документации. Но это так, к сведению, вам не нужно самим вычислять этот параметр, это как-раз для специалиста. Более того, далеко не каждый программист владеет техникой формальной оценки сложности, часто это просто личный опыт.
Тем не менее, попробуем посчитать вычислительную сложность нашего нейронного алгоритма. Рассмотрим один скрытый слой. Каждый нейрон перемножает и суммирует сигнал каждого синапса на его вес: N*N. Далее каждая сумма подвергается извлечению из функции активации: N*N*N. Анализ точности результата тоже занимает порядка N операций: N^3+2N. То есть количество операций только для вывода одного результата зависит от куба количества задействованных в одном слое нейронов. Добавим еще один слой. Следующий слой вычисляет то же самое по результатам работы предыдущего слоя: (N^3+2N)*N. Вывод достаточно неутешительный: сложность вычислений зависит от количества нейронов задействованных в скрытых слоях в четвертой степени как минимум - О(N^4). И это еще ни слова не говоря об обучении.
Выбранный нами алгоритм расчета ближайшего к среднему числа, таким образом, мягко говоря, неэффективен.
Каждый дополнительный нейрон обходится нам очень дорого. Поэтому добавлять их наобум только ради увеличения точности мы не можем. И хоть нашей задачей является лишь демонстрация принципа, необходимо крепко подумать, сколько еще слоев или нейронов нужно, и стоит ли вообще, добавить к нашей нейронке дабы добиться лучшего результата.
Подаем горячее
В прошлой части мы договорились о том, что теоретически может быть достаточно и одного слоя. Один слой нам будет выдавать результат похожий на детскую игру "тепло-холодно". Нейронка будет пытаться угадать, какое из чисел далеко от среднего, а какое близко. Основываясь только на этой информации уже можно оценивать погрешность результата и пробовать настроить веса. С другой стороны нам не нужен такой результат - нейронка в лучшем случае будет делить последовательность на "теплые" и "холодные". Нам нужен результат обозначающий "горячо".
Попытаемся опять залезть обратно в собственную голову и предсказать, как бы мы могли извлечь из "теплых" результатов один "горячий". Как минимум, пограничные значения стоящие у края "теплого" и "холодного" можно забраковать сразу если это не единственный "теплый" вариант. Далее, развивая мысль, можно понять, что результат, который ближе по позиции к такому отброшенному тоже хуже, чем результат, стоящий ближе к другому "теплому". Очевидно, что для отсортированной последовательности, таким образом, "горячим" будет результат, стоящий где-то посередине "теплых". То есть мы можем добавить еще один слой, который бы оценивал результат именно по этому критерию.
Еще один нюанс о котором можно подумать, это сколько нужно отдельных нейронов для следующего слоя. В данном случае мы можем сразу отбросить два крайних результата - наибольшее и наименьшее по определению не могут быть ближе к среднему, чем остальные. С другой стороны, нам ни кто не сказал, что у человека в течении десяти дней температура тела обязательно поменяется - она может быть одинаковой. Что ж, мое предложение - попробовать все-таки сократить количество до восьми и посмотреть потом, что случится с сетью в каких-то специально подстроенных случаях. И увидеть в конце стоило ли жертвовать двумя дополнительными нейронами ради порядка 2^4 лишних операций.
Сперва нужно озаботиться весами. Добавим в лист 'weight' веса следующего слоя. Заметьте, весов нам по-прежнему требуется по десять синапсов на каждый нейрон. Со значениями же поступим более хитро чем с предыдущим слоем, сразу "дадим понять" нейронке, что среднее у нас будет скорее всего где-то посередине '=(1-ABS(5,5-СТОЛБЕЦ())/4,5)*(1-ABS(18,5-СТРОКА())/5)'. Мы это просто сами вот так предположили. Это должно сказаться еще на одном параметре НС, но об этом позже.
Добавим слой из восьми нейронов в третью строчку листа 'sigma' в ячейку 'B3' формулу '=ЕСЛИ(=1/(1+EXP(-(СУММПРОИЗВ($A2:$J2;ТРАНСП(weight!B14:B23))-5)))>0,5;1;0)' и привычным движением размножим ее на остальные нейроны.
Уже на этом этапе несложно заметить, что при подсовывании разных значений появилась тенденция к тому, что посередине второго слоя чаще будут попадаться единицы, более того они будут, как правило идти подряд. Можно сказать, что нейронка уже примерно "догадывается" что от нее требуется, разумеется, не без нашей ей помощи. Нужно теперь измерить насколько корректна эта догадка.
Цена ошибки
Перейдем непосредственно к анализу результата. Ранее мы уже ввели метод оценки точности при помощи среднеквадратического отклонения E = (y - y')^2, где y - ожидание, у' - реальность. Им и воспользуемся. Если вы все еще используете формулы случайных значений использованные для заполнения весов или температур, то самое время зафиксировать их - Копировать - Специальная вставка - Только значения.
На листе 'result' первая строчка будет представлять из себя отклонение от среднего значения - это не ошибка - это ожидаемый результат. То есть в ячейку 'B1' введем '=1-ABS(sigma!B1-input!$M2)/3,6', где 3,6 - максимальное отклонение в нашем примере, а ABS взятие по модулю. Приведение всего подряд к единичному отрезку не является каким-то требованием, просто мне так удобнее рассуждать. Если хотите это такие проценты. Заполним остальные семь результатов в строке. Далее я буду пропускать те места, где подразумевается использовать автозаполнение и какие элементы формулы следует фиксировать знаком '$'. Думаю, это уже довольно очевидно.
В следующую, вторую, строку введем, собственно отклонение результата нейронки от ожидаемого - квадратичную ошибку '=(1/2)*(B1-sigma!B3)^2' или как её еще не очень удачно называют функция стоимости (cost function). Не стоит беспокоиться здесь из-за непонятно откуда взявшейся '1/2' - это сделано для удобства математических выкладок.
Такими результатам уже можно смело пользоваться: во-первых это числа, что, как мы понимаем, для компьютера, только в радость; во-вторых они отображают именно конкретную степень ошибки, а не только ее отсутствие или наличие.
Обратное распространение
Подходим к самому интересному - непосредственно к обучению. Зная значения ошибок, можно браться за настройку весов. Логично предположить, что чем больше ошибка, тем сильнее надо изменить вес соответствующих связей, так, чтобы результат стал ближе к искомому. Другой вопрос состоит в том, на сколько и в какую сторону. Количественным показателем корректировки конкретного веса будет некая функция δ (дельта-функция), аргументами которой должны быть порядковый номер веса и ранее вычисленные ошибки.
Поразмыслим немного о свойствах этой функции прежде чем разбрасываться сложными формулами. Во-первых, как мы понимаем, количественно дельта должна выражать степень ошибки и то насколько сильно влияет данный конкретный вес, который нужно подкорректировать, на общий результат. Очевидно что, чем меньше влияние и чем меньше ошибка, тем меньшую корректировку необходимо внести. Во-вторых, из первого следует, что тогда ошибка должна зависеть от той функции которая привела к этой ошибке - функции активации. В-третьих, дельта должна либо изменять вес в соответствии с позитивным или негативными влиянием на результат. То есть это нечто взятое от результата функции активации, помноженное на ошибку.
Если читатель догадался, что под определение такой конструкции как ни что другое подходит производная, то ему нужно с этого момента самостоятельно заняться программированием, а не выслушивать дальнейшие мои выкладки. Действительно, основатели теории НС еще в 70-е года додумались, что производная от функции активации это прямо то что надо. И надо ли сомневаться в том, что именно логистическая функция легко дифференцируется. Так же геометрический смысл производной указывает нам в какую сторону следует двигаться.
Не вдаваясь в дальнейшие математические тонкости, введем в третью стоку листа 'result' формулу дельты '=(B1-$sigma.B3)*$sigma.B3*(1-$sigma.B3)'. И обнаружим, что... ничего не работает.
Везде должны получиться нули, которые, впрочем, следовало ожидать глядя на множитель 'sigma!B3*(1-sigma!B3)', который при допустимых значениях 1 и 0 может принимать единственное значение - ноль. Дело в том, что для наглядности мы ранее доопределили функцию активации условием 'ЕСЛИ(...>0,5;1;0)' и теперь функция не является непрерывно дифференцируемой. Хоть это немного ломает нашу модель идеального нейрона, и нейрон теперь помимо наличия самого импульса будет нести на выходе еще и силу этого импульса, уберем это условие из всех нейронов '=1/(1+EXP(-(СУММПРОИЗВ($A1:$J1;ТРАНСП(weight!A1:weight!A10))-38,2*10*weight!A12)))' для первого скрытого слоя, и '=1/(1+EXP(-
(СУММПРОИЗВ($A2:$J2;ТРАНСП(weight!B14:B23))-5)))' для второго.
Все что осталось сделать, это посчитать новые веса. В ячейку 'result.K1' введем, пока что загадочный, коэффициент 0,85 и с пятой по четырнадцатую строки вычислим новые веса для второго слоя при помощи формулы '= weight!B14 - $K$1 * B$3 * sigma!B$3'.
Здесь нетрудно заметить, что получившиеся веса мало отличаются от исходных. Более того, они сохранили общий рисунок по рядам и столбцам. Очевидно, что настройки весов только второго слоя недостаточно. Главное волшебство должно происходить в первом слое. Но мы на данный момент вычислили ошибки и корректировку только для второго слоя. Как же быть с предыдущим?
Ответ кроется в заголовке и в навязчивом хождении вокруг да около производных. Строка 2 в листе 'result' специально была оставлена без должного внимания. По сути третья строка является как-раз производной от квадратичной ошибки. А множитель '(1/2)' в формуле это такое математическое жульничество, которое избавляет нас от лишней двойки в производной. А мешаться бы она стала начиная как-раз со следующего шага. Благодаря дальнейшим, чуть более навороченным приемам матанализа, можно продифференцировать ошибку и для следующих слоев.
На доске выражение может выглядеть несколько изощренно, но по сути все сводится к нехитрой формуле '= СУММПРОИЗВ ( $B3:$I3 ; weight!$B14:$I14 * sigma!A2 * ( 1 - sigma!A2 ) )' для следующей (16) строчки в 'result'.
На что здесь следует обратить внимание, это на то, что аргументы в произведении векторов, а конкретно '$B3:$I3' - ранее вычисленные дельты для предыдущего слоя. То есть на каждом k-том слое δ k-тая вычисляется из δ k-1-го - более формально δ(k) = F(δ(k-1)). Такие функции F, обращающиеся сами к себе называются рекурсивными. А весь метод вычисления новых значений весов при помощи рекурсии называется обратным распространением ошибки.
Расчет весов первого слоя предлагаю сделать самостоятельно. Ну или подсмотреть в знакомой вам табличке. А эта часть получилась, мне кажется уже слишком объемной. К сожалению, или к счастью, пописать программы кроме как в эксельке снова не получилось, но это я считаю не беда. Тем более, что дальше справляться одними лишь формулами станет значительно сложнее. Как минимум придется посмотреть что это за зверь такой этот ваш "Макрос".
Продолжение следует...
PS. Ввиду растущей сложности формул и цепочек рассуждений, в тексте и в таблице возможны ошибки и расхождения.
Для тех кто решил, что хватит баловаться в песочнице или уверен, что где-то что-то я наврал, следующая порция ссылок для более вдумчивого углубления в проблематику.
Оценка сложности алгоритмов, или Что такое О(log n)
Критерии сравнения распознающих моделей на основе нейронных сетей и анализ их взаимосвязей
Computational Complexity Of Neural Networks
Обучение нейронной сети. Алгоритм обратного распространения ошибки