Discussão do artigo "Redes Neurais de Maneira Fácil (Parte 9): Documentação do trabalho"

 

Novo artigo Redes Neurais de Maneira Fácil (Parte 9): Documentação do trabalho foi publicado:

Nós já percorremos um longo caminho e o código em nossa biblioteca está se tornando cada vez maior. Isso torna difícil controlar todas as conexões e dependências. Portanto, eu sugiro criar uma documentação para o código criado anteriormente e mantê-lo atualizado a cada nova etapa. A documentação devidamente preparada nos ajudará a ver a integridade do nosso trabalho.

Assim que o programa for concluído, você receberá uma documentação pronta para uso. Algumas imagens são mostradas abaixo. A documentação completa é fornecida em anexo.



Autor: Dmitriy Gizlyk

 

Material de artigo excelente e útil

Muito obrigado!

 
Legal! A documentação está disponível - você pode fazer isso no SB. Embora fosse necessário parafusar o LSTM nos kernels, você está planejando fazer isso?
 
Aleksey Mavrin:
Legal! A documentação está disponível - você pode fazer isso no SB. Embora fosse necessário parafusar o LSTM nos kernels, isso está planejado?

Sim, está planejado :)

 
Dmitriy Gizlyk:

Sim, está planejado :)

Dmitry, por favor, eu não percebi a princípio por que o seno e o cosseno são adicionados aos valores de entrada

neuron.setOutputVal(inputVals.At(i)+(i%2==0 ? sin(i) : cos(i)) );

Também preciso de uma orientação: devo tentar normalizar os dados de entrada de alguma forma para minhas tarefas?

Agora, nos exemplos, pelo que entendi, tudo é fornecido "como está". Mas, mesmo nos exemplos com fractais, há alguns osciladores de 0 a 1, e os preços podem ser muito maiores que 1, dependendo do instrumento.

Isso não cria uma tendência inicial ao treinar com entradas não normalizadas?

 
Aleksey Mavrin:

Dmitry, diga-me por que o seno e o cosseno são adicionados aos valores de entrada.

Também preciso de uma orientação: devo tentar normalizar os dados de entrada de alguma forma para minhas tarefas?

Agora, nos exemplos, pelo que entendi, tudo é fornecido "como está". Mas mesmo nos exemplos com fractais, alguns osciladores estão entre 0 e 1, e os preços podem ser muito maiores que 1, dependendo do instrumento.

Isso não cria uma tendência inicial ao treinar com entradas não normalizadas?

Isso é para incorporação de tempo. Entrarei em mais detalhes no próximo artigo.
 
Dmitriy Gizlyk:
Isso é para incorporação de tempo. Entrarei em mais detalhes no próximo artigo.

Estou me esforçando para entender o significado)

Os valores de entrada são ajustados aritmeticamente a uma matriz constante de 0 a 1, independentemente dos dados de entrada e de seus valores absolutos.

Entendo a incorporação temporal nesse sentido da seguinte forma: você sobrepõe uma onda senoidal em uma série temporal para que o significado das velas passadas flutue no tempo.

Ok, está claro, aparentemente não importa que as flutuações dos dados de entrada de cada barra tenham uma fase diferente, ou que isso seja uma característica.

Mas então a questão da normalização se torna ainda mais relevante. O significado é bem diferente para EURUSD e SP500, por exemplo.

E, aparentemente, é correto transferir essa incorporação de tempo da bíblia para a função Train.

[Excluído]  
Seria interessante ler sobre embeddings posicionais. Mas há dúvidas de que eles sejam muito necessários. Você pode usar a volatilidade, que é como uma senoide. Mas sei que essa é uma prática comum para séries temporais. Talvez haja algum outro "know-how" inventado.
 

@Dmitriy Gizlyk, a pergunta surgiu enquanto estudava e trabalhava com a biblioteca:

no método de cálculo do gradiente para camadas ocultas, você adiciona outputVal

isso é para compensar seu valor mais tarde no método calcOutputGradients para universalidade, certo?

Você também adicionou a normalização do gradiente.

bool CNeuron::calcHiddenGradients(CLayer *&nextLayer)
  {
   double targetVal=sumDOW(nextLayer)+outputVal;
   return calcOutputGradients(targetVal);
  }
//+------------------------------------------------------------------+
bool CNeuron::calcOutputGradients(double targetVal)
  {
   double delta=(targetVal>1 ? 1 : targetVal<-1 ? -1 : targetVal)-outputVal;
   gradient=(delta!=0 ? delta*activationFunctionDerivative(outputVal) : 0);
   return true;
  }

A questão é se seria mais correto normalizar não o alvo, mas o delta final dessa forma.

double delta=targetVal-outputVal;
delta=delta>1?1:delta<-1?-1:delta;

Por quê? Exemplo: se outputVal estiver próximo de 1 e o gradiente ponderado total da próxima camada também for alto e positivo, teremos um delta final próximo de zero, o que parece errado.

Afinal, o delta do gradiente deve ser proporcional ao erro da próxima camada, ou seja, em outras palavras, quando o peso efetivo de um neurônio é negativo (e possivelmente em alguns outros casos), o neurônio é penalizado por um erro menor do que com um peso positivo. Talvez eu tenha explicado de forma resumida, mas espero que aqueles que estão no assunto entendam a ideia :) Talvez você já tenha notado esse ponto e tomado essa decisão, seria interessante esclarecer os motivos.

Também o mesmo ponto para o código OCL

__kernel void CalcHiddenGradient(__global double *matrix_w,
                                 __global double *matrix_g,
                                 __global double *matrix_o,
                                 __global double *matrix_ig,
                                 int outputs, int activation)
  {
..............   
switch(activation)
     {
      case 0:
        sum=clamp(sum+out,-1.0,1.0)-out;
        sum=sum*(1-pow(out==1 || out==-1 ? 0.99999999 : out,2));
 
Aleksey Mavrin:

@Dmitriy Gizlyk, a pergunta surgiu enquanto estudava e trabalhava com a biblioteca:

no método de cálculo de gradiente para camadas ocultas, você adiciona outputVal

isso é para compensação adicional de seu valor no método calcOutputGradients para universalidade, certo?

Você também adicionou a normalização do gradiente

A questão é se seria mais correto normalizar não o alvo, mas o delta final da seguinte forma

Por quê? Exemplo: se outputVal estiver próximo de 1 e o gradiente ponderado total da próxima camada também for alto e positivo, então agora teremos um delta final próximo de zero, o que parece errado.

Afinal, o delta do gradiente deve ser proporcional ao erro da próxima camada, ou seja, em outras palavras, quando o peso efetivo de um neurônio é negativo (e possivelmente em alguns outros casos), o neurônio é penalizado por um erro menor do que com um peso positivo. Talvez eu tenha explicado de forma resumida, mas espero que aqueles que estão no assunto entendam a ideia :) Talvez você já tenha notado esse ponto e tomado essa decisão, seria interessante esclarecer os motivos.

Também o mesmo ponto para o código OCL

Não é bem assim. Verificamos os valores-alvo, assim como nas camadas ocultas, adicionamos outpuVal ao gradiente para obter o alvo e verificar seu valor. A questão é que o sigmoide tem um intervalo limitado de resultados: função logística de 0 a 1, tanh - de -1 a 1. Se penalizarmos o neurônio por desvio e aumentarmos o coeficiente de peso indefinidamente, chegaremos a um estouro de peso. Afinal, se chegarmos ao valor de um neurônio igual a 1 e a camada subsequente transmitir um erro e disser que devemos aumentar o valor para 1,5. O neurônio obedientemente aumentará os pesos a cada iteração, e a função de ativação cortará os valores no nível 1. Portanto, limito os valores do alvo aos intervalos de valores aceitáveis da função de ativação. E deixo o ajuste fora do intervalo para os pesos da camada subsequente.

 
Dmitriy Gizlyk:

Não é bem assim. Verificamos os valores do alvo, assim como nas camadas ocultas, adicionamos outpuVal ao gradiente para obter o alvo e verificar seu valor. A questão é que o sigmoide tem uma faixa limitada de resultados: função logística de 0 a 1, tanh - de -1 a 1. Se penalizarmos o neurônio pelo desvio e aumentarmos o fator de ponderação indefinidamente, chegaremos ao estouro do peso. Afinal, se o valor de um neurônio for igual a 1 e a camada subsequente transmitir um erro e disser que devemos aumentar o valor para 1,5. O neurônio obedientemente aumentará os pesos a cada iteração, e a função de ativação cortará os valores no nível 1. Portanto, limito os valores do alvo aos intervalos de valores aceitáveis da função de ativação. E deixo o ajuste fora do intervalo para os pesos da camada subsequente.

Acho que consegui. Mas ainda estou me perguntando se essa é a abordagem correta, em um exemplo como esse:

se a rede comete um erro ao dar 0 quando na verdade é 1. A partir da última camada, o gradiente ponderado na camada anterior é (provavelmente, pelo que entendi) positivo e pode ser maior que 1, digamos 1,6.

Suponha que haja um neurônio na camada anterior que produziu +0,6, ou seja, produziu o valor correto - seu peso deve aumentar em mais. E com essa normalização, cortamos a alteração em seu peso.

O resultado é norm(1,6)=1. 1-0,6=0,4, e se o normalizarmos como sugeri, será 1. Nesse caso, inibimos a amplificação do neurônio correto.

O que você acha?

Sobre o aumento infinito de pesos, ouvi dizer que isso acontece no caso de "função de erro ruim", quando há muitos mínimos locais e nenhum global expresso, ou a função não é convexa, algo assim, não sou um super especialista, apenas acredito que você pode e deve lutar com pesos infinitos e outros métodos.

Estou pedindo um experimento para testar as duas variantes. Se eu pensar em como formular o teste )