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

от автора

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

В предыдущей версии мне не нравится, что в процедуре обучения есть оператор сравнения 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')

Примечание

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


ссылка на оригинал статьи https://habr.com/ru/articles/908462/