Esta é minha primeiríssima tarefa com redes neurais. Inteligência artificial é um tema que venho estudando há algum tempo. Até então, estava muito mais engajado na teoria. Esta tarefa é a implementação de um classificador de cores em código RGB. Ou seja, ofereço uma cor para a rede e ela deve ser capaz de dar um nome para essa cor, dentre uma lista de nomes possíveis.

Este texto se parece mais com um relato, um compartilhamento de experiência, e não com um tutorial. Durante o desenvolvimento enfrentei alguns desafios técnicos cujo caminho para encontrar as soluções foram divertidas e enriquecedores.

Definindo o problema

A rede neural deve classificar códigos de cores em RGB, escolhendo um nome de cor dentre 12 cores possíveis. Eu escolhi essas cores. Cada uma dessas cores pode variar. O modelo RGB permite mais de 16 milhões de cores. A rede deve escolher o nome que mais se adequar.

A tabela a seguir mostra todas as cores de referência.

Lista de cores para classificação

A próxima imagem lista variações que eu posso considerar vermelho. A rede neural deve classificar todas elas dessa forma.

Variações da cor vermelha

Gerando o dataset

Um modelo de rede neural precisa ser treinado. Existem algumas formas de fazer isso. Aqui, vou dar para a rede alguns códigos RGB e dizer qual a cor de cada um deles. Com sorte, essa porção de exemplos ela vai servir para ensinar a rede as nuances entre as cores e relacionar o que diferencia uma da outra.

Alguns dias atrás escrevi um texto sobre gerar variações de cores. A postagem, na verdade, nasceu a partir deste problema de classificação. O texto completo está disponível aqui: https://welyab.dev/2024/01/14/gerando-variacoes-de-cores/.

No entanto, as variações geradas a partir das 12 cores de refrência não conseguiam ser suficientes para treinar a rede. Não consegui construir um algoritmo para, por exemplo, gerar variações de azul suficientemente diversas. Era necessário variar entre azul mais claro, puxando para uma tom de neon, até um azul bem escuro. Todos os algoritmos que usei, ao tentar variar uma mesma numa faixa da mais clara até a mais escura, gerava outras cores que fugiam da cor de referência.

A imagem a seguir mostra cerca de 1000 variações da cor de referência azul. O algoritmo utilizado é a distância euclidiana sobre o modelo de cor CIELAB. Embora todas as cores sejam diferentes, elas continuam muito próximas da cor de referência, o que não traz uma variedade capaz de treinar a rede neural.

A alternativa de aumentar a quantidade de variações também não surtiu efeito. A imagem a seguir é uma amostra extraída de 300 mil variações da cor azul. O algoritmo utilizado também é a distância euclidiana com o modelo de cores CIELAB. Mesmo com essa quantidade, as cores ainda não chegaram próximas de azul claro. Além disso, outro problema ainda pior, foram geradas cores que, ao meu ver, não pode ser classificadas como azul.

Para contornar esses problemas, eu mesmo criei algumas variações de cores para cada uma das referências. Adicionei alguns tons claros, escuros, com mais e menos saturação, etc. A partir dessas variações, executei o mesmo algoritmo da distância euclidiana com modelo de cores CIELAB em conjunto com algumas variações diretas no RGB. As imagens a seguir mostram o comportamento para a cor azul. Agora as cores variam entre mais claras e mais escuras com bastante variedade.

Variações de azul
Variações da cor azul

Essa estratégia foi usada para gerar variações de todas as outras cores.

Implementação

A rede foi implementada com o framework Deeplearning4j – DL4J (https://deeplearning4j.konduit.ai/). É uma biblioteca para rodar modelos de aprendizagem com redes neurais na Java Virtual Machine – JVM.

Testei algumas arquiteturas. No final a rede ficou com 4 camadas internas.

NeuralNetConfiguration
    .Builder()
    .seed(seed)
    .activation(Activation.TANH)
    .weightInit(WeightInit.XAVIER)
    .updater(Sgd(0.1))
    .l2(1e-4)
    .list()
   
.layer(DenseLayer.Builder().nIn(numInputs).nOut(24).activation(Activation.RELU).build())
    .layer(DenseLayer.Builder().nOut(24).activation(Activation.RELU).build())
    .layer(DenseLayer.Builder().nOut(24).activation(Activation.RELU).build())
    .layer(DenseLayer.Builder().nOut(16).activation(Activation.RELU).build())
    .layer(
        OutputLayer
            .Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
            .activation(Activation.SOFTMAX)
            .nIn(numInputs)
            .nOut(outputNum)
            .build()
    )
    .build()

Testei várias funções de ativação. Acabei deixando as camadas internas com a função RELU. Outras configurações foram obtidas a partir dos exemplos disponibilizados no repositório do DL4J (https://github.com/deeplearning4j/deeplearning4j-examples).

Para a camada de saída, eu sabia que a função de ativação SOFTMAX é útil para modelos de classificação com múltiplos rótulos, como é caso desta tarefa, onde a rede precisa escolher entre verde, vermelho, azul, branco, preto, etc. Essa função entrega valores entre 0 e 1, que podem ser interpretados como uma probabilidade. Assim, ao oferecer um RGB para a rede, a resposta é o nó com a maior probabilidade.

Treinamento da rede

Nos primeiros testes, a rede não conseguia aprender o nome das cores. Testei diversas arquiteturas e funções de ativação, mas os resultados não eram satisfatórios.

Cheguei a pensar sobre o tamanho do conjunto de dados. Em uma pesquisa mais detalhada, encontrei um trabalho que resolve exatamente o mesmo problema (https://github.com/AjinkyaChavan9/RGB-Color-Classifier-with-Deep-Learning-using-Keras-and-Tensorflow). O conjunto de dados usado nele contém apenas cerca de 5000 exemplos para descrever 11 cores. Assim, descartei que meu problema poderia ser o tamanho da base de exemplos. Mesmo assim, no gerei cerca de 45 mil exemplos para descrever 12 cores.

As coisas mudaram quando comecei a normalizar os valores de entrada. É um processo que ajusta a escala, deixando a média próxima de 0 e o desvio padrão próximo de 1.

Imagine um modelo de rede neural para estimar o valor máximo do empréstimo bancário oferecido com risco mínimo a um cliente. A rede recebe como informações o salário, a idade, o tempo de profissão, se tem curso superior, etc. Note que esses valores podem variar bastante entre si. O salário é algo que vai de R$ 1412,00 até, digamos, R$ 10000,00, ou mais. Faz sentido dizer que a faixa de idade vai de 18 anos até 100 anos. O tempo de profissão pode começar com 1 ano e ir até 30 anos. A informação de curso superior é algo descrito como “sim” ou “não”, etc. O processo de normalização, assim, busca deixar esses valores em intervalos mais próximos entre si, como falado, variando a média e o desvio padrão.

Esse tema foi algo que surgiu na minha pesquisa do TCC, sobre Xadrez e Inteligência Artificial (https://welyab.dev/2023/12/21/terminei-meu-tcc/). Cheguei pensar sobre isso para esse problema de classificação de cores, mas não achei que fosse um problema, pois os valores de RGB estão todos entre 0 e 255.

Para normalizar os valores usando o DL4J, usei o código abaixo, encontrado nos exemplos do framework. O processo interno utilizado é um mistério pra mim. Felizmente, a rede começou a convergir muito bem durante o processo de treinamento.

val normalizer: DataNormalization = NormalizerStandardize()
normalizer.fit(trainingData)
normalizer.transform(trainingData)
normalizer.transform(testData)

O treinamento com 3000 iterações fez a rede convergir bem. Utilizei 65% do conjunto de exemplos para treinamento e os 35% restantes para teste. Teve 100% de precisão nessa etapa.

for (i in 0..3000) {
    network.fit(trainingData)
}

val eval = Evaluation(numClasses)
val output = network.output(testData.features)
eval.eval(testData.labels, output, testMetaData)

Resultados

Os nomes das cores abaixo são os valores previstos pela rede neural após o treinamento. Logo na primeira linha, as duas primeiras cores foram sinalizadas como verde. Eu não concordo. Diria que está mais próximo do marrom, ou amarelo bem escuro. Alguns tons de azul bem claro foram classificados como cinza. A rede apresentou também falha em classificar outras cores.

Imagino que todos problemas de classificação poderiam ser resolvidos com a geração de um conjunto de exemplos mais completo de diversificado. Os tons de azul claro que foram classificados como cinza podem ser contornados ao oferecer à rede exemplos com essas informações, dentre outros ajustes.

Mas o exercício acabou aqui =)

Conclusão

Fiquei bastante satisfeito com o resultado. A jornada trouxe muito aprendizado. Classificar cores mostrou-se um ótimo problema de introdução ao estudo redes neurais profundas.

Alguns temas estão anotados para aprofundamento nos estudos. Saber quais funções de ativação usar para determinados problemas é essencial.

Também, normalizar os dados foi primordial. O conhecimento sobre como isso acontece, especialmente usando o DL4J, no entanto, ficou muito, muito raso.

O código fonte está disponível no Github: https://github.com/welyab/ai-studies-dl4j/tree/main/src/main/kotlin/dev/welyab/ai/classification/colornames

Referências