В предыдущих частях (первая, вторая) описан мой опыт обучения простого искусственного нейрона бинарной классификации и размышления об этом. В этой статье я продолжаю размышления и вношу соответствующие корректировки в код.
В предыдущей версии мне не нравится, что в процедуре обучения есть оператор сравнения 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')
Примечание
Если заметили какие-либо неточности или явные нестыковки - пожалуйста, отметьте это в комментариях.
