Implementação de classificador Naïve Bayes em MongoDB
O presente artigo é derivado do que escrevi para o evento Equinócio de Outono de 2012, promovido por São Paulo Perl Mongers.
Porém, ao invés de servir como uma espécie de complemento, é uma "volta às origens". O algoritmo original, desenvolvi em JavaScript puro, depois traduzi para Perl, refinei, detalhei e escrevi sobre. A intenção agora é ser mais show me the code possível, com um exemplo simples, enxuto e prático. Portanto, a teoria e os detalhes continuam lá no artigo original. Aqui, é mão na massa!
Ensinando o classificador
O classificador funciona por palavras-chave, e o MongoDB, nesse estágio, atua como um mero key/value storage. Salve o código de inicialização a seguir como init.js e carregue com mongo bayes init.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | db.bayes.update({_id: 'pelos'}, {$inc: {total: 1, 'categ.gato': 1}}, 1); db.bayes.update({_id: 'bigodes'}, {$inc: {total: 1, 'categ.gato': 1}}, 1); db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.gato': 1}}, 1); db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.gato': 1}}, 1); db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.gato': 1}}, 1); db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.gato': 1}}, 1); db.bayes.update({_id: 'rabo'}, {$inc: {total: 1, 'categ.gato': 1}}, 1); db.bayes.update({_id: 'pelos'}, {$inc: {total: 1, 'categ.cachorro': 1}}, 1); db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.cachorro': 1}}, 1); db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.cachorro': 1}}, 1); db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.cachorro': 1}}, 1); db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.cachorro': 1}}, 1); db.bayes.update({_id: 'rabo'}, {$inc: {total: 1, 'categ.cachorro': 1}}, 1); db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.galo': 1}}, 1); db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.galo': 1}}, 1); db.bayes.update({_id: 'bico'}, {$inc: {total: 1, 'categ.galo': 1}}, 1); db.bayes.update({_id: 'asa'}, {$inc: {total: 1, 'categ.galo': 1}}, 1); db.bayes.update({_id: 'asa'}, {$inc: {total: 1, 'categ.galo': 1}}, 1); db.bayes.update({_id: 'penas'}, {$inc: {total: 1, 'categ.galo': 1}}, 1); db.bayes.update({_id: 'bico'}, {$inc: {total: 1, 'categ.ornitorrinco': 1}}, 1); db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.ornitorrinco': 1}}, 1); db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.ornitorrinco': 1}}, 1); db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.ornitorrinco': 1}}, 1); db.bayes.update({_id: 'pata'}, {$inc: {total: 1, 'categ.ornitorrinco': 1}}, 1); db.bayes.update({_id: 'rabo'}, {$inc: {total: 1, 'categ.ornitorrinco': 1}}, 1); db.bayes.update({_id: 'pelos'}, {$inc: {total: 1, 'categ.ornitorrinco': 1}}, 1); |
Agora, entrando em MongoDB Interactive Shell com mongo bayes, podemos ver a estrutura resultante com db.mongo.find():
{ "_id" : "bigodes", "categ" : { "gato" : 1 }, "total" : 1 } { "_id" : "bico", "categ" : { "galo" : 1, "ornitorrinco" : 1 }, "total" : 2 } { "_id" : "asa", "categ" : { "galo" : 2 }, "total" : 2 } { "_id" : "penas", "categ" : { "galo" : 1 }, "total" : 1 } { "_id" : "pata", "categ" : { "cachorro" : 4, "galo" : 2, "gato" : 4, "ornitorrinco" : 4 }, "total" : 14 } { "_id" : "rabo", "categ" : { "cachorro" : 1, "gato" : 1, "ornitorrinco" : 1 }, "total" : 3 } { "_id" : "pelos", "categ" : { "cachorro" : 1, "gato" : 1, "ornitorrinco" : 1 }, "total" : 3 } |
Cada palavra-chave é associada aos contadores das categorias, além do contador geral de ocorrências. Agora, vamos processar essa estrutura de dados!
Classificador em map/reduce
O código a seguir é quase que um programa independente em JavaScript, salve-o como bayes-mongodb.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | var words = []; for (var word in doc) words.push(word); var categ = [ 'gato', 'cachorro', 'galo', 'ornitorrinco' ]; var guess = db.runCommand({ mapreduce: 'bayes', out: { inline: 1 }, query: { _id: { $in: words } }, scope: { categ: categ, doc: doc, }, map: function () { for (var i = 0; i < categ.length; i++) { var ctg = categ[i]; var prob = Math.log( typeof(this.categ[ctg]) != 'undefined' ? this.categ[ctg] : 1.18e-38 ); prob -= Math.log(this.total); emit(ctg, prob * doc[this._id]); } }, reduce: function (key, values) { var result = 0; values.forEach(function (value) { result += value; }); return result; } }); var res = { value: -Infinity }; for (var i in guess.results) if (guess.results[i].value > res.value) res = guess.results[i]; print(res._id); |
Para passar parâmetros via linha de comando, usamos a opção --eval do MongoDB Shell, e o script imprime na tela a categoria "adivinhada":
$ mongo bayes --quiet --eval 'var doc = { bigodes: 1, rabo: 1 }' bayes-mongodb.js
gato
$ mongo bayes --quiet --eval 'var doc = { bico: 1, rabo: 1, patas: 4 }' bayes-mongodb.js
ornitorrinco
$ mongo bayes --quiet --eval 'var doc = { bico: 1, asa: 2 }' bayes-mongodb.js
galo
Conclusão
O básico do básico esta aí 
É evidente que os resultados e a eficiência do classificador bayesiano variam de caso para caso, e até de implementação para implementação. Se a primeira tentativa não for um sucesso, se quiser entender para que serve o "número mágico" 1.18e-38, ou se quiser automatizar o "treinamento" através de um pequeno script em Perl, consulte o artigo original!
Perl e módulos na $HOME
Mas, por quê?!
Qual o sentido de alguém instalar os módulos dentro do seu $HOME? Normalmente, uma das duas:
- Você não tem permissão suficiente para instalar no próprio sistema (você está mexendo num servidor ou coisa do tipo);
- Você está desenvolvendo a bagaça, e quer manter a versão bleeding edge só para você.
Bom, se for o 2-o caso, existe o tal do perlbrew.
Mas eu gosto das instalações locais e as uso desde '97. E o local::lib foi uma mão na roda e tanto! Então, vamos lá:
Passo-a-passo:
Download do local::lib:
curl -LO http://search.cpan.org/CPAN/authors/id/A/AP/APEIRON/local-lib-1.008004.tar.gz tar xzf local-lib-1.008004.tar.gz cd local-lib-1.008004/
Instalação do local::lib:
perl Makefile.PL --bootstrap=$HOME/perl5 make make test make install
Ativando local::lib na inicialização da shell (Bash):
cat << PROFILE >> $HOME/.profile export PERL_MM_USE_DEFAULT=1 eval \$(perl -I\$HOME/perl5/lib/perl5/ -Mlocal::lib) PROFILE
Agora, é hora de logoff/logon para quem está acostumado com o Windows, ou então:
. $HOME/.profile
Prosseguindo com a configuração/teste do CPAN (essa configuração faz as dependências serem instaladas automaticamente):
cpan cpan[1]> o conf prerequisites_policy follow cpan[2]> o conf commit cpan[3]> install App::cpanminus cpan[4]> quit
Enumerando os módulos instalados:
perl -MExtUtils::Installed -E 'chdir $ENV{PERL_LOCAL_LIB_ROOT}; my $i = new ExtUtils::Installed; say for $i->modules'
cpanminus permite atualizar todos de uma vez, de uma maneira eficiente:
perl -MExtUtils::Installed -E 'chdir $ENV{PERL_LOCAL_LIB_ROOT}; my $i = new ExtUtils::Installed; say for $i->modules' | cpanm
