paint-brush
Un estudio de caso de clasificación de textos con aprendizaje automático con un toque impulsado por el productopor@bemorelavender
29,341 lecturas
29,341 lecturas

Un estudio de caso de clasificación de textos con aprendizaje automático con un toque impulsado por el producto

por Maria K17m2024/03/12
Read on Terminal Reader

Demasiado Largo; Para Leer

Este es un estudio de caso de aprendizaje automático con un giro impulsado por el producto: 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.
featured image - Un estudio de caso de clasificación de textos con aprendizaje automático con un toque impulsado por el producto
Maria K HackerNoon profile picture


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!

Configuración de tareas

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 Conjunto de datos de texto de comercio electrónico Zenodo , que contiene descripciones y categorías de artículos.


¿Bueno o malo? Cómo elegir el mejor modelo

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?


Convertir métricas técnicas en métricas orientadas al impacto

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í .


tiempo de inferencia

¿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.


Escalabilidad

Cuando nuestro producto vaya a crecer, ¿qué tan fácil será gestionar el crecimiento utilizando una arquitectura determinada?


Por crecimiento podríamos entender:

  • más categorías, mayor granularidad de categorías
  • descripciones más largas
  • conjuntos de datos más grandes
  • etc.

¿Tendremos que repensar la elección del modelo para hacer frente al crecimiento o bastará con un simple reentrenamiento?


Interpretabilidad

¿Qué tan fácil será depurar los errores del modelo durante el entrenamiento y después de la implementación?


Tamaño del modelo

El tamaño del modelo importa si:

  • queremos que nuestro modelo sea evaluado en el lado del cliente
  • es tan grande que no cabe en la RAM


Más adelante veremos que los dos elementos anteriores no son relevantes, pero aún así vale la pena considerarlos brevemente.

Exploración y limpieza de conjuntos de datos

¿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)


Valores faltantes

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)


Duplicados

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


Idioma de descripción

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:


  • Puntuación ASCII: porcentaje de símbolos no ASCII en una descripción
  • Puntuación de palabras válidas en inglés: si consideramos solo letras latinas, ¿qué porcentaje de palabras en la descripción son válidas en inglés? Digamos que las palabras en inglés válidas son las que están presentes en 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.

Modelado

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))


Modelo de referencia: bolsa de palabras + regresión logística

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.


Matriz de confusión para la solución inicial.


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 :

Importancia de las características para la solución básica para la etiqueta 'Ropa y accesorios'


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


RNN

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:

  1. Dividir descripciones en palabras
  2. Asigne un índice a cada palabra del corpus según el conjunto de datos de entrenamiento
  3. Reserve índices especiales para palabras desconocidas y relleno
  4. Transforme cada descripción en conjuntos de datos de entrenamiento y prueba en vectores de índices.


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:

  • capa de incrustación
  • capa RNN
  • capa lineal
  • capa de activación


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 

Pérdidas de entrenamiento y prueba por época, modelo RNN

Este modelo mostró una precisión equilibrada del 84,3 % en el conjunto de datos de evaluación. ¡Vaya, qué progreso!


Introducción de incrustaciones previamente entrenadas

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) 

Pérdidas de entrenamiento y prueba por época, modelo RNN + incorporaciones previamente entrenadas

Ahora obtenemos una precisión equilibrada del 93,7 % en el conjunto de datos de evaluación. ¡Cortejar!


BERT

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.


Eligiendo a nuestro ganador

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:

Métricas de rendimiento de los modelos


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é?