Aprendizado multitarefa no TensorFlow com o Head API

Uma introdução aos estimadores personalizados para aprendizagem multitarefa

Simone Scardapane Blocked Unblock Seguir Seguindo 4 de janeiro

Uma característica fundamental da aprendizagem humana é que aprendemos muitas coisas simultaneamente. A idéia equivalente em aprendizado de máquina é chamada de aprendizado multi-tarefa (MTL), e tem se tornado cada vez mais útil na prática, particularmente para aprendizado por reforço e processamento de linguagem natural. De fato, mesmo em situações de single-tarefa padrão, auxiliar adicional tarefas podem ser planejadas e incluídas no processo de otimização para ajudar na aprendizagem .

Este post fornece uma introdução ao campo mostrando como resolver um problema simples de multitarefa em um benchmark de classificação de imagem. O foco está em um componente experimental do TensorFlow, a API Head , que ajuda a projetar estimadores personalizados para o MTL, desacoplando o componente compartilhado da rede neural dos específicos da tarefa. Ao longo da rota, também teremos a oportunidade de discutir recursos adicionais do núcleo do TensorFlow, incluindo tf.data, tf.image e estimadores personalizados.

O código para o tutorial está disponível como um notebook da Colab , fique à vontade para testar e experimentar!

Conteúdo de relance

Para tornar o tutorial mais interessante, consideramos um caso de uso realista, reimplementando (parte de) um artigo de 2014: Detecção Facial Landmark por Deep Multi-task Learning . O problema é simples: recebemos uma imagem facial, e precisamos localizar uma série de pontos de referência , isto é, pontos de interesse na imagem (nariz, olho esquerdo, boca,…) e tags , incluindo a idade e o gênero da imagem. a pessoa. Cada marco / tag constitui uma tarefa separada na imagem, e as tarefas são claramente correlacionadas (ou seja, pensar em prever a posição do olho esquerdo sabendo primeiro onde está o olho direito).

Imagens de amostra do conjunto de dados ( origem ). Os pontos verdes são os pontos de referência e cada imagem também está associada a alguns marcadores adicionais, incluindo idade e sexo.

Dividimos nossa implementação em três partes: (i) carregando as imagens (usando tf.data e tf.image ); (ii) implementar a rede convolucional a partir do trabalho (usando os estimadores customizados do TF); (iii) adicionar a lógica MTL com a API Head. Muitas coisas para ver, não há tempo a perder!

Passo 0 – Carregando o conjunto de dados

Depois de baixar o conjunto de dados ( link ), uma inspeção rápida revela que as imagens são divididas em três pastas diferentes (AFLW, lfw_5590 e net_7876). As divisões de trem e teste são fornecidas por meio de diferentes arquivos de texto, cada linha correspondente ao caminho de uma imagem e rótulos:

A primeira imagem e rótulos do conjunto de dados de treinamento. Números azuis são locais de imagem (do canto superior esquerdo), números vermelhos são classes (veja abaixo).

Para simplificar, usaremos Pandas para carregar os arquivos de texto e ajustar os URLs de caminho ao padrão Unix, por exemplo, para a parte de treinamento:

Carregamento de dados em Pandas e scikit-learn.

Como o arquivo de texto não é grande, usar o Pandas nesse caso é um pouco mais fácil e fornece um pouco de flexibilidade. Para arquivos maiores, no entanto, a melhor opção é usar diretamente o objeto tf.data TextLineDataset .

Etapa 1 – Trabalhando com tf.data e o objeto Dataset

Agora que temos nossos dados, podemos carregá-los usando o tf.data para torná-lo pronto para o estimador! No caso mais simples, podemos apenas dividir o DataFrame dos Pandas para obter nossos lotes de dados:

Carregando dados em tf.data do DataFrame de um Pandas.

Anteriormente, um grande problema do uso de tf.data com Estimadores era que a depuração do conjunto de dados era bastante complexa, tendo que passar por objetos tf.Session . A partir das versões mais recentes, no entanto, é possível depurar os conjuntos de dados com a execução ansiosa ativada , mesmo ao trabalhar com estimadores. Como exemplo, podemos usar o Dataset para construir lotes de 8 elementos, pegar o primeiro lote e imprimir tudo na tela:

Depuração de objetos do conjunto de dados em execução ávida.

Agora é hora de carregar as imagens a partir dos caminhos! Observe que, em geral, isso não é trivial, porque as imagens podem ter diversas extensões, tamanhos, algumas em preto-e-branco e assim por diante. Felizmente para nós, podemos nos inspirar em um tutorial TF para criar uma função simples para encapsular toda essa lógica, aproveitando as ferramentas do módulo tf.image :

Analisando imagens com o módulo tf.image.

A função se encarrega da maioria dos problemas de análise:

  1. O parâmetro 'channels' permite carregar imagens coloridas e em preto e branco em uma única linha;
  2. redimensionamos todas as imagens para o formato desejado (40×40, de acordo com o papel original);
  3. na linha 8, também normalizamos nossos rótulos de referência para denotar um local relativo entre 0 e 1, em vez de um absoluto (já que redimensionamos todas as imagens e as imagens podem ter diferentes formas).

Podemos aplicar a função de análise a cada elemento de um conjunto de dados usando sua função de 'mapa' interno: colocando isso junto com alguma lógica adicional para treinamento / teste, obtemos nossa função de carregamento final:

Função de carregamento de dados completa a partir de um objeto DataFrame do Pandas. Uma única imagem carregada com êxito do conjunto de dados.

Passo 2 – Construindo uma rede convolucional com os estimadores personalizados

Como próximo passo, queremos replicar a rede neural convolucional (CNN) retirada do artigo original:

Fonte: Facial Landmark Detection por Deep Multi-task Learning .

A lógica da CNN é composta de duas partes: a primeira é um extrator de característica genérica para a imagem inteira (que é compartilhada em todas as tarefas), enquanto para cada tarefa temos um modelo separado e menor atuando na incorporação final de recursos a imagem. Por razões aparentes abaixo, nos referiremos a cada um desses modelos mais simples como uma "cabeça". Todas as cabeças são treinadas simultaneamente através de gradiente descendente.

Vamos começar a partir da parte de extração de recursos. Para isso, aproveitamos os objetos tf.layers para construir nossa rede principal:

Implementação da parte de extração de recurso com tf.layers.

No momento, vamos nos concentrar em uma única cabeça / tarefa, ou seja, a estimativa da posição do nariz na imagem. Uma maneira de fazer isso é usar estimadores personalizados , permitindo combinar nossa própria implementação de modelo com todas as funcionalidades de um objeto Estimador padrão.

Uma desvantagem dos estimadores personalizados é que seu código tende a ser bastante "detalhado", porque precisamos encapsular toda a lógica do estimador (treinamento, avaliação e previsão) em uma única função:

O código do nosso primeiro estimador customizado.

Grosso modo, a função de modelo recebe um parâmetro de modo , que podemos usar para distinguir que tipo de operação (por exemplo, treinamento), devemos fazer. Por sua vez, a função de modelo troca todas as informações com o objeto Estimador principal por meio de outro objeto personalizado, um EstimatorSpec :

Formulação esquemática de estimadores personalizados ( fonte ).

Isto não só torna o código mais difícil de ler, mas a maior parte do código acima tende a ser código “padronizado”, que depende apenas da tarefa específica que estamos enfrentando, por exemplo, usando o erro quadrático médio para um problema de regressão. O Head API é um recurso experimental projetado para simplificar a gravação do código nesse tipo de situação, e é o nosso próximo tópico.

Etapa 3a – Reescrevendo nosso estimador personalizado com a API Head

A ideia da API Head é que o principal componente de previsão (nossa função de modelo acima) pode ser gerado automaticamente quando alguns itens-chave são especificados: a parte de extração de recurso, a perda e nosso algoritmo de otimização:

Fonte: YouTube, The Practitioner's Guide com APIs de alto nível do TF (TensorFlow Dev Summit 2018) .

De certa forma, essa é uma idéia similar à interface de alto nível de Keras , mas ainda deixa bastante flexibilidade para definir uma série de cabeças mais interessantes, como veremos em breve.

Por enquanto, vamos reescrever o código anterior, desta vez usando uma “cabeça de regressão”:

O mesmo modelo de antes, usando uma cabeça de regressão.

Para todos os efeitos, os dois modelos são equivalentes, mas o último é mais legível e menos propenso a erros, já que a maior parte da lógica específica do estimador é agora encapsulada dentro da cabeça. Podemos treinar qualquer um dos dois modelos usando a interface 'train' do estimador e começar a receber nossas previsões:

Exemplo de previsão para o nosso modelo de tarefa única.

Por favor, não confunda a API Head (que está em tf.contrib) com tf.contrib.learn.head , que está obsoleto.

Passo 3b – Multi-tarefa de aprendizagem com o multihead

Finalmente chegamos à parte mais interessante deste tutorial: a lógica MTL. Lembre-se de que, no caso mais simples, fazer MTL é equivalente a ter 'multiple heads' no topo da mesma parte de extração de recurso, conforme mostrado esquematicamente aqui:

Fonte: Uma Visão Geral do Aprendizado Multitarefas em Redes Neurais Profundas (Sebastien Ruder).

Matematicamente, podemos otimizar todas as tarefas em conjunto, minimizando a soma das perdas específicas da tarefa. Por exemplo, suponha que temos perda L1 para a parte de regressão (erro quadrático médio sobre cada ponto de referência) e L2 para a parte de classificação (tags diferentes), podemos minimizar L = L1 + L2 por meio de gradiente de descida.

Após essa introdução (bastante demorada), você pode não se surpreender que a API Head tenha um cabeçalho específico para essa situação, chamado de multi-head . De acordo com a nossa descrição anterior, permite combinar linearmente múltiplas perdas provenientes de cabeças separadas. Neste ponto, deixarei o código falar por si:

Para simplificar, estou considerando apenas duas tarefas: a previsão da posição do nariz e da face 'pose' (perfil esquerdo, esquerdo, frontal, direito, perfil direito). Nós só precisamos definir duas cabeças separadas (uma de regressão e uma de classificação) e combiná-las com o objeto multi_head . Adicionar mais cabeças agora é apenas uma questão de algumas linhas de código!

Há uma pequena modificação na função de entrada que omitimos aqui por brevidade: você pode encontrá-la no notebook da Colab.

O estimador neste momento pode ser treinado com métodos padrão, e podemos obter ambas as previsões simultaneamente:

Previsões do nosso modelo multitarefa: posição do nó e pose (frontal neste caso).

Concluindo …

Espero que tenham gostado desta pequena introdução ao problema do MTL: se você estiver interessado, sugiro muito a este post bastante informativo de Sebastian Ruder para aprender algo mais sobre o campo. Mais em geral, falando sobre MTL foi a desculpa perfeita para introduzir alguns conceitos interessantes do framework TF, mais notavelmente as APIs Head. Não se esqueça de brincar com o notebook completo no Google Colab !

Este artigo apareceu originalmente em italiano no blog da Associação Italiana de Aprendizado de Máquina : https://iaml.it/blog/multitask-learning-tensorflow .

Texto original em inglês.