Existem muitos tipos diferentes de modelos usados no aprendizado de máquina. No entanto, uma classe de modelos de aprendizado de máquina que se destaca são as redes neurais artificiais (RNAs). Dado que as redes neurais artificiais são usadas em todos os tipos de aprendizado de máquina, este capítulo abrange o básico das RNAs.
As RNAs são sistemas de computação baseados em uma coleção de unidades ou nós conectados chamados neurônios artificiais, que modelam frouxamente os neurônios em um cérebro biológico. Cada conexão, como as sinapses em um cérebro biológico, pode transmitir um sinal de um neurônio artificial para outro. Um neurônio artificial que recebe um sinal pode processá-lo e depois sinalizar neurônios artificiais adicionais conectados a ele.
A aprendizagem profunda envolve o estudo de algoritmos complexos relacionados à RNA. A complexidade é atribuída a padrões elaborados de como a informação flui em todo o modelo. A aprendizagem profunda tem a capacidade de representar o mundo como uma hierarquia de conceitos aninhados, com cada conceito definido em relação a um conceito mais simples. As técnicas de aprendizado profundo são amplamente utilizadas na aprendizagem de reforço e nos aplicativos de linguagem natural que examinaremos nos capítulos 9 e 10.
Analisaremos a terminologia e os processos detalhados usados no campo das RNAs e cobrirão os seguintes tópicos:
Arquitetura de RNAs: neurônios e camadas;
Treinamento de uma RNA: Propagação forward, retropagação e descida de gradiente;
Hiperparâmetros de RNAs: Número de camadas e nós, função de ativação, função de perda, taxa de aprendizado, etc;
Definindo e treinando um modelo profundo de rede neural em Python;
Melhorando a velocidade de treinamento das RNAs e modelos de aprendizado profundo.
RNAs: Arquitetura, treinamento e hiperparâmetros
As RNAs contêm vários neurônios dispostos em camadas. Uma RNA passa por uma fase de treinamento comparando a saída modelada com a saída desejada, onde aprende a reconhecer padrões nos dados. Vamos seguir os componentes do RNAs.
Arquitetura
Uma arquitetura da RNA compreende neurônios, camadas e pesos.
Neurônios
Os blocos de construção para RNAs são neurônios (também conhecidos como neurônios artificiais, nós ou perceptrons artificiais). Os neurônios têm uma ou mais entradas e uma saída. É possível construir uma rede de neurônios para calcular proposições lógicas complexas. As funções de ativação nesses neurônios criam mapeamentos funcionais complicados e não lineares entre as entradas e a saída.
Como mostrado na Figura 1, um neurônio pega uma entrada (x1, x2… xn), aplica os parâmetros de aprendizado para gerar uma soma ponderada (z) e depois passa essa soma para uma função de ativação (f) que calcula a saída f(z).
Figura 1: Um neurônio artificial.
Camadas
A saída f(z) de um único neurônio (como mostrado na Figura 1) não será capaz de modelar tarefas complexas. Portanto, para lidar com estruturas mais complexas, temos camadas múltiplas de tais neurônios. Enquanto continuamos empilhando os neurônios horizontal e verticalmente, a classe de funções que podemos obter se torna um complexo crescente. A Figura 2 mostra uma arquitetura de uma RNA com uma camada de entrada, uma camada de saída e uma camada oculta.
Figura 2: Arquitetura de rede neural.
Camada de entrada
A camada de entrada recebe a entrada do conjunto de dados e é a parte exposta da rede. Uma rede neural é frequentemente desenhada com uma camada de entrada de um neurônio por valor de entrada (ou coluna) no conjunto de dados. Os neurônios na camada de entrada simplesmente passam o valor de entrada para a próxima camada.
Camadas ocultas
As camadas após a camada de entrada são chamadas de camadas ocultas porque não estão diretamente expostas à entrada. A estrutura de rede mais simples é ter um único neurônio na camada oculta que gera diretamente o valor.
Uma RNA multicamada é capaz de resolver tarefas relacionadas ao aprendizado de máquina mais complexas devido às suas camadas ocultas. Dados aumentos no poder de computação e nas bibliotecas eficientes, as redes neurais com muitas camadas podem ser construídas. As RNAs com muitas camadas ocultas (mais de três) são conhecidas como redes neurais profundas. Várias camadas ocultas permitem que redes neurais profundas aprendam recursos dos dados em uma chamada hierarquia de recursos, porque os recursos simples recombinam de uma camada para a próxima para formar recursos mais complexos. As RNAs com muitas camadas passam dados de entrada (recursos) através de mais operações matemáticas do que as RNAs com poucas camadas e, portanto, são mais computacionalmente mais intensivas no treinamento.
Camada de saída
A camada final é chamada de camada de saída; é responsável pela saída de um valor ou vetor de valores que correspondem ao formato necessário para resolver o problema.
Pesos dos neurônios
Um peso do neurônio representa a força da conexão entre unidades e mede a influência que a entrada terá na saída. Se o peso do neurônio 1 para o neurônio 2 tiver maior magnitude, significa que o neurônio 1 tem uma influência maior sobre o neurônio 2. Pesos próximos de zero significam que alterar esta entrada não alterará a saída. Pesos negativos significam que aumentar essa entrada diminuirá a saída.
Treinamento
Treinar uma rede neural significa basicamente calibrar todos os pesos na RNA. Essa otimização é realizada usando uma abordagem iterativa envolvendo etapas de proposta para a frente (forward) e retropropatação.
Propagação direta
A propagação direta é um processo de alimentar valores de entrada para a rede neural e obter uma saída, que chamamos de valor previsto. Quando alimentamos os valores de entrada para a primeira camada da rede neural, ela fica sem operações. A segunda camada pega valores da primeira camada e aplica operações de multiplicação, adição e ativação antes de passar esse valor para a próxima camada. O mesmo processo se repete para qualquer camada subsequente até que um valor de saída da última camada seja recebido.
Retropropagação
Após a propagação direta, obtemos um valor previsto da RNA. Suponha que a saída desejada de uma rede seja Y e o valor previsto da rede da propagação direta é Y'. A diferença entre a saída prevista e a saída desejada (Y-Y') é convertida na função de perda (ou custo) J(w), onde w representa os pesos na RNA. O objetivo é otimizar a função de perda, ou seja, tornar a perda a menor possível sobre o conjunto de treinamento.
O método de otimização utilizado é a descida de gradiente. O objetivo do método de descida de gradiente é encontrar o gradiente de J(w) em relação a w no ponto atual e dar um pequeno passo na direção do gradiente negativo até que o valor mínimo seja atingido, como mostra a Figura 3.
Figura 3: Gradiente descendente.
Em uma RNA, a função J(w) é essencialmente uma composição de várias camadas, conforme explicado no texto anterior. Portanto, se a camada 1 for representada como função p(), camada 2 como q() e camada 3 como r(), então a função geral é J(w) = r(q(p())). w consiste em todos os pesos nas três camadas. Queremos encontrar o gradiente de J(w) em relação a cada componente de w.
Ignorando os detalhes matemáticos, o texto acima implica essencialmente que o gradiente de um componente w na primeira camada dependeria dos gradientes na segunda e terceira camadas. Da mesma forma, os gradientes na segunda camada dependerão dos gradientes na terceira camada. Portanto, começamos a calcular os derivados na direção inversa, começando com a última camada, e usamos retropacagação para calcular gradientes da camada anterior.
No geral, no processo de retropropagação, o erro do modelo (diferença entre a saída prevista e desejada) é propagada de volta pela rede, uma camada de cada vez e os pesos são atualizados de acordo com o valor que eles contribuíram para o erro.
Quase todas as RNAs usam gradiente descendente e retropagação. A retropagação é uma das maneiras mais limpas e eficientes de encontrar o gradiente.
Hiperparâmetros
Os hiperparâmetros são as variáveis definidas antes do processo de treinamento e não podem ser aprendidas durante o treinamento. As RNAs têm um grande número de hiperparâmetros, o que os torna bastante flexíveis. No entanto, essa flexibilidade dificulta o processo de ajuste do modelo. Compreender os hiperparâmetros e a intuição por trás deles ajuda a dar uma ideia de quais valores são razoáveis para cada hiperparâmetro para que possamos restringir o espaço de pesquisa. Vamos começar com o número de camadas e nós ocultos.
Número de camadas e nós ocultos
Camadas ou nós mais ocultos por camada significa mais parâmetros na RNA, permitindo que o modelo se ajuste a funções mais complexas. Para ter uma rede treinada que generalize bem, precisamos escolher um número ideal de camadas ocultas, bem como os nós em cada camada oculta. Poucos nós e camadas levarão a altos erros para o sistema, pois os fatores preditivos podem ser muito complexos para um pequeno número de nós para capturar. Muitos nós e camadas serão super ajustados para os dados de treinamento e não generalizarão bem.
Não existe uma regra rígida para decidir o número de camadas e nós.
O número de camadas ocultas depende principalmente da complexidade da tarefa. Tarefas muito complexas, como classificação de imagens grandes ou reconhecimento de fala, geralmente exigem redes com dezenas de camadas e uma enorme quantidade de dados de treinamento. Para a maioria dos problemas, podemos começar com apenas uma ou duas camadas ocultas e, em seguida, aumentar gradualmente o número de camadas ocultas até começarmos a ajustar demais o conjunto de treinamento.
O número de nós ocultos deve ter uma relação com o número de nós de entrada e saída, a quantidade de dados de treinamento disponíveis e a complexidade da função que está sendo modelada. Como regra geral, o número de nós ocultos em cada camada deve estar em algum lugar entre o tamanho da camada de entrada e o tamanho da camada de saída, idealmente a média. O número de nós ocultos não deve exceder o dobro do número de nós de entrada para evitar o excesso de ajuste.
Taxa de aprendizado
Quando treinamos RNAs, usamos muitas iterações de propagação e retropropagação avançados para otimizar os pesos. Em cada iteração, calculamos a derivada da função de perda em relação a cada peso e subtraí-la desse peso. A taxa de aprendizado determina a rapidez ou lentidão com que queremos atualizar nossos valores de peso (parâmetro). Essa taxa de aprendizagem deve ser alta o suficiente para convergir em um período de tempo razoável. No entanto, deve ser baixo o suficiente para encontrar o valor mínimo da função de perda.
Funções de ativação
As funções de ativação (como mostrado na Figura 1) referem-se às funções usadas sobre a soma ponderada de entradas nas RNAs para obter a saída desejada. As funções de ativação permitem que a rede combine as entradas de maneiras mais complexas e elas fornecem uma capacidade mais rica no relacionamento que podem modelar e a saída que podem produzir. Eles decidem quais neurônios serão ativados, isto é, quais informações são passadas para outras camadas.
Sem funções de ativação, as RNAs perdem uma maior parte de seu poder de aprendizagem de representação. Existem várias funções de ativação. Os mais amplamente utilizados são os seguintes:
Função linear (identidade):
Representado pela equação de uma linha reta (isto é, f(x) = ax + b), onde a ativação é proporcional à entrada. Se tivermos muitas camadas e todas as camadas são de natureza linear, a função de ativação final da última camada é a mesma que a função linear da primeira camada. O intervalo de uma função linear é de –inf a +inf.
Função sigmoide:
Refere-se a uma função projetada como um gráfico em forma de S (como mostrado na Figura 4). É representado pela equação matemática f(x) = 1 /(1 + e^–x) e varia de 0 a 1. Uma entrada positiva grande resulta em uma grande saída positiva; uma grande entrada negativa resulta em uma grande saída negativa. Também é referido como função de ativação logística.
Função Tanh:
Semelhante à função de ativação sigmoide com uma equação matemática Tanh(x) = 2sigmoide(2x) - 1, onde o sigmoide representa a função sigmoide discutida acima. A saída desta função varia de –1 a 1, com uma massa igual em ambos os lados do eixo zero, como mostrado na Figura 4.
Função ReLU:
Relu significa Rectified Linear Unit (unidade linear retificada) e é representada como f(x) = max (x, 0). Portanto, se a entrada for um número positivo, a função retorna o número em si e se a entrada for um número negativo, a função retornará zero. É a função mais usada devido à sua simplicidade.
A Figura 4 mostra um resumo das funções de ativação discutidas nesta seção.
Figura 4: Funções de ativação.
Não existe uma regra rígida para a seleção de funções de ativação. A decisão depende completamente das propriedades do problema e dos relacionamentos que estão sendo modelados. Podemos tentar diferentes funções de ativação e selecionar a que ajuda a fornecer convergência mais rápida e um processo de treinamento mais eficiente. A escolha da função de ativação na camada de saída é fortemente restringida pelo tipo de problema modelado.
Funções de custo
As funções de custo (também conhecidas como funções de perda) são uma medida do desempenho da RNA, medindo o quão bem a RNA se encaixa em dados empíricos. As duas funções de custo mais comuns são:
Erro quadrático médio (MSE):
Essa é a função de custo usada principalmente para problemas de regressão, onde a saída é um valor contínuo. O MSE é medido como a média da diferença quadrada entre previsões e observação real. MSE é descrito mais adiante no Capítulo 4.
Entropia cruzada (ou perda logarítmica):
Essa função de custo é usada principalmente para problemas de classificação, onde a saída é um valor de probabilidade entre zero e um. A perda de entropia cruzada aumenta à medida que a probabilidade prevista diverge do rótulo real. Um modelo perfeito teria uma entropia cruzada de zero.
Otimizadores
Os otimizadores atualizam os parâmetros de peso para minimizar a função de perda. A função de custo atua como um guia para o terreno, informando ao otimizador se estiver se movendo na direção certa para atingir o mínimo global. Alguns dos otimizadores comuns são os seguintes:
Momentum:
O otimizador momentum analisa os gradientes anteriores, além da etapa atual. Serão necessárias passos maiores se as atualizações anteriores e a atualização atual moverem os pesos na mesma direção (ganhando impulso). Serão necessárias passos menores se a direção do gradiente for oposta. Uma maneira inteligente de visualizar isso é pensar em uma bola rolando por um vale, ela ganhará impulso quando se aproxima do fundo do vale.
AdaGrad (Adaptive Gradient Algorithm):
O AdaGrad adapta a taxa de aprendizado aos parâmetros, realizando atualizações menores para parâmetros associados a recursos que ocorrem frequentemente e atualizações maiores para parâmetros associados a recursos pouco frequentes.
RMSProp:
RMSProp significa raiz média quadrática da propagação. No RMSProp, a taxa de aprendizado é ajustada automaticamente e escolhe uma taxa de aprendizado diferente para cada parâmetro.
Adam (Adaptive Moment Estimation):
Adam combina as melhores propriedades dos algoritmos AdaGrad e RMSProp para fornecer uma otimização e é um dos algoritmos de otimização de descida de gradiente mais populares.
Época
Uma rodada de atualização da rede para todo o conjunto de dados de treinamento é chamada de época. Uma rede pode ser treinada para dezenas, centenas ou milhares de épocas dependentes do tamanho dos dados e das restrições computacionais.
Tamanho do lote
O tamanho do lote é o número de exemplos de treinamento em um passe para frente/para trás. Um tamanho em lote de 32 significa que 32 amostras do conjunto de dados de treinamento serão usadas para estimar o gradiente de erro antes que os pesos do modelo sejam atualizados. Quanto maior o tamanho do lote, mais espaço de memória é necessário.
Criando um modelo de rede neural artificial em Python
No Capítulo 2, discutimos as etapas para o desenvolvimento de modelos de ponta a ponta no Python. Nesta seção, nos aprofundamos nas etapas envolvidas na construção de um modelo baseado em RNA em Python.
Nosso primeiro passo será olhar para Keras, o pacote Python criado especificamente para RNA e aprendizado profundo.
Instalando pacotes Keras e de aprendizado de máquina
Existem várias bibliotecas Python que permitem a construção de modelos de RNA e aprendizado profundo de maneira fácil e rápida, sem entrar nos detalhes dos algoritmos subjacentes. Keras é um dos pacotes mais amigáveis que permitem uma computação numérica eficiente relacionada às RNAs. Usando Keras, modelos complexos de aprendizado profundo podem ser definidos e implementados em algumas linhas de código. Usaremos principalmente pacotes de Keras para implementar modelos de aprendizado profundo em vários estudos de caso.
Keras é simplesmente um invólucro em torno de motores de computação numérica mais complexos, como Tensorflow e Theano (substituído por PyTensor). Para instalar Keras, o Tensorflow ou o Theano (PyTensor) precisa ser instalado primeiro.
Esta seção descreve as etapas para definir e compilar um modelo baseado em RNA em Keras, com foco nas etapas a seguir.
Importar os pacotes
Antes de começar a construir um modelo de RNA, você precisa importar dois módulos do pacote Keras: Sequencial e Dense:
import numpy as np
from Keras.layers import Dense
from Keras.models import Sequential
Carregar os dados
Este exemplo utiliza o módulo aleatório de NumPy para gerar rapidamente alguns dados e rótulos a serem usados pela RNA que construímos na próxima etapa. Especificamente, uma matriz com tamanho (1000,10) é construída pela primeira vez. Em seguida, criamos uma matriz de rótulos que consiste em 0s e 1s com tamanho (1000,1):
dados = np.random.random((1000, 10))
Y = np.random.randint(2, size=(1000, 1))
modelo = Sequential()
Construção do modelo - definindo a arquitetura da rede neural
Uma maneira rápida de começar é usar o modelo sequencial de Keras, que é uma pilha linear de camadas. Criamos um modelo sequencial e adicionamos camadas uma de cada vez até que a topologia de rede seja finalizada. A primeira coisa é garantir que a camada de entrada tenha o número certo de entradas. Podemos especificar isso ao criar a primeira camada. Em seguida, selecionamos uma camada densa ou totalmente conectada para indicar que estamos lidando com uma camada de entrada usando o argumento input_dim.
Adicionamos uma camada ao modelo com a função add() e o número de nós em cada camada é especificado. Finalmente, outra camada densa é adicionada como uma camada de saída.
A arquitetura para o modelo mostrada na Figura 5 é a seguinte:
O modelo espera a entrada de dados com 10 variáveis (input_dim = 10);
A primeira camada oculta possui 32 nós e usa a função de ativação do ReLU;
A segunda camada oculta possui 32 nós e usa a função de ativação do ReLU;
A camada de saída possui um nó e usa a função de ativação sigmoide.
Figura 5: Arquitetura de uma RNA.
O código Python para a rede na Figura 5 é mostrado abaixo:
modelo = Sequential()
modelo.add(Dense(32, input_dim=10, activation='relu'))
modelo.add(Dense(32, activation='relu'))
modelo.add(Dense(1, activation='sigmoid'))
Compilar o modelo
Com o modelo construído, ele pode ser compilado com a ajuda da função compile(). A compilação do modelo utiliza as bibliotecas numéricas eficientes nos pacotes Theano (PyTensor) ou TensorFlow. Ao compilar, é importante especificar as propriedades adicionais necessárias ao treinar a rede. Treinar uma rede significa encontrar o melhor conjunto de pesos para fazer previsões para o problema em questão. Portanto, devemos especificar a função de perda usada para avaliar um conjunto de pesos, o otimizador usado para pesquisar pesos diferentes para a rede e quaisquer métricas opcionais que gostaríamos de coletar e relatar durante o treinamento.
No exemplo a seguir, usamos a função de perda de entropia cruzada, que é definida em Keras como binary_crossentropy. Também usaremos o otimizador Adam, que é a opção padrão. Finalmente, por ser um problema de classificação, coletaremos e reportaremos a acurácia da classificação como métrica. Segue o código Python:
modelo.compile(
loss='binary_crossentreopy',
optimizer='adam',
metrics=['accuracy']
)
Treinar o modelo
Com nosso modelo definido e compilado, é hora de executá-lo nos dados. Podemos treinar ou ajustar nosso modelo em nossos dados carregados chamando a função fit() no modelo.
O processo de treinamento será executado para um número fixo de iterações (épocas) através do conjunto de dados, especificado usando o argumento nb_epoch. Também podemos definir o número de instâncias avaliadas antes que uma atualização de peso na rede seja executada. Isso é definido usando o argumento batch_size. Para esse problema, executaremos um pequeno número de épocas (10) e usaremos um tamanho de lote relativamente pequeno de 32. Novamente, eles podem ser escolhidos experimentalmente por tentativa e erro. O código Python segue:
modelo.fit(
dados,
Y,
nb_epoch=10,
batch_size=32
)
Avaliar o modelo
Treinamos nossa rede neural em todo o conjunto de dados e podemos avaliar o desempenho da rede no mesmo conjunto de dados. Isso nos dará uma ideia de quão bem modelamos o conjunto de dados (por exemplo, precisão do treinamento), mas não forneceremos informações sobre o desempenho do algoritmo em novos dados. Para isso, separamos os dados nos conjuntos de dados de treinamento e teste. O modelo é avaliado no conjunto de dados de treinamento usando a função evaluation(). Isso gerará uma previsão para cada par de entrada e saída e coletará pontuações, incluindo a perda média e quaisquer métricas configuradas, como acurácia. O código Python segue:
metricas = modelo.evaluate(X_teste, Y_teste)
print(f'{modelo.metrics_names[1]}: {metricas[1] * 100}')
Executando um modelo de RNA mais rápido: GPU e serviços em nuvem
Para o treinamento de RNAs (especialmente redes neurais profundas com muitas camadas), é necessária uma grande quantidade de poder de computação. As CPUs disponíveis são responsáveis pelo processamento e execução de instruções em uma máquina local. Como as CPUs são limitadas no número de núcleos e aceitam o trabalho sequencialmente, elas não podem fazer cálculos de matriz rápida para o grande número de matrizes necessárias para o treinamento de modelos de aprendizado profundo. Portanto, o treinamento de modelos de aprendizado profundo pode ser extremamente lento nas CPUs.
As alternativas a seguir são úteis para a execução de RNAs que geralmente exigem uma quantidade significativa de tempo para executar em uma CPU:
Executar notebooks localmente em uma GPU;
Executar notebooks em Kaggle Kernels ou Google Colaboratory;
Usar serviços da Amazon Web.
GPU
Uma GPU é composta por centenas de núcleos que podem lidar com milhares de threads simultaneamente. A execução de RNAs e modelos de aprendizado profundo podem ser acelerados pelo uso de GPUs.
As GPUs são particularmente hábeis no processamento de operações de matriz complexa. Os núcleos da GPU são altamente especializados e aceleram massivamente processos como treinamento de aprendizado profundo, descarregando o processamento de CPUs aos núcleos no subsistema GPU.
Todos os pacotes Python relacionados ao aprendizado de máquina, incluindo Tensorflow, Theano (PyTensor) e Keras, podem ser configurados para o uso de GPUs.
Serviços em nuvem como Kaggle e Google Colab
Se você possui um computador habilitado para GPU, poderá executar RNAs localmente. Caso contrário, recomendo que você use um serviço como Kaggle Kernels, Google Colab ou AWS:
Kaggle:
Um site popular de ciência de dados de propriedade do Google que hospeda o Jupyter Service e também é chamado de Kaggle Kernels. Os Kaggle Kernels são livres para usar e vêm com os pacotes mais usados com frequência pré-instalados. Você pode conectar um kernel a qualquer conjunto de dados hospedado no Kaggle ou, alternativamente, você pode fazer o upload de um novo conjunto de dados em tempo real.
Google Colaboratory:
Um ambiente de notebook Jupyter gratuito fornecido pelo Google, onde você pode usar GPUs gratuitas. Os recursos do Google Colaboratory são os mesmos que Kaggle.
Serviços Web Amazon (AWS):
O AWS Deep Learning fornece uma infraestrutura para acelerar o aprendizado profundo na nuvem, em qualquer escala. Você pode lançar rapidamente instâncias do AWS Server pré-instaladas com estruturas e interfaces de aprendizado profundo populares para treinar modelos sofisticados de IA personalizados, experimentar novos algoritmos ou aprender novas habilidades e técnicas. Esses servidores da web podem ser executados mais que os Kaggle Kernels. Portanto, para grandes projetos, pode valer a pena usar um AWS em vez de um kernel.
Resumo do capítulo
As RNAs compreendem uma família de algoritmos usados em todos os tipos de aprendizado de máquina. Esses modelos são inspirados nas redes neurais biológicas que contêm neurônios e camadas de neurônios que constituem cérebros de animais. As RNAs com muitas camadas são chamadas de redes neurais profundas. Várias etapas, incluindo propagação direta e retropropagação, são necessárias para o treinamento dessas RNAs. Pacotes Python, como o Keras, tornam possível o treinamento dessas RNAs em algumas linhas de código. O treinamento dessas redes neurais profundas exige mais poder computacional, e as CPUs por si só podem não ser suficientes. As alternativas incluem o uso de um serviço de GPU ou nuvem, como Kaggle Kernels, Google Colaboratory ou Amazon Web Services, para o treinamento de redes neurais profundas.
Próximos passos
Como próximo passo, entraremos nos detalhes dos conceitos de aprendizado de máquina para aprendizado supervisionado, seguidos por estudos de caso usando os conceitos abordados neste capítulo.