Implementação do Algoritmo ID3 em Linguagem C: Conceitos e Exemplos

Introdução ao Algoritmo ID3

O algoritmo ID3 (Iterative Dichotomiser 3) é um método supervisionado para construção de árvores de decisão, amplamente utilizado em aprendizado de máquina. Sua implementação em C envolve cálculos de entropia e ganho de informação para selecionar os melhores atributos de divisão, seguido pela criação recursiva de nós de decisão. Este artigo explora a estrutura do algoritmo, oferece um exemplo simplificado em código C e discute aspectos como complexidade, vantagens e aplicações práticas.

Definição de Estruturas de Dados

Para armazenar os dados de treinamento e a árvore de decisão, definimos estruturas em C. Considere um cenário com rótulos de classe discretos e características representadas como inteiros.

typedef enum { CLASSE_X, CLASSE_Y, ... } TipoRotulo; // Definição conforme o problema

// Estrutura para uma amostra de treinamento
typedef struct {
    int valores_caracteristicas[MAX_CARACTERISTICAS]; // Array de valores das características
    TipoRotulo rotulo;                                // Rótulo da classe
} AmostraTreinamento;

// Estrutura para um nó da árvore de decisão
typedef struct No {
    int indice_caracteristica;      // Índice da característica usada para divisão
    float limiar_divisao;           // Valor de limiar para divisão (para características contínuas)
    struct No* filho_esquerdo;      // Ponteiro para o nó filho esquerdo
    struct No* filho_direito;       // Ponteiro para o nó filho direito
    TipoRotulo rotulo_padrao;       // Rótulo previsto se for um nó folha
} NoArvoreDecisao;

Cálculo de Entropia e Ganho de Informação

A entropia mede a impureza de um conjunto de dados, enquanto o ganho de informação quantifica a redução de entropia após a divisão com base em uma característica. Implementamos funções para calcular esses valores.

// Calcula a entropia de um conjunto de dados
float calcular_entropia(ConjuntoDados conjunto) {
    float entropia = 0.0;
    // Contar a frequência de cada classe e aplicar a fórmula da entropia
    // ... (implementação específica)
    return entropia;
}

// Calcula o ganho de informação para uma característica específica
float calcular_ganho_informacao(ConjuntoDados conjunto, int indice_caracteristica) {
    float entropia_total = calcular_entropia(conjunto);
    
    // Dividir o conjunto com base na característica e calcular a entropia condicional ponderada
    float entropia_condicional_ponderada = 0.0;
    // ... (cálculo omitido por brevidade)
    
    // Ganho de informação = entropia total - entropia condicional
    return entropia_total - entropia_condicional_ponderada;
}

Seleção da Melhor Característica

O algoritmo avalia todas as características disponíveis e escolhe aquela que maximiza o ganho de informação, que será usada para dividir o nó raiz.

int encontrar_melhor_caracteristica(ConjuntoDados conjunto) {
    float max_ganho = 0.0;
    int melhor_indice = -1;

    for (int i = 0; i < conjunto.num_caracteristicas; ++i) {
        float ganho = calcular_ganho_informacao(conjunto, i);
        if (ganho > max_ganho) {
            max_ganho = ganho;
            melhor_indice = i;
        }
    }

    return melhor_indice;
}

Construção Recursiva da Árvore de Decisão

A construção da árvore ocorrre de forma recursiva, dividindo o conjunto de dados com base na melhor característica em cada etapa até atingir condições de parada, como pureza total ou falta de características.

NoArvoreDecisao* construir_arvore(ConjuntoDados conjunto) {
    if (/* Condições de parada: conjunto vazio ou todos os rótulos iguais */) {
        // Criar nó folha
        NoArvoreDecisao* no_folha = (NoArvoreDecisao*)malloc(sizeof(NoArvoreDecisao));
        no_folha->rotulo_padrao = /* Determinar o rótulo predominante */;
        no_folha->filho_esquerdo = NULL;
        no_folha->filho_direito = NULL;
        return no_folha;
    }

    // Encontrar a melhor característica para divisão
    int melhor_indice = encontrar_melhor_caracteristica(conjunto);

    // Criar um novo nó de decisão
    NoArvoreDecisao* no = (NoArvoreDecisao*)malloc(sizeof(NoArvoreDecisao));
    no->indice_caracteristica = melhor_indice;

    // Dividir o conjunto de dados (ex.: para características discretas, por valores únicos)
    ConjuntoDados conjunto_esquerdo, conjunto_direito;
    // ... (lógica de divisão específica)

    // Construir subárvores recursivamente
    no->filho_esquerdo = construir_arvore(conjunto_esquerdo);
    no->filho_direito = construir_arvore(conjunto_direito);

    return no;
}

Análise de Complexidade

Complexidade Temporal

O tempo de execução depende de dois fatores principais: o cálculo do ganho de informação para cada característica e a construção recursiva da árvore. Para um conjunto com m amostras e n características, o cálculo do ganho requer O(m × n) operações. A divisão recursiva do conjunto pode levar, no pior caso, a uma complexidade de O(m × n × log m), assumindo divisões balanceadas.

Complexidade Espacial

O armazenamento do conjunto de dados consome O(m × n) espaço. A árvore de decisão, em um cenário ideal, pode ter até O(m) nós, resultando em uma complexidade espacial proporcional ao tamanho do conjunto.

Vantagens e Desvantagens

Vantagens

  • Clareza conceitual: Baseia-se em teoria da informação, tornando-o intuitivo.
  • Simplicidade de implementação: Adequado para fins educacionais e aplicações iniciais.
  • Eficiência em conjuntos pequenos: Oferece construções rápidas para dados com atributos informativos.
  • Interpretabilidade: A árvore resultante é fácil de visualizar e explicar.

Desvantagens

  • Limitações com dados contínuos: Requer discretização prévia para características numéricas.
  • Viés para atributos com muitos valores: Pode preferir características com mais categorias, levando a overfitting.
  • Incapacidade de tratar valores faltantes: Não oferece mecanismos nativos para dados incompletos.
  • Escalabilidade limitada: O alto custo computacional o torna impróprio para grandes conjuntos de dados.
  • Propensão a overfitting: Estratégia gulosa pode gerar árvores excessivamente complexas.

Aplicações Práticas

O algoritmo ID3 é empregado em diversos campos, incluindo:

  • Avaliação de crédito: Modelos de decisão para aprovação de empréstimos com base em histórico financeiro.
  • Marketing direcionado: Segmentação de clientes por comportamento de compra e características demográficas.
  • Diagnóstico médico: Sistemas de apoio à decisão baseados em sintomas e indicadores clínicos.
  • Recomendação de produtos: Personalização de sugestões em plataformas de e-commerce.
  • Previsão na agricultura: Aálise de condições ambientais para prever pragas em cultivos.

Tags: ID3 entropia ganho de informação

Publicado em 6-17 23:20