¿Qué son los datos sintéticos?
Datos creados por una computadora destinada a replicar o aumentar los datos existentes.
¿Por qué es útil?
Todos hemos experimentado el éxito de ChatGPT, LLAMA, y más recientemente, Deepseek. Estos modelos de idiomas se están utilizando de manera ubicua en toda la sociedad y han desencadenado muchas afirmaciones de que nos estamos acercando rápidamente a la inteligencia general artificial, la IA capaz de replicar cualquier función humana.
Antes de emocionarnos demasiado o asustarse, dependiendo de su perspectiva, también nos estamos acercando rápidamente a un obstáculo para el avance de estos modelos de idiomas. Según un artículo publicado por un grupo del Instituto de Investigación, Epoch [1], Nos estamos quedando sin datos. Estiman que para 2028 habremos alcanzado el límite superior de posibles datos sobre los cuales capacitar a los modelos de idiomas.
¿Qué sucede si nos quedamos sin datos?
Bueno, si nos quedamos sin datos, entonces no vamos a tener nada nuevo para entrenar nuestros modelos de idiomas. Estos modelos dejarán de mejorar. Si queremos seguir la inteligencia general artificial, entonces tendremos que encontrar nuevas formas de mejorar la IA sin solo aumentar el volumen de datos de entrenamiento del mundo real.
Un salvador potencial son los datos sintéticos que se pueden generar para imitar los datos existentes y ya se han utilizado para mejorar el rendimiento de modelos como Gemini y DBRX.
Datos sintéticos más allá de LLMS
Más allá de superar la escasez de datos para modelos de idiomas grandes, los datos sintéticos se pueden usar en las siguientes situaciones:
- Datos confidenciales – Si no queremos compartir o usar atributos confidenciales, se pueden generar datos sintéticos que imitan las propiedades de estas características mientras mantienen el anonimato.
- Datos caros-Si recopilar datos es costoso, podemos generar un gran volumen de datos sintéticos a partir de una pequeña cantidad de datos del mundo real.
- Falta de datos– Los conjuntos de datos están sesgados cuando hay un número desproporcionadamente bajo de puntos de datos individuales de un grupo en particular. Los datos sintéticos se pueden usar para equilibrar un conjunto de datos.
Conjuntos de datos desequilibrados
Los conjuntos de datos desequilibrados pueden (*pero no siempre*) ser problemáticos, ya que pueden no contener suficiente información para capacitar efectivamente a un modelo predictivo. Por ejemplo, si un conjunto de datos contiene muchos más hombres que mujeres, nuestro modelo puede estar sesgado para reconocer a los hombres y clasificar mal las futuras muestras femeninas como hombres.
En este artículo mostramos el desequilibrio en el popular UCI Conjunto de datos para adultos [2], y cómo podemos usar un variacional automático para generar Datos sintéticos Para mejorar la clasificación en este ejemplo.
Primero descargamos el conjunto de datos para adultos. Este conjunto de datos contiene características como la edad, la educación y la ocupación que pueden usarse para predecir el “ingreso” del resultado objetivo.
# Download dataset into a dataframe
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data"
columns = [
"age", "workclass", "fnlwgt", "education", "education-num", "marital-status",
"occupation", "relationship", "race", "sex", "capital-gain",
"capital-loss", "hours-per-week", "native-country", "income"
]
data = pd.read_csv(url, header=None, names=columns, na_values=" ?", skipinitialspace=True)
# Drop rows with missing values
data = data.dropna()
# Split into features and target
X = data.drop(columns=["income"])
y = data['income'].map({'>50K': 1, '<=50K': 0}).values
# Plot distribution of income
plt.figure(figsize=(8, 6))
plt.hist(data['income'], bins=2, edgecolor="black")
plt.title('Distribution of Income')
plt.xlabel('Income')
plt.ylabel('Frequency')
plt.show()
En el conjunto de datos para adultos, el ingreso es una variable binaria, que representa a las personas que ganan arriba y por debajo de $ 50,000. Trazamos la distribución de ingresos en todo el conjunto de datos a continuación. Podemos ver que el conjunto de datos está muy desequilibrado con un número mucho mayor de personas que ganan menos de $ 50,000.
A pesar de este desequilibrio, todavía podemos capacitar a un clasificador de aprendizaje automático en el conjunto de datos de adultos que podemos usar para determinar si no se ve o prueba, las personas deben clasificarse como ganancias anteriores o inferiores a 50k.
# Preprocessing: One-hot encode categorical features, scale numerical features
numerical_features = ["age", "fnlwgt", "education-num", "capital-gain", "capital-loss", "hours-per-week"]
categorical_features = [
"workclass", "education", "marital-status", "occupation", "relationship",
"race", "sex", "native-country"
]
preprocessor = ColumnTransformer(
transformers=[
("num", StandardScaler(), numerical_features),
("cat", OneHotEncoder(), categorical_features)
]
)
X_processed = preprocessor.fit_transform(X)
# Convert to numpy array for PyTorch compatibility
X_processed = X_processed.toarray().astype(np.float32)
y_processed = y.astype(np.float32)
# Split dataset in train and test sets
X_model_train, X_model_test, y_model_train, y_model_test = train_test_split(X_processed, y_processed, test_size=0.2, random_state=42)
rf_classifier = RandomForestClassifier(n_estimators=100, random_state=42)
rf_classifier.fit(X_model_train, y_model_train)
# Make predictions
y_pred = rf_classifier.predict(X_model_test)
# Display confusion matrix
plt.figure(figsize=(6, 4))
sns.heatmap(cm, annot=True, fmt="d", cmap="YlGnBu", xticklabels=["Negative", "Positive"], yticklabels=["Negative", "Positive"])
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.title("Confusion Matrix")
plt.show()
Impresión de la matriz de confusión de nuestro clasificador muestra que nuestro modelo funciona bastante bien a pesar del desequilibrio. Nuestro modelo tiene una tasa de error general del 16%, pero la tasa de error para la clase positiva (ingresos> 50k) es del 36% donde la tasa de error para la clase negativa (ingresos <50k) es del 8%.
Esta discrepancia muestra que el modelo está sesgado hacia la clase negativa. El modelo frecuentemente clasifica incorrectamente a las personas que ganan más de 50k como ganando menos de 50k.
A continuación mostramos cómo podemos usar un Autoencoder variacional generar datos sintéticos de la clase positiva para equilibrar este conjunto de datos. Luego entrenamos el mismo modelo utilizando el conjunto de datos sintéticamente equilibrado y reducimos los errores del modelo en el conjunto de pruebas.
¿Cómo podemos generar datos sintéticos?
Hay muchos métodos diferentes para generar datos sintéticos. Estos pueden incluir métodos más tradicionales, como SMote y ruido gaussiano, que generan nuevos datos modificando los datos existentes. Alternativamente, los modelos generativos, como los autoencoders variacionales o las redes adversas generales, están predispuestos a generar nuevos datos a medida que sus arquitecturas aprenden la distribución de datos reales y los usan para generar muestras sintéticas.
En este tutorial utilizamos un autoencoder variacional para generar datos sintéticos.
Autoencoders variacionales
Los autoencoders variacionales (VAE) son excelentes para la generación de datos sintéticos porque usan datos reales para aprender un espacio latente continuo. Podemos ver este espacio latente como un cubo mágico del que podemos probar datos sintéticos que se parecen mucho a los datos existentes. La continuidad de este espacio es uno de sus grandes puntos de venta, ya que significa que el modelo se generaliza bien y no solo memoriza el espacio latente de entradas específicas.
Un VAE consiste en un codificador que mapea los datos de entrada en una distribución de probabilidad (media y varianza) y un descifrador que reconstruye los datos del espacio latente.
Para ese espacio latente continuo, los VAE usan un truco de reparameterización, Cuando un vector de ruido aleatorio se escala y cambia utilizando la media y la varianza aprendidas, asegurando representaciones suaves y continuas en el espacio latente.
A continuación construimos un Vasas básicas clase que implementa este proceso con una arquitectura simple.
- El codificadorComprime la entrada en una representación oculta más pequeña, produciendo una varianza media y logarítmica que defina una distribución gaussiana, también conocida como creación de nuestro cubo de muestreo mágico. En lugar de un muestreo directamente, el modelo aplica el truco de reparameterización para generar variables latentes, que luego se pasan al decodificador.
- El decodificadorReconstruye los datos originales de estas variables latentes, asegurando que los datos generados mantengan las características del conjunto de datos original.
class BasicVAE(nn.Module):
def __init__(self, input_dim, latent_dim):
super(BasicVAE, self).__init__()
# Encoder: Single small layer
self.encoder = nn.Sequential(
nn.Linear(input_dim, 8),
nn.ReLU()
)
self.fc_mu = nn.Linear(8, latent_dim)
self.fc_logvar = nn.Linear(8, latent_dim)
# Decoder: Single small layer
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 8),
nn.ReLU(),
nn.Linear(8, input_dim),
nn.Sigmoid() # Outputs values in range [0, 1]
)
def encode(self, x):
h = self.encoder(x)
mu = self.fc_mu(h)
logvar = self.fc_logvar(h)
return mu, logvar
def reparameterize(self, mu, logvar):
std = torch.exp(0.5 * logvar)
eps = torch.randn_like(std)
return mu + eps * std
def decode(self, z):
return self.decoder(z)
def forward(self, x):
mu, logvar = self.encode(x)
z = self.reparameterize(mu, logvar)
return self.decode(z), mu, logvar
Dada nuestra arquitectura BasicVae, construimos nuestras funciones de pérdida y la capacitación de modelos a continuación.
def vae_loss(recon_x, x, mu, logvar, tau=0.5, c=1.0):
recon_loss = nn.MSELoss()(recon_x, x)
# KL Divergence Loss
kld_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
return recon_loss + kld_loss / x.size(0)
def train_vae(model, data_loader, epochs, learning_rate):
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
model.train()
losses = []
reconstruction_mse = []
for epoch in range(epochs):
total_loss = 0
total_mse = 0
for batch in data_loader:
batch_data = batch[0]
optimizer.zero_grad()
reconstructed, mu, logvar = model(batch_data)
loss = vae_loss(reconstructed, batch_data, mu, logvar)
loss.backward()
optimizer.step()
total_loss += loss.item()
# Compute batch-wise MSE for comparison
mse = nn.MSELoss()(reconstructed, batch_data).item()
total_mse += mse
losses.append(total_loss / len(data_loader))
reconstruction_mse.append(total_mse / len(data_loader))
print(f"Epoch {epoch+1}/{epochs}, Loss: {total_loss:.4f}, MSE: {total_mse:.4f}")
return losses, reconstruction_mse
combined_data = np.concatenate([X_model_train.copy(), y_model_train.cop
y().reshape(26048,1)], axis=1)
# Train-test split
X_train, X_test = train_test_split(combined_data, test_size=0.2, random_state=42)
batch_size = 128
# Create DataLoaders
train_loader = DataLoader(TensorDataset(torch.tensor(X_train)), batch_size=batch_size, shuffle=True)
test_loader = DataLoader(TensorDataset(torch.tensor(X_test)), batch_size=batch_size, shuffle=False)
basic_vae = BasicVAE(input_dim=X_train.shape[1], latent_dim=8)
basic_losses, basic_mse = train_vae(
basic_vae, train_loader, epochs=50, learning_rate=0.001,
)
# Visualize results
plt.figure(figsize=(12, 6))
plt.plot(basic_mse, label="Basic VAE")
plt.ylabel("Reconstruction MSE")
plt.title("Training Reconstruction MSE")
plt.legend()
plt.show()
vae_loss consta de dos componentes: pérdida de reconstrucciónque mide qué tan bien los datos generados coinciden con la entrada original utilizando el error medio cuadrado (MSE), y Pérdida de divergencia de KLque asegura que el espacio latente aprendido siga una distribución normal.
Train_VaeOptimiza el VAE usando el Optimizer Adam en múltiples épocas. Durante el entrenamiento, el modelo toma mini lotes de datos, los reconstruye y calcula la pérdida utilizando vae_loss. Estos errores se corrigen mediante backpropagation donde se actualizan los pesos del modelo. Entrenamos el modelo para 50 épocas y trazamos cómo la reconstrucción media del error al cuadrado disminuye sobre el entrenamiento.
Podemos ver que nuestro modelo aprende rápidamente cómo reconstruir nuestros datos, evidenciando el aprendizaje eficiente.
Ahora hemos capacitado a nuestras BasicVae para reconstruir con precisión el conjunto de datos para adultos, ahora podemos usarlo para generar datos sintéticos. Queremos generar más muestras de la clase positiva (individuos que ganan más de 50k) para equilibrar las clases y eliminar el sesgo de nuestro modelo.
Para hacer esto, seleccionamos todas las muestras de nuestro conjunto de datos VAE donde el ingreso es la clase positiva (gane más de 50k). Luego codificamos estas muestras en el espacio latente. Como solo hemos seleccionado muestras de la clase positiva para codificar, este espacio latente reflejará las propiedades de la clase positiva de la que podemos probar para crear datos sintéticos.
Muestra 15000 muestras nuevas de este espacio latente y decodificamos estos vectores latentes nuevamente en el espacio de datos de entrada como nuestros datos sintéticos.
# Create column names
col_number = sample_df.shape[1]
col_names = [str(i) for i in range(col_number)]
sample_df.columns = col_names
# Define the feature value to filter
feature_value = 1.0 # Specify the feature value - here we set the income to 1
# Set all income values to 1 : Over 50k
selected_samples = sample_df[sample_df[col_names[-1]] == feature_value]
selected_samples = selected_samples.values
selected_samples_tensor = torch.tensor(selected_samples, dtype=torch.float32)
basic_vae.eval() # Set model to evaluation mode
with torch.no_grad():
mu, logvar = basic_vae.encode(selected_samples_tensor)
latent_vectors = basic_vae.reparameterize(mu, logvar)
# Compute the mean latent vector for this feature
mean_latent_vector = latent_vectors.mean(dim=0)
num_samples = 15000 # Number of new samples
latent_dim = 8
latent_samples = mean_latent_vector + 0.1 * torch.randn(num_samples, latent_dim)
with torch.no_grad():
generated_samples = basic_vae.decode(latent_samples)
Ahora hemos generado datos sintéticos de la clase positiva, podemos combinar esto con los datos de entrenamiento originales para generar un conjunto de datos sintético equilibrado.
new_data = pd.DataFrame(generated_samples)
# Create column names
col_number = new_data.shape[1]
col_names = [str(i) for i in range(col_number)]
new_data.columns = col_names
X_synthetic = new_data.drop(col_names[-1],axis=1)
y_synthetic = np.asarray([1 for _ in range(0,X_synthetic.shape[0])])
X_synthetic_train = np.concatenate([X_model_train, X_synthetic.values], axis=0)
y_synthetic_train = np.concatenate([y_model_train, y_synthetic], axis=0)
mapping = {1: '>50K', 0: '<=50K'}
map_function = np.vectorize(lambda x: mapping[x])
# Apply mapping
y_mapped = map_function(y_synthetic_train)
plt.figure(figsize=(8, 6))
plt.hist(y_mapped, bins=2, edgecolor="black")
plt.title('Distribution of Income')
plt.xlabel('Income')
plt.ylabel('Frequency')
plt.show()
Ahora podemos usar nuestro conjunto de datos sintético de entrenamiento equilibrado para volver a entrenar nuestro clasificador de bosques aleatorios. Luego podemos evaluar este nuevo modelo en los datos de la prueba originales para ver cuán efectivos son nuestros datos sintéticos para reducir el sesgo del modelo.
rf_classifier = RandomForestClassifier(n_estimators=100, random_state=42)
rf_classifier.fit(X_synthetic_train, y_synthetic_train)
# Step 5: Make predictions
y_pred = rf_classifier.predict(X_model_test)
cm = confusion_matrix(y_model_test, y_pred)
# Create heatmap
plt.figure(figsize=(6, 4))
sns.heatmap(cm, annot=True, fmt="d", cmap="YlGnBu", xticklabels=["Negative", "Positive"], yticklabels=["Negative", "Positive"])
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.title("Confusion Matrix")
plt.show()
Nuestro nuevo clasificador, entrenado en el conjunto de datos sintético equilibrado, comete menos errores en el conjunto de pruebas original que nuestro clasificador original entrenado en el conjunto de datos desequilibrado y nuestra tasa de error ahora se reduce al 14%.
Sin embargo, no hemos podido reducir la discrepancia en los errores en una cantidad significativa, nuestra tasa de error para la clase positiva sigue siendo el 36%. Esto podría deberse a las siguientes razones:
- Hemos discutido cómo uno de los beneficios de los VAE es el aprendizaje de un espacio latente continuo. Sin embargo, si la clase mayoritaria domina, el espacio latente podría sesgarse hacia la clase mayoritaria.
- Es posible que el modelo no haya aprendido adecuadamente una representación distinta para la clase minoritaria debido a la falta de datos, lo que dificulta la muestra de esa región con precisión.
En este tutorial, hemos introducido y creado una arquitectura BasicVAE que puede usarse para generar datos sintéticos que mejoran la precisión de clasificación en un conjunto de datos desequilibrado.
Siga para futuros artículos donde mostraré cómo podemos construir arquitecturas VAE más sofisticadas que aborden los problemas anteriores con el muestreo desequilibrado y más.
[1] Villalobos, P., Ho, A., Sevilla, J., Besiroglu, T., Heim, L. y Hobbhahn, M. (2024). ¿Nos quedaremos sin datos? Límites de la escala LLM basada en datos generados por humanos. preimpresión arxiv arxiv: 2211.04325, 3.
[2] Becker, B. y Kohavi, R. (1996). Adulto [Dataset]. Repositorio de aprendizaje automático de UCI. https://doi.org/10.24432/c5xw20.