用R语言对主数据管理(MDM)背景下的分类模型进行文本处理和词干处理

162 阅读9分钟

您对客座发帖感兴趣吗?通过你的RStudio编辑器在DataScience+发布。

类别

标签

在商业条件下,经常会遇到一些狭隘的专门任务,这些任务需要一种特殊的方法,因为它们不适合标准的数据处理流程和构建模型。其中一项任务是在主数据管理过程中对新产品进行分类(MDM)。

例子1

你在一家大型公司(供应商)工作,从事产品的生产和/或销售,包括通过批发中介(分销商)。通常你的分销商有义务(在你工作的公司面前)定期提供你自己的产品销售报告--所谓的销售出去。并非总是如此,分销商能够以你们公司的代码报告所销售的产品,更多的是他们自己的代码和他们自己的产品名称,与你们系统中的名称不同。因此,在你的数据库中,有必要保留与你的账户产品代码相匹配的分销商表。分销商越多,同一产品名称的变化就越多。如果你有一个大的品种组合,这就成为一个问题,当你的会计系统中收到新的产品名称变化时,要通过手工劳动密集型支持这种匹配表来解决。

如果它指的是这种产品的名称作为文件的文本,和你的会计系统的代码(这些变化与之挂钩)来考虑类,那么我们得到一个文本的多重分类的任务。这样一个匹配表(操作员手动维护)可以被视为一个训练样本,如果在其上建立这样一个分类模型--它将能够减少操作员对现有产品的新名称流进行分类的工作的复杂性。然而,"按原样 "处理文本的经典方法并不能拯救你,下面会说到这一点。

例2

在你公司的数据库中,来自外部分析(营销)机构的产品销售(或价格)数据,或来自第三方网站的解析。每个数据源的同一产品也会包含书写的变化。作为这个例子的一部分,任务可能比例1更困难,因为往往你公司的业务用户不仅需要分析你的产品,还需要分析你的直接竞争对手的产品范围,相应地,变化所涉及的类(参考产品)的数量--急剧增加。

这样一类任务的特殊性是什么?

首先,有很多类(事实上,你有多少产品有这么多类),如果在这个过程中,你不仅要与公司的产品合作,而且还要与竞争对手合作,这种新类的增长每天都可能发生 - 因此,教一次模型来反复用于预测新产品变得毫无意义。

其次,类中的文件(同一产品的不同变化)的数量不是很平衡:可能有一个一个的类,也可能有更多。

为什么经典的文本多重分类方法效果不佳?

按步骤考虑一下经典文本处理方法的缺点。

  • 停顿词。

在这样的任务中,任何文本处理包的普遍接受的概念中都没有停止词。

  • 符号化(Tochenization

在经典的软件包中,对文字的划分是基于标点符号或空格的存在。作为该类任务的一部分(文本字段输入的长度往往是有限的),收到没有空格的产品名称并不罕见,其中的单词没有明确分开,但在视觉上是数字或其他语言的注册。如何从你最喜欢的编程语言的盒子里传出葡萄酒的名字 "Dom.CHRISTIANmoreau0,75LPeLtr.EtFilChablis"?(不幸的是这不是一个笑话)

产品名称不是经典理解的任务中的文本(如来自网站的新闻、服务评论或报纸标题),它是可以被释放的后缀,可以被丢弃。在产品的名称中,经常会出现缩写,而且其中的词的减少并不清楚如何分配这个后缀。还有一些来自其他语言组的品牌名称(例如,包括法语或意大利语的品牌),也不能用正常的词根处理。

  • 减少文档-术语矩阵。

通常,在建立 "文档-术语 "矩阵时,你的语言包会提供减少矩阵的稀疏性,以删除频率低于某个最低阈值的词(矩阵的列)。在经典的任务中,这确实有助于提高质量,减少模型训练的开销。但在这样的任务中则不然。上面,我已经写过,类的分布不是很平衡--它很容易出现在同一产品名称的类上(例如,一个罕见的昂贵的品牌,第一次出售,而在训练样本中只有一次)。经典的稀疏度减少方法为我们带来了分类器的质量。

  • 训练模型。

通常,在文本上训练某种模型(LibSVM、天真贝叶斯分类器、神经网络或其他东西),然后反复使用。在这种情况下,每天都可能出现新的类,类中的文件数量可以算作一个实例。因此,长期使用学习一个大的模型是没有意义的--任何具有在线训练的算法,例如,具有一个最近邻居的KNN分类器,都是足够的。

接下来,我们将尝试比较传统方法的分类和基于提议的软件包的分类。我们将使用tidytext作为一个辅助包。

实例

#devtools::install_github(repo = 'https://github.com/edvardoss/abbrevTexts')
library(abbrevTexts)
library(tidytext) # text proccessing
library(dplyr) # data processing
library(stringr) # data processing
library(SnowballC) # traditional stemming approach
library(tm) #need only for tidytext internal purpose

该包包括2个关于葡萄酒名称的数据集:来自外部数据源的葡萄酒原始名称--"rawProducts",以及为维护公司主数据而在标准中编写的统一的葡萄酒名称--"standardProducts"。rawProducts表中有许多同一产品的拼写变化,这些变化通过 "standartId "键列上的多对一关系在standardProducts中被简化为一个产品。PS "rawProducts "表中的变体是通过程序生成的,但根据我的经验,与产品名称来自外部各种来源的情况有最大可能的相似性(尽管在某处我可能做得过了)。

data(rawProducts, package = 'abbrevTexts')
head(rawProducts)
data(standardProducts, package = 'abbrevTexts')
head(standardProducts)

训练和测试分割

set.seed(1234)
trainSample <- sample(x = seq(nrow(rawProducts)),size = .9*nrow(rawProducts))
testSample <- setdiff(seq(nrow(rawProducts)),trainSample)
testSample

为 "无干系模式 "和 "传统干系模式 "创建数据框架

df <- rawProducts %>% mutate(prodId=row_number(), 
                             rawName=str_replace_all(rawName,pattern = '\\.','. ')) %>% 
  unnest_tokens(output = word,input = rawName) %>% count(StandartId,prodId,word)

df.noStem <- df %>% bind_tf_idf(term = word,document = prodId,n = n)

df.SnowballStem <- df %>% mutate(wordStm=SnowballC::wordStem(word)) %>% 
  bind_tf_idf(term = wordStm,document = prodId,n = n)

创建文档术语矩阵

dtm.noStem <- df.noStem %>% 
  cast_dtm(document = prodId,term = word,value = tf_idf) %>% data.matrix()

dtm.SnowballStem <- df.SnowballStem %>% 
  cast_dtm(document = prodId,term = wordStm,value = tf_idf) %>% data.matrix()

为 "无干系模式 "创建knn模型,并计算准确性

knn.noStem <- class::knn1(train = dtm.noStem[trainSample,],
                          test = dtm.noStem[testSample,],
                          cl = rawProducts$StandartId[trainSample])
mean(knn.noStem==rawProducts$StandartId[testSample])

准确性 knn.noStem: 0.4761905 (47%)

为 "干化模式 "创建knn模型并计算准确率

knn.SnowballStem <- class::knn1(train = dtm.SnowballStem[trainSample,],
                               test = dtm.SnowballStem[testSample,],
                               cl = rawProducts$StandartId[trainSample])
mean(knn.SnowballStem==rawProducts$StandartId[testSample])

准确性 knn.SnowballStem: 0.5 (50%)

abbrevTexts入门

下面是一个使用abbrevTexts包中的函数在相同数据上的例子

按大小写分离单词

df <- rawProducts %>% mutate(prodId=row_number(), 
                             rawNameSplitted= makeSeparatedWords(rawName)) %>% 
        unnest_tokens(output = word,input = rawNameSplitted)
print(df)

正如你所看到的,文本的标记化被正确地进行了:不仅考虑了一起书写时大写和小写的过渡,而且还考虑了一起书写的单词之间没有空格的标点符号。

基于训练中的词汇样本创建词干词典

在对不同的词干实现方法进行了长时间的搜索后,我得出结论,基于语言规则的传统方法不适合这种特殊的任务,所以我不得不寻找自己的方法。结果,我找到了最理想的解决方案,即简化为无监督学习,它对文本语言或训练样本中可用词的减少程度不敏感。

该函数将一个词的向量作为输入,训练样本的最小词长和将子词视为父词缩写的最小分数,然后做以下工作。

  1. 丢弃长度小于设定阈值的词
  2. 丢弃由数字组成的词
  3. 按照长度降序对单词进行排序
  4. 对于列表中的每个词
  • 4.1 筛选出小于当前词的长度且大于或等于当前词的长度乘以最小分数的词
  • 4.2 从过滤后的单词列表中选择那些是当前单词的开头的单词

假设我们固定min.share=0.7 在这个中间阶段(4.2),我们得到一个父子表,可以找到这样的例子。

##    parent child
## 1 bodegas bodeg
## 2   bodeg  bode

请注意,每一行都满足子词的长度不短于父词长度的70%的条件。

然而,可能会发现一些不能被视为词的缩写的配对,因为在这些配对中,不同的父母被简化为一个孩子,例如。

##    parent child
## 1 bodegas bodeg
## 2 bodegue bodeg

我的函数对这种情况只留下一个配对。

让我们回到那个有明确的单词缩写的例子

##    parent child
## 1 bodegas bodeg
## 2   bodeg  bode

但是,如果你再仔细看一下,我们会发现这两对词有一个共同的词'bodeg',这个词允许你把这两对词连接成一个缩写链,而不违反我们关于一个词的长度的初始条件,把它看作是另一个词的缩写。

bodegas->bodeg->bode

因此,我们得出了这样一个表格。

##    parent child terminal.child
## 1 bodegas bodeg           bode
## 2   bodeg  bode           bode

这样的链可以有任意的长度,而且有可能从找到的配对中递归地组合成这样的链。因此,我们来到了第五个阶段,即为所构建的单词缩写链的每个参与者确定最后的孩子

  1. 递归地迭代找到的词对,以确定链上所有成员的最终(终端)孩子
  2. 返回缩略语词典

makeAbbrStemDict函数会自动由几个线程加载所有的处理器核心来并行,所以对于大量的文本,最好考虑到这一点。

abrDict <- makeAbbrStemDict(term.vec = df$word,min.len = 3,min.share = .6)
abrDict <- as_tibble(abrDict)
print(abrDict) # We can see parent word, intermediate results and total result (terminal child)

以表格形式输出的词干词典也很方便,因为它可以在 "dplyr "范式中以简单的方式有选择地删除一些词干行。

假设我们要从干系词典中排除父词 "abruzz "和终端子组 "absolu"。

abrDict.reduced <- abrDict %>% filter(parent!='abruzz',terminal.child!='absolu')
print(abrDict.reduced)

将这个解决方案的简单性和清晰性与stackoverflow中提供的解决方案进行比较。

用tm包进行文本挖掘--词干化

使用缩略语词典进行干化

df.AbbrStem <- df %>% left_join(abrDict %>% select(parent,terminal.child),by = c('word'='parent')) %>% 
    mutate(wordAbbrStem=coalesce(terminal.child,word)) %>% select(-terminal.child)
print(df.AbbrStem)

以表格形式输出的干化词典也很方便,因为可以在 "dplyr "范式中以简单的方式有选择地删除一些干化行。
为例。

干系词的TF-IDF

df.AbbrStem <- df.AbbrStem %>% count(StandartId,prodId,wordAbbrStem) %>% 
  bind_tf_idf(term = wordAbbrStem,document = prodId,n = n)
print(df.AbbrStem)

创建文档术语矩阵

dtm.AbbrStem <- df.AbbrStem %>% 
  cast_dtm(document = prodId,term = wordAbbrStem,value = tf_idf) %>% data.matrix()

为 "abbrevTexts模式 "创建knn模型并计算准确性

knn.AbbrStem <- class::knn1(train = dtm.AbbrStem[trainSample,],
                                test = dtm.AbbrStem[testSample,],
                                cl = rawProducts$StandartId[trainSample])
mean(knn.AbbrStem==rawProducts$StandartId[testSample]) 

准确性 knn.AbbrStem: 0.8333333 (83%)

正如你所看到的,我们在测试样本中的分类质量得到了显著改善。Tidytext是一个方便的软件包,适用于小的文本院子,但在大的文本院子的情况下,"AbbrevTexts "软件包也完全适用于预处理和规范化,与传统的方法相比,在这样的特定任务中通常会有更好的准确性。

链接到github:https://github.com/edvardoss/abbrevTexts

**

相关文章

**