Portando o "Free Associative Neurons" (FAN) para o WEKA

O WEKA é a melhor ferramenta livre (open source) para mineração de dados disponível na atualidade.

As concorrentes que conheço são: Orange, RapidMiner, JHepWork, Konstanz Information Miner (KNIME) e o R Analytical Tool To Learn Easily(RATTLE). No geral, as principais técnicas de classificação, associação e agrupamento estão disponíveis nessas ferramentas, mas considero o WEKA sendo a ferramenta mais fácil de integrar em outras aplicações. Tanto que existem extensões para usar o WEKA em alguns dos programas acima.

Apenas para deixar essa pequena lista mais completa, durante alguns anos eu usei o Tanagra na disciplina de Tópicos de Banco de Dados (TADS/UFPR), muito bom. Mas no final, o fato do WEKA ter uma ótima documentação, dos fontes da aplicação estarem disponíveis para os interessados, sempre se mostrou, nos meus estudos, o mais interessante.

O "Free Associative Neurons" (FAN) é um modelo neuro-fuzzy de rede neural, inventada pelo Roberto Raittz, que associa o aprendizado automático (supervisionado) e os modelos difusos. O resultado do treinamento pode ser representado graficamente. Ao portar a última implementação do FAN (denominado SIBILA) que fiz para o WEKA, estarei tornando mais fácil o acesso ao método, bem como não precisarei refazer toda a parte de interface e de tratamento de dados que já estão disponíveis no WEKA.

A estrutura básica


O esqueleto básico está descrito na seção "18.1 WRITING A NEW CLASSIFIER" do manual do WEKA (versão 3.7.8 - 21/01/2013). No meu caso, criei o projeto no NetBeans (a equipe do WEKA prefere o Eclipse), denominado WFAN (WEKA+FAN) e a classe FANClassifier no pacote weka.classifiers.functions e derivei da superclasse AbstractClassifier e adicionei as implementações para as interfaces OptionHandler, WeightedInstancesHandler, Randomizable. Irei comentar cada uma, tentando justificar o uso delas no meu projeto.

Para começar, meu projeto irá se tornar um "pacote" externo, para usá-lo no WEKA, o usuário terá que ir no menu "Tools" e na opção "Package manager", atualizar a lista, clicar no WFAN e pedir para fazer a instalação.

Para exemplificar aqui o uso, peguei o arquivo iris.arff (que contém os dados do famoso experimento de Fisher - 1936 - link para o repositório da UCI ou no formato para o WEKA).

No WEKA, clique no botão "Explorer", posteriormente em "Open file...", escolha o arquivo iris.arff, clique na aba "Classify", no botão "Choose" que abrirá uma árvore. Os nós da árvore a serem seguidos são: weka / classifiers / functions / FANClassifier. Na caixa "Test options" é recomendável marcar a opção "Use training set", uma vez que o conjunto de dados é pequeno e apresenta problemas de classificação. Ao clicar no botão start, o weka chamará as minhas funções e apresentará o resultado do aprendizado na caixa de texto "Classifier output".

Na linha de comando, podemos chamá-lo diretamente com:
java weka.classifiers.functions.FANClassifier -t iris.arff -v

Bom, para isso precisamos do método main, na nossa classe... direto do manual:

public static void main(String[] args) {
        AbstractClassifier.runClassifier(new FANClassifier(), args);
}

Os métodos básicos que temos que implementar são:

1) public void buildClassifier(Instances data) throws Exception

Chamado para treinar o modelo, bem como verificar se o método é capaz de trabalhar com o conjunto de dados. Isso porque o conjunto pode ter valores ausentes nos atributos, atributos numéricos ou nominais, classes numéricas ou nominais, classes ausentes etc.

2) public double classifyInstance(Instance instance) throws Exception {

Chamado para testar a classificação de uma instância, previamente conhecida.

3) public double[] distributionForInstance(Instance instance) throws Exception {

Esse método é legal, o vetor de double deve ter o mesmo número de classes que o padrão de entrada (instance) e informar a probabilidade de cada classe. Segundo a documentação, o método anterior deve chamar esse método e pegar o de maior probabilidade como resposta.


Além disso, existem alguns métodos auxiliares, que ajudam a integrar o seu classificador com a interface do weka, entre outros, os provenientes da interface OptionHandler, são eles:

Enumeration listOptions();
void setOptions(String[] options) throws Exception;
String[] getOptions();

Basicamente, o primeiro lista as opções, com uma descrição e o tipo de dado esperado para o parâmetro  ou seja, a descrição de cada parâmetro do seu algoritmo. No meu caso:

    public Enumeration listOptions() {
        Vector
        newVector.addElement(new Option(
                "\tRadius diffuse or neighborhood size (default 6)\n",
                "FR", 1, "-FR "));
        newVector.addElement(new Option(
                "\tsupport FAN space (default is 100).\n",
                "FS", 1, "-FS "));
        newVector.addElement(new Option(
                "\tNumber of epochs to train through.\n"
                + "\t(Default = 2000).",
                "N", 1, "-N "));
        newVector.addElement(new Option(
                "\tGUI will be opened.\n"
                + "\t(Use this to bring up a GUI).",
                "G", 0, "-G"));
        newVector.addElement(new Option(
                "\tscramble the training set every n epochs.\n"
                + "\t(Default = 50).",
                "SE", 1, "-SE "));
        newVector.addElement(new Option(
                "\tThe value used to seed the random number generator\n"
                + "\t(Value should be >= 0 and and a long, Default = 0).",
                "S", 1, "-S "));        
        newVector.addElement(new Option(
              "\tReseting the network will NOT be allowed.\n"
     +"\t(Set this to not allow the network to reset).",
     "R", 0,"-R"));
        Enumeration enu = super.listOptions();
        while (enu.hasMoreElements()) {
            newVector.addElement((Option) enu.nextElement());
        }
        return newVector.elements();
    }



Um pequeno trecho do setOptions:

    public void setOptions(String[] options) throws Exception {
        String auxRadius = Utils.getOption("FR", options);
        try {
            setRadius(Integer.decode(auxRadius));
        } catch (NumberFormatException ne) {
            setRadius(6);
        }

//....
        if (Utils.getFlag('R', options)) {
            setReset(false);
        } else {
            setReset(true);
        }
        super.setOptions(options);
    }



Mais um pequeno trecho, agora do getOptions:

    public String[] getOptions() {
        String[] superOptions = super.getOptions();
        String[] options = new String[superOptions.length + 11];
        int current = 0;
        options[current++] = "-FR";
        options[current++] = String.format("%d", getRadius());
//...
        if (!isReset()) {
            options[current++] = "-R";
        } else {
            options[current++] = "";
        }
        System.arraycopy(superOptions, 0, options, current,
                superOptions.length);
        current += superOptions.length;
        while (current < options.length) {
            options[current++] = "";
        }
        return options;

    }



O Treinamento

O método previsto na classe AbstractClassifier para realizar o treinamento é o buildClassifier, mas olhando a implementação dos outros classificadores, observei que eles tendem a separar o processo de treinamento do processo de inicialização do modelo. Isso nos levou a criação do método initializeClassifier que recebe os mesmos parâmetros do buildClassifier e retorna a coleção dos dados para treinamento. No caso do FAN, os dados originais ficam armazenados no atributo m_data, enquanto que os dados normalizados para o treinamento ficam em m_norm_data. Então o método build foi inicialmente criado dessa forma:

    public void buildClassifier(Instances data) throws Exception {
        // Set up the initial arrays
        m_data = initializeClassifier(data);


Agora vamos ver o que faremos na inicialização do modelo FAN, dentro da perspectiva do ambiente WEKA. Aqui podemos chamar o método getCapabilities que retorna uma instância de Capabilities, que em outros termos, diz quais tipos de atributos e classes o método está capacitado a trabalhar.

    /**
     * Method used to pre-process the data, create model objects, and set the
     * initial parameter vector.
     */
    protected Instances initializeClassifier(Instances data) throws Exception {

        // can classifier handle the data?
        getCapabilities().testWithFail(data);


O FAN não precisa de cuidados especiais para o tipo de dado que representa a informação da classe, mas precisa que os atributos sejam numéricos, e ainda não encontramos uma boa estratégia para trabalhar com atributos ausentes (campos vazios, ou dados incompletos). Dessa forma, nosso método getCapabilities ficou:

    public Capabilities getCapabilities() {
        Capabilities result = super.getCapabilities();
        result.disableAll();
        // attributes
        //result.enable(Capabilities.Capability.NOMINAL_ATTRIBUTES);
        result.enable(Capabilities.Capability.NUMERIC_ATTRIBUTES);
        // class
        result.enable(Capabilities.Capability.NOMINAL_CLASS);
        result.enable(Capabilities.Capability.NUMERIC_CLASS);
        result.enable(Capabilities.Capability.DATE_CLASS);
        return result;
    }


Estamos mantendo os atributos nominais fora do escopo nesse momento, mas o WEKA possui o filtro NominalToBinary que permite a conversão de atributos nominais para numéricos. Portanto, para futuras versões, poderemos avaliar o desempenho do modelo com o uso desse filtro.

O restante do nosso método initializeClassifier pode ser visto a seguir:

    protected Instances initializeClassifier(Instances data) throws Exception {
        // can classifier handle the data?
        getCapabilities().testWithFail(data);

        // data ana
        Instances result = new Instances(data);
        result.deleteWithMissingClass();

        // verifying data
        normalizeFilter = new Normalize();
        normalizeFilter.setScale(1d);
        normalizeFilter.setTranslation(0d);
        normalizeFilter.setInputFormat(result);
        result = Filter.useFilter(result, normalizeFilter);

        // Transform nominal attributes
        //m_NominalToBinary = new NominalToBinary();
        //m_NominalToBinary.setInputFormat(result);
        //result = Filter.useFilter(result, m_NominalToBinary);

        //
        // verifying model
        //
       //...
        if (isReset() || this.fan == null
                || this.fan.getNumberOfAttributes() != m_numAttributes
                || this.fan.getNeuronios().size() != m_numClasses) {
            this.fan = new FAN();
            // ....
        }
        return result;
    }


Nessa parte os padrões sem classes são removidos do conjunto, os atributos são normalizados e o modelo fan é inicializado (o número de neurônios, os conjuntos de suporte etc).

Dessa forma o método de treinamento fica bastante simples:

    public void buildClassifier(Instances data) throws Exception {
        m_norm_data = initializeClassifier(data);
        if (m_norm_data == null) {
            return;
        }
        for (int i_epoch = 0; i_epoch < m_epoch; i_epoch++) {
            for (Instance m_currentInstance : m_norm_data) {
                if (!m_currentInstance.classIsMissing()) {
                    training(m_currentInstance);
                }
            }
        }



Validação do aprendizado (teste)

Tendo o modelo construído (treinado), o WEKA irá fazer uso de dois métodos para testar e apresentar as análises do treinamento, são eles:

    @Override
    public double[] distributionForInstance(Instance m_currentInstance) throws Exception {
        checkForTransient();
        normalizeFilter.input(m_currentInstance);
        m_currentInstance = normalizeFilter.output();
        double[] caracPadrao = convertAttributesArray.convert(m_currentInstance.toDoubleArray(),
                m_currentInstance.classIndex());
        classify(caracPadrao);
        return neuronsPower;
    }


Observe a chamada do filtro de normalização, requisito para o modelo FAN. O método de classificação foi modificado para preservar a força (pertinência) calculados a partir das características passadas em relação a cada neurônio, esses valores são mantidos na matriz neuronsPower. O resultado desse método é utilizado pelo método classifyInstance (herdado de AbstractClassifier), o maior valor é considerado como a classe indicada (o mesmo resultado do método classify do modelo FAN).

Em exemplo dos resultados obtidos no ambiente WEKA pode ser visto a seguir:

=== Run information ===

Scheme:       weka.classifiers.functions.FANClassifier -FR 6 -FS 100 -S 0 -N 1000 -SE 50
Relation:     iris
Instances:    150
Attributes:   5
              sepallength
              sepalwidth
              petallength
              petalwidth
              class
Test mode:    evaluate on training data

=== Classifier model (full training set) ===

weka.classifiers.functions.FANClassifier@76974876

Time taken to build model: 3.72 seconds

=== Evaluation on training set ===

Time taken to test model on training data: 0.05 seconds

=== Summary ===

Correctly Classified Instances         146               97.3333 %
Incorrectly Classified Instances         4                2.6667 %
Kappa statistic                          0.96  
Mean absolute error                      0.2921
Root mean squared error                  0.3332
Relative absolute error                 65.7213 %
Root relative squared error             70.6839 %
Coverage of cases (0.95 level)         100      %
Mean rel. region size (0.95 level)      96.4444 %
Total Number of Instances              150     

=== Detailed Accuracy By Class ===

                 TP Rate  FP Rate  Precision  Recall   F-Measure  MCC      ROC Area  PRC Area  Class
                 1,000    0,000    1,000      1,000    1,000      1,000    1,000     1,000     Iris-setosa
                 0,960    0,020    0,960      0,960    0,960      0,940    0,984     0,971     Iris-versicolor
                 0,960    0,020    0,960      0,960    0,960      0,940    0,969     0,943     Iris-virginica
Weighted Avg.    0,973    0,013    0,973      0,973    0,973      0,960    0,984     0,971     

=== Confusion Matrix ===
  a  b  c   <-- classified as
 50  0  0 |  a = Iris-setosa
  0 48  2 |  b = Iris-versicolor
  0  2 48 |  c = Iris-virginica

Comentários

Postagens mais visitadas deste blog

Jellyfish script

Conversão do encode do MariaDB para atender o moodle 3.8

O GBParsy é uma biblioteca para realizar o parser de arquivos GenBank para o Python