Você está na página 1de 6

Exemplo: Naive Bayes para Filtros de Spam

Linguagem de Programação Aplicada

Prof. Alex Kutzke

04 de maio 2019

Introdução ao Naive Bayes


Introdução
• Naive Bayes é o nome que se dá à técnica de classificação baseada, entre outras coisas, no Teorema de Bayes;
• Veremos a seguir uma pequena aplicação dessa técnica para a classificação de emails entre Spam e Não Spam;

Teorema de Bayes
• Antes, para relembrar, segue o teorema de Bayes:
P (F |E)P (E)
‘P (E|F ) = P (F ) ‘
Ou
P (F |E)P (E)
‘P (E|F ) = [P (F |E)P (E)+P (F |¬E)P (¬E)] ‘

• Em palavras simples: o teorema nos informa sobre a probabilidade de um evento E condicionado a F sendo
que só temos informações iniciais sobre o evento F condicionado a E.
• Por exemplo, se sabemos a probabilidade de um email ser SPAM e conter a palavra X, gostaríamos de saber
qual a probabilidade de um email ter a palavra X e ser SPAM.

Um Filtro de Spam Muito Simples


Cenário
• Considere os seguintes eventos:
– ‘S‘: “a mensagem é spam”;
– ‘V ‘: “a mensagem contém a palavra viagra”;
• Segundo o Teorema de Bayes, temos:
P (V |S)P (S)
‘P (S|V ) = [P (V |S)P (S)+P (V |¬S)P (¬S)] ‘

• Numerador: probabilidade de a mensagem ser spam e conter viagra;


• Denominador: probabilidade de a mensagem conter viagra;

Aplicando o modelo
• Se temos uma grande coleção de mensagens classificadas como spam e não-spam:
– Podemos calcular ‘P (V |S)‘ e ‘P (V |¬S)‘.
• Se considerarmos que qualquer mensagem é igualmente provável de ser spam ou não-spam:
– Temos que ‘P (S) = P (¬S) = 0.5‘
P (V |S)
‘P (S|V ) = [P (V |S)+P (V |¬S)] ‘

1
Exemplo
• Por exemplo, se:
– ‘50%‘ das mensagens spam possuem a palavra “viagra”;
– ‘1%‘ das mensagens não-spam possuem a palavra “viagra”;
P (V |S)
‘P (S|V ) = [P (V |S)+P (V |¬S)] ‘
0.5
‘P (S|V ) = (0.5+0.01) = 98%‘
A probabilidade de qualquer email que contenha “viagra” seja spam é: ‘98%‘

Um Filtro de Spam Mais Sofisticado


Ampliando o Contexto
• Suponha um vocabulário de palavras ‘w1 , . . . , wn ‘;
• ‘Xi ‘ é o evento "mensagem contém palavra ‘wi ‘;
• ‘P (Xi |S)‘ é a probabilidade de uma mensagem spam conter a palavra ‘wi ‘;
• ‘P (Xi |¬S)‘ é a probabilidade de uma mensagem não-spam conter a palavra ‘wi ‘;

A Suposição de Naive Bayes


• A técnica Naive Bayes se baseia em uma suposição um tanto inocente:
– A presenças (ou ausências) de cada palavra são independentes uma das outras;
– Obviamente isso é uma grande simplificação;
– Entretanto, mesmo assim, Naive Bayes apresenta bons resultados.
• Isso significa, em outras palavras, por exemplo, que saber que uma mensagem contém ou não “viagra” não nos
informa em nada sobre se ela contém ou não a palavra “carro”.
‘P (X1 = x1 , . . . , Xn = xn |S) = P (X1 = x1 |S) × . . . × P (Xn = xn |S)‘

Explicando
• Se todo vocabulário for composto por apenas “viagra” e “carro”;
• Se metade das mensagens de spam contém “viagra” e a outra metade contém “carro”;
• Assim, a técnica Naive Bayes nos diz que a probabilidade de uma mensagem spam conter ambas as palavras é:
‘P (Xviagra = 1, Xcarro = 1|S) = P (Xviagra = 1|S)P (Xcarro = 1|S) = ‘
‘ = .5 × .5 = .25‘

Juntando tudo
• Segundo o nosso primeiro filtro, sabemos que podemos utilizar a seguinte equação para calcular a probabilidade
de uma mensagem ser spam dado que contém uma palavra X:
P (X=x|S)
‘P (S|X = x) = [P (X=x|S)+P (X=x|¬S)] ‘

• Considerando a suposição de Naive Bayes:


– Podemos calcular as probabilidades da direita multiplicando as probabilidades associadas a cada uma das
palavras independentemente:

Detalhes antes da implementação (1)


• Multiplicação de muitas probabilidades pode causar underflow (números excessivamente pequenos);
• Como todos lembram:
– ‘ log(ab) = log(a) + log(b)‘
– ‘exp(log(x)) = x‘
• Assim, podemos substituir a multiplicação ‘p1 × . . . × pn ‘ por:
– ‘exp(log(p1 ) + . . . + log(pn ))‘

2
Detalhes antes da implementação (2)
• Ao calcular ‘P (Xi |S)‘ e ‘P (Xi |¬S)‘:
– Se a palavra “dado”, por exemplo, ocorre apenas em mensagens não-spam, então ‘P (dado|S) = 0‘;
• Nosso classificador irá atribuir probabilidade 0 de spam para qualquer mensagem que contenha “dado” (por
quê?);
– Mesmo uma que contenha “dado que o viagra . . . ”;
• Portanto, é necessário suavizar as probabilidades, para fugirmos de valores extremos (1 e 0);
• Para isso, utilizaremos um coeficiente ‘k‘ e a seguinte equação adaptada:
(k+número_mensagens_contendo_wi )
‘P (Xi |S) = 2k+número_de_spams ‘
• O mesmo para ‘P (Xi |¬S)‘

Implementação
Imports
from collections import Counter, defaultdict
from machine_learning import split_data
import math, random, re, glob

Divisão em palavras
def tokenize(message):
message = message.lower() # convert to lowercase
all_words = re.findall("[a-z0-9']+", message) # extract the words
return set(all_words) # remove duplicates

Contador de palavras
def count_words(training_set):
"""training set consists of pairs (message, is_spam)"""
counts = defaultdict(lambda: [0, 0])
for message, is_spam in training_set:
for word in tokenize(message):
counts[word][0 if is_spam else 1] += 1
return counts

Calculo das probabilidades de cada palavra


def word_probabilities(counts, total_spams, total_non_spams, k=0.5):
"""turn the word_counts into a list of triplets
w, p(w | spam) and p(w | ~spam)"""
return [(w,
(spam + k) / (total_spams + 2 * k),
(non_spam + k) / (total_non_spams + 2 * k))
for w, (spam, non_spam) in counts.items()]

Calculo das probabilidades de uma mensagem


def spam_probability(word_probs, message):
message_words = tokenize(message)
log_prob_if_spam = log_prob_if_not_spam = 0.0

for word, prob_if_spam, prob_if_not_spam in word_probs:

# for each word in the message,

3
# add the log probability of seeing it
if word in message_words:
log_prob_if_spam += math.log(prob_if_spam)
log_prob_if_not_spam += math.log(prob_if_not_spam)

# for each word that's not in the message


# add the log probability of _not_ seeing it
else:
log_prob_if_spam += math.log(1.0 - prob_if_spam)
log_prob_if_not_spam += math.log(1.0 - prob_if_not_spam)

prob_if_spam = math.exp(log_prob_if_spam)
prob_if_not_spam = math.exp(log_prob_if_not_spam)
return prob_if_spam / (prob_if_spam + prob_if_not_spam)

Encapsulamento
class NaiveBayesClassifier:

def __init__(self, k=0.5):


self.k = k
self.word_probs = []

def train(self, training_set):

# count spam and non-spam messages


num_spams = len([is_spam
for message, is_spam in training_set
if is_spam])
num_non_spams = len(training_set) - num_spams

# run training data through our "pipeline"


word_counts = count_words(training_set)
self.word_probs = word_probabilities(word_counts,
num_spams,
num_non_spams,
self.k)

def classify(self, message):


return spam_probability(self.word_probs, message)

Lendo mensagens reais


def get_subject_data(path):

data = []

# regex for stripping out the leading "Subject:" and any spaces after it
subject_regex = re.compile(r"^Subject:\s+")

# glob.glob returns every filename that matches the wildcarded path


for fn in glob.glob(path):
is_spam = "ham" not in fn

with open(fn,'r',encoding='ISO-8859-1') as file:


for line in file:
if line.startswith("Subject:"):

4
subject = subject_regex.sub("", line).strip()
data.append((subject, is_spam))

return data

Probabilidade para uma única palavra


def p_spam_given_word(word_prob):
word, prob_if_spam, prob_if_not_spam = word_prob
return prob_if_spam / (prob_if_spam + prob_if_not_spam)

Treino e testes
def train_and_test_model(path):

data = get_subject_data(path)
random.seed(0) # just so you get the same answers as me
train_data, test_data = split_data(data, 0.75)

classifier = NaiveBayesClassifier()
classifier.train(train_data)

classified = [(subject, is_spam, classifier.classify(subject))


for subject, is_spam in test_data]

counts = Counter((is_spam, spam_probability > 0.5) # (actual, predicted)


for _, is_spam, spam_probability in classified)

print(counts)

Continuação (def train_and_test_model(path))


classified.sort(key=lambda row: row[2])
spammiest_hams = list(filter(lambda row: not row[1], classified))[-5:]
hammiest_spams = list(filter(lambda row: row[1], classified))[:5]

print("spammiest_hams", spammiest_hams)
print("hammiest_spams", hammiest_spams)

words = sorted(classifier.word_probs, key=p_spam_given_word)

spammiest_words = words[-5:]
hammiest_words = words[:5]

print("spammiest_words", spammiest_words)
print("hammiest_words", hammiest_words)

Programa “principal”
if __name__ == "__main__":
#train_and_test_model(r"c:\spam\*\*")
train_and_test_model(r"/home/joel/src/spam/*/*")

Possibilidades para melhorar o modelo


• Analisar o conteúdo da mensagem e não apenas o Assunto;
• Considerar apenas palavras que aparecem um número mínimo de vezes (min_count);

5
• Utilizar apenas radicais das palavras (pesquise por “Porter Stemmer”);
• Considerar não apenas presença de palavras, mas outras características:
– Por exemplo, se a mensagem possuí números:
∗ A função tokenizer pode retornar tokens especiais para isso (por exemplo: contains:number).

Referências
• GRUS, Joel - Data Science do Zero: Primeiras Regras com Python, Editora Alta Books, 1a Edição, 2016;