Обзор и реализация на Python

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

Справочная информация: биологические нейронные сети

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

Каждый нейрон получает электрические сигналы (импульсы) от соседних нейронов через волокна, называемые дендритами. Когда общая сумма входящих сигналов превышает некоторый порог, нейрон «запускает» свой собственный сигнал через длинные волокна, называемые аксонами, которые связаны с дендритами других нейронов.

Соединение между двумя нейронами называется синапсом. В среднем каждый нейрон связан примерно с 7000 синапсами, что демонстрирует высокую связность сети, которую мы имеем в нашем мозгу. Когда мы изучаем новые ассоциации между двумя понятиями, усиливается синаптическая сила между нейронами, представляющими эти понятия. Это явление известно как правило Хебба (1949), которое гласит: «Клетки, которые активируются вместе, соединяются вместе».

Модель персептрона

Модель персептрона, представленная Фрэнком Розенблаттом в 1957 году, представляет собой упрощенную модель биологического нейрона.

Персептрон имеет m двоичных входов, обозначенных x₁, …, xₘ, которые представляют входящие сигналы от соседних нейронов, и он выводит одно двоичное значение, обозначаемое o, которое указывает, «запускается» ли персептрон или нет.

Каждый входной нейрон xᵢ подключен к персептрону через связь, сила которой представлена ​​весом wᵢ. Входные данные с более высокими весами оказывают большее влияние на выход персептрона.

Персептрон сначала вычисляет взвешенную сумму входящих сигналов, умножая каждый вход на соответствующий вес. Эту взвешенную сумму часто называют net input и обозначают z:

Если чистый вход превышает некоторое предопределенное пороговое значение θ, то персептрон срабатывает (его выход равен 1), в противном случае он не срабатывает (его выход равен 0). Другими словами, персептрон срабатывает тогда и только тогда, когда:

Наша цель — найти веса w₁, …, wₘ и пороговое значение θ,, чтобы персептрон правильно отображал свои входы x₁, …, xₘ (представляющие функции в наших данных) к желаемому результату y (представляющему метку).

Чтобы упростить процесс обучения, вместо того, чтобы изучать отдельно веса и порог, мы добавляем специальный входной нейрон, называемый нейрон смещения, который всегда выдает значение 1. Этот нейрон обычно обозначается x₀, а его вес соединения обозначается как b или w₀.

В результате чистый вход персептрона становится:

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

В векторной форме мы можем записать z как скалярное произведение входного вектора x = (x₁, …, xₘ) и вектор веса w = (w₁, …, wₘ)плюс смещение:

И персептрон срабатывает тогда и только тогда, когда входная сеть неотрицательна, т. е.

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

  1. Ступенчатая функция (также известная как функция Хевисайда) — это функция, значение которой равно 0 для отрицательных входных данных и 1 для неотрицательных входных данных:

2. Знаковая функция — это функция, значение которой равно -1 для отрицательных входных данных и 1 для неотрицательных входных данных:

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

Подводя итог, вычисление персептрона состоит из двух шагов:

  1. Умножение входных значений x₁, …, xₘ на соответствующие веса w₁, …, wₘ, и добавление смещения b, что дает нам чистый ввод персептрона z =w х+ b.
  2. Применение функции активации f(z) к сетевому входу, который генерирует двоичный выход (0/1 или -1/+1).

Мы можем записать все это вычисление в одном уравнении:

где f — выбранная функция активации, а o — выход персептрона.

Реализация логических вентилей с помощью персептронов

Чтобы продемонстрировать, как работают персептроны, давайте попробуем построить персептроны, которые вычисляют логические функции И и ИЛИ.

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

Персептрон, реализующий функцию И, имеет два бинарных входа и смещение. Мы хотим, чтобы этот персептрон «срабатывал» только тогда, когда «срабатывают» оба его входа. Этого можно добиться, например, выбрав одинаковый вес для обоих входных данных, например, w₁ = w₂ = 1, а затем выбрав смещение в пределах диапазона [-2, -1). Это гарантирует, что когда оба нейрона активируются, вход сети 2 + b будет неотрицательным, но когда активируется только один из них, вход сети 1 + b будет отрицательным (и когда ни один из них не срабатывает, сетевой вход b также будет отрицательным).

Аналогичным образом мы можем построить персептрон, который вычисляет логическую функцию ИЛИ:

Убедитесь, что вы понимаете, как работает этот персептрон!

В качестве упражнения попробуйте построить персептрон для функции И-НЕ, таблица истинности которой показана ниже:

Персептроны как линейные классификаторы

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

Весовой вектор w ортогонален этой гиперплоскости и, таким образом, определяет ее ориентацию, а смещение b определяет ее расстояние от начала координат.

Каждый пример над гиперплоскостью (wx+ b› 0) классифицируется персептроном как положительный пример, в то время как каждый пример ниже гиперплоскости (wx+ b‹ 0) классифицируется как отрицательный пример.

Другие линейные классификаторы включают логистическую регрессию и линейные SVM (машины опорных векторов).

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

Например, следующий набор данных не является линейно разделимым, поэтому персептрон не может правильно классифицировать все примеры в этом наборе данных:

Правило обучения персептрона

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

Для каждой обучающей выборки (x, yᵢ), неправильно классифицированной персептроном (т. е. oᵢyᵢ), мы применяем следующее правило обновления к вектору весов:

где α — скорость обучения (0 ‹ α≤ 1), которая определяет размер корректировки веса при каждом обновлении.

Другими словами, мы добавляем к каждому весу соединения wⱼ ошибку перцептрона в этом примере (разницу между истинной меткой yᵢ и выходом oᵢ), умноженное на значение соответствующего входа xⱼ и скорость обучения.

Это правило обучения пытается уменьшить несоответствие между выходом персептрона oᵢ и истинной меткой yᵢ. Чтобы понять, почему это работает, давайте рассмотрим два возможных случая неправильной классификации персептроном:

  1. Истинная метка — yᵢ= 1, но прогноз персептрона — oᵢ=0, т. е. wxᵢ + b‹ 0. В этом случае мы хотели бы увеличитьчистый ввод персептрона, чтобы в конечном итоге он становится положительным.
    Для этого складываем количество (yᵢ — oᵢ)x = xв вектор весов (умноженный на скорость обучения). Это увеличивает веса входных данных с положительными значениями (где xᵢⱼ › 0) и уменьшает веса входных данных с отрицательными значениями (где xᵢⱼ ‹ 0). Следовательно, общий чистый вход персептрона увеличивается.
  2. Истинная метка yᵢ= 0, но прогноз перцептрона oᵢ= 1, т. е. wxᵢ + b › 0. Как и в предыдущем случае, здесь мы хотели бы уменьшить чистый вход персептрона, чтобы в конечном итоге он стал отрицательным.
    Это достигается сложением количества (yᵢ — oᵢ)x = -xв вектор весов, поскольку это уменьшает веса входных данных с положительными значениями (xᵢⱼ › 0) и увеличивает веса входных данных с отрицательные значения (xᵢⱼ ‹ 0). Следовательно, общий чистый вход персептрона уменьшается.

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

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

Алгоритм обучения персептрона

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

Алгоритм обучения персептрона сводится к следующему псевдокоду:

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

Пример. Изучение функции большинства

Например, давайте посмотрим, как можно использовать алгоритм обучения персептрона для изучения функции большинства трех двоичных входных данных. Функция большинства — это функция, которая оценивается как истина (1), когда половина или более ее входных данных верны, и как ложь (0) в противном случае.

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

В этом примере мы будем предполагать, что начальные веса и смещение равны 0, а скорость обучения равна α = 0,5.

Давайте проследим за изменениями веса во время первой тренировочной эпохи. Первый образец, представленный перцептрону, равен x = (0, 0, 0). Чистый вход персептрона в этом случае: z = wx + b = 0 × 0 + 0 × 0 + 0 × 0 + 0 = 0. Следовательно, его выход равен o = 1 (помните, что ступенчатая функция выводит 1 всякий раз, когда ее вход ≥ 0) . Однако целевая метка в этом случае равна y = 0, поэтому ошибка персептрона составляет y - o = -1.

Согласно правилу обучения персептрона, мы обновляем каждый вес wᵢ, добавляя к нему α(y - o)xᵢ = -0,5xᵢ. Поскольку в этом случае все входные данные равны 0, за исключением нейрона смещения (x₀ = 1), мы только обновляем смещение до -0,5 вместо 0.

Мы повторяем тот же процесс для всех остальных 7 обучающих выборок. В следующей таблице показаны обновления веса после каждого образца:

В первую эпоху персептрон сделал 4 ошибки. Вектор весов после первой эпохи равен w = (0, 0,5, 1), а смещение равно 0.

Во второй тренировочной эпохе мы получаем следующие обновления веса:

На этот раз персептрон допустил только три ошибки. Вектор весов после второй эпохи равен w = (0,5, 0,5, 1), а смещение равно -0,5.

Обновления веса в третьей эпохе:

После обновления второго примера в эту эпоху персептрон сошелся к вектору весов, который решает эту задачу классификации:
w = (0,5, 0,5, 0,5) и b = -1. Поскольку все веса равны, персептрон срабатывает только тогда, когда хотя бы два входа равны 1, и в этом случае их взвешенная сумма равна не менее 1, т. е. больше или равна абсолютному значению смещения (-1), следовательно чистый вход персептрона неотрицательный.

Реализация персептрона на Python

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

Мы реализуем его как пользовательский оценщик Scikit-Learn, расширив класс sklearn.base.BaseEstimator. Это позволит нам использовать его как любой другой оценщик в Scikit-Learn (например, добавив его в конвейер).

Пользовательский оценщик должен реализовать методы fit() и predict() и установить все свои гиперпараметры в методе __init__().

Сначала я покажу полный код этого класса, а затем шаг за шагом пройдусь по нему.

from sklearn.base import BaseEstimator

class Perceptron(BaseEstimator):
    def __init__(self, alpha, n_epochs):
        self.alpha = alpha
        self.n_epochs = n_epochs
        
    def fit(self, X, y):
        (n, m) = X.shape
        
        # Initialize the weights
        self.w = np.random.randn(m)
        self.b = 0
        
        # The training loop
        for epoch in range(self.n_epochs):
            n_errors = 0
            
            for i in range(n):
                o = self.predict(X[i])
                if o != y[i]:
                    self.w += self.alpha * (y[i] - o) * X[i]
                    self.b += self.alpha * (y[i] - o)
                    n_errors += 1
            
            accuracy = 1 - (n_errors / n)
            print(f'Epoch {epoch + 1}: accuracy = {accuracy:.3f}')
            
            if n_errors == 0:
                break
                
    def predict(self, X):
        z = X @ self.w + self.b
        return np.heaviside(z, 1)

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

def __init__(self, alpha, n_epochs):
    self.alpha = alpha
    self.n_epochs = n_epochs

Метод fit() запускает алгоритм обучения на заданном наборе данных X с метками y. Сначала мы узнаем, сколько выборок и признаков у нас есть в наборе данных, запросив форму X:

(n, m) = X.shape

n — количество обучающих выборок, а m — количество признаков.

Затем мы инициализируем вектор весов, используя стандартное нормальное распределение (со средним значением 0 и стандартным отклонением 1) и смещением до 0:

self.w = np.random.randn(m)
self.b = 0

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

for i in range(n):
    o = self.predict(X[i])
    if o != y[i]:

Если персептрон неверно классифицировал выборку, мы применяем правило обновления весов как к вектору весов, так и к смещению, а затем увеличиваем количество ошибок неправильной классификации на 1:

self.w += self.alpha * (y[i] - o) * X[i]
self.b += self.alpha * (y[i] - o)
n_errors += 1

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

accuracy = 1 - (n_errors / n)
print(f'Epoch {epoch + 1}: accuracy = {accuracy:.3f}')

if n_errors == 0:
    break

Метод predict() довольно прост. Сначала мы вычисляем чистый вход персептрона как скалярное произведение между входным вектором и весами плюс смещение:

z = X @ self.w + self.b

Затем мы используем функцию NumPy heaviside(), чтобы применить ступенчатую функцию к чистому входу и вернуть результат:

return np.heaviside(z, 1)

Второй параметр np.heaviside() указывает, каким должно быть значение функции для z = 0.

Давайте теперь протестируем нашу реализацию на наборе данных, сгенерированном функцией make_blobs() из Scikit-Learn.

Сначала мы генерируем набор данных со 100 случайными точками, разделенными на две группы:

from sklearn.datasets import make_blobs

X, y = make_blobs(n_samples=100, n_features=2, centers=2, cluster_std=0.5)

Мы устанавливаем для cluster_std значение 0,5 (вместо значения по умолчанию 1), чтобы обеспечить линейную разделимость данных.

Давайте построим набор данных:

import seaborn as sns

sns.scatterplot(x=X[:, 0], y=X[:, 1], hue=y, style=y, markers=('s', 'o'), 
                palette=('r', 'b'), edgecolor='black')
plt.xlabel('$x_1$')
plt.ylabel('$x_2$')

Теперь мы создаем экземпляр класса Perceptron и подгоняем его к набору данных:

perceptron = Perceptron(alpha=0.01, n_epochs=10)
perceptron.fit(X, y)

Результат во время обучения:

Epoch 1: accuracy = 0.250
Epoch 2: accuracy = 0.950
Epoch 3: accuracy = 1.000

Персептрон сошелся после трех периодов обучения.

Мы можем построить разделяющую гиперплоскость и границы решений, найденные персептроном, используя следующую функцию:

def plot_decision_boundary(model, X, y):
    # Retrieve the model parameters
    bias = model.b
    w1, w2 = model.w[0], model.w[1]

    # Calculate the intercept and slope of the decision boundary
    b = -bias / w2
    m = -w1 / w2
    
    x1 = X[:, 0]
    x2 = X[:, 1]
    x1_min, x1_max = x1.min() - 0.2, x1.max() + 0.2
    x2_min, x2_max = x2.min() - 0.5, x2.max() + 0.5
    x1_d = np.array([x1_min, x1_max])
    x2_d = m * x1_d + b

    plt.plot(x1_d, x2_d, 'k', ls='--')
    plt.fill_between(x1_d, x2_d, x2_min, color='blue', alpha=0.25)
    plt.fill_between(x1_d, x2_d, x2_max, color='red', alpha=0.25)
    plt.xlim(x1_min, x1_max)
    plt.ylim(x2_min, x2_max)
    
    sns.scatterplot(x=x1, y=x2, hue=y, style=y, markers=('s', 'o'), 
                    palette=('r', 'b'), edgecolor='black')
plot_decision_boundary(perceptron, X, y)

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

Ограничения модели персептрона

Хотя модель персептрона показала некоторый первоначальный успех, быстро стало понятно, что персептроны не могут обучиться некоторым простым функциям, таким как функция XOR:

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

Это открытие привело к застою в области нейронных сетей на многие годы (период, известный как «зима ИИ»), пока не было осознано, что размещение нескольких персептронов слоями может решить более сложные и нелинейные проблемы, такие как проблема XOR. .

Многослойные персептроны (MLP) рассматриваются в этой статье.

Заключительные примечания

Все изображения, если не указано иное, принадлежат автору.

Вы можете найти примеры кода этой статьи на моем github: https://github.com/roiyeho/medium/tree/main/perceptrons

Спасибо за прочтение!