более быстрый способ обнаружить н-граммы в строке?

Я нашел это решение на SO для обнаружения n-граммов в строке: (здесь: N- образование грамма из предложения)

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();
        }
    }
}

=> этот фрагмент кода занимает намного больше времени обработки (28 секунд для обнаружения 1-граммов, 2-граммов, 3-граммов и 4 граммов для моего корпуса: 4 МБ необработанного текста) по сравнению с миллисекундами для других операций (удаление стоп-слов и т. д.)

Кто-нибудь знает решения на Java, которые будут работать быстрее, чем решение с циклами, представленное выше? (Я думал о многопоточности, использовании коллекций или, может быть, о творческих способах разделения строки ...?) Спасибо!


person seinecle    schedule 02.01.2012    source источник
comment
Вы пробовали профилировать то, что требует времени? Создается впечатление, что создается множество ненужных объектов. Это разбиение, которое требует времени, или создание объектов ngram, или их вставка в список?   -  person Roger Lindsjö    schedule 02.01.2012
comment
Я бы начал с того, что не разбивал на отдельные строки, а затем собирал их обратно, а вместо этого просматривал разделитель и просто запоминал индексы, поэтому для 3gram вы отслеживаете разделитель n, n-1, n-2 и n-3. 3gram начинается с n-3 и заканчивается на n. Затем переместите n вперед (m-3 теперь то, что было n-2, и так далее.   -  person Roger Lindsjö    schedule 02.01.2012
comment
спасибо @ RogerLindsjö, это выглядит многообещающим! Я немного пробовал со сканером, но не уверен, что правильно понимаю ваш метод. Если я отслеживаю последние 3 разделителя, как я могу получить соответствующие 3 слова, когда я достигну n (после n-3, n-2, n-1). AFAICS не существует метода сканирования для поиска предыдущих значений (своего рода scanner.previous (), если хотите!). Что я не понимаю? Еще раз спасибо!   -  person seinecle    schedule 02.01.2012


Ответы (2)


Вы можете попробовать что-то вроде этого:

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);
    }
}

При тестировании примерно 5 МБ текста кажется, что он выполняется примерно в 10 раз быстрее, чем исходный код. Основное отличие здесь в том, что регулярное выражение не используется для разделения, и строки ngram не создаются путем конкатенации.

Обновление: это результат, который я получаю при работе с текстом, упомянутым выше, ngram 1-4. Я использую 2 ГБ памяти, чтобы определить влияние на сборку мусора во время выполнения. Я запускал несколько раз, чтобы увидеть влияние компилятора точки доступа.

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
person Roger Lindsjö    schedule 02.01.2012
comment
спасибо Роджер! Я создал цикл для (n = 1; n ‹= 4; n ++), и в этом цикле я создаю экземпляр вашего класса с конструктором NGram (String text, int n): в результате я получаю +/- то же время выполнения как упомянутый в моем вопросе. Кроме того, результаты не соответствуют тем, которые я получил из кода, который я цитирую в своем вопросе. Странный... - person seinecle; 03.01.2012
comment
В последний раз я тестировал 3 грамма. Я должен был изменить это обратно на n или сделать комментарий для значения. Вы можете дать мне образец, дающий другие результаты? Я запустил исходный код и свой код на нескольких разных образцах с одинаковыми результатами. Кстати - результаты будут разными в разных программах. - person karakuricoder; 03.01.2012
comment
Каракури Я еще не тестировал ваш код, мой комментарий выше о другом выводе был адресован Роджеру. Буду рад, если бы вы выложили свою версию с n вместо 3! Кроме того, я сбит с толку: список, возвращаемый вашим классом, находится или нет? Мне кажется, что это так, но что это было бы довольно нелогично в качестве названия для вывода? - person seinecle; 03.01.2012
comment
Когда я проверяю сгенерированные списки, они точно такие же. Вы можете показать какой-нибудь текст, для которого они различаются? - person Roger Lindsjö; 03.01.2012
comment
При запуске с вашим текстом (соединил все строки в одну строку) я получаю почти 570000 нграмм. Время немного различается (много GC), но моя реализация занимает около 100 мс, а ваша - 500 мс. Много времени тратится на сборку мусора (при выполнении цикла создается и выбрасывается много строк). - person Roger Lindsjö; 03.01.2012
comment
да. Реализации ведут себя по-разному для конечных пробелов. У вас мало памяти для вашей программы? Ваша реализация занимает около 2 секунд для 1-4 ngram на моем ноутбуке, а ваши вопросы говорят, что это занимает 28 секунд. - person Roger Lindsjö; 03.01.2012
comment
невероятно невероятно быстро (у) - person Divyang Shah; 27.10.2017

Запуск около 5 мегабайт текста Lorus Ipsum через предоставленный вами код обычно занимал чуть более 7 секунд для обнаружения 1–4 н-граммов. Я переработал код, чтобы составить список самых длинных n-граммов, а затем перебирал этот список, создавая списки последовательно более коротких n-граммов. При тестировании тот же текст занимал около 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);
    }
}
person karakuricoder    schedule 02.01.2012
comment
Спасибо каракури! Я не очень хорошо понимаю ваш код. Почему 3 в качестве аргумента для уменьшения числа граммов, если ищутся 4 н граммы? Спасибо! - person seinecle; 03.01.2012
comment
Тройка осталась от тестирования. Подставляем нужную глубину. Я должен был изменить это на переменную или оставить комментарий. - person karakuricoder; 03.01.2012