www.machinelearningmastery.ru

Машинное обучение, нейронные сети, искусственный интеллект
Header decor

Home

Однослойный персептрон в Pharo

Дата публикации Apr 4, 2017

В этом посте я опишу мою реализацию однослойного персептрона в Pharo. Он будет поддерживать мультиклассовую классификацию (один или несколько нейронов). Каждый нейрон будет реализован как объект. Код этого проекта можно получить из Smalltalkhub с помощью этого сценария Metacello («Сделай это на игровой площадке» своего изображения Pharo):

Metacello new 
repository: 'http://smalltalkhub.com/mc/Oleks/NeuralNetwork/main';
configuration: 'MLNeuralNetwork';
version: #development;
load.

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

Что такое персептрон?

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

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

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

Ограничения персептронов

  • Они могут сходиться только на линейно отделимом входе (см. Проблему XOR, Минский и Паппет). Но если вход линейно разделим, персептрон гарантированно сходится на нем независимо от начальных весов и скорости обучения (см. Теорему о сходимости перцептрона, доказанную в 1962 году Блоком и Новиковым)
  • Перцептроны (как определено выше) могут иметь только один слой нейронов. Дело в том, что (3-я неделя курса «Нейронные сети для машинного обучения» Джеффри Хинтона) процедура обучения перцептроном может применяться только к одному слою нейронов. Нейронам в скрытых слоях понадобится какая-то обратная связь, чтобы рассчитать свои ошибки и обновить веса. Вот почему нам нужен другой алгоритм обучения (например, обратное распространение, которое будет реализовано на следующем этапе).
  • Перцептроны могут учиться только онлайн (один пример за раз). Это потому, что обучение персептрона основано на добавлении (или вычитании) входного вектора к весам на основе ошибки двоичного классификатора (эта ошибка может быть равна -1, 0 или 1).

Проблемы дизайна

Как измерить вес

Когда дело доходит до объектно-ориентированной реализации нейронных сетей, это, вероятно, самый важный вопрос, на который нужно ответить. Должны ли веса принадлежать нейрону? Если да, должен ли это быть отправляющий или принимающий нейрон? Или, может быть, они должны принадлежать слою? Или, может быть, для всей сети? Может быть, мы должны даже реализовать их как отдельные объекты?

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

  1. Входные веса каждого нейрона хранятся в виде вектора внутри этого нейрона.
  2. Матрица всех входных весов хранится в сети.
  3. Веса реализованы как объекты и связаны с нейронами.

Второй вариант является наиболее эффективным (умножение вектора на матрицу), но не очень объектно-ориентированным. Что такое нейрон в этой реализации? Очевидно, что сеть - это просто матрица весов + некоторые правила обучения. Должен ли нейрон быть функцией активации со скоростью обучения? Но опять же, хранение их в сети будет еще более эффективным. В общем, нам не нуженNeuronучебный класс. Все, что нам нужно, это матрица и несколько функций для управления ею. Это не звучит объектно-ориентированным для меня.

Третий вариант в этом случае будет чрезмерным. Это просто усложнит ситуацию. Реализация весов как объектов, вероятно, имела бы какой-то смысл в многослойной нейронной сети, где каждый вес является связью между двумя нейронами (мы можем думать о входах как о поддельных нейронах). Он соединяет два нейрона, посылает сигнал между ними и обладает «силой», которую можно обновить. В результате нейроны не знали бы о других нейронах. Они будут просто получать, обрабатывать и излучать сигналы. Я предполагаю, что такая реализация не будет очень быстрой, но она может быть использована для целей моделирования. Я напишу больше об этой идее в посте, посвященном многослойным сетям.

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

Функции активации

В этом проекте есть два способа представления функций активации:

  1. Реализуйте их как методы
  2. Реализуйте их как классы

Первый подход будет быстрее и потреблять меньше памяти. Создаем базовый класс Neuron с абстрактными методамиactivationа такжеactivationDerivative, Каждый подкласс будет особого типа нейрона, такого какBinaryThresholdNeuron,SigmoidNeuron, который реализует соответствующую функцию активации.

Другим способом реализации активаций является создание базового класса.ActivationFunctionс двумя абстрактными методами,value:а такжеderivative:, Этот подход более гибкий, потому что если кто-то захочет использовать новую функцию активации, он сможет реализовать ее в качестве подкласса, определяя только то, что это такое и какова его производная. Тогда он сможет передать объект этого класса существующему нейрону. Не представляется логичным переопределять весь нейрон каждый раз, когда нам нужно создать функцию.

Таким образом, реальный вопрос может звучать так (конечно, это может звучать лучше):
Нейроны определяются их активациями? Означает ли наличие другой активации активацию совершенно другого типа нейрона?

Общая или отдельная скорость активации и обучения?

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

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

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

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

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

Перестановка данных

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

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

Реализация

Собирая все вместе, вот мой дизайн однослойного пепстрона:

Нейрон класс

Object subclass: #Neuron
instanceVariableNames: 'weights activation learningRate'
classVariableNames: ''
package: 'NeuralNetwork'

Веса инициализируются случайными числами в диапазоне [0, 1]. Я не уверен, что это хороший диапазон, но на простых примерах он работает просто отлично.

BinaryThresholdявляется функцией активации по умолчанию, а скорость обучения по умолчанию равна 0,1. Эти параметры могут быть изменены с помощью аксессоровactivation:а такжеlearningRate:,

initialize: inputSize
"Creates a weight vector and initializes it with random values. Assigns default values to activation and learning rate" activation := BinaryThreshold new.
learningRate := 0.1.

weights := DhbVector new: (inputSize + 1).

1 to: (inputSize + 1) do: [ :i |
weights at: i put: (1 / (10 atRandom))].
^ self

Нам также необходимо добавить 1 в качестве единицы смещения для каждого входного вектора.

prependBiasToInput: inputVector
“this method prepends 1 to input vector for a bias unit”

^ (#(1), inputVector) asDhbVector.

Согласно книге «Численные методы», каждая функция должна реализовыватьvalue:метод. Хочу подчеркнуть, что с математической точки зрения нейрон - это функция.

Хотя внутреннее представление использует DhbVector, я хочу, чтобы пользователь написал что-то вродеperceptron value: #(1 0).вместо тогоperceptron value: #(1 0) asDhbVector.

value: inputVector
"Takes a vector of inputs and returns the output value"

| inputDhbVector |
inputDhbVector := self prependBiasToInput: inputVector.
^ activation value: (weights * inputDhbVector).

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

И, конечно же, правило обучения персептрона.

learn: inputVector target: target
"Applies the perceptron learning rule after looking at one training example"

| input output error delta |
output := self value: inputVector.
error := target - output.

input := self prependBiasToInput: inputVector.

delta := learningRate * error * input *
(activation derivative: weights * input).

Класс SLPerceptron

Однослойный персептрон (согласно моей конструкции) представляет собой контейнер нейронов. Единственная переменная экземпляра этоneuronsмассив.

Object subclass: #SLPerceptron
instanceVariableNames: ‘neurons’
classVariableNames: ‘’
package: ‘NeuralNetwork’

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

initialize: inputSize classes: outputSize
“Creates an array of neurons”
neurons := Array new: outputSize.

1 to: outputSize do: [ :i |
neurons at: i put: (Neuron new initialize: inputSize). ]

Выход однослойного персептрона является вектором скалярных выходов каждого нейрона в слое.

value: input
“Returns the vector of outputs from each neuron”
| outputVector |

outputVector := Array new: (neurons size).

1 to: (neurons size) do: [ :i |
outputVector at: i put: ((neurons at: i) value: input) ].

^ outputVector

Если мы попросим SLPerceptron изучить, он передаст этот запрос всем своим нейронам (в основном, SLPerceptron - это просто контейнер нейронов, который предоставляет интерфейс для управления ими).

learn: input target: output
"Trains the network (perceptron) on one (in case of online learning) or multiple (in case of batch learning) input/output pairs" 1 to: (neurons size) do: [ :i |
(neurons at: i) learn: input target: (output at: i) ].

тестирование

Я тестирую свой SLPerceptron с функцией активации BinaryThreshold на 4 линейно разделимых логических функциях: AND, OR, NAND и NOR, и он сходится на всех них.

Вот тест для функции AND. Другие 3 выглядят точно так же (отличаются только ожидаемые выходные значения).

testANDConvergence
"tests if perceptron is able to classify linearly-separable data"
"AND function" | perceptron inputs outputs k |
perceptron := SLPerceptron new initialize: 2 classes: 1.
perceptron activation: (BinaryThreshold new).

"logical AND function"
inputs := #(#(0 0) #(0 1) #(1 0) #(1 1)).
outputs := #(#(0) #(0) #(0) #(1)).

1 to: 100 do: [ :i |
k := 4 atRandom.
perceptron learn: (inputs at: k) target: (outputs at: k) ].

1 to: 4 do: [ :i |
self assert: (perceptron value: (inputs at: i)) equals: (outputs at: i) ].

И этот тест (или, скорее, демонстрация) показывает, что однослойный персептрон не может выучить функцию XOR (не является линейно-разделяемым).

testXORDivergence
"single-layer perceptron should not be uneble to classify data that is not linearly-separable"
"XOR function"

| perceptron inputs outputs k notEqual |
perceptron := SLPerceptron new initialize: 2 classes: 1.
perceptron activation: (BinaryThreshold new).

"logical XOR function"
inputs := #(#(0 0) #(0 1) #(1 0) #(1 1)).
outputs := #(#(0) #(1) #(1) #(0)).

1 to: 100 do: [ :i |
k := 4 atRandom.
perceptron learn: (inputs at: k) target: (outputs at: k) ].

notEqual := false.

1 to: 4 do: [ :i |
notEqual := notEqual or:
((perceptron value: (inputs at: i)) ~= (outputs at: i)) ].

self assert: notEqual.

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

Что дальше?

  • Внедрение однослойной нейронной сети с пакетным обучением и различными правилами обучения
  • Реализация многослойной нейронной сети с обратным распространением

Оригинальная статья

Footer decor

© www.machinelearningmastery.ru | Ссылки на оригиналы и авторов сохранены. | map