Portando o "Free Associative Neurons" (FAN) para o WEKA
Autor: Blog do Dieval | Publicado: 2013-02-15T10:30:00-08:00

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&nbsp;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&nbsp;FANClassifier no pacote&nbsp;weka.classifiers.functions e derivei da superclasse&nbsp;AbstractClassifier e adicionei as implementações para as interfaces&nbsp;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&nbsp;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) {
&nbsp; &nbsp; &nbsp; &nbsp; 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)&nbsp;public double classifyInstance(Instance instance) throws Exception {

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

3)&nbsp;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&nbsp;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&nbsp;parâmetro&nbsp; ou seja, a descrição de cada&nbsp;parâmetro&nbsp;do seu algoritmo. No meu caso:


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



Um pequeno trecho do setOptions:


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


//....

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




Mais um pequeno trecho, agora do getOptions:


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

&nbsp; &nbsp; }





O Treinamento
O método previsto na classe&nbsp;AbstractClassifier para realizar o treinamento é o&nbsp;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&nbsp;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&nbsp;m_data, enquanto que os dados normalizados para o treinamento ficam em&nbsp;m_norm_data. Então o método build foi inicialmente criado dessa forma:


&nbsp; &nbsp; public void buildClassifier(Instances data) throws Exception {
&nbsp; &nbsp; &nbsp; &nbsp; // Set up the initial arrays
&nbsp; &nbsp; &nbsp; &nbsp; 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.

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

&nbsp; &nbsp; &nbsp; &nbsp; // can classifier handle the data?
&nbsp; &nbsp; &nbsp; &nbsp; 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:


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




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&nbsp;initializeClassifier pode ser visto a seguir:


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

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

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

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

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


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:


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





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:


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



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&nbsp;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   &lt;-- classified as
 50  0  0 |  a = Iris-setosa
  0 48  2 |  b = Iris-versicolor
  0  2 48 |  c = Iris-virginica