Вы могли уже видеть мой рассказ на тему того, почему в нейронных сетях так часто используют Сигмоиды для всевозможных преобразований. А сейчас я хочу привести пример простой нейронной сети, которая обучается и прекрасно аппроксимирует произвольные монотонные зависимости с помощью единственной универсальной функции. Ну и делать мы будем это снова в Excel, чтобы это мог повторить любой офисный работник без установки чего-либо к себе на комп.
Для начала нам придётся открыть Excel и как в прошлый раз нажать Alt+F11. В открывшемся окне создадим новый модуль для класса (правый клик где-нибудь слева => insert => Class Module). И в него будем последовательно вставлять весь тот код, что будет ниже. Его я буду сопровождать своими комментариями.
Dim c(0 To 3) As Double 'Коэффициенты
Dim tc(0 To 3) As Double 'Пробные коэффициенты
Dim dc(0 To 3) As Double 'Пробные изменения
Dim dy(0 To 3) As Double 'Результаты изменения коэффициентов
Const st = 0.2 'Скорость обучения
Здесь мы просто определяем все переменные, которые будут использоваться в рамках конкретного нейрона.
Sub init() 'Инициализация случайных значений коэффициентов сигмоиды
For i = 0 To 3
c(i) = Rnd() * 2 - 1
Next i
End Sub
Сигмоида у нас задаётся четырьмя параметрами. Первый коэффициент стоит, как множитель ко всей функции в целом. Второй коэффициент - перед иксом. Третий - прибавляется к иксу. Четвёртый - прибавляется ко всей функции в целом. И все эти коэффициенты задаются случайным образом от минус единицы до плюс единицы. F(x)=c1/e^(-c2*(x+c3))+c4
Таким подходом мы задаём всё многообразие сигмоид.
Sub NewC(De As Double, n As Integer) 'Генерируем новый тестовый вес
For i = 0 To 3
If n = i Then
dc(i) = De * st
End If
tc(i) = c(i) + IIf(n = i, dc(i), 0)
Next i
End Sub
Передаём значение, на которое ошиблась наша функция и номер коэффициента, который хотим изменить. Изменяем этот коэффициент с учётом скорости обучения и нашей ошибки.
Sub SaveC(d As Worksheet, r As Integer) 'Сохраняем веса
For i = 0 To 3
d.Cells(r, 8 + i) = c(i)
Next i
End Sub
Sub LoadC(d As Worksheet, r As Integer) 'Загружаем веса
For i = 0 To 3
c(i) = d.Cells(r, 8 + i)
Next i
End Sub
Простые функции сохранения и загрузки коэффициентов, чтобы можно было продолжать работать с того места, где остановились.
Sub FixC() 'Сохраняем тестовые веса
Dim my As Double
Dim j As Integer
j = 1
my = dy(0)
For i = 1 To 3 'Определяем максимальное изменение y
If dy(i) > my Then
my = dy(i)
j = i
End If
Next i
For i = 0 To 3 'Для максимального изменения y вес меняем полностью, для остальных в зависимости от величины изменения
c(i) = c(i) + dy(i) / my * dc(i)
Next i
End Sub
Сохраняем веса так, чтобы наиболее эффективное изменение веса с точки зрения устранения ошибки применялось полностью, а остальные - в соответствии с их вкладом в минимизацию ошибки.
Function res(x As Double, T As Boolean) As Double 'Расчёт значения функции
c1 = IIf(T, tc(0), c(0))
c2 = IIf(T, tc(1), c(1))
c3 = IIf(T, tc(2), c(2))
c4 = IIf(T, tc(3), c(3))
ie = -c2 * (x + c3)
If ie > 40 Then 'Если число в знаменателе очень большое, можно считать значение нулём
res = c4
ElseIf ie < -40 Then 'Если число очень маленькое - единицей
res = c1 + c4
Else 'В остальных случаях считаем честно
res = c1 / (1 + Exp(ie)) + c4
End If
End Function
Функция расчёта значения нашей сигмоиды. Чтобы не было деления на ноль и прочих проблем, для больших значений экспоненты ставим известные значения. Параметр Т отвечает за то, используем мы тестовые коэффициенты или боевые.
Sub TryC(x As Double, y As Double, n As Integer) 'Пробуем изменить конкретный вес. Если стало лучше, фиксируем это
Dim De As Double
De = Abs(y - res(x, False))
Call NewC(De, n)
Dim NDe As Double
NDe = Abs(y - res(x, True))
If NDe >= De Then
Call NewC(-De, n)
NDe = Abs(y - res(x, True))
End If
If NDe < De Then
dy(n) = De - NDe
End If
End Sub
Пробуем изменить тестовые коэффициенты. Если ошибка уменьшилась, фиксируем это, чтобы в будущем изменить уже боевые коэффициенты.
Public Function MakeRnd(n As Integer) As String 'Формируем случайный перебор значений от нуля до n-1
Randomize
Dim i As Integer
MakeRnd = ","
Dim S As String
NewS:
S = Val(Rnd() * n)
If MakeRnd Like "*," & S & ",*" Then GoTo NewS
If S >= n Then GoTo NewS
MakeRnd = MakeRnd & S & ","
i = i + 1
If i < n Then GoTo NewS
MakeRnd = Left(MakeRnd, Len(MakeRnd) - 1)
MakeRnd = Right(MakeRnd, Len(MakeRnd) - 1)
End Function
Это простая функция, которая генерирует случайную по порядку последовательность от нуля до n-1. Мы её будем использовать, чтобы случайно перебирать коэффициенты не по порядку. В некоторых ситуациях это может иметь значение и делать расчёт сети предвзятым.
Sub Chng(x As Double, y As Double)
For i = 0 To 3 'Обнуляем значения
dy(i) = 0
dc(i) = 0
Next i
Dim tw() As String
tw = Split(MakeRnd(4), ",")
For i = 0 To UBound(tw)
Call TryC(x, y, Val(tw(i)))
Next i
Call FixC
End Sub
Эта процедура просто отражает логику использования функций. Сначала обнуляем все значения, затем формируем последовательность, для перебора весов. Затем вычисляем эффективность изменения веса при устранении ошибки. Затем сохраняем новые веса.
Теперь заполним какими-нибудь данными нашу функцию. Я в первом столбце перечислил значения аргументов, в третьем - значение целевой функции. А четвёртый столбец остаётся для занесения результатов расчёта сети, чтобы сравнить с целевой функцией. Ну и отдельно посчитал среднюю квадратичную ошибку. Вышло примерно так:
Это всё, что нужно в нашем классе. Теперь будет небольшой код, который призван перебирать входящие данные и запускать обучение нейрона на тестовых данных. Этот код можно вставить в любое место кроме самого модуля класса. Например в "ЭтаКнига".
Sub run()
Dim cr As Integer 'текущая строка
Dim y As Sgm
Set y = New Sgm
Call y.init
Dim mr As Integer
Dim d As Worksheet
Set d = ThisWorkbook.Sheets(4)
'Call y.LoadC(d, 1)
i = 2
While d.Cells(i, 1) <> 0
i = i + 1
Wend
mr = i - 1 'Определяем максимальную заполненную строчку
For i = 1 To 100000
cr = Rnd() * (mr - 1) + 2 'Генерируем случайную строку для подстановки в нейрон для обучения
Call y.Chng(d.Cells(cr, 1), d.Cells(cr, 3)) 'Обучаем нейрон на случайной строке
Next i
i = 2
While d.Cells(i, 1) <> 0 'Заполняем значениями для визуализации результатов работы
d.Cells(i, 4) = y.res(d.Cells(i, 1), False)
i = i + 1
Wend
'Call y.SaveC(d, 1)
End Sub
Инициализируем нейрон и его параметры. При желании загружаем старые веса. Чтобы не было предвзятости при обучении, генерируем случайный номер строки 100 тысяч раз и обучаем наш нейрон. Затем выводим получившиеся данные и, если необходимо, сохраняем коэффициенты.
Это вся нейронная сеть. И сейчас я приведу целый ряд примеров, как ей удаётся аппроксимировать самые разные функции. Например, так ей удаётся приблизиться к некоторой произвольной сигмоиде. Что очевидно, результат довольно хороший. Оранжевое - это целевая функция. Серое - результат аппроксимации.
А теперь давайте попробуем аппроксимировать смещённую экспоненту:
Результат аппроксимации квадратичной функции оказался ещё более замечательным. Ошибка оказалась пренебрежимо малой:
Особенно интересной может быть попытка аппроксимировать "ступеньку". Такая функция, которая до определённого значения нулевая, а затем равна единице:
И тоже вышло неплохо. Но есть ситуация, когда одной сигмоидой не обойтись. Достаточно задать функцию с "ямкой":
И чтобы справиться с этим, нужно уже просуммировать две сигмоиды. Одна будет описывать правую часть. Вторая - левую. Но этого мы в рамках текущей статьи делать не станем.
Из описанного выше становится ясно, что сигмоида - это замечательная функция, которая может достаточно точно описать практически любую монотонную функцию. Но и она не всесильна, когда имеются "ямки" и "горки". Но с помощью определённых методов можно избавиться и от этих проблем.