Transformador de columnas con fuentes de datos heterogéneas

Los conjuntos de datos pueden contener a menudo componentes que requieren diferente extracción de características y pipeline de procesamiento. Este escenario puede ocurrir cuando:

  1. tu conjunto de datos está formado por tipos de datos heterogéneos (por ejemplo, imágenes rasterizadas y leyendas de texto),

  2. su conjunto de datos se almacena en un pandas.DataFrame y las diferentes columnas requieren diferentes pipelines de procesamiento.

Este ejemplo demuestra cómo utilizar ColumnTransformer en un conjunto de datos que contiene diferentes tipos de características. La elección de las características no es especialmente útil, pero sirve para ilustrar la técnica.

# Author: Matt Terry <matt.terry@gmail.com>
#
# License: BSD 3 clause

import numpy as np

from sklearn.preprocessing import FunctionTransformer
from sklearn.datasets import fetch_20newsgroups
from sklearn.decomposition import TruncatedSVD
from sklearn.feature_extraction import DictVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics import classification_report
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.svm import LinearSVC

Conjunto de datos de 20 grupos de noticias

Utilizaremos el conjunto de datos 20 newsgroups, que comprende mensajes de grupos de noticias sobre 20 temas. Este conjunto de datos se divide en subconjuntos de entrenamiento y de prueba basados en los mensajes publicados antes y después de una fecha específica. Sólo utilizaremos mensajes de dos categorías para acelerar el tiempo de ejecución.

categories = ['sci.med', 'sci.space']
X_train, y_train = fetch_20newsgroups(random_state=1,
                                      subset='train',
                                      categories=categories,
                                      remove=('footers', 'quotes'),
                                      return_X_y=True)
X_test, y_test = fetch_20newsgroups(random_state=1,
                                    subset='test',
                                    categories=categories,
                                    remove=('footers', 'quotes'),
                                    return_X_y=True)

Cada característica comprende meta información sobre esa entrada, como el tema, y el cuerpo de la noticia.

print(X_train[0])

Out:

From: mccall@mksol.dseg.ti.com (fred j mccall 575-3539)
Subject: Re: Metric vs English
Article-I.D.: mksol.1993Apr6.131900.8407
Organization: Texas Instruments Inc
Lines: 31




American, perhaps, but nothing military about it.  I learned (mostly)
slugs when we talked English units in high school physics and while
the teacher was an ex-Navy fighter jock the book certainly wasn't
produced by the military.

[Poundals were just too flinking small and made the math come out
funny; sort of the same reason proponents of SI give for using that.]

--
"Insisting on perfect safety is for people who don't have the balls to live
 in the real world."   -- Mary Shafer, NASA Ames Dryden

Creación de transformadores

En primer lugar, queremos un transformador que extraiga el asunto y el cuerpo de cada mensaje. Como se trata de una transformación sin estado (no requiere información de estado de los datos de entrenamiento), podemos definir una función que realice la transformación de los datos y luego utilizar FunctionTransformer para crear un transformador de scikit-learn.

def subject_body_extractor(posts):
    # construct object dtype array with two columns
    # first column = 'subject' and second column = 'body'
    features = np.empty(shape=(len(posts), 2), dtype=object)
    for i, text in enumerate(posts):
        # temporary variable `_` stores '\n\n'
        headers, _, body = text.partition('\n\n')
        # store body text in second column
        features[i, 1] = body

        prefix = 'Subject:'
        sub = ''
        # save text after 'Subject:' in first column
        for line in headers.split('\n'):
            if line.startswith(prefix):
                sub = line[len(prefix):]
                break
        features[i, 0] = sub

    return features


subject_body_transformer = FunctionTransformer(subject_body_extractor)

También crearemos un transformador que extraiga la longitud del texto y el número de frases.

def text_stats(posts):
    return [{'length': len(text),
             'num_sentences': text.count('.')}
            for text in posts]


text_stats_transformer = FunctionTransformer(text_stats)

Pipeline de clasificación

El siguiente pipeline extrae el asunto y el cuerpo de cada entrada utilizando SubjectBodyExtractor, produciendo un arreglo ( n_samples, 2). Este arreglo se utiliza para calcular las características estándar de la bolsa de palabras para el tema y el cuerpo, así como la longitud del texto y el número de frases en el cuerpo, utilizando ColumnTransformer. Las combinamos, con pesos, y luego entrenamos un clasificador en el conjunto combinado de características.

pipeline = Pipeline([
    # Extract subject & body
    ('subjectbody', subject_body_transformer),
    # Use ColumnTransformer to combine the subject and body features
    ('union', ColumnTransformer(
        [
            # bag-of-words for subject (col 0)
            ('subject', TfidfVectorizer(min_df=50), 0),
            # bag-of-words with decomposition for body (col 1)
            ('body_bow', Pipeline([
                ('tfidf', TfidfVectorizer()),
                ('best', TruncatedSVD(n_components=50)),
            ]), 1),
            # Pipeline for pulling text stats from post's body
            ('body_stats', Pipeline([
                ('stats', text_stats_transformer),  # returns a list of dicts
                ('vect', DictVectorizer()),  # list of dicts -> feature matrix
            ]), 1),
        ],
        # weight above ColumnTransformer features
        transformer_weights={
            'subject': 0.8,
            'body_bow': 0.5,
            'body_stats': 1.0,
        }
    )),
    # Use a SVC classifier on the combined features
    ('svc', LinearSVC(dual=False)),
], verbose=True)

Por último, ajustamos nuestro pipeline a los datos de entrenamiento y lo utilizamos para predecir los temas de X_test. A continuación, se imprimen las métricas de rendimiento de nuestro pipeline.

pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)
print('Classification report:\n\n{}'.format(
    classification_report(y_test, y_pred))
)

Out:

[Pipeline] ....... (step 1 of 3) Processing subjectbody, total=   0.0s
[Pipeline] ............. (step 2 of 3) Processing union, total=   0.7s
[Pipeline] ............... (step 3 of 3) Processing svc, total=   0.0s
Classification report:

              precision    recall  f1-score   support

           0       0.84      0.87      0.86       396
           1       0.87      0.83      0.85       394

    accuracy                           0.85       790
   macro avg       0.85      0.85      0.85       790
weighted avg       0.85      0.85      0.85       790

Tiempo total de ejecución del script: ( 0 minutos 3.151 segundos)

Galería generada por Sphinx-Gallery