【问题标题】:Fast Text Preprocessing快速文本预处理
【发布时间】:2011-03-22 21:02:49
【问题描述】:

在我的项目中,我通常使用文本。我发现预处理可能非常慢。所以我想问你是否知道如何优化我的代码。流程是这样的:

获取 HTML 页面 -> (到纯文本 -> 词干提取 -> 删除停用词) -> 进一步的文本处理

括号中是预处理步骤。应用程序运行大约 10.265 秒,但预处理需要 9.18 秒!现在是预处理 50 个 HTML 页面(不包括下载)的时间。

我使用 HtmlAgilityPack 库将 HTML 转换为纯文本。这是相当快的。转换1个文档需要2.5ms,还算可以。

问题来了。提取一份文档最多需要 120 毫秒。不幸的是,这些 HTML 页面是波兰语的。不存在用 C# 编写的波兰语词干分析器。我只知道用 Java 编写的两个免费使用:stempel 和 morfologic。我在 IKVM 软件的帮助下将 stempel.jar 预编译为 stempel.dll。所以没有什么可做的了。

消除停用词也需要很长时间(1 个文档约 70 毫秒)。这样做是这样的:


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

首先,我删除所有数字、特殊字符、1 和 2 字母单词。然后在循环中我删除停用词。大约有 270 个停用词。

有没有可能让它更快?

编辑:

我想要做的是删除所有不超过 2 个字母的单词。所以我想取出所有特殊字符(包括'.'、','、'?'、'!'等)数字、停用词。我只需要可以用于数据挖掘的纯词。

【问题讨论】:

  • 您是否考虑过使用 hashmap 来搜索单词,而不是一次一个地搜索?

标签: c# regex text-processing


【解决方案1】:

好的,我知道 SO 不是一个纯粹的论坛,也许我不应该回答我自己的问题,但我想分享我的结果。

最后,感谢你们,我设法更好地优化了我的文本预处理。首先,我从我的问题中简化了那个冗长的表达方式(遵循 Josh Kelley 的回答):

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

它与第一个相同,但非常简单。然后再次按照 Josh Kelley 的建议,我将这个正则表达式组装起来。我发现 here 将表达式编译成程序集的好例子。我这样做了,因为这个正则表达式被使用了很多很多次。在讲了几篇关于编译正则表达式的文章之后,这是我的决定。我在删除停用词后删除了最后一个表达式(没有真正意义)。

所以 12KiB 文本文件的执行时间约为 15 毫秒。仅用于上述表达。

最后一步是停用词。我决定对 3 个不同的选项进行测试(执行时间是针对同一个 12KiB 文本文件)。

一个大的正则表达式

所有停用词并编译成程序集(mquander 的建议)。这里没有什么要清除的。

  • 执行时间:~215ms

String.Replace()

人们说这可以比 Regex 更快。因此,对于每个停用词,我都使用了string.Replace() 方法。许多循环与结果:

  • 执行时间:~65ms

LINQ

LBushkin 提出的方法。没什么好说的了。

  • 执行时间:~2.5ms

我只能说哇。只需比较第一个和最后一个的执行时间!非常感谢 LBushkin!

【讨论】:

    【解决方案2】:

    加速你的正则表达式

    您的正则表达式可能需要一些工作。

    例如,这一行:

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

    使用括号捕获停用词以供以后使用,然后它永远不会使用它。也许 .NET 正则表达式引擎足够聪明,可以在这种情况下跳过捕获,也许不是。

    这个正则表达式太复杂了:

    "(([-]|[.]|[-.]|[0-9])?[0-9]*([.]|[,])*[0-9]+)|(\b\w{1,2}\b)|([^\w])"
    
    • "([-]|[.]|[-.]|[0-9])?""([-.0-9])?" 相同。 (除非您尝试将“-.”匹配为您的一种可能性?我假设暂时不需要。)如果您不需要捕获(并且在您的示例中不需要),那么它与 @ 相同987654326@.
    • "[-.0-9]?""[0-9]*" 之前有点多余。您可以进一步简化为"[-.]?[0-9]*"
    • 同样,如果您不需要捕获,则"([.]|[,])*""[,.]*" 相同。

    最后,测试compiling your regexes 是否可以产生更好的性能。

    减少正则表达式和字符串操作

    构造一堆字符串,组成一堆正则表达式对象,然后丢弃它们,就像你在这个循环中所做的那样,可能不是很快:

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

    尝试将停用词预处理为一组 Regex 对象或创建一个怪物预编译的 Regex(正如其他人所建议的那样)。

    重构你的算法

    您似乎只对处理词干、非停用词、文本感兴趣,而不是标点符号、数字等。

    为此,您的算法采用以下方法:

    • 词干所有文本(包括停用词?)。
    • 使用正则表达式(不一定是最快的方法)用空格替换(需要不断地重新排列字符串的主体)非单词。
    • 使用正则表达式(再次强调,不一定是最快的方法)用空格替换(再次)停用词,一次一个停用词。

    我开始在这里写另一种方法,但 LBushkin 打败了我。照他说的做。请记住,作为一般规则,更改算法通常比微优化带来更大的改进,例如改进正则表达式的使用。

    【讨论】:

    • 我有足够的分析 C# 的经验可以说绝对不可能执行该字符串连接的 270 次占用大量时间。如果有 270,000 个,可能值得解决。其余的建议都是可靠的。
    • 老实说(正如您所见),我对正则表达式的知识和经验简直是无所不能:P 我只是在学习如何使用它。我在网上某处找到的这个长模式......实际上我想在删除不相关的字符串之后阻止文本,而不是之前。可能我的帖子说的不是很清楚。但是为了回答:)
    • @mquander:谢谢,我在分析 C# 方面的经验有限。我更新了我的答案。
    【解决方案3】:

    迭代替换单词将成为您实现中的最大瓶颈。 在每次迭代中,您必须扫描整个字符串以查找停用词,然后替换操作必须分配一个新字符串并用替换后的文本填充它。这不会很快。

    一种更有效的方法是对字符串进行标记并以流方式执行替换。 将输入分成单独的单词,由任何合适的空格或分隔符分隔。您可以逐步执行此操作,因此您无需为此分配任何额外的内存。对于每个单词(令牌),您现在可以在停用词哈希集中执行查找 - 如果找到匹配项,您将在将最终文本流式传输到单独的 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 );
    }
    

    【讨论】:

      【解决方案4】:

      我同意 mquander 的观点,这里有更多信息。 每次使用正则表达式时,C# 都会创建一个表来匹配文本。如果您只调用 regex 函数几次,这很好而且很花哨,但是您在这里所做的是创建大约 270 个新表并为每个 html 文档删除它们。

      我会尝试创建一个 Regex 对象,然后使用 |运算符来匹配所有不同的停用词和第一个过滤器。之后,你应该使用正则表达式编译来汇编,以便让 JIT 生成机器码。

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

      你应该会看到这个显着的加速

      【讨论】:

        【解决方案5】:

        与其在循环中替换正则表达式,为什么不动态构造一个匹配任何一个停用词的怪物匹配正则表达式,然后运行一个替换,什么都不替换?如果您的停用词是“what”、“ok”和“yeah”,则类似于"\b(what|ok|yeah)\b"。看起来它可能会更有效率。

        【讨论】:

        • 如果它真的对我有帮助,我会检查并稍后写:)
        • 我不确定这是否总是更有效。我对 .NET 正则表达式做得不多,但是在 Perl 中,我知道一堆小的正则表达式可以比一个怪物正则表达式更快。
        • @msarchet 也许我应该添加一个免责声明:“示例停用词可能非常不适合某些极端情况。”
        • @Josh:我也没有,但绝对值得一试。
        【解决方案6】:

        可能遇到Schlemiel the Painter problem。在 C#(和其他语言)中,当您附加或连接字符串时,您实际上是在创建一个全新的字符串。在循环中执行此操作通常会导致大量内存分配,否则可以避免。

        【讨论】:

        • Stringbuilder 不错。我想知道为什么微软不提供当前值属性或允许扩大与字符串之间的转换——这会让它们更加方便。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-02-19
        • 2019-04-08
        • 1970-01-01
        相关资源
        最近更新 更多