оригинал:Deep Learning From Scratch I: Computational Graphs
Перевод: Сунь Имэн
Рецензент: Кайзер
Это первая глава в этой серии руководств. В этой главе вы познакомитесь с математическими и алгоритмическими основами глубоких нейронных сетей. Затем мы будем следовать TensorFlow API и самостоятельно реализовывать библиотеку нейронной сети на Python.
- Глава 1: Вычислительные графики
- Глава 2: Персептроны
- Глава 3: Стандарты обучения
- Глава 4: Градиентный спуск и обратное распространение
- Глава 5: Многослойные персептроны
- Глава 6: TensorFlow
Чтобы изучить эту главу, вам не нужно никакого машинного обучения или фундамента нейронной сети. Однако для исчисления на уровне бакалавриата, линейной алгебры, базовых алгоритмов и вероятности требуется прочная основа. Если вы столкнулись с трудностями в процессе обучения, пишите в комментариях.
К концу этой главы у вас будет четкое понимание математики, лежащей в основе нейронных сетей, и роли библиотек глубокого обучения, стоящих за ними.
Я буду делать код как можно более простым и понятным, его легче понять, чем эффективно запускать. Поскольку наш API смоделирован по образцу TensorFlow, вы, естественно, узнаете, как использовать API TensorFlow и операционный механизм, лежащий в основе TensorFlow, после того, как вы закончите эту главу (вместо того, чтобы тратить время на изучение универсального и наиболее эффективного API).
Вычислительные графики
Начнем с вычислительного графа (computational graph
), поскольку нейронные сети сами по себе являются особой формой вычислительных графов.
Вычислительный граф — это ориентированный граф, в котором вершины соответствуют операциям (Operation
) или переменная (Variable
).
Переменные могут передавать свои значения Операциям, а Операции могут передавать свои выходные данные другим Операциям. В этом случае каждый узел в графе вычислений определяет функцию переменной в графе (значение этого предложения может относиться к определению «функции», что означает, что один вход соответствует одному выходу).
Значения, передаваемые в узел и из узла, называются тензором, словом, используемым для многомерных массивов. Следовательно, он включает скаляры, векторы, матрицы, а также тензоры более высокого порядка.
Вычислительный граф в приведенном ниже примере добавляет два входа x и y и вычисляет сумму z.
В этом примере x и y являются входными узлами z, а z является потребителем x и y. z таким образом определяет функцию, а именно:
where
По мере усложнения вычислений концепция вычислительного графа становится все более важной. Например, следующий вычислительный граф определяет аффинное преобразование:
Операции
Каждая операция имеет три характеристики:
- Функция расчета: используется для расчета значения, которое должно быть выведено для данного входа.
- входной узел (
node
): их может быть несколько, это может быть переменная или другая операция. -
consumer
: Их может быть более одного, использующих выходные данные Операции в качестве входных данных.
Код реализации:
class Operation:
"""Represents a graph node that performs a computation.
An `Operation` is a node in a `Graph` that takes zero or
more objects as input, and produces zero or more objects
as output.
"""
def __init__(self, input_nodes=[]):
"""Construct Operation
"""
self.input_nodes = input_nodes
# Initialize list of consumers (i.e. nodes that receive this operation's output as input)
self.consumers = []
# Append this operation to the list of consumers of all input nodes
for input_node in input_nodes:
input_node.consumers.append(self)
# Append this operation to the list of operations in the currently active default graph
_default_graph.operations.append(self)
def compute(self):
"""Computes the output of this operation.
"" Must be implemented by the particular operation.
"""
pass
несколько простых операций
Чтобы познакомиться с классом операции (он понадобится вам в будущем), давайте реализуем несколько простых операций. В обеих операциях мы предполагаем, что все тензорыNumPy
Массивы, чтобы поэлементное сложение и умножение матриц (.dots) не нужно было реализовывать самим.
добавление
class add(Operation):
"""Returns x + y element-wise.
"""
def __init__(self, x, y):
"""Construct add
Args:
x: First summand node
y: Second summand node
"""
super().__init__([x, y])
def compute(self, x_value, y_value):
"""Compute the output of the add operation
Args:
x_value: First summand value
y_value: Second summand value
"""
return x_value + y_value
умножение матриц
class matmul(Operation):
"""Multiplies matrix a by matrix b, producing a * b.
"""
def __init__(self, a, b):
"""Construct matmul
Args:
a: First matrix
b: Second matrix
"""
super().__init__([a, b])
def compute(self, a_value, b_value):
"""Compute the output of the matmul operation
Args:
a_value: First matrix value
b_value: Second matrix value
"""
return a_value.dot(b_value)
Заполнители
В графе вычислений не все узлы являются операциями, как, например, в графе аффинных изменений,, иНи то, ни другое не является операцией. Вместо этого они являются входными данными для графика, и если мы хотим вычислить выходные данные графика, мы должны указать значение для каждого из них. Чтобы предоставить такое значение, мы вводим заполнитель.
class placeholder:
"""Represents a placeholder node that has to be provided with a value
when computing the output of a computational graph
"""
def __init__(self):
"""Construct placeholder
"""
self.consumers = []
# Append this placeholder to the list of placeholders in the currently active default graph
_default_graph.placeholders.append(self)
Переменные
В аффинно преобразованном графеииЕсть принципиальная разница. x — вход в операцию, а A и b — параметры операции, т. е. они присущи самому графу. Мы называем такие параметры, как A и b, переменными.
class Variable:
"""Represents a variable (i.e. an intrinsic, changeable parameter of a computational graph).
"""
def __init__(self, initial_value=None):
"""Construct Variable
Args:
initial_value: The initial value of this variable
"""
self.value = initial_value
self.consumers = []
# Append this variable to the list of variables in the currently active default graph
_default_graph.variables.append(self)
Класс графа
Наконец, нам нуженoperation
, placeholder
иvariable
Классы, которые включены вместе. При создании нового графика вы можете вызватьas_default
способ установить его_defaultgraph
.
Таким образом, мы можем создавать операции, заполнители и переменные, не передавая каждый раз ссылку на график.
class Graph:
"""Represents a computational graph
"""
def __init__(self):
"""Construct Graph"""
self.operations = []
self.placeholders = []
self.variables = []
def as_default(self):
global _default_graph
_default_graph = self
Пример
Теперь давайте создадим вычислительный граф аффинных преобразований, используя перечисленные выше классы:
# Create a new graph
Graph().as_default()
# Create variables
A = Variable([[1, 0], [0, -1]])
b = Variable([1, 1])
# Create placeholder
x = placeholder()
# Create hidden node y
y = matmul(A, x)
# Create output node z
z = add(y, b)
Выходные данные вычислительной операции
Теперь, когда мы научились создавать вычислительные графы, пришло время рассмотреть, как вычислить результат операции.
создать сеанс (Session
) для включения выполнения операции. Мы хотим иметь возможность вызывать метод запуска для экземпляра сеанса, передавая операцию для вычисления и словарь, содержащий все значения, необходимые заполнителю.
session = Session()
output = session.run(z, {
x: [1, 2]
})
Процесс расчета здесь следующий:
Чтобы вычислить функцию, представленную операцией, нам нужно выполнить вычисления в правильном порядке. Например, если промежуточный результат y не был рассчитан, мы не можем сначала вычислить z. Поэтому мы должны убедиться, что операции выполняются в правильном порядке.Только таким образом мы можем гарантировать, что значения входных узлов, требуемых операцией, были рассчитаны до ее вычисления. Это можно сделать с помощьюобход дерева после заказавыполнить.
import numpy as np
class Session:
"""Represents a particular execution of a computational graph.
"""
def run(self, operation, feed_dict={}):
"""Computes the output of an operation
Args:
operation: The operation whose output we'd like to compute.
feed_dict: A dictionary that maps placeholders to values for this session
"""
# Perform a post-order traversal of the graph to bring the nodes into the right order
nodes_postorder = traverse_postorder(operation)
# Iterate all nodes to determine their value
for node in nodes_postorder:
if type(node) == placeholder:
# Set the node value to the placeholder value from feed_dict
node.output = feed_dict[node]
elif type(node) == Variable:
# Set the node value to the variable's value attribute
node.output = node.value
else: # Operation
# Get the input values for this operation from node_values
node.inputs = [input_node.output for input_node in node.input_nodes]
# Compute the output of this operation
node.output = node.compute(*node.inputs)
# Convert lists to numpy arrays
if type(node.output) == list:
node.output = np.array(node.output)
# Return the requested node value
return operation.output
def traverse_postorder(operation):
"""Performs a post-order traversal, returning a list of nodes
in the order in which they have to be computed
Args:
operation: The operation to start traversal at
"""
nodes_postorder = []
def recurse(node):
if isinstance(node, Operation):
for input_node in node.input_nodes:
recurse(input_node)
nodes_postorder.append(node)
recurse(operation)
return nodes_postorder
Протестируйте класс, который мы написали в примере выше:
session = Session()
output = session.run(z, {
x: [1, 2]
})
print(output)
Низкое масло, не плохо.
Если у вас есть какие-либо вопросы, пожалуйста, прокомментируйте и обменяйтесь.