Искусственные нейронные сети (ИНС) сейчас очень популярны, и это правильно.
Они используются повсеместно в крупных технологических компаниях. Например, когда вы используете переводчик Google или когда рекомендации появляются в вашей ленте Netflix, за кулисами используются сложные искусственные нейронные сети. За успехом Альфа Го в игре Го против Ли Седоля для определения следующего лучшего хода использовалась ИНС.
В этой статье я расскажу о причинах популярности нейронных сетей.
Спойлер: это связано с тем, что ИНС являются универсальными аппроксиматорами функций.
Я включил код Julia, чтобы проиллюстрировать, как все работает на практике. Я предпочитаю инструмент Julia, потому что он очень быстрый и становится все более популярным языком программирования (несложно научиться, если вы уже используете Python). Для задач машинного обучения Flux.jl — действительно хороший вариант, поэтому воспользуемся и им. Скачать код можно здесь.
I. Немного теории
Архитектура
Если вы оказались здесь, вы, вероятно, уже знаете, что такое искусственная нейронная сеть. Поэтому буду краток. В двух словах, нейронная сеть состоит из нескольких взаимосвязанных слоев. Каждый слой состоит из узлов. Узлы между соседними слоями обмениваются информацией между собой. То, как узлы взаимодействуют друг с другом, фиксируется значениями параметров, связанных с каждым узлом.
См. график ниже:
Источник: https://en.wikipedia.org/wiki/Artificial_neural_network#/media/File:Artificial_neural_network.svg
Искусственная нейронная сеть на высоком уровне имитирует то, что делает мозг. Мозг состоит из нейронов, и нейроны связаны друг с другом через синапсы. Наш мозг очень хорошо распознает закономерности, поэтому можно надеяться, что искусственная нейронная сеть может стать хорошей машиной для распознавания образов.
На практике так оно и есть. Более того, у нас есть несколько теорем, которые говорят нам, что ИНС действительно хороши.
Теоремы универсального приближения
Позвольте мне описать два важных документа. Ниже я воспроизвожу некоторые избранные части их тезисов:
Хорник, Стинчкомб и Уайт (1989)
«В этой статье строго установлено, что стандартные многослойные сети прямой связи с одним скрытым слоем, использующие произвольные функции сжатия, способны аппроксимировать любую измеримую по Борелю функцию из одного конечномерного пространства в другое в любой желаемой степени. точности при условии, что доступно достаточно много скрытых единиц. В этом смысле многослойные сети с прямой связью представляют собой класс универсальных аппроксиматоров».
«Показано, что сети с прямой связью с одним слоем сигмоидальных нелинейностей достигают интегральной квадратичной ошибки порядка O(1/n), где n — количество узлов. […] Для рассматриваемого класса функций здесь скорость аппроксимации и экономичность параметризации сетей на удивление выгодны в многомерных настройках».
В статье Хорника Стинчкомба и Уайта (1989) говорится, что очень большой класс функций может быть аппроксимирован ИНС с архитектурой, которую мы представили выше. Базовая функция, которую мы стремимся аппроксимировать, должна быть «измеримой по Борелю» (из одного конечномерного пространства в другое), что содержит почти все полезные функции, которые вы используете в экономике (непрерывные функции из одного конечномерного пространства в другое — это борелевские функции). измеримые функции).
В статье Баррона (1993) говорится, что ИНС являются особенно хорошими аппроксиматорами при работе со многими измерениями. Иными словами, ИНС может помочь смягчить проклятие размерности. Один из способов понять проклятие размерности состоит в том, что количество точек, необходимых для аппроксимации функции, растет экспоненциально с количеством измерений, а не линейно. Мы хотели бы объяснить сложное явление со многими измерениями и взаимодействиями, но традиционные методы аппроксимации обычно плохо работают в таких условиях.
В совокупности эти результаты говорят нам, что ИНС являются очень хорошими аппроксиматорами функций, даже когда количество измерений велико.
II. Приложение
Теперь давайте посмотрим два приложения. Чтобы разогреться, мы начнем с гладкой и приятной функции для аппроксимации. Затем мы перейдем к более сложной функции.
II. А. Простая функция
Давайте загрузим несколько полезных пакетов и определим функцию, которую мы хотели бы аппроксимировать.
# Dependencies using Flux using Plots using LinearAlgebra using ProgressMeter using Statistics using LaTeXStrings using Surrogates gr()
# Define function that we would like to learn with our neural network f(x) = x[1].^2 + x[2].^2
f (generic function with 1 method)
Функция — это бесконечномерный объект. Но нам нужно конечное количество значений для обучения нашей нейронной сети. Для этого создадим выборку точек из интервала (я использую выборку по Соболю), а затем оценим значение истинной функции для этих точек.
n_samples = 100 lower_bound = [-1.0, -1.0] upper_bound = [1.0, 1.0]
xys = Surrogates.sample(n_samples, lower_bound, upper_bound, SobolSample()) rawInputs = xys rawOutputs = [[f(xy)] for xy in xys] # Compute outputs for each input trainingData = zip(rawInputs, rawOutputs);
Теперь самое интересное — определить архитектуру нашей ИНС. Я выбираю два скрытых слоя. Количество узлов для первого слоя определяется размерностью входных данных (двухмерный вектор), а также размерностью конечного узла (скаляр). Нам все еще нужно выбрать количество узлов между ними. Для первого скрытого слоя я выбираю 784 узла и 50 для второго скрытого слоя. Справедливости ради, эти выборы немного случайны (на меня повлиял учебник по Flux.jl здесь). Не стесняйтесь экспериментировать с разными значениями.
# Define the neural network layers (this defines a function called model(x)) # Specify our model dim_input = 2 dim_ouptut = 1 Q1 = 784; Q2 = 50;
# Two inputs, one output model = Chain(Dense(2,Q1,relu), Dense(Q1,Q2,relu), Dense(Q2,1,identity));
Затем мы определяем функцию потерь, которая измеряет точность приближения. Чем меньше потери, тем лучше. Используем функцию потерь среднеквадратичная ошибка. Название игры состоит в том, чтобы найти значения параметров, которые минимизируют функцию потерь. Один из способов минимизировать функцию потерь — использовать алгоритм градиентного спуска.
Вот интуитивное объяснение градиентного спуска. Представьте, что вы находитесь на вершине горы, а вокруг много тумана, который мешает вам видеть далеко. Ты очень хочешь спуститься. Что вы должны сделать?
Одна из стратегий заключается в том, чтобы посмотреть, где вы стоите, и оценить направление наибольшего спуска в окрестности вашего местоположения (вы не можете видеть далеко). Тогда сделайте шаг в этом направлении. Затем повторите процесс. Если гора «хорошо себя ведет» (у нее нет локальных минимумов), вам удастся спуститься с горы, даже если вы просто использовали локальную информацию на каждом шагу. (Перейдите в самый низ этого сообщения в блоге, чтобы увидеть иллюстрацию градиентного спуска для очень простой задачи).
# Define loss function and weights loss(x, y) = Flux.Losses.mse(model(collect(x)), y)
lr = 0.001 # learning rate
# V1. Gradient descent opt = Descent(lr)
# V2. ADAM #decay = 0.9 #momentum =0.999 #opt = ADAM(lr, (decay, momentum))
epochs = 1000 # Define the number of epochs trainingLosses = zeros(epochs);# Initialize a vector to keep track of the training progress
Далее следует самый полезный шаг: обучающая часть. Следующий блок кода выполняет градиентный спуск. Функция Flux.train!
использует все наблюдения, которые есть в нашей выборке, один раз. Поскольку одной итерации недостаточно для достижения минимума, мы повторяем процесс несколько epochs
. После каждой эпохи мы вычисляем среднеквадратичную ошибку, чтобы увидеть, насколько хорошо работает модель.
ps = Flux.params(model) #initialize weigths p = Progress(epochs; desc = "Training in progress"); # Creates a progress bar showProgress = true
# Training loop @time for ii in 1:epochs
Flux.train!(loss, ps, trainingData, opt)
# Update progress indicator if showProgress trainingLosses[ii] = mean([loss(x,y) for (x,y) in trainingData]) next!(p; showvalues = [(:loss, trainingLosses[ii]), (:logloss, log10.(trainingLosses[ii]))], valuecolor = :grey) end
end
24.753884 seconds (41.00 M allocations: 37.606 GiB, 6.56% gc time, 0.48% compilation time)
На следующем графике показан поверхностный график для исходной функции и значения, возвращаемого ИНС (точки). Результаты неплохие. Верхний правый график отображает значение функции потерь по мере прохождения обучения. Градиентный спуск, кажется, работает хорошо, потому что функция потерь убывает красивым и монотонным образом. Нижний график отображает график поверхности для обученной ИНС.
II.Б. Более сложная функция
Итак, наша ИНС работает с простой функцией, что обнадеживает. Теперь обратимся к более сложной функции. Например, мы можем попытаться аппроксимировать функцию Экли, которая представляет собой слегка сумасшедшую функцию, часто используемую для тестирования алгоритмов минимизации (у нее есть глобальный минимум в начале координат).
Даже с более сложной функцией наша ИНС отлично справляется с приближением истинной функции, как вы можете видеть на графике ниже.
function ackley(x; e = exp(1), a = 10.0, b = -0.2, c=2.0*π) #a, b, c = 10.0, -0.2, 2.0*π len_recip = inv(length(x)) sum_sqrs = zero(eltype(x)) sum_cos = sum_sqrs for i in x sum_cos += cos(c*i) sum_sqrs += i^2 end return -a * exp(b * sqrt(len_recip*sum_sqrs)) - exp(len_recip*sum_cos) + a + e end
n_samples = 1000 lower_bound = [-2.0, -2.0] upper_bound = [2.0, 2.0] xys = Surrogates.sample(n_samples, lower_bound, upper_bound, SobolSample()) rawInputs = xys
rawOutputs = [[ackley(xy)] for xy in xys] # Compute outputs for each input trainingData = zip(rawInputs, rawOutputs);
# Define the neural network layers (this defines a function called model(x)) # Specify our model Q1 = 784; Q2 = 50; Q3 = 10;
# Two inputs, one output model = Chain(Dense(2,Q1,relu), Dense(Q1,Q2,relu), Dense(Q2,1,identity));
# Define loss function and weights loss(x, y) = Flux.Losses.mse(model(collect(x)), y) ps = Flux.params(model)
# Train the neural network epochs = 1000 showProgress = true lr = 0.001 # learning rate
# Gradient descent opt = Descent(lr)
trainingLosses = zeros(epochs) # Initialize vectors to keep track of training p = Progress(epochs; desc = "Training in progress") # Creates a progress bar
@time for ii in 1:epochs
Flux.train!(loss, ps, trainingData, opt)
# Update progress indicator if showProgress trainingLosses[ii] = mean([loss(x,y) for (x,y) in trainingData]) next!(p; showvalues = [(:loss, trainingLosses[ii]), (:logloss, log10.(trainingLosses[ii]))], valuecolor = :grey) end
end
242.064635 seconds (407.63 M allocations: 375.931 GiB, 6.81% gc time, 0.04% compilation time)
Заключение
Искусственная нейронная сеть — это универсальные аппроксиматоры функций. В этом сообщении в блоге объясняется, что это значит, и показано, как начать использовать ИНС для аппроксимации относительно простых функций.
Этот пост в блоге изначально был размещен здесь, на моем веб-сайте:
https://julienpascal.github.io/post/ann_1/
Дополнительно: градиентный спуск визуально
Ниже приведена иллюстрация градиентного спуска. Мы хотим найти минимум функции J(x)=x^2
и начинаем с точки -20
.
Алгоритм работает итеративно:
- Вычислите градиент при текущем значении. Это дает нам направление максимального изменения функции
J
. - Поскольку мы ищем минимум, а не максимум, сделайте шаг в направлении, противоположном максимальному изменению.
- Повторите шаги 1–2
using GradDescent #Code from here: https://jacobcvt12.github.io/GradDescent.jl/stable/ #I made only slight modifications to the original code
# objective function and gradient of objective function J(x) = x^2 dJ(x) = 2*x
# number of epochs epochs = 150
# instantiation of Adagrad optimizer with learning rate of 2 opt = Adagrad(η=2.0)
# initial value for x (usually initialized with a random value) x = -20.0 #initial position on the function values_x = zeros(epochs) #initialization value_y = zeros(epochs) #initialization iter_x = zeros(epochs) #initialization
for i in 1:epochs # Save values for plotting values_x[i] = x value_y[i] = J(x) iter_x[i] = i
# calculate the gradient wrt to the current x g = dJ(x)
# change to the current x δ = update(opt, g) x -= δ end
Как вы можете видеть на графике ниже, мы начинаем с левой стороны, а затем делаем довольно большие движения вправо. Со временем точки меняют цвет с желтого на более темный. Примерно после 150 итераций мы очень близки к истинному минимуму на уровне 0.
plot(values_x, value_y, label="J(x)")
scatter!(values_x, value_y, marker_z = iter_x, color = cgrad(:thermal, rev = true), label="Position", colorbar_title="Iteration")
xlabel!(L"x")
ylabel!(L"J(x)")