Diseño de Tamaños de Kernel en Redes Neuronales Convolucionales para Extracción de Características de Imágenes y Modelado con Regresión Logística en Control de Riesgos

Las Redes Neuronales Convolucionales (CNNs) son fundamentales en la visión artificial para extraer características de imágenes de forma jerárquica. La elección del tamaño del kernel de convolución es crucial, ya que determina la escala de las características capturadas: kernels pequeños son ideales para texturas finas, mientras que kernels más grandes capturan mejor la estructura global. Este artículo explora las metodologías de diseño de kernels y sus detalles de modelado con regresión logística en el ámbito del control de riesgos financiero, estableciendo un vínculo técnico entre estas áreas.

Fundamentos Teóricos del Diseño de Kernels

Relación entre Campo Receptivo y Escala de Características

El campo receptivo (Receptive Field) de una CNN define el tamaño de la región de la imagen de entrada que influye en un único píxel de la salida de una capa. El tamaño del kernel es el factor más directo en la determinación del campo receptivo.


import torch
import torch.nn as nn

def calcular_campo_receptivo(capas):
    rf = 1
    for kernel_size, stride in capas:
        rf = rf + (kernel_size - 1) * stride
    return rf

configuraciones_capas = [
    [(3, 1), (3, 1), (3, 1)],
    [(5, 1), (5, 1)],
    [(7, 1)]
]

for config in configuraciones_capas:
    rf = calcular_campo_receptivo(config)
    print(f'Configuración {config}: campo receptivo = {rf}')

Ventajas de Apilar Kernels Pequeños

Investigaciones como VGGNet demostraron que apilar kernels pequeños puede lograr el mismo campo receptivo que un kernel grande, pero con una reducción significativa en el número de parámetros.


def comparar_parametros_kernel():
    config_kernels = {
        'Kernel 7x7 único': (1, 7, 7),
        'Tres kernels 3x3': (3, 3, 3),
        'Dos kernels 5x5': (2, 5, 5)
    }
    parametros = {}
    for nombre, (num_capas, k, _) in config_kernels.items():
        canales_entrada = 64
        canales_salida = 64
        if nombre == 'Kernel 7x7 único':
            params_totales = num_capas * (canales_entrada * canales_salida * k * k)
        elif nombre == 'Tres kernels 3x3':
            params_por_capa = canales_entrada * canales_salida * k * k
            params_totales = num_capas * params_por_capa
        else:
            params_por_capa = canales_entrada * canales_salida * k * k
            params_totales = num_capas * params_por_capa
        parametros[nombre] = params_totales
    return parametros

parametros_kernels = comparar_parametros_kernel()
for nombre, num_params in parametros_kernels.items():
    print(f'{nombre}: {num_params:,} parámetros')

Metodología Central de Diseño de Kernels

Aplicación Estándar del Kernel 3x3

El kernel de 3x3 se ha convertido en un estándar en arquitecturas CNN modernas. Su pirncipal ventaja es lograr un campo receptivo amplio mediante apilamiento, manteniendo un número reducido de parámetros.


class BloqueConvolucionEstandar(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size=3):
        super().__init__()
        padding = kernel_size // 2
        self.conv = nn.Conv2d(
            in_channels, out_channels,
            kernel_size=kernel_size,
            padding=padding,
            bias=False
        )
        self.bn = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        return self.relu(self.bn(self.conv(x)))

class ExtractorCaracteristicasMultiescala(nn.Module):
    def __init__(self, in_channels, base_channels=64):
        super().__init__()
        self.etapa1 = nn.Sequential(
            BloqueConvolucionEstandar(in_channels, base_channels, 3),
            BloqueConvolucionEstandar(base_channels, base_channels, 3),
            nn.MaxPool2d(2)
        )
        self.etapa2 = nn.Sequential(
            BloqueConvolucionEstandar(base_channels, base_channels * 2, 3),
            BloqueConvolucionEstandar(base_channels * 2, base_channels * 2, 3),
            nn.MaxPool2d(2)
        )
        self.etapa3 = nn.Sequential(
            BloqueConvolucionEstandar(base_channels * 2, base_channels * 4, 3),
            BloqueConvolucionEstandar(base_channels * 4, base_channels * 4, 3),
            BloqueConvolucionEstandar(base_channels * 4, base_channels * 4, 3),
            nn.MaxPool2d(2)
        )

    def forward(self, x):
        f1 = self.etapa1(x)
        f2 = self.etapa2(f1)
        f3 = self.etapa3(f2)
        return f1, f2, f3

Kernels Asimétricos

Las redes Inception introdujeron kernels asimétricos, descomponiendo un kernel NxN en la combinación de kernels 1xN y Nx1. Esto reduce aún más la cantidad de parámetros.


class BloqueConvolucionAsimetrico(nn.Module):
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.conv_h = nn.Conv2d(
            in_channels, out_channels,
            kernel_size=(1, 3),
            padding=(0, 1),
            bias=False
        )
        self.conv_v = nn.Conv2d(
            out_channels, out_channels,
            kernel_size=(3, 1),
            padding=(1, 0),
            bias=False
        )
        self.bn = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        x = self.conv_h(x)
        x = self.conv_v(x)
        return self.relu(self.bn(x))

Convolución Dilatada y Mantenimiento de Resolución de Mapas de Características

Para tareas como la segmentación semántica, que requieren mapas de características de alta resolución, la convolución dilatada (atrous convolution) amplía el campo receptivo sin reducir la resolución espacial mediante la inserción de "huecos" en el kernel.


class BloqueConvolucionDilatada(nn.Module):
    def __init__(self, in_channels, out_channels, dilation_rate=2):
        super().__init__()
        self.conv = nn.Conv2d(
            in_channels, out_channels,
            kernel_size=3,
            padding=dilation_rate,
            dilation=dilation_rate,
            bias=False
        )
        self.bn = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        return self.relu(self.bn(self.conv(x)))

class BloqueResidualDilatado(nn.Module):
    def __init__(self, channels, dilation_rate):
        super().__init__()
        self.conv1 = BloqueConvolucionDilatada(channels, channels, dilation_rate)
        self.conv2 = BloqueConvolucionDilatada(channels, channels, dilation_rate)

    def forward(self, x):
        residual = x
        out = self.conv1(x)
        out = self.conv2(out)
        return self.relu(out + residual)

    def relu(self, x):
        return torch.relu(x)

Estrategias de Diseño de Kernel para Extracción de Características Finas

Selección del Kernel en la Primera Capa

La primera capa de convolución opera directamente sobre la imagen de entrada, y su kernel influye decisivamente en la extracción de características subsiguientes.


def analisis_kernel_primera_capa():
    estrategias = {
        'Detallada': {
            'kernel_size': 3,
            'stride': 1,
            'padding': 1,
            'adecuado_para': 'Imágenes ricas en detalles de textura',
            'campo_receptivo': 3
        },
        'Equilibrada': {
            'kernel_size': 5,
            'stride': 2,
            'padding': 2,
            'adecuado_para': 'Escenarios generales',
            'campo_receptivo': 5
        },
        'General': {
            'kernel_size': 7,
            'stride': 2,
            'padding': 3,
            'adecuado_para': 'Imágenes grandes, submuestreo rápido',
            'campo_receptivo': 7
        }
    }
    return estrategias

def crear_primera_capa(kernel_size, in_channels=3, out_channels=64):
    padding = kernel_size // 2
    return nn.Conv2d(
        in_channels, out_channels,
        kernel_size=kernel_size,
        stride=1 if kernel_size <= 3 else 2,
        padding=padding,
        bias=False
    )

Fusión de Características Multiescala

Para capturar características finas de diferentes escalas simultáneamente, se pueden emplear estructuras de convolución paralelas con múltiples ramas.


class ModuloConvolucionMultiescala(nn.Module):
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.rama1 = nn.Conv2d(
            in_channels, out_channels // 4,
            kernel_size=1, padding=0, bias=False
        )
        self.rama2 = nn.Conv2d(
            in_channels, out_channels // 4,
            kernel_size=3, padding=1, bias=False
        )
        self.rama3 = nn.Conv2d(
            in_channels, out_channels // 4,
            kernel_size=5, padding=2, bias=False
        )
        self.rama4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            nn.Conv2d(in_channels, out_channels // 4, kernel_size=1, bias=False)
        )
        self.bn = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        b1 = self.rama1(x)
        b2 = self.rama2(x)
        b3 = self.rama3(x)
        b4 = self.rama4(x)
        out = torch.cat([b1, b2, b3, b4], dim=1)
        return self.relu(self.bn(out))

Convolución Profunda Separable

Esta técnica descompone la convolución estándar en una convolución por canal (depthwise) y una convolución punto a punto (pointwise), reduciendo drásticamente los parámetros. Es ideal para dispositivos con recursos limitados.


class ConvolucionSeparableProfunda(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size=3):
        super().__init__()
        self.depthwise = nn.Conv2d(
            in_channels, in_channels,
            kernel_size=kernel_size,
            padding=kernel_size // 2,
            groups=in_channels,
            bias=False
        )
        self.pointwise = nn.Conv2d(
            in_channels, out_channels,
            kernel_size=1,
            bias=False
        )
        self.bn = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        x = self.depthwise(x)
        x = self.pointwise(x)
        return self.relu(self.bn(x))

def comparar_parametros_convolucion(in_c, out_c, k=3):
    params_estandar = in_c * out_c * k * k
    params_depthwise = in_c * k * k + in_c * out_c
    ratio = params_estandar / params_depthwise
    return {
        'estandar': params_estandar,
        'separable_profunda': params_depthwise,
        'ratio_reduccion': round(ratio, 2)
    }

Experimento de Ablación del Tamaño del Kernel


def estudio_ablacion_tamano_kernel():
    tamanios_kernel = [1, 3, 5, 7, 9]
    resultados = []
    for k in tamanios_kernel:
        padding = k // 2
        capa_conv = nn.Conv2d(64, 128, k, padding=padding, bias=False)
        num_parametros = sum(p.numel() for p in capa_conv.parameters())
        flops_por_pixel = 128 * 64 * k * k
        campo_receptivo = k
        resultados.append({
            'tamano_kernel': k,
            'parametros': num_parametros,
            'flops_por_pixel': flops_por_pixel,
            'campo_receptivo': campo_receptivo
        })
    return resultados

Caso Práctico: Detección de Defectos Industriales con Diseño de Kernel


class CNNDefectosIndustriales(nn.Module):
    def __init__(self, num_clases=2):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            nn.Conv2d(32, 32, kernel_size=3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2),
            nn.Conv2d(64, 128, kernel_size=3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 128, kernel_size=3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.AdaptiveAvgPool2d((1, 1))
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Dropout(0.5),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(64, num_clases)
        )

    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x)
        return x

Detalles de Modelado con Regresión Logística en Control de Riesgos Financieros

Preprocesamiento de Datos e Ingeniería de Características

La calidad de los datos en control de riesgos impacta directamente en la ponderación de los modelos de regresión logística. A continuación, se presenta un flujo de preprocesamiento estándar.


import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, RobustScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score, classification_report

def preprocesamiento_datos_financieros(raw_data):
    df = raw_data.copy()
    tasa_nulos = df.isnull().sum() / len(df)
    cols_nulos_altos = tasa_nulos[tasa_nulos > 0.5].index
    df = df.drop(columns=cols_nulos_altos)
    for col in df.select_dtypes(include=[np.number]).columns:
        if df[col].isnull().sum() > 0:
            df[col] = df[col].fillna(df[col].median())
    for col in df.select_dtypes(include=['object']).columns:
        if df[col].isnull().sum() > 0:
            df[col] = df[col].fillna('Unknown')
    return df

def manejo_valores_atipicos(df, columnas, metodo='winsorize'):
    df_limpio = df.copy()
    if metodo == 'winsorize':
        for col in columnas:
            q1 = df_limpio[col].quantile(0.01)
            q99 = df_limpio[col].quantile(0.99)
            df_limpio[col] = df_limpio[col].clip(q1, q99)
    elif metodo == 'iqr':
        for col in columnas:
            q1 = df_limpio[col].quantile(0.25)
            q3 = df_limpio[col].quantile(0.75)
            iqr = q3 - q1
            lower = q1 - 1.5 * iqr
            upper = q3 + 1.5 * iqr
            df_limpio[col] = df_limpio[col].clip(lower, upper)
    return df_limpio

Ajuste de Parámetros de Regresión Logística en Riesgos


def ajuste_regresion_logistica(X_train, y_train, X_val, y_val):
    param_grid = {
        'C': [0.001, 0.01, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0],
        'penalty': ['l1', 'l2', 'elasticnet'],
        'l1_ratio': [0.1, 0.3, 0.5, 0.7, 0.9],
        'solver': ['saga'],
        'class_weight': [None, 'balanced', {0: 1, 1: 5}, {0: 1, 1: 10}]
    }
    from sklearn.model_selection import GridSearchCV
    modelo_base = LogisticRegression(max_iter=1000, random_state=42)
    grid = GridSearchCV(
        modelo_base, param_grid,
        cv=5, scoring='roc_auc',
        n_jobs=-1, verbose=0
    )
    grid.fit(X_train, y_train)
    mejor_modelo = grid.best_estimator_
    y_pred_proba = mejor_modelo.predict_proba(X_val)[:, 1]
    auc_val = roc_auc_score(y_val, y_pred_proba)
    return mejor_modelo, grid.best_params_, auc_val

División de Características en Bin (Binning) y Codificación WOE

En control de riesgos, las características continuas a menudo se dividen en bins y se codifican con WOE (Weight of Evidence) para mejorar la capacidad de ajuste no lineal de la regresión logística.


def calcular_woe_iv(data, feature, target):
    df = data[[feature, target]].copy()
    if data[feature].dtype in ['int64', 'float64']:
        df['bin'] = pd.qcut(df[feature], q=10, duplicates='drop')
    else:
        df['bin'] = df[feature]
    grouped = df.groupby('bin')[target].agg(
        good_count=lambda x: (x == 0).sum(),
        bad_count=lambda x: (x == 1).sum()
    ).reset_index()
    total_good = grouped['good_count'].sum()
    total_bad = grouped['bad_count'].sum()
    grouped['good_dist'] = grouped['good_count'] / total_good
    grouped['bad_dist'] = grouped['bad_count'] / total_bad
    grouped['woe'] = np.log(
        (grouped['bad_dist'] + 1e-10) / (grouped['good_dist'] + 1e-10)
    )
    grouped['iv'] = (grouped['bad_dist'] - grouped['good_dist']) * grouped['woe']
    iv_total = grouped['iv'].sum()
    return grouped, iv_total

def transformar_woe(data, feature, woe_mapping):
    bin_col = f'{feature}_bin'
    if data[feature].dtype in ['int64', 'float64']:
        data[bin_col] = pd.qcut(data[feature], q=10, duplicates='drop', labels=False)
    else:
        data[bin_col] = data[feature]
    woe_dict = dict(zip(woe_mapping['bin'], woe_mapping['woe']))
    data[f'{feature}_woe'] = data[bin_col].map(woe_dict)
    return data

Evaluación y Validación del Modelo


from sklearn.metrics import roc_curve, precision_recall_curve, auc

def evaluacion_integral_modelo(model, X_test, y_test):
    y_pred = model.predict(X_test)
    y_proba = model.predict_proba(X_test)[:, 1]
    fpr, tpr, _ = roc_curve(y_test, y_proba)
    roc_auc = auc(fpr, tpr)
    precision, recall, _ = precision_recall_curve(y_test, y_proba)
    pr_auc = auc(recall, precision)
    resultados = {
        'roc_auc': roc_auc,
        'pr_auc': pr_auc,
        'classification_report': classification_report(y_test, y_pred)
    }
    return resultados

def estadistica_ks(model, X_test, y_test):
    y_proba = model.predict_proba(X_test)[:, 1]
    good_probs = y_proba[y_test == 0]
    bad_probs = y_proba[y_test == 1]
    from scipy import stats
    ks_stat, p_value = stats.ks_2samp(good_probs, bad_probs)
    return ks_stat, p_value

Aplicación de Fusión de Características CNN y Regresión Logística

Las CNNs pueden extraer características de datos no estructurados (ej. fotos de identificación, capturas de pantalla de transacciones), las cuales pueden ser combinadas con características estructuradas para alimentar un modelo de regresión logística.


class ModeloRiesgoHibrido(nn.Module):
    def __init__(self, cnn_backbone, structured_dim, num_clases=2):
        super().__init__()
        self.cnn = cnn_backbone
        self.cnn_fc = nn.Linear(512, 128)
        self.structured_fc = nn.Linear(structured_dim, 64)
        self.classifier = nn.Sequential(
            nn.Linear(128 + 64, 64),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(64, num_clases),
            nn.Sigmoid()
        )

    def forward(self, image_input, structured_input):
        cnn_features = self.cnn(image_input)
        cnn_features = self.cnn_fc(cnn_features)
        structured_features = self.structured_fc(structured_input)
        combined = torch.cat([cnn_features, structured_features], dim=1)
        return self.classifier(combined)

Análisis Comparativo: Regresión Logística vs. CNN

Dimensión de Comparación Regresión Logística CNN
Tipo de Datos Adecuado Datos tabulares, características estructuradas Imágenes, secuencias, datos no estructurados
Extracción de Características Ingeniería manual de caratcerísticas Aprendizaje automático de características
Interpretabilidad Alta (coeficientes directamente interpretables) Baja (caja negra)
Número de Parámetros Linealmente relacionado con la dimensión de características Relacionado con el tamaño del kernel y la profundidad
Velocidad de Entrenamiento Rápida (minutos) Lenta (horas o días)
Rendimiento con Pocos Datos Mejor Peor
Riesgo de Sobreajuste Bajo (buena regularización) Alto (requiere aumento de datos)
Costo de Despliegue Bajo Alto (requiere GPU)

Mejores Prácticas para la Selección del Tamaño del Kernel


def recomendacion_tamano_kernel(input_size, task_type):
    recomendaciones = {
        'clasificacion_detallada': {
            'primer_layer': 3,
            'capas_intermedias': 3,
            'ultimas_capas': 3,
            'pooling': 'max',
            'justificacion': 'Kernels pequeños preservan diferencias de textura sutiles'
        },
        'deteccion_objetos': {
            'primer_layer': 7,
            'capas_intermedias': [3, 5, 3],
            'ultimas_capas': 3,
            'pooling': 'max + avg',
            'justificacion': 'Kernel grande para reducir resolución rápidamente, kernels medianos para multiescala'
        },
        'segmentacion_semantica': {
            'primer_layer': 3,
            'capas_intermedias': 3,
            'capas_dilatadas': [2, 4, 8],
            'pooling': 'ninguno',
            'justificacion': 'Convolución dilatada para mantener resolución, kernels pequeños para detalles'
        },
        'deteccion_defectos': {
            'primer_layer': 5,
            'capas_intermedias': 3,
            'ultimas_capas': 3,
            'pooling': 'max',
            'justificacion': 'Kernel de tamaño medio para equilibrar detalles e información regional'
        }
    }
    return recomendaciones.get(task_type, recomendaciones['clasificacion_detallada'])

Conclusión

El diseño del tamaño del kernel en CNNs es un equilibrio entre el campo receptivo y el número de parámetros. Los kernels de 3x3 son el estándar, pero el uso flexible de kernels de 5x5, convoluciones dilatadas o ramas multiescala puede mejorar significativamente el rendimiento en la extracción de características finas. En el control de riesgos, la regresión logística requiere un ajuste cuidadoso de sus pesos, y la codificación WOE junto con el ajuste de pesos de muestra son técnicas clave. Combinar la capacidad de extracción de características de CNNs con la interpretabilidad de la regresión logística es un paradigma efectivo para la aplicación de IA en industrias tradicionalse, aprovechando el poder del deep learning en datos no estructurados y manteniendo la explicabilidad requerida en el control de riesgos.

Etiquetas: redes neuronales convolucionales CNN diseño de kernel campo receptivo convolución dilatada

Publicado el 11-6 04:49