挑战赛
在本文中,我们要讨论如何创建一种解决方案,该解决方案用于将应用程序中的收据中的产品名称分类以记录支票和购物助手的费用。 我们希望为用户提供查看购买统计信息的机会,这些统计信息是根据扫描的收据自动收集的,即按类别分配用户购买的所有商品。 因为强迫用户独立地分组产品已经是上个世纪了。 解决此问题的方法有几种:您可以尝试将聚类算法与单词的矢量表示方法或经典分类算法一起应用。 我们还没有发明任何新东西,在本文中,我们只想共享一个有关该问题的可能解决方案的小指南,如何不执行此操作的示例,分析其他方法为何不起作用以及您在此过程中可能遇到的问题的分析。
聚类
问题之一是,我们从支票中获得的商品名称,即使对于一个人来说,也并不总是易于解密。 您不太可能知道在一家俄罗斯商店中购买了
哪种名称为
“ UTRUSTA krnsht”的产品? 真正的瑞典设计鉴赏家肯定会立即回答我们:Utrust烤箱的托架,但在总部保留这样的专家非常昂贵。 此外,我们还没有适合我们数据的现成的,带有标签的样本,我们可以在该样本上训练模型。 因此,首先我们将讨论在缺乏训练数据的情况下如何应用聚类算法以及为什么我们不喜欢它。
这样的算法基于测量对象之间的距离,这需要它们的向量表示或使用度量来测量单词的相似性(例如,Levenshtein距离)。 在此步骤中,困难在于名称的有意义的矢量表示。 从名称中提取属性以完整,全面地描述产品及其与其他产品的关系是有问题的。
最简单的选择是使用Tf-Idf,但是在这种情况下,向量空间的维数很大,并且空间本身是稀疏的。 此外,这种方法不会从名称中提取任何其他信息。 因此,在一个集群中,可以有许多不同类别的产品,它们由一个共同的词结合在一起,例如“土豆”或“沙拉”:
我们也无法控制将要组装的集群。 唯一可以指出的是簇的数量(如果使用了基于空间中非密度峰的算法)。 但是,如果您指定的数量太少,则会形成一个巨大的群集,其中将包含所有其他群集无法使用的名称。 如果您指定了足够大的簇,那么在算法起作用之后,我们将不得不浏览数百个簇并将它们手工组合成语义类别。
下表提供了使用KMeans和Tf-Idf算法进行矢量表示的聚类信息。 从这些表中我们可以看到,簇中心之间的距离小于对象与它们所属的簇中心之间的平均距离。 可以通过以下事实来解释此类数据:在向量空间中没有明显的密度峰,并且簇的中心位于圆周围,而大多数对象位于圆的外部。 此外,形成了一个簇,其中包含大多数向量。 在此类别中,最有可能使用的名称包含的单词在不同类别的所有产品中比其他单词更常见。
表1.群集之间的距离。丛集 | C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 |
---|
C1 | 0.0 | 0.502 | 0.354 | 0.475 | 0.481 | 0.527 | 0.498 | 0.501 | 0.524 |
---|
C2 | 0.502 | 0.0 | 0.614 | 0.685 | 0.696 | 0.728 | 0.706 | 0.709 | 0.725 |
---|
C3 | 0.354 | 0.614 | 0.0 | 0.590 | 0.597 | 0.635 | 0.610 | 0.613 | 0.632 |
---|
C4 | 0.475 | 0.685 | 0.590 | 0.0 | 0.673 | 0.709 | 0.683 | 0.687 | 0.699 |
---|
C5 | 0.481 | 0.696 | 0.597 | 0.673 | 0.0 | 0.715 | 0.692 | 0.694 | 0.711 |
---|
C6 | 0.527 | 0.727 | 0.635 | 0.709 | 0.715 | 0.0 | 0.726 | 0.728 | 0.741 |
---|
C7 | 0.498 | 0.706 | 0.610 | 0.683 | 0.692 | 0.725 | 0.0 | 0.707 | 0.714 |
---|
C8 | 0.501 | 0.709 | 0.612 | 0.687 | 0.694 | 0.728 | 0.707 | 0.0 | 0.725 |
---|
C9 | 0.524 | 0.725 | 0.632 | 0.699 | 0.711 | 0.741 | 0.714 | 0.725 | 0.0 |
---|
表2.有关集群的简要信息丛集 | 物件数量 | 平均距离 | 最小距离 | 最大距离 |
---|
C1 | 62530 | 0.999 | 0.041 | 1.001 |
---|
C2 | 2159 | 0.864 | 0.527 | 0.964 |
---|
C3 | 1099 | 0.934 | 0.756 | 0.993 |
---|
C4 | 1292 | 0.879 | 0.733 | 0.980 |
---|
C5 | 746 | 0.875 | 0.731 | 0.965 |
---|
C6 | 2451 | 0.847 | 0.719 | 0.994 |
---|
C7 | 1133 | 0.866 | 0.724 | 0.986 |
---|
C8 | 876 | 0.863 | 0.704 | 0.999 |
---|
C9 | 1879年 | 0.849 | 0.526 | 0.981 |
---|
但是在某些地方,集群变得相当不错,例如,如下图所示-那里几乎所有产品都是猫食。
Doc2Vec是允许您以矢量形式表示文本的另一种算法。 使用这种方法,每个名称将由比使用Tf-Idf尺寸小的矢量来描述。 在生成的向量空间中,相似的文本将彼此靠近,而不同的文本将相距很远。
该方法可以解决通过Tf-Idf方法获得的大尺寸和放电空间的问题。 对于此算法,我们使用了最简单的令牌化选项:将名称分解为单独的单词,并采用其初始形式。 他通过以下方式接受了数据方面的培训:
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)
但是通过这种方法,我们得到了不携带名称信息的向量-成功的话,您可以使用随机值。 这是该算法操作的一个示例:该图像显示的产品在算法看来类似于“ n pn 0.45k形式的Borodino面包”。
也许问题出在名称的长度和上下文上:名称“ __ club。Banana 200ml”的通行证可以是酸奶,果汁或一大罐奶油。 使用不同的名称标记化方法可以达到更好的结果。 我们没有使用这种方法的经验,并且在第一次尝试失败时,我们已经找到了带有产品名称的标记集,因此我们决定暂时退出此方法,转而使用分类算法。
分类
数据预处理
支票上的商品名称并非总是以清晰的方式出现:拉丁和西里尔字母混在一起。 例如,字母“ a”可以用拉丁字母“ a”代替,这增加了唯一名称的数量-例如,单词“ milk”和“ milk”将被认为是不同的。 这些名称还包含许多其他错字和缩写。
我们检查了数据库,发现名称中的典型错误。 在此阶段,我们放弃了正则表达式,借助它们我们清理了名称并将它们带入了某种通用视图。 使用这种方法,结果提高了大约7%。 加上基于带有扭曲参数的Huber损失函数的简单SGD分类器选项,我们得出F1的准确度为81%(所有产品类别的平均准确度)。
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_
另外,不要忘记某些类别的人购买的频率高于其他类别:例如,“茶和糖果”,“蔬菜和水果”比“服务”和“化妆品”更受欢迎。 通过这样的数据分布,最好使用允许您为每个类别设置权重(重要程度)的算法。 类别的权重可以取反值,该值等于类别中产品数与产品总数之比。 但您不必考虑,因为在实施这些算法时,可以自动确定类别的权重。
获取新数据进行培训
我们的应用程序所需要的类别与比赛中使用的类别略有不同,并且我们数据库中产品的名称与比赛中所展示的名称明显不同。 因此,我们需要在收据中标记货物。 我们尝试自己完成此操作,但我们意识到即使我们与整个团队保持联系,也将花费很长时间。 因此,我们决定使用Yandex的
“ Toloka” 。
在那里,我们使用了这种形式的分配:
- 在每个单元格中,我们展示了一个产品,必须定义其类别
- 由我们以前的模型之一定义的假设类别
- 响应字段(如果建议的类别不正确)
我们创建了带有示例的详细说明,这些示例解释了每个类别的功能,还使用了质量控制方法:带有标准答案的集合以及常规任务(我们自己实施了标准答案,标记了数百种产品)。 根据这些任务的答案结果,排除了错误地标记数据的用户。 但是,对于整个项目,我们仅禁止了600多个用户中的三个。
有了新数据,我们得到了一个更适合我们数据的模型,准确性提高了一点(〜11%),已经达到了92%。
最终模型
我们使用来自Kaggle的多个数据集的数据组合-74%来开始分类过程,此后,我们改进了预处理-81%,收集了新数据集-92%,最后改进了分类过程:最初,使用逻辑回归我们获得商品归因的初步概率SGD使基于产品名称的类别更加准确,但是损失函数的值仍然很大,这严重影响了最终分类器的结果。 此外,我们将获得的数据与产品上的其他数据(产品价格,购买商店,商店的统计信息,支票和其他元信息)相结合,并对XGBoost进行了所有这些数据量的训练,其准确性达到了98%(提高了另外6%)。 事实证明,培训样本的质量做出了最大的贡献。
在服务器上运行
为了加快部署,我们在Flask上将一个简单的服务器提升到了Docker。 有一种方法是从服务器接收需要分类的商品,然后退还带有类别的商品。 因此,我们可以轻松地集成到以Tomcat为中心的现有系统中,而不必对体系结构进行更改-我们只需向其添加一个块即可。
发布日期
几周前,我们在Google Play上发布了一个分类发行版(过一会儿就会出现在App Store上)。 原来是这样的:
在将来的版本中,我们计划增加纠正类别的功能,这将使我们能够快速收集分类错误并重新训练分类模型(同时我们自己完成)。
在Kaggle提及的比赛:
www.kaggle.com/c/receipt-categorisationwww.kaggle.com/c/market-basket-analysiswww.kaggle.com/c/prod-price-prediction