Introducción a PyTorch para clasificación

PyTorch, junto con Tensorflow, es una biblioteca de aprendizaje profundo extremadamente popular para Python. En este artículo, cubriremos una de las tareas básicas en el aprendizaje automático: la clasificación.

Las bibliotecas PyTorch y TensorFlow son dos de las bibliotecas de Python más utilizadas para el aprendizaje profundo. PyTorch es desarrollado por Facebook, mientras que TensorFlow es un proyecto de Google. En este artículo, verá cómo se puede usar la biblioteca PyTorch para resolver problemas de clasificación.

Los problemas de clasificación pertenecen a la categoría de problemas de aprendizaje automático en los que, dado un conjunto de características, la tarea es predecir un valor discreto. Predecir si un tumor es canceroso o no, o si es probable que un estudiante apruebe o suspenda el examen, son algunos de los ejemplos comunes de problemas de clasificación.

En este artículo, dadas ciertas características de un cliente bancario, predeciremos si es probable que el cliente abandone el banco después de 6 meses. El fenómeno en el que un cliente deja una organización también se denomina rotación de clientes. Por lo tanto, nuestra tarea es predecir la rotación de clientes en función de varias características de los clientes.

Antes de continuar, se supone que tiene una competencia de nivel intermedio con el lenguaje de programación Python y ha instalado la biblioteca PyTorch. Además, el conocimiento de los conceptos básicos de aprendizaje automático puede ayudar. Si no ha instalado PyTorch, puede hacerlo con el siguiente comando pip:

1
$ pip install pytorch

El conjunto de datos

El conjunto de datos que vamos a utilizar en este artículo está disponible gratuitamente en este Enlace Kaggle. Importemos las bibliotecas requeridas y el conjunto de datos en nuestra aplicación de Python:

1
2
3
4
5
6
7
import torch
import torch.nn as nn
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

Podemos usar el método read_csv() de la biblioteca pandas para importar el archivo CSV que contiene nuestro conjunto de datos.

1
dataset = pd.read_csv(r'E:Datasets\customer_data.csv')

Imprimamos la forma de nuestro conjunto de datos:

1
dataset.shape

Producción:

1
(10000, 14)

El resultado muestra que el conjunto de datos tiene 10 mil registros y 14 columnas.

Podemos usar el método head() del marco de datos pandas para imprimir las primeras cinco filas de nuestro conjunto de datos.

1
dataset.head()

Producción:

first five rows of the dataset

Puede ver las 14 columnas en nuestro conjunto de datos. Basándonos en las primeras 13 columnas, nuestra tarea es predecir el valor de la columna 14, es decir, “Salida”. Es importante mencionar que los valores de las primeras 13 columnas se registran 6 meses antes de que se obtenga el valor de la columna ‘Salido’, ya que la tarea es predecir la rotación de clientes después de 6 meses desde el momento en que se registra la información del cliente.

Análisis de datos exploratorios {#análisis de datos exploratorios}

Realicemos algunos análisis de datos exploratorios en nuestro conjunto de datos. Primero predeciremos la proporción de clientes que realmente abandonaron el banco después de 6 meses y usaremos un gráfico circular para visualizar.

Primero aumentemos el tamaño de trazado predeterminado para los gráficos:

1
2
3
4
fig_size = plt.rcParams["figure.figsize"]
fig_size[0] = 10
fig_size[1] = 8
plt.rcParams["figure.figsize"] = fig_size

La siguiente secuencia de comandos dibuja el gráfico circular para la columna Salida.

1
dataset.Exited.value_counts().plot(kind='pie', autopct='%1.0f%%', colors=['skyblue', 'orange'], explode=(0.05, 0.05))

Producción:

dibujando un gráfico circular para la columna de salida

El resultado muestra que en nuestro conjunto de datos, el 20% de los clientes abandonaron el banco. Aquí 1 pertenece al caso en que el cliente abandonó el banco, donde 0 se refiere al escenario en el que un cliente no abandonó el banco.

Tracemos el número de clientes de todas las ubicaciones geográficas en el conjunto de datos:

1
sns.countplot(x='Geography', data=dataset)

Producción:

trazando números de clientes de todas las ubicaciones

La salida muestra que casi la mitad de los clientes pertenecen a Francia, mientras que la proporción de clientes pertenecientes a España y Alemania es del 25% cada uno.

Ahora representemos el número de clientes de cada ubicación geográfica única junto con la información de abandono de clientes. Podemos usar la función countplot() de la biblioteca seaborn para hacerlo.

1
sns.countplot(x='Exited', hue='Geography', data=dataset)

Producción:

trazando números de clientes de ubicaciones únicas

El resultado muestra que aunque el número total de clientes franceses es el doble que el número de clientes españoles y alemanes, la proporción de clientes que abandonaron el banco es la misma para los clientes franceses y alemanes. De manera similar, el número total de clientes alemanes y españoles es el mismo, pero el número de clientes alemanes que abandonaron el banco es el doble que el de los clientes españoles, lo que muestra que es más probable que los clientes alemanes abandonen el banco después de 6 meses.

En este artículo no trazaremos visualmente la información relacionada con el resto de las columnas de nuestro conjunto de datos, pero si quieres hacerlo, consulta mi artículo sobre [cómo realizar análisis de datos exploratorios con Python Seaborn Library](/biblioteca -seaborn-para-visualizacion-de-datos-en-python-parte-1/).

Preprocesamiento de datos {#preprocesamiento de datos}

Antes de entrenar nuestro modelo PyTorch, necesitamos preprocesar nuestros datos. Si observa el conjunto de datos, verá que tiene dos tipos de columnas: numéricas y categóricas. Las columnas numéricas contienen información numérica. Puntuación crediticia, Saldo, Edad, etc. Del mismo modo, Geografía y Género son columnas categóricas, ya que contienen información categórica, como la ubicación y el género de los clientes. Hay algunas columnas que se pueden tratar como numéricas y categóricas. Por ejemplo, la columna HasCrCard puede tener 1 o 0 como valores. Sin embargo, las columnas HasCrCard contienen información sobre si un cliente tiene o no una tarjeta de crédito. Se recomienda que la columna que se puede tratar como categórica y numérica, se trate como categórica. Sin embargo, depende totalmente del conocimiento del dominio del conjunto de datos.

Imprimamos nuevamente todas las columnas en nuestro conjunto de datos y averigüemos cuáles de las columnas se pueden tratar como numéricas y qué columnas se deben tratar como categóricas. El atributo columns de un marco de datos imprime todos los nombres de las columnas:

1
dataset.columns

Producción:

1
2
3
4
Index(['RowNumber', 'CustomerId', 'Surname', 'CreditScore', 'Geography',
       'Gender', 'Age', 'Tenure', 'Balance', 'NumOfProducts', 'HasCrCard',
       'IsActiveMember', 'EstimatedSalary', 'Exited'],
      dtype='object')

De las columnas en nuestro conjunto de datos, no usaremos las columnas RowNumber, CustomerId y Surname ya que los valores de estas columnas son totalmente aleatorios y no tienen relación con la salida. Por ejemplo, el apellido de un cliente no influye en si el cliente abandonará o no el banco. Entre el resto de las columnas, las columnas Geografía, Género, HasCrCard y IsActiveMember se pueden tratar como columnas categóricas. Vamos a crear una lista de estas columnas:

1
categorical_columns = ['Geography', 'Gender', 'HasCrCard', 'IsActiveMember']

Todas las columnas restantes, excepto la columna Salida, se pueden tratar como columnas numéricas.

1
numerical_columns = ['CreditScore', 'Age', 'Tenure', 'Balance', 'NumOfProducts', 'EstimatedSalary']

Finalmente, la salida (los valores de la columna Exited) se almacenan en la variable outputs.

1
outputs = ['Exited']

Hemos creado listas de columnas categóricas, numéricas y de salida. Sin embargo, por el momento el tipo de las columnas categóricas no es categórico. Puede verificar el tipo de todas las columnas en el conjunto de datos con el siguiente script:

1
dataset.dtypes

Producción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
RowNumber            int64
CustomerId           int64
Surname             object
CreditScore          int64
Geography           object
Gender              object
Age                  int64
Tenure               int64
Balance            float64
NumOfProducts        int64
HasCrCard            int64
IsActiveMember       int64
EstimatedSalary    float64
Exited               int64
dtype: object

Puede ver que el tipo de las columnas Geografía y Género es objeto y el tipo de las columnas HasCrCard y IsActive es int64. Necesitamos convertir los tipos de columnas categóricas a categoría. Podemos hacerlo usando la función astype(), como se muestra a continuación:

1
2
for category in categorical_columns:
    dataset[category] = dataset[category].astype('category')

Ahora, si vuelve a trazar los tipos de las columnas en nuestro conjunto de datos, debería ver los siguientes resultados:

1
dataset.dtypes

Producción

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
RowNumber             int64
CustomerId            int64
Surname              object
CreditScore           int64
Geography          category
Gender             category
Age                   int64
Tenure                int64
Balance             float64
NumOfProducts         int64
HasCrCard          category
IsActiveMember     category
EstimatedSalary     float64
Exited                int64
dtype: object

Veamos ahora todas las categorías en la columna Geografía:

1
dataset['Geography'].cat.categories

Producción:

1
Index(['France', 'Germany', 'Spain'], dtype='object')

Cuando cambia el tipo de datos de una columna a categoría, a cada categoría de la columna se le asigna un código único. Por ejemplo, tracemos las primeras cinco filas de la columna Geografía e imprimamos los valores de código para las primeras cinco filas:

1
dataset['Geography'].head()

Producción:

1
2
3
4
5
6
7
0    France
1     Spain
2    France
3    France
4     Spain
Name: Geography, dtype: category
Categories (3, object): [France, Germany, Spain]

El siguiente script traza los códigos para los valores en las primeras cinco filas de la columna Geografía:

1
dataset['Geography'].head().cat.codes

Producción:

1
2
3
4
5
6
0    0
1    2
2    0
3    0
4    2
dtype: int8

El resultado muestra que Francia se ha codificado como 0 y España se ha codificado como 2.

El propósito básico de separar las columnas categóricas de las columnas numéricas es que los valores de la columna numérica se puedan introducir directamente en las redes neuronales. Sin embargo, los valores de las columnas categóricas primero deben convertirse en tipos numéricos. La codificación de los valores en la columna categórica resuelve parcialmente la tarea de conversión numérica de las columnas categóricas.

Dado que usaremos PyTorch para el entrenamiento de modelos, debemos convertir nuestras columnas categóricas y numéricas en tensores.

Primero, convirtamos las columnas categóricas en tensores. En PyTorch, los tensores se pueden crear a través de matrices numpy. Primero convertiremos los datos de las cuatro columnas categóricas en matrices numpy y luego apilaremos todas las columnas horizontalmente, como se muestra en el siguiente script:

1
2
3
4
5
6
7
8
geo = dataset['Geography'].cat.codes.values
gen = dataset['Gender'].cat.codes.values
hcc = dataset['HasCrCard'].cat.codes.values
iam = dataset['IsActiveMember'].cat.codes.values

categorical_data = np.stack([geo, gen, hcc, iam], 1)

categorical_data[:10]

El script anterior imprime los primeros diez registros de las columnas categóricas, apilados horizontalmente. La salida es la siguiente:

Producción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
array([[0, 0, 1, 1],
       [2, 0, 0, 1],
       [0, 0, 1, 0],
       [0, 0, 0, 0],
       [2, 0, 1, 1],
       [2, 1, 1, 0],
       [0, 1, 1, 1],
       [1, 0, 1, 0],
       [0, 1, 0, 1],
       [0, 1, 1, 1]], dtype=int8)

Ahora, para crear un tensor a partir de la matriz numpy antes mencionada, simplemente puede pasar la matriz a la clase tensor del módulo torch. Recuerde, para las columnas categóricas, el tipo de datos debe ser torch.int64.

1
2
categorical_data = torch.tensor(categorical_data, dtype=torch.int64)
categorical_data[:10]

Producción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
tensor([[0, 0, 1, 1],
        [2, 0, 0, 1],
        [0, 0, 1, 0],
        [0, 0, 0, 0],
        [2, 0, 1, 1],
        [2, 1, 1, 0],
        [0, 1, 1, 1],
        [1, 0, 1, 0],
        [0, 1, 0, 1],
        [0, 1, 1, 1]])

En el resultado, puede ver que la matriz numpy de datos categóricos ahora se ha convertido en un objeto tensor.

De la misma manera, podemos convertir nuestras columnas numéricas a tensores:

1
2
3
numerical_data = np.stack([dataset[col].values for col in numerical_columns], 1)
numerical_data = torch.tensor(numerical_data, dtype=torch.float)
numerical_data[:5]

Producción:

1
2
3
4
5
tensor([[6.1900e+02, 4.2000e+01, 2.0000e+00, 0.0000e+00, 1.0000e+00, 1.0135e+05],
        [6.0800e+02, 4.1000e+01, 1.0000e+00, 8.3808e+04, 1.0000e+00, 1.1254e+05],
        [5.0200e+02, 4.2000e+01, 8.0000e+00, 1.5966e+05, 3.0000e+00, 1.1393e+05],
        [6.9900e+02, 3.9000e+01, 1.0000e+00, 0.0000e+00, 2.0000e+00, 9.3827e+04],
        [8.5000e+02, 4.3000e+01, 2.0000e+00, 1.2551e+05, 1.0000e+00, 7.9084e+04]])

En el resultado, puede ver las primeras cinco filas que contienen los valores de las seis columnas numéricas en nuestro conjunto de datos.

El paso final es convertir la matriz numpy de salida en un objeto tensor.

1
2
outputs = torch.tensor(dataset[outputs].values).flatten()
outputs[:5]

Producción:

1
tensor([1, 0, 1, 0, 0])

Ahora tracemos la forma de nuestros datos categóricos, datos numéricos y la salida correspondiente:

1
2
3
print(categorical_data.shape)
print(numerical_data.shape)
print(outputs.shape)

Producción:

1
2
3
torch.Size([10000, 4])
torch.Size([10000, 6])
torch.Size([10000])

Hay un paso muy importante antes de que podamos entrenar nuestro modelo. Convertimos nuestras columnas categóricas a numéricas donde un valor único está representado por un solo número entero. Por ejemplo, en la columna Geografía, vimos que Francia está representada por 0 y Alemania está representada por 1. Podemos usar estos valores para entrenar nuestro modelo. Sin embargo, una mejor manera es representar los valores en una columna categórica en forma de un vector N-dimensional, en lugar de un solo número entero. Un vector es capaz de capturar más información y puede encontrar relaciones entre diferentes valores categóricos de una manera más apropiada. Por lo tanto, representaremos los valores en las columnas categóricas en forma de vectores N-dimensionales. Este proceso se llama incrustación.

Necesitamos definir el tamaño de incrustación (dimensiones vectoriales) para todas las columnas categóricas. No existe una regla estricta y rápida con respecto al número de dimensiones. Una buena regla general para definir el tamaño de incrustación de una columna es dividir el número de valores únicos en la columna por 2 (pero sin exceder los 50). Por ejemplo, para la columna “Geografía”, el número de valores únicos es 3. El tamaño de incrustación correspondiente para la columna “Geografía” será 3/2 = 1,5 = 2 (redondeo).

El siguiente script crea una tupla que contiene el número de valores únicos y los tamaños de dimensión para todas las columnas categóricas:

1
2
3
categorical_column_sizes = [len(dataset[column].cat.categories) for column in categorical_columns]
categorical_embedding_sizes = [(col_size, min(50, (col_size+1)//2)) for col_size in categorical_column_sizes]
print(categorical_embedding_sizes)

Producción:

1
[(3, 2), (2, 1), (2, 1), (2, 1)]

Un modelo de aprendizaje profundo supervisado, como el que estamos desarrollando en este artículo, se entrena utilizando datos de entrenamiento y el rendimiento del modelo se evalúa en el conjunto de datos de prueba. Por lo tanto, necesitamos dividir nuestro conjunto de datos en conjuntos de entrenamiento y prueba como se muestra en el siguiente script:

1
2
3
4
5
6
7
8
9
total_records = 10000
test_records = int(total_records * .2)

categorical_train_data = categorical_data[:total_records-test_records]
categorical_test_data = categorical_data[total_records-test_records:total_records]
numerical_train_data = numerical_data[:total_records-test_records]
numerical_test_data = numerical_data[total_records-test_records:total_records]
train_outputs = outputs[:total_records-test_records]
test_outputs = outputs[total_records-test_records:total_records]

Tenemos 10 mil registros en nuestro conjunto de datos, de los cuales el 80 % de los registros, es decir, 8000 registros, se usarán para entrenar el modelo, mientras que el 20 % restante se usará para evaluar el rendimiento de nuestro modelo. Observe que, en el script anterior, los datos categóricos y numéricos, así como los resultados, se han dividido en conjuntos de entrenamiento y de prueba.

Para verificar que hemos dividido correctamente los datos en conjuntos de entrenamiento y prueba, imprimamos las longitudes de los registros de entrenamiento y prueba:

1
2
3
4
5
6
7
print(len(categorical_train_data))
print(len(numerical_train_data))
print(len(train_outputs))

print(len(categorical_test_data))
print(len(numerical_test_data))
print(len(test_outputs))

Producción:

1
2
3
4
5
6
8000
8000
8000
2000
2000
2000

Creación de un modelo para predicción {#creación de un modelo para predicción}

Hemos dividido los datos en conjuntos de entrenamiento y prueba, ahora es el momento de definir nuestro modelo para el entrenamiento. Para hacerlo, podemos definir una clase llamada Modelo, que se usará para entrenar el modelo. Mira el siguiente guión:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Model(nn.Module):

    def __init__(self, embedding_size, num_numerical_cols, output_size, layers, p=0.4):
        super().__init__()
        self.all_embeddings = nn.ModuleList([nn.Embedding(ni, nf) for ni, nf in embedding_size])
        self.embedding_dropout = nn.Dropout(p)
        self.batch_norm_num = nn.BatchNorm1d(num_numerical_cols)

        all_layers = []
        num_categorical_cols = sum((nf for ni, nf in embedding_size))
        input_size = num_categorical_cols + num_numerical_cols

        for i in layers:
            all_layers.append(nn.Linear(input_size, i))
            all_layers.append(nn.ReLU(inplace=True))
            all_layers.append(nn.BatchNorm1d(i))
            all_layers.append(nn.Dropout(p))
            input_size = i

        all_layers.append(nn.Linear(layers[-1], output_size))

        self.layers = nn.Sequential(*all_layers)

    def forward(self, x_categorical, x_numerical):
        embeddings = []
        for i,e in enumerate(self.all_embeddings):
            embeddings.append(e(x_categorical[:,i]))
        x = torch.cat(embeddings, 1)
        x = self.embedding_dropout(x)

        x_numerical = self.batch_norm_num(x_numerical)
        x = torch.cat([x, x_numerical], 1)
        x = self.layers(x)
        return x

Si nunca antes ha trabajado con PyTorch, el código anterior puede parecer desalentador, sin embargo, intentaré desglosarlo por usted.

En la primera línea, declaramos una clase Modelo que hereda de la clase Módulo del módulo nn de PyTorch. En el constructor de la clase (el método __init__()) se pasan los siguientes parámetros:

  1. embedding_size: contiene el tamaño de incrustación para las columnas categóricas
  2. num_numerical_cols: almacena el número total de columnas numéricas
  3. output_size: El tamaño de la capa de salida o el número de salidas posibles.
  4. capas: Lista que contiene el número de neuronas para todas las capas.
  5. p: abandono con el valor predeterminado de 0,5

Dentro del constructor, se inicializan algunas variables. En primer lugar, la variable all_embeddings contiene una lista de objetos ModuleList para todas las columnas categóricas. El embedding_dropout almacena el valor de abandono para todas las capas. Finalmente, batch_norm_num almacena una lista de objetos BatchNorm1d para todas las columnas numéricas.

A continuación, para encontrar el tamaño de la capa de entrada, la cantidad de columnas categóricas y numéricas se suman y se almacenan en la variable input_size. Después de eso, un bucle for itera y las capas correspondientes se agregan a la lista all_layers. Las capas añadidas son:

  • Lineal: Se usa para calcular el producto escalar entre las entradas y las matrices de peso
  • ReLu: Que se aplica como función de activación
  • BatchNorm1d: Se utiliza para aplicar la normalización por lotes a las columnas numéricas
  • Dropout: Se utiliza para evitar el sobreajuste

Después del ciclo for, la capa de salida se agrega a la lista de capas. Dado que queremos que todas las capas de las redes neuronales se ejecuten secuencialmente, la lista de capas se pasa a la clase nn.Sequential.

A continuación, en el método forward, tanto las columnas categóricas como las numéricas se pasan como entradas. La incrustación de las columnas categóricas se lleva a cabo en las siguientes líneas.

1
2
3
4
5
embeddings = []
for i, e in enumerate(self.all_embeddings):
    embeddings.append(e(x_categorical[:,i]))
x = torch.cat(embeddings, 1)
x = self.embedding_dropout(x)

La normalización por lotes de las columnas numéricas se aplica con el siguiente script:

1
x_numerical = self.batch_norm_num(x_numerical)

Finalmente, las columnas categóricas incrustadas x y las columnas numéricas x_numerical se concatenan juntas y se pasan a las capas secuenciales.

Entrenando al modelo {#entrenando al modelo}

Para entrenar el modelo, primero tenemos que crear un objeto de la clase Modelo que definimos en la última sección.

1
model = Model(categorical_embedding_sizes, numerical_data.shape[1], 2, [200,100,50], p=0.4)

Puedes ver que pasamos el tamaño de incrustación de las columnas categóricas, el número de columnas numéricas, el tamaño de salida (2 en nuestro caso) y las neuronas en las capas ocultas. Puedes ver que tenemos tres capas ocultas con 200, 100 y 50 neuronas, respectivamente. Puedes elegir cualquier otro tamaño si quieres.

Imprimamos nuestro modelo y veamos cómo se ve:

1
print(model)

Producción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Model(
  (all_embeddings): ModuleList(
    (0): Embedding(3, 2)
    (1): Embedding(2, 1)
    (2): Embedding(2, 1)
    (3): Embedding(2, 1)
  )
  (embedding_dropout): Dropout(p=0.4)
  (batch_norm_num): BatchNorm1d(6, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (layers): Sequential(
    (0): Linear(in_features=11, out_features=200, bias=True)
    (1): ReLU(inplace)
    (2): BatchNorm1d(200, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (3): Dropout(p=0.4)
    (4): Linear(in_features=200, out_features=100, bias=True)
    (5): ReLU(inplace)
    (6): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (7): Dropout(p=0.4)
    (8): Linear(in_features=100, out_features=50, bias=True)
    (9): ReLU(inplace)
    (10): BatchNorm1d(50, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (11): Dropout(p=0.4)
    (12): Linear(in_features=50, out_features=2, bias=True)
  )
)

Puede ver que en la primera capa lineal el valor de la variable in_features es 11 ya que tenemos 6 columnas numéricas y la suma de las dimensiones incrustadas para las columnas categóricas es 5, por lo tanto, 6+5 = 11. De manera similar, en la última capa, out_features tiene un valor de 2 ya que solo tenemos 2 salidas posibles.

Antes de que podamos entrenar nuestro modelo, necesitamos definir la función de pérdida y el optimizador que se usará para entrenar el modelo. Como estamos resolviendo un problema de clasificación, usaremos la pérdida de entropía cruzada. Para la función de optimizador, utilizaremos el optimizador de adam.

El siguiente script define la función de pérdida y el optimizador:

1
2
loss_function = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

Ahora tenemos todo lo que se necesita para entrenar el modelo. El siguiente script entrena el modelo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
epochs = 300
aggregated_losses = []

for i in range(epochs):
    i += 1
    y_pred = model(categorical_train_data, numerical_train_data)
    single_loss = loss_function(y_pred, train_outputs)
    aggregated_losses.append(single_loss)

    if i%25 == 1:
        print(f'epoch: {i:3} loss: {single_loss.item():10.8f}')

    optimizer.zero_grad()
    single_loss.backward()
    optimizer.step()

print(f'epoch: {i:3} loss: {single_loss.item():10.10f}')

El número de épocas se establece en 300, lo que significa que para entrenar el modelo, se usará el conjunto de datos completo 300 veces. Un bucle for se ejecuta 300 veces y durante cada iteración, la pérdida se calcula utilizando la función de pérdida. La pérdida durante cada iteración se agrega a la lista de “pérdida_agregada”. Para actualizar los pesos, se llama a la función backward() del objeto single_loss. Finalmente, el método step() de la función optimizer actualiza el gradiente. La pérdida se imprime después de cada 25 épocas.

El resultado del script anterior es el siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
epoch:   1 loss: 0.71847951
epoch:  26 loss: 0.57145703
epoch:  51 loss: 0.48110831
epoch:  76 loss: 0.42529839
epoch: 101 loss: 0.39972275
epoch: 126 loss: 0.37837571
epoch: 151 loss: 0.37133673
epoch: 176 loss: 0.36773482
epoch: 201 loss: 0.36305946
epoch: 226 loss: 0.36079505
epoch: 251 loss: 0.35350436
epoch: 276 loss: 0.35540250
epoch: 300 loss: 0.3465710580

El siguiente script traza las pérdidas contra épocas:

1
2
3
plt.plot(range(epochs), aggregated_losses)
plt.ylabel('Loss')
plt.xlabel('epoch');

Producción:

graficando las pérdidas contra épocas

La salida muestra que inicialmente la pérdida disminuye rápidamente. Después de alrededor de la época 250, hay una disminución muy pequeña en la pérdida.

Hacer predicciones

El último paso es hacer predicciones sobre los datos de prueba. Para hacerlo, simplemente necesitamos pasar categorical_test_data y numerical_test_data a la clase model. Los valores devueltos se pueden comparar con los valores de salida de prueba reales. El siguiente script hace predicciones sobre la clase de prueba e imprime la pérdida de entropía cruzada para los datos de prueba.

1
2
3
4
with torch.no_grad():
    y_val = model(categorical_test_data, numerical_test_data)
    loss = loss_function(y_val, test_outputs)
print(f'Loss: {loss:.8f}')

Producción:

1
Loss: 0.36855841

La pérdida en el conjunto de prueba es 0,3685, que es un poco más de 0,3465 logrado en el conjunto de entrenamiento, lo que muestra que nuestro modelo se sobreajusta ligeramente.

Es importante tener en cuenta que dado que especificamos que nuestra capa de salida contendrá 2 neuronas, cada predicción contendrá 2 valores. Por ejemplo, los primeros 5 valores predichos se ven así:

1
print(y_val[:5])

Producción:

1
2
3
4
5
tensor([[ 1.2045, -1.3857],
        [ 1.3911, -1.5957],
        [ 1.2781, -1.3598],
        [ 0.6261, -0.5429],
        [ 2.5430, -1.9991]])

La idea detrás de tales predicciones es que si la salida real es 0, el valor en el índice 0 debería ser mayor que el valor en el índice 1 y viceversa. Podemos recuperar el índice del valor más grande de la lista con el siguiente script:

1
y_val = np.argmax(y_val, axis=1)

Producción:

Ahora imprimamos de nuevo los primeros cinco valores para la lista y_val:

1
print(y_val[:5])

Producción:

1
tensor([0, 0, 0, 0, 0])

Dado que en la lista de salidas previstas originalmente, para los primeros cinco registros, los valores en los índices cero son mayores que los valores en los primeros índices, podemos ver 0 en las primeras cinco filas de las salidas procesadas.

Finalmente, podemos usar las clases confusion_matrix, accuracy_score y classification_report del módulo sklearn.metrics para encontrar los valores de exactitud, precisión y recuperación para el conjunto de prueba, junto con la matriz de confusión.

1
2
3
4
5
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score

print(confusion_matrix(test_outputs,y_val))
print(classification_report(test_outputs,y_val))
print(accuracy_score(test_outputs, y_val))

Producción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[[1527   83]
 [ 224  166]]
              precision    recall  f1-score   support

           0       0.87      0.95      0.91      1610
           1       0.67      0.43      0.52       390

   micro avg       0.85      0.85      0.85      2000
   macro avg       0.77      0.69      0.71      2000
weighted avg       0.83      0.85      0.83      2000

0.8465

El resultado muestra que nuestro modelo logra una precisión del 84,65 %, lo cual es bastante impresionante dado que seleccionamos aleatoriamente todos los parámetros para nuestro modelo de red neuronal. Le sugiero que intente cambiar los parámetros del modelo, es decir, dividir el entrenamiento/probar, el número y el tamaño de las capas ocultas, etc. para ver si puede obtener mejores resultados.

Conclusión

PyTorch es una biblioteca de aprendizaje profundo de uso común desarrollada por Facebook que se puede usar para una variedad de tareas, como clasificación, regresión y agrupación. Este artículo explica cómo usar la biblioteca PyTorch para la clasificación de datos tabulares.