Алгоритм вычисления правдоподобия функции / метод Монте-Карло


Я пишу программу, которая пытается повторить алгоритм, описанный в начале этой статьи,

Http://www-stat.stanford.edu/~cgates / PERSI / papers / MCMCRev. pdf

F-функция от char к char. Предположим, что Pl (f) является мерой "правдоподобия" этой функции. Алгоритм таков:

Начиная с предварительного предположения о функции, скажем f, а затем новая функция f* --

  • вычислить Pl (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 - вот что по существу стоит за методом Transpose (). Я использую словарь / хэш-таблицу типа >, которую используют функции-кандидаты для поиска любого заданного преобразования 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;
    }
3 7

3 ответа:

Ориентировочно, codereview.stackexchange.com может быть " лучшим форумом для этого".
Тем не менее, я сделаю быстрый удар по нему:

  • на первый взгляд, приведенный фрагмент являетсяправильной реализацией алгоритма.
  • будет ли алгоритм "застревать" в локальных минимумах-это вопрос, относящийся к алгоритму , а не к реализации . (см. обсуждение ниже)
  • ваш поиск "оптимального подхода " кажется быть направленным на изменения в алгоритме (отклонение от исходного алгоритма), а не на изменения в реализации (чтобы сделать ее быстрее и/или устранить некоторые возможные недостатки). Для рассмотрения алгоритма см. обсуждение ниже; для обсуждения реализации рассмотрим следующее:
    • убедитесь, что метод Flip() справедлив
    • убедитесь, что ComputePl() корректен: некоторые ошибки часто возникают из-за проблем с арифметической точностью в функция ценности.
    • обеспечить справедливость (равновероятность) с помощью метода Transpose ().
    • повышение производительности, скорее всего, будет происходить за счет оптимизации в методе ComputePl () (не показан), а не в основном цикле.

Обсуждение алгоритма per-se и его применимости к различным задачам.
В двух словах алгоритм представляет собой управляемый стохастический поиск, при котором [огромное] пространство решений отбирается двумя случайные устройства: метод транспонирования (), который изменяет (очень незначительно каждый раз) текущую функцию-кандидата и метод флипа (), который решает, должно ли [локально] субоптимальное решение выжить. Поиск осуществляется с помощью целевой функции ComputePl (), которая сама основана на матрице перехода первого порядка в некотором справочном корпусе.

В этом контексте локальных минимумов "смоляных ям" можно избежать, увеличив вероятность выбора "неоптимальных" функций: вместо fair 50-50 Flip (), возможно, попытается с вероятностью сохранения "неоптимальных" решений 66% или даже 75%. Этот подход обычно увеличивает число поколений, необходимых для достижения оптимального решения, но, как уже было сказано, может избежать застревания в локальных минимумах.

Другим способом обеспечения применимости алгоритма является обеспечение лучшей оценки правдоподобности заданных функций. Вероятные объяснения относительного успеха и общности алгоритма являются
    Распределение переходов первого порядка в английском языке не очень однородно (и поэтому довольно хорошо моделирует правдоподобие данной функции, вознаграждая ее за совпадения и наказывая за несовпадения). Этот вид многомерной статистики более устойчив к отклонению от эталонного корпуса, чем, скажем, распределение символов "порядка 0" в данном языке/корпусе.
  • распределение переходов первого порядка при специфичные для языка, как правило, схожи между различными языками и/или в контексте жаргонных словарей [основанных на указанных языках]. В случае с жаргоном коротких рук, когда такие буквы, как гласные, обычно опущены, все действительно ломается.
Поэтому для улучшения применимости алгоритма к данной задаче убедитесь, что используемая матрица распределения максимально точно соответствует языку и предметной области базового текста.

Из описания в статье ваша реализация кажется правильной (часть, которую вы отмечаете как "что здесь делать", действительно не должна быть ничем).

Если у вас возникли проблемы с локальными максимумами (то, чего, как утверждается в статье, бросок монеты должен избегать), убедитесь, что ваши имплементации Initial, Transpose, ComputePl и 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 если это так, то поведению может помочь изменение вероятности, с которой вы принимаете более плохие альтернативы текущему решению, особенно если вы уменьшаете эту вероятность с течением времени.

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

Когда вы проверяете это, вы обычно знаете правильный ответ для вашей тестовой задачи. Это хорошая идея, чтобы сравнить значение правдоподобия при правильном ответе с теми, которые ваш алгоритм придумывает, просто в случае, если слабость находится в Формуле правдоподобия, в этом случае ваш алгоритм придумает неправильные ответы, которые кажутся более правдоподобными, чем правильный.