Быстро Текст Предобработки

в моем проекте я работаю с текстом в целом. Я обнаружил, что предварительная обработка может быть очень медленным. Поэтому я хотел бы спросить вас, знаете ли вы, как оптимизировать мой код. Поток такой:

получить HTML-страницу - > (для обычного текста - > stemming - > удалить стоп-слова) - > дальнейшая обработка текста

в скобках указаны шаги предварительной обработки. Приложение работает в 10.265, но предобработки принимает 9.18 ы! Это время для предварительной обработки 50 HTML-страниц (исключая загрузка.)

Я использую библиотеку HtmlAgilityPack для преобразования HTML в обычный текст. Это довольно быстро. Для преобразования 1 документа требуется 2,5 МС, поэтому это относительно нормально.

проблема приходит позже. Stemming один документ принимает до 120ms. К сожалению, эти HTML-страницы на польском языке. Не существует stemmer для польского языка, написанного на C#. Я знаю только 2 свободно использовать написанные на Java: stempel и морфологическое. Я предкомпилированного штемпель.банку штемпель.dll с помощью программного обеспечения IKVM. Так делать больше нечего.

устранение стоп-слов занимает также много времени (~70 мс для 1 doc). Делается это так:


result = Regex.Replace(text.ToLower(), @"(([-]|[.]|[-.]|[0-9])?[0-9]*([.]|[,])*[0-9]+)|(bw{1,2}b)|([^w])", " ");
while (stopwords.MoveNext())
{
   string stopword = stopwords.Current.ToString();                
   result = Regex.Replace(result, "(b"+stopword+"b)", " ");                               
}
return result;

сначала я удаляю все цифры, специальные символы, 1 - и 2-х букв. Затем в цикле я удаляю стоп-слова. Насчитывается около 270 стоп-слов.

можно ли сделать это быстрее?

Edit:

то, что я хочу сделать, это удалить все, что не является словом длиннее 2 букв. Так Что Я ... хотите получить все специальные символы (в том числе '.",",","?', '!', п.) числа, стоп-слова. Мне нужны только чистые слова, которые я могу использовать для интеллектуального анализа данных.

6 ответов


итеративная замена слов будет самым большим узким местом в вашей реализации. на каждой итерации вы должны сканировать всю строку для стоп-слова, затем операция замены должна выделить новую строку и заполнить ее текстом после замены. Это будет не быстро.

гораздо более эффективным подходом является токенизация строки и выполнение замены потоковым способом. разделить входные данные на отдельные слова разделенные любыми пробелами или символами-разделителями. Вы можете делать это постепенно, поэтому вам не нужно выделять дополнительную память для этого. Для каждого слова (токена) теперь вы можете выполнить поиск в хэш - наборе стоп-слов-если вы найдете совпадение, вы замените его при потоковой передаче окончательного текста на отдельный StringBuilder. Если токен не является стоп-словом, просто передайте его StringBuilder неизмененной. Этот подход должен иметь производительность O(n), так как он сканирует строку только один раз и использует HashSet для выполнения поиска стоп-слов.

Ниже приведен один подход, который я ожидал бы выполнить лучше. Хотя он не полностью потоковый (он использует String.Split() который выделил массив дополнительных строк), он выполняет всю обработку за один проход. Уточнение кода, чтобы избежать выделения дополнительной строки, вероятно, не обеспечит значительного улучшения, так как вам все равно нужно извлечь подстроки для сравнения с вашими стоп-словами.

код ниже возвращает список слов, который исключает все стоп-слова и слова из двух букв или короче результата. Он также использует сравнение без учета регистра в стоп-словах.

public IEnumerable<string> SplitIntoWords( string input,
                                           IEnumerable<string> stopwords )
{
    // use case-insensitive comparison when matching stopwords
    var comparer = StringComparer.InvariantCultureIgnoreCase;
    var stopwordsSet = new HashSet<string>( stopwords, comparer );
    var splitOn = new char[] { ' ', '\t', '\r' ,'\n' };

    // if your splitting is more complicated, you could use RegEx instead...
    // if this becomes a bottleneck, you could use loop over the string using
    // string.IndexOf() - but you would still need to allocate an extra string
    // to perform comparison, so it's unclear if that would be better or not
    var words = input.Split( splitOn, StringSplitOptions.RemoveEmptyEntries );

    // return all words longer than 2 letters that are not stopwords...
    return words.Where( w => !stopwordsSet.Contains( w ) && w.Length > 2 );
}

хорошо, я знаю, что SO не является чистым форумом, и, возможно, я не должен отвечать на свой вопрос, но я хотел бы поделиться своими результатами.

наконец, благодаря вам, ребята, мне удалось получить лучшую оптимизацию предварительной обработки текста. Прежде всего, я упростил это длинное выражение из моего вопроса (после ответа Джоша Келли):

[0-9]|[^\w]|(\b\w{1,2}\b)

он делает то же самое, что и первый, но очень прост. Затем, следуя предложению Джоша Келли, Я снова собрал это регулярное выражение. Отличный пример компиляции выражений в сборку я нашел здесь. Я сделал это, потому что это регулярное выражение используется много, много раз. После лекции из нескольких статей о скомпилированном regex, это было мое решение. Я удалил последнее выражение после устранения стоп-слов (без реального смысла).

таким образом, время выполнения текстового файла 12KiB составляло ~15 мс. Это только для выражения, упомянутого выше.

последним шагом были стоп-слова. Я решил сделать тест для 3 разных параметры (время выполнения на тот же текстовый файл 12KiB).

одно большое регулярное выражение

со всеми стоп-словами и скомпилированными в сборку (предложение mquander). Здесь нечего убирать.

  • время выполнения: ~215ms

строку.Replace ()

люди говорят, что это может быть быстрее, чем регулярное выражение. Поэтому для каждого стоп-слова я использовал string.Replace() метод. Много петель, чтобы взять с результатом:

  • выполнение время: ~65ms

LINQ

метод представлен LBushkin. Больше нечего сказать.

  • время выполнения: ~2,5 МС

Я могу только сказать wow. Просто сравните время выполнения первого с последним! Большое спасибо LBushkin!


вместо замены регулярного выражения в цикле, почему бы не динамически построить соответствующее регулярное выражение монстра, которое соответствует любому из ваших стоп-слов, а затем запустить одну замену, заменив ее ничем? Что-то вроде "\b(what|ok|yeah)\b" Если ваш стоп-слова "что", "ОК" И "да". Это кажется более эффективным.


ускорить ваши regexes

ваши regexes могли бы использовать некоторую работу.

например, эта строка:

result = Regex.Replace(result, "(\b"+stopword+"\b)", " ");

использует круглые скобки для захвата стоп-слова для последующего использования, а затем никогда не использует его. Возможно, движок .NET regex достаточно умен, чтобы пропустить захват в этом случае, возможно, нет.

это регулярное выражение слишком сложно:

"(([-]|[.]|[-.]|[0-9])?[0-9]*([.]|[,])*[0-9]+)|(\b\w{1,2}\b)|([^\w])"
  • "([-]|[.]|[-.]|[0-9])?" идентичен "([-.0-9])?". (Если вы не пытаетесь спичка.-" - как одна из Ваших возможностей? Полагаю, не сейчас.) Если вам не нужен захват (и вы не в своем примере), то он идентичен "[-.0-9]?".
  • "[-.0-9]?" немного избыточно, прежде чем "[0-9]*". Вы можете еще больше упростить его до "[-.]?[0-9]*".
  • аналогично, если вам не нужен захват, то "([.]|[,])*" идентичен "[,.]*".

наконец, проверить, если компиляция регулярных выражений может лучше уступить спектакль.

сократить регулярное выражение и манипуляции со строками

построение связки строк, составляющих связку объектов регулярных выражений, а затем отбрасывание их, как вы делаете в этом цикле, вероятно, не очень быстро:

result = Regex.Replace(result, "(\b"+stopword+"\b)", " ");  

попробуйте предварительно обработать стоп-слова в массив объектов Regex или создать одно предварительно скомпилированное регулярное выражение monster (как предлагали другие).

реструктуризировать ваш алгоритм

похоже, вас интересует только обработка стволовых, нон-стоп-слов, текста, а не пунктуации, чисел и т. д.

для этого ваш алгоритм использует следующий подход:

  • Stem весь текст (включая стоп-слова?).
  • используйте regexes (не обязательно самый быстрый подход), чтобы заменить (что требует постоянной перестановки тела строки) не-слова пробелами.
  • использовать регулярные выражения (опять же, не обязательно самый быстрый подход) заменить (снова) стоп-слова пробелами, по одному стоп-слову за раз.

Я начал писать здесь другой подход, но LBushkin меня опередил. Делай, что он говорит. Имейте в виду, как правило, что изменение алгоритма обычно дает большие улучшения, чем микро-оптимизации, такие как улучшение использования регулярных выражений.


вы might работает в Schlemiel проблема художника. В C# (и других языках), когда вы добавляете или объединяете строки, вы фактически создаете совершенно новую строку. Выполнение этого в цикле часто вызывает много выделения памяти, которое в противном случае можно избежать.


Я согласен с mquander, и вот немного больше информации. Каждый раз, когда вы используете регулярное выражение, C# создаст таблицу в соответствии с текстом. Это прекрасно и Денди, если вы вызываете функцию regex только пару раз, но то, что вы делаете здесь, создает около 270 новых таблиц и разбивает их для каждого html-документа.

Я бы попытался просто создать один объект Regex и использовать оператор|, чтобы соответствовать всем различным стоп-словам и первому фильтру. После этого, вы должны использовать regex компиляция сборки, чтобы компилятор генерирует машинный код.

http://en.csharp-online.net/CSharp_Regular_Expression_Recipes%E2%80%94Compiling_Regular_Expressions

вы должны увидеть резкое ускорение только с этим