Se propone la construcción de un sistema de aprendizaje automático capaz de predecir si un correo determinado se corresponde con un correo de SPAM o no, para ello, se utilizará el siguiente conjunto de datos:
Antes de comenzar a realizar ninguna acción, debemos implementar una función que lea de disco los correos electrónicos que forman parte del conjunto de datos de manera que podamos visualizar el formato que tienen.
Implementa una función en Python que lea el conjunto de datos de correos electrónicos y las etiquetas asociados a ellos.
Pista: Revisa los archivos que se encuentran en el directorio del conjunto de datos, el archivo full/index contiene la etiqueta de los correos y la ruta al mismo.
defleer_correos(indice,num):withopen(indice, 'r')as f: labels = f.read().splitlines()# Leemos los correos de disco X = [] y = []for l in labels[:num]: label, email_path = l.split(' ../') y.append(label)withopen(email_path, errors='ignore')as f: X.append(f.read())return X, y
X , y =leer_correos('full/index', 10)print(X[0])From RickyAmes@aol.com Sun Apr 813:07:322007Return-Path:<RickyAmes@aol.com>Received:from129.97.78.23 ([211.202.101.74]) by speedy.uwaterloo.ca (8.12.8/8.12.5)with SMTP id l38H7G0I003017; Sun,8 Apr 200713:07:21-0400Received:from0.144.152.6 by 211.202.101.74; Sun,08 Apr 200719:04:48+0100Message-ID:<WYADCKPDFWWTWTXNFVUE@yahoo.com>From:"Tomas Jacobs"<RickyAmes@aol.com>Reply-To:"Tomas Jacobs"<RickyAmes@aol.com>To: the00@speedy.uwaterloo.caSubject: Generic Cialis, branded quality@Date: Sun,08 Apr 200721:00:48+0300X-Mailer: Microsoft Outlook Express 6.00.2600.0000MIME-Version:1.0Content-Type: multipart/alternative; boundary="--8896484051606557286"X-Priority:3X-MSMail-Priority: NormalStatus: ROContent-Length:988Lines:24----8896484051606557286Content-Type: text/html;Content-Transfer-Encoding: 7Bit<html><body bgcolor="#ffffff"><div style="border-color: #00FFFF; border-right-width: 0px; border-bottom-width: 0px; margin-bottom: 0px;" align="center">
<table style="border: 1px; border-style: solid; border-color:#000000;" cellpadding="5" cellspacing="0" bgcolor="#CCFFAA">
<tr><td style="border: 0px; border-bottom: 1px; border-style: solid; border-color:#000000;"><center>Do you feel the pressure to perform andnot rising to the occasion??<br></center></td></tr><tr><td bgcolor=#FFFF33 style="border: 0px; border-bottom: 1px; border-style: solid; border-color:#000000;"><center><b><a href='http://excoriationtuh.com/?lzmfnrdkleks'>Try <span>V</span><span>ia<span></span>gr<span>a</span>.....</a></b></center>
</td></tr><td><center>your anxiety will be a thing of the past and you will<br>be back to your old self.</center></td></tr></table></div></body></html>----8896484051606557286--print(y[0])spam
2. Procesamiento de texto HTML en los emails
En este caso práctico relacionado con la detección de correos electrónicos de SPAM, el conjunto de datos que disponemos esta formado por correos electrónicos, con sus correspondientes cabeceras y campos adicionales. Por lo tanto, requieren un preprocesamiento previo a que sean ingeridos por el algoritmo de Machine Learning.
Implementa una clase en Python 3 que permita procesar texto que contiene código HTML y elimine los tags HTML.
from html.parser import HTMLParserclassHTMLStripper(HTMLParser):def__init__(self):# inicializamos la clase padresuper().__init__()# Atributo que almacena los datos self.data = [] # Corregir aquí: self_data -> self.datadefhandle_data(self,data): self.data.append(data)
html_parser =HTMLStripper()html_parser.feed(X[0])print(''.join(html_parser.data))From RickyAmes@aol.com Sun Apr 813:07:322007Return-Path:Received:from129.97.78.23 ([211.202.101.74]) by speedy.uwaterloo.ca (8.12.8/8.12.5)with SMTP id l38H7G0I003017; Sun,8 Apr 200713:07:21-0400Received:from0.144.152.6 by 211.202.101.74; Sun,08 Apr 200719:04:48+0100Message-ID:From:"Tomas Jacobs"Reply-To:"Tomas Jacobs"To: the00@speedy.uwaterloo.caSubject: Generic Cialis, branded quality@Date: Sun,08 Apr 200721:00:48+0300X-Mailer: Microsoft Outlook Express 6.00.2600.0000MIME-Version:1.0Content-Type: multipart/alternative; boundary="--8896484051606557286"X-Priority:3X-MSMail-Priority: NormalStatus: ROContent-Length:988Lines:24----8896484051606557286Content-Type: text/html;Content-Transfer-Encoding: 7BitDo you feel the pressure to perform andnot rising to the occasion??Try Viagra.....your anxiety will be a thing of the past and you willbe back to your old self.----8896484051606557286--
print(strip_tags(X[0]))From RickyAmes@aol.com Sun Apr 813:07:322007Return-Path:Received:from129.97.78.23 ([211.202.101.74]) by speedy.uwaterloo.ca (8.12.8/8.12.5)with SMTP id l38H7G0I003017; Sun,8 Apr 200713:07:21-0400Received:from0.144.152.6 by 211.202.101.74; Sun,08 Apr 200719:04:48+0100Message-ID:From:"Tomas Jacobs"Reply-To:"Tomas Jacobs"To: the00@speedy.uwaterloo.caSubject: Generic Cialis, branded quality@Date: Sun,08 Apr 200721:00:48+0300X-Mailer: Microsoft Outlook Express 6.00.2600.0000MIME-Version:1.0Content-Type: multipart/alternative; boundary="--8896484051606557286"X-Priority:3X-MSMail-Priority: NormalStatus: ROContent-Length:988Lines:24----8896484051606557286Content-Type: text/html;Content-Transfer-Encoding: 7BitDo you feel the pressure to perform andnot rising to the occasion??Try Viagra.....your anxiety will be a thing of the past and you willbe back to your old self.----8896484051606557286--
3. Procesamiento de lenguaje natural
Además de eliminar los posibles tags HTML que se encuentren en el correo electrónico, deben realizarse otras acciones de preprocesamiento para evitar que los mensajes contengan ruido innecesario. Entre ellas se encuentra la eliminación de los signos de puntuación, eliminación de posibles campos del correo electrónico que no son relevantes o eliminación de los afijos de una palabra manteniendo únicamente la raiz de la misma (Stemming).
Explora e implementa diferentes funciones en Python 3 que permitan realizar los procesamientos que se indican en el texto anterior. Ten en cuenta que el texto de los correos electrónicos esta en Inglés.
Pista 1: Ten en cuenta que los correos electrónicos se encuentran en bruto y, por lo tanto, contienen valores que no nos van a resultar de interés, por ejemplo, las cabeceras o el pie del correo electrónico. Utiliza el paquete externo email para procesar los correos y eliminar todo menos el cuerpo del mismo.
dir(email)['base64mime','charset','encoders','errors','feedparser','header','headerregistry','iterators','message','message_from_binary_file','message_from_bytes','message_from_file','message_from_string',<---- Vamos a usar este método'parser','quoprimime','utils']parsed_mail = email.message_from_string(X[0])parsed_mail<email.message.Message at 0x106ad2c50>
cuerpo_email = parsed_mail.get_payload()
cuerpo_email[0].get_payload()'<html>\n<body bgcolor="#ffffff">\n<div style="border-color: #00FFFF; border-right-width: 0px; border-bottom-width: 0px; margin-bottom: 0px;" align="center">\n<table style="border: 1px; border-style: solid; border-color:#000000;" cellpadding="5" cellspacing="0" bgcolor="#CCFFAA">\n<tr>\n<td style="border: 0px; border-bottom: 1px; border-style: solid; border-color:#000000;">\n<center>\nDo you feel the pressure to perform and not rising to the occasion??<br>\n</center>\n</td></tr><tr>\n<td bgcolor=#FFFF33 style="border: 0px; border-bottom: 1px; border-style: solid; border-color:#000000;">\n<center>\n\n<b><a href=\'http://excoriationtuh.com/?lzmfnrdkleks\'>Try <span>V</span><span>ia<span></span>gr<span>a</span>.....</a></b></center>\n</td></tr><td><center>your anxiety will be a thing of the past and you will<br>\nbe back to your old self.\n</center></td></tr></table></div></body></html>\n\n'
strip_tags(cuerpo_email[0].get_payload())'\n\nDo you feel the pressure to perform and not rising to the occasion??\n\nTry Viagra.....\nyour anxiety will be a thing of the past and you will\nbe back to your old self.\n\n'
print(strip_tags(cuerpo_email[0].get_payload()))Do you feel the pressure to perform andnot rising to the occasion??Try Viagra.....your anxiety will be a thing of the past and you willbe back to your old self.
defget_body(correo):# Definimos una función interna que procese el cuerpodefparse_body(payload): body = []iftype(payload)isstr:return[payload]eliftype(payload)islist:for p in payload: body +=parse_body(p.get_payload())return body parsed_mail = email.message_from_string(correo)returnparse_body(parsed_mail.get_payload())
get_body(X[2])['Mega authenticV I A G R A $ DISCOUNT priceC I A L I S $DISCOUNT priceDo not miss IT, CLICK here.\nhttp://www.moujsjkhchum.com\n\n',
'<HTML><HEAD><TITLE>authentic viagra</TITLE></HEAD>\n<BODY>\nMega authentic<br>V I A G R A $ DISCOUNT price<br>C I A L I S $DISCOUNT price<br><a href="http://www.moujsjkhchum.com">Do not miss IT, CLICK here.</a>\n</BODY></HTML>\n']
Pista 2: Una vez que hayas eliminado todos los componentes del correo menos su cuerpo, explora la librería nltk para eliminar signos de puntuación y afijos. Revisa la clase PorterStemmer() de nltk así como los métodos y atributos nltk.corpus.stopwords.words('english'), string.punctuation y nltk.tokenize.word_tokenize
import stringstring.punctuation'!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'"Hola como estas; me llamo Alex...".split()# Los signos de puntuación están pegados a las palabras['Hola','como','estas;','me','llamo','Alex...']from nltk.tokenize import word_tokenizefrom nltk.tokenize import word_tokenizeword_tokenize("Hola como estas; me llamo Alex...")# De esta manera separa los signos de puntuación['Hola','como','estas',';','me','llamo','Alex','...']
4. Procesamiento de texto HTML en los emails
Pon todo lo anterior en común y construye una clase EmailParser que procese los emails aplicando todas las transformaciones que hemos implementado en las secciones anteriores.
Implementa una clase en Python 3 que contenga toda la funcionalidad que hemos visto en el apartado anterior. Implementa una función que permita leer correos electrónicos y aplicarles las transformaciones de manera simultánea.
import emailimport refrom nltk.tokenize import word_tokenizefrom string import punctuationfrom nltk import PorterStemmerfrom nltk.corpus import stopwordsfrom html.parser import HTMLParserclassEmailParser:defparse(self,correo):# Obtenemos el cuerpo del email parsed_mail =" ".join(self.get_body(correo))# Eliminar los tags html parsed_mail = self.strip_tags(parsed_mail)# Eliminar las URLs parsed_mail = self.remove_urls(parsed_mail)# Transformamos el texto en tokens parsed_mail =word_tokenize(parsed_mail)# Hacemos stemming parsed_mail = self.clean_text(parsed_mail)return' '.join(parsed_mail)defget_body(self,correo):# Definimos una función interna que procese el cuerpo parsed_mail = email.message_from_string(correo)return self._parse_body(parsed_mail.get_payload())def_parse_body(self,payload): body = []iftype(payload)isstr:return[payload]eliftype(payload)islist:for p in payload: body += self._parse_body(p.get_payload())return bodydefstrip_tags(self,correo): html_stripper =HTMLStripper() html_stripper.feed(correo)return''.join(html_stripper.data)defremove_urls(self,correo):return re.sub(r'http\S+', '', correo)defclean_text(self,correo): parsed_mail = [] st =PorterStemmer() punct =list(punctuation)+ ['\n','\t']for word in correo:if word notin stopwords.words('english')and word notin punct:#Aplicamos stemming parsed_mail.append(st.stem(word))return parsed_mailclassHTMLStripper(HTMLParser):def__init__(self):super().__init__() self.data = []defhandle_data(self,d): self.data.append(d)defget_data(self):return''.join(self.data)
parser =EmailParser()parser.parse(X[0])'do feel pressur perform rise occas tri viagra ..... anxieti thing past back old self'
defcrear_dataset(indice,num): email_parser =EmailParser() X, y =leer_correos(indice, num) X_proc = []for i, email inzip(range(len(X)), X):print('\rParsing email: {0}'.format(i+1), end='') X_proc.append(email_parser.parse(email))return X_proc, y
X, y =crear_dataset('full/index', 30)Parsing email:30X[0]'do feel pressur perform rise occas tri viagra ..... anxieti thing past back old self'y[0]'spam'X[25]'your histori show last order readi refil to visit pleas visit site complet order becaus order us previous prescript pre-approv thank sam mcfarland custom servic'
5. Codificando el conjunto de datos
Con las funciones presentadas anteriormente se permite la lectura de los correos electrónicos de manera programática y el procesamiento de los mismos para eliminar aquellos componentes que no resultan de utilidad para la detección de correos de SPAM. Sin embargo, cada uno de los correos sigue estando representado por un string.
La mayoría de los algoritmos de Machine Learning no son capaces de ingerir texto como parte del conjunto de datos. Por lo tanto, deben aplicarse una serie de funciones adicionales que transformen el texto de los correos electrónicos parseados en una representación numérica.
Aplica alguna técnica de codificación/vectorización sobre los correos electrónicos parseados para transformar el texto en una representación numérica.
Pista: Revisa la clase CountVectorizer de Sklearn para realizar la codificación.
X[0]'do feel pressur perform rise occas tri viagra ..... anxieti thing past back old self'X[2]'mega authenticv i a g r a discount pricec i a l i s discount pricedo miss it click authent viagra mega authenticv i a g r a discount pricec i a l i s discount pricedo miss it click'
X[10]'good day visit new onlin drug store save upto 85 today special offer viagra for as low as 1.62 per dose ciali super viagra for as low as 4.38 per dose levitra for as low as 4.44 per dose ... much much special offer today.y need 15 minut to be readi for action most need medic avail viagra ciali levitra propecia much much free ship worlwid no doctor visit no prescript full custom satisfactionclick visit new drugstor best regard good day visit new onlin drug store save= upto 85 today special offer viagra for as low as 1.62 per dose br ciali super viagra for as low as 4.38 per dose levitra for as low as 4.44 per dose ... much much special offer today you need 15 minut to be readi for action most need medic avail viagra ciali levitra prope= cia much much free ship worlwid no doctor visit no prescript full custom satisfact click visit new drugstor best regard'
from sklearn.feature_extraction.text import CountVectorizervectorizer =CountVectorizer()vectorizer.fit([X[0], X[2], X[10]])vectorizer.get_feature_names_out()array(['15', '38', '44', '62', '85', 'action', 'anxieti', 'as', 'authent','authenticv', 'avail', 'back', 'be', 'best', 'br', 'cia', 'ciali','click', 'custom', 'day', 'discount', 'do', 'doctor', 'dose','drug', 'drugstor', 'feel', 'for', 'free', 'full', 'good', 'it','levitra', 'low', 'medic', 'mega', 'minut', 'miss', 'most', 'much','need', 'new', 'no', 'occas', 'offer', 'old', 'onlin', 'past','per', 'perform', 'prescript', 'pressur', 'pricec', 'pricedo','prope', 'propecia', 'readi', 'regard', 'rise', 'satisfact','satisfactionclick', 'save', 'self', 'ship', 'special', 'store','super', 'thing', 'to', 'today', 'tri', 'upto', 'viagra', 'visit','worlwid', 'you'], dtype=object)X_vect = vectorizer.transform([X[0]])# Le pasamos un mail como una lista []X_vect.toarray()# Esto nos devuelve un array de 0 y 1, donde 1 es la palabra que se repite en los emails, vemos que la primera que se repite es en la posición 7 que se corresponde con anxieti
array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0,0, 1, 0, 0, 1, 0, 1, 0, 0, 0]])X[0]'do feel pressur perform rise occas tri viagra ..... anxieti thing past back old self'
6. Entrenamiento del algoritmo
¡Enhorabuena! Ya tienes la parte más complicada del ejercicio realizada. En este apartado vamos a entrenar un algoritmo de Machine Learning que aprenderá de los vectores anteriores a clasificar los correos en spam y legítimos.
Utiliza el algoritmo de Machine Learning LogisticRegression para clasificar entre correos electrónicos de spam y legítimos. Puedes encontrar la implementación de este algoritmo en Sklearn.
Pista 1: Comienza leyendo un número de correos pequeño para que no tarde demasiado tiempo, por ejemplo 100 correos. Aplica todas las transformaciones que hemos implementado anteriormente sobre ellos.
# Leemos un subconjunto de 100 correosX, y =crear_dataset('full/index', 100)
Pista 2: Aplica la vectorización al conjunto de datos para representar los correos de manera numérica.
Podemos leer estos datos de una manera más visual usando Pandas:
import pandas as pdpd.DataFrame(X_vect.toarray(), columns=[vectorizer.get_feature_names_out()])
Pista 3: Entrena el algoritmo LogisticRegression de Sklearn. Este algoritmo se basa en aprendizaje supervisado y funciona exactamente igual que el algoritmo Perceptron que hemos presentado en secciones anteriores.
from sklearn.linear_model import LogisticRegressionclf =LogisticRegression()clf.fit(X_vect, y)
7. Predicción
¡Muy bien, ya casi has terminado! Ya tenemos nuestro algoritmo entrenado y listo para realizar predicciones, lo único que nos queda es probar que tal se comporta para correos que no ha visto nunca.
Utiliza el algoritmo de Machine Learning LogisticRegression para predecir si un nuevo correo que no ha visto nunca es spam o legitimo.
Pista: Revisa el método predict() que tiene la clase LogisticRegression. Ten en cuenta que cuando recibas un nuevo correo para el que quieres realizar una predicción debes aplicar sobre él todas las transformaciones que hemos realizado anteriormente. Al aplicar la vectorización utiliza únicamente el método transform() del objeto CountVectorizer.
# Leemos 150 correos de nuestro conjunto de datos y nos quedamos solo con los últimos 50# Estos últimos 50 correos no se han utilizado en el entrenamiento del algoritmoX, y =crear_dataset('full/index', 150)Parsing email:150len(X)150X_test = X[100:]y_test = y[100:]len(X_test)50# Aplicamos CountVectorizerX_test = vectorizer.transform(X_test)X_test.toarray()array([[0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 1, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]])# Predicción para un correoy_pred = clf.predict(X_test)print('Predicción:', y_pred)print('Etiquetas reales:', y_test)Predicción: ['spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''ham''spam''spam''spam''spam''spam''ham''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam''spam']Etiquetas reales: ['spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'ham', 'spam', 'spam', 'spam', 'spam', 'spam', 'ham', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'ham', 'spam', 'spam', 'ham', 'spam', 'spam', 'ham', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam', 'spam']
8. Evaluación de resultados
Pista: Utiliza una métrica para evaluar los resultados. En este caso, utiliza la métrica accuracy_score. Puedes encontrar esta métrica implementada en Sklearn.
from sklearn.metrics import accuracy_scoreprint('Precisión: {:.3f}'.format(accuracy_score(y_test, y_pred)))Precisión:0.940
Vemos que el algoritmo ha acertado en un 94%, lo que es un porcentaje altísimo