计算函数合理性的算法/蒙特卡罗方法

本文关键字:蒙特卡罗 方法 算法 函数 合理性 计算 | 更新日期: 2023-09-27 18:08:31

我正在编写一个程序,试图复制本文开头讨论的算法,

http://www-stat.stanford.edu/cgates/PERSI/论文/MCMCRev.pdf

F是一个从char到char的函数。假设Pl(f)是该函数的"合理性"度量。算法为:

从对函数的初步猜测开始,比如f,然后是一个新的函数f*——

  • 计算Pl (f)。
  • 将f赋给两个符号的值随机调换为f*。
  • 计算Pl (f *);如果该值大于Pl(f),则接受f*.
  • 如果没有,抛一个Pl(f)/Pl(f*)硬币;如果出现正面,接受f*.
  • 如果抛硬币是反面,保持在f。

我使用下面的代码实现它。我正在使用c#,但试图使它对每个人都更简化。如果有更好的论坛,请让我知道。

 var current_f = Initial();    // current accepted function f
 var current_Pl_f = InitialPl();  // current plausibility of accepted function f
 for (int i = 0; i < 10000; i++)
        {
            var candidate_f = Transpose(current_f); // create a candidate function
            var candidate_Pl_f = ComputePl(candidate_f);  // compute its plausibility
            if (candidate_Pl_f > current_Pl_f)            // candidate Pl has improved
            {
                current_f = candidate_f;            // accept the candidate
                current_Pl_f = candidate_Pl_f; 
            }
            else                                    // otherwise flip a coin
            {
                int flip = Flip(); 
                if (flip == 1)                      // heads
                {
                    current_f = candidate_f;        // accept it anyway
                    current_Pl_f = candidate_Pl_f; 
                }
                else if (flip == 0)                 // tails
                {
                    // what to do here ?
                }
            }
        }

我的问题基本上是这看起来是不是实现该算法的最佳方法。似乎我可能会陷入一些局部最大值/局部最小值,尽管实施这种方法。

EDIT -这是转置()方法背后的本质。我使用类型为<<的字典/哈希表。Char, Char>>,候选函数用来查找任何给定的Char -> Char转换。所以转置方法只是交换字典中指示函数行为的两个值。

    private Dictionary<char, char> Transpose(Dictionary<char, char> map, params int[] indices)
    {
        foreach (var index in indices)
        {
            char target_val = map.ElementAt(index).Value;   // get the value at the index
            char target_key = map.ElementAt(index).Key;     // get the key at the index
            int _rand = _random.Next(map.Count);   // get a random key (char) to swap with
            char rand_key = map.ElementAt(_rand).Key;
            char source_val = map[rand_key]; // the value that currently is used by the source of the swap
            map[target_key] = source_val; // make the swap
            map[rand_key] = target_val;
        }
        return map; 
    }

请记住,使用底层字典的候选函数基本上只是:

public char GetChar(char in, Dictionary<char, char> theMap)
{
     return theMap[char]; 
}

这是计算Pl(f)的函数:

    public decimal ComputePl(Func<char, char> candidate, string encrypted, decimal[][] _matrix)
    {
        decimal product = default(decimal);
        for (int i = 0; i < encrypted.Length; i++)
        {
            int j = i + 1;
            if (j >= encrypted.Length)
            {
                break;
            }
            char a = candidate(encrypted[i]);
            char b = candidate(encrypted[j]);
            int _a = GetIndex(_alphabet, a); // _alphabet is just a string/char[] of all avl chars 
            int _b = GetIndex(_alphabet, b);
            decimal _freq = _matrix[_a][_b]; 
            if (product == default(decimal))
            {
                product = _freq;
            }
            else
            {
                product = product * _freq;
            }
        }
        return product;
    }

计算函数合理性的算法/蒙特卡罗方法

暂时,codereview.stackexchange.com可能是一个"更好的论坛"。
尽管如此,我还是要试一试:

    乍一看,所示的代码片段是该算法的正确的实现。
  • 算法是否会在局部最小值中"卡住"是与算法有关的问题,而不是与实现有关的问题。(见下文讨论)
  • 您对"最佳方法"的追求似乎是针对算法(偏离原始算法)的调整,而不是在实现中的调整(使其更快和/或根除一些可能的缺陷)。有关算法的考虑,请参见下面的讨论;有关实施的讨论,请考虑以下事项:
    • 确保Flip()方法是公平的
    • 确保ComputePl()是正确的:由于值函数的算术精度问题,经常会出现一些错误。
    • 使用转置()方法确保公平性(等概率)。
    • 性能改进可能来自于ComputePl()方法的优化(未显示),而不是主循环。

关于算法本身及其对不同问题的适用性的讨论。
简而言之,该算法是一种引导随机搜索,通过两种随机设备对[巨大的]解空间进行采样:转置()方法(每次都非常轻微地修改)当前候选函数,而Flip()方法决定[局部]次优解是否应该存在。搜索由目标函数ComputePl()引导,该函数本身基于参考语料库中的一阶转移矩阵。

在这种情况下,可以通过增加选择"次优"函数的概率来避免局部最小"沥青坑":与其公平的50-50 Flip(),不如尝试保留66%甚至75%的"次优"解的概率。这种方法通常扩展了收敛到最优解所需的代数,但是,如前所述,可以避免陷入局部最小值。

确保算法适用性的另一种方法是确保对给定函数的合理性进行更好的评估。对于该算法的相对成功和通用性的可能解释如下

  • 英语语言中的一阶转换分布不是很均匀(因此通过对匹配的奖励和对不匹配的惩罚来很好地模拟给定函数的合理性)。这种多维统计,比给定语言/语料库中字符的"0阶"分布更有弹性。
  • 一阶转换的分布虽然是特定于语言的,但在不同的语言和/或术语词汇的上下文中(基于所述语言)通常是相似的。在速记行话的情况下,事情确实会崩溃,因为像元音这样的字母通常被省略。

因此,为了提高算法对给定问题的适用性,请确保所使用的分布矩阵尽可能与底层文本的语言和领域相匹配。

从文章中的描述来看,您的实现似乎是正确的(您标记为"这里要做什么"的部分实际上应该是什么都没有)。

如果你遇到局部最大值的问题(这是文章声称抛硬币应该避免的),确保你的Initial, Transpose, ComputePl和Flip的实现是正确的。

你也可以试着让掷出的硬币有偏差(增加Flip() == 1的概率将使其更接近随机漫步,更不容易被卡住)。

这是你的代码的一个稍微紧凑的版本:

var current_f = Initial();    // current accepted function f
var current_Pl_f = ComputePl(current_f);  // current plausibility of accepted function f
for (int i = 0; i < 10000; i++)
{
    var candidate_f = Transpose(current_f); // create a candidate function
    var candidate_Pl_f = ComputePl(candidate_f);  // compute its plausibility
    if (candidate_Pl_f > current_Pl_f  ||  Flip() == 1)
    {
        // either candidate Pl has improved,
        // or it hasn't and we flipped the coin in candidate's favor
        //  - accept the candidate
        current_f = candidate_f;            
        current_Pl_f = candidate_Pl_f; 
    }
}

这个算法似乎与http://en.wikipedia.org/wiki/Simulated_annealing有关。如果是这样的话,通过改变你接受当前解决方案较差替代方案的概率,尤其是随着时间的推移降低这种概率,这种行为可能会有所帮助。

或者,您可以尝试从多个随机起点进行简单的爬坡-永远不要接受较差的选择,这意味着您将更频繁地陷入局部最大值,而是从不同的起点重复运行算法。

当你测试这个,你通常知道你的测试问题的正确答案。将正确答案的似是而非值与你的算法得出的似是而非值进行比较是个好主意,以防似是而非公式的弱点,在这种情况下,你的算法会得出比正确答案更似是而非的错误答案。