O código fonte para este artigo está disponível aqui.
Nota para nerds: O código mostrado neste artigo pode ser incompleto e não conter todas as verificações de segurança que você costuma executar em seu código, uma vez que é dado aqui apenas para fins demonstrativos. O código fonte para download pode conter bugs (não existe, definitivamente, software sem bugs). Ele é fornecido como é, sem qualquer garantia. Você pode usar e redistribuí-lo como quiser, desde que mencione o autor e o site onde foi publicado originalmente.
Recentemente, eu estava revirando minhas fontes e me deparei com uma pequena biblioteca RNA (rede neural artificial) que escrevi há alguns meses em linguagem de 64 bits Intel Assembly (sintaxe FASM) e decidi compartilhá-la na esperança de que possa ser útil em alguns casos.
Rede neural artificial
A Internet está cheia de artigos sobre este tópico em geral ou em profundidade. Pessoalmente, prefiro não criar um clone com imagens de sinapses etc. Em resumo, RNA é um modelo computacional inspirado na forma como nosso cérebro parece funcionar. Há um artigo na Wikipedia (inglês) muito bom que dá várias explicações. Parece ser importante mencionar que, ao dizer “RNA”, as pessoas costumam pensar em perceptron ou perceptron multi-camadas, mas há muito mais tipos por aí. Você deveria conferir este artigo (inglês) da Wikipedia se estiver interessado.
No entanto, este artigo aborda a implementação de perceptron multi-camadas na linguagem Assembly, o que é mais fácil do que parece. A biblioteca é adequada para a criação de perceptron multi-camadas com qualquer número de camadas ocultas, qualquer número de neurônios de entrada e de saída, embora ele esteja vinculado ao Linux de 64 bits, vou tentar explicar como você pode alterar o código para torná-lo compatível com Windows 64, mas seria necessário muito mais esforço para reescrever a coisa toda para rodar em plataformas de 32 bits.
Neurônio
Esta é a base de todo o projeto. Neurônio é a parte principal do cálculo. Neste exemplo, todos os neurônios são dispostos em uma lista encadeada, tendo os neurônios de entrada no início da lista e os neurônios de saída no seu final. É importante mencionar que todos eles devem ser processados na mesma ordem em que aparecem na lista encadeada. Antes de qualquer coisa, vamos definir uma estrutura, que deverá conter todas as informações que precisamos para um único neurônio:
struc list
{
.prev_ptr dq ?
.next_ptr dq ?
}
struc neuron
{
.list list ;Pointers to previous and next neurons
.input dq ? ;Pointer to the first input synapse
.output dq ? ;Pointer to the first output synapse
.value dq ? ;Resulting value of the neuron
.signal dq ? ;Error signal
.sum dq ? ;Sum of all weighted inputs
.bias dq ? ;Bias weight (threshold)
.bias_delta dq ? ;Bias weight delta
.index dw ? ;Index of the given neuron
.num_inputs dw ? ;Number of input synapses
.num_outputs dw ? ;Number of output synapses
.type dw ? ;Type of the neuron (bit field)
.size = $ - .list
}
A figura 1 mostra a disposição dos neurônios em um perceptron projetado para executar a operação XOR. Ele tem 2 neurônios de entrada, três na camada escondida e dois de saída. As setas mostram a ordem de processamento.
Figura 1
Eu implementei esse perceptron com 2 neurônios de saída apenas para fins de teste, como ele poderia muito bem ser implementado com um único de saída, onde o valor de saída > 0,5 seria 1 e abaixo seria 0.
Sinapse
Não haveria perceptron sem ligações sinápticas. Esse é o lugar onde a seguinte estrutura aparece em cena.
struc synaps { .inputs list ;Pointers to previous and next input synapses ;if such exist .outputs list ;Pointers to previous and next output synapses ;if such exist .value dq ? ;Value to be transmitted .weight dq ? ;Weight of the synapse .delta dq ? ;Weight delta .signal dq ? ;Error signal .input_index dw ? ;Index of the input neuron in the list of neurons .output_index dw ? ;Index of the output neuron in the list of neurons dd ? ;Alignment .size = $ - .inputs }
À primeira vista, ele pode ser um pouco difícil de entender, porque há muitos ponteiros em ambas as estruturas. Infelizmente, minhas habilidades verbais estão longe de serem perfeitas, portanto, deixe-me ilustrar a forma como os neurônios estão interconectados com as sinapses nesta primeira implementação, na esperança de que minhas habilidades gráficas não sejam piores que as verbais).
Figura 2
A Figura 2 mostra que cada neurônio (exceto os de saída) tem um ponteiro (neuron.output) para uma lista de sinapses que precisam ser alimentadas com valor calculado desse neurônio. Para um neurônio, suas saídas de sinapses estão ligadas com ponteiros synaps.outputs. Por sua vez, cada neurônio (exceto os de entrada) tem um ponteiro (neuron.input) para uma lista de sinapses da qual recolher entradas. Na figura, cada seta cinza vai de um neurônio na camada da esquerda para neurônio na camada da direita por meio da ligação sináptica correspondente.
Processando um único neurônio
Cada neurônio na rede é processado com a mesma função, na qual o protótipo em C é assim:
void neuron_process(neuron_t* n, int activation_type);
Onde n é um ponteiro (?) para o neuron (?) que queremos processar e activation_type especifica qual função de ativação deve ser usada. Como já mencionado anteriormente, essa aplicação tem apenas uma função de ativação – logística (conhecida como exponencial):
f (x) = 1,0 / (1,0 + exp (-2 * x))
O seguinte trecho de código é uma implementação Assembly de EXP():
;double exp(double d)
exp:
push rbp
mov rbp, esp
sub rsp, 8
push rbx rcx rdx rdi rsi
movsd qword [rbp-8], xmm0
fld qword [rbp-8]
fld2e
fmulp st1, st0
fld st0
frndint
fsub st1, st0
fxch st1
f2xm1
fld1
faddp st1, st0
fscale
fstp st1
fstp qword [rbp-8]
fwait
movsd xmm0, qword [rbp-8]
pop rsi rdi rdx rcx rbx
add rsp, 8
leave
ret
Agora, o x. Ele é uma soma de produtos de valor e peso de toda a entrada de ligações sinápticas mais peso bias de um neurônio. O resultado de f() é então armazenado para cada e todos as saída de ligações sinápticas (se não neurônio de saída), em conformidade com o diagrama mostrado na figura 3:
A rede
Estamos quase terminando de construir a rede. Vamos definir uma estrutura que incorpore todas as informações sobre nossa perceptron (?) e todos os valores necessários para a treinamento e execução:
struc net
{
.neurons dq ? ;Pointer to the first neuron in the linked list of neurons
.outs dq ? ;Pointer to the first output neuron
.num_neurons dd ? ;Total amount of neurons
.activation dd ? ;Which activation method to use (we only have one here)
.qerror dq ? ;Mean quadratic error
.num_inputs dw ? ;Number of input neurons
.num_outputs dw ? ;Number of output neurons
.rate dq ? ;Learning rate regulates learning speed
.momentum dq ? ;Roughly saying - error tolerance
.size = $ - .neurons
}
A diversão
O código fonte ligado a este artigo implementa todas as funções necessárias para manipular a rede, conforme necessário. Todas as funções são exportadas pela biblioteca e descritas em “ann.h“. No entanto, nós só precisamos lidar com algumas delas:
net_t* net_alloc(void);
Esta função aloca o objeto net_t e retorna um ponteiro.
void net_fill(net_t* net, int number_of_neurons, int number_of_inputs, int number_of_outputs);
Esta função preenche a rede com uma quantidade requerida de neurônios e define todos os valores e os ponteiros apropriadamente.
void net_set_links(net_t* net, int* links);
Esta função é responsável pela configuração de todas as ligações sinápticas entre os neurônios. Enquanto a rede é um ponteiro para estrutura anteriormente alocada net_t structure, links é um ponteiro para o array de pares de números inteiros terminados por um par de 0 a:
int pairs[][2]={
{1, 3},
{1, 4},
{2, 4},
{2, 5},
{3, 6},
{3, 7},
{4, 6},
{4, 7},
{5, 6},
{5, 7},
{0, 0}};
O array acima é exatamente aquele utilizada na aplicação do teste fornecido, a fim de estabelecer ligações como mostrado na figura 3.
double net_train(net_t* net, double* values, double* targets);
Esta função é responsável por tudo o que for necessário a fim de treinar a nossa perceptron usando o paradigma de treinamento back-propagation. Retorna erro quadrático médio de todos os neurônios de saída (que também é acessível através de net -> qerror).
values – array de valores a ser alimentado na rede antes de executá-lo (a função não verifica se o cumprimento do array é apropriado, então seu código é responsável por isso);
targets – array de resultados esperados.
Devido ao fato de que estamos utilizando a função ativação logística, é necessário normalizar os dados de entrada para estarem em [0 <x <1] (saídas estariam aqui também).
Execute essa função quantas vezes forem necessárias para obter um erro razoável. Você terá que alternar a taxa de impulso e parâmetros para obter melhores valores, mas pode começar com 0,9 de taxa e 0,02 no momento. É importante especificar esses valores, já que a biblioteca não verifica se eles estão definidos ou não!
void net_run(net_t* net, double* values);
Esta função é utilizada para executar a rede.
values – Igual ao caso da função net_train;
Essa função não retorna um valor, então você precisa acessar manualmente net-> outs.
Código fonte anexado
O código fonte anexado pode ser compilado com flat assembly:
fasm libann.asm libann.o
e ligado a qualquer código C compilado com o GCC em Linux de 64 bits.
Tornando o código compatível com Windows
Isso requer um pouco de trabalho. Antes de tudo, você teria que editar o arquivo libann.asm, mudando o format elf64 para format MS64 COFF e atributos de seções de acordo. Você também teria que fazer algumas alterações no código. Linux de 64 bits usa AMD64 ABI, enquanto a Microsoft possui o seu próprio. As principais diferenças estão em como os parâmetros são passados para as funções. Enquanto no Linux eles são passados por meio de RDI, RSI, RDX, RCX, R8 e R9 (todo o resto na pilha na ordem inversa) registram para inteiros e XMM0 – XMM7 para duplas, a Microsoft usa RCX, RDX, R8 e R9 para inteiros e XMM0 – XMM3 para duplas e quaisquer parâmetros adicionais são passados na pilha na ordem inversa.
Saída
A saída para o problema XOR deve ser semelhante a esta:
Obrigado pela leitura! Espero que este artigo tenha sido interessante e possa ser útil.
Texto original disponível em http://syprog.blogspot.com.br/2012/03/trivial-artificial-neural-network-in.html