Статьи

Ориентированный на сообщения объектный дизайн и машинное обучение в JavaScript

В этой статье будет показано, как использовать объектно-ориентированное проектирование сообщений (мало чем отличающееся от Message-Oriented Programming, также известного как MOP или модель актора) для моделирования пользовательского интерфейса в качестве актера и обработки более сложной обработки при обновлении пользовательского интерфейса. В частности, пример кода реализует простое упражнение машинного обучения, в котором вы вводите любой символ на клавиатуре, и программа пытается угадать, что вы выбрали (без обмана;).
 
Во-первых, что такое Message-Oriented Object Design (далее именуемый MOOD)? Объектно-ориентированный дизайн сообщений — это философия объектно-ориентированного дизайна, в которой мы рассматриваем объекты как отправку неизменных сообщений / публикацию событий по каналам. Системы MOOD также полагаются на конфигурацию объектных сетей, чтобы обеспечить взаимодействие между ними. Основным принципом является отсутствие методов получения объектов между объектами (будь то вызовы метода или свойства). Так как я написал этот пример в Javascript, и он не имеет внутренней поддержки этой концепции, все идеалы MOOD должны будут реализовываться конвенцией. Объектно-ориентированный дизайн сообщений — это термин, который я придумал. Я не уверен, что он достаточно отличается от Программирования на основе сообщений или Программирования на основе сообщений, чтобы гарантировать существование, но я также неЯ не хочу запачкать эти термины моими собственными идеями, если есть важные тонкости, которые мне не хватает.
 
Я нахожусь в процессе написания очень углубленной статьи о Message-Oriented Object Design, так что если вы хотите узнать больше, просто дайте мне знать, и я свяжусь с вами, когда она будет доступна. Пока достаточно сказать, что слова «объект» и «субъект» взаимозаменяемы, как и слова «сообщение», «метод» или «событие».
 
Проблема, которую мы собираемся решить, заключается в следующем: учитывая текстовое поле, в котором пользователь может ввести любой символьный литерал, мы создадим систему, которая будет использовать эту информацию для прогнозирования следующей записи пользователя, а также обновит веб-страницу с помощью наша статистика о том, как мы делаем. Поскольку мы используем философию MOOD, между объектами не будет получателей (хотя их использование в приватных методах вполне приемлемо).
 
Для начала я написал следующий очень простой объект javascript для представления пользовательского интерфейса:

var user_actor = function(guess_dom_target, accuracy_dom_target)
{
  this._target_element = guess_dom_target;
  this._accuracy_target_element = accuracy_dom_target;
};
user_actor.prototype.send_guesses_to = function(channel)
{
  this._guess_channel = channel;
}
user_actor.prototype.value_entered = function(value)
{
  this._guess_channel.next(value);
};
user_actor.prototype.previously_guessed_value_updated = function(guess_value)
{
  this._target_element.html(guess_value);
};
user_actor.prototype.accuracy_updated = function(accuracy_value)
{
  this._accuracy_target_element.html(accuracy_value * 100);
};

Одна из ключевых идей, которая делает MOOD настолько мощным, заключается в том, что он рассматривает ваш пользовательский интерфейс как просто еще один объект MOOD (в основном как актер). Это означает, что все события пользовательского интерфейса, которые могут быть такими хлопотными, находятся здесь. Идея асинхронных действий будет встроена во все наши объекты, так что даже когда мы переключаем контексты для работы с частью системы машинного обучения, общий дизайн объекта будет выглядеть очень знакомым.
 
Здесь вы также можете увидеть концепцию канала в моих объектах. В MOOD (и Message-Oriented Programming) мы всегда предполагаем, что мы используем канал, который хорошо передает наше сообщение нужному объекту. Поэтому, в то время как мы в конечном итоге передадим ссылку на объект в качестве канала, это предположение заставляет нас рассматривать наш код, как будто это изолированный объект, не подозревающий, как его вызовы методов повлияют на другие. Это позволит нам обеспечить чрезвычайно чистое разделение интересов (SRP) и упростит нам проверку, когда мы нарушаем SRP. Как? Посмотрите на семантическое значение кода в объекте. Кажется ли что-либо неуместным для объекта, который управляет типом пользовательского интерфейса, которым мы являемся? Почему нет знаний о том, что эта программа будет делать? Подумайте об этом, как мы продолжим.
 
После написания этого кода я написал небольшой тестовый код, чтобы убедиться, что он выводит правильные значения в правильные места в HTML. Я оставлю написание этого кода в качестве упражнения для читателя, поскольку это довольно тривиально.
 
Затем я перебрал актера, который управлял учебной задачей. Хотя в последнем коде используется цепочка марков для изучения шаблонов пользователей, я начал ее постепенно, просто угадав «вчерашнюю погоду» (т. Е. Используйте текущий вход в качестве нашего прогноза следующего ввода). Это завершенная реализация:

var learning_actor = function()
{
  this._markov_chain = {};
  this._guessed_value = "";
  this._previous_value = "";
};
learning_actor.prototype.set_guess_channel_to = function(channel)
{
  this._guess_channel = channel;
};
learning_actor.prototype._make_best_guess = function(current_value)
{
  var value_to_guess = current_value;
  var score_to_beat = -1;
  
  var guess_list = this._markov_chain[current_value];
  for(var previously_guessed_value in guess_list)
  {
    var score = guess_list[previously_guessed_value];
    if(score > score_to_beat)
    {
      value_to_guess = previously_guessed_value;
      score_to_beat = score;
    }
  }
  
  return value_to_guess;
};
learning_actor.prototype._learn_from_new_information = function(previous_value, current_value)
{
  if(this._markov_chain[previous_value] == null)
  {
    this._markov_chain[previous_value] = {};
    this._markov_chain[previous_value][current_value] = 0;
  }
  
  if(isNaN(this._markov_chain[previous_value][current_value]))
    this._markov_chain[previous_value][current_value] = 0;
    
  this._markov_chain[previous_value][current_value] = this._markov_chain[previous_value][current_value] + 1;
};
learning_actor.prototype.next = function(value)
{
  this._guess_channel.guessed(this._guessed_value, value);

  this._learn_from_new_information(this._previous_value, value);
  this._guessed_value = this._make_best_guess(value);
  this._previous_value = value;
};

В качестве переподготовки, цепь Маркова, как я ее реализовал, говорит нам, какое значение, скорее всего, будет введено следующим, учитывая предыдущее значение. Я не буду вдаваться в детали реализации, но код довольно лаконичен и, надеюсь, достаточно разборчив, чтобы его можно было расшифровать.
 
Учебный актер имеет только пару основных частей.

  • Следующее (значение) сообщение, которому передается значение, введенное пользователем.
  • Метод _learn_from_new_information (previous_value, current_value), который обучает нашу цепочку markov.
  • Метод _make_best_guess (value), который использует нашу обученную цепочку markov, чтобы сделать обоснованное предположение о следующей записи пользователя.
  • И последнее, но не менее важное, простое сообщение set_guess_channel_to (channel), которое мы можем использовать, чтобы опубликовать то, что мы догадались, и что на самом деле было правильное предположение.

Изначально я написал код, который теперь есть у актера на табло, как часть актера, обучающегося. Вот этот код:

var scoreboard_actor = function()
{
  this._values_entered_count = 0;
  this._correctly_guessed_value_count = 0;
};
scoreboard_actor.prototype.set_display_channel_to = function(channel)
{
  this._display_channel = channel;
};
scoreboard_actor.prototype.guessed = function(my_guess, correct_guess)
{
  this._values_entered_count = this._values_entered_count + 1;
  
  if(my_guess == correct_guess)
  {
    this._correctly_guessed_value_count = this._correctly_guessed_value_count + 1;
  }
  
  this._display_channel.accuracy_updated(this._correctly_guessed_value_count / this._values_entered_count);
  this._display_channel.previously_guessed_value_updated(my_guess);
};

Вы можете видеть, что это довольно просто, и я не решался перевести его в новый класс. Когда вы начнете с этим стилем, вы будете чувствовать это довольно часто. Я рекомендую бороться с болью, пока вы не наткнетесь на первый «серьезный» рефакторинг, который вам нужно сделать. Легкость, с которой вы сможете сделать это изменение, я гарантирую, поразит вас, и вы будете зацеплены. Еще одна причина, по которой я не решался убрать это из моего обучающегося актера, заключается в том, что я предполагал, что буду дублировать концепцию «предыдущее значение должно равняться последнему». Поскольку MOOD не допускает получение, я знал, что единственный способ поделиться этой логикой — это повторное использование copy ‘n’ paste (читай: ewww). Посмотрите на алгоритм, оставшийся в процессе обучения актера. Неважно, правильно мы догадались или нет.Он только отслеживает догадки и выдвигает гипотезу о них. Итак, если проверка догадок не была проблемой алгоритма обучения, почему у меня это было с самого начала? Я просто хотел показать табло. Отсюда и создание моего табло актера.
 
У нас есть все эти объекты, но что с ними делать? Конфигурация наших объектов называется конфигурацией сети. По сути, это просто другой вид внедрения зависимости. Разница в том, что ваша конфигурация сможет быть отделена от остальной части вашего кода и изолирована, если вы того пожелаете. Вот конфигурация сети объекта для этого кода:

var computer_guess_display = $('#computer_guess');
var computer_guess_accuracy_display = $('#computer_guess_accuracy');

my_user = new user_actor(computer_guess_display, computer_guess_accuracy_display);
var my_learner = new learning_actor();
var my_scoreboard = new scoreboard_actor();

my_user.send_guesses_to(my_learner);
my_learner.set_guess_channel_to(my_scoreboard);
my_scoreboard.set_display_channel_to(my_user);

Первое, что должно вас заметить, это то, что мы не пытаемся сделать наши объекты неизменяемыми. В MOOD, как и в Actor Model, мы гарантируем, что актер будет использоваться только из контекста одного потока в течение всей его жизни. Это может показаться плохим ограничением, вот почему это не так: представьте, что обучающийся актер получает ОЧЕНЬ сложную логику. Это не натяжение в зависимости от того, насколько точными вы хотите быть догадки. Итак, если бы вы написали этот код без использования этого стиля и явно не создавали асинхронность, что может произойти? В первый раз, когда учебный актер должен действительно подумать, ваш пользовательский интерфейс зависнет. Однако, поскольку мы написали это с помощью Message Oriented Object Design, мы можем использовать эту логику _anywhere_, и она не будет блокировать наш пользовательский интерфейс.Что я имею в виду где-нибудь? Я имею в виду, что мы могли бы буквально разместить его в веб-сервисе, и вместо реализации нашего актера в HTML у нас мог бы быть актер, который отвечал бы за взаимодействие с веб-сервисом. Когда-нибудь, если Javascript получит потоки, мы можем даже добавить дополнительную работу в поток и создать объект канала для управления контекстом потоков в передаваемых сообщениях. Остальная часть нашего кода не изменится ни для одного случая. Если вам нужен реальный пример, оставьте мне комментарий на этот счет, потому что сейчас кажется, что это легко увидеть, особенно когда кто-то на это указал. В то же время, если вы думаете, что Message Oriented Object Design — это дополнительная работа для педантичных программистов-самоубийц, подумайте, может ли ваш код сделать это.
 
О да. Также обратите внимание, что во всем этом коде есть только один ОЧЕНЬ тонкий объект, который имеет какое-либо отношение к DOM. Остальное тривиально тестируемо. И не просто проверяемым, но проверяемым, так как будет выполняться только проверяемый объект. Я не использовал этот код. Именно так настроение тянет меня.
 
Кроме того, я заранее прошу прощения за мое ужасное наименование методов и объектов. Надеюсь, это все еще дает понять.
 
Это оно! Идите и попробуйте, это довольно аккуратно. Просто «случайное» нажатие клавиш на клавиатуре так, как я делал код, могло правильно угадать 40% времени или около того. Совсем неплохо! Кроме того, обычные шаблоны, такие как «abcabcabc», будут работать довольно быстро, и вы увидите, как код пытается следовать за вами, если вы делаете что-то вроде «aaaabababaaaababababab». Конечно, как и у всех обучающих агентов, чем более случайная строка вводится, тем хуже будет работать агент.
 
Полный исходный код HTML вы можете скачать и попробовать здесь. Это требует от вас включить JQuery для его работы. Оставьте комментарий, если у вас есть какие-либо вопросы! ?

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
  <title>LearningU.js</title>
  <script type="text/javascript" src="jquery.js"></script>
  <script type="text/javascript">
    var my_user = null;
    $(document).ready(function(){
    // Start user_actor
      var user_actor = function(guess_dom_target, accuracy_dom_target)
      {
        this._target_element = guess_dom_target;
        this._accuracy_target_element = accuracy_dom_target;
      };
      user_actor.prototype.send_guesses_to = function(channel)
      {
        this._guess_channel = channel;
      }
      user_actor.prototype.value_entered = function(value)
      {
        this._guess_channel.next(value);
      };
      user_actor.prototype.previously_guessed_value_updated = function(guess_value)
      {
        this._target_element.html(guess_value);
      };
      user_actor.prototype.accuracy_updated = function(accuracy_value)
      {
        this._accuracy_target_element.html(accuracy_value * 100);
      };
      
    // Start learning_actor
      var learning_actor = function()
      {
        this._markov_chain = {};
        this._guessed_value = "";
        this._previous_value = "";
      };
      learning_actor.prototype.set_guess_channel_to = function(channel)
      {
        this._guess_channel = channel;
      };
      learning_actor.prototype._make_best_guess = function(current_value)
      {
        var value_to_guess = current_value;
        var score_to_beat = -1;
        
        var guess_list = this._markov_chain[current_value];
        for(var previously_guessed_value in guess_list)
        {
          var score = guess_list[previously_guessed_value];
          if(score > score_to_beat)
          {
            value_to_guess = previously_guessed_value;
            score_to_beat = score;
          }
        }
        
        return value_to_guess;
      };
      learning_actor.prototype._learn_from_new_information = function(previous_value, current_value)
      {
        if(this._markov_chain[previous_value] == null)
        {
          this._markov_chain[previous_value] = {};
          this._markov_chain[previous_value][current_value] = 0;
        }
        
        var likely_values_based_on_prev_value = this._markov_chain[previous_value];
        
        if(isNaN(likely_values_based_on_prev_value[current_value]))
          likely_values_based_on_prev_value[current_value] = 0;
          
        likely_values_based_on_prev_value[current_value] = likely_values_based_on_prev_value[current_value] + 1;
      };
      learning_actor.prototype.next = function(value)
      {
        this._guess_channel.guessed(this._guessed_value, value);
      
        this._learn_from_new_information(this._previous_value, value);
        this._guessed_value = this._make_best_guess(value);
        this._previous_value = value;
      };
      
    // Start scoreboard_actor
      var scoreboard_actor = function()
      {
        this._values_entered_count = 0;
        this._correctly_guessed_value_count = 0;
      };
      scoreboard_actor.prototype.set_display_channel_to = function(channel)
      {
        this._display_channel = channel;
      };
      scoreboard_actor.prototype.guessed = function(my_guess, correct_guess)
      {
        this._values_entered_count = this._values_entered_count + 1;
        
        if(my_guess == correct_guess)
        {
          this._correctly_guessed_value_count = this._correctly_guessed_value_count + 1;
        }
        
        var guess_accuracy = this._correctly_guessed_value_count / this._values_entered_count;
        this._display_channel.accuracy_updated(guess_accuracy);
        this._display_channel.previously_guessed_value_updated(my_guess);
      };
      
      //Create actors
      // my_user needs to be global so UI can use it.
      var computer_guess_display = $('#computer_guess');
      var computer_guess_accuracy_display = $('#computer_guess_accuracy');
      
      my_user = new user_actor(computer_guess_display, computer_guess_accuracy_display);
      var my_learner = new learning_actor();
      var my_scoreboard = new scoreboard_actor();
      
      my_user.send_guesses_to(my_learner);
      my_learner.set_guess_channel_to(my_scoreboard);
      my_scoreboard.set_display_channel_to(my_user);
    });
  </script>
</head>
<body>
<div>
    You: <input name="human_value" value="" onclick="this.select();" onfocus="this.select();" maxlength="1" onkeyup="my_user.value_entered(this.value); this.select();" />
  </div>
  <div>
    My Guess: <span id="computer_guess"></span>
  </div>
  <div>
    My Accuracy: <span id="computer_guess_accuracy">0</span>%
  </div>
</body>
</html>