оригинал: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)
Низкое масло, не плохо.
Если у вас есть какие-либо вопросы, пожалуйста, прокомментируйте и обменяйтесь.