Некоторое время назад, поскольку проект должен использовать TensorFlow в течение определенного периода времени, я чувствую, что этот фреймворк очень интересен.Помимо построения сложных нейронных сетей, он также может оптимизировать другие вычислительные модели, которые мне нужны, поэтому я всегда хочу научитесь писать аналогичную структуру графовых вычислений самостоятельно. Несколько дней назад на собрании группы было решено внедрить простую версию фреймворка графовых вычислений, имитирующую интерфейс TensorFlow, чтобы научиться писать вычислительные графовые программы и реализовывать прямое и обратное распространение. В настоящее время реализованы оптимизаторы прямого и обратного распространения и градиентного спуска, а также написан пример оптимизации линейной модели.
Код размещен на GitHub под названиемSimpleFlow, ссылка на склад:GitHub.com/pgirllab/simp…
Хотя принципы прямого распространения и обратного распространения не очень сложны для понимания, только когда вы начинаете писать, вы обнаружите, что есть еще много деталей, которые необходимо изучить и обработать, чтобы оптимизировать реальную модель (например, Функция потерь для обработки вывода матрицы каждого вычислительного узла). Код SimpleFlow не учитывает слишком много таких вещей, какdtype
и тензорыsize
Поскольку он предназначен только для реализации основной функции расчета графа и не предусматривает какой-либо оптимизации, интерфейс Numpy используется для внутренней тензорной операции (в конце концов, это цель обучения и практики). Я давно не обновлял свой блог.В следующих нескольких статьях я подытожу детали процесса реализации, надеясь, что буду справочной информацией по детской обуви для изучения позже.
текст
Эта статья в основном знакомит с реализацией вычислительных графов и прямого распространения, в основном с участиемПостроение графаи, выполнивпост-порядковый обходЗатем выполняется вычисление прямого распространения для получения выходного значения на конкретном узле.
Давайте сначала вставим простой эффект реализации:
import simpleflow as sf
# Create a graph
with sf.Graph().as_default():
a = sf.constant(1.0, name='a')
b = sf.constant(2.0, name='b')
result = sf.add(a, b, name='result')
# Create a session to compute
with tf.Session() as sess:
print(sess.run(result))
Вычислительный граф
Вычислительный граф - это основной метод обработки в вычислительной алгебре.Мы можем представить заданное математическое выражение через ориентированный граф и можем быстро и легко вывести переменные в выражении в соответствии с характеристиками графа. Суть нейронной сети — многослойная составная функция, поэтому ее выражение также можно представить в виде графика.
Этот раздел в основном суммирует реализацию вычислительных графов.В направленном графе вычислительных графов каждый узел представляет определенную операцию, такую как суммирование, произведение, векторное произведение, квадрат и т. д., например выражения суммирования.Используйте направленный граф для представления:
выражениеИспользуйте направленный граф для представления:
В отличие от реализации TensorFlow, для простоты в SimpleFlow я не определялTensor
класс для представления потока данных между узлами в вычислительном графе, ноОпределите тип узла напрямую, который в основном определяет четыре типа для представления узлов в графе:
-
Operation
: Операционный узел в основном принимает один или два входных узла, а затем выполняет простые операции, такие как сложение и умножение на приведенном выше рисунке. -
Variable
: Узел без входного узла, данные, содержащиеся в этом узле, могут изменяться во время операции. -
Constant
: похожийVariable
узла, а входного узла нет, данные в этом узле не будут изменяться в процессе работы графа -
Placeholder
: Входной узел также отсутствует, данные этого узла передаются пользователем после построения графа
По сути, все узлы в графе можно рассматривать как какую-то операцию, среди которыхVariable
, Constant
, Placeholder
это спецоперация, как раз по отношению к обычнойOperation
, они не имеют ввода, но будут иметь вывод (например,, узлы, которые сами выводят свое значение наузел), обычно выводится вOperation
узла для дальнейших расчетов.
Ниже мы в основном расскажем, как реализовать основные компоненты вычислительного графа: узлы и ребра.
Operation
узел
Узлы представляют операции, ребра представляют данные, полученные и выведенные узлами, а узлы операций должны содержать следующие атрибуты:
-
input_nodes
: Входной узел, в котором хранится ссылка входного узла, подключенного к текущему узлу. -
output_nodes
: Выходной узел, в котором хранится узел, принимающий текущий узел в качестве входных данных, то есть пункт назначения текущего узла. -
output_value
: сохранить значение текущего узла, если оноAdd
узел, эта переменная хранит два входных узлаoutput_value
сумма -
name
: имя текущего узла -
graph
: граф, которому принадлежит этот узел
Ниже мы определяемOperation
Базовый класс используется для представления узлов операций на графе (подробнее см. https://github.com/PytLab/simpleflow/blob/master/simpleflow/operations.py):
class Operation(object):
''' Base class for all operations in simpleflow.
An operation is a node in computational graph receiving zero or more nodes
as input and produce zero or more nodes as output. Vertices could be an
operation, variable or placeholder.
'''
def __init__(self, *input_nodes, name=None):
''' Operation constructor.
:param input_nodes: Input nodes for the operation node.
:type input_nodes: Objects of `Operation`, `Variable` or `Placeholder`.
:param name: The operation name.
:type name: str.
'''
# Nodes received by this operation.
self.input_nodes = input_nodes
# Nodes that receive this operation node as input.
self.output_nodes = []
# Output value of this operation in session execution.
self.output_value = None
# Operation name.
self.name = name
# Graph the operation belongs to.
self.graph = DEFAULT_GRAPH
# Add this operation node to destination lists in its input nodes.
for node in input_nodes:
node.output_nodes.append(self)
# Add this operation to default graph.
self.graph.operations.append(self)
def compute_output(self):
''' Compute and return the output value of the operation.
'''
raise NotImplementedError
def compute_gradient(self, grad=None):
''' Compute and return the gradient of the operation wrt inputs.
'''
raise NotImplementedError
В дополнение к определению упомянутых выше свойств в методе инициализации требуются две операции:
- добавляет ссылку на текущий узел в свой входной узел
output_nodes
Таким образом, вы можете найти текущий узел во входном узле. - Добавьте ссылку на текущий узел в граф, чтобы упростить такие операции, как повторное использование ресурсов в графе позже.
Кроме того, каждый узел операции имеет два обязательных метода:comput_output
иcompute_gradient
Они соответственно отвечают за вычисление выходного значения текущего узла на основе значения входного узла и вычисление градиента на основе атрибута операции и значения текущего узла. Расчет градиента будет подробно описан в следующих статьях, в этой статье представлен только расчет выходного значения узла.
Ниже я используюсуммаОперация взята в качестве примера для иллюстрации реализации конкретного узла операции:
class Add(Operation):
''' An addition operation.
'''
def __init__(self, x, y, name=None):
''' Addition constructor.
:param x: The first input node.
:type x: Object of `Operation`, `Variable` or `Placeholder`.
:param y: The second input node.
:type y: Object of `Operation`, `Variable` or `Placeholder`.
:param name: The operation name.
:type name: str.
'''
super(self.__class__, self).__init__(x, y, name=name)
def compute_output(self):
''' Compute and return the value of addition operation.
'''
x, y = self.input_nodes
self.output_value = np.add(x.output_value, y.output_value)
return self.output_value
Видимый, вычислить текущий узелoutput_value
значениеПредварительные условияэтоЗначение его входного узла было рассчитано до этого.
Variable
узел
иOperation
Узел похож,Variable
узел тоже нуженoutput_value
, output_nodes
и т. д., но у него нет входных узлов и, следовательно, нетinput_nodes
свойства, но вам нужно определить начальное значение во время созданияinitial_value
:
class Variable(object):
''' Variable node in computational graph.
'''
def __init__(self, initial_value=None, name=None, trainable=True):
''' Variable constructor.
:param initial_value: The initial value of the variable.
:type initial_value: number or a ndarray.
:param name: Name of the variable.
:type name: str.
'''
# Variable initial value.
self.initial_value = initial_value
# Output value of this operation in session execution.
self.output_value = None
# Nodes that receive this variable node as input.
self.output_nodes = []
# Variable name.
self.name = name
# Graph the variable belongs to.
self.graph = DEFAULT_GRAPH
# Add to the currently active default graph.
self.graph.variables.append(self)
if trainable:
self.graph.trainable_variables.append(self)
def compute_output(self):
''' Compute and return the variable value.
'''
if self.output_value is None:
self.output_value = self.initial_value
return self.output_value
Constant
узел иPlaceholder
узел
Constant
иPlaceholder
узел сVariable
Узлы похожи, подробнее см.: https://github.com/PytLab/simpleflow/blob/master/simpleflow/operations.py.
Объект вычислительного графа
После определения узлов в графе нам нужно поместить определенные узлы в граф для унифицированного хранилища, поэтому нам нужно определитьGraph
Класс для хранения созданных узлов, что удобно для унифицированной работы ресурсов узлов в графе.
class Graph(object):
''' Graph containing all computing nodes.
'''
def __init__(self):
''' Graph constructor.
'''
self.operations, self.constants, self.placeholders = [], [], []
self.variables, self.trainable_variables = [], []
Чтобы предоставить график по умолчанию, создайте глобальную переменную для ссылки на график по умолчанию при импорте модуля simpleflow:
from .graph import Graph
# Create a default graph.
import builtins
DEFAULT_GRAPH = builtins.DEFAULT_GRAPH = Graph()
Чтобы имитировать интерфейс Tensorflow, мы даемGraph
Добавьте метод протокола диспетчера контекста, чтобы сделать его диспетчером контекста, а также добавьтеas_default
метод:
class Graph(object):
#...
def __enter__(self):
''' Reset default graph.
'''
global DEFAULT_GRAPH
self.old_graph = DEFAULT_GRAPH
DEFAULT_GRAPH = self
return self
def __exit__(self, exc_type, exc_value, exc_tb):
''' Recover default graph.
'''
global DEFAULT_GRAPH
DEFAULT_GRAPH = self.old_graph
def as_default(self):
''' Set this graph as global default graph.
'''
return self
так входяwith
Перед блоком кода сохраните старый объект графа по умолчанию, а затем назначьте текущий граф объекту глобального графа, чтобыwith
Узлы в блоках кода добавляются к текущему графу по умолчанию. последний выходwith
Когда кодовый блок используется, граф может быть восстановлен. Таким образом, мы можем создавать узлы в графе способом TensorFlow.
Хорошо, в соответствии с приведенной выше реализацией мы уже можем создать вычислительный граф:
import simpleflow as sf
with sf.Graph().as_default():
a = sf.constant([1.0, 2.0], name='a')
b = sf.constant(2.0, name='b')
c = a * b
Пропаривание вперед
После реализации вычислительного графа и узлов в графе нам нужно выполнить вычисление на вычислительном графе.В этом разделе резюмируется реализация прямого распространения вычислительного графа.
беседа
Во-первых, нам нужно реализоватьSession
Для расчета уже созданного расчетного графа, потому что, когда мы создали определенный ранее узел, мы просто создали пустой узел, и в узле нет значения, которое можно использовать для расчета, то естьoutput_value
пусто. Чтобы имитировать интерфейс TensorFlow, мы также определяем сеанс в качестве менеджера контекста здесь:
class Session(object):
''' A session to compute a particular graph.
'''
def __init__(self):
''' Session constructor.
'''
# Graph the session computes for.
self.graph = DEFAULT_GRAPH
def __enter__(self):
''' Context management protocal method called before `with-block`.
'''
return self
def __exit__(self, exc_type, exc_value, exc_tb):
''' Context management protocal method called after `with-block`.
'''
self.close()
def close(self):
''' Free all output values in nodes.
'''
all_nodes = (self.graph.constants + self.graph.variables +
self.graph.placeholders + self.graph.operations +
self.graph.trainable_variables)
for node in all_nodes:
node.output_value = None
def run(self, operation, feed_dict=None):
''' Compute the output of an operation.'''
# ...
Вычислить выходное значение узла
Выше мы можем построить расчетный граф.Каждый узел в расчетном графе связан со своими соседними узлами по направлению.Теперь нам нужно вычислить значение узла в соответствии с отношениями между узлами в графе. Так как же его рассчитать?Или использовать то, что мы только чтоНапример, расчетная схема
Если нам нужно посчитать оранжевыйЧтобы вычислить выходное значение узла, нам нужно вычислить выходное значение двух входных узлов, подключенных к нему, а затем нам нужно вычислить зеленыйВыходное значение входного узла. Мы можем получить выходные значения всех узлов, необходимых для вычисления узла, путем обхода по порядку. Чтобы облегчить реализацию, я напрямую использую рекурсивный метод для реализации обхода после заказа:
def _get_prerequisite(operation):
''' Perform a post-order traversal to get a list of nodes to be computed in order.
'''
postorder_nodes = []
# Collection nodes recursively.
def postorder_traverse(operation):
if isinstance(operation, Operation):
for input_node in operation.input_nodes:
postorder_traverse(input_node)
postorder_nodes.append(operation)
postorder_traverse(operation)
return postorder_nodes
С помощью этой функции мы можем получить список всех узлов, необходимых для вычисления значения узла, а затем по очереди вычислить выходное значение узлов в списке, и, наконец, мы можем легко вычислить выходное значение текущего узла.
class Session(object):
# ...
def run(self, operation, feed_dict=None):
''' Compute the output of an operation.
:param operation: A specific operation to be computed.
:type operation: object of `Operation`, `Variable` or `Placeholder`.
:param feed_dict: A mapping between placeholder and its actual value for the session.
:type feed_dict: dict.
'''
# Get all prerequisite nodes using postorder traversal.
postorder_nodes = _get_prerequisite(operation)
for node in postorder_nodes:
if type(node) is Placeholder:
node.output_value = feed_dict[node]
else: # Operation and variable
node.compute_output()
return operation.output_value
пример
Выше мы реализовали график расчета и прямое распространение, мы можем создать график расчета для вычисления значения выражения следующим образом:
import simpleflow as sf
# Create a graph
with sf.Graph().as_default():
w = sf.constant([[1, 2, 3], [3, 4, 5]], name='w')
x = sf.constant([[9, 8], [7, 6], [10, 11]], name='x')
b = sf.constant(1.0, 'b')
result = sf.matmul(w, x) + b
# Create a session to compute
with sf.Session() as sess:
print(sess.run(result))
выходное значение:
array([[ 54., 54.],
[ 106., 104.]])
Суммировать
В этой статье Python используется для реализации вычислительного графа и прямого распространения вычислительного графа, а также имитирует интерфейс TensorFlow для созданияSession
а такжеGraph
объект. В следующей части мы продолжим обобщать метод вычисления градиента узла вычислительного графа и реализацию оптимизатора обратного распространения и градиентного спуска.
Наконец, прикрепите ссылку на проект simpleflow, добро пожаловать, чтобы учиться и общаться друг с другом:GitHub.com/pgirllab/simp…
Ссылаться на
- Deep Learning From Scratch
- https://en.wikipedia.org/wiki/Tree_traversal#Post-order
- https://zhuanlan.zhihu.com/p/25496760
- http://blog.csdn.net/magic_anthony/article/details/77531552