quarta-feira, 25 de novembro de 2015

Implementação Entropia

    Antes de iniciarmos o estudo do Ganho de Informação, vamos ver a implementação de um método mais genérico que calcula a entropia de um conjunto de valores, vamos ao código:


public class Entropy {
    /**
     * Metodo que dado um conjunto de valores calcula e entropia deste
     * conjunto.
     * 
     * @param values  - Lista de valores que se quer calcular a entropia
     * @return    - A entropia do conjunto
     */
     public static Double calculateEntropy(List values) {
         Map map = new HashMap();
  
         // Somatório para calcular a ocorrencia de cada valor
         for (String sequence : values) {
             // Preenche o mapa com a key sendo o valor e o value a quantidade
             if (!map.containsKey(sequence)) {
                 map.put(sequence, 0);
             }
             // Adiciona um a quantidade
             map.put(sequence, map.get(sequence) + 1);
         }

         // Calcula a entropia
         Double result = 0.0;
  
         // Itera pelo conjunto de possíveis valores
         for (String sequence : map.keySet()) {
             // Calcula a frequencia que o registro aparece, a probabilidade do valor
             // aparecer na base de dados informada
             Double frequency = (double) map.get(sequence) / values.size();
             // Faz o calculo da entropia
             result -= frequency * (Math.log(frequency) / Math.log(2));
         }

         // Retorna o valor
         return result;
     }
}


    Podemos ver através do código acima que a implementação do calculo de entropia é bastante simples quando se entende o funcionamento da equação e consegue ver além das notações matemáticas.

sábado, 21 de novembro de 2015

Entropia Passo a Passo

    Continuando o último post sobre as equações utilizadas no algoritmo ID3, podemos agora que conhecemos o que cada uma das equações se propõe a fazer, começar a conhecê-las melhor, vamos iniciar pela entropia, pois ela é a base do cálculo do ganho de informação e, portanto, essencial para que possamos avançar. O primeiro passo é conhecermos a fórmula da entropia:



    Sempre quando temos uma equação que não conhecemos o primeiro passo é descobrir o que cada um dos símbolos significa:
  • = Representa o conjunto de dados que queremos calcular a entropia.
  • = É a quantidade de possíveis valores, que os registros presentes no conjunto a ser calculado podem assumir, no atributo que classifica a base de dados.
  • = É a probabilidade, de cada um dos possíveis valores que o atributo que classifica a base de dados, aparecer no conjunto em que será calculada a entropia.
  • = Esse símbolo representa um somatório, que é uma operação matemática que soma os valores informados a ela n vezes. 
  • = É a variável de inicialização, imagine que essa variável funcione como a primeira parte de um for.
  •  = Esse símbolo representa um logaritmo na base dois.
    Após entendermos o que cada componente da equação significa, podemos tentar aplicar o conceito à um exemplo prático:
  1. Considere o seguinte conjunto de dados:


  2.     Devemos primeiro identificar qual o atributo que classifica a minha base de dados, no caso do exemplo acima é o atributo "Aproveitar". A segunda etapa é verificar quais valores o atributo classificatório da base de dados pode assumir, para esse exemplo "Sim" ou "Não".
        Apenas com esses dois passos já conseguimos saber o valor de uma das variáveis da equação , o valor atribuído será 2, pois esse é o número de valores que o atributo "Aproveitar" pode assumir.
  3. Temos agora a equação:
     

       Note que a variável que limita a somatória foi substituída. Próximo passo é atribuirmos valores de  para os possíveis valores do atributo "Aproveitar", como no exemplos abaixo:
    • = Portanto temos que quando o valor de  for igual a um  será igual a probabilidade de um registro com o atributo "Aproveitar" conter o valor "Sim".
    • = Nesse caso temos que quando o valor de  for igual a dois será igual a probabilidade de um registro com o atributo "Aproveitar" conter o valor "Não".
    • Esse processo se repetiria, caso o atributo tivesse um número maior de valores possíveis, até que o valor de chegasse ao número de valores.
  4. Agora que sabemos o significado de podemos expandir o somatório da seguinte forma:



  5. Podemos agora calcular as probabilidade de da seguinte forma:
    • Sabemos pela tabela apresentada anteriormente que a quantidade de total de registro é de 4.
    • Podemos observar que a quantidade de registros que a classificação é "Sim" são 3.
    • Portanto a quantidade de registros com a classificação "Não" é de 1.
    Com essas informações termos o seguinte cálculo:


  6. Após esse cálculo podemos novamente substituir alguns termos na equação da entropia, nossa equação ficará da seguinte forma:


  7. Próximo passo é resolvermos o logaritmo:


  8. E agora que temos apenas operações básicas podemos calcular o resultado final:

   
    Basta seguir o exemplo acima e você irá conseguir calcular a entropia de qualquer conjunto de dados ou de qualquer atributo.
    No próximo post iremos ver como é possível calcular o ganho de informação de um conjunto, iremos conhecer a fórmula e fazer também um passo a passo detalhado para que todos possam entender e conseguir aplicar a seus problemas ou até mesmo implementar uma solução.

sexta-feira, 6 de novembro de 2015

Entropia e Ganho de Informação

    Hoje iremos falar sobre dois assuntos muito importantes dentro do Aprendizado de Máquina (Machine Learning), Entropia e Ganho de Informação. Antes de começarmos a ver como eles funcionam, precisamos primeiramente entender qual a funcionalidade de cada uma deles:


Entropia: A entropia da informação, no caso do aprendizado de máquina, visto que ela pode e é usada em outras áreas, mede a impureza de um determinado conjunto de dados. Em outras palavras mede a dificuldade que se tem para saber qual a classificação de cada registro dentro do meu conjunto de dados. O valor da entropia da informação varia de 0 até 1.


    Este seria o gráfico da entropia para uma base de dados em que a classificação é binária, ou seja possui apenas dois valores possíveis. O valor da coluna da esquerda, eixo Y, é o resultado da entropia do conjunto de dados, e o valor da linha de baixo, eixo X, é a probabilidade de uma das classes ocorrer no conjunto de dados.
    Vale a pena notar que apesar do exemplo ter apenas dois possíveis valores para a classificação do conjunto de dados, a entropia pode ser calculada para qualquer quantidade de valores.

Ganho de Informação: O ganho de informação ao contrário da entropia mede a pureza de um determinado conjunto de dados, essa definição nada mais é do que a eficácia do atributo testado ao tentar classificar a base de dados.
    Uma maneira de entender o ganho de informação é imaginando um atributo chamado Temperatura, e vamos imaginar que esse atributo pode assumir três valores "Alta", "Normal" e "Baixa". O que o cálculo do ganho da informação faz é nos dizer o quanto esse atributo sozinho consegue ajudar a classificar a minha base de dados.
   

    Nos próximos post iremos ver em detalhes cada uma dessas duas equações e iremos fazer um passo a passo do cálculo.

quinta-feira, 29 de outubro de 2015

Padrões de Comentários

Bom pessoal, hoje vou falar um pouco sobre as boas práticas na hora de comentar e documentar um código no ambiente corporativo. Primeiramente gostaria de deixar bem claro o fato de que o mundo acadêmico e o mundo corporativo têm expectativas e realidades muito diferentes, portanto os padrões que foram aprendidos dentro de uma universidade, ou através de livros, nem sempre são bem vistos no mundo dos negócios.

Por exemplo, os códigos fontes que são postados aqui nesse blog possuem uma quantidade excessiva e desnecessária de comentários, do ponto de vista empresarial, e o porquê disso é o que iremos abordar abaixo. Porém, esse excesso aqui cometido é proposital para que possamos ser o mais didático possível e para que qualquer um possa entender o funcionamento dos nossos códigos, aí você me pergunta, “Mas esse não é o objetivo dos comentários em qualquer situação? Inclusive no ambiente corporativo. ”, para responder essa pergunta vamos levar em conta os seguintes pontos:


1) Comentários não são legendas

// Passa por todas as bananas no cacho
foreach(banana b no cacho) {
    monkey.eat(b);  //Faz o macaco comer a banana
}

Você não precisa explicitar o que faz cada linha do seu código, após um período não tão grande de tempo, qualquer programador poderá ler linhas de códigos quase tão bem quanto entende a sua linguagem nativa. A exceção para esses casos são linguagens de programação muito próximas a linguagem de máquina, como por exemplo Assembly.

2) Comentários não são projetos de arte 


Esta é uma péssima prática de programação muito disseminada por exemplos em livros e copyright de códigos open source, pois são formas de chamar a atenção do leitor. Como por exemplo:

/*
   _     _      _     _      _     _      _     _      _     _      _     _
  (c).-.(c)    (c).-.(c)    (c).-.(c)    (c).-.(c)    (c).-.(c)    (c).-.(c)
   / ._. \      / ._. \      / ._. \      / ._. \      / ._. \      / ._. \
 __\( Y )/__  __\( Y )/__  __\( Y )/__  __\( Y )/__  __\( Y )/__  __\( Y )/__
(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)
   || M ||      || O ||      || N ||      || K ||      || E ||      || Y ||
 _.' `-' '._  _.' `-' '._  _.' `-' '._  _.' `-' '._  _.' `-' '._  _.' `-' '._
(.-./`-'\.-.)(.-./`-'\.-.)(.-./`-'\.-.)(.-./`-'\.-.)(.-./`-'\.-.)(.-./`-'\.-.)
 `-'     `-'  `-'     `-'  `-'     `-'  `-'     `-'  `-'     `-'  `-'     `-'
 
                 -It's Monkey Business Time! (Version 1.5)
*/

Você pode achar que nunca faria algo parecido, mas repare que o código abaixo não é muito diferente, apenas mais trabalhados.

+------------------------------------------------------------+
 | Module Name: classMonkey                                   |
 | Module Purpose: emulate a monkey                           |
 | Inputs: Bananas                                              |
 | Outputs: Grunts                                            |
 | Throws: Poop                                               |
 +------------------------------------------------------------+


Programadores adoram enfeitar os seus comentários, como no exemplo acima, quando estão cansados e precisam parar de pensar no problema que estão tentando resolver. O problema com essa prática é que ela gera uma perda de horas para a manutenção desses comentários para qualquer um que vá trabalhar com eles. Mesmo programadores que não gostam disso, perdem tempo na manutenção dos layouts, pois se incomodam com a inconsistência ou a quebra do layout. Você pode até achar que isso não é verdade, mas aposto que a quebra de layout na coluna da direta está te incomodando.

 3) Cabeçalhos: Incomodo ou Ameaça?


Cabeçalhos tanto de classes como de métodos são ruins pois eles abrem uma brecha para que se dê nomes ruins aos métodos, pois são uma desculpa para não colocar nomes que representem as funcionalidades. Além disso, esses códigos nunca são atualizados o que pode levar ao leitor a uma ideia errada do real funcionamento do código, caso alguém leia esse comentário, o que raramente ocorre.

Óbvio que existem exceções para esse caso, uma delas é o Java que disponibiliza o JavaDoc que possibilita a criação de cabeçalhos, especialmente formatados, que disponibilizam essa documentação no momento que o desenvolvedor usa o auto complete. Além de ser possível gerar uma documentação completa do sistema através de ferramentas que extraem esses comentários.

 4) Comentários não são controles de código

// method name: pityTheFoo (Incluido para aumentar a redundancia)
// created: Feb 18, 2009 11:33PM
// Author: Bob
// Revisions: Sue (2/19/2009) - Lengthened monkey's arms
//            Bob (2/20/2009) - Solved drooling issue
 
void pityTheFoo() {
     ...
}

Hoje não existem empresas que desenvolvam um sistema sem o uso de softwares de controle de versão e esse software é perfeitamente capaz de armazenar e atualizar as informações que são colocadas em comentários como o mostrado acima, isso sem que seja preciso sujar o código e obrigar os outros desenvolvedores a gastar seu tempo para atualiza-los. Obviamente, os desenvolvedores que gostam desse tipo de comentário geralmente são aqueles que não colocam nenhuma informação, ou informações imprecisas, nos commits de código.

5) Comentários são sinais de códigos ruins


A quantidade excessiva de comentários são geralmente um indicativo que o código precisa passar por um processo de refactor. Sempre que você achar que seu código precisa de comentários para ser entendido, você deve se fazer a seguinte pergunta "Como posso refatorar meu código para que seu objetivo fique claro?".

Conclusão:

Com esses 5 pontos podemos responder a pergunta, “Mas esse não é o objetivo dos comentários em qualquer situação? Inclusive no ambiente corporativo. ”, códigos quando utilizados corretamente podem e são muito uteis, mas devemos ter cuidado com excessos ou comentários desnecessários pois isso pode gerar um gasto de tempo de outros programadores para manter atualizados e com o formato correto, os nossos comentários. Segue abaixo algumas dicas de como podemos evitar o uso de comentários desnecessários:

1) Sempre prefira nomes significativos a comentários

// Before
// Calculate monkey's arm length
// using its height and the magic monkey arm ratio
double length = h * 1.845; //magic numbers are EVIL!
 
// After - No comment required
double armLength = height * MONKEY_ARM_HEIGHT_RATIO;

2) Utilize entradas e saídas tipadas para os seus métodos

// Before
// input parameter monkeysToFeed:
// DataSet with one table containing two columns
//     MonkeyID (int) the monkey to feed
//     MonkeyDiet (string) that monkey's diet
    void feedMonkeys(DataSet monkeysToFeed) {
}
 
//  After: No comment required
    void feedMonkeys(Monkeys monkeysToFeed) {
}

3) Extraia o trecho que necessita de comentários para outro método

// Before
// Begin: flip the "hairy" bit on monkeys
foreach(monkey m in theseMonkeys) {
    // 5-6 steps to flip bit.
}
// End: flip the "hairy" bit on monkeys
 
// After No comment required
flipHairyBit(theseMonkeys);

4) Evite um encadeamento grande de estruturas de repetição ou decisão

		} // ... if see evil
      } // ... while monkey do.
    } // ... if monkey see.
  } // ... class monkey
} // ... namespace primate

Espero com esse artigo ter ajuda programadores novos e veteranos a produzirem códigos melhores e principalmente a saber diferenciar o estilo de programação que usamos ao criar tutoriais, entregar trabalhos na faculdade, ou fazermos códigos para nós mesmos, do estilo que deve ser usado no ambiente corporativo.

Fonte:

terça-feira, 20 de outubro de 2015

ID3 Implementação

Implementação ID3 


Como prometido nesse post iremos analisar a implementação do algoritmo de árvore de decisão ID3, o algoritmo foi implementado em Java 7 e no final do post irei colocar o link para o repositório que contém os códigos fontes. 

A seguir iremos fazer uma análise das principais classes que compõe o algoritmo:

Start.java

Classe inicial do projeto, responsável por realizar as chamadas dos métodos de carregamento dos atributos, carregamento da base e geração da árvore, assim como impressão da árvore no arquivo de resultados.

public class Start {

 /**
  * Função main, ela que irá carregar os dados e iniciar o processamento da árvore.
  * 
  * @param args
  */
 public static void main(String[] args) {
  
  //Caminho para a pasta onde será lido o arquivo com a base de dados
  String path = "C:\\Users\\davidson.sestaro\\Dropbox\\IA\\";
  
  //Carrega os atributos da base de dados
  ListDiscreteAttributes attributes = FileReader.readAttributes(path + "PlayGolf.txt");
  
  //Carrega os registros da base de dados
  List records = FileReader.readDataset(path + "PlayGolf.txt", attributes);
  
  //Instância o primeiro ramo da nossa árvore
  Node root = new Node();
  root.setData(records);
  
  //Inicia o processamento da árvore
  ID3 id3 = new ID3();
  id3.generateTree(records, root, attributes);
  
  //Imprime a arvore resultante no arquivo Result.txt
  PrintWriter writer = null;
  
  try {
   writer = new PrintWriter(path + "Result.txt", "UTF-8");
  } catch (FileNotFoundException | UnsupportedEncodingException e) {
   e.printStackTrace();
  }
  
  FileWriter.writeTree(root, writer, 0);
  
  //Fecha o arquivo
  writer.close();
 }

}
Destacando os pontos importantes da classe que necessitam alteração para a execução, temos:

Aqui deverá estar a massa de dados que iremos usar e será o caminho em que será salvo o arquivo de retorno:
String path = "C:\\desenvolvimento\\IA\\";

Substituir o nome do arquivo com a massa de dados para o arquivo correto:
//Carrega os atributos da base de dados
ListDiscreteAttributes attributes = FileReader.readAttributes(path + "PlayGolf.txt");
//Carrega os registros da base de dados
List<record> records = FileReader.readDataset(path + "PlayGolf.txt", attributes);

Arquivo com a árvore resultante:
writer = new PrintWriter(path + "Result.txt", "UTF-8");

ID3.java

Classe core do código do algoritmo, nela são realizados toda a lógica de classificação de atributos e de divisão da massa de dados. Aqui também são gerados os ramos da árvore. Todo o processamento é realizado de forma recursiva.
public class ID3 {

 /**
  * Gera a arvore de decisao de forma recursiva.
  * 
  * @param records   - Dados a serem classificados pela arvore
  * @param root    - No da arvore do topo da arvore para essa iteracao
  * @param learningSet  - Atributos a serem utilizados pelo classificador
  * @return     - Arvore de decisao
  */
 public Node generateTree(List<Record> records, Node root, ListDiscreteAttributes learningSet) {
  
  //Inicializa as variaveis para selecionar o melhor atributp
  int bestAttribute = -1;
  double bestGain = 0.0;
  
  //Calcula a entropia para os registros a serem considerados
  root.setEntropy(Entropy.calculateEntropy(root.getData(), learningSet));
  
  //Condicao de para da arvore
  if(root.getEntropy() == 0) {
   return populateResult(root.getData(), root, learningSet);
  }
  
  //Avalia cada atributo ainda nao utilizado nesse galho da arvore
  for(int i = 0; i < learningSet.getAttributeQuantity() - 1; i++) {
   double entropy = 0;
   
   LinkedList<Double> entropies = new LinkedList<Double>();
   LinkedList<Integer> setSizes = new LinkedList<Integer>();

   //Faz um de para com a posicao do atributo no vetor de atributos com a posicao real dele na base de dados
   int attributePositionRecord = Utils.getAttributePositionOnRecords(learningSet.getAttributeInfo(i), root.getData().get(0));
   
   //Itera por cada possivel valor do atributo selecionado
   for(int j = 0; j < learningSet.getAttributeInfo(i).getListAttributes().getQuantity(); j++) {
    //Pega os registros com o valor a ser considerado
    ArrayList<Record> subset = Utils.subset(root, attributePositionRecord, j);
    //Pega o tamanho desse subset
    setSizes.add(subset.size());

    //Calcula a entropia para o subset
    if(subset.size() != 0) {
     entropy = Entropy.calculateEntropy(subset, learningSet);
     entropies.add(entropy);
    } else {
     entropies.add(0.0);
    }
   }
   
   //Calcula o ganho de informacao
   double gain = InformationGain.calculateGain(root.getEntropy(), entropies, setSizes, root.getData().size());
   
   //Se for melhor do que o melhor atributo atualiza os valores
   if(gain > bestGain) {
    bestAttribute = i;
    bestGain = gain;
   }
  }
  
  //Caso exista um atributo a ser considerado
  if(bestAttribute != -1) {
   //Preenche o no da arvore com os valores desse atributo
   AttributeInfo chosen = learningSet.getAttributeInfo(bestAttribute); 
   String testedAttributeName = root.getTestAttribute().getValue();
   
   root.setTestAttribute(chosen);
   root.setValue(testedAttributeName);
   root.children = new Node[chosen.getListAttributes().getQuantity()];
   root.setUsed(true);
   
   learningSet.removeAttribute(bestAttribute);
   
   int bestAttributePositionRecord = Utils.getAttributePositionOnRecords(chosen, records.get(0));
   
   //Preenche as folhas geradas a partir desse atributo
   for (int j = 0; j < chosen.getListAttributes().getQuantity(); j++) {
    root.children[j] = new Node();
    root.children[j].setParent(root);
    root.children[j].setData(Utils.subset(root, bestAttributePositionRecord, j));
    root.children[j].getTestAttribute().setValue(chosen.getListAttributes().getValue(j));
   }

   //Itera recursivamente pelos filhos
   for (int j = 0; j < chosen.getListAttributes().getQuantity(); j++) {
    generateTree(records, root.children[j], learningSet.clone());
   }
  }
  //Metodo de para do algoritmo
  else {
   return populateResult(root.getData(), root, learningSet);
  }
  
  return root;
 }

 /**
  * Popula as folhas durante as condicoes de para do algoritmo
  * 
  * @param records   - Registros filhos dessa folha
  * @param root    - No com o atributo que gerou a folha
  * @param learningSet  - Atributos ainda nao utilizados no ramo
  * @return
  */
 private Node populateResult(List<Record> records, Node root, ListDiscreteAttributes learningSet) {
  AttributeInfo chosen = learningSet.getAttributeInfo(learningSet.getAttributeQuantity() - 1);
  
  root.children = new Node[1];
  
  root.children[0] = new Node();
  root.children[0].setParent(root);
  
  int classAttributePositionRecord = Utils.getAttributePositionOnRecords(chosen, records.get(0));
  int resultPosition = Utils.getMajority(root.getData(), learningSet.getAttributeInfo(learningSet.getAttributeQuantity() - 1).getListAttributes(), classAttributePositionRecord);
  
  root.children[0].getTestAttribute().setValue(chosen.getListAttributes().getValue(resultPosition));   
  
  return root;
 }
}

Entropy.java

Classe que realiza o cálculo da entropia de um determinado conjunto de dados.


public class Entropy {
 /**
  * Metodo que dado um conjunto de registros e os atributos a serem calculados, calcula a entropia
  * do conjunto
  * 
  * @param data   - registros do qual sera calculado a entropia
  * @param learningSet - atributos
  * @return
  */
 public static double calculateEntropy(List<Record> data, ListDiscreteAttributes learningSet) {
  double entropy = 0;
  
  if(data.size() == 0) {
   return 0;
  }
  
  //Obtem a posicao em que se encontra a classe no conjunto de atributos
  int positionClass = learningSet.getAttributeQuantity() - 1;
  //Obtem a posicao em que se encontra a classe nos registros
  int positionClassRecord = data.get(0).getAttributes().size() - 1;
  
  //Itera pelas classes existentes
  for(int i = 0; i < learningSet.getAttributeInfo(positionClass).getListAttributes().getQuantity(); i++) {
   int count = 0;
   for(int j = 0; j < data.size(); j++) {
    Record record = data.get(j);
    
    if(record.getAttributes().get(positionClassRecord).getValue() == i) {
     count++;
    }
   }
    
   //Calcula a entropia
   double probability = count / (double)data.size();
   if(count > 0) {
    entropy += -probability * (Math.log(probability) / Math.log(2));
   }
  }
  
  return entropy;
 }
}

InformationGain.java

Classe que realiza o cálculo do ganho de informação de um determinado atributo em um determinado conjunto de dados.


public class InformationGain {
 /**
  * Calcula o ganho de informacao de determinado atributo
  * 
  * @param rootEntropy  - Entropia do conjunto como um todo
  * @param subEntropies  - Entropia dos possiveis subgrupos do atributo
  * @param setSizes   - Tamanho dos possiveis subgrupos do atributo
  * @param data    - Quantidade de registros total
  * @return
  */
 public static double calculateGain(double rootEntropy, LinkedList<Double> subEntropies, LinkedList<Integer> setSizes, int data) {
  double gain = rootEntropy; 
  
  for(int i = 0; i < subEntropies.size(); i++) {
   gain += -((setSizes.get(i) / (double)data) * subEntropies.get(i));
  }
  
  return gain;
 }
}


Essas são as principais classes que compõe o projeto para geração de árvores de decisão, o projeto se encontra no link:

https://github.com/dsestaro/Algorithms

Todos as implementações feitas aqui no blog irão ser commitadas nesse mesmo repositório.

quarta-feira, 14 de outubro de 2015

Impressão de Árvores em Java

Impressão de Árvores 

Após o início dos desenvolvimentos do algoritmo ID3, comecei a pesquisar algum método pronto para a impressão de árvores, tanto em arquivo quanto na saída padrão, em Java com a restrição de cada ramo poderia ter N filhos, para que o tempo de desenvolvimento diminuísse. Porém, todas as implementações que encontrei eram desenvolvidas visando árvores binárias e na sua grande maioria, formadas por códigos confusos e desnecessariamente complexos. 

Por isso decidi deixar aqui, em um post a parte, a minha versão para impressão de árvores em arquivo, independente do número de folhas ou do número de níveis que a árvore possuí. Segue abaixo o esquema no qual a árvore será impressa: 

Folha-Nível1 
        Folha1-Nível2 
        Folha2-Nível2 
                Folha1-Nível3
                Folha2-Nível3 
                        Folha1-Nível4 
                        Folha2-Nível4 
                Folha3-Nível3 
            Folha4-Nível3 
        Folha3-Nível2 
            Folha5-Nível3
        Folha4-Nível2  

(i) Notar que o nível em que a folha se encontra é determinado pela tabulação. 

Dessa forma ficou fácil analisar as saídas do algoritmo, sem ter que ficar utilizando a funcionalidade de debug do Eclipse. Segue abaixo o código implementado:

public class FileWriter {
 /**
  * Função recursiva para impressão da árvore resultante dos algoritmos de
  * árvore de decisão.
  * 
  * @param root  - Nó que será impresso
  * @param writer - Arquivo onde será impresso
  * @param treeLevel - Nível em que a árvore se encontra
  */
    public static void writeTree(Node root, PrintWriter writer, int treeLevel) {
        String line = "";
  
        //Verifica se existe apenas o resultado ou se existe um outro campo a ser avaliado
        if(root.getTestAttribute().getName().isEmpty()) {
            line = root.getTestAttribute().getValue();
        } else {
            line = root.getValue() + " -> " + root.getTestAttribute().getName();
        }
  
        //Imprime o nível do nó
        for(int i = 0; i < treeLevel * 2; i++){
            writer.print("\t");
        }
  
        //Aumenta em 1 o nível da árvore
        treeLevel++;
  
        writer.print(line + "\n");
  
        //Imprime de forma recursiva as folhas desse nó
        if(root.getChildren() != null) {
            for(Node child : root.getChildren()) {
                writeTree(child, writer, treeLevel);
            }
        }
  
        return;
    }
}

segunda-feira, 12 de outubro de 2015

Algumas definições - Classes Úteis

Algumas definições

Para o desenvolvimento dos algoritmos vou criar algumas classes genéricas, e irei acrescentar funcionalidades a elas conforme o necessário. Para o algoritmo ID3, foram criadas as seguintes classes:

public class Record {
    
    private ArrayList<attribute> attributes;

    publicArrayList<attribute> getAttributes() {
        return attributes;
    }
    
    public void setAttributes(ArrayList<attribute> attributes) {
        this.attributes = attributes;
    }

}
Classe criada para abrigar os exemplos do conjunto de treinamento e seus respectivos atributos.
public class Attribute {

    private String name;
    private double value;
    private boolean isUnknown;
 
    public Attribute(String name, double value) {
        this.name = name;
        this.value = value;
        isUnknown = false;
    }
 
    public Attribute(String name, String value) {
        this.name = name;
        try {
            this.value = Double.valueOf(value);
            this.isUnknown = false;
        }
        catch(NumberFormatException nfe) {
            this.value = -1;
            this.isUnknown = true;
        }
    }
 
    public void setName(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
 
    public void setValue(double value) {
        this.value = value;
    }

    public double getValue() {
        return value;
    }
 
    public void setUnknown(boolean isUnknown) {
        this.isUnknown = isUnknown;
    }

    public boolean isUnknown() {
        return isUnknown;
    }
}
Classe utilizada para guardar as informações referentes ao atributo, como por exemplo, nome, valor, etc. Como se trata de uma classe com propósitos genéricos o único tipo de dados aceito pela classe é o double, para aceitar os dados processados pelo ID3, que só aceita constantes, foi utilizado a classe descrita abaixo:
public class DiscreteAttribute {
    private Map<String, Integer> values;

    public DiscreteAttribute() {
        values = new HashMap<String, Integer>();
    }

    public void setValue(String key) {
        if(!values.containsKey(key)) {
            values.put(key, values.size());
        }
    }

    public int getValue(String key) {
        return values.get(key);
    }
}
Classe que funciona como um enum dinâmico e tem como propósito transformar os valores discretos do conjunto de dados de treinamento do ID3 em valores numéricos.

domingo, 11 de outubro de 2015

ID3

Árvores de Decisão


O primeiro tópico a ser abordado aqui no blog são as árvores de decisão, sendo os mais conhecidos o algoritmo ID3 e no seu sucessor C4.5. Iremos implementar primeiramente a versão mais básica (ID3) e depois de pronto o modificaremos para que se torne o seu sucessor.

O nosso algoritmo básico constrói as árvores de decisão de uma forma top-down (de cima para baixo), fazendo uma simples pergunta: "Qual atributo deve ser usado?". Para responder isso cada atributo é testado usando um método estatístico para determinar o ganho de informação que aquele atributo gera na hora de classificar a nossa base de treinamento. Após a seleção esse atributo vira o topo da árvore e cada valor que esse atributo pode assumir vira uma folha e a base de dados é separada de acordo, o processo todo é repetido para cada folha, usando apenas os exemplos associados a ela. Iremos usar a implementação greedy desse algoritmo, pois ele nunca volta atrás para reconsiderar as escolhas feitas anteriormente.

Para a implementação de uma árvore de decisão que aceite apenas valores booleanos como saída, teremos o seguinte pseudocódigo:

IDE3 (Exemplos, atribuito_classificador, Atributos)
  • Criar a folha inicial da árvore Root
  • Se todos os objetos são positivos em Exemplos, retornar uma única folha com a saída verdadeira.
  • Se todos os objetos são negativos em Exemplos, retornar uma única folha com a saída false.
  • Se Atributos estiver vazio retornar uma única folha com a saída mais comum na base.
  • Caso contrário faça:
    • A recebe o atributo de Atributos que melhor classifica Exemplos.
    • Root recebe A
    • Para cada possível valor v de A teremos:
    • Adicionar uma nova folha a A com o valor correspondente a v.
    • Sendo X o resultado de Exemplos com os objetos que contenham v como atributo.
      • Se o X gerado for vazio:
      • Adicionar uma folha a Root com o valor mais comum da base.
    • Caso contrário:
    • ID3 (X, atribuito_classificador, Atributos – A).
  • Término.
  • Retorne Root.

Para medir qual atributo melhor classifica a base de dados vamos utilizar o cálculo de ganho de informação, você pode conferir uma descrição completa da equação nesse link.

Como todos algoritmos o ID3 possuí algumas limitações e capacidades, vamos ver abaixo algumas delas:


  • O ID3 na sua forma inicial, e a que será implementada, não possui nenhuma forma de backtracking, portanto está sujeito a convergir para soluções ótimas locais. No caso do ID3, uma solução ótima local corresponde ao único caminho considerado pelo algoritmo.
  • ID3 usa todos os exemplos do conjunto de treinamento a cada iteração, não importando o nível em que a árvore se encontra. Essa característica faz com que o ID3 seja muito menos suscetível a erros no conjunto de treinamento.
  • O ID3 mantém apenas uma única solução, o que faz com que ele não seja capaz de otimizar os resultados obtidos.
Para encerrar esse post podemos ver abaixo um exemplo de resultado obtido com o ID3:





sexta-feira, 9 de outubro de 2015

Database (Golfe|Clima)

Database (Golfe|Clima)


A base de dados de clima é uma pequena base pública com apenas 14 exemplos, que contém informações sobre qual clima é favorável a jogar ou não jogar golfe.

Data Set

Perspectiva
Temperatura
Numérica
Temperatura
Nominal

Umidade
Numérica

Umidade
Nominal

Vento
Jogar
Nublado
83
Quente
86
Alta
Não
Sim
Nublado
64
Frio
65
Normal
Sim
Sim
Nublado
72
Suave
90
Alta
Sim
Sim
Nublado
81
Quente
75
Normal
Não
Sim
Chuvoso
70
Suave
96
Alta
Não
Sim
Chuvoso
68
Frio
80
Normal
Não
Sim
Chuvoso
65
Frio
70
Normal
Sim
Não
Chuvoso
75
Suave
80
Normal
Não
Sim
Chuvoso
71
Suave
91
Alta
Sim
Não
Ensolarado
85
Quente
85
Alta
Não
Não
Ensolarado
80
Quente
90
Alta
Sim
Não
Ensolarado
72
Suave
95
Alta
Não
Não
Ensolarado
69
Frio
70
Normal
Não
Sim
Ensolarado
75
Suave
70
Normal
Sim
Sim


Esta base será usada para os algoritmos de Árvore de Decisão (ID3 e C4.5), com algumas modificações. Para o algoritmo ID3 os campos numéricos serão removidos, pois como veremos no próximo post o algoritmo não aceita valores numéricos, e para o algoritmo C4.5 os campos Nominais de temperatura e umidade serão removidos, pois são informações redundantes.

Link para a base de dados: