【问题标题】:Checking if a string contains an English sentence检查字符串是否包含英文句子
【发布时间】:2013-08-02 17:41:35
【问题描述】:

截至目前,我决定拿一本字典并遍历整个内容。每次看到换行符时,我都会创建一个包含从该换行符到下一个换行符的字符串,然后执行 string.find() 以查看该英文单词是否在其中。这需要很长时间,每个单词大约需要每秒 1/2-1/4 秒来验证。

它运行良好,但我需要每秒检查数千个单词。我可以运行几个窗口,这不会影响速度(多线程),但它仍然只能每秒检查 10 个。 (我需要数千)

我目前正在编写代码来预编译一个包含英语中每个单词的大型数组,这应该会加快速度,但仍然没有达到我想要的速度。 必须有更好的方法来做到这一点。

我正在检查的字符串如下所示:

"hithisisastringthatmustbechecked"

但其中大部分都包含完整的垃圾,只是随机字母。

我无法检查不可能的字母组合,因为在 'thatmust' 之间,该字符串会因为 'tm' 而被丢弃。

【问题讨论】:

  • 单词不能用空格分隔吗?您必须验证所有字母都构成单词,还是至少检测到一个英文单词就足够了?您是否按使用频率对单词进行排序并从最常用的单词开始?
  • 您实际上想要完成什么?字符串中是否有空格?你需要这个完全准确还是概率猜测好?垃圾线是随机字符还是什么?
  • 我会首先生成一个经常引用的非单词的缓存——可能是 4-6 个字符的前缀,这些字符永远不会有效。有几种方法可以做到这一点。
  • 我可以看到一些方法来做到这一点,但我认为神奇的调味汁将在字典数据结构中。也许将字典存储为一棵树,每个节点都是一个字母,所以所有到叶子的路径都会产生一个完整的单词。 (所以 h-a-t
  • 例如,假设“stbe”不是任何有效单词的开头。当你在上面的字符串中找到“stbe”时,你会检查你的缓存,发现它是无效的,然后快速继续。

标签: c++ string linguistics


【解决方案1】:

您可以使用Knuth–Morris–Pratt (KMP) algorithm 加快搜索速度。

遍历字典中的每个单词,然后build a search table for it。你只需要做一次。现在您搜索单个词的速度会更快,因为“错误开始”将被消除。

【讨论】:

  • 你知道 OP 的 string.find() 还没有使用这个算法吗?
  • @LeeMeador 即使是这样,它也无法重用预先构建的搜索表,并且每次都会通过find函数强制重建它。
  • 我明白了。我重读并注意到有多个“待搜索”字符串。除非它们很长,否则我不愿意尝试替换“内置”功能进行搜索。这些可能在与 KMP 算法不同的级别上得到了很好的优化。
【解决方案2】:

有很多策略可以快速做到这一点。

想法 1

获取您正在搜索的字符串,并从某个列开始复制每个可能的子字符串并继续整个字符串。然后将每一个存储在一个以它开头的字母为索引的数组中。 (如果一个字母被使用两次存储较长的子串。

所以数组看起来像这样:

a - substr[0] = "astringthatmustbechecked"
b - substr[1] = "bechecked"
c - substr[2] = "checked"
d - substr[3] = "d"
e - substr[4] = "echecked"
f - substr[5] = null // since there is no 'f' in it
... and so forth

然后,对于字典中的每个单词,在其第一个字母表示的数组元素中进行搜索。这限制了必须搜索的内容的数量。另外,在字符串中的第一个 'r' 之前的任何位置,您都找不到以 'r' 开头的单词。如果字母根本不在其中,有些单词甚至不会进行搜索。

想法 2

通过注意字典中最长的单词来扩展这个想法,并从数组中那些比那个距离更长的字符串中删除字母。

所以你在数组中有这个:

a - substr[0] = "astringthatmustbechecked"

但如果列表中最长的单词是5个字母,则无需保留:

a - substr[0] = "astri"

如果该字母出现多次,您必须保留更多字母。所以这个必须保留整个字符串,因为“e”总是显示不到 5 个字母。

e - substr[4] = "echecked"

您可以在压缩字符串时使用以任何特定字母开头的最长单词来对此进行扩展。

想法 3

这与1和2无关。你可以使用它的想法。

您可以将字典转换为一种存储在链接数据结构中的正则表达式。也可以写正则表达式然后应用它。

假设这些是字典中的单词:

arun
bob
bill
billy
body
jose

构建这种链接结构。 (它实际上是一棵二叉树,我可以解释如何使用它。)

a -> r -> u -> n -> *
|
b -> i -> l -> l -> *
|    |              |
|    o -> b -> *    y -> *
|         |
|         d -> y -> *
|
j -> o -> s -> e -> *

箭头表示一个字母必须跟在另一个字母之后。所以“r”必须在“a”之后,否则不能匹配。

向下的行表示一个选项。您有“a 或 b 或 j”可能的字母,然后是“b”之后的“i 或 o”可能的字母。

正则表达式看起来有点像: /(arun)|(b(ill(y+))|(o(b|dy)))|(jose)/ (尽管我可能漏掉了括号)。这给出了将其创建为正则表达式的要点。

一旦你构建了这个结构,你就可以从第一列开始将它应用到你的字符串中。尝试通过检查替代方案来运行匹配,如果匹配,则试探性地向前移动并尝试箭头后面的字母及其替代方案。如果您到达星号/星号,则匹配。如果您用尽了包括回溯在内的备选方案,请移至下一列。

这是一项繁重的工作,但有时会很方便。

旁注我以前通过编写一个程序构建了其中一个,该程序编写了直接运行算法的代码,而不是让代码查看二叉树数据结构。

认为每组竖线选项都是针对特定字符列的switch 语句,并且每个箭头都变成嵌套。如果只有一个选项,则不需要完整的 switch 语句,只需 if

这是一些快速的字符匹配,并且出于某种今天让我难以理解的原因非常方便。

【讨论】:

  • 这是个好主意!我会尝试实现它,如果它给了我想要的速度,我会接受答案。 :) 该列表包含所有英语语言,而我正在检查的字符串不是那么大,所以想法 2 不起作用。
  • 如果列表很小,这将很有用,但在检查整个字典时几乎每个组合都存在。
  • 但是请记住,使用想法 2,您只能从数组中搜索以特定字母开头的单词的任何特定字符串。所以组合的数量并不重要。重要的是这个词只能从有那个特定字母的地方开始匹配。
  • 我不记得 5 年前写这篇文章时我在做什么,但我只是碰巧在翻阅旧帖子。有趣的是,我在 5 年前知之甚少,但是使用 Trie 解决方案是微不足道的。由于“想法 3”而被接受。
【解决方案3】:

Bloom Filter 怎么样?

Burton Howard Bloom 在 1970 年构想的 Bloom 过滤器是 用于测试的节省空间的概率数据结构 元素是否是集合的成员。假阳性匹配是 可能,但假阴性不是;即查询返回 “在集合内(可能是错误的)”或“绝对不在集合内”。元素可以 被添加到集合中,但不被删除(尽管这可以解决 带有“计数”过滤器)。添加的元素越多 集,误报的概率就越大。

该方法可以按如下方式工作:您创建要检查的一组单词(仅执行一次),然后您可以快速运行“in/not-in”检查每个子字符串.如果结果是“不在”,您可以安全地继续(布隆过滤器不会给出假阴性)。如果结果是“in”,则运行更复杂的检查以确认(布隆过滤器可能会给出误报)。

据我了解,一些拼写检查器依靠布隆过滤器来快速测试您的最新单词是否属于已知单词词典。

【讨论】:

  • 你如何使用这个。提取字符串 OP 正在搜索的每个有效子字符串并将其传递给布隆过滤器?即使您使用只关心长度为 3 且比字典中最长单词更长或更短的信息,提取所有这些子字符串似乎需要做很多工作。
  • 是的 - 将父字符串标记为候选子字符串将非常耗时。布隆过滤器只是一个快速测试子字符串是否不在字典中的想法(在这种情况下,我们会快速前进)。
  • 我喜欢它扭转搜索的想法。它不是将所有单词应用于“待搜索”字符串的子字符串,而是将所有子字符串应用于要查找的单词列表的表示。
【解决方案4】:

此代码由How to split text without spaces into list of words?修改:

from math import log

words = open("english125k.txt").read().split()
wordcost = dict((k, log((i+1)*log(len(words)))) for i,k in enumerate(words))
maxword = max(len(x) for x in words)

def infer_spaces(s):
    """Uses dynamic programming to infer the location of spaces in a string
    without spaces."""

    # Find the best match for the i first characters, assuming cost has
    # been built for the i-1 first characters.
    # Returns a pair (match_cost, match_length).
    def best_match(i):
        candidates = enumerate(reversed(cost[max(0, i-maxword):i]))
        return min((c + wordcost.get(s[i-k-1:i], 9e999), k+1) for k,c in candidates)

    # Build the cost array.
    cost = [0]
    for i in range(1,len(s)+1):
        c,k = best_match(i)
        cost.append(c)

    # Backtrack to recover the minimal-cost string.
    costsum = 0
    i = len(s)
    while i>0:
        c,k = best_match(i)
        assert c == cost[i]
        costsum += c
        i -= k

    return costsum

使用the same dictionary of that answer 并测试您的字符串输出

>>> infer_spaces("hithisisastringthatmustbechecked")
294.99768817854056

这里的诀窍是找出你可以使用的阈值,记住使用较小的单词会使成本更高(如果算法找不到任何可用的单词,它会返回inf,因为它会将所有内容拆分为单字母词)。

【讨论】:

    【解决方案5】:

    理论上,我认为您应该能够训练马尔可夫模型并使用它来确定字符串是可能是句子还是可能是垃圾。还有一个关于这样做是为了识别单词而不是句子的问题:How do I determine if a random string sounds like English?

    句子训练的唯一区别是您的概率表会更大一些。但是,根据我的经验,现代台式计算机具有足够的 RAM 来处理马尔可夫矩阵,除非您正在对整个国会图书馆进行培训(这是不必要的——即使是 5 本书左右不同作者的书也应该足以进行非常准确的分类) .

    由于您的句子是在没有明确的单词边界的情况下混合在一起的,这有点棘手,但好消息是马尔可夫模型不关心单词,只关心后面的内容。因此,您可以通过首先从训练数据中去除所有空格来使其忽略空格。如果您打算使用《爱丽丝梦游仙境》作为训练文本,那么第一段可能看起来像这样:

    爱丽丝开始厌倦了在银行里坐在她姐姐身边,而且她已经两次偷看她的书,但她的妹妹正在看书,但没有图片或对话,而没有图片或对话的书本思想有什么用

    它看起来很奇怪,但就马尔可夫模型而言,它与经典实现是微不足道的。

    我看到您担心时间:培训可能需要几分钟(假设您已经编译了黄金标准“句子”和“随机加扰字符串”文本)。您只需要训练一次,您可以轻松地将“训练过的”模型保存到磁盘,并通过从磁盘加载将其重新用于后续运行,这可能需要几秒钟。对字符串进行调用需要进行少量浮点乘法来获得概率,因此在完成训练后,它应该非常快。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-11-09
      • 2013-05-18
      • 2017-11-02
      • 1970-01-01
      • 2021-12-20
      • 1970-01-01
      • 2013-03-04
      • 2014-11-22
      相关资源
      最近更新 更多