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.
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:
Bom, para isso precisamos do método main, na nossa classe... direto do manual:
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:
Um pequeno trecho do setOptions:
Mais um pequeno trecho, agora do getOptions:
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.
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:
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:
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:
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:
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 -vBom, 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);
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;
}
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;
}
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
Postar um comentário