Реализуйте свой собственный TensorFlow (1) — вычислительный граф и прямое распространение

машинное обучение TensorFlow глубокое обучение Python
Реализуйте свой собственный TensorFlow (1) — вычислительный граф и прямое распространение

Некоторое время назад, поскольку проект должен использовать 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))

Вычислительный граф

Вычислительный граф - это основной метод обработки в вычислительной алгебре.Мы можем представить заданное математическое выражение через ориентированный граф и можем быстро и легко вывести переменные в выражении в соответствии с характеристиками графа. Суть нейронной сети — многослойная составная функция, поэтому ее выражение также можно представить в виде графика.

Этот раздел в основном суммирует реализацию вычислительных графов.В направленном графе вычислительных графов каждый узел представляет определенную операцию, такую ​​как суммирование, произведение, векторное произведение, квадрат и т. д., например выражения суммирования.f(x, y) = x + yИспользуйте направленный граф для представления:

выражениеf(x, y, z) = z(x+y)Используйте направленный граф для представления:

В отличие от реализации TensorFlow, для простоты в SimpleFlow я не определялTensorкласс для представления потока данных между узлами в вычислительном графе, ноОпределите тип узла напрямую, который в основном определяет четыре типа для представления узлов в графе:

  1. Operation: Операционный узел в основном принимает один или два входных узла, а затем выполняет простые операции, такие как сложение и умножение на приведенном выше рисунке.
  2. Variable: Узел без входного узла, данные, содержащиеся в этом узле, могут изменяться во время операции.
  3. Constant: похожийVariableузла, а входного узла нет, данные в этом узле не будут изменяться в процессе работы графа
  4. Placeholder: Входной узел также отсутствует, данные этого узла передаются пользователем после построения графа

По сути, все узлы в графе можно рассматривать как какую-то операцию, среди которыхVariable, Constant, Placeholderэто спецоперация, как раз по отношению к обычнойOperation, они не имеют ввода, но будут иметь вывод (например,x, yузлы, которые сами выводят свое значение на+узел), обычно выводится вOperationузла для дальнейших расчетов.

Ниже мы в основном расскажем, как реализовать основные компоненты вычислительного графа: узлы и ребра.

Operationузел

Узлы представляют операции, ребра представляют данные, полученные и выведенные узлами, а узлы операций должны содержать следующие атрибуты:

  1. input_nodes: Входной узел, в котором хранится ссылка входного узла, подключенного к текущему узлу.
  2. output_nodes: Выходной узел, в котором хранится узел, принимающий текущий узел в качестве входных данных, то есть пункт назначения текущего узла.
  3. output_value: сохранить значение текущего узла, если оноAddузел, эта переменная хранит два входных узлаoutput_valueсумма
  4. name: имя текущего узла
  5. 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

В дополнение к определению упомянутых выше свойств в методе инициализации требуются две операции:

  1. добавляет ссылку на текущий узел в свой входной узелoutput_nodesТаким образом, вы можете найти текущий узел во входном узле.
  2. Добавьте ссылку на текущий узел в граф, чтобы упростить такие операции, как повторное использование ресурсов в графе позже.

Кроме того, каждый узел операции имеет два обязательных метода: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.'''
        # ...

Вычислить выходное значение узла

Выше мы можем построить расчетный граф.Каждый узел в расчетном графе связан со своими соседними узлами по направлению.Теперь нам нужно вычислить значение узла в соответствии с отношениями между узлами в графе. Так как же его рассчитать?Или использовать то, что мы только чтоf(x, y, z) = z(x + y)Например, расчетная схема

Если нам нужно посчитать оранжевый\timesЧтобы вычислить выходное значение узла, нам нужно вычислить выходное значение двух входных узлов, подключенных к нему, а затем нам нужно вычислить зеленый+Выходное значение входного узла. Мы можем получить выходные значения всех узлов, необходимых для вычисления узла, путем обхода по порядку. Чтобы облегчить реализацию, я напрямую использую рекурсивный метод для реализации обхода после заказа:

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

пример

Выше мы реализовали график расчета и прямое распространение, мы можем создать график расчета для вычисления значения выражения следующим образом:

f = \left[ \begin{matrix} 1 & 2 & 3 \\ 3 & 4 & 5 \\ \end{matrix} \right] \times \left[ \begin{matrix} 9 & 8 \\ 7 & 6 \\ 10 & 11 \\ \end{matrix} \right] + 3 =  \left[ \begin{matrix} 54 & 54 \\ 106 & 104 \\ \end{matrix} \right]
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