Статьи

Нейронная сеть с прямой связью с HyperGraphDB

Одним из очевидных применений графовых баз данных, таких как HyperGraphDB, является реализация искусственных нейронных сетей (NN). В этом посте я покажу одну такую ​​реализацию на основе практической и информативной книги Тоби Сегарана «Программирование коллективного интеллекта»., В главе 4 автор показывает, как можно использовать обратную связь с пользователем для улучшения ранжирования результатов поиска. Идея состоит в том, чтобы обучить нейронную сеть ранжированию возможных URL-адресов с учетом комбинации ключевых слов. Предполагается, что пользователи выполнят поиск, затем изучат список результатов (с их названиями, резюме, типом документа и т. Д.) И, наконец, выберу тот, который, скорее всего, будет содержать информацию, которую он ищет. Это действие щелчка обеспечивает обратную связь, необходимую для обучения нейронной сети. С обучением сеть все чаще будет ранжировать лучшие документы по комбинации ключевых слов. И не только это, но это даст довольно хорошие предположения для поисков, которые он никогда не видел прежде.


Если вы не знакомы с нейронными сетями и не владеете вышеупомянутой книгой,
основная статья в Википедии на эту тему всегда является хорошим началом. Также я могу порекомендовать
эту вводную книгу, где пара глав находится в свободном доступе. Для тех, кто слишком занят / ленив для подробного ознакомления с NN, ниже приводится краткое описание конкретной нейронной сети, которую мы будем внедрять: NN с 3 уровнями прямой связи. Он состоит из 3 слоев нейронов, связанных с синапсами. Нейроны и синапсы являются абстрактными моделями их мозговых аналогов. Каждый синапс соединяет два нейрона, его вход и выход, и имеет соответствующее действительное число, называемое его
силой . Есть входной слой, скрытый слой и выходной слой, как показано на
этом рисунке, Сеть имеет прямую связь, потому что нейроны входного слоя «питают» нейроны скрытого слоя, которые, в свою очередь, «питают» нейроны выходного слоя, но обратной петли нет. NN выполняется пошагово: сначала всем входным нейронам присваивается реальное число, которое приходит откуда-то из «внешнего мира»: это их уровень активации. Во-вторых, уровень активации всех скрытых нейронов рассчитывается на основе подключенных к ним входных нейронов и силы соединений (синапсов). Наконец, то же самое вычисление применяется для выходных нейронов, получая набор действительных чисел, который является выходом NN. Чтобы дать правильные результаты, сеть обучается по алгоритму, известному как
обратное распространение Запустите сеть для получения выходных данных, сравните с ожидаемыми выходными данными и отрегулируйте силу синапсов, чтобы уменьшить дельту между ними.

В «Программировании коллективного разума» входные нейроны соответствуют отдельным ключевым словам поиска, и их уровень активации установлен на 1, в то время как выходные нейроны соответствуют URL-адресам, а их конечный уровень активации обеспечивает их ранжирование.
Скрытые нейроны соответствуют комбинации ключевых слов (т.е. фактически выполненных поисков). В нашей реализации входы и выходы будут произвольными атомами HyperGraphDB, которые являются частью любого представления любого домена, с которым он работает.

Как и в большинстве программ, реализации нейронных сетей обычно имеют два представления — представление оперативной памяти и представление постоянного хранилища (например, в базе данных SQL).
Но один из основных принципов HyperGraphDB — устранить это различие и встроить базу данных в процесс как прямое расширение памяти RAM. Таким образом, не нужно беспокоиться о кэшировании часто используемых данных, что находится в оперативной памяти, а что нет, по крайней мере, не так много. Например, хотя обычно набор соединений между двумя последовательными слоями представляется в виде взвешенной матрицы, теперь мы увидим, как прямое представление HyperGraphDB можно использовать без особых затрат!

Когда вы думаете о представлении нейронной сети в виде графа, наиболее интуитивно понятным способом является создание узла для каждого нейрона и ссылки для каждого синаптического соединения:

public class Neuron { ... }
public class Synapse extends HGPlainLink
{
 private double strength;
 public Synapse(HGHandle...args) { super(args); }
 public HGHandle getInput() { return getTargetAt(0); }
 public HGHandle getOutput() { return getTargetAt(1); }
 public getStrength() { return strength; }
 public setStrength(double strength) { this.strength = strength; }
}

Чтобы получить все входы нейрона:

List<synapse> inputs = hg.getAll(graph, hg.and(hg.type(Synapse.class),hg.orderedLink(hg.anyHandle(), neuronHandle)));
</synapse>

Это будет работать просто отлично. Когда приходит время активировать нейрон, мы перебираем вышеприведенный список и применяем любую формулу, которую выбрали для нашей модели. Но есть и лучший способ: каждый нейрон может быть просто связующим звеном между своими входными нейронами, и он может содержать сильные стороны всех синапсов в двойном []:

public class Neuron extends HGPlainLink
{
  private double [] weights;
 
  public Neuron(HGHandle...inputNeurons)
  {
      super(inputNeurons);
  }
 
  public double fire(Map input,...) { ... }
 
  // etc...
}

Таким образом, входные данные легко доступны, и активация нейрона может быть легко рассчитана. Обратите внимание, как универсальность модели гиперграфа позволяет нам представлять ту же структуру графа, но гораздо более компактно! Если бы мы хранили синапсы как явные ссылки, мы бы получили взрыв дополнительных сущностей в базе данных и менее неэффективный способ выполнения сети, где, вероятно, было бы оправдано отдельное представление во время выполнения. Здесь мы используем способность ребра удерживать произвольное число атомов, включая другие ребра: искусственные нейроны в некотором смысле связаны рекурсивно.


С помощью этой схемы может быть представлена ​​очень большая сеть, только части которой загружаются в память по мере необходимости.
Поисковое приложение для корпоративного сайта с, скажем, несколькими сотнями тысяч URL-адресов и несколькими тысячами повторяющихся поисков даст NN с небольшим коэффициентом из такого количества атомов HyperGraphDB.

Теперь входной слой в нашем представлении на самом деле состоит не из экземпляров нейронов, а из произвольных атомов.
Для поискового приложения это будут просто поисковые термины. Однако выходной слой состоит из нейронов, и нам нужен механизм, который связывает выходной нейрон с выходным атомом. Один из вариантов — создать подкласс Neuron, который содержит ссылку на связанный атом. Другой вариант — создать новую ссылку для ассоциации:

public class NeuronOutput extends HGPlainLink
{
  public NeuronOutput(HGHandle...args)
  {
      super(args);
  }
  public HGHandle getNeuron()
  {
      return getTargetAt(0);
  }
  public HGHandle getOutputAtom()
  {
      return getTargetAt(1);
  }
}

При обоих подходах потребуется некоторая бухгалтерия для удаления выходного нейрона, если удален сам атом вывода. Представление связи NeuronOutput приводит к небольшому падению производительности из-за дополнительного скачка, чтобы получить от выходного нейрона до выходного атома, но это должно быть незначительным для наших целей.


Имея это в виду, мы создаем класс NeuralNet для хранения во время выполнения информации об активной части всей сети, хранящейся в базе данных. Этот класс должен быть легковесным, создаваться по требованию и использоваться в одном потоке (например, для обработки поискового запроса). Он реализует алгоритм прямой связи, который должен быть быстрым, и алгоритм обучения, который может быть медленнее, потому что он будет использоваться только для обучения во время простоя. Например, поисковая система может обрабатывать накопленные отзывы ночью, обучая нейронную сеть на основе ежедневной активности. Чтобы представить активацию нейронов на данном уровне, у нас есть вспомогательный класс ActivationMap, который является HashMap, который возвращает значение по умолчанию (обычно 0) для отсутствующих ключей.Активация нейрона включает в себя вычисление, при котором все входы, умноженные на силу синапса, суммируются, а затем к сумме применяется функция активации. Функция активации может быть пороговой функцией или сигмовидной функцией, такой как
тан, который мы используем здесь. Сокращенная версия класса NeuralNet выглядит следующим образом:

public class NeuralNet
{
  private HyperGraph graph;
  private ActivationFunction activationFunction = new TanhFun();
  Map inputLayer = null;
  Map hiddenLayer = null;
  Map outputLayer = null;
 
  private Map activateNextLayer(Map previousLayer, Map nextLayer)
  {
      for (Map.Entry in : previousLayer.entrySet())
      {
          Collection downstream = hg.getAll(graph,
                                            hg.and(hg.type(Neuron.class),
                                                   hg.incident(in.getKey())));
          for (Neuron n : downstream)
              if (!nextLayer.containsKey(n))
                  nextLayer.put(graph.getHandle(n),
                                n.fire(previousLayer, activationFunction));
      }
      return nextLayer;
  }
 
  public NeuralNet(HyperGraph graph)
  {
      this.graph = graph;
  }
 
  public Map getOutputLayer()
  {
      return outputLayer;
  }
 
  public void feedforward(ActivationMap inputs)
  {
      inputLayer = inputs;
      hiddenLayer = activateNextLayer(inputLayer, new ActivationMap(0.0));
      outputLayer = activateNextLayer(hiddenLayer, new ActivationMap(0.0));
  }
 
  public void train(Collection inputs, Collection outputs,
                    HGHandle selectedOutput)
  {
      Collection outputNeurons = updateNeuralStructure(inputs, outputs);
      ActivationMap inputMap = new ActivationMap(0.0);
      for (HGHandle in : inputs)
          inputMap.put(in, 1.0);
      selectedOutput = hg.findOne(graph,
                                  hg.apply(hg.targetAt(graph, 0),
                                           hg.and(hg.type(NeuronOutput.class),
                                                  hg.incident(selectedOutput))));
      Map outputMap = new HashMap();
      for (HGHandle h : outputNeurons)
          outputMap.put(h, 0.0);
      outputMap.put(selectedOutput, 1.0);
      feedforward(inputMap);
      backpropagate(outputMap);
  }
}

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

Метод
train использует набор входных атомов, набор выходных атомов и один выходной атом, который был выбран среди всех выходов. Он начинается с того, что все выходные атомы имеют соответствующий нейрон, который подключен к соответствующему скрытому нейрону для этой конкретной входной комбинации. Все это делается в вызове
updateNeuralStructure . Затем метод создает ActivationMap, представляющий ожидаемую активацию выходного уровня: 0 для всех выходов, кроме выбранного, который имеет активацию 1. Затем на входе выполняется сеть, чтобы произвести свой собственный результат для выходного слоя (сохраненный в переменная члена outputLayer). Наконец,
обратное распространениерегулирует синаптические силы на основе различий между ожидаемым и рассчитанным выходом. Здесь можно тривиально добавить версию
поезда, которая использует полную карту ожидаемого выходного значения.


Объяснение того, как работает обратное распространение, и математика, стоящая за ним, выходит за рамки этого поста.
Итак, мы оставим это на этом. Полный код доступен в SVN-репозитории HyperGraphDB:

Этот пост предназначен в основном как пример использования HyperGraphDB.
Тем не менее, код должен быть применим в реальном приложении. Получите его из SVN, попробуйте и напишите, если у вас есть вопросы, ошибки или предложения по улучшению.

Ура,

Борис