Skip to content

Latest commit

 

History

History

perceptron-adaline

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

Perceptrons e Adaline

1 Introdução

Dando sequência ao tópico de Redes Neurais Artificiais, é apresentado neste trabalho o processo de treinamento de um Perceptron e um Adaline, visando encontrar os pesos e bias finais para a base de dados representada na tabela tb1, abaixo.

s_1s_2t
1.01.01
1.11.51
2.51.7-1
1.02.01
0.31.41
2.81.0-1
0.81.51
2.50.5-1
2.31.0-1
0.51.11
1.91.3-1
2.00.9-1
0.51.81
2.10.6-1

Os Perceptrons foram propostos por Frank Rosenblatt, um psicólogo. É um tipo de rede neural destinada a fazer classificações lineares, como será exibido posteriormente. Neste algoritmo, existe uma atualização dos pesos em caso de erro na dedução pela máquina. Então, o treinamento passa a ser um processo iterativo, no qual a rede neural retorna a resposta primeiro, e depois é ajustada de acordo com a exatidão do resultado.

O Adaline é um neurônio que apresenta um algoritmo de treinamento baseado na regra delta ou LMS (least mean square). Proposto e implementado por Bernard Widrow e Ted Hoff na Stanford University, em 1960, possui como diferencial a possibilidade de trabalhar com entradas e saídas contínuas, sendo a atualização dos pesos proporcional à diferença entre o valor desejado e o obtido. Também é um classificador linear \cite{yamanaka}.

Além disso, alguns resultados serão exibidos de forma gráfica, ou seja, será necessário implementar, também, uma função para plotagem dos dados. Finalmente, um novo conceito é introduzido, o de learning rate, o qual será abordado com maiores detalhes nas seções a seguir.

2 Objetivos

  • Aprimorar o conhecimento sobre Redes Neurais Artificiais e obter experiência prática na implementação das mesmas.
  • Implementar o algoritmo de treinamento de um Perceptron e de um Adaline.
  • Realizar o treinamento destas redes para a tabela tb1.

3 Materiais e Métodos

Para implementação da rede neural foi utilizada a linguagem de programação Common Lisp, compilando-a com o SBCL (Steel Bank Common Lisp). Como interface de desenvolvimento, foi utilizado o Emacs em Org Mode, configurado com a plataforma SLIME (The Superior Lisp Interaction Mode for Emacs) para melhor comunicação com o SBCL. Foi utilizada uma abordagem bottom-up para o desenvolvimento. O código produzido segue majoritariamente o paradigma funcional, sendo este trabalho como um todo uma obra de programação literária. Uma parte das funções já foram implementadas em Regra de Hebb.

4 Perceptron

Inicialmente, o Perceptron será implementado. Como no treinamento de um perceptron é utilizada a execução da rede neural, a função running-single deve estar presente em iterative-training. Running-single é definida da seguinte forma:

(defun running-single (input weights threshold net-fn activation-fn)
  (funcall activation-fn (funcall net-fn weights input) threshold))

Desta forma, é possível executar uma rede neural para um único conjunto de entradas:

;;(running-single input weights threshold net-fn activation-fn)
(running-single '(1 1 1) '(2 2 0) 0 #'net #'activation)
1

Como as funções net e activation ainda são as mesmas, não há necessidade de modificá-las. Já a função de training era chamada da seguinte maneira:

;;(training source target weights)
(training '((1 1 1) (-1 1 1) (1 -1 1) (-1 -1 1)) '(1 -1 -1 -1) '(0 0 0))
22-2

Como não é possível passar a função de ajuste dos pesos e o comportamento geral do treinamento é diferente, training será alterada. Primeiramente, a função de atualização dos pesos de um único par source target é implementada:

(defun perceptron-update (source target output weights learning-rate)
  (if (eq output target)
      (list weights nil)
      (list (mapcar #'(lambda (weight source)
			(+ weight (* learning-rate target source)))
		    weights source)
	    t)))

Sendo a chamada da seguinte forma:

;;(perceptron-update source target output weights learning-rate)
(perceptron-update '(-1 -1 1) -1 0 '(1 1 1) 1)
(2 2 0)T

Esta função retorna dois valores. O primeiro corresponde ao valor atualizado dos pesos, enquanto o segundo informa se alguma alteração foi feita. Isto será útil na determinação da parada da iteração. Abaixo a implementação de iterative-training. Tal função é adequada para ambos os algoritmos, ou seja, consegue se ajustar tanto à execução do perceptron quanto da adaline. Para isto, algumas regras devem ser impostas.

Primeiramente a função de update precisa retornar uma lista do tipo =’(new-value change-p)=, indicando os novos valores de pesos e se houve alguma atualização nos mesmos. Além disso, deve receber o source, o target, o output obtido, os pesos antigos e a taxa de aprendizagem.

A segunda regra se refere à função de condição de parada, a qual deve receber o valor antigo dos pesos, o valor novo (do tipo =’(new-value change-p)=), o valor de p corrente, a tolerância da alteração de pesos, o número de ciclos atual e o máximo. O valor de p determina como está a execução daquele ciclo até o momento. Ele é quem será atualizado pela função de parada. Assim, um retorno de true fará o algoritmo continuar a execução.

As funções de net e de ativação devem continuar com a mesma assinatura das já implementadas. Assim, iterative-training é definida:

(defun perceptron-stop-condition (old update current-p tolerance current-cicles max-cicles)
  (declare (ignorable old tolerance current-cicles max-cicles))
  (or (second update) current-p))

(defun iterative-training (source-list target-list initial-weights threshold learning-rate tolerance max-cicles update-fn stop-fn net-fn activation-fn)
  (let (quadratic-error quadratic-error-aux)
    (labels ((rec (w p src trg cicle)
	       (if (and src trg)
		   (let* ((output (running-single (car src) w threshold net-fn activation-fn))
			  (target (car trg))
			  (update (funcall update-fn (car src) target output w learning-rate)))
		     (push (expt (- target output) 2) quadratic-error-aux)
		     (rec (first update)
			  (funcall stop-fn w update p tolerance cicle max-cicles)
			  (cdr src) (cdr trg) cicle))
		   (progn
		     (push (list cicle
				 (apply #'+ quadratic-error-aux)
				 1)
			   quadratic-error)
		     (setf quadratic-error-aux nil)
		     (if p
			 (rec w nil source-list target-list (1+ cicle))
			 w)))))
      (values (rec initial-weights t source-list target-list 0)
	      (nreverse quadratic-error)))))

Para a porta lógica and, tem-se a seguinte chamada:

;;(iterative-training source-list target-list initial-weights
;;		      threshold learning-rate tolerance max-cicles
;;		      update-fn stop-fn net-fn activation-fn)
(iterative-training
 '((1 1 1) (1 -1 1) (-1 1 1) (-1 -1 1)) '(1 -1 -1 -1) '(0 0 0) 0 1 0 0
 #'perceptron-update #'perceptron-stop-condition #'net #'activation)
11-1

Com estes pesos, podemos utilizar a função running, para verificar a saída:

;;(running inputs weights threshold net-fn activation-fn)
(running '((1 1 1) (1 -1 1) (-1 1 1) (-1 -1 1)) '(1 1 -1) 0 #'net #'activation)
1-1-1-1

Como o resultado obtido foi o mesmo da função lógica and, o treinamento foi bem sucedido.

Assim, realiza-se o mesmo processo para a base de dados da tb1, tratada no código pela variável tbsrc.

;;(iterative-training source-list target-list initial-weights
;;		      threshold learning-rate tolerance max-cicles
;;		      update-fn stop-fn net-fn activation-fn)
(iterative-training
 tbsrc '(1 1 -1 1 1 -1 1 -1 -1 1 -1 -1 1 -1) '(0 0 0) 0 1 0 0
 #'perceptron-update #'perceptron-stop-condition #'net #'activation)                          
-2.62.19999981

Testando:

;;(running inputs weights threshold net-fn activation-fn)
(running tbsrc '(-2.6 2.1999998 1) 0 #'net #'activation)
11-111-11-1-11-1-11-1

Logo, os valores de w_1, w_2 e b são respectivamente: -2.6, 2.1999998 e 1.

Para a parte de plotagem, o pacote eazy-gnuplot será utilizado. A função abaixo recebe um caminho de saída, uma tabela de pontos e uma fronteira do tipo =’((x_i y_i) (x_f y_f))= (a qual será convertida em uma reta) e os imprime na tela:

(defun scatter-plot (output table &optional boundary (min -1) (max 1)
				    (initial-color "red") (final-color "blue"))
  (eazy-gnuplot::with-plots (*standard-output* :debug nil)
    (eazy-gnuplot::gp-setup :terminal '(:pngcairo) :output output)
    (eazy-gnuplot::gp :set :palette
		      (list (concatenate 'string
					 "defined ("
					 (write-to-string min)
					 " '"
					 initial-color
					 "', "
					 (write-to-string max)
					 " '"
					 final-color
					 "')")))
    (eazy-gnuplot::plot
     (lambda ()
       (loop
	  for p in boundary
	  do (format t "~&~{~a~^ ~}" p)))
     :title "Boundary"
     :with '(:lines))
    (eazy-gnuplot::plot
     (lambda ()
       (loop
	  for p in (ensure-plot-format table)
	  do (format t "~&~{~a~^ ~}" p)))
     :title "Points"
     :with '(:points :pt 7 :lc :palette)))
  output)

(defun ensure-plot-format (table)
  (let* ((lt (length (car table)))
	 (rst (case lt
		(1 '(0 0))
		(2 '(0))
		(3 nil)
		(otherwise (error "Couldn't ensure plotable format")))))
    (mapcar #'(lambda (x) (append x rst)) table)))
  

A função a seguir retorna os dois pontos necessários para traçar a fronteira de separação linear, indo de x_min a x_max.

(defun linear-boundary (weights threshold min max)
  (destructuring-bind (w1 w2 b) weights
    (labels ((equation (x) (/ (- threshold b (* x w1)) w2)))
      (list (list min (equation min))
	    (list max (equation max))))))

Para a tb1, utilizando os pesos encontrados, representados por w-perceptron:

;;(scatter-plot output table boundary)
;;(linear-boundary weights threshold min max)
(scatter-plot "plots/scatter-plot-perceptron.png" tb1
	      (linear-boundary w-perceptron 0 0.3 2.8))

./plots/scatter-plot-perceptron.png

5 Adaline

Para o Adaline, é necessário uma função para inicialização aleatória dos pesos, para isto:

(defun random-weights (n min max)
  (let ((range (float (- max min))))
    (loop for i from 1 upto n collecting (+ min (random range)))))

Seguindo a mesma lógica anterior, a função de atualização dos pesos deve ser implementada (seguindo as regras colocadas):

(defun adaline-update (source target output weights learning-rate)
  (let ((er (- target output)))
    (list (mapcar #'(lambda (weight source)
		      (+ weight (* learning-rate er source)))
		  weights source)
	  t)))

Além disso, a condição de parada. Vale notar que a ativação durante o treinamento deve ser uma função identidade, visto que deseja-se a saída contínua, e não a discreta (-1 ou 1).

(defun adaline-activation (net threshold)
  (declare (ignore threshold))
  net)

(defun adaline-stop-condition (old update current-p tolerance current-cicles max-cicles)
  (if (> current-cicles max-cicles)
      nil
      (let ((min 1))
	(mapcar #'(lambda (o-w n-w)
		    (let ((s (- n-w o-w)))
		      (when (< s min)
			(setf min s))))
		old (first update))
	(or (> min tolerance) current-p))))

Assim, é necessário apenas chamar a função iterative-training, utilizando estas novas funções:

;;(iterative-training source-list target-list initial-weights
;;		      threshold learning-rate tolerance max-cicles
;;		      update-fn stop-fn net-fn activation-fn)
(iterative-training
 tbsrc '(1 1 -1 1 1 -1 1 -1 -1 1 -1 -1 1 -1) (random-weights 3 -1 1)
 0 0.05 0.03 1000
 #'adaline-update #'adaline-stop-condition #'net #'adaline-activation)
-1.15796410.18141731.324608

Executando a rede com estes pesos (representados por w-adaline):

;;(running inputs weights threshold net-fn activation-fn)
(running tbsrc w-adaline 0 #'net #'activation)
11-111-11-1-11-1-11-1

A plotagem dos pontos de treinamento, em conjunto com a fronteira de separação é a seguinte:

;;(scatter-plot output table boundary)
;;(linear-boundary weights threshold min max)
(scatter-plot "plots/scatter-plot-adaline.png" tb1
	      (linear-boundary w-adaline 0 0.3 2.8))

./plots/scatter-plot-adaline.png

Vale observar que devido à inicialização aleatória dos pesos, o resultado final pode apresentar variações. Entretanto, a fronteira de separação em ambos os casos é bem semelhante. O código abaixo mostra uma lista com os valores obtidos após alterações na taxa de aprendizagem, indo de 0 até 1.

(let ((initial-weights (random-weights 3 -1 1)))
  (loop for i from 0 upto 0.5 by 0.05 collecting 
       (iterative-training
	tbsrc '(1 1 -1 1 1 -1 1 -1 -1 1 -1 -1 1 -1) initial-weights 0 i 0.03 10
	#'adaline-update #'adaline-stop-condition #'net #'adaline-activation)))
0.23266816-0.6098385-0.81111765
-0.87652680.778132140.20705172
-0.976872440.59332830.5577808
-0.99916870.493592260.76181954
-0.96710790.464413020.7845334
-0.90680050.459664670.6545085
-0.81329470.44168930.36455387
-0.333828570.5407491-0.30766192
7.0327763.0768452-7.34341
401035000.0522531650.0510136260.0

Para valores mais altos de α, o treinamento não obtém o sucesso desejado.

;;(iterative-training source-list target-list initial-weights
;;		      threshold learning-rate tolerance max-cicles
;;		      update-fn stop-fn net-fn activation-fn)
;;(scatter-plot output table boundary)
(multiple-value-bind (weights er)
    (iterative-training
     tbsrc '(1 1 -1 1 1 -1 1 -1 -1 1 -1 -1 1 -1) (random-weights 3 -1 1) 0 0.05 0.03 1000
     #'adaline-update #'adaline-stop-condition #'net #'adaline-activation)
  (declare (ignorable weights))
  (scatter-plot "plots/scatter-plot-adaline-error.png" er nil))

./plots/scatter-plot-adaline-error.png

6 Conclusão

Pelos resultados obtidos, comprova-se a eficácia de tais métodos para classificações lineares. A plotagem dos pontos de treinamento em conjunto com a fronteira de separação demonstra muito bem este comportamento.

Em relação à taxa de aprendizagem, tal valor apresentou uma forte influência na saída dos pesos através do treinamento por Adaline. Quando o valor de α crescia o suficiente (geralmente acima de 0.5), os resultados ficavam errados, apresentando valores exorbitantes. Além disso, os valores de pesos aleatórios conferem a cada execução um caráter único.

A plotagem da soma dos erros quadráticos de cada ciclo exibiu o comportamento desejado, convergindo bem rapidamente a um valor de tolerância.

\bibliographystyle{plain} \bibliography{../references}