Нейронная сеть против DDoS'а. Как создать собственную нейронную сеть с нуля на языке Python

21.07.2019
  • Программирование
    • Перевод

    О чём статья

    Лично я лучше всего обучаюсь при помощи небольшого работающего кода, с которым могу поиграться. В этом пособии мы научимся алгоритму обратного распространения ошибок на примере небольшой нейронной сети, реализованной на Python.

    Дайте код!

    X = np.array([ ,,, ]) y = np.array([]).T syn0 = 2*np.random.random((3,4)) - 1 syn1 = 2*np.random.random((4,1)) - 1 for j in xrange(60000): l1 = 1/(1+np.exp(-(np.dot(X,syn0)))) l2 = 1/(1+np.exp(-(np.dot(l1,syn1)))) l2_delta = (y - l2)*(l2*(1-l2)) l1_delta = l2_delta.dot(syn1.T) * (l1 * (1-l1)) syn1 += l1.T.dot(l2_delta) syn0 += X.T.dot(l1_delta)

    Слишком сжато? Давайте разобьём его на более простые части.

    Часть 1: Небольшая игрушечная нейросеть

    Нейросеть, тренируемая через обратное распространение (backpropagation), пытается использовать входные данные для предсказания выходных.

    Предположим, нам нужно предсказать, как будет выглядеть колонка «выход» на основе входных данных. Эту задачу можно было бы решить, подсчитав статистическое соответствие между ними. И мы бы увидели, что с выходными данными на 100% коррелирует левый столбец.

    Обратное распространение, в самом простом случае, рассчитывает подобную статистику для создания модели. Давайте попробуем.

    Нейросеть в два слоя

    import numpy as np # Сигмоида def nonlin(x,deriv=False): if(deriv==True): return f(x)*(1-f(x)) return 1/(1+np.exp(-x)) # набор входных данных X = np.array([ , , , ]) # выходные данные y = np.array([]).T # сделаем случайные числа более определёнными np.random.seed(1) # инициализируем веса случайным образом со средним 0 syn0 = 2*np.random.random((3,1)) - 1 for iter in xrange(10000): # прямое распространение l0 = X l1 = nonlin(np.dot(l0,syn0)) # насколько мы ошиблись? l1_error = y - l1 # перемножим это с наклоном сигмоиды # на основе значений в l1 l1_delta = l1_error * nonlin(l1,True) # !!! # обновим веса syn0 += np.dot(l0.T,l1_delta) # !!! print "Выходные данные после тренировки:" print l1

    Выходные данные после тренировки: [[ 0.00966449] [ 0.00786506] [ 0.99358898] [ 0.99211957]]

    Переменные и их описания.






    "*" - поэлементное умножение – два вектора одного размера умножают соответствующие значения, и на выходе получается вектор такого же размера
    "-" – поэлементное вычитание векторов
    x.dot(y) – если x и y – это вектора, то на выходе получится скалярное произведение. Если это матрицы, то получится перемножение матриц. Если матрица только одна из них – это перемножение вектора и матрицы.

    • сравните l1 после первой итерации и после последней
    • посмотрите на функцию nonlin.
    • посмотрите, как меняется l1_error
    • разберите строку 36 – основные секретные ингредиенты собраны тут (отмечена!!!)
    • разберите строку 39 – вся сеть готовится именно к этой операции (отмечена!!!)

    Разберём код по строчкам

    import numpy as np

    Импортирует numpy, библиотеку линейной алгебры. Единственная наша зависимость.

    Def nonlin(x,deriv=False):

    Наша нелинейность. Конкретно эта функция создаёт «сигмоиду». Она ставит в соответствие любое число значению от 0 до 1 и преобразовывает числа в вероятности, а также имеет несколько других полезных для тренировки нейросетей свойств.

    If(deriv==True):

    Эта функция также умеет выдавать производную сигмоиды (deriv=True). Это одно из её полезных свойств. Если выход функции – это переменная out, тогда производная будет out * (1-out). Эффективно.

    X = np.array([ , …

    Инициализация массива входных данных в виде numpy-матрицы. Каждая строка – тренировочный пример. Столбцы – это входные узлы. У нас получается 3 входных узла в сети и 4 тренировочных примера.

    Y = np.array([]).T

    Инициализирует выходные данные. ".T" – функция переноса. После переноса у матрицы y есть 4 строки с одним столбцом. Как и в случае входных данных, каждая строка – это тренировочный пример, и каждый столбец (в нашем случае один) – выходной узел. У сети, получается, 3 входа и 1 выход.

    Np.random.seed(1)

    Благодаря этому случайное распределение будет каждый раз одним и тем же. Это позволит нам проще отслеживать работу сети после внесения изменений в код.

    Syn0 = 2*np.random.random((3,1)) – 1

    Матрица весов сети. syn0 означает «synapse zero». Так как у нас всего два слоя, вход и выход, нам нужна одна матрица весов, которая их свяжет. Её размерность (3, 1), поскольку у нас есть 3 входа и 1 выход. Иными словами, l0 имеет размер 3, а l1 – 1. Поскольку мы связываем все узлы в l0 со всеми узлами l1, нам требуется матрица размерности (3, 1).

    Заметьте, что она инициализируется случайным образом, и среднее значение равно нулю. За этим стоит достаточно сложная теория. Пока просто примем это как рекомендацию. Также заметим, что наша нейросеть – это и есть эта самая матрица. У нас есть «слои» l0 и l1, но они представляют собой временные значения, основанные на наборе данных. Мы их не храним. Всё обучение хранится в syn0.

    For iter in xrange(10000):

    Тут начинается основной код тренировки сети. Цикл с кодом повторяется многократно и оптимизирует сеть для набора данных.

    Первый слой, l0, это просто данные. В X содержится 4 тренировочных примера. Мы обработаем их все и сразу – это называется групповой тренировкой . Итого мы имеем 4 разных строки l0, но их можно представить себе как один тренировочный пример – на этом этапе это не имеет значения (можно было загрузить их 1000 или 10000 без всяких изменений в коде).

    L1 = nonlin(np.dot(l0,syn0))

    Это шаг предсказания. Мы позволяем сети попробовать предсказать вывод на основе ввода. Затем мы посмотрим, как это у неё получается, чтобы можно было подправить её в сторону улучшения.

    В строке содержится два шага. Первый делает матричное перемножение l0 и syn0. Второй передаёт вывод через сигмоиду. Размерности у них следующие:

    (4 x 3) dot (3 x 1) = (4 x 1)

    Матричные умножения требуют, чтобы в середине уравнения размерности совпадали. Итоговая матрица имеет количество строк, как у первой, а столбцов – как у второй.

    Мы загрузили 4 тренировочных примера, и получили 4 догадки (матрица 4х1). Каждый вывод соответствует догадке сети для данного ввода.

    L1_error = y - l1

    Поскольку в l1 содержатся догадки, мы можем сравнить их разницу с реальностью, вычитая её l1 из правильного ответа y. l1_error – вектор из положительных и отрицательных чисел, характеризующий «промах» сети.

    А вот и секретный ингредиент. Эту строку нужно разбирать по частям.

    Первая часть: производная

    Nonlin(l1,True)

    L1 представляет три этих точки, а код выдаёт наклон линий, показанных ниже. Заметьте, что при больших значениях вроде x=2.0 (зелёная точка) и очень малые, вроде x=-1.0 (фиолетовая) линии имеют небольшой уклон. Самый большой угол у точки х=0 (голубая). Это имеет большое значение. Также отметьте, что все производные лежат в пределах от 0 до 1.

    Полное выражение: производная, взвешенная по ошибкам

    L1_delta = l1_error * nonlin(l1,True)

    Математически существуют более точные способы, но в нашем случае подходит и этот. l1_error – это матрица (4,1). nonlin(l1,True) возвращает матрицу (4,1). Здесь мы поэлементно их перемножаем, и на выходе тоже получаем матрицу (4,1), l1_delta.

    Умножая производные на ошибки, мы уменьшаем ошибки предсказаний, сделанных с высокой уверенностью. Если наклон линии был небольшим, то в сети содержится либо очень большое, либо очень малое значение. Если догадка в сети близка к нулю (х=0, у=0,5), то она не особенно уверенная. Мы обновляем эти неуверенные предсказания и оставляем в покое предсказания с высокой уверенностью, умножая их на величины, близкие к нулю.

    Syn0 += np.dot(l0.T,l1_delta)

    Мы готовы к обновлению сети. Рассмотрим один тренировочный пример. В нём мы будем обновлять веса. Обновим крайний левый вес (9.5)

    Weight_update = input_value * l1_delta

    Для крайнего левого веса это будет 1.0 * l1_delta. Предположительно, это лишь незначительно увеличит 9.5. Почему? Поскольку предсказание было уже достаточно уверенным, и предсказания были практически правильными. Небольшая ошибка и небольшой наклон линии означает очень небольшое обновление.

    Но поскольку мы делаем групповую тренировку, указанный выше шаг мы повторяем для всех четырёх тренировочных примеров. Так что это выглядит очень похоже на изображение вверху. Так что же делает наша строчка? Она подсчитывает обновления весов для каждого веса, для каждого тренировочного примера, суммирует их и обновляет все веса – и всё одной строкой.

    Понаблюдав за обновлением сети, вернёмся к нашим тренировочным данным. Когда и вход, и выход равны 1, мы увеличиваем вес между ними. Когда вход 1, а выход – 0, мы уменьшаем вес.

    Вход Выход 0 0 1 0 1 1 1 1 1 0 1 1 0 1 1 0

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

    Часть 2: задачка посложнее

    Вход Выход 0 0 1 0 0 1 1 1 1 0 1 1 1 1 1 0

    Попробуем предсказать выходные данные на основе трёх входных столбцов данных. Ни один из входных столбцов не коррелирует на 100% с выходным. Третий столбец вообще ни с чем не связан, поскольку в нём всю дорогу содержатся единицы. Однако и тут можно увидеть схему – если в одном из двух первых столбцов (но не в обоих сразу) содержится 1, то результат также будет равен 1.

    Это нелинейная схема, поскольку прямого соответствия столбцов один к одному не существует. Соответствие строится на комбинации входных данных, столбцов 1 и 2.

    Интересно, что распознавание образов является очень похожей задачей. Если у вас есть 100 картинок одинакового размера, на которых изображены велосипеды и курительные трубки, присутствие на них определённых пикселей в определённых местах не коррелирует напрямую с наличием на изображении велосипеда или трубки. Статистически их цвет может казаться случайным. Но некоторые комбинации пикселей не случайны – те, что формируют изображение велосипеда (или трубки).


    Стратегия

    Чтобы скомбинировать пиксели в нечто, у чего может появиться однозначное соответствие с выходными данными, нужно добавить ещё один слой. Первый слой комбинирует вход, второй назначает соответствие выходу, используя в качестве входных данных выходные данные первого слоя. Обратите внимание на таблицу.

    Вход (l0) Скрытые веса (l1) Выход (l2) 0 0 1 0.1 0.2 0.5 0.2 0 0 1 1 0.2 0.6 0.7 0.1 1 1 0 1 0.3 0.2 0.3 0.9 1 1 1 1 0.2 0.1 0.3 0.8 0

    Случайным образом назначив веса, мы получим скрытые значения для слоя №1. Интересно, что у второго столбца скрытых весов уже есть небольшая корреляция с выходом. Не идеальная, но есть. И это тоже является важной частью процесса тренировки сети. Тренировка будет только усиливать эту корреляцию. Она будет обновлять syn1, чтобы назначить её соответствие выходным данным, и syn0, чтобы лучше получать данные со входа.

    Нейросеть в три слоя

    import numpy as np def nonlin(x,deriv=False): if(deriv==True): return f(x)*(1-f(x)) return 1/(1+np.exp(-x)) X = np.array([, , , ]) y = np.array([, , , ]) np.random.seed(1) # случайно инициализируем веса, в среднем - 0 syn0 = 2*np.random.random((3,4)) - 1 syn1 = 2*np.random.random((4,1)) - 1 for j in xrange(60000): # проходим вперёд по слоям 0, 1 и 2 l0 = X l1 = nonlin(np.dot(l0,syn0)) l2 = nonlin(np.dot(l1,syn1)) # как сильно мы ошиблись относительно нужной величины? l2_error = y - l2 if (j% 10000) == 0: print "Error:" + str(np.mean(np.abs(l2_error))) # в какую сторону нужно двигаться? # если мы были уверены в предсказании, то сильно менять его не надо l2_delta = l2_error*nonlin(l2,deriv=True) # как сильно значения l1 влияют на ошибки в l2? l1_error = l2_delta.dot(syn1.T) # в каком направлении нужно двигаться, чтобы прийти к l1? # если мы были уверены в предсказании, то сильно менять его не надо l1_delta = l1_error * nonlin(l1,deriv=True) syn1 += l1.T.dot(l2_delta) syn0 += l0.T.dot(l1_delta)

    Error:0.496410031903 Error:0.00858452565325 Error:0.00578945986251 Error:0.00462917677677 Error:0.00395876528027 Error:0.00351012256786

    Переменные и их описания

    X - матрица входного набор данных; строки – тренировочные примеры
    y – матрица выходного набора данных; строки – тренировочные примеры
    l0 – первый слой сети, определённый входными данными
    l1 – второй слой сети, или скрытый слой
    l2 – финальный слой, это наша гипотеза. По мере тренировки должен приближаться к правильному ответу
    syn0 – первый слой весов, Synapse 0, объединяет l0 с l1.
    syn1 – второй слой весов, Synapse 1, объединяет l1 с l2.
    l2_error – промах сети в количественном выражении
    l2_delta – ошибка сети, в зависимости от уверенности предсказания. Почти совпадает с ошибкой, за исключением уверенных предсказаний
    l1_error – взвешивая l2_delta весами из syn1, мы подсчитываем ошибку в среднем/скрытом слое
    l1_delta – ошибки сети из l1, масштабируемые по увеернности предсказаний. Почти совпадает с l1_error, за исключением уверенных предсказаний

    Код должен быть достаточно понятным – это просто предыдущая реализация сети, сложенная в два слоя один над другим. Выход первого слоя l1 – это вход второго слоя. Что-то новое есть лишь в следующей строке.

    L1_error = l2_delta.dot(syn1.T)

    Использует ошибки, взвешенные по уверенности предсказаний из l2, чтобы подсчитать ошибку для l1. Получаем, можно сказать, ошибку, взвешенную по вкладам – мы подсчитываем, какой вклад в ошибки в l2 вносят значения в узлах l1. Этот шаг и называется обратным распространением ошибок. Затем мы обновляем syn0, используя тот же алгоритм, что и в варианте с нейросетью из двух слоёв.

    В этот раз я решил изучить нейронные сети. Базовые навыки в этом вопросе я смог получить за лето и осень 2015 года. Под базовыми навыками я имею в виду, что могу сам создать простую нейронную сеть с нуля. Примеры можете найти в моих репозиториях на GitHub. В этой статье я дам несколько разъяснений и поделюсь ресурсами, которые могут пригодиться вам для изучения.

    Шаг 1. Нейроны и метод прямого распространения

    Так что же такое «нейронная сеть»? Давайте подождём с этим и сперва разберёмся с одним нейроном.

    Нейрон похож на функцию: он принимает на вход несколько значений и возвращает одно.

    Круг ниже обозначает искусственный нейрон. Он получает 5 и возвращает 1. Ввод - это сумма трёх соединённых с нейроном синапсов (три стрелки слева).

    В левой части картинки мы видим 2 входных значения (зелёного цвета) и смещение (выделено коричневым цветом).

    Входные данные могут быть численными представлениями двух разных свойств. Например, при создании спам-фильтра они могли бы означать наличие более чем одного слова, написанного ЗАГЛАВНЫМИ БУКВАМИ, и наличие слова «виагра».

    Входные значения умножаются на свои так называемые «веса», 7 и 3 (выделено синим).

    Теперь мы складываем полученные значения со смещением и получаем число, в нашем случае 5 (выделено красным). Это - ввод нашего искусственного нейрона.

    Потом нейрон производит какое-то вычисление и выдает выходное значение. Мы получили 1, т.к. округлённое значение сигмоиды в точке 5 равно 1 (более подробно об этой функции поговорим позже).

    Если бы это был спам-фильтр, факт вывода 1 означал бы то, что текст был помечен нейроном как спам.

    Иллюстрация нейронной сети с Википедии.

    Если вы объедините эти нейроны, то получите прямо распространяющуюся нейронную сеть - процесс идёт от ввода к выводу, через нейроны, соединённые синапсами, как на картинке слева.

    Шаг 2. Сигмоида

    После того, как вы посмотрели уроки от Welch Labs, хорошей идеей было бы ознакомиться с четвертой неделей курса по машинному обучению от Coursera , посвящённой нейронным сетям - она поможет разобраться в принципах их работы. Курс сильно углубляется в математику и основан на Octave, а я предпочитаю Python. Из-за этого я пропустил упражнения и почерпнул все необходимые знания из видео.

    Сигмоида просто-напросто отображает ваше значение (по горизонтальной оси) на отрезок от 0 до 1.

    Первоочередной задачей для меня стало изучение сигмоиды , так как она фигурировала во многих аспектах нейронных сетей. Что-то о ней я уже знал из третьей недели вышеупомянутого курса , поэтому я пересмотрел видео оттуда.

    Но на одних видео далеко не уедешь. Для полного понимания я решил закодить её самостоятельно. Поэтому я начал писать реализацию алгоритма логистической регрессии (который использует сигмоиду).

    Это заняло целый день, и вряд ли результат получился удовлетворительным. Но это неважно, ведь я разобрался, как всё работает. Код можно увидеть .

    Вам необязательно делать это самим, поскольку тут требуются специальные знания - главное, чтобы вы поняли, как устроена сигмоида.

    Шаг 3. Метод обратного распространения ошибки

    Понять принцип работы нейронной сети от ввода до вывода не так уж и сложно. Гораздо сложнее понять, как нейронная сеть обучается на наборах данных. Использованный мной принцип называется

    Искусственные нейронные сети (ИНС) - математические модели, а также их программные или аппаратные реализации, построенные по принципу организации и функционирования биологических нейронных сетей - сетей нервных клеток живого организма.

    ИНС представляют собой систему соединённых и взаимодействующих между собой простых процессоров (искусственных нейронов).

    Нейронные сети не программируются в привычном смысле этого слова, они обучаются. Возможность обучения - одно из главных преимуществ нейронных сетей перед традиционными алгоритмами. wikipedia

    Нейронные сети были вдохновлены нашим собственным мозгом. Модель стандартного нейрона изобретена более пятидесяти лет назад и состоит из трех основных частей:

    • Дентрит(ы) (Dendrite) - ответственны за сбор поступающих сигналов;
    • Сома (Soma) - ответствена за основную обработку и суммирование сигналов;
    • Аксон (Axon) - отвечает за передачу сигналов другим дендритам.

    Работу нейрона можно описать примерно так: дендриды собирают сигналы, полученные от других нейронов, затем сомы выполняют суммирование и вычисление сигналов и данных, и наконец на основе результата обработки могут "сказать" аксонам передать сигнал дальше. Передача далее зависит от ряда факторов, но мы можем смоделировать это поведение как передаточную функцию, которая принимает входные данные, обрабатывает их и готовит выходные данные, если выполняются свойства передаточной функции.

    Биологический нейрон - сложная система, математическая модель которого до сих пор полностью не построена. Введено множество моделей, различающихся вычислительной сложностью и сходством с реальным нейроном. Одна из важнейших - формальный нейрон (ФН). Несмотря на простоту ФН, сети, построенные из таких нейронов, могут сформировать произвольную много мерную функцию на выходе (источник: Заенцев И. В. Нейронные сети: основные модели).

    Нейрон состоит из взвешенного сумматора и нелинейного элемента. Функционирование нейрона определяется формулами:

    Нейрон имеет несколько входных сигналов x и один выходной сигнал OUT . Параметрами нейрона, определяющими его работу, являются: вектор весов w , пороговый уровень θ и вид функции активации F .

    Нейронные сети привлекают к себе внимание за счет следующих возможностей:

    • способны решать трудно формализуемые задачи;
    • присущ параллельный принцип работы, что очень важно при обработке больших объемов данных;
    • способность к обучению и способность к обобщению;
    • толерантность к ошибкам;

    К основным свойствам нейронных сетей можно отнести:

      Способность обучаться . Нейронные сети не программируются, а обучаются на примерах. После предъявления входных сигналов (возможно, вместе с требуемыми выходами) сеть настраивают свои параметры таким образом, чтобы обеспечивать требуемую реакцию.

      Обобщение . Отклик сети после обучения может быть до некоторой степени нечувствителен к небольшим изменениям входных сигналов. Эта внутренне присущая способность "видеть"" образ сквозь шум и искажения очень важна для распознавания образов. Важно отметить, что искусственная нейронная сеть делает обобщения автоматически благодаря своей структуре, а не с помощью использования "человеческого интеллекта"" в форме специально написанных компьютерных программ.

      Параллелизм . Информация в сети обрабатывается параллельно, что позволяет достаточно выполнять сложную обработку данных с помощью большого числа простых устройств.

      Высокая надежность . Сеть может правильно функционировать даже при выходе из строя части нейронов, за счет того, что вычисления производятся локально и параллельно.

    Алгоритм решения задач с помощью многослойного персептрона (источник: Заенцев И. В. Нейронные сети: основные модели)

    Чтобы построить многослойный персептрон, необходимо выбрать его параметры. Чаще всего выбор значений весов и порогов требует обучения, т.е. пошаговых изменений весовых коэффициентов и пороговых уровней.

    Общий алгоритм решения:

    1. Определить, какой смысл вкладывается в компоненты входного вектора x . Входной вектор должен содержать формализованное условие задачи, т.е. всю информацию, необходимую для получения ответа.
    2. Выбрать выходной вектор y таким образом, чтобы его компоненты содержали полный ответ поставленной задачи.
    3. Выбрать вид нелинейности в нейронах (функцию активации). При этом желательно учесть специфику задачи, т.к. удачный выбор сократит время обучения.
    4. Выбрать число слоев и нейронов в слое.
    5. Задать диапазон изменения входов, выходов, весов и пороговых уровней, учитывая множество значений выбранной функции активации.
    6. Присвоить начальные значения весовым коэффициентам и пороговым уровням и дополнительным параметрам (например, крутизне функции активации, если она будет настраиваться при обучении). Начальные значения не должны быть большими, чтобы нейроны не оказались в насыщении (на горизонтальном участке функции активации), иначе обучение будет очень медленным. Начальные значения не должны быть и слишком малыми, чтобы выходы большей части нейронов не были равны нулю, иначе обучение также замедлится.
    7. Провести обучение, т.е. подобрать параметры сети так, чтобы задача решалась наилучшим образом. По окончании обучения сеть готова решить задачи того типа, которым она обучена.
    8. Подать на вход сети условия задачи в виде вектора x . Рассчитать выходной вектор y , который и даст формализованное решение задачи.

    Решаемые проблемы

    Проблемы решаемые с помощью нейронных сетей ().

      Классификация образов . Задача состоит в указании принадлежности входного образа (например, речевого сигнала или рукописного символа), представленного вектором признаков, одному или нескольким предварительно определенным классам. К известным приложениям относятся распознавание букв, распознавание речи, классификация сигнала электрокардиограммы, классификация клеток крови.

      Кластеризация/категоризация . При решении задачи кластеризации, которая известна также как классификация образов "без учителя", отсутствует обучающая выборка с метками классов. Алгоритм кластеризации основан на подобии образов и размещает близкие образы в один кластер. Известны случаи применения кластеризации для извлечения знаний, сжатия данных и исследования свойств данных.

      Аппроксимация функций . Предположим, что имеется обучающая выборка ((x1,y1), (x2,y2)..., (xn,yn)) (пары данных вход-выход), которая генерируется неизвестной функцией (x), искаженной шумом. Задача аппроксимации состоит в нахождении оценки неизвестной функции (x). Аппроксимация функций необходима при решении многочисленных инженерных и научных задач моделирования.

      Предсказание/прогноз . Пусть заданы n дискретных отсчетов {y(t1), y(t2)..., y(tn)} в последовательные моменты времени t1, t2,..., tn . Задача состоит в предсказании значения y(tn+1) в некоторый будущий момент времени tn+1. Предсказание/прогноз имеют значительное влияние на принятие решений в бизнесе, науке и технике. Предсказание цен на фондовой бирже и прогноз погоды являются типичными приложениями техники предсказания/прогноза.

      Оптимизация . Многочисленные проблемы в математике, статистике, технике, науке, медицине и экономике могут рассматриваться как проблемы оптимизации. Задачей алгоритма оптимизации является нахождение такого решения, которое удовлетворяет системе ограничений и максимизирует или минимизирует целевую функцию. Задача коммивояжера, относящаяся к классу NP-полных, является классическим примером задачи оптимизации.

      Память, адресуемая по содержанию . В модели вычислений фон Неймана обращение к памяти доступно только посредством адреса, который не зависит от содержания памяти. Более того, если допущена ошибка в вычислении адреса, то может быть найдена совершенно иная информация. Ассоциативная память, или память, адресуемая по содержанию, доступна по указанию заданного содержания. Содержимое памяти может быть вызвано даже по частичному входу или искаженному содержанию. Ассоциативная память чрезвычайно желательна при создании мультимедийных информационных баз данных.

      Управление . Рассмотрим динамическую систему, заданную совокупностью {u(t), y(t)}, где u(t) является входным управляющим воздействием, а y(t) - выходом системы в момент времени t. В системах управления с эталонной моделью целью управления является расчет такого входного воздействия u(t), при котором система следует по желаемой траектории, диктуемой эталонной моделью. Примером является оптимальное управление двигателем.

    Виды архитектур

    Архитектура нейронной сети - способ организации и связи отдельных элементов нейросети(нейронов). Архитектурные отличия самих нейронов заключаются главным образом в использовании различных активационных (возбуждающих) функций. По архитектуре связей нейронные сети можно разделить на два класса: сети прямого распространения и рекуррентные сети.

    Классификация искусственных нейронных сетей по их архитектуре приведена на рисунке ниже.

    Похожая классификация, но немного расширенная

    Сеть прямого распространения сигнала (сеть прямой передачи) - нейронная сеть без обратных связей (петель). В такой сети обработка информации носит однонаправленный характер: сигнал передается от слоя к слою в направлении от входного слоя нейросети к выходному. Выходной сигнал (ответ сети) гарантирован через заранее известное число шагов (равное числу слоев). Сети прямого распространения просты в реализации, хорошо изучены. Для решения сложных задач требуют большого числа нейронов.

    Сравнительная таблица многослойного персепторна и RBF-сети

    Многослойный персептрон RBF-сети
    Граница решения представляет собой пересечение гиперплоскостей Граница решения - это пересечение гиперсфер, что задает границу более сложной формы
    Сложная топология связей нейронов и слоев Простая 2-слойная нейронная сеть
    Сложный и медленно сходящийся алгоритм обучения Быстрая процедура обучения: решение системы уравнений + кластеризация
    Работа на небольшой обучающей выборке Требуется значительное число обучающих данных для приемлемого результат
    Универсальность применения: кластеризация, аппроксимация, управление и проч Как правило, только аппроксимация функций и кластеризация

    Значение производной легко выражается через саму функцию. Быстрый расчет производной ускоряет обучение.

    Гауссова кривая

    Применяется в случаях, когда реакция нейрона должна быть максимальной для некоторого определенного значения NET.

    Модули python для нейронных сетей

    Простой пример

    В качестве примера приведу простую нейроную сеть (простой персептрон ), которая после обучения сможет распознавать летающие объекты, не все, а только чайку :), все остальные входные образы будут распознаваться как НЛО .

    # encoding=utf8 import random class NN: def __init__(self, threshold, size): """ Установим начальные параметры. """ self.threshold = threshold self.size = size self.init_weight() def init_weight(self): """ Инициализируем матрицу весов случайными данными. """ self.weights = [ for j in xrange(self.size)] def check(self, sample): """ Считаем выходной сигнал для образа sample. Если vsum > self.threshold то можно предположить, что в sample есть образ чайки. """ vsum = 0 for i in xrange(self.size): for j in xrange(self.size): vsum += self.weights[i][j] * sample[i][j] if vsum > self.threshold: return True else: return False def teach(self, sample): """ Обучение нейронной сети. """ for i in xrange(self.size): for j in xrange(self.size): self.weights[i][j] += sample[i][j] nn = NN(20, 6) # Обучаем нейронную сеть. tsample1 = [ , , , , , , ] nn.teach(tsample1) tsample2 = [ , , , , , , ] nn.teach(tsample2) tsample3 = [ , , , , , , ] nn.teach(tsample3) tsample4 = [ , , , , , , ] nn.teach(tsample4) # Проверим что может нейронная сеть. # Передадим образ чайки, который примерно похож на тот, про который знает персептрон. wsample1 = [ , , , , , , ] print u"чайка" if nn.check(wsample1) else u"НЛО" # Передадим неизвестный образ. wsample2 = [ , , , , , , ] print u"чайка" if nn.check(wsample2) else u"НЛО" # Передадим образ чайки, который примерно похож на тот, про который знает персептрон. wsample3 = [ , , , , , , ] print u"чайка" if nn.check(wsample3) else u"НЛО"

    Некоторые из вас наверняка недавно проходили Stanford"ские курсы, в частности ai-class и ml-class . Однако, одно дело просмотреть несколько видео-лекций, поотвечать на вопросики quiz"ов и написать десяток программ в Matlab / Octave , другое дело начать применять полученные знания на практике. Дабы знания полученые от Andrew Ng не угодили в тот же тёмный угол моего мозга, где заблудились dft , Специальная теория относительности и Уравнение Эйлера Лагранжа , я решил не повторять институтских ошибок и, пока знания ещё свежи в памяти, практиковаться как можно больше.

    И тут как раз на наш сайтик приехал DDoS. Отбиваться от которого можно было админско-программерскими (grep / awk / etc) способами или же прибегнуть к использованию технологий машинного обучения.

    Пример построения словаря и feature-vector"а

    Предположим, мы обучаем нашу нейронную сеть всего на двух примерах: одном хорошем и одном плохом. Потом попробуем её активировать на тестовой записи.

    Запись из "плохого" лога:
    0.0.0.0 - - "POST /forum/index.php HTTP/1.1" 503 107 "http://www.mozilla-europe.org/" "-"

    Запись из "хорошего" лога:
    0.0.0.0 - - "GET /forum/rss.php?topic=347425 HTTP/1.0" 200 1685 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.9) Gecko/2008052906 Firefox/3.0"

    Получившийся словарь:
    ["__UA___OS_U", "__UA_EMPTY", "__REQ___METHOD_POST", "__REQ___HTTP_VER_HTTP/1.0", "__REQ___URL___NETLOC_", "__REQ___URL___PATH_/forum/rss.php", "__REQ___URL___PATH_/forum/index.php", "__REQ___URL___SCHEME_", "__REQ___HTTP_VER_HTTP/1.1", "__UA___VER_Firefox/3.0", "__REFER___NETLOC_www.mozilla-europe.org", "__UA___OS_Windows", "__UA___BASE_Mozilla/5.0", "__CODE_503", "__UA___OS_pl", "__REFER___PATH_/", "__REFER___SCHEME_http", "__NO_REFER__", "__REQ___METHOD_GET", "__UA___OS_Windows NT 5.1", "__UA___OS_rv:1.9", "__REQ___URL___QS_topic", "__UA___VER_Gecko/2008052906"]

    Тестовая запись:
    0.0.0.0 - - "GET /forum/viewtopic.php?t=425550 HTTP/1.1" 502 107 "-" "BTWebClient/3000(25824)"

    Её feature-vector:

    Заметьте, насколько "разрежен" (sparse) feature-vector - такое поведение будет наблюдаться для всех запросов.

    Разделение Dataset"а

    Хорошей практикой является разделение dataset "а на несколько частей. Я бил на две части в пропорции 70/30:
    • Training set . На нём мы обучаем нашу нейронную сеть.
    • Test set . Им мы проверяем, насколько хорошо обучена наша нейронная сеть.
    Такое разбиение обусловлено тем фактом, что нейронная сеть с наименьшим training error (ошибкой на training set) будет выдавать бо льшую ошибку на новых данных, ибо мы «переобучили» сеть, заточив её под training set.
    В дальнейшем, если придётся озадачиться выбором оптимальных констант, dataset надо будет разбить на 3 части в соотношении 60/20/20: Training set , Test set и Cross validation . Последний как раз и будет служить для выбора оптимальных параметров нейронной сети (например weightdecay).

    Нейронная сеть в частности

    Теперь, когда у нас на руках больше нет никаких текстовых логов, а есть только матрицы из feature-vector "ов, можно приступать к построению самой нейронной сети.

    Начнём с выбора структуры. Я выбрал сеть из одного скрытого слоя размером с удвоенный входной слой. Почему? Всё просто: так завещал Andrew Ng в случае, если не знаете с чего начать. Думаю, в дальнейшем с этим можно поиграться, порисовав графики обучения.
    Функцией активации для скрытого слоя выбрана многострадальная сигмойда, а для выходного слоя - Softmax . Последний выбран на случай, если придётся делать
    многоклассовую класиффикацию c mutually exclusive классами. Например, "хорошие" запросы отправлять на бэкенд, "плохие" - в бан на фаерволе, а "серые" - разгадывать капчу.

    Нейронная сеть склонна к уходу в локальный минимум, поэтому у себя в коде я строю несколько сетей и выбираю ту, у которой наименьший Test error (Заметьте, именно ошибка на test set , а не trainig set).

    Disclaimer

    Я не настоящий сварщик. О Machine Learning я знаю только то, что подчерпнул из ml-class и ai-class. На питоне программировать начал относительно недавно, а код ниже был написан минут за 30 (время, как вы понимаете, поджимало) и в дальнейшем был лишь слегка подпилен напильником.

    Также этот код не самодостаточен. Ему всё равно нужна скриптовая обвязка. Например, если IP сделал N плохих запросов в течение X минут, то банить его на firewall"е.

    Производительность

    • lfu_cache. Портировал с ActiveState, дабы сильно ускорить обработку запросов-"высокочастотников". Down-side - повышенное потребление памяти.
    • PyBrain, внезапно, написан на python и поэтому не очень быстр, однако, он может использовать ATLAS-based -модуль arac , если при создании сети указать Fast=True . Подробнее про это можно почитать в документации к PyBrain .
    • Распараллеливание. Свою нейронную сеть я обучил на довольно "толстом" серверном Nehalem"е, однако, даже там чувствовалась ущербность однопоточного обучения. Можно поразмыслить на тему распараллеливания обучения нейронной сети. Простое решение - тренировать сразу несколько нейронных сетей параллельно и из них выбирать лучшую, но это создаст дополнительную нагрузку на память, что тоже не очень хорошо. Хотелось бы более универсальное решение. Возможно имеет смысл просто переписать всё на C, благо вся теоретическая база в ml-class"е была расжевана.
    • Потребление памяти и кол-во features. Хорошей оптимизацией по памяти являлся переход со стндартных питоновских массивов на numpy"ные. Так же уменьшение размера dictionary и/или использование PCA может очень хорошо помочь, об этом чуть ниже.

    На будущее

    • Дополнительные поля в лог. В combined лог можно добавить ещё много всего, стоит подумать на тему, какие поля помогут в идентификации ботов. Возможно, имеет смысл учитывать первый октет IP адреса, ибо в не интернациональном web-проекте китайские пользователи вероятнее всего боты.

    Мы сейчас переживаем настоящий бум нейронных сетей. Их применяют для распознания, локализации и обработки изображений. Нейронные сети уже сейчас умеют многое что не доступно человеку. Нужно же и самим вклиниваться в это дело! Рассмотрим нейтронную сеть которая будет распознавать числа на входном изображении. Все очень просто: всего один слой и функция активации. Это не позволит нам распознать абсолютно все тестовые изображения, но мы справимся с подавляющим большинством. В качестве данных будем использовать известную в мире распознания чисел подборку данных MNIST.

    Для работы с ней в Python есть библиотека python-mnist. Что-бы установить:

    Pip install python-mnist

    Теперь можем загрузить данные

    From mnist import MNIST mndata = MNIST("/path_to_mnist_data_folder/") tr_images, tr_labels = mndata.load_training() test_images, test_labels = mndata.load_testing()

    Архивы с данными нужно загрузить самостоятельно, а программе указать путь к каталогу с ними. Теперь переменные tr_images и test_images содержат изображения для тренировки сети и тестирования соотвественно. А переменные tr_labels и test_labels - метки с правильной классификацией (т.е. цифры с изображений). Все изображения имеют размер 28х28. Зададим переменную с размером.

    Img_shape = (28, 28)

    Преобразуем все данные в массивы numpy и нормализуем их (приведем к размеру от -1 до 1). Это увеличит точность вычислений.

    Import numpy as np for i in range(0, len(test_images)): test_images[i] = np.array(test_images[i]) / 255 for i in range(0, len(tr_images)): tr_images[i] = np.array(tr_images[i]) / 255

    Отмечу, что хоть и изображения принято представлять в виде двумерного массива мы будем использовать одномерный, это проще для вычислений. Теперь нужно понять "что же такое нейронная сеть"! А это просто уравнение с большим количеством коэффициентов. Мы имеем на входе массив из 28*28=784 элементов и еще по 784 веса для определения каждой цифры. В процессе работы нейронной сети нужно перемножить значения входов на веса. Сложить полученные данные и добавить смещение. Полученный результат подать на функцию активации. В нашем случае это будет Relu. Эта функция равна нулю для всех отрицательных аргументов и аргументу для всех положительных.

    Есть еще много функций активации! Но это же самая простая нейронная сеть! Определим эту функцию при помощи numpy

    Def relu(x): return np.maximum(x, 0)

    Теперь чтобы вычислить изображение на картинке нужно просчитать результат для 10 наборов коэффициентов.

    Def nn_calculate(img): resp = list(range(0, 10)) for i in range(0,10): r = w[:, i] * img r = relu(np.sum(r) + b[i]) resp[i] = r return np.argmax(resp)

    Для каждого набора мы получим выходной результат. Выход с наибольшим результатом вероятнее всего и есть наше число.

    В данном случае 7. Вот и все! Но нет... Ведь нужно эти самые коэффициенты где-то взять. Нужно обучить нашу нейронную сеть. Для этого применяют метод обратного распространения ошибки. Его суть в том чтобы рассчитать выходы сети, сравнить их с правильными, а затем отнять от коэффициентов числа необходимые чтобы результат был правильным. Нужно помнить, что для того чтобы вычислить эти значения нужна производная функции активации. В нашем случае она равна нулю для всех отрицательных чисел и 1 для всех положительных. Определим коэффициенты случайным образом.

    W = (2*np.random.rand(10, 784) - 1) / 10 b = (2*np.random.rand(10) - 1) / 10 for n in range(len(tr_images)): img = tr_images[n] cls = tr_labels[n] #forward propagation resp = np.zeros(10, dtype=np.float32) for i in range(0,10): r = w[i] * img r = relu(np.sum(r) + b[i]) resp[i] = r resp_cls = np.argmax(resp) resp = np.zeros(10, dtype=np.float32) resp = 1.0 #back propagation true_resp = np.zeros(10, dtype=np.float32) true_resp = 1.0 error = resp - true_resp delta = error * ((resp >= 0) * np.ones(10)) for i in range(0,10): w[i] -= np.dot(img, delta[i]) b[i] -= delta[i]

    В процессе обучения коэффициенты станут слегка похожи на числа:

    Проверим точность работы:

    Def nn_calculate(img): resp = list(range(0, 10)) for i in range(0,10): r = w[i] * img r = np.maximum(np.sum(r) + b[i], 0) #relu resp[i] = r return np.argmax(resp) total = len(test_images) valid = 0 invalid = for i in range(0, total): img = test_images[i] predicted = nn_calculate(img) true = test_labels[i] if predicted == true: valid = valid + 1 else: invalid.append({"image":img, "predicted":predicted, "true":true}) print("accuracy {}".format(valid/total))

    У меня получилось 88%. Не так уж круто, но очень интересно!