Заявление об ограничении ответственности: это очень объемное сообщение в блоге, включающее математические доказательства и реализации на Python для различных алгоритмов оптимизации.
Оптимизация, одна из самых интересных тем в области машинного обучения. Большинство проблем, с которыми мы сталкиваемся в повседневной жизни, решаются с помощью методов численной оптимизации. В этом блоге давайте подробно рассмотрим некоторые базовые алгоритмы численной оптимизации для поиска локальных оптимумов любой заданной функции (которая лучше всего работает с выпуклыми функциями). Давайте начнем с простых выпуклых функций, в которых локальный и глобальный минимумы совпадают, и перейдем к сильно нелинейным функциям с множественными локальными
и глобальными минимумами. Вся оптимизация вращается вокруг основных понятий линейной алгебры и дифференциального исчисления. Недавние обновления в области глубокого обучения вызвали огромный интерес к алгоритмам численной и стохастической оптимизации, чтобы теоретически обосновать потрясающие качественные результаты, демонстрируемые сетями глубокого обучения. В таких алгоритмах обучения не существует какой-либо явно известной функции для оптимизации, но у нас есть доступ только к оракулам 0-го и 1-го порядка. Оракулы - это черные ящики, которые возвращают значение функции (0-й порядок), градиент (1-й порядок) или гессиан (2-й порядок) функции в любой заданной точке. Этот блог предоставляет базовое теоретическое и численное понимание функций неограниченной и ограниченной оптимизации, а также включает их реализацию на языке Python.
Необходимые и достаточные условия для того, чтобы точка была локальным минимумом:
Пусть f (.) - непрерывная дважды дифференцируемая функция. Чтобы любая точка была минимумом, она должна удовлетворять следующим условиям:
- Необходимое условие первого порядка:
- Необходимое условие второго порядка:
- Условие достаточности второго порядка:
Градиентный спуск:
Градиентный спуск является основой всех достижений в области алгоритмов обучения (машинное обучение, глубокое обучение или глубокое обучение с подкреплением). В этом разделе мы увидим различные модификации градиентного спуска для более быстрой и лучшей сходимости. Рассмотрим случай линейной регрессии, где оценим коэффициенты уравнения:
Предполагая, что функция представляет собой линейную комбинацию всех функций. Оптимальный набор коэффициентов определяется путем минимизации функции потерь:
что оказывается оценкой максимального правдоподобия для задачи линейной регрессии. Минимизация OLS (обыкновенного наименьшего квадрата) включает в себя нахождение обратной матрицы признаков, заданной следующим образом:
Для практической задачи размерность данных огромна, что легко подорвет вычислительную мощность. Например, давайте рассмотрим проблему анализа признаков изображения: обычно изображения имеют размер 1024x1024, что означает, что количество признаков порядка 10⁶. Из-за большого количества функций такие задачи оптимизации решаются только итеративным способом, и это приводит нас к очень широко известному алгоритму, называемому градиентным спуском и методу Ньютона-Рафсона.
Алгоритм градиентного спуска:
Алгоритм градиентного спуска обновляет итерацию (X) в направлении отрицательного градиента (следовательно, в направлении наискорейшего спуска) с ранее указанной скоростью обучения (eta). Скорость обучения используется для предотвращения превышения
локальных минимумов на любой данной итерации.
На рисунке ниже показана сходимость алгоритма градиентного спуска для функции f (x) = x² с eta = 0,25
Поиск оптимального eta - это непосредственная задача, которая требует предварительных знаний о понимании функций и области действия.
# python implementation of vanilla gradient descent update ruledef def gradient_descent_update (x, eta): """ get_gradient is 1st order oracle """ return x - eta*get_gradient(x)
Градиентный спуск с условием Армийо Гольдштейна:
Чтобы сократить объем ручной настройки, применяются условия Армиджо Гольдштейна (AG), чтобы найти (eta) для следующей итерации. Формальный вывод условия AG требует знания линейного приближения, условий Липшица и базового исчисления, о которых я не буду рассказывать в этом блоге.
Давайте определим две функции f1 (x) и f2 (x) как линейную аппроксимацию f (x) с двумя разными коэффициентами альфа и бета, задаваемыми следующим образом:
В каждой итерации условия AG конкретная эта, удовлетворяющая соотношению:
найден, и текущая итерация обновляется соответствующим образом.
На рисунке ниже показан диапазон следующей итерации для сходимости функции f (x) = x² с альфа = 0,25 и бета = 0,5:
Красная, синяя и зеленая линии на рисунке выше соответствуют
зеленому диапазону следующей возможной итерации.
# python implementation of gradient descent with AG condition update rule def gradient_descent_update_AG(x, alpha =0.5, beta =0.25): eta =0.5 max_eta =np.inf min_eta =0. value = get_value(x) grad = get_gradient(x) while True : x_cand = x - (eta)*grad f = get_value(x_cand) f1 = value - eta *a lpha*np.sum(np.abs(grad)* *2 ) f2 = value - eta *be ta *np.sum(np.abs(grad)* *2 ) if f<=f2 and f>=f1: return x_cand if f <= f1: if eta == max_eta: eta = np.min([2.*eta, eta + max_eta/2.]) else: eta = np.min([2.*eta, (eta + max_eta)/2.]) if f>=f2: max_eta = eta eta = (eta+min_eta)/2.0
Градиентный спуск с условием полной релаксации:
В случае условия полной релаксации новая функция g (eta) минимизируется для получения eta, которая впоследствии используется для поиска следующей итерации.
Этот метод включает решение задачи оптимизации для поиска каждой следующей итерации, которая выполняется следующим образом:
# The python implementation for FR is shown below: def gradient_descent_update_FR (x): eta = 0.5 thresh = 1e-6 max_eta = np.inf min_eta = 0. while True : x_cand = x - eta*get_gradient(x) g_diff = -1. *get_gradient(x)*get_gradient(x_cand) if np.sum(np.abs(g_diff)) < thresh and eta > 0 : return x_cand if g_diff > 0: if eta == max_eta: eta = np.min([2.*eta, eta + max_eta/2.]) else: eta = np.min([2.*eta, (eta + max_eta)/2.]) else: max_eta = eta eta = (min_eta + eta)/2.0
Стохастический градиентный спуск
Как мы знаем, в реальной установке размер данных будет довольно большим, что делает дальнейшее вычисление градиента для всех функций дорогостоящим. В таких случаях выбирается случайный набор точек (объектов) и вычисляется ожидаемый градиент. Вся эта установка сводится только к математическому ожиданию.
Математически это будет означать:
Случайным образом выбрать точку (p) для оценки градиентов.
В приведенной выше итерации wt можно рассматривать как шум. Эта итерация приводит к локальным оптимумам только тогда, когда E (wt) стремится к 0.
Точно так же можно показать, что дисперсия wt также конечна. Приведенное выше доказательство гарантирует сходимость SGD.
# SGD implementation in python def SGD (self, X, Y, batch_size, thresh= 1 ): loss = 100 step = 0 if self.plot: losses = [] while loss >= thresh: # mini_batch formation index = np.random.randint( 0 , len(X), size = batch_size) trainX, trainY = np.array(X)[index], np.array(Y)[index] self.forward(trainX) loss = self.loss(trainY) self.gradients(trainY) # update self.w0 -= np.squeeze(self.alpha*self.grad_w0) self.weights -= np.squeeze(self.alpha*self.grad_w) if self.plot: losses.append(loss) if step % 1000 == 999 : print "Batch number: {}" .format(step)+ " current loss: {}" .format(loss) step += 1 if self.plot : self.visualization(X, Y, losses) pass
AdaGrad
Adagrad - это оптимизатор, который помогает в автоматической адаптации скорости обучения для каждой функции, задействованной в задаче оптимизации. Это достигается за счет отслеживания истории всех градиентов. Этот метод также сходится только в смысле ожидания.
def AdaGrad (self, X, Y, batch_size,thresh=0.5 ,epsilon=1e-6 ): loss = 100 step = 0 if self.plot: losses = [] G = np.zeros((X.shape[ 1 ], X.shape[ 1 ])) G0 = 0 while loss >= thresh: # mini_batch formation index = np.random.randint( 0 , len(X), size = batch_size) trainX, trainY = np.array(X)[index], np.array(Y)[index] self.forward(trainX) loss = self.loss(trainY) self.gradients(trainY) G += self.grad_w.T*self.grad_w G0 += self.grad_w0** 2 den = np.sqrt(np.diag(G)+epsilon) delta_w = self.alpha*self.grad_w / den delta_w0 = self.alpha*self.grad_w0 / np.sqrt(G0 + epsilon) # update parameters self.w0 -= np.squeeze(delta_w0) self.weights -= np.squeeze(delta_w) if self.plot: losses.append(loss) if step % 500 == 0 : print "Batch number: {}".format (step)+ " current loss: {}".format(loss) step += 1 if self.plot : self.visualization(X, Y, losses) pass
Давайте перейдем к методам на основе Гессе, Ньютона и квазиньютона:
Методы на основе Гессе - это методы на основе градиента второго порядка для оптимизации, геометрически они включают информацию о градиенте и кривизне для обновления весов. По этой причине сходимость происходит намного быстрее по сравнению с методами, основанными только на градиенте. Правило обновления для метода Ньютона определяется как :
Сходимость этого алгоритма намного быстрее, чем у градиентных методов. Математически скорость сходимости градиентного спуска пропорциональна O (1 / t), тогда как для метода Ньютона она пропорциональна O (1 / t²). Но в случае данных более высокой размерности оценка оракула 2-го порядка для каждой итерации требует больших вычислительных ресурсов, что приводит к моделированию оракула 2-го порядка с использованием оракула 1-го порядка. Это дает нам квазиньютоновский класс алгоритмов. Чаще всего в этом классе квазиньютоновских методов используются алгоритмы BFGS и LMFGS. В этом разделе мы обсуждаем только алгоритм BFGS, который включает обновление матрицы ранга один гессиана функции. Общая идея метода состоит в том, чтобы случайным образом инициализировать Гессен и продолжать обновлять Гессе с каждой итерацией, используя правило обновления первого ранга. Математически это показано как:
# python implementation for BFGS def BFGS_update (H, s, y): smooth = 1e-3 s = np.expand_dims(s, axis= -1 ) y = np.expand_dims(y, axis= -1 ) rho = 1. /(s.T.dot(y) + smooth) p = (np.eye(H.shape[ 0 ]) - rho*s.dot(y.T)) return p.dot(H.dot(p.T)) + rho*s.dot(s.T) def quasi_Newton (x0, H0, num_iter= 100 , eta= 1 ): xo, H = x0, H0 for _ in range(num_iter): xn = xo - eta*H.dot(get_gradient(xo)) y = get_gradient(xn) - get_gradient(xo) s = xn - xo H = BFGS_update(H, s, y) xo = xn return xo
Ограниченная оптимизация
Теперь пришло время обсудить несколько ключевых концепций, связанных с оптимизацией с ограничениями (которая включает в себя формулировку проблемы и стратегии решения). В этом разделе также обсуждаются теория и реализация Python алгоритма, известного как SVM (машина опорных векторов). Когда мы сталкиваемся с реальными проблемами, придумать идеальную функцию оптимизации довольно сложно, а иногда и невозможно, поэтому мы ослабляем функцию оптимизации, налагая дополнительные ограничения на проблему, и оптимизация этой ограниченной настройки обеспечит приближение, которое мы могли бы быть как можно ближе к реальному решению проблемы, но также и выполнимо. Для решения задач оптимизации с ограничениями используются такие методы, как лагранжева формулировка, методы штрафов, прогнозируемый градиентный спуск, внутренние точки и многие другие методы. В этом разделе мы рассмотрим лагранжеву формулировку и прогнозируемый градиентный спуск. В этом разделе также рассказывается о наборе инструментов с открытым исходным кодом, используемом для оптимизации CVXOPT, а также рассматривается реализация SVM с использованием этого набора инструментов.
Общая форма задач оптимизации с ограничениями:
где f (x) - целевая функция, g (x) и h (x) - ограничения неравенства и равенства соответственно. Если f (x) выпукло, а ограничения образуют выпуклое множество (т.е. g (x) выпукло, а h (x) аффинно), оптимизация гарантированно сходится в глобальных минимумах. Для любого другого набора задач он сходится к
локальным минимумам.
Прогнозируемый градиентный спуск
Предварительный (и наиболее очевидный) шаг при решении настройки оптимизации с ограничениями - это проекция итерации по ограниченному набору. Оказывается, это самый мощный алгоритм в решении задачи оптимизации с ограничениями
. Это включает в себя два шага (1) для поиска следующей возможной итерации в направлении минимизации (спуска), (2) поиск проекции итерации на ограниченное множество.
# projected gradient descent implementation def projection_oracle_l2 (w, l2_norm): return (l2_norm/np.linalg.norm(w))*w def projection_oracle_l1 (w, l1_norm): # first remember signs and store them. Modify w signs = np.sign(w) w = w*signs # project this modified w onto the simplex in first orthant. d=len(w) if np.sum(w)<=l1_norm: return w*signs for i in range(d): w_next = w+ 0 w_next[w> 1e-7 ] = w[w> 1e-7 ] - np.min(w[w> 1e-7 ]) if np.sum(w_next)<=l1_norm: w = ((l1_norm - np.sum(w_next))*w + (np.sum(w) - l1_norm)*w_next)/(np.sum(w)-np.sum(w_next)) return w*signs else : w=w_next def main (): eta= 1.0 /smoothness_coeff for l1_norm in np.arange( 0 , 10 , 0.5 ): w=np.random.randn(data_dim) for i in range( 1000 ): w = w - eta*get_gradient(w) w = projection_oracle_l1(w, l1_norm) pass
Понимание лагранжевой формулировки
В большинстве задач оптимизации поиск проекции итерации на ограниченный набор является сложной задачей (особенно в случае сложного ограниченного набора). Это похоже на решение задачи оптимизации на каждой итерации, которая в большинстве случаев была бы невыпуклой. На самом деле люди пытаются избавиться от ограничений, решая двойную проблему, а не основную проблему.
Прежде чем погрузиться в лагранжеву дуальную и первичную формулировки, давайте разберемся с условиями KKT и их значение.
- Чтобы любая точка была локальным / глобальным минимумом с ограничениями равенства:
- Аналогично в случае ограничений неравенства:
Эти два условия могут быть легко соблюдены, если рассматривать единичный круг как ограниченное множество. В первой части рассмотрим только границу, где знак (\ mu) не имеет значения, это результат ограничений равенства. Во втором случае рассмотрим внутренний набор единичного круга, где -ve знак для (\ lambda) означает область допустимого решения.
Условия ККТ (Каруш – Куна – Таккера) рассматриваются как необходимые условия первого порядка, которым должна удовлетворять точка, чтобы считаться стационарной точкой (локальные минимумы, локальные максимумы, седловая точка. При x ^ *
чтобы быть локальными минимумами:
Условие LICQ: все активные ограничения
должны быть линейно независимыми друг от друга.
Лагранжиан функции
Для любой функции f (x) с ограничениями-неравенствами g_i (x) ≤ 0 и ограничениями-равенствами h_i (x) = 0 лагранжиан L (x, \ lambda, \ mu)
Функция оптимизации:
вышеуказанная оптимизация эквивалентна:
Вышеупомянутая формулировка известна как основная проблема (p ^ *) в соответствии с утверждением теории игр (т.е. у второго игрока всегда будет больше шансов на оптимизацию), легко увидеть, что:
Эта формулировка известна как двойная задача (d ^ *).
И прямая, и двойственная формулировки приводят к одному и тому же оптимальному значению тогда и только тогда, когда целевая функция является выпуклой и ограниченное множество также является выпуклым. Доказательство этого утверждения обосновывается ниже:
пусть функция выпуклая, что влечет
SVM (машины опорных векторов)
SVM принадлежит к классу алгоритмов контролируемого обучения, используемых в задачах классификации и регрессии. SVM легко масштабируются и могут решать линейные и нелинейные задачи (используя уловку на основе ядра). В большинстве задач мы не можем провести единую линию, разграничивающую два разных класса данных, из-за чего нам нужно обеспечить небольшой запас при построении границ принятия решений, что легко формулируется с помощью SVM. Идея SVM состоит в том, чтобы создать разделяющую гиперплоскость между двумя разными наборами точек данных. После получения разделяющей гиперплоскости становится относительно проще классифицировать точки данных (контрольный пример) в один из классов. SVM хорошо работает даже с данными более высокой размерности. Преимущества SVM в том, что они эффективны с точки зрения памяти, точны и быстры по сравнению с другими моделями машинного обучения. Давайте посмотрим на математику SVM
Основная проблема SVM
Двойная задача SVM с использованием лагранжиана
Происхождение Dual
CVXOPT
В этом разделе мы обсудим реализацию описанного выше двойного алгоритма SVM на Python с использованием библиотеки CVXOPT. Вы можете узнать больше о библиотеке (http://cvxopt.org/userguide/intro.html)
Общий формат CVXOPT для таких проблем
# SVM using CVXOPT import numpy as np from cvxopt import matrix,solvers def solve_SVM_dual_CVXOPT (x_train, y_train, x_test): """ Solves the SVM training optimisation problem (the Arguments: x_train: A numpy array with shape (n,d), denoting \R^d. y_train: numpy array with shape (n,) Each element x_train: A numpy array with shape (m,d), denoting dual) using cvxopt. n training samples in takes +1 or -1 m test samples in \R^d. Limits: n<200, d<100, m<1000 Returns: y_pred_test : A numpy array with shape (m,). This is the result of running the learned model on the test instances x_test. Each element is +1 or -1. """ n, d = x_train.shape c = 10 # let max \lambda value be 10 y_train = np.expand_dims(y_train, axis= 1 )* 1. P = (y_train * x_train).dot((y_train * x_train).T) q = -1. *np.ones((n, 1 )) G = np.vstack((np.eye(n)* -1 ,np.eye(n))) h = np.hstack((np.zeros(n), np.ones(n) * c)) A = y_train.reshape( 1 , -1 ) b = np.array([ 0.0 ]) P = matrix(P); q = matrix(q) G = matrix(G); h = matrix(h) A = matrix(A); b = matrix(b) sol = solvers.qp(P, q, G, h, A, b) lambdas = np.array(sol[ 'x' ]) w = ((y_train * lambdas).T.dot(x_train)).reshape( -1 , 1 ) b = y_train - np.dot(x_train, w) prediction = lambda x, w, b: np.sign(np.sum(w.T.dot(x) + b)) y_test = np.array([prediction(x_, w, b) for x_ in x_test]) return y_test if __name__ == "__main__" : # Example format of input to the functions n= 100 m= 100 d= 10 x_train = np.random.randn(n,d) x_test = np.random.randn(m,d) w = np.random.randn(d) y_train = np.sign(np.dot(x_train, w)) y_test = np.sign(np.dot(x_test, w)) y_pred_test = solve_SVM_dual_CVXOPT(x_train, y_train, x_test) check1 = np.sum(y_pred_test == y_test) print ( "Score: {}/{}" .format(check1, len(y_Test)))
Для более глубокого понимания см .:
● Численная оптимизация (Хорхе Нокедал и Стивен Дж. Райт)
● Нелинейное программирование (Ю. Нестров)
Полный код, использованный в этом посте, можно найти
(https://github.com/koriavinash1/Numerical-Optimization)