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!
Y is the new X
Segue aqui uma listinha de utilitários ligeiramente diferentes dos tradicionais e consolidados, mas cujas pequenas e grandes melhorias me fizeram renunciar quase que completamente os mais clássicos e abraçar os bleeding edge ;)
mosh is the new ssh
Não me entenda mal: SSH é muito mais do que uma simples shell remota! Também serve para tunelamento de conexões, transferência de arquivos, autenticação remota... Aliás, o próprio Mosh se autentica via SSH. Só que daí em diante, é outra história.
Mosh utiliza o protocolo UDP ao invés do TCP, então não precisa manter uma conexão. Simplificando muito, Mosh faz sincronização do estado do terminal remoto com o local, ao invés de redesenhar as telas caractere por caractere.
Ou seja: o seu link pode ser muito ruim, que o Mosh não está nem aí. Pode perder conexão, pode colocar o notebook em standby; quando o link voltar, está aí a sua shell, do jeito que você deixou! 'Network error: Connection reset by peer', nunca mais.
Outro "efeito colateral" bacana do Mosh é que ele dispensa buffers. Já me ferrei dando um lsof numa conexão lerda; até que todo o output saia, SSH não deixa nem o input de Ctrl-C passar.
Para finalizar, o comparativo de tempo de resposta de SSH com Mosh, do site oficial do MIT:

tmux is the new screen
Mesmo que você seja um ávido fã do screen, o tmux vale uma espiada. O grande distintivo talvez seja o modelo client/server, que garante uma grande flexibilidade às sessões gerenciadas pelo tmux. Por exemplo, dá para mover janelas entre diversas sessões, ou então replicar o input em várias janelas (útil quando se quer iniciar o mesmo processo em diversos nós). Os atalhos de teclas tem perfis pré-definidos "emprestados" de Vim ou Emacs, e, é claro, dá para redefinir quase tudo do jeito que quiser pelo arquivo de configuração bastante legível. Ah, um bônus: split vertical da tela funciona sem patch adicional 
Um breve tutorial:
htop is the new top
Esse dispensa apresentações. É praticamente um 'Activity Monitor.app' versão terminal. Infelizmente, a versão para MacOS tem algumas esquisitices (especialmente na parte que se refere à exibição da quantidade de memória). Ainda assim, é excelente para overview geral do sistema. Sempre mantenho um htop aberto nos meus servidores 😉
ack is the new grep
Descobri o ack recentemente. Não é bem um grep, mas sim uma combinação de find/grep específica para trabalhar com código-fonte. Em suma, ele faz busca recursiva utilizando regexp "sabor Perl", e desconsidera coisas como subdiretórios do Git ou SVN. Por exemplo uma versão (very) quick & dirty do Perl::PrereqScanner seria:
ack -ho --perl '^\s*use\s+[\w+\.:]+' | sort -u
xz is the new bzip2
Um pequeno devaneio: aparentemente, Inteligência Artificial e compressão de dados são assuntos afins. Os organizadores do Hutter Prize, por exemplo, acreditam que uma compressão eficiente de um texto natural seria de dificuldade equivalente a passar no teste de Turing.
Voltando ao tema original: anos atrás, quando clock de um PC high-end não passava de 1 GHz, e conexão broadband ainda estava engatinhando nos países do 3-o mundo, ver um código-fonte compactado com bzip2 numa página de download era de restaurar a fé na humanidade. Pois era evidentemente um ato altruístico por parte do disponibilizador levar muito mais tempo para compactar para poupar um pouquinho de tempo e banda de quem fizer o download.
Hoje, o bzip2 roda na velocidade do gzip de outrora. Eis que revive o algoritmo LZMA: muito mais voraz quanto à CPU/RAM, na grande maioria dos casos chuta bonito o traseiro do bzip2! O conheci através do 7-Zip, de longe o melhor compactador para Windows (xupem, WinZip e WinRAR!). Depois, para a minha alegria, descobri o xz, que:
- É mais fácil de digitar do que 'bzip2';
- Compacta mais do que o bzip2;
- Usa os mesmos parâmetros que o bzip2 (
cat ... | xz -cv9 > dump.xzcontinua válido).
E você, tem algum exemplo assim? Comente!
DBIx::Class HATE

Exercendo a minha liberdade de expressão, vou tentar explicar por que raios, afinal, eu detesto tanto o DBIx::Class. Ninguém é obrigado a concordar comigo, mas espero que os que leiam, reflitam.
Sem dúvida, é um treco robusto e consistente, mas já dizia o Oscar Wilde: "Consistency is the last refuge of the unimaginative." Agora eu, felizmente (ou não), tenho imaginação de sobra. Sou conhecido por minhas gambiarras atrozes. Sou um péssimo programador, a ponto de ter largado a faculdade de computação no quarto ano e ter me formado em desenho industrial 
O que gosto no Perl é a sua capacidade de ser um master-glue, amarrando pedaços desconexos numa "coisa" que, magicamente, atende às necessidades.
Como bem ilustra o fabuloso poema "Black Perl", o Perl é livre de preconceitos e purismos. Posso "furar" a privacidade dos atributos do Moose, subverter o MVC com o Mojolicious, além de fazer todo tipo de monkeypatching bizarro e ad-hoc.
Aí entra o DBIx::Class. Seu glorioso dbicdump já ofende os meus olhos:
# Created by DBIx::Class::Schema::Loader v0.07010 @ 2011-08-24 13:26:20 # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:qqyhu9B0Vr7l1KsQ6CG7Jw
Desgraçado, como assim?! Em primeiro lugar, a caralhada de classes (uma por tabela?!), para mim, por si só, é uma coisa javesca. E, em segundo lugar, o mentecapto é prolixo e nem posso ousar de intervir com o Perl::Tidy para deixar o código legível do jeito que gosto. Sem falar que o schema é "lossy" e perde várias informações.
Mas isso é o de menos. Não consigo imaginar um workflow que me agrade. Eu gosto do pgAdmin e do HeidiSQL e costumo elaborar as minhas tabelas e queries nessas GUIs. Isso me dá um retrabalho na hora de "portar" para Perl (diferentemente do DBI, aonde é só copiar e colar, ráááá).
E olha só, CRUD não é o foco do meu trabalho. Às vezes, preciso dar uma cutucada no banco de dados, ou então um grande processamento em lote. Mas variedade de queries? Não me pertence.
Aliás, um ponto importante: em geral, sou "equipe de um homem só". E Perl dá um grande poder nas mãos das equipes de um homem só. Não preciso de um código de fácil manutenção. Eu assumo a penalidade e o risco de me perder nas próprias queries quilométricas. Me divirto muito com isso.
Nesse ponto, DBIx::Class me parece coisa de tiozão. Me lembra aqueles emails corporativos aonde a assinatura é 100x maior do que a mensagem, sendo o schema a assinatura e o uso da mesma, a mensagem 
