Z.2. Uma Rede Neural com Tcl/Tk

Considerando a grande utilidade das Redes Neurais Artificiais em várias aplicações, mas principalmente as possíveis aplicações em Química (Neural Networks in Chemistry, Applications of Artificial Neural Networks in Chemical Problems e Redes Neurais e suas Aplicações em Calibração Multivariada) resolvi implementar do zero (from scratch) um programa baseado na técnica de Redes Neurais usando a linguagem Tcl/Tk.

O programa foi desenvolvido seguindo o paradigma da programação estruturada conforme a sequência:

  1. Definir o número de neurônios da camada de entrada

  2. Definir o número de camadas escondidas com os respectivos neurônios

  3. Definir o número de neurônios na camada de saída

  4. Definir os pesos das conexões

  5. Definir o bias

  6. Abrir o arquivo de dados para treinamento (dataset)

  7. Ler os dados de entrada

  8. Escrever os dados de entrada nos neurônios da camada de entrada

  9. Propagar o sinal ao longo da rede

  10. Exibir a(s) saída(s)

  11. Calcular o erro

  12. Retropropagar o erro

  13. Repetir a partir da etapa 7 até o final do dataset

Z.2.1. Construindo a Rede

A Rede Neural foi estruturada em uma variável do tipo dicionário com uma estrutura representada na figura Z.26.

Figura Z.26. Diagrama parcial da estrutura da variável criada para representar uma rede neural.

Diagrama parcial da estrutura da variável criada para representar uma rede neural.

A variável armazena em diferentes níveis os neurônios de cada camada, os sinais, os pesos das conexões, o somatório do sinal de entrada (Net), o bias e o sinal de ativação (Activation).

O procedimento buildInputLayer cria os neurônios da camada de entrada e recebe como argumentos a variável do tipo dict neural_network, que armazena a estrutura da rede, e a variável do tipo list input_l que define os número de neurônios da camada de entrada:

#Procedimento para a criação dos neurônios da camada de entrada (input)
#A chave (key) activation armazena a saída (output) de cada neurônio

proc buildInputLayer { neural_network input_l } {
    
    upvar 1 $neural_network n_n
    upvar 1 $input_l i_l

    foreach neuron $i_l {
	dict set n_n input $neuron activation output_$neuron
    }
}

O procedimento buildHiddenLayers cria a estrutura das camadas escondidas e recebe como argumentos as variáveis do tipo dict neural_network e hidden_l que define o número de neurônios na camada escondida:

proc buildHiddenLayers { neural_network hidden_l} {
    
    upvar 1 $neural_network n_n
    upvar 1 $hidden_l h_l
    
    foreach layer [dict keys $h_l] {
	
	#Create the neurons of first hidden layer
	foreach neuron [dict get $h_l $layer] {
	    
	    foreach id_input [dict keys [dict get $n_n input ] ] {
		
		#Obtendo a entrada de cada neurônio da camada input
		set s [dict get $n_n input $id_input activation]
		dict set n_n $layer $neuron $id_input signal $s
		dict set n_n $layer $neuron $id_input weight w
		
	    }
	    
	    #Criação dos campos para armazenar o somatório das entradas (net) e a saída (output)
	    dict set n_n $layer $neuron net sum_input_$neuron
	    dict set n_n $layer $neuron activation output_$neuron
	    #Se houver necessidade podemos incluir o "bias"
	    dict set n_n $layer $neuron bias b
	    #Parâmetro delta para o cálculo do erro
	    dict set n_n $layer $neuron delta delta_$neuron

	}
	break
    }

    #Create the neurons of the remainder hidden layers

    foreach layer [lrange [dict keys $h_l] 1 end] {

	#The returned keys will be in the order that they were inserted into the dictionary! 
	set last_hidden_layer [lindex  [dict keys $n_n] end]
	
	foreach neuron [dict get $h_l $layer] {
	    
	    foreach id_hidden [dict keys [dict get $n_n $last_hidden_layer ] ] {
		
		#Getting the output of each neuron from the last hidden layer
		set s [dict get $n_n $last_hidden_layer $id_hidden activation]
		dict set n_n $layer $neuron $id_hidden signal $s
		dict set n_n $layer $neuron $id_hidden weight w

	    }
	    
	    #Criação dos campos para armazenar o somatório das entradas (net) e a saída (output)
	    dict set n_n $layer $neuron net sum_input_$neuron
	    dict set n_n $layer $neuron activation output_$neuron
	    #Se houver necessidade podemos incluir o "bias"
	    dict set n_n $layer $neuron bias b
	    #Parâmetro delta para o cálculo do erro
	    dict set n_n $layer $neuron delta delta_$neuron
	}
	
    }
}

E o procedimento buildOutputLayer cria os neurônios de saída:

proc buildOutputLayer { neural_network output_l } {

    upvar 1 $neural_network n_n
    upvar 1 $output_l o_l

    
    #Last hidden layer to be used no create the connections to the output layer
    set last_hidden_layer [lindex [dict keys $n_n] end]

    
    #Criando os neurônios da camada de saída
    #A camada de saída só vai precisar da saída de cada neurônio da camada escondida (hidden)
    foreach neuron $o_l {

	
	set n_l_h_l [dict keys [dict get $n_n $last_hidden_layer]]
	
	foreach id_hidden [dict keys [dict get $n_n $last_hidden_layer]] {
	    set s [dict get $n_n $last_hidden_layer $id_hidden activation]
	    dict set n_n output $neuron $id_hidden signal $s
	    dict set n_n output $neuron $id_hidden weight w
	}
	#Criação dos campos para armazenar o somatório das entradas (net), a saída (output) e o bias
	dict set n_n output $neuron net sum_input_$neuron
	dict set n_n output $neuron activation output_$neuron
	dict set n_n output $neuron bias b
	#Parâmetro delta para o cálculo do erro
	dict set n_n output $neuron delta delta_$neuron
    }

}  

O número de camadas e o número de neurônios em cada camada é definido pelas variáveis: input_neurons, hidden_layers e output_neurons.

#Definindo o número de camadas e o número de neurônios em cada camada

#Definição dos neurônios da camada de entrada (input)
#in - id da entrada
set input_neurons {i1 i2}

#Criação das camadas escondidas (hidden) com os respectivos neurônios
dict set hidden_layers hidden_1 {1h1 1h2 1h3}
dict set hidden_layers hidden_2 {2h1 2h2 2h3}
dict set hidden_layers hidden_3 {3h1 3h2 3h3}
dict set hidden_layers hidden_4 {4h1 4h2 4h3}

#Lista dos neurônios na camada de saída
set output_neurons {o1}

A estrutura da rede neural é armazenada na variável rede_neural do tipo dict (dicionário) e é inicializada com os seguintes comandos:

#Criação de uma rede neural usando uma variável do tipo "dicionário" (rede_neural)

#Criando a camada de entrada
buildInputLayer rede_neural input_neurons

#Criando a(s) camada(s) escondida(s)
buildHiddenLayers rede_neural hidden_layers

#Criando a camada de saída
buildOutputLayer rede_neural output_neurons

O número de neurônios na camada de saída deve ser igual ao número de possíveis saídas do problema (ou número de classes). Por exemplo, se for uma classificação binária (2 classes) a rede deverá ter dois neurônios na camada de saída.

Z.2.2. Inicializando a Rede

Com a estrutura da rede já definida é hora de gerar os pesos das conexões com valores aleatórios entre 0 e 1 com o procedimento initWeight.

proc initWeight { neural_network } {
    
    upvar 1 $neural_network nn
    
    #Remove the first layer (input)
    set all_layers [dict keys $nn]
    set layers [ lrange $all_layers 1 end ]
    
    foreach l $layers {

	set neurons [dict keys [dict get $nn $l]]
	foreach n $neurons {

	    set keys [dict keys [dict get $nn $l $n]]

	    foreach k $keys {
		
		if [dict exists $nn $l $n $k weight] {
		    
		    dict set nn $l $n $k weight [expr rand()]
		    puts "Pesos iniciais para $n $k: [dict get $nn $l $n $k weight]"

		}
		
	    }

	}
	
	
    }
}

E em seguida inicializar o bias com o procedimento initBias.

#Procedimento para inicializar os bias

proc initBias { neural_network b } {
    
    upvar 1 $neural_network nn
    
    #Remove the first layer (input)
    set all_layers [dict keys $nn]
    set layers [ lrange $all_layers 1 end ]
    
    foreach l $layers {

	set neurons [dict keys [dict get $nn $l]]

	foreach n $neurons {

	    set keys [dict keys [dict get $nn $l $n]]

	    foreach k $keys {

		if {$k == "bias"} { dict set nn $l $n bias $b }
		
	    }

	}
	
	
    }
}

Esses procedimentos são executados com os comandos:

#Inicializando o weight com valores aleatórios entre 0 e 1
initWeight rede_neural

#Inicializando o bias
initBias rede_neural 0

Z.2.3. Gravando a Rede

Após a criação, na memória, da estrutura da rede neural ela poderia ser treinada imediatamente. Mas, em algum momento teria que ser armazenada em um arquivo em disco para ser usada posteriormente.

Resolvi implementar as operações de gravação e leitura da estrutura da rede neural com os procedimentos saveNeuralNetwork e openNeuralNetwork respectivamente.

#Procedure to save the neural network in a file
#Wojciech Kocjan e Piotr Beltowski, Tcl 8.5 Network Programming, page 205

proc saveNeuralNetwork { neural_network file_name } {

    upvar 1 $neural_network n_n
    
    set channel [open $file_name w]
    puts $channel $n_n
    close $channel

}
#Procedure to open the file with the neural network structure
#Wojciech Kocjan e Piotr Beltowski, Tcl 8.5 Network Programming, page 205

proc openNeuralNetwork { file_name } {
    
    set channel [open $file_name r]
    set neural_network [read $channel]
    close $channel

    return $neural_network
        
}

E, na sequência, executar as operações de gravação e leitura com os comandos:

#Save the neural network structure in file
saveNeuralNetwork rede_neural rede_neural.txt

#Unset the variable just to check commands save and open
#unset rede_neural

#Read the neural network from file
set rede_neural [openNeuralNetwork rede_neural.txt]

Z.2.4. Lendo o Dataset

Para abrir o arquivo com os dados de treinamento (Dataset) implementamos o procedimento openDatasetFile.

#Open the file with the dataset and receive as parameter the name of the file
#openDatasetFile

proc openDatasetFile { file_name } {

    set channel [open $file_name r]
    set dataset [read $channel]
    close $channel

    return $dataset
}

Ler o arquivo de dados e armazenar o identificador na variável dataset.

#Read the dataset from file
set dataset [openDatasetFile dataset_00.txt]

Definimos:

  • input: entradas da rede neural

  • output: saídas da rede neural

  • target: resultados esperados

Após abrir o arquivo com o dataset é preciso definir os campos correspondentes à entrada e os campos correspondentes à saída. E para isso implementamos o procedimento readInputs que irá ler os campos de entrada em cada registro e retornar uma lista de listas contendo os dados de entrada.

#Read the input fields from dataset and extract the label if necessary
#The fields are separated by space
#the parameter input_fields is a list with the numbers of fields which contain the inputs

proc readInputs { dataset input_fields label} {
    
    set dataset_list [split $dataset "\n"]
    
    #Remove the first record if it is a label
    
    if {$label} { set dataset_list [lreplace $dataset_list 0 0] }
    
    set input_list {}
    
    foreach record $dataset_list {
	
	if { $record != "" } {

	    set inputs {}
	    
	    foreach field $input_fields {

		lappend inputs [lindex $record [expr $field - 1]]

	    }

	    lappend input_list $inputs
	}
    }
    
    return $input_list
}

Os campos referentes aos dados de entrada são armazenados na variável input_list com o comando:

#Split the dataset in list of inputs with the parameters { dataset input_fields label (boolean) }
#input_fields is the number of fields of inputs

set input_list [readInputs $dataset {1 2} 1]

#puts "input_list: $input_list"

O comando readInputs recebe como argumentos o identificador do arquivo de dados (dataset), uma lista indicando os campos referentes aos dados de entrada (input_fields) e um argumento (label) do tipo boolean (0 ou 1) para indicar se a primeira linha do arquivo é um comentário que deve ou não ser descartado.

No exemplo anterior os campos 1 e 2 são campos de dados e o argumento label com valor 1 indica que a primeira linha do arquivo contém informações sobre o arquivo que deve ser descartada.

E para ler os campos contendo os dados de saída (target) implementamos o procedimento readTarget:

#Read the target(s) from the dataset

proc readTargets { dataset target_fields label } {

    set dataset_list [split $dataset "\n"]

    #Remove the first record if it is a label
    
    if {$label} { set dataset_list [lreplace $dataset_list 0 0] }

    set target_list {}
   
    foreach record $dataset_list {

	if { $record != "" } {

	    set targets {}

	    foreach field $target_fields {

		lappend targets [lindex $record [expr $field - 1]]

	    }

	    lappend target_list $targets

	}

    }

    return $target_list

}

Da mesma forma que readInputs o procedimento readTargets recebe como argumento os números dos campos com as saídas e um indicador se a primeira linha do dataset deve ser desconsiderada.

#Split the dataset in list of targets with the parameters { dataset target_fields label (boolean) }
#if the targets are stored in more than one field the parameter target_fields could be passed as a list

set target_list [readTargets $dataset { 3 } 1]

É importante lembrar que o número de campos de dados de entrada deve ser igual ao número de neurônios na camada de entrada (input layer). E o número de elementos na lista de resultados esperados deve ser igual ao número de neurônios na camada de saída (output layer) para que o procedimento calcErrorOutputLayer possa atribuir a cada neurônio da camada de saída um dos elementos do parâmetro target_list.

Z.2.5. Calculando o Erro

O procedimento calcErrorOutputLayer foi implementado para calcular o erro de predição, ou seja, a diferença entre os valores gerados pela rede e os valores esperados conforme o Dataset, e o valor de delta.

#Calculate the error

#The parameters target_list (and the dataset) must be formatted in such a way that each item of list target_list
#corresponds to the target of each output neuron

proc calcErrorOutputLayer { neural_network target_list } {

    upvar 1 $neural_network n_n

    set output_neurons_list [dict keys [dict get $n_n output]]

    
 
    if { [llength $output_neurons_list] != [llength $target_list] } {
	puts "ERROR - number of targets is different from number of output neurons"
	return 0
    }
    
    foreach o_n $output_neurons_list t $target_list {

	set output_signal [dict get $n_n output $o_n activation]
	
	set error [expr $t - $output_signal]

	sumErrorPrediction $error
	
#	puts "calcErrorOutputLayer error: $error"
	
	set delta [expr [expr $t - $output_signal] * [expr $output_signal * (1 - $output_signal)]] 
	
	dict set n_n output $o_n delta $delta

    }
    
    return 1
}

Z.2.6. Exibindo a Rede

Para exibir no terminal a estrutura da rede neural implementamos o procedimento printNeuralNetwork:

proc printNeuralNetwork { neural_network } {
    
    upvar 1 $neural_network rede_neural

    puts "Rede Neural"
    foreach layer [dict keys $rede_neural] {
	puts "Camada $layer"
	puts [dict get $rede_neural $layer]
	foreach neuron [dict keys [dict get $rede_neural $layer]] {
	    puts "Neurônio $neuron da camada $layer"
	    puts [dict get $rede_neural $layer $neuron]
	    foreach key [dict keys [dict get $rede_neural $layer $neuron]] {
		if [dict exists $rede_neural $layer $neuron $key weight] {
		    set w [dict get $rede_neural $layer $neuron $key weight]
		    puts "O peso (weight) da conexão do neurônio $neuron com o neurônio $key é $w"
		}
	    }
	}
    }

    }
    proc printNeuralNetwork { neural_network } {
    
    upvar 1 $neural_network rede_neural

    puts "Rede Neural"
    foreach layer [dict keys $rede_neural] {
	puts "Camada $layer"
	puts [dict get $rede_neural $layer]
	foreach neuron [dict keys [dict get $rede_neural $layer]] {
	    puts "Neurônio $neuron da camada $layer"
	    puts [dict get $rede_neural $layer $neuron]
	    foreach key [dict keys [dict get $rede_neural $layer $neuron]] {
		if [dict exists $rede_neural $layer $neuron $key weight] {
		    set w [dict get $rede_neural $layer $neuron $key weight]
		    puts "O peso (weight) da conexão do neurônio $neuron com o neurônio $key é $w"
		}
	    }
	}
    }
}

E é executado com o comando:

  printNeuralNetwork rede_neural

Z.2.7. Treinando a Rede

Com a rede estruturada e o dataset carregado é hora de iniciar o treinamento da rede com o seguinte loop:

set i 0

while { $i < 1000 } {
    
    foreach input_record $input_list target_record $target_list {
	
	puts "\n\nProcessing input_record: $input_record target_record: $target_record"
	
	#set the inputs to the input neurons
	if { ![writeInputs rede_neural $input_record] } { break }
	
	#propagate the signal through the network
	propagateInputs rede_neural
	
	#print the signal of output neurons
	printOutputs rede_neural
	
	if { ![calcErrorOutputLayer rede_neural $target_record] } { break }
	
	backPropagateError rede_neural   

	#Exibindo a rede

#	printNeuralNetwork rede_neural

    }
    
    incr i
    
}

O loop de treinamento lê os dados de entrada (input_record) e de saída (target_record):

foreach input_record $input_list target_record $target_list {

Escreve os dados de entrada nos neurônios de entrada e verifica se o número de campos de entrada corresponde ao número de neurônios de entrada:

#set the inputs to the input neurons
if { ![writeInputs rede_neural $input_record] } { break }

Propaga o sinal através das camadas da rede:

#propagate the signal through the network
propagateInputs rede_neural

Calcula o erro para cada neurônio de saída:

if { ![calcErrorOutputLayer rede_neural $target_record] } { break }

E faz a retropropagação do erro:

backPropagateError rede_neural

Essas etapas são repetidas quantas vezes forem necessárias para minimizar o erro de predição conforme o valor de i e a rede neural, com os pesos das conexões otimizados, é gravada em um arquivo com o comando:

#Save the neural network in file
saveNeuralNetwork rede_neural rede_neural.txt

Agora vamos incluir mais algumas funcionalidades para acompanhar a variação do erro de predição e exibir em gráfico a variação do erro ao longo dos ciclos de treinamento.

Para isso criamos a variável list_error_prediction e mais 3 procedimentos: resetSumErrorPrediction, sumErrorPrediction e getSumErrorPrediction.

#Procedures to control error predicition

proc resetSumErrorPrediction {} {

    global sum_error_prediction

    set sum_error_prediction 0.0

}

proc sumErrorPrediction { e } {

    global sum_error_prediction

    set e [expr pow($e,2)]

    set sum_error_prediction [expr $sum_error_prediction + $e]

}

proc getSumErrorPrediction {} {

    global sum_error_prediction

    return $sum_error_prediction

}

Z.2.8. Gráfico do Erro

E para exibir em gráfico a variação do erro ao longo do treinamento usamos a biblioteca Plotchart:

#######################################################################################
#Graph

#Cria o widget canvas onde será construído o gráfico

canvas .c -background white -width 1000 -height 600
pack   .c -fill both

#Get the max error
#set formated_list_error_prediction [regsub -all " " $list_error_prediction ","]
#puts "Valor máximo do erro: [ expr max($formated_list_error_prediction)]"

#or

set max_error [lindex [lsort -real $list_error_prediction] end]

#set max_error [format "%.1f" $max_error]

set num_epoch [llength $list_error_prediction]

set interval_error [expr round($max_error / 10)]

#set interval_error [format "%.4f" $interval_error]

set interval_epoch [expr $num_epoch / 10]

puts "max_error: $max_error num_epoch: $num_epoch interval_error: $interval_error interval_epoch: $interval_epoch"

# Cria o gráfico com os eixos x e y

set lim_epoch [list 0 $num_epoch $interval_epoch]
set lim_error [list 0 $max_error $interval_error]

set s [::Plotchart::createXYPlot .c $lim_epoch $lim_error]

# Loop foreach que executa o comando "plot" para cada par xy

set x 0
foreach y $list_error_prediction {
    incr x
    $s plot series1 $x $y
}

# Define o nome do gráfico

$s title "Graph Error prediction X Epoch"
$s xtext "Epoch"
$s ytext "Error"

A figura Z.27 mostra um exemplo de como pode variar o erro ao longo das scessivas etapas de treinamento (epoch).

Figura Z.27. Variação do erro ao longo do treinamento de uma rede neural.

Variação do erro ao longo do treinamento de uma rede neural.

O treinamento da rede pode ser feito de duas formas:

  • Treinamento em Batch (Lote): também chamado aprendizado por ciclo no qual os erros são acumulados durante todo o conjunto de dados do Dataset (epoch) e só então é feito os ajustes nos pesos.

  • Treinamento Incremental (ou Padrão): também chamado aprendizado on-line ou por padrão, no qual o ajuste dos pesos é feito a cada linha do Dataset.

Z.2.9. Teste da Rede com o Dataset Iris Flower

Iris Flower é um dataset clássico que está disponível no repositório de Machine Learning da Universidade da California, Irvine (UCI) no link http://archive.ics.uci.edu/ml/machine-learning-databases/iris/.

Este conjunto de dados (Dataset) consiste em 50 amostras de cada uma das três espécies da flor Iris (Iris setosa, Iris virginica e Iris versicolor). Quatro características foram medidas a partir de cada amostra: o comprimento e a largura das sépalas e pétalas, em centímetros. O objetivo é treinar uma Rede Neural para ser capaz de identificar a espécie com base na combinação dessas quatro características.

O arquivo iris.data, com 150 registros, possui a seguinte estrutura:


5.1,3.5,1.4,0.2,Iris-setosa
4.9,3.0,1.4,0.2,Iris-setosa
4.7,3.2,1.3,0.2,Iris-setosa
4.6,3.1,1.5,0.2,Iris-setosa
...
7.0,3.2,4.7,1.4,Iris-versicolor
6.4,3.2,4.5,1.5,Iris-versicolor
6.9,3.1,4.9,1.5,Iris-versicolor
5.5,2.3,4.0,1.3,Iris-versicolor
...
6.3,3.3,6.0,2.5,Iris-virginica
5.8,2.7,5.1,1.9,Iris-virginica
7.1,3.0,5.9,2.1,Iris-virginica
6.3,2.9,5.6,1.8,Iris-virginica
...

Para ler os quatro parâmetros de entrada a rede deve ter 4 neurônios na camada de entrada, mas a camada de saída poderia ter 1 ou 3 neurônios.

Poderíamos usar apenas um neurônio de saída assumindo valores numéricos discretos representando cada uma das três categorias (Ex: 1-setosa, 2-versicolor e 3-virginica).

Ou com 3 neurônios que poderiam assumir valores binários de 0 ou 1 para identificar as três categorias da seguinte forma:

  • 1 0 0 -> Iris-setosa

  • 0 1 0 -> Iris-versicolor

  • 0 0 1 -> Iris-virginica

Decidimos implementar uma rede com 3 neurônios de saída e avaliar o efeito do número de camadas escondidas e o número de neurônios em diferentes camadas.

Mas antes de ler o Dataset é necessário formatar o arquivo para poder ser usado pela rede neural, e para isso implementamos o script format_data_iris.tcl:

#!/usr/bin/env tclsh

#format_data_iris.tcl

proc openDatasetFile { file_name } {

    set channel [open $file_name r]
    set dataset [read $channel]
    close $channel

    return $dataset

}

proc saveDatasetFile { file_name dataset } {

    set channel [ open $file_name w ]
    puts $channel $dataset
    close $channel

}

set dataset [openDatasetFile iris.data]

set formated_dataset [regsub -all {\,} $dataset "  "]

set formated_dataset [regsub -all {Iris-setosa} $formated_dataset "1  0  0"]

set formated_dataset [regsub -all {Iris-versicolor} $formated_dataset "0  1  0"]

set formated_dataset [regsub -all {Iris-virginica} $formated_dataset "0  0  1"]

saveDatasetFile formated_iris.data $formated_dataset

Este script converte o Dataset original iris.data substituindo a vírgula por espaço e o tipo de flor para o formato binário correspondente gerando o arquivo formated_iris.data:


5.1  3.5  1.4  0.2  1  0  0
4.9  3.0  1.4  0.2  1  0  0
4.7  3.2  1.3  0.2  1  0  0
4.6  3.1  1.5  0.2  1  0  0
...
7.0  3.2  4.7  1.4  0  1  0
6.4  3.2  4.5  1.5  0  1  0
6.9  3.1  4.9  1.5  0  1  0
5.5  2.3  4.0  1.3  0  1  0
...
6.3  3.3  6.0  2.5  0  0  1
5.8  2.7  5.1  1.9  0  0  1
7.1  3.0  5.9  2.1  0  0  1
6.3  2.9  5.6  1.8  0  0  1

Em seguida montamos inicialmente uma rede neural com 4 neurônios de entrada:

#Rede
set input_neurons {i1 i2 i3 i4}

Os neurônios da camada de entrada são indicados pela letra i (input) seguido de um índice numérico.

1 camada escondida com 3 neurônios:

#Criação das camadas escondidas (hidden) com os respectivos neurônios
dict set hidden_layers hidden_1 {1h1 1h2 1h3}

Cada camada escondida é idenficiada no formato hidden_N, onde N indica o número da camada e os neurônios são indicados no formato Nhn. Onde N corresponde ao número da camada, h corresponde a hidden e n é um índice numérico para cada neurônio.

E 3 neurônios na camada de saída:

#Lista dos neurônios na camada de saída
set output_neurons {o1 o2 o3}

Os neurônios da camada de saída são indicados pela letra o (output) seguido de um índice numérico.

Depois é definida a taxa de aprendizagem (learning rate):

#Define the learning rate
set learning_rate 0.5

E os comandos para a criação da rede neural são executados:

#Criando a camada de entrada
buildInputLayer rede_neural input_neurons

#Criando a(s) camada(s) escondida(s)
buildHiddenLayers rede_neural hidden_layers

#Criando a camada de saída
buildOutputLayer rede_neural output_neurons

#Inicializando o weight com valores aleatórios entre 0 e 1
initWeight rede_neural

...

Em seguida ler o dataset e montar a lista de dados de entrada na variável input_list:

#Split the dataset in list of inputs with the parameters { dataset input_fields label (boolean) }
#input_fields is the number of fields of inputs

set input_list [readInputs $dataset {1 2 3 4} 0]

E os dados de saída na variável target_list:

#Split the dataset in list of targets with the parameters { dataset target_fields label (boolean) }
set target_list [readTargets $dataset { 5 6 7 } 0]

E fizemos o treinamento da rede por 500 ciclos (epoch):

set i 0

set list_error_prediction {}

while { $i < 500 } {

    resetSumErrorPrediction
    
    foreach input_record $input_list target_record $target_list {
	
	puts "\n\nProcessing input_record: $input_record target_record: $target_record"
	
	#set the inputs to the input neurons
	if { ![writeInputs rede_neural $input_record] } { break }
	
	#propagate the signal through the network
	propagateInputs rede_neural
	
	#print the signal of output neurons
	printOutputs rede_neural
	
	if { ![calcErrorOutputLayer rede_neural $target_record] } { break }
	
	backPropagateError rede_neural   

	#Exibindo a rede

#	printNeuralNetwork rede_neural

    }

    lappend list_error_prediction [getSumErrorPrediction]
    
    incr i
    
}

E erro apresentou queda até 100 epochs e permaneceu constante até o final do treinamento conforme a figura Z.28.

Figura Z.28. Gráfico de variação do erro de previsão ao longo dos ciclos de treinamento da rede neural com o dataset formated_iris.data

Gráfico de variação do erro de previsão ao longo dos ciclos de treinamento da rede neural com o dataset formated_iris.data

Para avaliar o efeito do número da camadas na eficiência de treinamento adicionei mais uma camada escondida:

set input_neurons {i1 i2 i3 i4}

#Criação das camadas escondidas (hidden) com os respectivos neurônios
dict set hidden_layers hidden_1 {1h1 1h2 1h3}
dict set hidden_layers hidden_2 {2h1 2h2 2h3}

#Lista dos neurônios na camada de saída
set output_neurons {o1 o2 o3}

#Define the learning rate
set learning_rate 0.5

O erro chegou a um mínimo de ~60 e não reduziu mais como mostra a figura Z.29.

Figura Z.29. Gráfico de variação do erro de previsão ao longo dos ciclos de treinamento de uma rede com 2 camadas, de 3 neurônios, e o dataset formated_iris.data

Gráfico de variação do erro de previsão ao longo dos ciclos de treinamento de uma rede com 2 camadas, de 3 neurônios, e o dataset formated_iris.data

Repetimos o treinamento com duas camadas escondidas (3 neurônios cada), mas um novo conjunto inicial de pesos (aleatórios) e foi obtido um resultado semelhante (Figura Z.30).

Figura Z.30. Repetição do treinamento de uma rede, com novos pesos aleatórios, 2 camadas escondidas de 3 neurônios cada, e o dataset formated_iris.data

Repetição do treinamento de uma rede, com novos pesos aleatórios, 2 camadas escondidas de 3 neurônios cada, e o dataset formated_iris.data

Adicionei uma terceira camada escondida:

set input_neurons {i1 i2 i3 i4}

#Criação das camadas escondidas (hidden) com os respectivos neurônios
dict set hidden_layers hidden_1 {1h1 1h2 1h3}
dict set hidden_layers hidden_2 {2h1 2h2 2h3}
dict set hidden_layers hidden_3 {3h1 3h2 3h3}

#Lista dos neurônios na camada de saída
set output_neurons {o1 o2 o3}

#Define the learning rate
set learning_rate 0.5

Mas após 500 epochs de treinamento o erro não melhorou.

Resolvi testar a eficiência de 1 camada escondida com 4 neurônios:

set input_neurons {i1 i2 i3 i4}

#Criação das camadas escondidas (hidden) com os respectivos neurônios
dict set hidden_layers hidden_1 {1h1 1h2 1h3 1h4}

#Lista dos neurônios na camada de saída
set output_neurons {o1 o2 o3}

#Define the learning rate
set learning_rate 0.5

E após 500 epochs de treinamento o erro chegou a 5,96.

Colocando mais um neurônio na camada escondida foi obtido um erro de 6,54.

Para conhecer um pouco mais as propriedades das redes neurais resolvi fazer vários treinamentos usando a rede com 1 camada de 6 neurônios e o mesmo dataset. Para isso fiz um primeiro treinamento e salvei a rede neural inicial (rede_neural_original.txt).

Em seguida comentei os comandos para criação de uma nova rede neural e deixei apenas o comando para abrir o arquivo rede_neural_original.txt:

#Criando a camada de entrada
#buildInputLayer rede_neural input_neurons

#Criando a(s) camada(s) escondida(s)
#buildHiddenLayers rede_neural hidden_layers

#Criando a camada de saída
#buildOutputLayer rede_neural output_neurons

#Inicializando o weight com valores aleatórios entre 0 e 1
#initWeight rede_neural

#Inicializando o bias
#initBias rede_neural 1

#Save the neural network structure in file
#saveNeuralNetwork rede_neural rede_neural_original.txt

#Unset the variable just to check commands save and open
#unset rede_neural

#Read the neural network from file
set rede_neural [openNeuralNetwork rede_neural_original.txt]

Repeti o treinamento mais 2 vezes com 100 ciclos e obtivemos o mesmo gráfico de erro x epoch como mostra a figura Z.31.

Figura Z.31. Três repetições de treinamento usando a rede com 1 camada de 6 neurônios (rede_neural_original.txt), e o mesmo dataset (formated_iris.data).

Três repetições de treinamento usando a rede com 1 camada de 6 neurônios (rede_neural_original.txt), e o mesmo dataset (formated_iris.data).

Z.2.10. Dividindo o Dataset em Treinamento e Validação

Até agora fizemos o treinamento de diferentes configurações da rede para avaliar o efeito no treinamento.

Mas o correto é separar o conjundo de dados em dois grupos:

  • dados de treinamento: que serão utilizados para o treinamento da rede (50% a 80% do dataset original)

  • dados de teste: que serão utilizados para verificar sua performance sob condições reais de utilização.

Dica

Além dessa divisão, pode-se usar também uma subdivisão do conjunto de treinamento, criando um conjunto de validação, utilizado para verificar a eficiência da rede quanto à sua capacidade de generalização durante o treinamento, e podendo ser empregado como critério de parada do treinamento. (Fonte: http://conteudo.icmc.usp.br/pessoas/andre/research/neural/desenv.htm)

Para isso o dataset original foi dividido em dois grupos: training_iris_dataset.data e test_iris_dataset.data com o script:

#!/usr/bin/env tclsh

proc openDatasetFile { file_name } {

    set channel [open $file_name r]
    set dataset [read $channel]
    close $channel

    return $dataset

}

set dataset [openDatasetFile formated_iris.data]

set training_iris_dataset [open "training_iris_dataset.data" "w"]

set test_iris_dataset [open "test_iris_dataset.data" "w"]

set dataset_list [split $dataset "\n"]

set i 1

foreach line $dataset_list {

    if {[expr $i % 2]} {

	puts $training_iris_dataset $line
	
    } else {

	puts $test_iris_dataset $line
    }

    incr i
}

close $training_iris_dataset

close $test_iris_dataset

O arquivo training_iris_dataset.data foi usado para treinar a rede e o arquivo test_iris_dataset.data foi usado para calcular o índice de acertos da rede comparando os resultados previstos com os resultados experimentais.

Para o cálculo do índice de acertos implementei inicialmente um loop com o comando foreach que lê lê os dados de entrada (input_record) e de saída (target_record), escreve os dados de entrada nos neurônios de entrada, propaga o sinal através das camadas da rede e simplesmente verifica se o neurônio de saída com o maior sinal é o mesmo neurônio com maior sinal do arquivo de teste.

set i 0

set hit_rate 0

foreach input_record $input_list target_record $target_list {

    puts "\n\nProcessing input_record: $input_record target_record: $target_record"

    #set the inputs to the input neurons
    if { ![writeInputs rede_neural $input_record] } { break }

    #propagate the signal through the network
    propagateInputs rede_neural

    #print the signal of output neurons
    set output_list [printOutputs rede_neural]

    set output_list_w_comma [join $output_list ", "]
    
    set min_output [expr min($output_list_w_comma)]

    set max_output [expr max($output_list_w_comma)]

    set pos_min_output [lsearch $output_list $min_output]

    set pos_max_output [lsearch $output_list $max_output]

    set target_record_w_comma [join $target_record ", "]
    
    set min_target [expr min($target_record_w_comma)]

    set max_target [expr max($target_record_w_comma)]

    set pos_min_target [lsearch $target_record $min_target]

    set pos_max_target [lsearch $target_record $max_target]

    if {$pos_max_output == $pos_max_target} {
	incr hit_rate
    }
    
    incr i
}
puts "\n$i records in test dataset"
puts "hit_rate: $hit_rate"
puts "hit_rate %: [expr ( $hit_rate / double($i) ) * 100.0]"

É um critério de acerto pouco exigente, mas o objetivo é ter uma idéia de como essa rede se comporta em uma atividade de classificação.

Registrei o índice de acertos (com o arquivo test_iris_dataset.data) e após sucessivas etapas de treinamento (com o arquivo training_iris_dataset.data) os resultados (Figura Z.32) mostram que os acertos chegam a 57 (76%) após 4000 ciclos de treinamento (epoch) e permanecem nesse nível até 50000 ciclos e chegam a diminuir para 56 (75%) com 100000 ciclos de treinamento.

Figura Z.32. Gráfico acertos x ciclos de treinamento (epoch) de uma rede com 1 camada escondida de 6 neurônios, e o dataset test_iris_dataset.data com 75 registros.

Gráfico acertos x ciclos de treinamento (epoch) de uma rede com 1 camada escondida de 6 neurônios, e o dataset test_iris_dataset.data com 75 registros.

Figura Z.33. Gráfico do erro x ciclos de treinamento (epoch) de uma rede com 1 camada escondida de 6 neurônios, e o dataset test_iris_dataset.data com 75 registros

Gráfico do erro x ciclos de treinamento (epoch) de uma rede com 1 camada escondida de 6 neurônios, e o dataset test_iris_dataset.data com 75 registros

O erro é reduzido a um mínimo até ~1000 ciclos e permanece praticamente constante até 50000 ciclos.

Mas visualmente foi possível perceber que havia uma diferença no acertos para os 3 diferentes tipos de flor (Setosa, Versicolor e Virginica). E para quantificar essa diferença incluí alguns comandos no programa de teste para calcular o índice de acertos para cada tipo de flor:

  ...
#100
set hit_rate_iris_setosa 0
#010
set hit_rate_iris_versicolor 0
#001
set hit_rate_iris_virginica 0

...

if {$pos_max_output == $pos_max_target} {
	incr hit_rate

	if { $pos_max_target == 0 } {
	    incr hit_rate_iris_setosa
	} elseif { $pos_max_target == 1 } {
	    incr hit_rate_iris_versicolor
	} else {
	    incr hit_rate_iris_virginica

	}

	...

Foi possível constatar que o índice de acertos para a variedade versicolor era bem menor do que os outros dois grupos mesmo depois de sucessivos ciclos de treinamento como mostra a figura Z.34.

Figura Z.34. Gráfico acertos x ciclos de treinamento (epoch) de uma rede com 1 camada escondida de 6 neurônios, e o dataset test_iris_dataset.data com 75 registros.

Gráfico acertos x ciclos de treinamento (epoch) de uma rede com 1 camada escondida de 6 neurônios, e o dataset test_iris_dataset.data com 75 registros.

Já estava quase desistindo mas resolvi investir na redução do número de camadas escondidas e de neurônios por camada. E depois de várias tentativas variando a taxa de aprendizado (lr), bias, número de camadas escondidas, número de neurônios por camada e o número de ciclos de treinamento (epoch), fiquei surpreso ao descobrir que os melhores resultados foram obtidos com uma rede contendo apenas 1 camada escondida com 2 neurônios. (Figura Z.35)

Figura Z.35. Gráfico acertos x ciclos de treinamento (epoch) de uma rede contendo 1 camada escondida com 2 neurônios, lr 0,1, bias 0,9, treinada com o dataset de treinamento training_iris_dataset.datae avaliada com o dataset de teste test_iris_dataset.data, ambos com 75 registros.

Gráfico acertos x ciclos de treinamento (epoch) de uma rede contendo 1 camada escondida com 2 neurônios, lr 0,1, bias 0,9, treinada com o dataset de treinamento training_iris_dataset.datae avaliada com o dataset de teste test_iris_dataset.data, ambos com 75 registros.

Após 10000 epochs com uma rede com 2 neurônios na camada escondida, lr=0,1 e bias=0,9 foram obtidos os seguintes resultados:

  • Rede neural treinada com 10000 epochs:

    75 registros - 70 acertos (93,3%)

    Iris Setosa 25 registros - 25 acertos (100%)

    Iris Versicolor 25 registros - 20 acertos (80%)

    Iris Virginica 25 registros - 25 acertos (100%)

  • Rede neural treinada com 20000 epochs:

    75 registros - 70 acertos (93,3%)

    Iris Setosa 25 registros - 25 acertos (100%)

    Iris Versicolor 25 registros - 22 acertos (88%)

    Iris Virginica 25 registros - 23 acertos (92%)

Com mais 10000 ciclos de treinamento o resultado global permaneceu o mesmo mas com uma pequena alteração nos acertos das classes Versicolor e Virginica.

Conclusões (parciais):

Esses estudos com o tema Redes Neurais foram muito úteis para entender na prática o funcionamento desse tipo de ferramenta.

Insisti no desenvolvimento da minha própria biblioteca (from scratch) como estratégia de aprendizagem mas pretendo, a partir de agora, conheceer melhor as bibliotecas profissionais para poder usar com mais eficiência e produtividade em futuros projetos ligados ao tema Água.

Z.2.11. Links Adicionais

  1. Programa de treinamento rede_neural_09.tcl

  2. Programa de teste test_rede_neural_00.tcl

  3. Site muito interessante que mostra passo a passo a implementação de uma rede neural usano Python e mostra também a implementação de uma rede neural usando orientação a objeto.

  4. Basic Neural Network Tutorial - Theory

  5. How to Implement the Backpropagation Algorithm From Scratch In Python

  6. Mind: How to Build a Neural Network (Part One)

  7. AI : Neural Network for beginners (Part 1 of 3)

  8. Help Getting Started with Applied Machine Learning

  9. Gradiente Descendente - Um método poderoso e flexível para otimização iterativa.

  10. Backpropagation

  11. Texto de referência que explica de forma didática o algoritmo para a retropropagação

  12. Neural Networks With Java

  13. Backpropagator's Review

  14. Introduction to Neural Networks for Senior Design

  15. Activation Functions: Neural Networks

  16. Redes Neurais - Parte 2

  17. How to Implement the Backpropagation Algorithm From Scratch In Python

  18. Um Simulador Interativo de Rede Neural

  19. Basic Neural Network Tutorial - Theory

  20. CS231n Convolutional Neural Networks for Visual Recognition

  21. An Intuitive Explanation of Convolutional Neural Networks

  22. Intelligent and Learning Systems

  23. How do I implement a simple neural network from scratch in Python?

  24. TensorFlow - Get Started

  25. Theano

  26. PyBrain

  27. A Neural Network in 11 lines of Python (Part 1)

  28. 10 misconceptions about Neural Networks (Comentários interessantes das características e mitos sobre redes neurais)

  29. Livro - Neural Networks - A Systematic Introduction

  30. Get started with machine learning using Python

  31. Clean Water AI

  32. Notes On Using Data Science & Machine Learning