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

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

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

enter image description here

Кроме того, это упражнение 1.4 на книге Learning from Data.

import numpy as np

a = 1
b = 1

def target(x):
    if x[1]>a*x[0]+b:
        return 1
    else:
        return -1

def gen_y(X_sim):
    return np.array([target(x) for x in X_sim])

def pcp(X,y):
    w = np.zeros(2)
    Z = np.hstack((X,np.array([y]).T))
    while ~all(z[2]*np.dot(w,z[:2])>0 for z in Z): # some training sample is missclassified
        i = np.where(y*np.dot(w,x)<0 for x in X)[0][0] # update the weight based on misclassified sample
        print(i)
        w = w + y[i]*X[i]
    return w

if __name__ == '__main__':
    X = np.random.multivariate_normal([1,1],np.diag([1,1]),20)
    y = gen_y(X)
    w = pcp(X,y)
    print(w)

w я намерен до бесконечности.

[-1.66580705  1.86672845]
[-3.3316141   3.73345691]
[-4.99742115  5.60018536]
[-6.6632282   7.46691382]
[-8.32903525  9.33364227]
[ -9.99484231  11.20037073]
[-11.66064936  13.06709918]
[-13.32645641  14.93382763]
[-14.99226346  16.80055609]
[-16.65807051  18.66728454]
[-18.32387756  20.534013  ]
[-19.98968461  22.40074145]
[-21.65549166  24.26746991]
[-23.32129871  26.13419836]
[-24.98710576  28.00092682]
[-26.65291282  29.86765527]
[-28.31871987  31.73438372]
[-29.98452692  33.60111218]
[-31.65033397  35.46784063]
[-33.31614102  37.33456909]
[-34.98194807  39.20129754]
[-36.64775512  41.068026  ]

В учебнике говорится:

enter image description here

Вопрос здесь:

enter image description here


Помимо вопроса: я действительно не понимаю, почему это правило обновления будет работать. Есть ли хорошая геометрическая интуиция, как это работает? Понятно, что книга ничего не дала. Правило обновления просто w(t+1)=w(t)+y(t)x(t) где x,y не классифицируется, т.е. y!=sign(w^T*x)


Следуя одному из ответов,

import numpy as np

np.random.seed(0)

a = 1
b = 1

def target(x):
    if x[1]>a*x[0]+b:
        return 1
    else:
        return -1

def gen_y(X_sim):
    return np.array([target(x) for x in X_sim])

def pcp(X,y):
    w = np.ones(3)
    Z = np.hstack((np.array([np.ones(len(X))]).T,X,np.array([y]).T))
    while not all(z[3]*np.dot(w,z[:3])>0 for z in Z): # some training sample is missclassified

        print([z[3]*np.dot(w,z[:3])>0 for z in Z])
        print(not all(z[3]*np.dot(w,z[:3])>0 for z in Z))

        i = np.where(z[3]*np.dot(w,z[:3])<0 for z in Z)[0][0] # update the weight based on misclassified sample
        w = w + Z[i,3]*Z[i,:3]

        print([z[3]*np.dot(w,z[:3])>0 for z in Z])
        print(not all(z[3]*np.dot(w,z[:3])>0 for z in Z))

        print(i,w)
    return w

if __name__ == '__main__':
    X = np.random.multivariate_normal([1,1],np.diag([1,1]),20)
    y = gen_y(X)
    # import matplotlib.pyplot as plt
    # plt.scatter(X[:,0],X[:,1],c=y)
    # plt.scatter(X[1,0],X[1,1],c='red')
    # plt.show()
    w = pcp(X,y)
    print(w)

Это все еще не работает и печатает

[False, True, False, False, False, True, False, False, False, False, True, False, False, False, False, False, False, False, False, False]
True
[True, False, True, True, True, False, True, True, True, True, True, True, True, True, True, True, False, True, True, True]
True
0 [ 0.         -1.76405235 -0.40015721]
[True, False, True, True, True, False, True, True, True, True, True, True, True, True, True, True, False, True, True, True]
True
[True, False, True, True, True, False, True, True, True, True, True, True, True, True, True, True, False, True, True, True]
True
0 [-1.         -4.52810469 -1.80031442]
[True, False, True, True, True, False, True, True, True, True, True, True, True, True, True, True, False, True, True, True]
True
[True, False, True, True, True, False, True, True, True, True, True, True, True, True, True, True, False, True, True, True]
True
0 [-2.         -7.29215704 -3.20047163]
[True, False, True, True, True, False, True, True, True, True, True, True, True, True, True, True, False, True, True, True]
True
[True, False, True, True, True, False, True, True, True, True, True, True, True, True, True, True, False, True, True, True]
True
0 [ -3.         -10.05620938  -4.60062883]
[True, False, True, True, True, False, True, True, True, True, True, True, True, True, True, True, False, True, True, True]
True

Кажется, что 1. только три +1 являются ложными, это указано ниже в графике. 2. Индекс возвращаемого помещения аналогично Matlab find является неправильным.

enter image description here

Ответ 1

  • Правило перцептрона всегда работает, если данные линейно разделяются
  • Гарантируется, что правило Перцептрона будет сходиться для линейно разделяемых данных
  • Перцептрон лучше всего работает, когда функция активации является гиперболическим касанием или жестким ограничением
  • Если вы используете линейную активацию, весы взорвутся
  • вам нужно использовать регуляризацию на весах, чтобы веса не становились слишком большими
  • Правило обновления веса: new_weight = old_weight + (target - logits) * input
  • В приведенном выше правиле обновления веса error = (target - logits)
  • Указанное правило обновления веса называется дельта-правилом
  • В правиле дельта вы также можете использовать скорость обучения: new_weight = old_weight + learning_rate * (target - logits) * input
  • Вы используете правило обновления веса: new_weight = old_weight + (logits) * input
  • Обновление веса, которое вы используете, не будет работать хорошо.
  • Использовать правило дельта
  • В вашем правиле обновления веса вы не используете цель, и, следовательно, это неконтролируемое правило hebb
  • Пожалуйста, обратитесь к этой ссылке github: https://github.com/jayshah19949596/Neural-Network-Demo/tree/master/Single%20Neuron%20Perceptron%20Learning
  • Эта ссылка точно делает то, что вы хотите с помощью gui

Обновлено: см. Код ниже.... Я тренировался на 100 epoochs

  • Веса могут идти в бесконечность, если использовать функцию линейной активации
  • Чтобы избежать переноса веса в бесконечность, применяйте регуляризацию или ограничивайте свои веса до определенного числа
  • В приведенном ниже коде я ограничил значение весов и не применял регуляризацию

==============================================

import numpy as np
import matplotlib.pyplot as plt


def plot_line(x_val, y_val, points):
    fig = plt.figure()
    plt.scatter(points[0:2, 0], points[0:2, 1], figure=fig, marker="v")
    plt.scatter(points[2:, 0], points[2:, 1], figure=fig, marker="o")

    plt.plot(x_val, y_val, "--", figure=fig)
    plt.show()


def activation(net_value, activation_function):
    if activation_function == 'Sigmoid':
        # =============================
        # Calculate Sigmoid Activation
        # =============================
        activation = 1.0 / (1 + np.exp(-net_value))

    elif activation_function == "Linear":
        # =============================
        # Calculate Linear Activation
        # =============================
        activation = net_value

    elif activation_function == "Symmetrical Hard limit":
        # =============================================
        # Calculate Symmetrical Hard limit Activation
        # =============================================
        if net_value.size > 1:
            activation = net_value
            activation[activation >= 0] = 1.0
            activation[activation < 0] = -1.0
        # =============================================
        # If net value is single number
        # =============================================
        elif net_value.size == 1:
            if net_value < 0:
                activation = -1.0
            else:
                activation = 1.0

    elif activation_function == "Hyperbolic Tangent":
        # =============================================
        # Calculate Hyperbolic Tangent Activation
        # =============================================
        activation = ((np.exp(net_value)) - (np.exp(-net_value))) / ((np.exp(net_value)) + (np.exp(-net_value)))

    return activation

# ==============================
# Initializing weights
# ==============================
input_weight_1 = 0.0
input_weight_2 = 0.0
bias = 0.0
weights = np.array([input_weight_1, input_weight_2])

# ==============================
# Choosing random data points
# ==============================
data_points = np.random.randint(-10, 10, size=(4, 2))
targets = np.array([1.0, 1.0, -1.0, -1.0])

outer_loop = False
error_array = np.array([5.0, 5.0, 5.0, 5.0])

# ==========================
# Training starts from here
# ==========================
for i in range(0, 100):
    for j in range(0, 4):
        # =======================
        # Getting the input point
        # =======================
        point = data_points[j, :]

        # =======================
        # Calculating net value
        # =======================
        net_value = np.sum(weights * point) + bias  # [1x2] * [2x1]

        # =======================
        # Calculating error
        # =======================
        error = targets[j] - activation(net_value, "Symmetrical Hard limit")
        error_array[j] = error

        # ============================================
        # Keeping the error in range from -700 to 700
        # this is to avoid nan or overflow error
        # ============================================
        if error > 1000 or error < -700:
            error /= 10000

        # ==========================
        # Updating Weights and bias
        # ==========================
        weights += error * point
        bias += error * 1.0  # While updating bias input is always 1

        ###########################################################
        # If you want to use unsupervised hebb rule then use the below update rule
        # weights += targets[j] * point
        # bias += targets[j] * 1.0  # While updating bias input is always 1
        ###########################################################
        if (error_array == np.array([0.0, 0.0, 0.0, 0.0])).all():
            outer_loop = True
            break
    x_values = np.linspace(-10, 10, 256)

    if weights[0] == 0:
        weights[0] = 0.1

    if weights[1] == 0:
        weights[1] = 0.1

    # ========================================================
    # Getting the y values to plot a linear decision boundary
    # ========================================================
    y_values = ((- weights[0] * x_values) - bias) / weights[1]  # Equation of a line
    input_weight_1 = weights[0]
    input_weight_2 = weights[1]

    if outer_loop:
        break

input_weight_1 = weights[0]
input_weight_2 = weights[1]
print(weights)
plot_line(x_values, y_values, data_points)

==============================================

Выход:

enter image description here

================================================== ====

Использование кода с моим кодом

import numpy as np
import matplotlib.pyplot as plt


def plot_line(x_val, y_val, targets, points):
    fig = plt.figure()
    for i in range(points.shape[0]):
        if targets[i] == 1.0:
            plt.scatter(points[i, 0], points[i, 1], figure=fig, marker="v", c="red")
        else:
            plt.scatter(points[i, 0], points[i, 1], figure=fig, marker="o", c="black")
    plt.plot(x_val, y_val, "--", figure=fig)
    plt.show()


def activation(net_value, activation_function):
    if activation_function == 'Sigmoid':
        # =============================
        # Calculate Sigmoid Activation
        # =============================
        activation = 1.0 / (1 + np.exp(-net_value))

    elif activation_function == "Linear":
        # =============================
        # Calculate Linear Activation
        # =============================
        activation = net_value

    elif activation_function == "Symmetrical Hard limit":
        # =============================================
        # Calculate Symmetrical Hard limit Activation
        # =============================================
        if net_value.size > 1:
            activation = net_value
            activation[activation >= 0] = 1.0
            activation[activation < 0] = -1.0
        # =============================================
        # If net value is single number
        # =============================================
        elif net_value.size == 1:
            if net_value < 0:
                activation = -1.0
            else:
                activation = 1.0

    elif activation_function == "Hyperbolic Tangent":
        # =============================================
        # Calculate Hyperbolic Tangent Activation
        # =============================================
        activation = ((np.exp(net_value)) - (np.exp(-net_value))) / ((np.exp(net_value)) + (np.exp(-net_value)))

    return activation


a = 1
b = 1


def target(x):
    if x[1] > a*x[0]+b:
        return 1
    else:
        return -1


def gen_y(X_sim):
    return np.array([target(x) for x in X_sim])


def train(data_points, targets, weights):
    outer_loop = False
    error_array = np.zeros_like(targets) + 0.5
    bias = 0

    # ==========================
    # Training starts from here
    # ==========================
    for i in range(0, 1000):
        for j in range(0, data_points.shape[0]):
            # =======================
            # Getting the input point
            # =======================
            point = data_points[j, :]

            # =======================
            # Calculating net value
            # =======================
            net_value = np.sum(weights * point) + bias  # [1x2] * [2x1]

            # =======================
            # Calculating error
            # =======================
            error = targets[j] - activation(net_value, "Symmetrical Hard limit")
            error_array[j] = error

            # ============================================
            # Keeping the error in range from -700 to 700
            # this is to avoid nan or overflow error
            # ============================================
            if error > 1000 or error < -700:
                error /= 10000

            # ==========================
            # Updating Weights and bias
            # ==========================
            weights += error * point
            bias += error * 1.0  # While updating bias input is always 1

            ###########################################################
            # If you want to use unsupervised hebb rule then use the below update rule
            # weights += targets[j] * point
            # bias += targets[j] * 1.0  # While updating bias input is always 1
            ###########################################################
            # if error_array.all() == np.zeros_like(error_array).all():
            #     outer_loop = True
            #     break
        x_values = np.linspace(-10, 10, 256)

        if weights[0] == 0:
            weights[0] = 0.1

        if weights[1] == 0:
            weights[1] = 0.1

        # ========================================================
        # Getting the y values to plot a linear decision boundary
        # ========================================================
        y_values = ((- weights[0] * x_values) - bias) / weights[1]  # Equation of a line

        if outer_loop:
            break

    plot_line(x_values, y_values, targets, data_points)


def pcp(X, y):
    w = np.zeros(2)
    Z = np.hstack((X, np.array([y]).T))
    X = Z[0:, 0:2]
    Y = Z[0:, 2]
    train(X, Y, w)
    # while ~all(z[2]*np.dot(w, z[:2]) > 0 for z in Z):  # some training sample is miss-classified
    #     i = np.where(y*np.dot(w, x) < 0 for x in X)[0][0]  # update the weight based on misclassified sample
    #     print(i)
    #     w = w + y[i]*X[i]
    return w


if __name__ == '__main__':
    X = np.random.multivariate_normal([1, 1], np.diag([1, 1]), 20)
    y = gen_y(X)
    w = pcp(X, y)
    print(w)

Я получаю следующий вывод enter image description here

Ответ 2

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

... где добавленная координата x0 фиксирована при x0 = 1

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

X = np.hstack(( np.array([ np.ones(len(X)) ]).T, X )) ## add a '1' column for bias

Соответственно, вам нужны три значения веса вместо двух: w = np.ones(3), и это может помочь инициализировать это значение ненулевым, возможно, в зависимости от вашей другой логики.

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


Что касается интуиции, книга пытается охватить это:

Это правило перемещает границу в направлении классификации x (t) правильно...

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


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

См. Также стандартную форму представления двумерного линейного уравнения: Ax + By = C*1

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

Одним из способов решения этого, мысленно, может быть осознание того, что ваши значения y были вычислены на основе среза гиперплоскости, вычисленного по этому двумерному входному домену. Тот факт, что построенная линия принятия решений, по-видимому, идеально отражает значения перехвата наклона, которые вы выбрали, является просто артефактом конкретной формулы, которая была использована для ее создания: 0 > a*X0 - 1*X1 + b


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


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


Исправление использования where:

>>> A=np.array([3,5,7,11,13])
>>> np.where(z>10 for z in A) ## this was wrong
(array([0]),)
>>> np.where([z>10 for z in A]) ## this seems to work
(array([3, 4]),)

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

Ответ 3

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

Алгоритм персептрона на самом деле w(t+1) = w(t) + a*(t(i) - y(i))*x, где t(i) - целевое или фактическое значение, а y(i) это выход алгоритма. Гарантируется сходимость, если ваши данные линейно разделяются, и ваши данные вряд ли могут быть. Помимо печати весов на каждой итерации, вы также должны распечатать количество ошибочных классификаций. Если он циклически перемещается вокруг 1 или 2 неправильных классификаций, ваши данные, вероятно, не разделяются. Если ошибочные классификации намного выше, вы должны посмотреть на свою реализацию.

Далее, персептрону всегда нужен термин смещения, который представляет собой вес с входом, который всегда равен 1. Он действует как b в y = mx+b из алгебры. Когда вы создаете Z, вместо того чтобы сделать третий столбец целевыми (y) значениями, вы должны просто добавить группу из них, например:

Z = np.hstack((X, np.ones(len(X))[...,None]))

Вам также необходимо увеличить количество весов:

w = np.zeros(3)

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

w = np.random.rand(3)

Я не уверен, что происходит с разделом ~all(...), поэтому мы перепишем это. Мы будем учитывать возможность того, что наши данные не будут разделяемыми, и добавьте точку остановки max_iter, но оставьте выход, если мы получим ошибку 0.

max_iter = 100
total_err = 100000 # Just really large
while total_err != 0 and max_iter > 0:
    total_err = 0

    for i in range(len(Z)):
    ...

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

err = y[i] - np.dot(w,Z[i,:])
total_err += err / 2 # Because errors will be -2 or 2.

Наконец, мы обновляем вес. Из вышеприведенного алгоритма персептрона нам необходимо:

w = w + err * Z[i,:].T # Transposed to match the shape of w

Есть еще несколько быстрых улучшений, которые вы могли бы внести в алгоритм. Во-первых, большинство людей внедряют какую-то скорость обучения в микс. Перед в while цикл, добавить a = 0.01 или что - то вокруг этого размера. Измените правило обучения персептрона

w = w + a * err * Z[i, :].T

Это заставит ваш алгоритм прыгать прямо за лучший набор весов.