【问题标题】:quicker way to detect n-grams in a string?更快的方法来检测字符串中的 n-gram?
【发布时间】:2012-01-03 22:38:52
【问题描述】:

我在 SO 上找到了这个解决方案来检测字符串中的 n-gram: (这里:N-gram generation from a sentence

import java.util.*;

public class Test {

    public static List<String> ngrams(int n, String str) {
        List<String> ngrams = new ArrayList<String>();
        String[] words = str.split(" ");
        for (int i = 0; i < words.length - n + 1; i++)
            ngrams.add(concat(words, i, i+n));
        return ngrams;
    }

    public static String concat(String[] words, int start, int end) {
        StringBuilder sb = new StringBuilder();
        for (int i = start; i < end; i++)
            sb.append((i > start ? " " : "") + words[i]);
        return sb.toString();
    }

    public static void main(String[] args) {
        for (int n = 1; n <= 3; n++) {
            for (String ngram : ngrams(n, "This is my car."))
                System.out.println(ngram);
            System.out.println();
        }
    }
}

=> 与毫秒相比,这段代码花费了迄今为止最长的处理时间(我的语料库检测 1-gram、2-gram、3-gram 和 4gram 需要 28 秒:4Mb 的原始文本)用于其他操作(去除停用词等)

是否有人知道 Java 中的解决方案会比上面介绍的循环解决方案更快? (我在考虑多线程,使用集合,或者可能是创造性的方法来拆分字符串......?)谢谢!

【问题讨论】:

  • 您是否尝试过分析需要时间的内容?似乎创建了很多不需要的对象。是拆分需要时间还是创建 ngram 对象或将它们插入列表?
  • 我会从不拆分成单独的字符串开始,然后将它们组合回来,而是扫描分隔符并只记住索引,因此对于 3gram,您可以跟踪分隔符 n、n-1、 n-2 和 n-3。 3gram 以 n-3 开始,以 n 结束。然后向前移动 n (m-3 现在是 n-2 等等。
  • 谢谢@RogerLindsjö 这看起来很有希望!我用扫描仪试了一下,但我不确定你的方法是否正确。如果我跟踪最后 3 个分隔符,当我达到 n 时(在 n-3、n-2、n-1 之后),如何检索相应的 3 个单词。 AFAICS 没有扫描仪方法可以获取以前的值(如果你愿意的话,一种“scanner.previous()”!)。我没有得到什么?再次感谢!

标签: java nlp n-gram


【解决方案1】:

你可以试试这样的:

public class NGram {

    private final int n;
    private final String text;

    private final int[] indexes;
    private int index = -1;
    private int found = 0;

    public NGram(String text, int n) {
        this.text = text;
        this.n = n;
        indexes = new int[n];
    }

    private boolean seek() {
        if (index >= text.length()) {
            return false;
        }
        push();
        while(++index < text.length()) {
            if (text.charAt(index) == ' ') {
                found++;
                if (found<n) {
                    push();
                } else {
                    return true;
                }
            }
        }
        return true;
    }

    private void push() {
        for (int i = 0; i < n-1; i++) {
            indexes[i] = indexes[i+1];
        }
        indexes[n-1] = index+1;
    }

    private List<String> list() {
        List<String> ngrams = new ArrayList<String>();
        while (seek()) {
            ngrams.add(get());
        }
        return ngrams;
    }

    private String get() {
        return text.substring(indexes[0], index);
    }
}

对大约 5mb 的文本进行测试,它的执行速度似乎比原始代码快 10 倍。这里的主要区别是regex不用于拆分,并且ngram字符串不是通过连接创建的。

更新: 这是我在上面提到的文本 ngram 1-4 上运行时得到的输出。我使用 2GB 内存来确定运行期间对 GC 的影响。跑了多次,看看热点编译器的影响。

Loop 01 Code mine ngram 1 time 071ms ngrams 294121
Loop 01 Code orig ngram 1 time 534ms ngrams 294121
Loop 01 Code mine ngram 2 time 016ms ngrams 294120
Loop 01 Code orig ngram 2 time 360ms ngrams 294120
Loop 01 Code mine ngram 3 time 082ms ngrams 294119
Loop 01 Code orig ngram 3 time 319ms ngrams 294119
Loop 01 Code mine ngram 4 time 014ms ngrams 294118
Loop 01 Code orig ngram 4 time 439ms ngrams 294118

Loop 10 Code mine ngram 1 time 013ms ngrams 294121
Loop 10 Code orig ngram 1 time 268ms ngrams 294121
Loop 10 Code mine ngram 2 time 014ms ngrams 294120
Loop 10 Code orig ngram 2 time 323ms ngrams 294120
Loop 10 Code mine ngram 3 time 013ms ngrams 294119
Loop 10 Code orig ngram 3 time 412ms ngrams 294119
Loop 10 Code mine ngram 4 time 014ms ngrams 294118
Loop 10 Code orig ngram 4 time 423ms ngrams 294118

【讨论】:

  • 谢谢罗杰!我为 (n = 1;n
  • 我最后一次测试是针对 3 克。我应该将其改回 n 或对该值发表评论。你能给我一个产生不同结果的样本吗?我在几个不同的样本上运行了原始代码和我的代码,结果相同。顺便说一句 - 结果在两个程序之间的顺序不同。
  • Karakuri 我还没有测试你的代码,我上面关于不同输出的评论是写给 Roger 的。如果您可以使用 n 而不是 3 发布您的版本,我会很高兴!另外,我很困惑:您的班级返回的 List 是“in”还是“out”?在我看来它是“in”,但这作为输出的名称会非常违反直觉吗?
  • 当我检查生成的列表时,它们完全相同。你能展示一些它们不同的文本吗?
  • 当你的文本运行时(将所有行合并为一行),我得到了近 570000 ngrams。时间略有不同(很多 GC),但我的实现大约需要 100 毫秒,而你的需要 500 毫秒。大量时间花在 GC 上(循环时会生成大量字符串并被丢弃)。
【解决方案2】:

通过您提供的代码运行大约 5 兆的 Lorus Ipsum 文本通常需要大约 7 秒多一点的时间来检测 1 到 4 个 n-gram。我重新编写了代码以制作最长 n-gram 的列表,然后遍历此列表,生成连续较短的 ngram 列表。在测试中,相同的文本大约需要 2.6 秒。此外,它占用的内存要少得多。

import java.util.*;

public class Test {

    public static List<String> ngrams(int max, String val) {
        List<String> out = new ArrayList<String>(1000);
        String[] words = val.split(" ");
        for (int i = 0; i < words.length - max + 1; i++) {
            out.add(makeString(words, i,  max));
        }
        return out;
    }

    public static String makeString(String[] words, int start, int length) {
        StringBuilder tmp= new StringBuilder(100);
        for (int i = start; i < start + length; i++) {
            tmp.append(words[i]).append(" ");
        }
        return tmp.substring(0, tmp.length() - 1);
    }

    public static List<String> reduceNgrams(List<String> in, int size) {
        if (1 < size) {
            List<String> working = reduceByOne(in);
            in.addAll(working);
            for (int i = size -2 ; i > 0; i--) {
                working = reduceByOne(working);
                in.addAll(working);
            }
        }
        return in;
    }

    public static List<String> reduceByOne(List<String> in) {
        List<String> out = new ArrayList<String>(in.size());
        int end;
        for (String s : in) {
            end = s.lastIndexOf(" ");
            out.add(s.substring(0, -1 == end ? s.length() : end));  
        }
        //the last one will always reduce twice - words 0, n-1 are in the loop this catches the words 1, n
        String s = in.get(in.size() -1);
        out.add(s.substring(s.indexOf(" ")+1));
        return out;
    }

    public static void main(String[] args) {
        long start;
        start = System.currentTimeMillis();
        List<String> ngrams = ngrams(3, "Your text goes here, actual mileage may vary");
        reduceNgrams(ngrams, 3);
        System.out.println(System.currentTimeMillis() - start);
    }
}

【讨论】:

  • 谢谢 karakuri!我不太了解您的代码。如果要查找 4-n-gram,为什么要将“3”作为 reduceNgrams 的参数?谢谢!
  • 测试中剩下的 3 个。替换所需的深度。我应该将其更改为变量或提供评论。
猜你喜欢
  • 2013-06-25
  • 2013-09-10
  • 1970-01-01
  • 2016-04-07
  • 1970-01-01
  • 1970-01-01
  • 2014-04-02
  • 2012-12-27
  • 2020-11-10
相关资源
最近更新 更多