Pelo que entendi, como muitos doces ou a classificação de mercadorias, verificando o aplicativo

Desafio


Neste artigo, queremos falar sobre como criamos uma solução para classificar nomes de produtos a partir de recibos no aplicativo para registrar despesas de cheques e o assistente de compras. Queríamos dar aos usuários a oportunidade de visualizar estatísticas de compras, coletadas automaticamente com base em recibos digitalizados, ou seja, para distribuir todos os bens adquiridos pelo usuário por categoria. Porque forçar o usuário a agrupar produtos de forma independente já é o século passado. Existem várias abordagens para resolver esse problema: você pode tentar aplicar algoritmos de agrupamento com diferentes formas de representação vetorial de palavras ou algoritmos de classificação clássica. Não inventamos nada de novo e, neste artigo, queremos compartilhar apenas um pequeno guia sobre uma possível solução para o problema, exemplos de como não fazer isso, uma análise de por que outros métodos não funcionaram e quais problemas você pode encontrar no processo.

Agrupamento


Um dos problemas era que nem sempre é fácil decifrar os nomes das mercadorias que recebemos dos cheques, mesmo para uma pessoa. É improvável que você saiba que tipo de produto com o nome "UTRUSTA krnsht" foi comprado em uma das lojas russas? Os verdadeiros conhecedores do design sueco certamente nos responderão imediatamente: suporte para o forno da Utrust, mas manter esses especialistas na sede é bastante caro. Além disso, não tínhamos uma amostra rotulada pronta para os nossos dados, na qual pudéssemos treinar o modelo. Portanto, primeiro falaremos sobre como, na ausência de dados para treinamento, aplicamos algoritmos de agrupamento e por que não gostamos.

Esses algoritmos são baseados na medição de distâncias entre objetos, o que requer sua representação vetorial ou o uso de uma métrica para medir a similaridade das palavras (por exemplo, distância de Levenshtein). Nesta etapa, a dificuldade está na representação significativa do vetor dos nomes. É problemático extrair propriedades dos nomes que descrevem de maneira completa e abrangente o produto e seu relacionamento com outros produtos.

A opção mais fácil é usar Tf-Idf, mas neste caso a dimensão do espaço vetorial é bastante grande e o espaço em si é escasso. Além disso, essa abordagem não extrai nenhuma informação adicional dos nomes. Assim, em um cluster, pode haver muitos produtos de diferentes categorias, unidos por uma palavra comum, como, por exemplo, “batata” ou “salada”:


Também não podemos controlar quais clusters serão montados. A única coisa que pode ser indicada é o número de clusters (se forem utilizados algoritmos baseados em picos de não densidade no espaço). Mas se você especificar uma quantidade muito pequena, será formado um cluster enorme, que conterá todos os nomes que não cabem em outros clusters. Se você especificar um tamanho suficientemente grande, depois que o algoritmo funcionar, teremos que examinar centenas de clusters e combiná-los manualmente em categorias semânticas.

As tabelas abaixo fornecem informações sobre clusters usando os algoritmos KMeans e Tf-Idf para representação vetorial. A partir dessas tabelas, vemos que as distâncias entre os centros dos aglomerados são menores que a distância média entre os objetos e os centros dos aglomerados aos quais pertencem. Esses dados podem ser explicados pelo fato de que no espaço de vetores não existem picos de densidade óbvios e os centros dos aglomerados estão localizados em um círculo, onde a maioria dos objetos está localizada fora desse círculo. Além disso, um cluster é formado, que contém a maioria dos vetores. Provavelmente, neste cluster, os nomes que contêm palavras são encontradas com mais frequência do que outros entre todos os produtos de categorias diferentes.

Tabela 1. Distâncias entre clusters.
ClusterC1C2C3C4C5C6C7C8C9
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

Tabela 2. Informações Breves sobre Clusters
ClusterNúmero de objetosDistância médiaDistância mínimaDistância 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,986
C88760.8630,7040,999
C918790.8490,5260,981


Mas, em alguns lugares, os grupos são bastante decentes, como, por exemplo, na imagem abaixo - quase todos os produtos são comida de gato.



Doc2Vec é outro dos algoritmos que permitem representar textos em forma de vetor. Usando essa abordagem, cada nome será descrito por um vetor de menor dimensão do que usando Tf-Idf. No espaço vetorial resultante, textos semelhantes ficarão próximos uns dos outros e textos distantes.

Essa abordagem pode resolver o problema de grandes dimensões e espaço descarregado obtido pelo método Tf-Idf. Para esse algoritmo, usamos a opção mais simples de tokenização: dividimos o nome em palavras separadas e assumimos suas formas iniciais. Ele foi treinado em dados desta maneira:

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 

Mas com essa abordagem, obtivemos vetores que não carregam informações sobre o nome - com o mesmo sucesso, você pode usar valores aleatórios. Aqui está um exemplo da operação do algoritmo: a imagem mostra produtos similares na opinião do algoritmo ao “pão Borodino da forma n pn 0,45k”.


Talvez o problema esteja no tamanho e no contexto dos nomes: o passe no nome "__ club. Banana 200ml" pode ser iogurte, suco ou uma lata grande de creme. Você pode obter um resultado melhor usando uma abordagem diferente para denominar a tokenização. Não tínhamos experiência com esse método e, quando as primeiras tentativas falharam, já encontramos alguns conjuntos marcados com nomes de produtos, por isso decidimos deixar temporariamente esse método e mudar para algoritmos de classificação.

Classificação


Pré-processamento de dados


Os nomes das mercadorias dos cheques chegam até nós de uma maneira nem sempre clara: latim e cirílico são misturados em palavras. Por exemplo, a letra "a" pode ser substituída por "a" latino e isso aumenta o número de nomes exclusivos - por exemplo, as palavras "leite" e "leite" serão consideradas diferentes. Os nomes também contêm muitos outros erros de digitação e abreviações.

Examinamos nosso banco de dados e encontramos erros típicos nos nomes. Nesse estágio, dispensamos expressões regulares, com a ajuda de que limpamos os nomes e os levamos a uma certa visão geral. Usando essa abordagem, o resultado é aumentado em aproximadamente 7%. Juntamente com uma versão simples do SGD Classifier baseada na função de perda Huber com parâmetros distorcidos, obtivemos uma precisão de 81% para F1 (precisão média para todas as categorias de produtos).

 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_ 

Além disso, não esqueça que algumas categorias de pessoas compram com mais frequência do que outras: por exemplo, “Chá e doces” e “Legumes e frutas” são muito mais populares que “Serviços” e “Cosméticos”. Com essa distribuição de dados, é melhor usar algoritmos que permitem definir pesos (grau de importância) para cada classe. O peso da classe pode ser determinado inversamente com o valor igual à razão entre o número de produtos na classe e o número total de produtos. Mas você não precisa pensar nisso, porque na implementação desses algoritmos, é possível determinar automaticamente o peso das categorias.


Obtendo novos dados para treinamento


Nossa aplicação exigia categorias ligeiramente diferentes daquelas usadas na competição, e os nomes dos produtos do nosso banco de dados eram significativamente diferentes daqueles apresentados no concurso. Portanto, precisávamos marcar as mercadorias em nossos recebimentos. Tentamos fazer isso sozinhos, mas percebemos que, mesmo se conectarmos toda a equipe, levará muito tempo. Portanto, decidimos usar o “Toloka” da Yandex.

Lá usamos esta forma de atribuição:

  • em cada célula, apresentamos um produto, cuja categoria deve ser definida
  • sua categoria hipotética definida por um de nossos modelos anteriores
  • campo de resposta (se a categoria proposta estiver incorreta)

Criamos instruções detalhadas com exemplos que explicam os recursos de cada categoria e também usamos métodos de controle de qualidade: um conjunto com respostas padrão que foram mostradas juntamente com as tarefas habituais (implementamos as respostas padrão, marcando várias centenas de produtos). De acordo com os resultados das respostas a essas tarefas, os usuários que marcaram incorretamente os dados foram rastreados. No entanto, para todo o projeto, banimos apenas três dos mais de 600 usuários.


Com os novos dados, obtivemos um modelo que melhor se adequava aos nossos dados, e a precisão aumentou um pouco mais (~ 11%) e já obteve 92%.

Modelo final


Iniciamos o processo de classificação com uma combinação de dados de vários conjuntos de dados com o Kaggle - 74%, após o que melhoramos o pré-processamento - 81%, coletamos um novo conjunto de dados - 92% e finalmente melhoramos o processo de classificação: inicialmente, usando regressão logística, obtemos probabilidades preliminares de bens pertencentes O SGD deu maior precisão às categorias com base nos nomes dos produtos, mas ainda possuía grandes valores nas funções de perda, o que afetou muito os resultados do classificador final. Além disso, combinamos os dados obtidos com outros dados sobre o produto (preço do produto, a loja em que foi comprado, estatísticas sobre a loja, cheques e outras meta-informações), e o XGBoost é treinado em todo esse volume de dados, o que deu uma precisão de 98% (aumento outros 6%). Como se viu, a maior contribuição foi dada pela qualidade da amostra de treinamento.

Executando no servidor


Para acelerar a implantação, criamos um servidor simples no Flask para o Docker. Havia um método que recebia mercadorias do servidor que precisava ser categorizado e devolvia mercadorias com categorias já. Assim, nos integramos facilmente ao sistema existente, cujo centro era o Tomcat, e não precisamos fazer alterações na arquitetura - apenas adicionamos mais um bloco a ele.

Data de lançamento


Algumas semanas atrás, publicamos um release de categorização no Google Play (ele aparecerá na App Store depois de um tempo). Aconteceu assim:


Em versões futuras, planejamos adicionar a capacidade de corrigir categorias, o que nos permitirá coletar rapidamente erros de categorização e treinar novamente o modelo de categorização (enquanto fazemos isso sozinhos).

Competições mencionadas no 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/pt430216/


All Articles