Класс дерева в MATLAB
Я внедряю структуру данных дерева в MATLAB. Добавление новых дочерних узлов в дерево, назначение и обновление значений данных, связанных с узлами, являются типичными операциями, которые я ожидаю выполнить. Каждый node имеет тот же тип data
, который связан с ним. Удаление узлов не требуется для меня. До сих пор я решил реализовать класс, наследующий класс handle
, чтобы иметь возможность передавать ссылки на узлы вокруг функций, которые будут изменять дерево.
Изменить: 2 декабря
Прежде всего, спасибо за все предложения в комментариях и ответах до сих пор. Они уже помогли мне улучшить мой древовидный класс.
Кто-то предложил попробовать digraph
, представленный в R2015b. Мне еще предстоит изучить это, но, поскольку он не работает как ссылочный параметр аналогично классу, наследующему от handle
, я немного скептически отношусь к тому, как он будет работать в моем приложении. На этом этапе еще не ясно, насколько легко будет работать с ним, используя пользовательский data
для узлов и ребер.
Изменить: (3 декабря) Дополнительная информация о главном приложении: MCTS
Вначале я предположил, что детали основного приложения будут иметь только предельный интерес, но, читая комментарии и ответ от @FirefoxMetzger, я понимаю, что это имеет важные последствия.
Я реализую алгоритм алгоритм поиска по дереву Монте-Карло. Дерево поиска исследуется и расширяется итеративно. Википедия предлагает приятный графический обзор процесса:
В моем приложении я выполняю большое количество поисковых итераций. На каждой итерации поиска я просматриваю текущее дерево, начиная с корня до листа node, затем разворачиваю дерево, добавляя новые узлы и повторяя. Поскольку метод основан на случайной выборке, в начале каждой итерации я не знаю, какой лист node я завершу на каждой итерации. Вместо этого это определяется совместно data
узлов, находящихся в настоящее время в дереве, и результатов случайных выборок. Независимо от того, какие узлы я посещаю во время одной итерации, обновляется их data
.
Пример. Я нахожусь в node n
, у которого есть несколько детей. Мне нужно получить доступ к данным у каждого из детей и нарисовать случайную выборку, которая определяет, какой из детей я перехожу к следующему в поиске. Это повторяется до тех пор, пока не будет достигнут лист node. Практически я делаю это, вызывая функцию search
в корне, которая решит, какой дочерний объект будет расширяться дальше, вызовите search
на этом node рекурсивно и т.д., Наконец, вернув значение, как только лист node будет достиг. Это значение используется при возврате из рекурсивных функций для обновления data
узлов, посещаемых во время итерации поиска.
Дерево может быть довольно неуравновешенным, так что некоторые ветки являются очень длинными цепочками узлов, а другие быстро заканчиваются после уровня корня и не расширяются дальше.
Текущая реализация
Ниже приведен пример моей текущей реализации с примером нескольких функций-членов для добавления узлов, запросов к глубине или количеству узлов в дереве и т.д.
classdef stree < handle
% A class for a tree object that acts like a reference
% parameter.
% The tree can be traversed in both directions by using the parent
% and children information.
% New nodes can be added to the tree. The object will automatically
% keep track of the number of nodes in the tree and increment the
% storage space as necessary.
properties (SetAccess = private)
% Hold the data at each node
Node = { [] };
% Index of the parent node. The root of the tree as a parent index
% equal to 0.
Parent = 0;
num_nodes = 0;
size_increment = 1;
maxSize = 1;
end
methods
function [obj, root_ID] = stree(data, init_siz)
% New object with only root content, with specified initial
% size
obj.Node = repmat({ data },init_siz,1);
obj.Parent = zeros(init_siz,1);
root_ID = 1;
obj.num_nodes = 1;
obj.size_increment = init_siz;
obj.maxSize = numel(obj.Parent);
end
function ID = addnode(obj, parent, data)
% Add child node to specified parent
if obj.num_nodes < obj.maxSize
% still have room for data
idx = obj.num_nodes + 1;
obj.Node{idx} = data;
obj.Parent(idx) = parent;
obj.num_nodes = idx;
else
% all preallocated elements are in use, reserve more memory
obj.Node = [
obj.Node
repmat({data},obj.size_increment,1)
];
obj.Parent = [
obj.Parent
parent
zeros(obj.size_increment-1,1)];
obj.num_nodes = obj.num_nodes + 1;
obj.maxSize = numel(obj.Parent);
end
ID = obj.num_nodes;
end
function content = get(obj, ID)
%% GET Return the contents of the given node IDs.
content = [obj.Node{ID}];
end
function obj = set(obj, ID, content)
%% SET Set the content of given node ID and return the modifed tree.
obj.Node{ID} = content;
end
function IDs = getchildren(obj, ID)
% GETCHILDREN Return the list of ID of the children of the given node ID.
% The list is returned as a line vector.
IDs = find( obj.Parent(1:obj.num_nodes) == ID );
IDs = IDs';
end
function n = nnodes(obj)
% NNODES Return the number of nodes in the tree.
% Equal to root + those whose parent is not root.
n = 1 + sum(obj.Parent(1:obj.num_nodes) ~= 0);
assert( obj.num_nodes == n);
end
function flag = isleaf(obj, ID)
% ISLEAF Return true if given ID matches a leaf node.
% A leaf node is a node that has no children.
flag = ~any( obj.Parent(1:obj.num_nodes) == ID );
end
function depth = depth(obj,ID)
% DEPTH return depth of tree under ID. If ID is not given, use
% root.
if nargin == 1
ID = 0;
end
if obj.isleaf(ID)
depth = 0;
else
children = obj.getchildren(ID);
NC = numel(children);
d = 0; % Depth from here on out
for k = 1:NC
d = max(d, obj.depth(children(k)));
end
depth = 1 + d;
end
end
end
end
Однако производительность порой медленная, причем операции над деревом занимают большую часть моего времени вычисления. Какие конкретные способы могли бы сделать реализацию более эффективной? Было бы возможно изменить реализацию на что-то еще, чем тип наследования handle
, если есть прирост производительности.
Результаты профилирования с текущей реализацией
Поскольку добавление новых узлов в дерево является наиболее типичной операцией (наряду с обновлением data
node), я сделал несколько профилирование об этом.
Я выполнил профайлер по следующему эталонному коду с Nd=6, Ns=10
.
function T = benchmark(Nd, Ns)
% Tree benchmark. Nd: tree depth, Ns: number of nodes per layer
% Initialize tree
T = stree(rand, 10000);
add_layers(1, Nd);
function add_layers(node_id, num_layers)
if num_layers == 0
return;
end
child_id = zeros(Ns,1);
for s = 1:Ns
% add child to current node
child_id(s) = T.addnode(node_id, rand);
% recursively increase depth under child_id(s)
add_layers(child_id(s), num_layers-1);
end
end
end
Производительность R2015b
Было обнаружено, что R2015b улучшает производительность функций MATLAB OOP. Я перечеркнул вышеупомянутый бенчмарк и действительно наблюдал улучшение производительности:
Итак, это уже хорошая новость, хотя, конечно, принимаются и другие улучшения;)
Резервирование памяти по-разному
В комментариях также было предложено использовать
obj.Node = [obj.Node; data; cell(obj.size_increment - 1,1)];
зарезервировать больше памяти, чем текущий подход, с помощью repmat
. Это немного улучшило производительность. Я должен отметить, что мой контрольный код предназначен для фиктивных данных, и поскольку фактический data
более сложный, это, скорее всего, поможет. Благодарю! Результаты профилировщика ниже:
Вопросы по еще большему повышению производительности
- Возможно, существует альтернативный способ сохранить память для дерева, которое более эффективно? К сожалению, я обычно не знаю заранее, сколько узлов будет в дереве.
- Добавление новых узлов и изменение
data
существующих узлов являются наиболее типичными операциями, которые я выполняю на дереве. На данный момент они фактически занимают большую часть времени обработки моего основного приложения. Любые улучшения в отношении этих функций будут приветствоваться.
Как последнее замечание, я бы идеально хотел, чтобы реализация была чистой MATLAB. Однако такие варианты, как MEX или использование некоторых встроенных функций Java, могут быть приемлемыми.