Spam Classifier com Python

Projeto prático onde construiremos um classificador de spam com Python, aplicável a SMS/EMAIL.

Faça parte da comunidade!

Hoje construiremos um classificador de spam utilizando Python. Por se tratar de dados em “texto”, ou strings, operaremos com NLP(Natural language processing), área do Machine Learning que atua com linguagem natural humana. Vamos lá?

A aquisição de dados em diferentes formas aumenta todo dia, incluindo dados numéricos, dados de texto, dados de imagem, aúdio, entre outros. Os dados numéricos são a principal fonte para a construção de modelos estatísticos e de aprendizado de máquina: sim, a máquina ama números, mas com o aumento dos dados de texto, as pessoas estão usando técnicas de processamento de linguagem natural(NLP) e extraindo informações significativas de dados de texto para obter mais insights que ajudam a tomar as principais decisões de negócios. 

Gerar insights a partir de dados de texto não é tão simples quanto gerar insights de dados numéricos porque os dados de texto não são estruturados. Para processar dados de texto, a primeira etapa é converter os dados de texto não estruturados em dados estruturados. Temos várias bibliotecas Python para extrair dados de texto: NLTK, Spacy, Blob… Usaremos Spacy por aqui.

PASSO 1: AQUISIÇÃO DE DADOS

Nem todos os emails/sms que chegam até nós aparecem na caixa de entrada. Muitos deles vão para pastas de spam ou até mesmo  lixo eletrônico. 

Já se perguntou como isso acontece? Como os emails são classificados e enviados para a caixa de entrada ou pasta de spam com base no texto do email? Antes de qualquer email/sms chegar à sua caixa de entrada, o Google(quando Android) está usando seu próprio classificador de sms/email, que identificará se o email/sms recebido precisa ser enviado para a caixa de entrada ou spam. 

O dataset você encontra no kaggle: https://www.kaggle.com/uciml/sms-spam-collection-dataset

Nós já hospedamos em nosso github o dataset e você não precisará baixá-lo ou ter dores de cabeça com o encoding-utf-8.

Usaremos a biblioteca Spacy para construir o classificador de sms/email. A principal vantagem do Spacy é que o código é bem otimizado, ele virá com muitas opções que nos ajudam a construir um modelo em muito menos tempo e com o mínimo de código. É interessante que, caso seja de seu interesse, crie um dataset em outros idiomas alvo para modelos preditivos serem capazes , e cada vez melhores, com emails em Português do Brasil, por exemplo.

# BIBLIOTECAS
import random
import spacy
import pandas as pd
import seaborn as sns
from spacy.util import minibatch
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.metrics import confusion_matrix
from matplotlib import pyplot as plt
# dados
data_path = "https://raw.githubusercontent.com/qodatecnologia/spam-data/main/spam-data.csv"
data = pd.read_csv(data_path)
print(data.head())

E nosso output será:

     v1  ... Unnamed: 4 
0   ham  ...        NaN 
1   ham  ...        NaN 
2  spam  ...        NaN 
3   ham  ...        NaN 
4   ham  ...        NaN 
[5 rows x 5 columns]

PASSO 2: ANÁLISE EXPLORATÓRIA

O conjunto de dados contém informações em 2 colunas, uma coluna contém o target e a outra coluna contém o texto.

target: ajuda a identificar se o texto é spam ou “ham”.
texto: o texto do email

Agora vamos verificar o número total de observações do conjunto de dados carregado e também entender a distribuição de spam e ham nos dados.

data = pd.read_csv(data_path)
observations = len(data.index)
print(f"Tamanho do Dataset: {observations}\n")
print(data['v1'].value_counts())
print()
print(data['v1'].value_counts() / len(data.index) * 100.0)
Tamanho do Dataset: 5572 
ham     4825 
spam     747 
Name: v1, dtype: int64 

ham     86.593683 
spam    13.406317 
Name: v1, dtype: float64

5572 amostras, sendo:

4825 “HAM”(86.5%),
747 “SPAM”(13.5%).

A distribuição diz que o conjunto de dados está tendo a maioria da população da classe HAM, e uma minoria como “SPAM”.

PASSO 3: PIPELINE SPACY

Para construir modelos com Spacy, você pode carregar os modelos de pipeline existentes ou criar um modelo vazio e adicionar as etapas de modelagem. Na 1ª linha, criamos o modelo vazio com spacy e passamos o idioma do dataset que é o inglês (en). Nas próximas linhas, estamos criando um pipeline dizendo que precisamos que esse modelo execute a classificação do texto. Utilizamos a arquitetura “bow” que, basicamente, executa “bag of words”. Em seguida, estamos adicionando o pipeline “text_cat” criado ao nosso modelo vazio. Nesta fase estamos tendo um modelo e estamos dizendo que este modelo deve realizar a classificação do texto utilizando a abordagem “bow”. Em seguida, estamos adicionando as classes target spam e ham ao modelo de categorização de texto criado. Não estamos executando nenhuma técnica de pré-processamento de texto com NLP pois foge do escopo deste tutorial e principalmente por recebermos o dataset já tratado: o que é raro no mundo real.

# Criamos um modelo vazio com o idioma do dataset
nlp = spacy.blank("en")

# Criamos agora um classificador de texto com classes exclusivas + arquitetura "bow" 
text_cat = nlp.create_pipe(
              "textcat",
              config={
                "exclusive_classes": True,
                "architecture": "bow"})

# Adicionamos o classificador a nosso modelo vazio
nlp.add_pipe(text_cat)

# Adicionamos as "classes exclusivas"
text_cat.add_label("ham")
text_cat.add_label("spam")

PASSO 4: TREINO/TESTE

Vamos dividir os dados carregados em TREINO/TESTE.

Conjunto de dados de treinamento: para treinar o modelo de categorização de texto.
Conjunto de dados de teste: para validar o desempenho do modelo.

Para dividir os dados em 2 conjuntos de dados, usaremos o train_test_split do scikit learn, de forma que os dados de teste representem 33% dos dados carregados.

x_train, x_test, y_train, y_test = train_test_split(
      data['v2'], data['v1'], test_size=0.33, random_state=7)

Ao contrário dos outros modelos do scikit-learn, você não pode passar o destino como uma única coluna para o spacy, precisamos criar explicitamente os destinos como uma lista booleana de True/False. Como para cada texto de e-mail, o target é True. Criamos onehot encoding para categorias target(o que queremos prever), onde estamos criando dois rótulos booleanos(True/False) e atribuindo True para o rótulo real e False para o outro rótulo. Agora temos features e targets para treinar o modelo, mas primeiro precisamos combinar as features e os targets em um único conjunto de dados para construir o modelo de classificação de e-mail. Estamos pegando as features (texto do e-mail), convertendo rótulos de treino (booleanos) e juntando-os usando o método zip, a mesma abordagem que estamos aplicando para conjuntos de dados de treinamento e teste.

train_lables = [{'cats': {'ham': label == 'ham',
                          'spam': label == 'spam'}}  for label in y_train]

test_lables = [{'cats': {'ham': label == 'ham',
                      'spam': label == 'spam'}}  for label in y_test]

# Spacy model data
train_data = list(zip(x_train, train_lables))
test_data = list(zip(x_test, test_lables))

Para cada época, estamos embaralhando os dados usando o método de embaralhamento “shuffle” e, em seguida, criando os batches(lotes de treinamento). 

Para cada lote atualizamos o modelo usando o otimizador e, no final, capturamos as loss functions.

model: modelo vazio já criado
train data: dados de treino
optimizer: Otimizador
batch size: Tamanho dos batches
epochs: épocas de treinamento

def train_model(model, train_data, optimizer, batch_size, epochs=10):
    losses = {}
    random.seed(1)

    for epoch in range(epochs):
        random.shuffle(train_data)

        batches = minibatch(train_data, size=batch_size)
        for batch in batches:
            # Split batch into texts and labels
            texts, labels = zip(*batch)

            # Update model with texts and labels
            model.update(texts, labels, sgd=optimizer, losses=losses)
        print("Loss: {}".format(losses['textcat']))

    return losses['textcat']
optimizer = nlp.begin_training()
batch_size = 5
epochs = 10

# Treinar!
train_model(nlp, train_data, optimizer, batch_size, epochs)
Loss: 3.9675071350220605
Loss: 4.947752909023897
Loss: 5.475570581757109
Loss: 5.8221962916606715
Loss: 6.0540166547841
Loss: 6.198435754179429
Loss: 6.297653166217902
Loss: 6.367637046391644
Loss: 6.42496641852461
Loss: 6.471268542697672

6.471268542697672

PASSO 5: PREDIÇÕES

Para o texto de e-mail abaixo, a saída real é “ham” e nosso modelo está tendo alta probabilidade de quase 99% para ham e 1% para spam. O que significa que nosso modelo está prevendo o texto do e-mail corretamente.

print(train_data[0])
sample_test = nlp(train_data[0][0])
print(sample_test.cats)

(‘Remember all those whom i hurt during days of satanic imposter in me.need to pay a price,so be it.may destiny keep me going and as u said pray that i get the mind to get over the same.’,
{‘cats’: {‘ham’: True, ‘spam’: False}})
{‘ham’: 0.9999992847442627, ‘spam’: 7.569611852886737e-07}

A função abaixo “get_predictions” usa dois parâmetros, um é o modelo e o outro é o texto do e-mail. Na 3ª linha da função, o texto é tokenizado e, em seguida, dividimos o conteúdo do e-mail e armazenamos em documentos. Nas próximas linhas chamamos o método “textcat” que criamos, usando o objeto textcat para prever a classe de emailham/spam. As Pontuações basicamente fornecem as probabilidades para ambas as classes. Para identificar a classe de rótulo, estamos pegando a probabilidade máxima usando o argmax, então estamos retornando as previsões.

def get_predictions(model, texts):
    # Tokenizar
    docs = [model.tokenizer(text) for text in texts]

    # textcat para verificar os scores
    textcat = model.get_pipe('textcat')
    scores, _ = textcat.predict(docs)

    # Mostramos os scores mais altos com argmax
    predicted_labels = scores.argmax(axis=1)
    predicted_class = [textcat.labels[label] for label in predicted_labels]

    return predicted_class

PASSO 6: AVALIAÇÃO DO MODELO

Para problemas supervisionados de classificação, podemos/devemos utilizar acurácia e a matriz de confusão.

# ACURÁCIA
train_predictions = get_predictions(nlp, x_train)
test_predictions = get_predictions(nlp, x_test)

train_accuracy = accuracy_score(y_train, train_predictions)
test_accuracy = accuracy_score(y_test, test_predictions)

print(f"Train accuracy: {train_accuracy}")
print(f"Test accuracy: {test_accuracy}")

Train accuracy: 0.998928475756764
Test accuracy: 0.9815116911364872

uau! 99% no treino e 98% nos testes: muito bom! Acreditamos que a missão por aqui foi cumprida, mas ainda falta uma última etapa: a matriz de confusão para treino e teste:

# MATRIZ DE CONFUSÃO TREINO
print("TREINO:")
cf_train_matrix = confusion_matrix(y_train, train_predictions)
plt.figure(figsize=(10,8))
sns.heatmap(cf_train_matrix, annot=True, fmt='d')
# MATRIZ DE CONFUSÃO TESTE
print("TESTE:")
cf_test_matrix = confusion_matrix(y_test, test_predictions)
plt.figure(figsize=(10,8))
sns.heatmap(cf_test_matrix, annot=True, fmt='d')

Passe adiante...!

Compartilhar no facebook
Compartilhar no linkedin
Compartilhar no twitter
Compartilhar no email
Compartilhar no whatsapp

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

7 + 18 =

Receba em seu email o acesso as aulas e materiais

Vá além:

Matplotlib: o guia inicial!

Matplotlib é um Módulo Python que serve pra gerar gráficos de maneira simples, mas, apesar de ser uma biblioteca compacta, o processo de fazer a