Дата публикации Apr 4, 2017
В этом посте я опишу мою реализацию однослойного персептрона в Pharo. Он будет поддерживать мультиклассовую классификацию (один или несколько нейронов). Каждый нейрон будет реализован как объект. Код этого проекта можно получить из Smalltalkhub с помощью этого сценария Metacello («Сделай это на игровой площадке» своего изображения Pharo):
Metacello new
repository: 'http://smalltalkhub.com/mc/Oleks/NeuralNetwork/main';
configuration: 'MLNeuralNetwork';
version: #development;
load.
Я начну с иллюстрации проблем дизайна и различных подходов к реализации каждой части этого проекта. Это будет довольно долго, поэтому вот мой окончательный дизайн:
Прежде всего, нам нужно определить персептрон. Это самая основная форма искусственной нейронной сети, однако большинство людей не могут четко определить, что это на самом деле.
Сейчас я буду называть персептрон искусственной нейронной сетью, которая следует процедуре обучения перцептрона.
Это определение подразумевает некоторые ограничения того, что представляют собой персептроны и что они могут делать.
Когда дело доходит до объектно-ориентированной реализации нейронных сетей, это, вероятно, самый важный вопрос, на который нужно ответить. Должны ли веса принадлежать нейрону? Если да, должен ли это быть отправляющий или принимающий нейрон? Или, может быть, они должны принадлежать слою? Или, может быть, для всей сети? Может быть, мы должны даже реализовать их как отдельные объекты?
Будучи сетью с прямой связью только с одним слоем и, следовательно, не имеющей весов, соединяющих два нейрона, однослойный персептрон упрощает эту проблему. В основном у нас есть три варианта:
Второй вариант является наиболее эффективным (умножение вектора на матрицу), но не очень объектно-ориентированным. Что такое нейрон в этой реализации? Очевидно, что сеть - это просто матрица весов + некоторые правила обучения. Должен ли нейрон быть функцией активации со скоростью обучения? Но опять же, хранение их в сети будет еще более эффективным. В общем, нам не нуженNeuron
учебный класс. Все, что нам нужно, это матрица и несколько функций для управления ею. Это не звучит объектно-ориентированным для меня.
Третий вариант в этом случае будет чрезмерным. Это просто усложнит ситуацию. Реализация весов как объектов, вероятно, имела бы какой-то смысл в многослойной нейронной сети, где каждый вес является связью между двумя нейронами (мы можем думать о входах как о поддельных нейронах). Он соединяет два нейрона, посылает сигнал между ними и обладает «силой», которую можно обновить. В результате нейроны не знали бы о других нейронах. Они будут просто получать, обрабатывать и излучать сигналы. Я предполагаю, что такая реализация не будет очень быстрой, но она может быть использована для целей моделирования. Я напишу больше об этой идее в посте, посвященном многослойным сетям.
Первый вариант выглядит наиболее подходящим для однослойных персептронов. И это очень легко реализовать, поэтому я буду придерживаться этого.
В этом проекте есть два способа представления функций активации:
Первый подход будет быстрее и потреблять меньше памяти. Создаем базовый класс 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).
Однослойный персептрон (согласно моей конструкции) представляет собой контейнер нейронов. Единственная переменная экземпляра это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
функция, но этот тест не прошел Это означает, что либо перцептроны (как определено в начале этого поста) не могут иметь сигмоид в качестве их активации, либо у меня недостаточно хорошего понимания того, как реализовать перцептрон с сигмоидом.
© www.machinelearningmastery.ru | Ссылки на оригиналы и авторов сохранены. | map