【问题标题】:Reconstruct input string given ngrams of that string给定字符串的 ngram 重构输入字符串
【发布时间】:2020-10-10 00:16:08
【问题描述】:

给定一个字符串,例如i am a string.

我可以像这样使用 nltk 包生成这个字符串的 n-gram,其中 n 是根据指定范围的变量。

from nltk import ngrams 

s = 'i am a string'
for n in range(1, 3):
    for grams in ngrams(s.split(), n):
        print(grams)

给出输出:

('i',)
('am',)
('a',)
('string',)
('i', 'am')
('am', 'a')
('a', 'string')

有没有办法使用生成的 ngram 组合来“重构”原始字符串?或者,用下面评论者的话来说,有没有办法将句子分成连续的单词序列,其中每个序列的最大长度为 k(在这种情况下 k 为 2)。

[('i'), ('am'), ('a'), ('string')]
[('i', 'am'), ('a'), ('string')]
[('i'), ('am', 'a'), ('string')]
[('i'), ('am'), ('a', 'string')]
[('i', 'am'), ('a', 'string')]

这个问题与this 的问题类似,但更复杂。

工作解决方案 - 改编自 here

我有一个可行的解决方案,但是对于较长的字符串,它真的很慢

def get_ngrams(s, min_=1, max_=4):
    token_lst = []
    for n in range(min_, max_):
        for idx, grams in enumerate(ngrams(s.split(), n)):
            token_lst.append(' '.join(grams))
    return token_lst 

def to_sum_k(s):
    for len_ in range(1, len(s.split())+1):
        for i in itertools.permutations(get_ngrams(s), r=len_):
            if ' '.join(i) == s:
                print(i)

to_sum_k('a b c')

【问题讨论】:

  • 基本上你使用它和一个累积和来找出你输入的所有可能的分割,这相当于你正在寻找的东西。
  • “类似于下面”不是很精确;你应该尽量使你的问题描述尽可能明确。您的意思是“将句子分成连续的单词序列,其中每个序列的最大长度为k(在这种情况下k 为2)”?或者你的意思是用所有可能的方式将句子分成一元和二元?
  • 嗨@rici,你一针见血。我已将问题更新为更具体。
  • @Andy 我的原始答案假设我们不知道原始字符串。我将添加一个新答案

标签: python algorithm


【解决方案1】:

编辑:
这个答案是基于这样的假设,即问题是根据它的 ngram 重建一个未知的唯一字符串。我会为任何对此问题感兴趣的人保留它。在 cmets 中阐明的实际问题的实际答案可以在 here 找到。
编辑结束

一般不会。考虑例如n = 2s = "a b a b" 的情况。那么你的 ngram 将是

[("a"), ("b"), ("a", "b"), ("b", "a")]

在这种情况下,生成这组 ngram 的字符串集可能是由

(ab(a|(ab)*a?))|(ba(b|(ba)*b?)

n = 2s = "a b c a b d a",其中"c""d" 可以在生成字符串中任意排序。例如。 "a b d a b c a" 也是一个有效的字符串。此外,还会出现与上述相同的问题,并且任意数量的字符串都可以生成 ngram 集。

话虽如此,有一种方法可以测试一组 ngram 是否唯一标识一个字符串:
将您的字符串集视为对非确定性状态机的描述。每个 ngram 可以定义为一个状态链,其中单个字符是转换。作为 ngram [("a", "b", "c"), ("c", "d"), ("a", "d", "b")] 的示例,我们将构建以下状态机:

0 ->(a) 1 ->(b) 2 ->(c) 3
0 ->(c) 3 ->(d) 4
0 ->(a) 1 ->(d) 5 ->(b) 6

现在执行此状态机的确定。如果存在可以从 ngram 重构的唯一字符串,则状态机将具有最长的转换链,其中不包含任何循环并包含我们构建原始状态机的所有 ngram。在这种情况下,原始字符串只是该路径的各个状态转换重新连接在一起。否则,存在可以从提供的 ngram 构建的多个字符串。

【讨论】:

  • baba 对您的第一个示例中的 ngram 不也有效吗?
  • @HansOlsson 很好。我完全错过了这种可能性。我会更新答案
【解决方案2】:

虽然我的previous answer 假设问题是根据它的 ngrams 找到一个未知字符串,但这个答案将解决寻找所有方法来使用它的 ngrams 构造给定字符串的问题。

假设允许重复,解决方案相当简单:生成所有可能的数字序列,总和等于原始字符串的长度,其中没有大于n 的数字,并使用它们来创建 ngram 组合:

import numpy

def generate_sums(l, n, intermediate):
    if l == 0:
        yield intermediate
    elif l < 0:
        return
    else:
        for i in range(1, n + 1):
            yield from generate_sums(l - i, n, intermediate + [i])

def combinations(s, n):
    words = s.split(' ')
    for c in generate_sums(len(words), n, [0]):
        cs = numpy.cumsum(c)
        yield [words[l:u] for (l, u) in zip(cs, cs[1:])]

编辑:
正如 cmets 中的 @norok2(感谢您的工作)所指出的那样,使用替代 cumsum-implementations 而不是 numpy 为这个用例提供的实现似乎更快。
结束编辑

如果不允许重复,事情就会变得有点棘手。在这种情况下,我们可以使用我之前回答中定义的非确定性有限自动机,并根据自动机的遍历构建我们的序列:

def build_state_machine(s, n):
    next_state = 1
    transitions = {}
    for ng in ngrams(s.split(' '), n):
        state = 0
        for word in ng:
            if (state, word) not in transitions:
                transitions[(state, word)] = next_state
                next_state += 1

            state = transitions[(state, word)]

     return transitions

def combinations(s, n):
    transitions = build_state_machine(s, n)
    states = [(0, set(), [], [])]

    for word in s.split(' '):
        new_states = []
        for state, term_visited, path, cur_elem in states:
            if state not in term_visited:
                new_states.append((0, term_visited.union(state), path + [tuple(cur_elem)], []))
            if (state, word) in transitions:
                new_states.append((transitions[(state, word)], term_visited, path, cur_elem + [word]))

        states = new_states

   return [path + [tuple(cur_elem)] if state != 0 else path for (state, term_visited, path, cur_elem) in states if state not in term_visited]

例如,将为字符串 "a b a" 生成以下状态机:

红色连接表示切换到下一个 ngram,需要单独处理(第二个 if 在循环中),因为它们只能被遍历一次。

【讨论】:

  • 伟大而有见地的答案,非常感谢!我要做的唯一改变是,我认为 yield [s[l:u] for (l, u) in zip(cs, cs[1:])] 应该改为 yield [words[l:u] for (l, u) in zip(cs, cs[1:])] :D
  • 我认为累积和的显式计算效率低于手动循环解决方案(并且需要导入 numpy)。我的 quick tests 表示加速了 3 倍。
  • @Andy 感谢您指出这一点。我会改正的。
  • @norok2 感谢您完成这项工作,我会将其添加到我的答案中。实际上我没想到。似乎很奇怪。旁注:一般来说,这些 sn-ps 旨在以(希望)可理解的方式显示基本算法,而不是高性能。两段代码都有很大的优化空间。
  • @norok2 非常感谢您准备和运行这些测试 - 非常有用和有趣
猜你喜欢
  • 1970-01-01
  • 2020-02-14
  • 1970-01-01
  • 2015-01-22
  • 1970-01-01
  • 1970-01-01
  • 2021-04-03
  • 2021-01-25
  • 1970-01-01
相关资源
最近更新 更多