Na última vez que tentei entender o C4.5, falhei, mas implementei uma variante do ID3 - originalmente por curiosidade, mas ele acabou sendo usado como parte de um gerador de código de despacho múltiplo por excesso de capacidade. Porém, isso nunca lida com grandes conjuntos de dados e é um bom trabalho. Você não faria bem em imitar a maior parte do que eu fiz, mas talvez com algumas exceções, e é claro que aprendi um pouco com os erros.
Costumo pensar em termos de construção de uma árvore de decisão para um sistema especialista, por isso uso os seguintes termos - desculpe se isso é confuso ...
Column = Question ..... A question the expert system might ask
Row = Conclusion ... A possible conclusion the expert system might reach
Cell = Answer ....... For the question and conclusion, what answer should
the user be expected to give
Na verdade, no meu caso, concluí em outra coluna - semelhante a uma tabela de verdade para um portão lógico. Os números de linha eram, portanto, apenas números de linha. Isso me permitiu lidar com problemas no estilo XOR que nem sequer podem ser representados se a mesma conclusão não puder aparecer em várias linhas. Não tenho certeza se isso é relevante para você ou não. De qualquer forma, estou ignorando isso abaixo - isso realmente não faz muita diferença, a menos que você veja os detalhes da escolha de qual pergunta fazer a seguir. Para a mineração de dados, você provavelmente não tem uma informação específica a ser tratada como a conclusão desejada - a "conclusão" é apenas o que resta quando você decide parar de fazer perguntas.
Portanto - para cada nó da árvore de decisão derivado até agora, você tem um conjunto de perguntas pendentes (colunas) e um conjunto de conclusões (linhas) ainda não eliminadas. Isso é o que eu fiz. O único ponto que vale a pena acrescentar é que eu usei vetores de bits.
O IIRC, o C ++, std::vector<bool>
e std::array<bool>
pode ser implementado como vetor de bits, mas você ainda depende dos algoritmos STL para operações definidas, que operam um item por vez. Eu usei minha própria classe de vetor de bits que foi gradualmente construída ao longo de um período de tempo e que usa operadores bit a bit no subjacente std::vector<CHUNK>
(onde CHUNK
é um tipo int sem sinal, geralmente com 32 bits de largura).
Pode haver uma opção melhor de vetor de bits no C ++ 11 ou no Boost, e deve haver algumas boas bibliotecas por aí - onde há vários tipos de programas nos quais você acaba trabalhando com conjuntos de números inteiros não assinados. Só não sei muito sobre eles porque tenho preguiça de deixar de usar os meus.
No entanto, os vetores de bits são melhores quando os conjuntos são densos. Nesse caso, o conjunto de linhas é o problema óbvio. Somente o nó raiz da árvore de decisão terá um conjunto de linhas perfeitamente denso. À medida que você se distancia da raiz, os conjuntos de linhas ficam cada vez mais escassos, com cada pergunta respondida, resultando no conjunto de linhas distribuídas entre dois ou mais conjuntos de linhas do próximo nó separados.
Portanto, uma simples matriz classificada de números de linhas pode ser a melhor representação para esses conjuntos. No entanto, também é possível que um "vetor de bits esparso" valha a pena. Uma implementação possível é uma matriz classificada de pares, em que o primeiro de cada par é o primeiro ID da linha de um bloco e o segundo é um vetor de bits de tamanho fixo para esse bloco. Por exemplo, o número da linha 35 pode ser armazenado no bloco 32 ( 35 & ~(32 - 1)
) na posição de bit 3 ( 35 & (32 - 1)
). Se você salvar apenas os pares em que o vetor de bits é diferente de zero, isso fornece algo entre uma matriz classificada de IDs e um vetor de bits simples - ele lida com matrizes esparsas razoavelmente bem, especialmente quando as IDs tendem a se agrupar em conjuntos.
Além disso, pode valer a pena usar uma classe que pode alternar de um vetor de bits para uma representação de matriz classificada quando o tamanho é pequeno o suficiente. A complicação extra, apenas para beneficiar alguns nós próximos à raiz, é provavelmente inútil.
De qualquer forma, porém, esses conjuntos são representados, pois se referem a um único "banco de dados" constante, economizando muito na cópia de dados e no desperdício de espaço à medida que o algoritmo é executado. Mas ainda vale a pena olhar para esse "banco de dados".
Usei uma estrutura de dados associativa, permitindo pesquisar usando uma tupla de ID de pergunta e ID de conclusão para obter um ID de resposta. Isso significa que eu tinha uma sobrecarga por item para a chave (ID da pergunta e ID da conclusão) e, neste caso, a sobrecarga da árvore no estilo B + também. A razão - basicamente hábito. Tenho contêineres muito flexíveis, e costumo usá-los muito, pois economiza em antecipar quais recursos realmente precisarei mais tarde. Há um preço por isso, mas isso é apenas a velha coisa de otimização prematura.
No seu caso, você está usando uma matriz - presumo que uma matriz bidimensional indexada por ID de pergunta e ID de resposta.
A única maneira de imaginar minha versão mais eficiente que a sua é se a maioria das respostas não for conhecida. Em uma matriz, você precisa de um ID de resposta não conhecido especial para isso, ocupando o mesmo espaço que um ID de resposta conhecido. Em um contêiner associativo, você exclui essas linhas.
Mesmo assim, uma matriz classificada seria mais eficiente do que minha solução baseada em árvore B +. Você não precisa permitir inserções eficientes; portanto, a única sobrecarga necessária é para as chaves.
Se você usar dois campos-chave (pergunta e conclusão, linha e coluna) que podem ser um problema (não me lembro) - talvez você não possa simplesmente manter uma cópia da tabela em uma ordem classificada. Mas se você usar uma única chave computada ao longo das linhas de (row * num_columns) + column
, estará basicamente implementando uma matriz esparsa bidimensional.
Para mim, a presença de respostas desconhecidas / indefinidas para uma pergunta em particular significa que ainda não estou autorizado a fazer essa pergunta - e mesmo essa foi apenas a teoria que usei quando implementei o algoritmo. Eu nunca coloquei isso em prática. Há um uso que eu poderia colocar, mas nunca cheguei a isso. Para o registro, naquele gerador de código de despacho múltiplo, uma idéia era despachar com base nos campos do tipo. Como o tipo em si é polimórfico, esses campos podem nem estar lá; portanto, é válido apenas examiná-los depois de confirmar que eles devem estar presentes.
Se você não possui um aplicativo para respostas desconhecidas / indefinidas, sua matriz existente provavelmente já é a melhor solução.
Então, basicamente, é isso - eu realmente não posso oferecer opções claramente melhores, e o que você está fazendo provavelmente já é melhor do que eu fiz. No entanto, existem algumas possibilidades de troca que você pode considerar - supondo que não seja uma otimização prematura (e possivelmente falsa), é claro.
A principal questão do trade-off refere-se à eficiência das representações de conjuntos de valores esparsos vs. densos, portanto, não é realmente específico do C4.5 ou da construção da árvore de decisão. E uma abordagem mais "sofisticada" geralmente é menos eficiente do que uma abordagem simples, escolhida com cuidado.