寻找一种优化此算法来解析非常大的字符串的方法

时间:2011-09-19 07:03:01

标签: c# artificial-intelligence nlp tuples montecarlo

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

我想知道是否有更快的方法。查找任何给定的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元组的概率,其中n <= 4.例如,如果我计算字符串“that”的总概率,我会将其分解为这些独立的元组并计算每个人首先的概率:

[t] [h]

[T] [H] [α]

[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毫秒,这是一个非常大的改进。不过,我仍然需要研究其他一些建议。

5 个答案:

答案 0 :(得分:1)

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

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

答案 1 :(得分:1)

在yoiu概率方法中,您将迭代地图8次。每个人都会遍历整个列表,计数也是如此。最后添加.ToList()广告会(可能)加速。这就是说,我认为你的主要问题是,你为了存储数据而选择的结构不适合概率方法的目的。您可以创建一个通过版本,其中您存储数据的结构计算插入时的暂定分布。这样当你完成插入(不应该减慢太多)时,你就完成了,或者你可以这样做,因为下面的代码可以在你需要的时候便宜地计算概率。

另外,您可能需要考虑puntuation和空白。一个句子的第一个字母/单词和一个单词的第一个字母通过采用标点符号和空白作为您分发的一部分,清楚地表明给定文本的语言是什么,包括样本数据的那些特征。几年前我们做到了。这样做我们表明只使用三个字符几乎一样精确(我们的测试数据没有三个没有失败,并且几乎同样的假设是假设有一些奇怪的文本,其中缺少信息会产生不正确的结果) 。使用更多(我们测试到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");

答案 2 :(得分:1)

好的,我没有时间计算细节,但这确实需要

  • 神经分类器网(只需要任何现成的,即使是Controllable Regex Mutilator也会以更大的可扩展性来完成工作) - 启发式蛮力

  • 你可以使用try(Patricia Tries a.k.a. Radix Trees来创建一个可以稀疏的数据结构的空间优化版本(字典词典词典...看起来像是对我的近似)

答案 3 :(得分:1)

现在解析函数的功能并不多。但是,元组似乎是来自大量文本的四个连续字符。为什么不用int替换元组,然后在需要字符值时使用int来索引大量文本。你的基于元组的方法实际上消耗了原始文本使用的内存的四倍,并且因为内存通常是性能的瓶颈,所以最好尽可能少地使用。

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

但是,正如评论中提到的那样,使用26 4 矩阵将是最有效的。解析输入文本一次并在解析时创建矩阵。你可能想要一组字典:

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本机类型,并且处理数据存在开销。使用双倍代替,我认为精度就足够了。最精确的方法是将概率存储为两个整数,即分子和分母,然后尽可能晚地进行除法。

答案 4 :(得分:1)

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

中找到更多理论