【问题标题】:Looking for a way to optimize this algorithm for parsing a very large string寻找一种方法来优化这个算法来解析一个非常大的字符串
【发布时间】:2011-11-20 00:20:54
【问题描述】:

下面的类解析一个非常大的字符串(整个文本小说)并将其分解为连续的 4 个字符的字符串,这些字符串存储为一个元组。然后可以根据计算为每个元​​组分配一个概率。我将其用作蒙特卡洛/遗传算法的一部分,以训练程序仅基于语法(仅字符转换)识别语言。

我想知道是否有更快的方法来做到这一点。查找任何给定的 4 字符元组的概率大约需要 400 毫秒。相关方法 _Probablity() 位于类的末尾。

这是与我的另一篇文章相关的计算密集型问题:Algorithm for computing the plausibility of a function / Monte Carlo Method

最终我想将这些值存储在 4d 矩阵中。但鉴于字母表中有 26 个字母,这将是一项艰巨的任务。 (26x26x26x26)。如果我只取小说的前 15000 个字符,那么性能会提高很多,但我的数据没有那么有用。

这里是解析文本'source'的方法:

    private List<Tuple<char, char, char, char>> _Parse(string src)
    {
        var _map = new List<Tuple<char, char, char, char>>(); 

        for (int i = 0; i < src.Length - 3; i++)
        {
          int j = i + 1;
          int k = i + 2;
          int l = i + 3;

          _map.Add
            (new Tuple<char, char, char, char>(src[i], src[j], src[k], src[l])); 
        }

        return _map; 
    }

这里是_Probability方法:

    private double _Probability(char x0, char x1, char x2, char x3)
    {
        var subset_x0 = map.Where(x => x.Item1 == x0);
        var subset_x0_x1_following = subset_x0.Where(x => x.Item2 == x1);
        var subset_x0_x2_following = subset_x0_x1_following.Where(x => x.Item3 == x2);
        var subset_x0_x3_following = subset_x0_x2_following.Where(x => x.Item4 == x3);

        int count_of_x0 = subset_x0.Count();
        int count_of_x1_following = subset_x0_x1_following.Count();
        int count_of_x2_following = subset_x0_x2_following.Count();
        int count_of_x3_following = subset_x0_x3_following.Count(); 

        decimal p1;
        decimal p2;
        decimal p3;

        if (count_of_x0 <= 0 || count_of_x1_following <= 0 || count_of_x2_following <= 0 || count_of_x3_following <= 0)
        {
            p1 = e;
            p2 = e;
            p3 = e;
        }
        else
        {
            p1 = (decimal)count_of_x1_following / (decimal)count_of_x0;
            p2 = (decimal)count_of_x2_following / (decimal)count_of_x1_following;
            p3 = (decimal)count_of_x3_following / (decimal)count_of_x2_following;

            p1 = (p1 * 100) + e; 
            p2 = (p2 * 100) + e;
            p3 = (p3 * 100) + e; 
        }

        //more calculations omitted

        return _final; 
    }
}

编辑 - 我正在提供更多细节来解决问题,

1) 严格来说,到目前为止我只使用过英语,但确实必须考虑不同的字母表。目前我只希望程序识别英语,类似于本文中描述的内容:http://www-stat.stanford.edu/~cgates/PERSI/papers/MCMCRev.pdf

2) 我正在计算 n

[t][h]

[t][h][a]

[t][h][a][t]

[t][h] 被赋予最大权重,然后是 [t][h][a],然后是 [t][h][a][t]。由于我不只是将 4 字符元组视为一个单元,因此我无法将文本中 [t][h][a][t] 的实例除以总数。接下来是 4 元组。

分配给每个 4 元组的值不能过拟合文本,因为偶然许多真正的英语单词可能永远不会出现在文本中,它们不应该得到不成比例的低分。强调一阶字符转换(2 元组)可以改善这个问题。移动到 3 元组,然后 4 元组只是细化了计算。

我想出了一个字典,它简单地计算元组在文本中出现的频率(类似于 Vilx 建议的),而不是重复相同的元组,这会浪费内存。这让我从每次查找约 400 毫秒到每次约 40 毫秒,这是一个相当大的改进。但是,我仍然需要研究其他一些建议。

【问题讨论】:

  • 26^4 还不错。只有456,976。即使您将每个计数存储为 64 位数量,它仍然只有 3.5MB。对于今天的计算机来说几乎没有。你试过这种方法吗?
  • 嗯,没错,但我遇到的主要障碍是,计算矩阵中的每个单元格并为其赋值大约需要 400 毫秒。因此,如果要分配 456,976 个值,那大约是 50 小时。我觉得对吗?我可以将 400 毫秒降低到大约 1 毫秒,但我只看一个小得多的样本,比如小说的前 15,000 个字符。
  • 我不知道所需的计算,但你不能只看一遍文本,逐个字符,递增每个字符上的相应单元格吗?然后在每个单元格中,您都有该特定 4 字符组合的计数。然后,您将每个单元格除以 4 字符组合的总数(始终为 total_number_of_chars_in_text - 3)。您甚至可以避免预先进行除法,并在每次需要特定组合的概率时进行除法。
  • 你说:“但是鉴于字母表中有 26 个字母......”。你必须小心那里。到那时,您几乎可以确定您的文本是英文 =)
  • +1。 26 仅适用于英文文本。并且你应该小心避免使用标点符号、空格、换行符等。

标签: c# artificial-intelligence nlp tuples montecarlo


【解决方案1】:

我建议更改数据结构以使其更快...

我认为Dictionary&lt;char,Dictionary&lt;char,Dictionary&lt;char,Dictionary&lt;char,double&gt;&gt;&gt;&gt; 会更有效率,因为您在计算时会访问每个“级别”(Item1...Item4)...并且您会将结果缓存在最里面的Dictionary 所以下次你根本不需要计算..

【讨论】:

  • 我不会使用 3 级深度嵌套字典,而是将列表保持原样,并为每个元组成员创建 4 个平面 ILookup 对象索引。
  • 我会试试字典。 @Aviad,您能详细说明一下 ILookup 策略吗?
【解决方案2】:

在 yoiu 概率方法中,您将地图迭代 8 次。您的每个 where 都会迭代整个列表,计数也是如此。在结尾添加 .ToList() 广告会(可能)加快速度。也就是说,我认为您的主要问题是您选择存储数据的结构不适合概率方法的目的。您可以创建一个一次性版本,其中存储数据的结构计算插入时的暂定分布。这样,当您完成插入(不应太慢)时,您就完成了,或者您可以按照下面的代码在您需要时对概率进行廉价计算。

顺便说一句,您可能需要考虑标点符号和空格。句子的第一个字母/单词和单词的第一个字母通过将标点符号和空格作为您分布的一部分,清楚地表明给定文本是用什么语言编写的,您包括样本数据的这些特征。几年前我们就这样做了。这样做,我们表明仅使用三个字符几乎一样准确(我们在测试数据上没有失败三个字符,并且几乎同样准确是假设存在一些奇怪的文本,缺少信息会产生不正确的结果) .使用更多(我们测试到 7 个),但三个字母的速度使其成为最佳情况。

编辑

这是我认为如何在 C# 中实现的示例

class TextParser{
        private Node Parse(string src){
            var top = new Node(null);

            for (int i = 0; i < src.Length - 3; i++){
                var first = src[i];
                var second = src[i+1];
                var third = src[i+2];
                var fourth = src[i+3];

                var firstLevelNode = top.AddChild(first);
                var secondLevelNode = firstLevelNode.AddChild(second);
                var thirdLevelNode = secondLevelNode.AddChild(third);
                thirdLevelNode.AddChild(fourth);
            }

            return top;
        }
    }

    public class Node{
        private readonly Node _parent;
        private readonly Dictionary<char,Node> _children 
                         = new Dictionary<char, Node>();
        private int _count;

        public Node(Node parent){
            _parent = parent;
        }

        public Node AddChild(char value){
            if (!_children.ContainsKey(value))
            {
                _children.Add(value, new Node(this));
            }
            var levelNode = _children[value];
            levelNode._count++;
            return levelNode;
        }
        public decimal Probability(string substring){
            var node = this;
            foreach (var c in substring){
                if(!node.Contains(c))
                    return 0m;
                node = node[c];
            }
            return ((decimal) node._count)/node._parent._children.Count;
        }

        public Node this[char value]{
            get { return _children[value]; }
        }
        private bool Contains(char c){
            return _children.ContainsKey(c);
        }
    }

那么用法是:

var top = Parse(src);
top.Probability("test");

【讨论】:

    【解决方案3】:

    好的,我没有时间详细说明,但这确实需要

    • 神经分类器网络(随便拿个现成的,即使是 Controllable Regex Mutilator 也能以更高的可扩展性完成这项工作)——启发式算法优于蛮力

    • 您可以使用尝试(Patricia Tries a.k.a. Radix Trees 制作可以稀疏的数据结构的空间优化版本(字典字典字典...对我来说看起来像是这个的近似值)

    【讨论】:

      【解决方案4】:

      对于 parse 函数,您无能为力。但是,元组似乎是来自大量文本的四个连续字符。为什么不直接用 int 替换元组,然后在需要字符值时使用 int 来索引大量文本。您的基于元组的方法实际上消耗了原始文本将使用的内存的四倍,并且由于内存通常是性能的瓶颈,因此最好尽可能少地使用。

      然后您尝试查找文本正文中与一组字符的匹配数。我想知道对原始文本正文的直接线性搜索与您正在使用的 linq 语句相比如何? .Where 将进行内存分配(这是一个缓慢的操作),而 linq 语句将有解析开销(但编译器可能会在这里做一些聪明的事情)。对搜索空间有一个很好的了解将更容易找到最佳算法。

      但是,正如 cmets 中提到的,使用 264 矩阵将是最有效的。解析一次输入文本并在解析时创建矩阵。您可能需要一套字典:

      SortedDictionary <int,int> count_of_single_letters; // key = single character
      SortedDictionary <int,int> count_of_double_letters; // key = char1 + char2 * 32
      SortedDictionary <int,int> count_of_triple_letters; // key = char1 + char2 * 32 + char3 * 32 * 32
      SortedDictionary <int,int> count_of_quad_letters;   // key = char1 + char2 * 32 + char3 * 32 * 32 + char4 * 32 * 32 * 32
      

      最后,关于数据类型的说明。您正在使用 decimal 类型。这不是一种高效的类型,因为没有直接映射到 CPU 本机类型,并且在处理数据时存在开销。改用双精度,我认为精度就足够了。最精确的方法是将概率存储为两个整数,即分子和分母,然后尽可能晚地进行除法。

      【讨论】:

        【解决方案5】:

        这里最好的方法是使用稀疏存储并在每个 10000 个字符之后进行修剪。在这种情况下,最好的存储结构是前缀树,它将允许快速计算概率、更新和稀疏存储。您可以在此 javadoc http://alias-i.com/lingpipe/docs/api/com/aliasi/lm/NGramProcessLM.html

        中找到更多理论

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-07-06
          • 2021-12-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多