Реализация градиентного спуска на C#
\(\)

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

Формула каждой итерации градиентного спуска
$$\begin{align*}& \theta_j := \theta_j — \alpha \frac{1}{m} \sum\limits_{i=1}^{m} (h_\theta(x^{(i)}) — y^{(i)}) \cdot x_j^{(i)} \; & \text{for j := 0…n}\end{align*}$$
Критерий сходимости
Алгоритм градиентного спуска — это численный алгоритм, и результатом его работы является приближенное значение. Каждый шаг алгоритма приближает результат и нужен критерий остановки алгоритма. Останавливать работу алгоритма нужно тогда, когда результат практически не улучшается. В виде формулы это можно представить так
$$\begin{align*}& f{(i)} — f{(i+1)} < \epsilon \text{ , где } \epsilon \text{- достаточно малое число} \end{align*}$$
Шаг алгоритма
Шаг алгоритма — это параметр альфа в формуле. Если он слишком большой — градиентный спуск начинает расходиться. В такой ситуации нужно уменьшить шаг алгоритма.
Вычисление производной на C#
Посчитать производную можно по следующей формуле:
$$\frac{f(x + \Delta x) — f(x — \Delta x)}{2 \Delta x} \text{ , где } \Delta x \text{- достаточно малое число}$$
На C# это будет выглядеть вот так:
public double GetDerivative(Func<double, double> function,
double point, double delta) {
return (function(point + delta) - function(point - delta)) /
(2 * delta);
}
Реализация алгоритма
public List<double> Calculate (List<double> startPoint,
Func<List<double>, double> function) {
double alpha = 1;
var alphaDecreaseRate = 0.9;
var currentPoint = startPoint;
while (true) {
var currentValue = function (currentPoint);
var newPoint = new List<double> ();
for (var i = 0; i < currentPoint.Count; i++) {
Func<double, double> func = x =>
function (CopyPointWithReplace (currentPoint, x, i));
newPoint.Add (currentPoint[i] -
alpha * (1.0 / Convert.ToDouble(startPoint.Count)) *
new Derivative ().GetDerivative (func, currentPoint[i]));
}
var newValue = function (newPoint);
if (newValue > currentValue)
alpha *= alphaDecreaseRate;
else {
if (currentValue - newValue <= EPSILON)
return newPoint;
else
currentPoint = newPoint;
}
}
}
Вспомогательный метод для копировании точки с заменой значения одной из координат:
private List<double> CopyPointWithReplace (List<double> point,
double replace, int replaceIndex) {
var result = new List<double> ();
for (var i = 0; i < point.Count; i++)
if (i == replaceIndex)
result.Add (replace);
else
result.Add (point[i]);
return result;
}
он исползуется в выражении
Func<double, double> func =
x => function (CopyPointWithReplace (currentPoint, x, i));
Это выражение преобразует функцию нескольких переменных к функции одной переменной, фиксируя значения всех остальных переменных.
Пример использования
private double TestFunction(List<double> point)
{
if (point.Count != 5)
throw new ArgumentException("Size of vector should be 5");
double result = 0;
foreach(var value in point)
result += (value - 2) * (value - 2);
return result;
}
...
var minimumPoint = new GradientDescent().Calculate(
new List<double>(){0,0,0,0,0}, TestFunction);
Исходный код
Исходный код доступен на гитхабе