使用阿里云试用Elasticsearch学习:3.4 处理人类语言——将单词还原为词根

大多数语言的单词都可以 词形变化 ,意味着下列单词可以改变它们的形态用来表达不同的意思:

  • 单复数变化 : fox 、foxes
  • 时态变化 : pay 、 paid 、 paying
  • 性别变化 : waiter 、 waitress
  • 动词人称变化 : hear 、 hears
  • 代词变化 : I 、 me 、 my
  • 不规则变化 : ate 、 eaten
  • 情景变化 : so be it 、 were it so

虽然词形变化有助于表达,但它干扰了检索,一个单一的词根 词义 (或意义)可能被很多不同的字母序列表达。 英语是一种弱词形变化语言(你可以忽略词形变化并且能得到合理的搜索结果),但是一些其他语言是高度词形变化的并且需要额外的工作来保证高质量的搜索结果。

词干提取 试图移除单词的变化形式之间的差别,从而达到将每个词都提取为它的词根形式。 例如 foxes 可能被提取为词根 fox ,移除单数和复数之间的区别跟我们移除大小写之间的区别的方式是一样的。

单词的词根形式甚至有可能不是一个真的单词,单词 jumping 和 jumpiness 或许都会被提取词干为 jumpi 。 这并没有什么问题—​只要在索引时和搜索时产生相同的词项,搜索会正常的工作。

如果词干提取很容易的话,那只要一个插件就够了。不幸的是,词干提取是一种遭受两种困扰的模糊的技术:词干弱提取和词干过度提取。

词干弱提取 就是无法将同样意思的单词缩减为同一个词根。例如, jumped 和 jumps 可能被提取为 jump , 但是 jumping 可能被提取为 jumpi 。弱词干提取会导致搜索时无法返回相关文档。

词干过度提取 就是无法将不同含义的单词分开。例如, general 和 generate 可能都被提取为 gener 。 词干过度提取会降低精准度:不相干的文档会在不需要他们返回的时候返回。

词形还原
原词是一组相关词的规范形式,或词典形式 — paying 、 paid 和 pays 的原词是 pay 。 通常原词很像与其相关的词,但有时也不像 — is 、 was 、 am 和 being 的原词是 be 。
词形还原,很像词干提取,试图归类相关单词,但是它比词干提取先进一步的是它企图按单词的 词义 ,或意义归类。 同样的单词可能表现出两种意思—例如, wake 可以表现为 to wake up 或 a funeral 。然而词形还原试图区分两个词的词义,词干提取却会将其混为一谈。
词形还原是一种更复杂和高资源消耗的过程,它需要理解单词出现的上下文来决定词的意思。实践中,词干提取似乎比词形还原更高效,且代价更低。

首先我们会讨论下两个 Elasticsearch 使用的经典词干提取器 — 词干提取算法 和 字典词干提取器 — 并且在 选择一个词干提取器 讨论了怎么根据你的需要选择合适的词干提取器。 最后将在 控制词干提取 和 原形词干提取 中讨论如何裁剪词干提取。

词干提取算法

Elasticsearch 中的大部分 stemmers (词干提取器)是基于算法的,它们提供了一系列规则用于将一个词提取为它的词根形式,例如剥离复数词末尾的 s 或 es 。提取单词词干时并不需要知道该词的任何信息。

这些基于算法的 stemmers 优点是:可以作为插件使用,速度快,占用内存少,有规律的单词处理效果好。缺点是:没规律的单词例如 be 、 are 、和 am ,或 mice 和 mouse 效果不好。

最早的一个基于算法的英文词干提取器是 Porter stemmer ,该英文词干提取器现在依然推荐使用。 Martin Porter 后来为了开发词干提取算法创建了 Snowball language 网站, 很多 Elasticsearch 中使用的词干提取器就是用 Snowball 语言写的。

使用基于算法的词干提取器

你可以使用 ​porter_stem​ 词干提取器或直接使用 kstem 分词过滤器,或使用 snowball 分词过滤器创建一个具体语言的 Snowball 词干提取器。所有基于算法的词干提取器都暴露了用来接受 语言 参数的统一接口: stemmer token filter 。

例如,假设你发现 英语 分析器使用的默认词干提取器太激进并且你想使它不那么激进。首先应在 language analyzers 查看 英语 分析器配置文件,配置文件展示如下:

PUT /my_index
{
  "settings": {
    "analysis": {
      "filter": {
        "english_stop": {
          "type":       "stop",
          "stopwords":  "_english_"
        },
        "english_keywords": {
          "type":       "keyword_marker", 
          "keywords":   []
        },
        "english_stemmer": {
          "type":       "stemmer",
          "language":   "english" 
        },
        "english_possessive_stemmer": {
          "type":       "stemmer",
          "language":   "possessive_english" 
        }
      },
      "analyzer": {
        "english": {
          "tokenizer":  "standard",
          "filter": [
            "english_possessive_stemmer",
            "lowercase",
            "english_stop",
            "english_keywords",
            "english_stemmer"
          ]
        }
      }
    }
  }
}
  • keyword_marker 分词过滤器列出那些不用被词干提取的单词。这个过滤器默认情况下是一个空的列表。
  • english 分析器使用了两个词干提取器: possessive_english 词干提取器和 english 词干提取器。 所有格词干提取器会在任何词传递到 english_stop 、 english_keywords 和 english_stemmer 之前去除 's 。

重新审视下现在的配置,添加上以下修改,我们可以把这份配置当作新分析器的基本配置:

  • 修改 english_stemmer ,将 english (​porter_stem​ 分词过滤器的映射)替换为 light_english (非激进的 kstem 分词过滤器的映射)。
  • 添加 asciifolding 分词过滤器用以移除外语的附加符号。
  • 移除 keyword_marker 分词过滤器,因为我们不需要它。(我们会在 控制词干提取 中详细讨论它)

新定义的分析器会像下面这样:

PUT /my_index
{
  "settings": {
    "analysis": {
      "filter": {
        "english_stop": {
          "type":       "stop",
          "stopwords":  "_english_"
        },
        "light_english_stemmer": {
          "type":       "stemmer",
          "language":   "light_english" 
        },
        "english_possessive_stemmer": {
          "type":       "stemmer",
          "language":   "possessive_english"
        }
      },
      "analyzer": {
        "english": {
          "tokenizer":  "standard",
          "filter": [
            "english_possessive_stemmer",
            "lowercase",
            "english_stop",
            "light_english_stemmer", 
            "asciifolding" 
          ]
        }
      }
    }
  }
}
  • 将 english 词干提取器替换为非激进的 light_english 词干提取器
  • 添加 asciifolding 分词过滤器
  • filter stop
  • filter stemmer
  • filter asciifolding
  • tokenizer standard

english_stemmer: 这是英语词干分析器,它用于将英文单词转换为它们的词干形式。词干是单词的基本形式,通常是其词根形式,可以用于搜索和匹配。例如,将“running”、“runs”、“ran”等单词转换为其基本形式“run”。

english_possessive_stemmer: 这是英语所有格词干分析器,它与英语词干分析器类似,但专门用于处理所有格形式的单词。在英语中,所有格形式通常以撇号 's 结尾,例如“John’s car”。该分词器会去除所有格形式中的 's。例如,“John’s”会转换为“John”。

因此,主要区别在于英语所有格词干分析器会去除所有格形式中的 's,而普通英语词干分析器不会。

GET _analyze
{
  "tokenizer": "standard",
  "filter": [
    {
      "type": "stemmer",
      "language": "english"
    }
  ],
  "text": "the John's foxes jumping Quickly"
}

GET _analyze
{
  "tokenizer": "standard",
  "filter": [
    {
      "type": "stemmer",
      "language": "possessive_english"
    }
  ],
  "text": "the John's foxes jumping Quickly"
}

结果分别是

the John' fox jump Quickli
the John foxes jumping Quickly

字典词干提取器

字典词干提取器 在工作机制上与 算法化词干提取器 完全不同。 不同于应用一系列标准规则到每个词上,字典词干提取器只是简单地在字典里查找词。理论上可以给出比算法化词干提取器更好的结果。一个字典词干提取器应当可以:

返回不规则形式如 feet 和 mice 的正确词干
区分出词形相似但词义不同的情形,比如 organ and organization
实践中一个好的算法化词干提取器一般优于一个字典词干提取器。应该有以下两大原因:

字典质量
一个字典词干提取器再好也就跟它的字典一样。 据牛津英语字典网站估计,英语包含大约75万个单词(包含变音变形词)。电脑上的大部分英语字典只包含其中的 10% 。

词的含义随时光变迁。mobility 提取词干 mobil 先前可能讲得通,但现在合并进了手机可移动性的含义。字典需要保持最新,这是一项很耗时的任务。通常等到一个字典变得好用后,其中的部分内容已经过时。

字典词干提取器对于字典中不存在的词无能为力。而一个基于算法的词干提取器,则会继续应用之前的相同规则,结果可能正确或错误。

大小与性能
字典词干提取器需要加载所有词汇、 所有前缀,以及所有后缀到内存中。这会显著地消耗内存。找到一个词的正确词干,一般比算法化词干提取器的相同过程更加复杂。

依赖于不同的字典质量,去除前后缀的过程可能会更加高效或低效。低效的情形可能会明显地拖慢整个词干提取过程。

另一方面,算法化词干提取器通常更简单、轻量和快速。

Hunspell 词干提取器

Elasticsearch 提供了基于词典提取词干的 hunspell 语汇单元过滤器(token filter). Hunspell hunspell.github.io 是一个 Open Office、LibreOffice、Chrome、Firefox、Thunderbird 等众多其它开源项目都在使用的拼写检查器。

可以从这里获取 Hunspell 词典 :

一个 Hunspell 词典由两个文件组成 — 具有相同的文件名和两个不同的后缀 — 如 en_US—和下面的两个后缀的其中一个:

.dic
包含所有词根,采用字母顺序,再加上一个代表所有可能前缀和后缀的代码表 【集体称之为词缀( affixes 】
.aff
包含实际 .dic 文件每一行代码表对应的前缀和后缀转换

安装一个词典

Hunspell 语汇单元过滤器在特定的 Hunspell 目录里寻找词典, 默认目录是 ./config/hunspell/ 。 .dic 文件和 .aff 文件应该要以子目录且按语言/区域的方式来命名。 例如,我们可以为美式英语创建一个 Hunspell 词干提取器,目录结构如下:

config/
  └ hunspell/ 
      └ en_US/ 
          ├ en_US.dic
          ├ en_US.aff
          └ settings.yml 
  • Hunspell 目录位置可以通过编辑 config/elasticsearch.yml 文件的: indices.analysis.hunspell.dictionary.location 设置来修改。
  • en_US 是这个区域的名字,也是我们传给 hunspell 语汇单元过滤器参数 language 值。
  • 一个语言一个设置文件,下面的章节会具体介绍。

按语言设置

在语言的目录设置文件 settings.yml 包含适用于所有字典内的语言目录的设置选项。

---
ignore_case:          true
strict_affix_parsing: true

这些选项的意思如下:
ignore_case
Hunspell 目录默认是区分大小写的,如,姓氏 Booker 和名词 booker 是不同的词,所以应该分别进行词干提取。 也许让 hunspell 提取器区分大小写是一个好主意,不过也可能让事情变得复杂:

  • 一个句子的第一个词可能会被大写,因此感觉上会像是一个名词。
  • 输入的文本可能全是大写,如果这样那几乎一个词都找不到。
  • 用户也许会用小写来搜索名字,在这种情况下,大写开头的词将找不到。

一般来说,设置参数 ignore_case 为 true 是一个好主意。

strict_affix_parsing
词典的质量千差万别。 一些网上的词典的 .aff 文件有很多畸形的规则。 默认情况下,如果 Lucene 不能正常解析一个词缀(affix)规则, 它会抛出一个异常。 你可以通过设置 strict_affix_parsing 为 false 来告诉 Lucene 忽略错误的规则。

自定义词典
如果一个目录放置了多个词典 (.dic 文件), 他们会在加载时合并到一起。这可以让你以自定义的词典的方式对下载的词典进行定制:

config/
  └ hunspell/
      └ en_US/  
          ├ en_US.dic
          ├ en_US.aff 
          ├ custom.dic
          └ settings.yml
  • custom 词典和 en_US 词典将合并到一起。
  • 多个 .aff 文件是不允许的,因为会产生规则冲突。

.dic 文件和 .aff 文件的格式在这里讨论: Hunspell 词典格式 。

创建一个 Hunspell 语汇单元过滤器

一旦你在所有节点上安装好了词典,你就能像这样定义一个 hunspell 语汇单元过滤器:

PUT /my_index
{
  "settings": {
    "analysis": {
      "filter": {
        "en_US": {
          "type":     "hunspell",
          "language": "en_US" 
        }
      },
      "analyzer": {
        "en_US": {
          "tokenizer":  "standard",
          "filter":   [ "lowercase", "en_US" ]
        }
      }
    }
  }
}
  • 参数 language 和目录下对应的名称相同。

你可以通过 analyze API 来测试这个新的分析器, 然后和 english 分析器比较一下它们的输出:

GET /my_index/_analyze?analyzer=en_US 
reorganizes

GET /_analyze?analyzer=english 
reorganizes
  • 返回 organize
  • 返回 reorgan

在前面的例子中,hunspell 提取器有一个有意思的事情,它不仅能移除前缀还能移除后缀。大多数算法词干提取仅能移除后缀。

Hunspell 词典会占用几兆的内存。幸运的是,Elasticsearch 每个节点只会创建一个词典的单例。 所有的分片都会使用这个相同的 Hunspell 分析器。

Hunspell 词典格式

选择一个词干提取器

在文档 stemmer token filter 里面列出了一些针对语言的若干词干提取器。 就英语来说我们有如下提取器:

english
porter_stem​ 语汇单元过滤器(token filter)。
light_english
kstem 语汇单元过滤器(token filter)。
minimal_english
Lucene 里面的 EnglishMinimalStemmer ,用来移除复数。
lovins
基于 Snowball 的 Lovins 提取器, 第一个词干提取器。
porter
基于 Snowball 的 Porter 提取器。
porter2
基于 Snowball 的 Porter2 提取器。
possessive_english
Lucene 里面的 EnglishPossessiveFilter ,移除 's
Hunspell 词干提取器也要纳入到上面的列表中,还有多种英文的词典可用。

有一点是可以肯定的:当一个问题存在多个解决方案的时候,这意味着没有一个解决方案充分解决这个问题。 这一点同样体现在词干提取上 — 每个提取器使用不同的方法不同程度的对单词进行了弱提取或是过度提取。

在 stemmer 文档 中,使用粗体高亮了每一个语言的推荐的词干提取器, 通常是因为它提供了一个在性能和质量之间合理的妥协。也就是说,推荐的词干提取器也许不适用所有场景。 关于哪个是最好的词干提取器,不存在一个唯一的正确答案 — 它要看你具体的需求。 这里有3个方面的因素需要考虑在内: 性能、质量、程度。

提取性能

算法提取器一般来说比 Hunspell 提取器快4到5倍。 “Handcrafted” 算法提取器通常(不是永远) 要比 Snowball 快或是差不多。 比如,porter_stem 语汇单元过滤器(token filter)就明显要比基于 Snowball 实现的 Porter 提取器要快的多。

Hunspell 提取器需要加载所有的词典、前缀和后缀表到内存,可能需要消耗几兆的内存。而算法提取器,由一点点代码组成,只需要使用很少内存。

提取质量

所有的语言,除了世界语(Esperanto)都是不规范的。 最日常用语使用的词往往不规则,而更正式的书面用语则往往遵循规律。 一些提取算法经过多年的开发和研究已经能够产生合理的高质量的结果了,其他人只需快速组装做很少的研究就能解决大部分的问题了。

虽然 Hunspell 提供了精确地处理不规则词语的承诺,但在实践中往往不足。 一个基于词典的提取器往往取决于词典的好坏。如果 Hunspell 碰到的这个词不在词典里,那它什么也不能做。 Hunspell 需要一个广泛的、高质量的、最新的词典以产生好的结果;这样级别的词典可谓少之又少。 另一方面,一个算法提取器,将愉快的处理新词而不用为新词重新设计算法。

如果一个好的算法词干提取器可用于你的语言,那明智的使用它而不是 Hunspell。它会更快并且消耗更少内存,并且会产生和通常一样好或者比 Hunspell 等价的结果.

如果精度和可定制性对你很重要,那么你需要(和有精力)来维护一个自定义的词典,那么 Hunspell 会给你比算法提取器更大的灵活性。 (查看 控制词干提取 来了解可用于任何词干提取器的自定义技术。)

提取程度

不同的词干提取器会将词弱提取或过度提取到一定的程度。 light_ 提取器提干力度不及标准的提取器。 minimal_ 提取器同样也不那么积极。Hunspell 提取力度要激进一些。

是否想要积极提取还是轻量提取取决于你的场景。如果你的搜索结果是要用于聚类算法,你可能会希望匹配的更广泛一点(因此,提取力度要更大一点)。 如果你的搜索结果是面向最终用户,轻量的提取一般会产生更好的结果。对搜索来说,将名称和形容词提干比动词提干更重要,当然这也取决于语言。

另外一个要考虑的因素就是你的文档集的大小。 一个只有 10,000 个产品的小集合,你可能要更激进的提干来确保至少匹配到一些文档。 如果你的文档集很大,使用轻量的弱提取可能会得到更好的匹配结果。

做一个选择

从推荐的一个词干提取器出发,如果它工作的很好,那没有什么需要调整的。如果不是,你将需要花点时间来调查和比较该语言可用的各种不同提取器, 来找到最适合你目的的那一个。

控制词干提取

开箱即用的词干提取方案永远也不可能完美。 尤其是算法提取器,他们可以愉快的将规则应用于任何他们遇到的词,包含那些你希望保持独立的词。 也许,在你的场景,保持独立的 skies 和 skiing 是重要的,你不希望把他们提取为 ski (正如 english 分析器那样)。

语汇单元过滤器 keyword_markerstemmer_override 能让我们自定义词干提取过程。

阻止词干提取

语言分析器(查看 配置语言分析器)的参数 stem_exclusion 允许我们指定一个词语列表,让他们不被词干提取。

在内部,这些语言分析器使用 keyword_marker 语汇单元过滤器 来标记这些词语列表为 keywords ,用来阻止后续的词干提取过滤器来触碰这些词语。

例如,我们创建一个简单自定义分析器,使用 porter_stem​ 语汇单元过滤器,同时阻止 skies 的词干提取:

PUT /my_index
{
  "settings": {
    "analysis": {
      "filter": {
        "no_stem": {
          "type": "keyword_marker",
          "keywords": [ "skies" ] 
        }
      },
      "analyzer": {
        "my_english": {
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "no_stem",
            "porter_stem"
          ]
        }
      }
    }
  }
}
  • 参数 keywords 可以允许接收多个词语。

使用 analyze API 来测试,可以看到词 skies 没有被提取:

GET /my_index/_analyze
{
  "text": ["sky skies skiing skis "],
  "analyzer": "my_english"
}
  • 返回: sky, skies, ski, ski

虽然语言分析器只允许我们通过参数 stem_exclusion 指定一个词语列表来排除词干提取, 不过 keyword_marker 语汇单元过滤器同样还接收一个 keywords_path 参数允许我们将所有的关键字存在一个文件。 这个文件应该是每行一个字,并且存在于集群的每个节点。查看 更新停用词(Updating Stopwords) 了解更新这些文件的提示。

自定义提取

在上面的例子中,我们阻止了 skies 被词干提取,但是也许我们希望他能被提干为 sky 。 The stemmer_override 语汇单元过滤器允许我们指定自定义的提取规则。 与此同时,我们可以处理一些不规则的形式,如:mice 提取为 mouse 和 feet 到 foot :

PUT /my_index
{
  "settings": {
    "analysis": {
      "filter": {
        "custom_stem": {
          "type": "stemmer_override",
          "rules": [ 
            "skies=>sky",
            "mice=>mouse",
            "feet=>foot"
          ]
        }
      },
      "analyzer": {
        "my_english": {
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "custom_stem", 
            "porter_stem"
          ]
        }
      }
    }
  }
}
GET /my_index/_analyze
{
  "text": ["The mice came down from the skies and ran over my feet"],
  "analyzer": "my_english"
}
  • 规则来自 original=>stem 。
  • stemmer_override 过滤器必须放置在词干提取器之前。
  • 返回 the, mouse, came, down, from, the, sky, and, ran, over, my, foot 。

正如 keyword_marker 语汇单元过滤器,规则可以被存放在一个文件中,通过参数 rules_path 来指定位置。

原形词干提取

为了完整地 完成本章的内容,我们将讲解如何将已提取词干的词和原词索引到同一个字段中。举个例子,分析句子 The quick foxes jumped 将会得到以下词项:

Pos 1: (the)
Pos 2: (quick)
Pos 3: (foxes,fox) 
Pos 4: (jumped,jump) 
  • 已提取词干的形式和未提取词干的形式位于相同的位置。

Warning:使用此方法前请先阅读 原形词干提取是个好主意吗 。

为了归档词干提取出的 原形 ,我们将使用 keyword_repeat 过滤器,跟 keyword_marker 过滤器 ( see 阻止词干提取 ) 一样,它把每一个词项都标记为关键词,以防止后续词干提取器对其修改。但是,它依然会在相同位置上重复词项,并且这个重复的词项 是 提取的词干。

单独使用 keyword_repeat token 过滤器将得到以下结果:

Pos 1: (the,the) 
Pos 2: (quick,quick) 
Pos 3: (foxes,fox)
Pos 4: (jumped,jump)

提取词干前后的形式一样,所以只是不必要的重复。

为了防止提取和未提取词干形式相同的词项中的无意义重复,我们增加了组合的 unique 语汇单元过滤器 :

PUT /my_index
{
  "settings": {
    "analysis": {
      "filter": {
        "unique_stem": {
          "type": "unique",
          "only_on_same_position": true 
        }
      },
      "analyzer": {
        "in_situ": {
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "keyword_repeat", 
            "porter_stem",
            "unique_stem" 
          ]
        }
      }
    }
  }
}
  • 设置 unique 类型语汇单元过滤器,是为了只有当重复语汇单元出现在相同位置时,移除它们。
  • 语汇单元过滤器必须出现在词干提取器之前。
  • unique_stem 过滤器是在词干提取器完成之后移除重复词项。

原形词干提取是个好主意吗

用户喜欢 原形 词干提取这个主意:“如果我可以只用一个组合字段,为什么还要分别存一个未提取词干和已提取词干的字段呢?” 但这是一个好主意吗?答案一直都是否定的。因为有两个问题:

第一个问题是无法区分精准匹配和非精准匹配。本章中,我们看到了多义词经常会被展开成相同的词干词:organs 和 organization 都会被提取为 organ 。

在 使用语言分析器 我们展示了如何整合一个已提取词干属性的查询(为了增加召回率)和一个未提取词干属性的查询(为了提升相关度)。 当提取和未提取词干的属性相互独立时,单个属性的贡献可以通过给其中一个属性增加boost值来优化(参见 语句的优先级 )。相反地,如果已提取和未提取词干的形式置于同一个属性,就没有办法来优化搜索结果了。

第二个问题是,必须搞清楚 相关度分值是否如何计算的。在 什么是相关性? 我们解释了部分计算依赖于逆文档频率(IDF)—— 即一个词在索引库的所有文档中出现的频繁程度。 在一个包含文本 jump jumped jumps 的文档上使用原形词干提取,将得到下列词项:

Pos 1: (jump)
Pos 2: (jumped,jump)
Pos 3: (jumps,jump)

jumped 和 jumps 各出现一次,所以有正确的IDF值;jump 出现了3次,作为一个搜索词项,与其他未提取词干的形式相比,这明显降低了它的IDF值。

基于这些原因,我们不推荐使用原形词干提取。

  • 18
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值