Как стать автором
Обновить

Бинарная классификация одним простым искусственным нейроном. 2 часть

Время на прочтение6 мин
Количество просмотров879

Сомнения

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

Так если для демонстрации принудительно начать с самого края y=0.2x с набором [-0.2 1. 0. ] (серая линия) , то количество ошибочно классифицированных объектов равно 5 и потом увеличивается. Когда дойдет до реальной разделяющей линии, то количество ошибочно классифицированных объектов равно 10, то есть все объекты классифицированы ошибочно. Затем, так как количество ошибочно классифицированных объектов не нулевое, алгоритм продолжается и прямая доходит до противоположного края, проскакивая нужный диапазон, после чего ситуация повторяется в обратном направлении. Так прямая будет ходить из края в край бесконечно или пока не дойдет до ограничивающего числа повторов.

Если же начинать с набора [ 0.2 -1. 0. ] , то все корректно - количество ошибочно классифицированных объектов постепенно уменьшается с 5 до 0 и коррекция завершается в нужном месте, когда количество ошибочно классифицированных объектов равно нулю.

Необходимо отметить, что визуально оба набора [-0.2 1. 0. ] и [ 0.2 -1. 0. ] дают одинаковую прямую y = 0.2x, но вывод метки класса будет отличаться знаком.
Соответственно наборы [ 1.6 -1. 0. ] и [ -1.6 1. 0. ] дают одинаковую прямую y = 1.6x, но первый набор дает 0 ошибок, а в второй - 10. Как видно, при одном и том же соотношении весов, дающих один и тот же коэффициент k в выражении y = kx + b количество ошибок непосредственно зависит от того, как распределены знаки по коэффициентам.

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

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

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

Учет соотношения параметров объектов

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

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

Ответ следует из математики.
В данном алгоритме одинаковый шаг коррекции для всех весов. Поэтому за некоторое количество шагов веса поменяются примерно на одинаковые величины и в одном направлении относительно друг друга. То есть ситуация, при которой первый вес из отрицательного увеличился бы до положительного, а второй вес, наоборот, с положительного уменьшился бы до отрицательного, возможна, но маловероятна. А нужно, чтобы за некоторое количество шагов первый и второй веса стабильно корректировались бы в разные стороны.
Это становится возможным, если при коррекции учитывать соответствующие параметры самих объектов. Визуально видно, что у "синих" объектов "ширины" и "длины" близки по значениям, а у "красных" объектов "длина" может превышать "ширину" в несколько раз. Если при расчете величины коррекции добавить множитель - параметр, соответствующий весу, то вес будет корректироваться в "нужную" сторону на чуть большую величину, чем в "ненужную". Таким образом за некоторое количество шагов первый вес может перейти из отрицательного в положительный, а второй - из положительного в отрицательный, что нам и требуется.
В таком случае в формулу шага коррекции следует добавить соответствующий множитель x[index]:
w[index] = w[index] + y*lmd => w[index] = w[index] + y*lmd*x[index],
и при этом уменьшить lmd в 50 раз для соответствия предыдущим форматам.

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

Код для исходных данных
import numpy as np
import matplotlib.pyplot as plt

# исходные данные
x_train = np.array([[10, 50], [20, 30], [25, 30], [20, 60], [15, 70], [40, 40], [30, 45], [20, 45], [40, 30], [7, 35]])
y_train = np.array([-1, 1, 1, -1, -1, 1, 1, -1, 1, -1])

# формируем множества по меткам
x_0 = x_train[y_train == 1]
x_1 = x_train[y_train == -1]

# создаем изображение
plt.xlim([0, max(x_train[:, 0]) + 10])
plt.ylim([0, max(x_train[:, 1]) + 10])
plt.scatter(x_0[:, 0], x_0[:, 1], color='blue')
plt.scatter(x_1[:, 0], x_1[:, 1], color='red')

plt.ylabel("длина")
plt.xlabel("ширина")
plt.grid(True)
plt.show()
Код создания класса Neuron и определения функций
class Neuron:
  def __init__(self, w):          # Действия при создании класса
    self.w = w

  def output(self, x):            # Сумматор
    return np.dot(self.w, x)      # Суммируем входы

# Функция активации - Функция единого скачка
def onestep(x):
  return 1 if x >= 0 else -1

# Функция сравнения вывода с меткой класса
def compare(x,y):
  return onestep(neuron.output(x)) == y

# Функция качества. Считает количество объектов с ошиибкой.
def Q(w):
  return sum([1 for index, x in enumerate(x_train) if not compare(x,y_train[index])])

# Функция добавления размерности
def add_axis(source_array, value):
  this_array = []
  for x in source_array: this_array.append(np.append(x, value))
  return np.array(this_array)

# добавляем фиксированное значение (1)
x_train = add_axis(x_train, 1)
Код обучения
import random

# Случайный выбор для коэффициентов
def random_w():
  return round((random.random() * 10 - 5),1)

N = 500                             # максимальное число итераций
lmd = 0.01                          # шаг изменения веса

w_history = []                      # Массив для сохранения истории

# Инициируем веса случайным образом
w = np.array([random_w(), random_w(), random_w()])
w_history.append(np.array(w))       # сохранякем историю
neuron = Neuron(w)

# Считаем количество ошибок
Q_current = Q(w)

# проходим итерации
for n in range(N):
  if Q_current == 0: break

  # Выбираем случайным образом объект
  random_index = random.randint(0,len(x_train)-1)
  x = x_train[random_index]
  y = y_train[random_index]

  # Если вывод не совпадает с меткой класса
  if not compare(x,y):

    # выбираем вес случайным образом
    index = random.randint(0,len(w)-1)

    # Применяем знак метки для выбора изменения и меняем вес
    w[index] = round(w[index] + y*lmd*x[index],4)      # добавляем влияние значения (!)
    w_history.append(np.array(w))                      # сохранякем историю
    neuron = Neuron(w)

    Q_current = Q(w)
    if Q_current == 0: break

print(w)
print('Q_current:', Q_current, 'n:', n)

Итак, теперь все корректно, алгоритм сам правильно корректирует веса из любых инициативных ситуаций.

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

[-0.2 1. 0. ] 5

0 [-0.35 1. 0. ] 5
2 [-0.35 1. -0.01] 5
3 [-0.35 1. -0.02] 5
4 [-0.35 0.3 -0.02] 7
5 [-0.35 0.7 -0.02] 5
7 [-0.35 0.7 -0.03] 5
8 [-0.35 0.25 -0.03] 8
9 [-0.35 -0.45 -0.03] 5
10 [ 0.05 -0.45 -0.03] 5
12 [ 0.05 -0.45 -0.02] 5
14 [ 0.05 -0.45 -0.01] 5
18 [ 0.05 -0.15 -0.01] 5
22 [ 0.3 -0.15 -0.01] 0

[ 0.3 -0.15 -0.01]
Q_current: 0 n: 22

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

y = 2.0x + -0.0667

Код создания изображения
# Рисование линии
def draw_line(x,w,color):
  if w[1]:
    line_x = [min(x[:, 0]) - 10, max(x[:, 0]) + 10]
    line_y = [-w[0]/w[1]*x - w[2]/w[1] for x in line_x]
    plt.plot(line_x, line_y, color=color)
  else:
    print('w1 = 0')

plt.xlim([0, max(x_train[:, 0]) + 10])
plt.ylim([0, max(x_train[:, 1]) + 10])
plt.scatter(x_0[:, 0], x_0[:, 1], color='blue')
plt.scatter(x_1[:, 0], x_1[:, 1], color='red')

for index, key in enumerate(w_history):
  if index:
    draw_line(x_train, key, 'yellow')
  else:
    draw_line(x_train, key, 'grey')

draw_line(x_train, w, 'green')

plt.ylabel("длина")
plt.xlabel("ширина")
plt.grid(True)
plt.show()

if w[1]:
  print('y = ' + str(round(-w[0]/w[1],4)) + 'x + ' + str(round(-w[2]/w[1],4)))
else:
  print('x = 0')

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

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

Дополнительный вывод

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

Примечание

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

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+1
Комментарии0

Публикации

Работа

Data Scientist
43 вакансии

Ближайшие события