Vamos a fingir que tenemos un producto real que necesitamos mejorar. Exploraremos un conjunto de datos y probaremos diferentes modelos como regresión logística, redes neuronales recurrentes y transformadores, observando qué tan precisos son, cómo mejorarán el producto, qué tan rápido funcionan y si son fáciles de depurar. y ampliar.
Puede leer el código completo del estudio de caso en GitHub y ver el cuaderno de análisis con gráficos interactivos en Jupyter Notebook Viewer .
¿Entusiasmado? ¡Hagámoslo!
Imaginemos que somos dueños de un sitio web de comercio electrónico. En este sitio web, el vendedor puede subir las descripciones de los artículos que desea vender. También tienen que elegir las categorías de los elementos manualmente, lo que puede ralentizarlos.
Nuestra tarea es automatizar la elección de categorías según la descripción del artículo. Sin embargo, una elección mal automatizada es peor que ninguna automatización, porque un error puede pasar desapercibido y provocar pérdidas en las ventas. Por lo tanto, podríamos optar por no establecer una etiqueta automática si no estamos seguros.
Para este estudio de caso, usaremos el
Consideraremos múltiples arquitecturas de modelos a continuación y siempre es una buena práctica decidir cómo elegir la mejor opción antes de comenzar. ¿Cómo afectará este modelo a nuestro producto? …nuestra infraestructura?
Evidentemente dispondremos de una métrica de calidad técnica para comparar varios modelos offline. En este caso, tenemos una tarea de clasificación de clases múltiples, así que usemos una puntuación de precisión equilibrada , que maneja bien las etiquetas desequilibradas.
Por supuesto, la etapa final típica de probar a un candidato es la prueba AB, la etapa en línea, que brinda una mejor idea de cómo el cambio afecta a los clientes. Por lo general, las pruebas AB requieren más tiempo que las pruebas fuera de línea, por lo que solo se realizan las pruebas a los mejores candidatos de la etapa fuera de línea. Este es un estudio de caso y no tenemos usuarios reales, por lo que no cubriremos las pruebas AB.
¿Qué más debemos considerar antes de presentar a un candidato a la prueba AB? ¿En qué podemos pensar durante la etapa fuera de línea para ahorrarnos algo de tiempo de prueba en línea y asegurarnos de que realmente estamos probando la mejor solución posible?
La precisión equilibrada es excelente, pero esta puntuación no responde a la pregunta "¿Cómo afectará exactamente el modelo al producto?". Para encontrar una puntuación más orientada al producto debemos entender cómo vamos a utilizar el modelo.
En nuestro medio, cometer un error es peor que no dar respuesta, porque el vendedor tendrá que darse cuenta del error y cambiar la categoría manualmente. Un error desapercibido disminuirá las ventas y empeorará la experiencia del usuario del vendedor, corremos el riesgo de perder clientes.
Para evitar eso, elegiremos umbrales para la puntuación del modelo de modo que solo nos permitamos un 1% de errores. La métrica orientada al producto se puede establecer de la siguiente manera:
¿Qué porcentaje de elementos podemos categorizar automáticamente si nuestra tolerancia a errores es solo del 1%?
Nos referiremos a esto como Automatic categorisation percentage
a continuación cuando seleccionemos el mejor modelo. Encuentre el código de selección de umbral completo aquí .
¿Cuánto tiempo le toma a un modelo procesar una solicitud?
Esto nos permitirá comparar aproximadamente cuántos recursos más tendremos que mantener para que un servicio maneje la carga de tareas si se selecciona un modelo sobre otro.
Cuando nuestro producto vaya a crecer, ¿qué tan fácil será gestionar el crecimiento utilizando una arquitectura determinada?
Por crecimiento podríamos entender:
¿Tendremos que repensar la elección del modelo para hacer frente al crecimiento o bastará con un simple reentrenamiento?
¿Qué tan fácil será depurar los errores del modelo durante el entrenamiento y después de la implementación?
El tamaño del modelo importa si:
Más adelante veremos que los dos elementos anteriores no son relevantes, pero aún así vale la pena considerarlos brevemente.
¿Con qué estamos trabajando? ¡Miremos los datos y veamos si es necesario limpiarlos!
El conjunto de datos contiene 2 columnas: descripción del artículo y categoría, un total de 50,5k filas.
file_name = "ecommerceDataset.csv" data = pd.read_csv(file_name, header=None) data.columns = ["category", "description"] print("Rows, cols:", data.shape) # >>> Rows, cols: (50425, 2)
A cada artículo se le asigna 1 de las 4 categorías disponibles: Household
, Books
, Electronics
o Clothing & Accessories
. Aquí hay 1 ejemplo de descripción de artículo por categoría:
Hogar SPK Decoración del hogar Cara colgante de pared hecha a mano de arcilla (multicolor, alto 35 x ancho 12 cm) Haga su hogar más hermoso con esta máscara facial india de terracota hecha a mano para colgar en la pared, nunca antes no se puede encontrar esta cosa hecha a mano en el mercado. Puede agregar esto a su sala de estar/vestíbulo de entrada.
Libros BEGF101/FEG1-Curso básico en inglés-1 (Publicaciones Neeraj edición 2018) BEGF101/FEG1-Curso básico en inglés-1
Ropa y accesorios Peto vaquero para mujer Broadstar Obtenga un pase de acceso total con peto de Broadstar. Confeccionados en denim, estos petos te mantendrán cómodo. Combínalos con un top de color blanco o negro para completar tu look casual.
Electronics Caprigo Heavy Duty - Soporte de montaje en techo para proyector premium de 2 pies (ajustable - blanco - Capacidad de peso 15 kg)
Solo hay un valor vacío en el conjunto de datos, que vamos a eliminar.
print(data.info()) # <class 'pandas.core.frame.DataFrame'> # RangeIndex: 50425 entries, 0 to 50424 # Data columns (total 2 columns): # # Column Non-Null Count Dtype # --- ------ -------------- ----- # 0 category 50425 non-null object # 1 description 50424 non-null object # dtypes: object(2) # memory usage: 788.0+ KB data.dropna(inplace=True)
Sin embargo, hay bastantes descripciones duplicadas. Afortunadamente, todos los duplicados pertenecen a una categoría, por lo que podemos eliminarlos de forma segura.
repeated_messages = data \ .groupby("description", as_index=False) \ .agg( n_repeats=("category", "count"), n_unique_categories=("category", lambda x: len(np.unique(x))) ) repeated_messages = repeated_messages[repeated_messages["n_repeats"] > 1] print(f"Count of repeated messages (unique): {repeated_messages.shape[0]}") print(f"Total number: {repeated_messages['n_repeats'].sum()} out of {data.shape[0]}") # >>> Count of repeated messages (unique): 13979 # >>> Total number: 36601 out of 50424
Después de eliminar los duplicados, nos queda el 55% del conjunto de datos original. El conjunto de datos está bien equilibrado.
data.drop_duplicates(inplace=True) print(f"New dataset size: {data.shape}") print(data["category"].value_counts()) # New dataset size: (27802, 2) # Household 10564 # Books 6256 # Clothing & Accessories 5674 # Electronics 5308 # Name: category, dtype: int64
Tenga en cuenta que, según la descripción del conjunto de datos,
El conjunto de datos se extrajo de la plataforma de comercio electrónico india.
Las descripciones no están necesariamente escritas en inglés. Algunos de ellos están escritos en hindi u otros idiomas utilizando símbolos que no son ASCII o transliterados al alfabeto latino, o utilizan una combinación de idiomas. Ejemplos de la categoría Books
:
यू जी सी – नेट जूनियर रिसर्च फैलोशिप एवं सहायक प्रोफेसर योग्यता …
Prarambhik Bhartiy Itihas
History of NORTH INDIA/வட இந்திய வரலாறு/ …
Para evaluar la presencia de palabras no inglesas en las descripciones, calculemos 2 puntuaciones:
Word2Vec-300
entrenado en un corpus en inglés.
Utilizando la puntuación ASCII aprendemos que sólo el 2,3% de las descripciones constan de más del 1% de símbolos no ASCII.
def get_ascii_score(description): total_sym_cnt = 0 ascii_sym_cnt = 0 for sym in description: total_sym_cnt += 1 if sym.isascii(): ascii_sym_cnt += 1 return ascii_sym_cnt / total_sym_cnt data["ascii_score"] = data["description"].apply(get_ascii_score) data[data["ascii_score"] < 0.99].shape[0] / data.shape[0] # >>> 0.023
La puntuación de palabras válidas en inglés muestra que solo el 1,5% de las descripciones tienen menos del 70% de palabras válidas en inglés entre las palabras ASCII.
w2v_eng = gensim.models.KeyedVectors.load_word2vec_format(w2v_path, binary=True) def get_valid_eng_score(description): description = re.sub("[^az \t]+", " ", description.lower()) total_word_cnt = 0 eng_word_cnt = 0 for word in description.split(): total_word_cnt += 1 if word.lower() in w2v_eng: eng_word_cnt += 1 return eng_word_cnt / total_word_cnt data["eng_score"] = data["description"].apply(get_valid_eng_score) data[data["eng_score"] < 0.7].shape[0] / data.shape[0] # >>> 0.015
Por lo tanto, la mayoría de las descripciones (alrededor del 96%) están en inglés o principalmente en inglés. Podemos eliminar todas las demás descripciones, pero en su lugar, dejémoslas como están y luego veamos cómo las maneja cada modelo.
Dividamos nuestro conjunto de datos en 3 grupos:
Entrenar 70% - para entrenar los modelos (19k mensajes)
Prueba 15 %: para elegir parámetros y umbrales (4,1 mil mensajes)
Evaluación 15% - por elegir el modelo final (4,1k mensajes)
from sklearn.model_selection import train_test_split data_train, data_test = train_test_split(data, test_size=0.3) data_test, data_eval = train_test_split(data_test, test_size=0.5) data_train.shape, data_test.shape, data_eval.shape # >>> ((19461, 3), (4170, 3), (4171, 3))
Es útil hacer algo sencillo y trivial al principio para obtener una buena base. Como punto de partida, creemos una estructura de bolsa de palabras basada en el conjunto de datos del tren.
También limitemos el tamaño del diccionario a 100 palabras.
count_vectorizer = CountVectorizer(max_features=100, stop_words="english") x_train_baseline = count_vectorizer.fit_transform(data_train["description"]) y_train_baseline = data_train["category"] x_test_baseline = count_vectorizer.transform(data_test["description"]) y_test_baseline = data_test["category"] x_train_baseline = x_train_baseline.toarray() x_test_baseline = x_test_baseline.toarray()
Estoy planeando utilizar la regresión logística como modelo, por lo que necesito normalizar las funciones del contador antes del entrenamiento.
ss = StandardScaler() x_train_baseline = ss.fit_transform(x_train_baseline) x_test_baseline = ss.transform(x_test_baseline) lr = LogisticRegression() lr.fit(x_train_baseline, y_train_baseline) balanced_accuracy_score(y_test_baseline, lr.predict(x_test_baseline)) # >>> 0.752
La regresión logística multiclase mostró una precisión equilibrada del 75,2%. ¡Esta es una excelente base!
Aunque la calidad general de la clasificación no es excelente, el modelo aún puede brindarnos algunas ideas. Veamos la matriz de confusión, normalizada por el número de etiquetas predichas. El eje X indica la categoría predicha y el eje Y, la categoría real. Al observar cada columna, podemos ver la distribución de categorías reales cuando se predijo una determinada categoría.
Por ejemplo, Electronics
se confunde frecuentemente con Household
. Pero incluso este modelo simple puede capturar Clothing & Accessories
con bastante precisión.
A continuación se detallan las características importantes al predecir la categoría Clothing & Accessories
:
Las 6 palabras que más contribuyen a y en contra de la categoría Clothing & Accessories
:
women 1.49 book -2.03 men 0.93 table -1.47 cotton 0.92 author -1.11 wear 0.69 books -1.10 fit 0.40 led -0.90 stainless 0.36 cable -0.85
Ahora consideremos modelos más avanzados, diseñados específicamente para trabajar con secuencias: redes neuronales recurrentes . GRU y LSTM son capas avanzadas comunes para combatir los gradientes explosivos que ocurren en RNN simples.
Usaremos la biblioteca pytorch
para tokenizar descripciones y construir y entrenar un modelo.
Primero, necesitamos transformar textos en números:
El vocabulario que obtenemos simplemente tokenizando el conjunto de datos del tren es grande: casi 90.000 palabras. Cuantas más palabras tengamos, mayor será el espacio de incrustación que tendrá que aprender el modelo. Para simplificar la capacitación, eliminemos las palabras más raras y dejemos solo aquellas que aparecen en al menos el 3% de las descripciones. Esto truncará el vocabulario a 340 palabras.
(Encuentre la implementación completa CorpusDictionary
aquí )
corpus_dict = util.CorpusDictionary(data_train["description"]) corpus_dict.truncate_dictionary(min_frequency=0.03) data_train["vector"] = corpus_dict.transform(data_train["description"]) data_test["vector"] = corpus_dict.transform(data_test["description"]) print(data_train["vector"].head()) # 28453 [1, 1, 1, 1, 12, 1, 2, 1, 6, 1, 1, 1, 1, 1, 6,... # 48884 [1, 1, 13, 34, 3, 1, 1, 38, 12, 21, 2, 1, 37, ... # 36550 [1, 60, 61, 1, 62, 60, 61, 1, 1, 1, 1, 10, 1, ... # 34999 [1, 34, 1, 1, 75, 60, 61, 1, 1, 72, 1, 1, 67, ... # 19183 [1, 83, 1, 1, 87, 1, 1, 1, 12, 21, 42, 1, 2, 1... # Name: vector, dtype: object
Lo siguiente que debemos decidir es la longitud común de los vectores que vamos a introducir como entradas en RNN. No queremos utilizar vectores completos, porque la descripción más larga contiene 9,4k tokens.
Sin embargo, el 95% de las descripciones en el conjunto de datos del tren no tienen más de 352 tokens; esa es una buena longitud para recortar. ¿Qué va a pasar con las descripciones más cortas?
Serán acolchados con índice de relleno hasta la longitud normal.
print(max(data_train["vector"].apply(len))) # >>> 9388 print(int(np.quantile(data_train["vector"].apply(len), q=0.95))) # >>> 352
A continuación, necesitamos transformar las categorías objetivo en vectores 0-1 para calcular la pérdida y realizar una retropropagación en cada paso de entrenamiento.
def get_target(label, total_labels=4): target = [0] * total_labels target[label_2_idx.get(label)] = 1 return target data_train["target"] = data_train["category"].apply(get_target) data_test["target"] = data_test["category"].apply(get_target)
Ahora estamos listos para crear un conjunto de datos y un cargador de datos pytorch
personalizados para alimentar el modelo. Encuentre la implementación completa PaddedTextVectorDataset
aquí .
ds_train = util.PaddedTextVectorDataset( data_train["description"], data_train["target"], corpus_dict, max_vector_len=352, ) ds_test = util.PaddedTextVectorDataset( data_test["description"], data_test["target"], corpus_dict, max_vector_len=352, ) train_dl = DataLoader(ds_train, batch_size=512, shuffle=True) test_dl = DataLoader(ds_test, batch_size=512, shuffle=False)
Finalmente, construyamos un modelo.
La arquitectura mínima es:
Comenzando con valores pequeños de parámetros (tamaño del vector de incrustación, tamaño de una capa oculta en RNN, número de capas RNN) y sin regularización, podemos hacer que el modelo se vuelva gradualmente más complicado hasta que muestre fuertes signos de sobreajuste y luego equilibrarlo. regularización (abandonos en la capa RNN y antes de la última capa lineal).
class GRU(nn.Module): def __init__(self, vocab_size, embedding_dim, n_hidden, n_out): super().__init__() self.vocab_size = vocab_size self.embedding_dim = embedding_dim self.n_hidden = n_hidden self.n_out = n_out self.emb = nn.Embedding(self.vocab_size, self.embedding_dim) self.gru = nn.GRU(self.embedding_dim, self.n_hidden) self.dropout = nn.Dropout(0.3) self.out = nn.Linear(self.n_hidden, self.n_out) def forward(self, sequence, lengths): batch_size = sequence.size(1) self.hidden = self._init_hidden(batch_size) embs = self.emb(sequence) embs = pack_padded_sequence(embs, lengths, enforce_sorted=True) gru_out, self.hidden = self.gru(embs, self.hidden) gru_out, lengths = pad_packed_sequence(gru_out) dropout = self.dropout(self.hidden[-1]) output = self.out(dropout) return F.log_softmax(output, dim=-1) def _init_hidden(self, batch_size): return Variable(torch.zeros((1, batch_size, self.n_hidden)))
Usaremos el optimizador Adam
y cross_entropy
como función de pérdida.
vocab_size = len(corpus_dict.word_to_idx) emb_dim = 4 n_hidden = 15 n_out = len(label_2_idx) model = GRU(vocab_size, emb_dim, n_hidden, n_out) opt = optim.Adam(model.parameters(), 1e-2) util.fit( model=model, train_dl=train_dl, test_dl=test_dl, loss_fn=F.cross_entropy, opt=opt, epochs=35 ) # >>> Train loss: 0.3783 # >>> Val loss: 0.4730
Este modelo mostró una precisión equilibrada del 84,3 % en el conjunto de datos de evaluación. ¡Vaya, qué progreso!
La principal desventaja de entrenar el modelo RNN desde cero es que tiene que aprender el significado de las palabras en sí mismo; ese es el trabajo de la capa de incrustación. Los modelos word2vec
previamente entrenados están disponibles para usar como una capa de incrustación lista para usar, lo que reduce la cantidad de parámetros y agrega mucho más significado a los tokens. Usemos uno de los modelos word2vec
disponibles en pytorch
: glove, dim=300
.
Solo necesitamos realizar cambios menores en la creación del conjunto de datos; ahora queremos crear un vector de glove
predefinidos para cada descripción y la arquitectura del modelo.
ds_emb_train = util.PaddedTextVectorDataset( data_train["description"], data_train["target"], emb=glove, max_vector_len=max_len, ) ds_emb_test = util.PaddedTextVectorDataset( data_test["description"], data_test["target"], emb=glove, max_vector_len=max_len, ) dl_emb_train = DataLoader(ds_emb_train, batch_size=512, shuffle=True) dl_emb_test = DataLoader(ds_emb_test, batch_size=512, shuffle=False)
import torchtext.vocab as vocab glove = vocab.GloVe(name='6B', dim=300) class LSTMPretrained(nn.Module): def __init__(self, n_hidden, n_out): super().__init__() self.emb = nn.Embedding.from_pretrained(glove.vectors) self.emb.requires_grad_ = False self.embedding_dim = 300 self.n_hidden = n_hidden self.n_out = n_out self.lstm = nn.LSTM(self.embedding_dim, self.n_hidden, num_layers=1) self.dropout = nn.Dropout(0.5) self.out = nn.Linear(self.n_hidden, self.n_out) def forward(self, sequence, lengths): batch_size = sequence.size(1) self.hidden = self.init_hidden(batch_size) embs = self.emb(sequence) embs = pack_padded_sequence(embs, lengths, enforce_sorted=True) lstm_out, (self.hidden, _) = self.lstm(embs) lstm_out, lengths = pad_packed_sequence(lstm_out) dropout = self.dropout(self.hidden[-1]) output = self.out(dropout) return F.log_softmax(output, dim=-1) def init_hidden(self, batch_size): return Variable(torch.zeros((1, batch_size, self.n_hidden)))
¡Y estamos listos para entrenar!
n_hidden = 50 n_out = len(label_2_idx) emb_model = LSTMPretrained(n_hidden, n_out) opt = optim.Adam(emb_model.parameters(), 1e-2) util.fit(model=emb_model, train_dl=dl_emb_train, test_dl=dl_emb_test, loss_fn=F.cross_entropy, opt=opt, epochs=11)
Ahora obtenemos una precisión equilibrada del 93,7 % en el conjunto de datos de evaluación. ¡Cortejar!
Los modelos modernos de última generación para trabajar con secuencias son los transformadores. Sin embargo, para entrenar un transformador desde cero, necesitaríamos enormes cantidades de datos y recursos computacionales. Lo que podemos intentar aquí es ajustar uno de los modelos previamente entrenados para que sirva a nuestro propósito. Para hacer esto, necesitamos descargar un modelo BERT previamente entrenado y agregar una capa lineal y de abandono para obtener la predicción final. Se recomienda entrenar un modelo sintonizado durante 4 épocas. Entrené solo 2 épocas adicionales para ahorrar tiempo; me tomó 40 minutos hacerlo.
from transformers import BertModel class BERTModel(nn.Module): def __init__(self, n_out=12): super(BERTModel, self).__init__() self.l1 = BertModel.from_pretrained('bert-base-uncased') self.l2 = nn.Dropout(0.3) self.l3 = nn.Linear(768, n_out) def forward(self, ids, mask, token_type_ids): output_1 = self.l1(ids, attention_mask = mask, token_type_ids = token_type_ids) output_2 = self.l2(output_1.pooler_output) output = self.l3(output_2) return output
ds_train_bert = bert.get_dataset( list(data_train["description"]), list(data_train["target"]), max_vector_len=64 ) ds_test_bert = bert.get_dataset( list(data_test["description"]), list(data_test["target"]), max_vector_len=64 ) dl_train_bert = DataLoader(ds_train_bert, sampler=RandomSampler(ds_train_bert), batch_size=batch_size) dl_test_bert = DataLoader(ds_test_bert, sampler=SequentialSampler(ds_test_bert), batch_size=batch_size)
b_model = bert.BERTModel(n_out=4) b_model.to(torch.device("cpu")) def loss_fn(outputs, targets): return torch.nn.BCEWithLogitsLoss()(outputs, targets) optimizer = optim.AdamW(b_model.parameters(), lr=2e-5, eps=1e-8) epochs = 2 scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=0, num_training_steps=total_steps ) bert.fit(b_model, dl_train_bert, dl_test_bert, optimizer, scheduler, loss_fn, device, epochs=epochs) torch.save(b_model, "models/bert_fine_tuned")
Registro de entrenamiento:
2024-02-29 19:38:13.383953 Epoch 1 / 2 Training... 2024-02-29 19:40:39.303002 step 40 / 305 done 2024-02-29 19:43:04.482043 step 80 / 305 done 2024-02-29 19:45:27.767488 step 120 / 305 done 2024-02-29 19:47:53.156420 step 160 / 305 done 2024-02-29 19:50:20.117272 step 200 / 305 done 2024-02-29 19:52:47.988203 step 240 / 305 done 2024-02-29 19:55:16.812437 step 280 / 305 done 2024-02-29 19:56:46.990367 Average training loss: 0.18 2024-02-29 19:56:46.990932 Validating... 2024-02-29 19:57:51.182859 Average validation loss: 0.10 2024-02-29 19:57:51.182948 Epoch 2 / 2 Training... 2024-02-29 20:00:25.110818 step 40 / 305 done 2024-02-29 20:02:56.240693 step 80 / 305 done 2024-02-29 20:05:25.647311 step 120 / 305 done 2024-02-29 20:07:53.668489 step 160 / 305 done 2024-02-29 20:10:33.936778 step 200 / 305 done 2024-02-29 20:13:03.217450 step 240 / 305 done 2024-02-29 20:15:28.384958 step 280 / 305 done 2024-02-29 20:16:57.004078 Average training loss: 0.08 2024-02-29 20:16:57.004657 Validating... 2024-02-29 20:18:01.546235 Average validation loss: 0.09
Finalmente, el modelo BERT ajustado muestra una precisión equilibrada del 95,1% en el conjunto de datos de evaluación.
Ya hemos establecido una lista de consideraciones a tener en cuenta para tomar una decisión final bien informada.
Aquí hay gráficos que muestran parámetros mensurables:
Aunque BERT ajustado es líder en calidad, RNN con capa de incrustación previamente entrenada LSTM+EMB
ocupa el segundo lugar, quedando atrás solo en un 3% de las asignaciones automáticas de categorías.
Por otro lado, el tiempo de inferencia de BERT ajustado es 14 veces más largo que el LSTM+EMB
. Esto se sumará a los costos de mantenimiento del backend que probablemente superarán los beneficios que aporta BERT
ajustado sobre LSTM+EMB
.
En cuanto a la interoperabilidad, nuestro modelo de regresión logística de referencia es, con diferencia, el más interpretable y cualquier red neuronal pierde en este sentido. Al mismo tiempo, la línea de base es probablemente la menos escalable: agregar categorías disminuirá la ya baja calidad de la línea de base.
Aunque BERT parece la superestrella por su alta precisión, terminamos optando por el RNN con una capa de incrustación previamente entrenada. ¿Por qué? Es bastante preciso, no demasiado lento y no resulta demasiado complicado de manejar cuando las cosas se ponen grandes.
Espero que hayas disfrutado de este estudio de caso. ¿Qué modelo habrías elegido y por qué?