Según tengo entendido, como muchos dulces, o la clasificación de los productos por cheque en la solicitud

Desafío


En este artículo, queremos hablar sobre cómo creamos una solución para clasificar los nombres de productos de los recibos en la aplicación para registrar los gastos de cheques y el asistente de compras. Queríamos darles a los usuarios la oportunidad de ver estadísticas sobre compras, recopiladas automáticamente sobre la base de recibos escaneados, a saber, distribuir todos los productos comprados por el usuario por categoría. Porque obligar al usuario a agrupar productos de forma independiente ya es el siglo pasado. Existen varios enfoques para resolver este problema: puede intentar aplicar algoritmos de agrupamiento con diferentes formas de representación vectorial de palabras o algoritmos de clasificación clásicos. No hemos inventado nada nuevo, y en este artículo solo queremos compartir una pequeña guía sobre una posible solución al problema, ejemplos de cómo no hacerlo, un análisis de por qué otros métodos no funcionaron y qué problemas podría encontrar en el proceso.

Agrupamiento


Uno de los problemas era que los nombres de los productos que obtenemos de los cheques no siempre son fáciles de descifrar, incluso para una persona. Es poco probable que sepa qué tipo de producto con el nombre "UTRUSTA krnsht" se compró en una de las tiendas rusas. Los verdaderos conocedores del diseño sueco ciertamente nos responderán de inmediato: Soporte para el horno de Utrust, pero mantener a tales especialistas en la sede es bastante costoso. Además, no teníamos una muestra etiquetada y preparada adecuada para nuestros datos, en la que pudiéramos capacitar al modelo. Por lo tanto, primero hablaremos sobre cómo, en ausencia de datos para el entrenamiento, aplicamos algoritmos de agrupamiento y por qué no nos gustó.

Dichos algoritmos se basan en la medición de distancias entre objetos, lo que requiere su representación vectorial o el uso de una métrica para medir la similitud de las palabras (por ejemplo, la distancia de Levenshtein). En este paso, la dificultad radica en la representación vectorial significativa de los nombres. Es problemático extraer propiedades de los nombres que describirán completa y exhaustivamente el producto y su relación con otros productos.

La opción más fácil es usar Tf-Idf, pero en este caso la dimensión del espacio vectorial es bastante grande, y el espacio en sí es escaso. Además, este enfoque no extrae ninguna información adicional de los nombres. Por lo tanto, en un grupo puede haber muchos productos de diferentes categorías, unidos por una palabra común, como, por ejemplo, "papa" o "ensalada":


Tampoco podemos controlar qué grupos se ensamblarán. Lo único que se puede indicar es la cantidad de clústeres (si se utilizan algoritmos basados ​​en picos de no densidad en el espacio). Pero si especifica una cantidad demasiado pequeña, se forma un clúster enorme, que contendrá todos los nombres que no caben en otros clústeres. Si especifica uno suficientemente grande, luego de que el algoritmo funcione, tendremos que examinar cientos de clústeres y combinarlos en categorías semánticas a mano.

Las siguientes tablas proporcionan información sobre los clústeres que utilizan los algoritmos KMeans y Tf-Idf para la representación vectorial. De estas tablas vemos que las distancias entre los centros de los grupos son menores que la distancia promedio entre los objetos y los centros de los grupos a los que pertenecen. Dichos datos pueden explicarse por el hecho de que en el espacio de los vectores no hay picos de densidad obvios y los centros de los grupos están ubicados alrededor del círculo, donde la mayoría de los objetos se encuentran fuera de este círculo. Además, se forma un grupo, que contiene la mayoría de los vectores. Lo más probable en este grupo son los nombres que contienen palabras que se encuentran con más frecuencia que otras entre todos los productos de diferentes categorías.

Tabla 1. Distancias entre grupos.
RacimoC1C2C3C4C5C6C7C8C9
C10.00,5020.3540.4750,4810,5270,4980,5010,524
C20,5020.00.6140,6850,6960,7280,7060,7090,725
C30.3540.6140.00,5900,5970.6350.6100.6130.632
C40.4750,6850,5900.00,6730,7090,6830,6870.699
C50,4810,6960,5970,6730.00,7150,6920,6940.711
C60,5270,7270.6350,7090,7150.00,7260,7280,741
C70,4980,7060.6100,6830,6920,7250.00,7070,714
C80,5010,7090.6120,6870,6940,7280,7070.00,725
C90,5240,7250.6320.6990.7110,7410,7140,7250.0

Tabla 2. Información breve sobre clústeres
RacimoNumero de objetosDistancia mediaDistancia minimaDistancia máxima
C1625300,9990,0411.001
C221590.8640,5270.964
C310990.9340,7560,993
C412920.8790,7330.980
C57460.8750,7310.965
C624510.8470,7190,994
C711330.8660,7240,996
C88760.8630,7040,999
C918790.8490,5260,981


Pero en algunos lugares los grupos resultan ser bastante decentes, como, por ejemplo, en la imagen a continuación: casi todos los productos son comida para gatos.



Doc2Vec es otro de los algoritmos que le permiten representar textos en forma vectorial. Usando este enfoque, cada nombre será descrito por un vector de menor dimensión que usando Tf-Idf. En el espacio vectorial resultante, los textos similares estarán cerca uno del otro, y los diferentes estarán muy lejos.

Este enfoque puede resolver el problema de gran dimensión y espacio descargado obtenido por el método Tf-Idf. Para este algoritmo, utilizamos la opción más simple de tokenización: dividimos el nombre en palabras separadas y tomamos sus formas iniciales. Fue entrenado en datos de esta manera:

max_epochs = 100 vec_size = 20 alpha = 0.025 model = doc2vec.Doc2Vec(vector_size=vec_size, alpha=alpha, min_alpha=0.00025, min_count=1, dm =1) model.build_vocab(train_corpus) for epoch in range(max_epochs): print('iteration {0}'.format(epoch)) model.train(train_corpus, total_examples=model.corpus_count, epochs=model.iter) # decrease the learning rate model.alpha -= 0.0002 # fix the learning rate, no decay model.min_alpha = model.epochs 

Pero con este enfoque, obtuvimos vectores que no llevan información sobre el nombre; con el mismo éxito, puede usar valores aleatorios. Aquí hay un ejemplo del funcionamiento del algoritmo: la imagen muestra productos similares en la opinión del algoritmo al "pan Borodino de la forma n pn 0.45k".


Quizás el problema esté en la longitud y el contexto de los nombres: el pase en el nombre "__ club. Banana 200ml" puede ser yogurt, jugo o una lata grande de crema. Puede lograr un mejor resultado utilizando un enfoque diferente para la tokenización de nombres. No teníamos experiencia en el uso de este método, y cuando los primeros intentos fallaron, ya encontramos un par de conjuntos marcados con nombres de productos, por lo que decidimos abandonar temporalmente este método y cambiar a algoritmos de clasificación.

Clasificación


Preprocesamiento de datos


Los nombres de los productos de los cheques nos llegan de una manera no siempre clara: el latín y el cirílico se mezclan en palabras. Por ejemplo, la letra "a" se puede reemplazar por "a" en latín, y esto aumenta el número de nombres únicos; por ejemplo, las palabras "leche" y "leche" se considerarán diferentes. Los nombres también contienen muchos otros errores tipográficos y abreviaturas.

Examinamos nuestra base de datos y encontramos errores típicos en los nombres. En esta etapa, prescindimos de las expresiones regulares, con la ayuda de las cuales limpiamos los nombres y los llevamos a una cierta visión general. Con este enfoque, el resultado aumenta aproximadamente un 7%. Junto con una opción simple de clasificador SGD basada en la función de pérdida de Huber con parámetros retorcidos, obtuvimos una precisión del 81% para F1 (precisión promedio para todas las categorías de productos).

 sgd_model = SGDClassifier() parameters_sgd = { 'max_iter':[100], 'loss':['modified_huber'], 'class_weight':['balanced'], 'penalty':['l2'], 'alpha':[0.0001] } sgd_cv = GridSearchCV(sgd_model, parameters_sgd,n_jobs=-1) sgd_cv.fit(tf_idf_data, prod_cat) sgd_cv.best_score_, sgd_cv.best_params_ 

Además, no olvide que algunas categorías de personas compran con más frecuencia que otras: por ejemplo, "Té y dulces" y "Verduras y frutas" son mucho más populares que "Servicios" y "Cosméticos". Con tal distribución de datos, es mejor usar algoritmos que le permitan establecer pesos (grado de importancia) para cada clase. El peso de la clase se puede determinar inversamente con el valor igual a la relación entre el número de productos en la clase y el número total de productos. Pero no tiene que pensarlo, porque en la implementación de estos algoritmos, es posible determinar automáticamente el peso de las categorías.


Obteniendo nuevos datos para entrenamiento


Nuestra aplicación requería categorías ligeramente diferentes a las que se usaron en la competencia, y los nombres de los productos de nuestra base de datos fueron significativamente diferentes de los presentados en el concurso. Por lo tanto, necesitábamos marcar los productos de nuestros recibos. Intentamos hacer esto por nuestra cuenta, pero nos dimos cuenta de que incluso si conectamos a todo nuestro equipo, tomará mucho tiempo. Por lo tanto, decidimos usar el "Toloka" de Yandex .

Allí usamos esta forma de asignación:

  • en cada celda presentamos un producto, cuya categoría debe definirse
  • su hipotética categoría definida por uno de nuestros modelos anteriores
  • campo de respuesta (si la categoría propuesta era incorrecta)

Creamos instrucciones detalladas con ejemplos que explicaban las características de cada categoría, y también utilizamos métodos de control de calidad: un conjunto con respuestas estándar que se mostraban junto con las tareas habituales (implementamos las respuestas estándar nosotros mismos, marcando varios cientos de productos). Según los resultados de las respuestas a estas tareas, los usuarios que marcaron incorrectamente los datos fueron eliminados. Sin embargo, para todo el proyecto, prohibimos solo tres de los más de 600 usuarios.


Con los nuevos datos, obtuvimos un modelo que mejor se adaptaba a nuestros datos, y la precisión aumentó un poco más (en ~ 11%) y ya obtuvo un 92%.

Modelo final


Comenzamos el proceso de clasificación con una combinación de datos de varios conjuntos de datos con Kaggle - 74%, después de lo cual mejoramos el preprocesamiento - 81%, recopilamos un nuevo conjunto de datos - 92% y finalmente mejoramos el proceso de clasificación: inicialmente, usando regresión logística obtenemos probabilidades preliminares de bienes pertenecientes SGD dio mayor precisión a las categorías basadas en los nombres de los productos, pero aún tenía grandes valores en las funciones de pérdida, lo que afectó gravemente los resultados del clasificador final. Además, combinamos los datos obtenidos con otros datos sobre el producto (precio del producto, la tienda en la que fue comprado, estadísticas de la tienda, cheque y otra metainformación), y XGBoost está capacitado en todo este volumen de datos, lo que dio una precisión del 98% (aumento otro 6%). Al final resultó que, la mayor contribución fue hecha por la calidad de la muestra de capacitación.

Corriendo en el servidor


Para acelerar la implementación, creamos un servidor simple en Flask to Docker. Hubo un método que recibió bienes del servidor que necesitaba ser categorizado y ya devolvió bienes con categorías. Por lo tanto, nos integramos fácilmente en el sistema existente, cuyo centro era Tomcat, y no tuvimos que hacer cambios en la arquitectura, solo le agregamos un bloque más.

Fecha de lanzamiento


Hace unas semanas publicamos un lanzamiento de categorización en Google Play (aparecerá en la App Store después de un tiempo). Resultó así:


En futuras versiones, planeamos agregar la capacidad de corregir categorías, lo que nos permitirá recopilar rápidamente errores de categorización y volver a entrenar el modelo de categorización (mientras lo hacemos nosotros mismos).

Concursos mencionados en Kaggle:

www.kaggle.com/c/receipt-categorisation
www.kaggle.com/c/market-basket-analysis
www.kaggle.com/c/prod-price-prediction

Source: https://habr.com/ru/post/es430216/


All Articles