# День 3: Haskell путеводитель по нейронным сетям

После того как мы посмотрели, как работает сеть, стало ясно, что понимание градиента жизненно необходимо. Отсюда, пересмотрим нашу стратегию на уровне ниже. Однако, так как нейронные сети становятся сложнее, вычисления градиента в ручном режиме становится еще тем делом. Но всё еще есть выход! Я очень рад, что сегодня мы наконец познакомимся автоматической дифференциацией, естественным инструментом в изучении арсенала глубокого обучнеия. Эта статья написана под впечатлением от Hacker's guide to Neural Networks. Для сравнения так же стоит посмотреть Python версию.

# Почему случайный локальный поиск не подходит
Следуя инструкции от Картпатого, для начала рассмотрим простую цепь умножений. Haskell не Javascript, поэтому перепишем явным образом. 
```haskell
forwardMultiplyGate = (*)
```
Or we could have written
```haskell
forwardMultiplyGate x y = x * y
```
чтобы сделать функцию более понятной `f(x,y)=x⋅y`. В любом случае,
```haskell
forwardMultiplyGate (-2) 3
```
Возвращает -6. Отлично!

Теперь вопрос: есть ли возможность изменить `(x,y)` чтобы улучшить вывод? Один из способов это произвести локальный случайный поиск. 
```haskell
_search tweakAmount (x, y, bestOut) = do
  x_try <- (x + ). (tweakAmount *) <$> randomDouble
  y_try <- (y + ). (tweakAmount *) <$> randomDouble
  let out = forwardMultiplyGate x_try y_try
  return $ if out > bestOut
               then (x_try, y_try, out)
               else (x, y, bestOut)
```
Не удивительно, функция выше отражает простую итерацию цикла `for`. Что он делает: случайным образом выбирает точки вокруг начальных `(x,y)` и проверяет увеличился ли вывод. Если да, тогда он обновляет лучшие известные входные и максимальные выходные данные. Чтобы пройтись по значениям, мы можем использовать `foldM :: (b -> a -> IO b) -> b -> [a] -> IO b`. Эта фукнция удобна так как ожидаем взаимодействие с "реальным миром" в виде случайно сгенерированных чисел. 
```haskell
localSearch tweakAmount (x0, y0, out0) =
 foldM (searchStep tweakAmount) (x0, y0, out0) [1..100]
```
Код говорит нам, что мы наполняем код с какими-то начальными значениями `x0`, `y0` и `out0` и проходимся от 1 до 100. Ядро алгоритма - `searchStep`
What the code essentially tells us is that we seed the algorithm with some initial values of `x0`, `y0`, and `out0` and iterate from 1 till 100. The core of the algorithm is searchStep:
```haskell
searchStep ta xyz _ = _search ta xyz
```
что есть довольно удобная функция, которая склеивает 2 части вместе. Она просто игнорирует итерационные числа и вызывает `_search`. Теперь, нам нужно случайное число в промежутке `[-1; 1)`. Из документации, мы знаем, что `randomIO` производит числа между 0 и 1. Проскалируем его умножая на 2 и вычитая 1.
```haskell
randomDouble :: IO Double
randomDouble = subtract 1. (*2) <$> randomIO
```
Функция `<$>` это синоним `fmap`. Она применяет чистую функцию `substract 1. (*2)` тип которой `Double-Double` ко "внешнему" действию `randomIO`, тип которой `IO Double` (yes, IO = input/output)1.

Хитрость для числа минус бесконечность:

```haskell
inf_ = -1.0 / 0
```
Теперь запускаем `localSearch 0.01 (-2, 3, inf_)` несколько раз:
```
(-1.7887454910045664,2.910160042416705,-5.205535653974539)
(-1.7912166830200635,2.89808308735154,-5.19109477484237)
(-1.8216809458018006,2.8372869694452523,-5.168631610010152)
```
На деле мы видим как вывод изменился с -6 до -5.2. Но улучшение только 0.008 едениц на итерацию. Это очень не эффективный метод. Проблема со случайным поиском в том, что каждый раз он пытается изменить входные данные в случайных направлениях. Если алгоритм делает ошибку, он должен сбросить результат и начать с последней лучшей позиции. Не правда ли лучше было бы, если вместо каждой итерации результат улучшался пусть даже по чуть чуть но постоянно и не приходилось откатываться?

# Автоматическое дифференцирование
Вместо случайного поиска в случайном направлении, мы можем использовать точное направление и количество для изменения входных данных таким образом, чтобы улучшался вывод. И это то что градиент нам говорит. Вместо ручного вычисления градиента каждый раз, мы можем использовать умный алгоритм. Есть можноество подходов: цифровой, символический и автоматическое дифференцирование. В этой статье, Доминик Стейнтц объясняет разницу между ними. Последений подход, автоматическое дифференцирование, именно то что нам нужно: точные градиенты с мнимальным количеством переработок. Кратко поясним идею. 

За идеей автоматического дифференцирования стоит ясно определенный градиент только для простых базовых операторов. Затем, мы составляем цепь правил комбинируя операторы в нейронную сеть как хотим. Такая стратегия будет влиять на сам градиент. Давайте посмотрим на метод через пример. 

Ниже можно посмотреть оператор умножения и его градиент используя правило. [![](https://notepad.gasick.ru/uploads/images/gallery/2022-09/scaled-1680-/image-1662195699348.png)](https://notepad.gasick.ru/uploads/images/gallery/2022-09/image-1662195699348.png)
```
(x, x') *. (y, y') = (x * y, x * y' + x' * y)
```
Тоже самое можно сделать со сложением, вычитанием, делением и экспонентой:
```haskell
(x, x') +. (y, y') = (x + y, x' + y')

x -. y = x +. (negate1 y)

negate1 (x, x') = (negate x, negate x')

(x, x') /. (y, y') = (x / y, (y * x' - x * y') / y^2)

exp1 (x, x') = (exp x, x' * exp x)
```
Мы так же имеем `constOp` для констант:
```haskell
constOp :: Double -> (Double,Double)
constOp x = (x, 0.0)
```
Наконец, мы можем определить наш любимый сигмоид `σ(x)` объединяя те операторы, что были выше:
```haskell
sigmoid1 x = constOp 1 /. (constOp 1 +. exp1 (negate1 x))
```
теперь давайте посчитаем нейрон `f(x,y)=σ(ax+by+c)`, где `x` и `y` это ввод а `a`, `b` и `c` паарметры. 
```haskell
neuron1 [a, b, c, x, y] = sigmoid1 ((a *. x) +. (b *. y) +. c)
```
Теперь можно получить градиент для `a` в точке `(a=1, b=2, c=−3, x=−1, y=3)`
```haskell
abcxy1 :: [(Double, Double)]
abcxy1 = [(1, 1), (2, 0), (-3, 0), (-1, 0), (3, 0)]
```
```haskell
neuron1 abcxy1
(0.8807970779778823,-0.1049935854035065)
```
Вотт первый пример результата вывода нейронно йсети и второй градиент относительно `a` [![](https://notepad.gasick.ru/uploads/images/gallery/2022-09/scaled-1680-/image-1662195835116.png)](https://notepad.gasick.ru/uploads/images/gallery/2022-09/image-1662195835116.png)
Проверим математику результата:
[![](https://notepad.gasick.ru/uploads/images/gallery/2022-09/scaled-1680-/image-1662195848290.png)](https://notepad.gasick.ru/uploads/images/gallery/2022-09/image-1662195848290.png)
Первое выражение это результат вычислений нейронов, а второй точное аналитическое выражение
Вот и вся магия за словами автоматическая дифференциация. Похожим образом, мы можем получить остаток градиента:[![](https://notepad.gasick.ru/uploads/images/gallery/2022-09/scaled-1680-/image-1662195868087.png)](https://notepad.gasick.ru/uploads/images/gallery/2022-09/image-1662195868087.png)
```haskell
neuron1 [(1, 0), (2, 1), (-3, 0), (-1, 0), (3, 0)]
(0.8807970779778823,0.3149807562105195)

neuron1 [(1, 0), (2, 0), (-3, 1), (-1, 0), (3, 0)]
(0.8807970779778823,0.1049935854035065)

neuron1 [(1, 0), (2, 0), (-3, 0), (-1, 1), (3, 0)]
(0.8807970779778823,0.1049935854035065)

neuron1 [(1, 0), (2, 0), (-3, 0), (-1, 0), (3, 1)]
(0.8807970779778823,0.209987170807013)
```
# Введение библиотеки обратного распределения 
Библиотека обратного распределения была написана специально для дифференциального программирования. Она предоставляет комбинаторов для уменьшения нашей головной боли. В добавок, самые полезные операции арфиметические и тригонометрические, уже были определены в библиотеке. Можно взглянуть на `hmatrix-backprop` для линейной алгебры. Всё что вам нужно для дифференциального программирования определить несколько функций:
```haskell
neuron
  :: Reifies s W
  => [BVar s Double] -> BVar s Double
neuron [a, b, c, x, y] = sigmoid (a * x + b * y + c)

sigmoid x = 1 / (1 + exp (-x))
```
Тут `BVar` обернут в маркер того, что функция дифференцируемая.
```haskell
forwardNeuron = BP.evalBP (neuron. BP.sequenceVar)
```
Используем изоморфимз `sequenceVar` для преобразования `BVar` список в список `BVar`ов, как того требует выражение `neuron`. И передаем дальше.
```haskell
backwardNeuron = BP.gradBP (neuron. BP.sequenceVar)

abcxy0 :: [Double]
abcxy0 = [1, 2, (-3), (-1), 3]

forwardNeuron abcxy0
-- 0.8807970779778823

backwardNeuron abcxy0
-- [-0.1049935854035065,0.3149807562105195,0.1049935854035065,0.1049935854035065,0.209987170807013]
```
Заметим, что все градиенты в одном списке, тип аргумента первого нейрона.

# Выводы
Современная нейронная сеть тяготеет к сложности. Написание градент обратного распределения в ручну может легко стать ужасом. В этом посте мы посмотрели как можно автоматизировать этот процесс при надобности. 

В следующем посту мы применим автоматическую дифференциацию к реальной сетке. Поговорим о нормализации, других важных методахв глубоком обучении. И затронем сверточные нейнонные сети, котороые помогут нам решить интересные задачи. 

# Что можно почитать. 
[Графический путеводитель по нейронным сетям](https://jalammar.github.io/visual-interactive-guide-basics-neural-networks/)

[Документация обратного распределения](https://backprop.jle.im/01-getting-started.html)

[Article on backpropagation by Dominic Steinitz](https://idontgetoutmuch.wordpress.com/2013/10/13/backpropogation-is-just-steepest-descent-with-automatic-differentiation-2/)