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

В предыдущей версии мне не нравится, что в процедуре обучения есть оператор сравнения if. Он применяется, когда вывод сравнивается с меткой класса (if not compare(x,y):), и если вывод и метка класса не равны, то происходит коррекция веса. Мне хочется "более чистой" математики и не применять операторы сравнения, если этого можно избежать..

Убираем оператор сравнения в формуле расчета коррекции

В данном случае избежать операторов сравнения можно и достаточно легко.
Коррекция веса происходит по формуле: w[index] =w[index] + y*lmd*x[index], где y - метка класса соответствующего объекта. Если заменить y на на разницу между меткой класса и выводом (y - onestep(neuron.output(x)) и убрать оператор сравнения, то это и будет решением.

Рассмотрим, почему это так.
Метка класса (y) равна или +1 или -1. А вывод равен также или +1 или -1. Соответственно если вывод и метка класса равны, то разница между выводом и меткой класса равна нулю. А если вывод и метка класса не равны, то разница (y - onestep(neuron.output(x)) будет равна или +2 или -2. Получается, что коррекция делается на каждой итерации, но при равенстве вывода и метки класса коррекция равна нулю, что равносильно тому, как если бы она не происходила, как и в предыдущей версии. Далее мы можем домножить величину коррекции на 1/2, чтобы вернуться к 1 и к прошлым форматам, и учесть это в размере lmd, либо оставить lmd как и было, так как уменьшение lmd в два раза в данном случае не принципиально на уровне формул - lmd так и так является параметром подбора.
Таким образом убираем оператор сравнения и коррекция весов теперь происходит на каждой итерации:

error = y - onestep(neuron.output(x))
w[index] = w[index] + error*lmd*x[index] 

Интересно, что таким ходом мы приходим к распространенной формуле, часто называемой "дельта-правило".

Убираем оператор сравнения в функции качества

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

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

Здесь тоже хочется избежать применения оператора сравнения, и для этого применим похожий способ.
Будем суммировать не 1 в случае несовпадения вывода с меткой класса, а непосредственно величины разницы, да еще возведенные в квадрат для устранения влияния знака.
Соответственно, если вывод и метка класса равны, то разница равна нулю, и к итоговой сумме ничего не прибавляется. Если же вывод и метка класса не равны, то разница равна или +2 или -2, и к общей сумме прибавляется 4.

Новый код функции качества получается таким:

# Функция качества. Считает количество объектов с ошиибкой.
def Q():
  return sum ([((y_train[index] - onestep(neuron.output(x)))**2) for index, x in enumerate(x_train) ])

Если добавить деление итоговой суммы на 4, то получим количество ошибочно классифицируемых объектов. Но в данном случае не принципиально - применять sum или sum/4, так как обучение прекращается, когда Q() = 0, а для sum и sum/4 это совпадает.

Итоговый код с изменениями

В итоге получили новый код с более чистой математикой.

Код для исходных данных
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 Q():
  return sum ([((y_train[index] - onestep(neuron.output(x)))**2 ) for index, x in enumerate(x_train)])

# Функция добавления размерности
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()

# проходим итерации
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]

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

  # Вместо метки класса  применяем величину отклонения
  error = y - onestep(neuron.output(x))                     # считаем величину отклонения (!)
  w[index] = round(w[index] + error*lmd*x[index],4)         # коррекция веса с учетом параметров данных и величины отклонения (!)
  w_history.append(np.array(w))                             # сохранякем историю
  #neuron = Neuron(w)
  neuron.w = w

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

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

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

[-0.1 -3.9 -2.4] 5.0

0 [-0.1 -3.9 -2.4] 5.0
1 [-0.1 -3.9 -2.38] 5.0
2 [-0.1 -3.9 -2.38] 5.0
3 [ 0.7 -3.9 -2.38] 5.0
4 [ 0.7 -3.3 -2.38] 5.0
5 [ 0.7 -3.3 -2.38] 5.0
6 [ 0.7 -3.3 -2.38] 5.0
7 [ 0.7 -3.3 -2.36] 5.0
8 [ 0.7 -3.3 -2.36] 5.0
9 [ 0.7 -3.3 -2.36] 5.0
10 [ 0.7 -3.3 -2.36] 5.0
11 [ 0.7 -3.3 -2.34] 5.0
12 [ 0.7 -2.4 -2.34] 5.0
13 [ 1.5 -2.4 -2.34] 5.0
14 [ 1.5 -2.4 -2.34] 5.0
15 [ 1.5 -2.4 -2.32] 5.0
16 [ 1.5 -2.4 -2.32] 5.0
17 [ 2.3 -2.4 -2.32] 4.0
18 [ 2.3 -2.4 -2.32] 4.0
19 [ 2.3 -2.4 -2.32] 4.0
20 [ 2.3 -1.8 -2.32] 2.0
21 [ 2.9 -1.8 -2.32] 0.0

[ 2.9 -1.8 -2.32]
Q_current: 0.0 n: 21

Визуально на графике это выглядит так:

Код создания изображения
# создаем изображение

# Рисование линии
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')

Примечание

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

Комментарии (0)