N-gram

将文本分割为 N 个连续字符或单词子序列的方法,用于搜索和文本相似度计算。

N-gram 是将文本分割为 N 个连续字符或单词子序列的方法。N=1 称为一元组 (unigram),N=2 称为二元组 (bigram),N=3 称为三元组 (trigram)。例如,把 "东京都" 按字符二元组分割,可以得到 "东京" 和 "京都" 两个。除了字符单位,还有单词单位的 N-gram,"I love Tokyo" 的单词二元组为 "I love" 和 "love Tokyo"。这是自然语言处理和信息检索领域中自古就在使用的基础方法,至今仍在众多系统中担当核心。

N-gram 最大的优势在于不需要形态素分析或词典。即使是日语和中文这样词的边界不明确的语言,也可以只靠机械地滑动字符串来生成词元。全文搜索引擎 Elasticsearch 和 Apache Solr 标准提供了 N-gram 分词器,被用于构建与语言无关的搜索索引。

N 取多少会直接左右检索的命中方式。Elasticsearch 的 ngram 分词器默认下限为 1 个字符、上限为 2 个字符。官方文档说明,通常把下限与上限设成相同的值较为妥当,作为出发点 3 个字符比较好。其理由是,长度取得越短,匹配到的文档就越多,但匹配的质量会下降。想放宽幅度时,就需要调整规定了上限与下限之差的容许量的 index.max_ngram_diff,若轻率地想登记很宽的范围,在创建索引的阶段就会被挡下来。也就是说,"总之先把 1 个字符到 5 个字符全都放进去" 这样的设计被加上了刹车。

文本相似度的计算也广泛使用 N-gram。用 Jaccard 系数或余弦相似度比较从两段文本生成的 N-gram 集合的重叠程度,就能把文档间的相似性数值化。这种方法被应用于拼写检查、模糊搜索、抄袭检测、推荐引擎等多种场景。由于不解释单词的意思、只靠字符排列的重叠就能得出数值,因此即使增加支持的语言,也几乎不用改变机制就能沿用,这是实务上的强项。

N-gram 作为语言模型的基础技术也一直发挥着重要作用。在 N-gram 语言模型中,根据紧前面的 N-1 个单词来推定下一个单词的出现概率。在深度学习成为主流之前,它作为机器翻译和语音识别的核心技术被广泛采用。即使在深度学习成为主流之后,由于计算成本低、行为易于说明的优点,在轻量的文本分类与过滤用途上它仍是现役。

另一方面,N-gram 也有几点需要注意。首先是容易搞错 N 变大时成本会在哪里增加。从一段文本中取出的 N-gram 的个数本身,N 越大反而越少。增加的是 "可能存在的 N-gram 的种类" 这一边,它会按字符种类数的 N 次方这样的量级组合式地膨胀。其结果是,实际存在的文本中大半的种类一次都不会出现,在语言模型中如何处理出现次数为零的 N-gram (平滑) 就成了课题。检索索引的容量真正膨胀起来,是在选择了把 1 个字符到 N 个字符同时登记的构成的场合。另外还存在像搜索 "京都" 时 "东京都" 也会命中那样,语义上无关的子串被匹配的误检问题。为了缓解这个问题,采用把 N-gram 与形态素分析组合起来的混合方式的系统也不在少数。

从字符计数的角度来看,N-gram 的分割结果直接取决于文本的长度。从字符数为 L 的文本中生成的字符 N-gram 的个数是 L-N+1 个,文本越短,生成的 N-gram 也越少。要注意的是 L 比 N 小的场合,这个式子在计算上会变成负数,但实际的个数是 0 个。对 3 个字符的文本施加 5-gram,一个也取不出来。在处理短标题或检索查询的设计中,若把 N 取得过大,索引一侧与检索词一侧就都不会登记任何东西,会以 "不报错却总是零命中" 这种难以察觉的形式失败。只要事先掌握所要处理的文本的最短长度,这个陷阱在设计阶段就能避开。

分享这篇文章