Saltar al contenido principal

Detección de noticias falsas con scikit-learn

Este tutorial de scikit-learn te guía paso a paso para crear un clasificador de noticias falsas con ayuda de modelos bayesianos.
· 15 min leer

Explora con IA

datacamp graphic

Detectar las llamadas “noticias falsas” no es tarea fácil. Para empezar, hay que definir qué es una noticia falsa, algo que hoy tiene una carga política evidente. Si consigues acordar una definición, tendrás que recopilar y etiquetar correctamente noticias reales y falsas (idealmente sobre temas similares para ver mejor las diferencias). Una vez recopiladas, deberás encontrar características útiles para distinguir noticias falsas de reales.

Para profundizar en este problema, te recomiendo el artículo de Miguel Martinez-Alvarez, “How can Machine Learning and AI Help Solve the Fake News Problem”.

Por la misma época en que leí el artículo de Miguel, me encontré con una entrada en Open Data Science sobre construir un detector de noticias falsas con modelos bayesianos. El autor incluso creó un repositorio con el conjunto de datos de noticias falsas y reales etiquetadas. Me picó la curiosidad por ver si podía reproducir fácilmente los resultados y, después, entender qué había aprendido el modelo.

En este tutorial, recorrerás conmigo parte de esa exploración inicial y verás si puedes construir un detector de noticias falsas que funcione.

Consejo: si quieres repasar los fundamentos del procesamiento del lenguaje natural (NLP), échale un ojo al curso Natural Language Processing Fundamentals in Python.

Exploración de datos

Para empezar, conviene echar un vistazo rápido a los datos y entender su contenido. Para ello, usa un DataFrame de Pandas, revisa su shape, head y aplica las transformaciones necesarias.

# Import `fake_or_real_news.csv` 
df = __.________("https://s3.amazonaws.com/assets.datacamp.com/blog_assets/fake_or_real_news.csv")
    
# Inspect shape of `df` 
df._____

# Print first lines of `df`
df.______
# Set index
df = df.set_index("Unnamed: 0") 

# Print first lines of `df` 
df.______

Extracción de los datos de entrenamiento

Ahora que el DataFrame tiene el aspecto que necesitas, separa las etiquetas y prepara los conjuntos de entrenamiento y prueba.

En este cuaderno decidí centrarme en usar el texto largo de los artículos. Como sabía que iba a utilizar bag-of-words y TF-IDF (frecuencia de término–frecuencia inversa de documento) para extraer características, parecía una buena elección. Usar texto más largo debería permitir que aparezcan palabras y rasgos distintivos entre mis datos de noticias reales y falsas.

#Set `y` 
y = df._____ 
 
# Drop the `label` column 
df.____("label", axis=1) 
 
# Make training and test sets 
X_train, X_test, y_train, y_test = ________________(df['text'], y, test_size=0.33, random_state=53)

Creación de clasificadores con vectorizadores

Ahora que ya tienes los datos de entrenamiento y prueba, puedes construir tus clasificadores. Para comprobar si las palabras y tokens de los artículos influyen de forma significativa en que la noticia sea falsa o real, empieza usando CountVectorizer y TfidfVectorizer.

Verás que el ejemplo fija un umbral máximo de .7 para el vectorizador TF-IDF tfidf_vectorizer mediante el argumento max_df. Esto elimina las palabras que aparecen en más del 70% de los artículos. Además, el parámetro integrado stop_words eliminará las stop words en inglés antes de crear los vectores.

Hay muchos más parámetros disponibles y puedes consultarlos en la documentación de scikit-learn para TfidfVectorizer y CountVectorizer.

# Initialize the `count_vectorizer` 
count_vectorizer = _______________(stop_words='english') 

# Fit and transform the training data 
count_train = count_vectorizer._____________(X_train) 

# Transform the test set
count_test = count_vectorizer._________(X_test)
# Initialize the `tfidf_vectorizer`
tfidf_vectorizer = _______________(stop_words='english', max_df=0.7) 

# Fit and transform the training data 
tfidf_train = tfidf_vectorizer._____________(X_train) 

# Transform the test set 
tfidf_test = tfidf_vectorizer._________(X_test)

Con los vectores listos, puedes revisar las características de los vectores, almacenadas en count_vectorizer y tfidf_vectorizer.

¿Ves algún problema llamativo? (¡Sí!)

Está claro que en el conjunto de datos hay comentarios, medidas u otras palabras sin sentido, además de artículos en varios idiomas. Normalmente dedicarías más tiempo al preprocesado y a eliminar ruido, pero como este tutorial es una pequeña prueba de concepto, veremos si el modelo es capaz de sobreponerse al ruido y clasificar correctamente a pesar de estos problemas.

# Get the feature names of `tfidf_vectorizer` 
print(tfidf_vectorizer.___________()[-10:])

# Get the feature names of `count_vectorizer` 
print(count_vectorizer.___________()[:10])

Intermedio: características de recuento frente a TF-IDF

Me picaba la curiosidad por saber si mis vectorizadores de recuento y TF-IDF habían extraído tokens diferentes. Para comparar características, puedes volcar la información de los vectores a un DataFrame y usar comparaciones sencillas en Python.

Como verás al ejecutar las celdas, ambos vectorizadores extraen los mismos tokens, pero con pesos distintos. Probablemente, cambiar los max_df y min_df del vectorizador TF-IDF alteraría el resultado y daría lugar a características diferentes en cada uno.

In [15]:
 count_df = pd.DataFrame(count_train.A, columns=count_vectorizer.get_feature_names())
In [16]:
tfidf_df = pd.DataFrame(tfidf_train.A, columns=tfidf_vectorizer.get_feature_names())
In [17]:
difference = set(count_df.columns) - set(tfidf_df.columns)difference
Out[17]:
set()
In [18]:
print(count_df.equals(tfidf_df))
In [19]:
count_df.head()
Out[19]:
 000000000000000310000350000600010001pt000ft000km...حلبعربيعنلممامحاولاتمنهذاوالمرضىยงade
00000000000...0000000000
10000000000...0000000000
20000000000...0000000000
30000000000...0000000000
40000000000...0000000000

5 rows × 56922 columns

In [20]:
tfidf_df.head()
Out[20]:
 000000000000000310000350000600010001pt000ft000km...حلبعربيعنلممامحاولاتمنهذاوالمرضىยงade
00.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
10.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
20.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
30.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
40.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0

5 rows × 56922 columns

Comparación de modelos

Ahora toca entrenar y probar tus modelos.

Empezaremos con un clásico de NLP, MultinomialNB. Nos servirá para comparar TF-IDF frente a bag-of-words. Mi intuición es que bag-of-words (es decir, CountVectorizer) funcionará mejor con este modelo. (Si quieres leer más sobre la distribución multinomial y por qué funciona mejor con enteros, aquí tienes una explicación bastante concisa de un curso de estadística de UPenn).

Personalmente, me resulta más fácil comparar y leer matrices de confusión, así que usé la documentación de scikit-learn para generar unas matrices fáciles de interpretar (¡gracias, open source!). Una matriz de confusión muestra las etiquetas correctas en la diagonal principal (de arriba a la izquierda a abajo a la derecha). El resto de celdas muestran etiquetas incorrectas, a menudo llamadas falsos positivos o falsos negativos. Según el problema, una u otra puede ser más importante. Por ejemplo, en el problema de noticias falsas, ¿es más crítico no etiquetar como falsas las noticias reales? Si es así, quizá queramos ponderar la métrica de precisión para reflejar mejor esta preocupación.

Además de las matrices de confusión, scikit-learn incluye muchas formas de visualizar y comparar modelos. Una muy popular es usar la curva ROC. Hay muchas otras métricas para evaluar modelos en el módulo metrics de scikit-learn.

In [21]:
def plot_confusion_matrix(cm, classes,                          normalize=False,                          title='Confusion matrix',                          cmap=plt.cm.Blues):    """    See full source and example:     http://scikit-learn.org/stable/auto_examples/model_selection/plot_confusion_matrix.html        This function prints and plots the confusion matrix.    Normalization can be applied by setting `normalize=True`.    """    plt.imshow(cm, interpolation='nearest', cmap=cmap)    plt.title(title)    plt.colorbar()    tick_marks = np.arange(len(classes))    plt.xticks(tick_marks, classes, rotation=45)    plt.yticks(tick_marks, classes)    if normalize:        cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]        print("Normalized confusion matrix")    else:        print('Confusion matrix, without normalization')    thresh = cm.max() / 2.    for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):        plt.text(j, i, cm[i, j],                 horizontalalignment="center",                 color="white" if cm[i, j] > thresh else "black")    plt.tight_layout()    plt.ylabel('True label')    plt.xlabel('Predicted label')
In [22]:
clf = MultinomialNB()
In [23]:
 clf.fit(tfidf_train, y_train)pred = clf.predict(tfidf_test)score = metrics.accuracy_score(y_test, pred)print("accuracy:   %0.3f" % score)cm = metrics.confusion_matrix(y_test, pred, labels=['FAKE', 'REAL'])plot_confusion_matrix(cm, classes=['FAKE', 'REAL'])
accuracy:   0.857Confusion matrix, without normalization
 
confusion matrix 1
In [24]:
clf = MultinomialNB() 
In [25]:
clf.fit(count_train, y_train)pred = clf.predict(count_test)score = metrics.accuracy_score(y_test, pred)print("accuracy:   %0.3f" % score)cm = metrics.confusion_matrix(y_test, pred, labels=['FAKE', 'REAL'])plot_confusion_matrix(cm, classes=['FAKE', 'REAL'])
accuracy:   0.893Confusion matrix, without normalization
 
confusion matrix 2
 

Y, efectivamente, sin ajustar parámetros, tu conjunto de entrenamiento vectorizado por recuento, count_train, supera claramente a los vectores TF-IDF.

Prueba de modelos lineales

Hay muchísimos artículos que explican por qué los modelos lineales funcionan bien con TF-IDF (mira word2vec para clasificación, la referencia a SVM en el análisis de texto de scikit-learn y muchos más).

Entonces, ¿deberías usar una SVM?

Pues bien, recientemente vi la clase de Victor Lavrenko sobre clasificación de texto, donde compara clasificadores Passive Aggressive con SVM lineales para texto. Vamos a probar este enfoque (con importantes ventajas de velocidad y desventajas de aprendizaje permanente) con el dataset de noticias falsas.

In [26]:
linear_clf = PassiveAggressiveClassifier(n_iter=50)
In [27]:
 linear_clf.fit(tfidf_train, y_train)pred = linear_clf.predict(tfidf_test)score = metrics.accuracy_score(y_test, pred)print("accuracy:   %0.3f" % score)cm = metrics.confusion_matrix(y_test, pred, labels=['FAKE', 'REAL'])plot_confusion_matrix(cm, classes=['FAKE', 'REAL'])
accuracy:   0.936Confusion matrix, without normalization
confusion matrix 3

¡Vaya!

Impresiona. La matriz de confusión es distinta y el modelo clasifica un poco mejor las noticias falsas. Podemos probar si ajustar el valor de alpha en MultinomialNB produce resultados comparables. También puedes usar ajuste de parámetros con grid search para una búsqueda más exhaustiva.

In [28]:
clf = MultinomialNB(alpha=0.1)
In [29]:
 last_score = 0for alpha in np.arange(0,1,.1):    nb_classifier = MultinomialNB(alpha=alpha)    nb_classifier.fit(tfidf_train, y_train)    pred = nb_classifier.predict(tfidf_test)    score = metrics.accuracy_score(y_test, pred)    if score > last_score:        clf = nb_classifier    print("Alpha: {:.2f} Score: {:.5f}".format(alpha, score))
Alpha: 0.00 Score: 0.61502Alpha: 0.10 Score: 0.89766Alpha: 0.20 Score: 0.89383Alpha: 0.30 Score: 0.89000Alpha: 0.40 Score: 0.88570Alpha: 0.50 Score: 0.88427Alpha: 0.60 Score: 0.87470Alpha: 0.70 Score: 0.87040Alpha: 0.80 Score: 0.86609Alpha: 0.90 Score: 0.85892

Casi, pero no... A estas alturas, podría ser interesante ajustar parámetros en todos los clasificadores o explorar otros clasificadores bayesianos de scikit-learn. También podrías probar con una máquina de vectores de soporte (SVM) para ver si supera al clasificador Passive Aggressive.

Pero me interesa más qué ha aprendido realmente el modelo Passive Aggressive. Pasemos a la introspección.

Introspección de modelos

Así que, ¿ya está resuelto lo de las noticias falsas? Hemos logrado un 93% de precisión en mi conjunto de datos, ¿cerramos y nos vamos?

Ni mucho menos. Estos resultados me generan cautela, sobre todo por el ruido que vimos en las características. Hay una gran respuesta en StackOverflow con una función muy útil para encontrar los vectores que más influyen en las etiquetas. Solo funciona para clasificación binaria (dos clases), pero eso nos viene bien: aquí solo hay FAKE o REAL.

Usando tu mejor clasificador con el conjunto TF-IDF (tfidf_vectorizer) y el clasificador Passive Aggressive (linear_clf), inspecciona los 30 vectores principales para noticias falsas y reales:

In [30]:
FAKE -4.86382369883 2016FAKE -4.13847157932 hillaryFAKE -3.98994974843 octoberFAKE -3.10552662226 shareFAKE -2.99713810694 novemberFAKE -2.9150746075 articleFAKE -2.54532100449 printFAKE -2.47115243995 advertisementFAKE -2.35915304509 sourceFAKE -2.31585837413 emailFAKE -2.27985826579 electionFAKE -2.2736680857 octFAKE -2.25253568246 warFAKE -2.19663276969 mosulFAKE -2.17921304122 podestaFAKE -1.99361009573 novFAKE -1.98662624907 comFAKE -1.9452527887 establishmentFAKE -1.86869495684 corporateFAKE -1.84166664376 wikileaksFAKE -1.7936566878 26FAKE -1.75686475396 donaldFAKE -1.74951154055 snipFAKE -1.73298170472 mainstreamFAKE -1.71365596627 ukFAKE -1.70917804969 ayotteFAKE -1.70781651904 entireFAKE -1.68272667818 jewishFAKE -1.65334397724 youtubeFAKE -1.6241703128 pipelineREAL 4.78064061698 saidREAL 2.68703967567 tuesdayREAL 2.48309800829 gopREAL 2.45710670245 islamicREAL 2.44326123901 saysREAL 2.29424417889 cruzREAL 2.29144842597 marriageREAL 2.20500735471 candidatesREAL 2.19136552672 conservativeREAL 2.18030834903 mondayREAL 2.05688105375 attacksREAL 2.03476457362 rushREAL 1.9954523319 continueREAL 1.97002430576 fridayREAL 1.95034103105 conventionREAL 1.94620720989 senREAL 1.91185661202 jobsREAL 1.87501303774 debateREAL 1.84059602241 presumptiveREAL 1.80111133252 sayREAL 1.80027216061 sundayREAL 1.79650823765 marchREAL 1.79229792108 parisREAL 1.74587899553 securityREAL 1.69585506276 conservativesREAL 1.68860503431 recountsREAL 1.67424302821 dealREAL 1.67343398121 campaignREAL 1.66148582079 foxREAL 1.61425630518 attack

También puedes hacerlo de forma bastante directa con unas pocas líneas en Python, emparejando coeficientes y características y mirando la parte superior e inferior de la lista.

In [31]:
feature_names = tfidf_vectorizer.get_feature_names()
In [32]:
### Most realsorted(zip(clf.coef_[0], feature_names), reverse=True)[:20]
Out[32]:
[(-6.2573612147015822, 'trump'), (-6.4944530943126777, 'said'), (-6.6539784739838845, 'clinton'), (-7.0379446628670728, 'obama'), (-7.1465399833812278, 'sanders'), (-7.2153760086475112, 'president'), (-7.2665628057416169, 'campaign'), (-7.2875931446681514, 'republican'), (-7.3411184585990643, 'state'), (-7.3413571102479054, 'cruz'), (-7.3783124419854254, 'party'), (-7.4468806724578904, 'new'), (-7.4762888011545883, 'people'), (-7.547225599514773, 'percent'), (-7.5553074094582335, 'bush'), (-7.5801506339098932, 'republicans'), (-7.5855405012652435, 'house'), (-7.6344781725203141, 'voters'), (-7.6484824436952987, 'rubio'), (-7.6734836186463795, 'states')]
In [33]:
### Most fakesorted(zip(clf.coef_[0], feature_names))[:20]
Out[33]:
[(-11.349866225220305, '0000'), (-11.349866225220305, '000035'), (-11.349866225220305, '0001'), (-11.349866225220305, '0001pt'), (-11.349866225220305, '000km'), (-11.349866225220305, '0011'), (-11.349866225220305, '006s'), (-11.349866225220305, '007'), (-11.349866225220305, '007s'), (-11.349866225220305, '008s'), (-11.349866225220305, '0099'), (-11.349866225220305, '00am'), (-11.349866225220305, '00p'), (-11.349866225220305, '00pm'), (-11.349866225220305, '014'), (-11.349866225220305, '015'), (-11.349866225220305, '018'), (-11.349866225220305, '01am'), (-11.349866225220305, '020'), (-11.349866225220305, '023')]

Está claro que hay ciertas palabras que pueden reflejar intencionalidad política y fuente entre las características destacadas de falsas (por ejemplo, corporate y establishment).

Además, en las noticias reales aparece con más frecuencia el verbo "to say", probablemente porque en periódicos y publicaciones periodísticas se citan fuentes directamente (“La canciller alemana Angela Merkel dijo…”).

Para extraer la lista completa de tu clasificador actual y revisar cada token (o comparar fácilmente tokens entre clasificadores), puedes exportarla así:

In [34]:
tokens_with_weights = sorted(list(zip(feature_names, clf.coef_[0])))

Intermedio: HashingVectorizer

Otro vectorizador que a veces se usa para clasificación de texto es HashingVectorizer. Los HashingVectorizer requieren menos memoria y son más rápidos (porque producen matrices dispersas y usan hashes en lugar de tokens), pero son más difíciles de inspeccionar. Si te interesa, puedes leer más sobre sus pros y contras en la documentación de scikit-learn: HashingVectorizer.

Prueba a compararlo con los otros vectorizadores. Suele rendir bastante bien, con mejores resultados que TF-IDF usando MultinomialNB (algo esperable por las mismas razones por las que CountVectorizer funciona mejor), aunque no alcanza al TF-IDF con el algoritmo lineal Passive Aggressive.

In [35]:
hash_vectorizer = HashingVectorizer(stop_words='english', non_negative=True)hash_train = hash_vectorizer.fit_transform(X_train)hash_test = hash_vectorizer.transform(X_test)
In [36]:
clf = MultinomialNB(alpha=.01)
In [37]:
clf.fit(hash_train, y_train)pred = clf.predict(hash_test)score = metrics.accuracy_score(y_test, pred)print("accuracy:   %0.3f" % score)cm = metrics.confusion_matrix(y_test, pred, labels=['FAKE', 'REAL'])plot_confusion_matrix(cm, classes=['FAKE', 'REAL'])
accuracy:   0.902Confusion matrix, without normalization
 
confusion matrix 4
In [38]:
clf = PassiveAggressiveClassifier(n_iter=50)
In [39]:
clf.fit(hash_train, y_train)pred = clf.predict(hash_test)score = metrics.accuracy_score(y_test, pred)print("accuracy:   %0.3f" % score)cm = metrics.confusion_matrix(y_test, pred, labels=['FAKE', 'REAL'])plot_confusion_matrix(cm, classes=['FAKE', 'REAL'])
accuracy:   0.921Confusion matrix, without normalization
confusion matrix 5

Conclusión

Entonces, ¿tu experimento de clasificador de noticias falsas ha sido un éxito? En absoluto.

Pero, ¿has podido trastear con un nuevo conjunto de datos, probar algunos modelos de clasificación NLP e inspeccionar qué tal funcionaban? Sí.

Como cabía esperar desde el principio, definir noticias falsas con un simple bag-of-words o TF-IDF es un enfoque demasiado simplificado. Más aún con un conjunto multilingüe lleno de ruido. Si no hubieras mirado qué aprendió realmente tu modelo, podrías haber pensado que había captado algo significativo. Así que recuerda: inspecciona siempre tus modelos (¡en la medida de lo posible!).

Me gustaría saber si encuentras otras tendencias en los datos que se me hayan pasado. Publicaré una continuación sobre cómo se comparan distintos clasificadores en cuanto a características importantes en mi blog. Si investigas y encuentras algo interesante, comparte tus hallazgos y notas en los comentarios o contáctame en Twitter (soy @kjam).

¡Espero que te hayas divertido explorando conmigo un nuevo dataset de NLP!

Echa un vistazo al tutorial de DataCamp Python Machine Learning: Scikit-Learn Tutorial.

Temas