Добро пожаловать на Курс по самоуправляемым автомобилям, часть 8. Этот курс в блоге познакомит нас с миром беспилотных автомобилей, расскажет, как работают беспилотные автомобили, плюсы и минусы вождения автомобилей, что такое беспилотные автомобили компании

Что мы узнали???

Мы рассмотрели большинство необходимых разделов. Если вы еще не читали ее, вы можете посетить часть 1, часть 2, часть 3 и часть 4.

Что будет

В этом разделе вы найдете информацию о:

  • Минипоток

В этой лабораторной работе вы создадите библиотеку под названием Miniflow, которая будет вашей собственной версией TensorFlow!

TensorFlow — одна из самых популярных библиотек нейронных сетей с открытым исходным кодом, созданная командой Google Brain всего за последние несколько лет.

Следуя этой лабораторной работе, вы проведете оставшуюся часть этого модуля, фактически работая с библиотеками глубокого обучения с открытым исходным кодом, такими как TensorFlow и Keras. Так зачем создавать MiniFlow? Хороший вопрос, рад, что вы его спросили!

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

Обратное распространение — это процесс, с помощью которого нейронные сети обновляют веса сети с течением времени.

Дифференцируемые графы — это графы, узлы которых являются дифференцируемыми функциями. Они также полезны в качестве наглядных пособий для понимания и расчета сложных производных. Это фундаментальная абстракция TensorFlow — это фреймворк для создания дифференцируемых графов.

С помощью графиков и обратного распространения вы сможете создавать свои собственные узлы и правильно вычислять производные. Что еще более важно, вы сможете думать и рассуждать с точки зрения этих графиков.

Теперь давайте первым делом заглянем под капот…

Мини-поток

Обратите внимание, что выходной слой выполняет математическую функцию сложения со своими входными данными. Нет скрытого слоя.

Графики

Узлы и ребра создают структуру графа. Хотя приведенный выше пример довольно прост, нетрудно представить, что все более сложные графы могут вычислять . . . Что ж . . . почти все.

Обычно создание нейронных сетей состоит из двух шагов:

  1. Задайте граф узлов и ребер.
  2. Распространение значений по графику.

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

Давайте рассмотрим, как реализовать эту структуру графа в Miniflow. Мы будем использовать класс Python для представления универсального узла.

Мы знаем, что каждый узел может получать данные от нескольких других узлов. Мы также знаем, что каждый узел создает один вывод, который, вероятно, будет передан другим узлам. Давайте добавим два списка: один для хранения ссылок на входящие узлы, а другой — для хранения ссылок на исходящие узлы.

class Node(object):
    def __init__(self, inbound_nodes=[]):
        # Node(s) from which this Node receives values
        self.inbound_nodes = inbound_nodes
        # Node(s) to which this Node passes values
        self.outbound_nodes = []
        # For each inbound_node, add the current Node as an outbound_node.
        for n in self.inbound_nodes:
            n.outbound_nodes.append(self)

Каждый узел в конечном итоге вычислит значение, которое представляет его вывод. Давайте инициализируем value значением None, чтобы указать, что он существует, но еще не установлен.

class Node(object):
    def __init__(self, inbound_nodes=[]):
        # Node(s) from which this Node receives values
        self.inbound_nodes = inbound_nodes
        # Node(s) to which this Node passes values
        self.outbound_nodes = []
        # For each inbound_node, add the current Node as an outbound_node.
        for n in self.inbound_nodes:
            n.outbound_nodes.append(self)
        # A calculated value
        self.value = None

Каждый узел должен иметь возможность передавать значения вперед и выполнять обратное распространение (подробнее об этом позже). А пока давайте добавим метод-заполнитель для прямого распространения. Позже мы займемся обратным распространением.

class Node(object):
    def __init__(self, inbound_nodes=[]):
        # Node(s) from which this Node receives values
        self.inbound_nodes = inbound_nodes
        # Node(s) to which this Node passes values
        self.outbound_nodes = []
        # For each inbound_node, add the current Node as an outbound_node.
        for n in self.inbound_nodes:
            n.outbound_nodes.append(self)
        # A calculated value
        self.value = None
    def forward(self):
        """
        Forward propagation.
        Compute the output value based on `inbound_nodes` and
        store the result in self.value.
        """
        raise NotImplemented

Узлы, выполняющие вычисления

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

class Input(Node):
    def __init__(self):
        # An Input node has no inbound nodes,
        # so no need to pass anything to the Node instantiator.
        Node.__init__(self)
    # NOTE: Input node is the only node where the value
    # may be passed as an argument to forward().
    #
    # All other node implementations should get the value
    # of the previous node from self.inbound_nodes
    #
    # Example:
    # val0 = self.inbound_nodes[0].value
    def forward(self, value=None):
        # Overwrite the value if one is passed in.
        if value is not None:
            self.value = value

В отличие от других подклассов Node, подкласс Input фактически ничего не вычисляет. Подкласс Input просто содержит value, например функцию данных или параметр модели (вес/смещение).

Вы можете установить value либо явно, либо с помощью метода forward(). Затем это значение передается через остальную часть нейронной сети.

Добавить подкласс

Add, который является еще одним подклассом Node, фактически может выполнять вычисление (сложение).

class Add(Node):
    def __init__(self, x, y):
        Node.__init__(self, [x, y])
    def forward(self):
	x_value = self.inbound_nodes[0].value
        y_value = self.inbound_nodes[1].value
        self.value = x_value + y_value

Обратите внимание на разницу в методе __init__, Add.__init__(self, [x, y]). В отличие от класса Input, у которого нет входящих узлов, класс Add принимает 2 входящих узла, x и y, и добавляет значения этих узлов.

MiniFlow имеет два метода, которые помогут вам определить, а затем запустить значения через ваши графики: topological_sort() и forward_pass().

Чтобы определить свою сеть, вам необходимо определить порядок операций для ваших узлов. Учитывая, что входные данные для одного узла зависят от выходных данных других, вам необходимо сгладить график таким образом, чтобы все входные зависимости для каждого узла были разрешены, прежде чем пытаться запустить его вычисление. Это техника, называемая топологической сортировкой.

Функция topological_sort() реализует топологическую сортировку с использованием алгоритма Кана. Детали этого метода не важны, важен результат; topological_sort() возвращает отсортированный список узлов, в котором все вычисления могут выполняться последовательно. topological_sort() принимает feed_dict, именно так мы первоначально установили значение для узла Input.

def topological_sort(feed_dict):
    """
    Sort generic nodes in topological order using Kahn's Algorithm.
    `feed_dict`: A dictionary where the key is a `Input` node and the value is the respective value feed to that node.
    Returns a list of sorted nodes.
    """
    input_nodes = [n for n in feed_dict.keys()]
    G = {}
    nodes = [n for n in input_nodes]
    while len(nodes) > 0:
        n = nodes.pop(0)
        if n not in G:
            G[n] = {'in': set(), 'out': set()}
        for m in n.outbound_nodes:
            if m not in G:
                G[m] = {'in': set(), 'out': set()}
            G[n]['out'].add(m)
            G[m]['in'].add(n)
            nodes.append(m)
    L = []
    S = set(input_nodes)
    while len(S) > 0:
        n = S.pop()
        if isinstance(n, Input):
            n.value = feed_dict[n]
        L.append(n)
        for m in n.outbound_nodes:
            G[n]['out'].remove(m)
            G[m]['in'].remove(n)
            # if no other incoming edges add to S
            if len(G[m]['in']) == 0:
                S.add(m)
    return L

Другой доступный вам метод — forward_pass(), который фактически запускает сеть и выводит значение.

def forward_pass(output_node, sorted_nodes):
    """
    Performs a forward pass through a list of sorted nodes.
    Arguments:
        `output_node`: The output node of the graph (no outgoing edges).
        `sorted_nodes`: a topologically sorted list of nodes.
    Returns the output node's value
    """
    for n in sorted_nodes:
        n.forward()
    return output_node.value

Наш miniflow.py готов, как показано ниже.

class Node(object):
    def __init__(self, inbound_nodes=[]):
        # Nodes from which this Node receives values
        self.inbound_nodes = inbound_nodes
        # Nodes to which this Node passes values
        self.outbound_nodes = []
        # A calculated value
        self.value = None
        # Add this node as an outbound node on its inputs.
        for n in self.inbound_nodes:
            n.outbound_nodes.append(self)
    # These will be implemented in a subclass.
    def forward(self):
        """
        Forward propagation.
        Compute the output value based on `inbound_nodes` and
        store the result in self.value.
        """
        raise NotImplemented

class Input(Node):
    def __init__(self):
        # an Input node has no inbound nodes,
        # so no need to pass anything to the Node instantiator
        Node.__init__(self)
    # NOTE: Input node is the only node that may
    # receive its value as an argument to forward().
    #
    # All other node implementations should calculate their
    # values from the value of previous nodes, using
    # self.inbound_nodes
    #
    # Example:
    # val0 = self.inbound_nodes[0].value
    def forward(self, value=None):
        if value is not None:
            self.value = value

class Add(Node):
    def __init__(self, x, y):
        # You could access `x` and `y` in forward with
        # self.inbound_nodes[0] (`x`) and self.inbound_nodes[1] (`y`)
        Node.__init__(self, [x, y])
    def forward(self):
        """
        Set the value of this node (`self.value`) to the sum of its inbound_nodes.
        x_value = self.inbound_nodes[0].value
        y_value = self.inbound_nodes[1].value
        self.value = x_value + y_value        """
def topological_sort(feed_dict):
    """
    Sort generic nodes in topological order using Kahn's Algorithm.
    `feed_dict`: A dictionary where the key is a `Input` node and the value is the respective value feed to that node.
    Returns a list of sorted nodes.
    """
    input_nodes = [n for n in feed_dict.keys()]
    G = {}
    nodes = [n for n in input_nodes]
    while len(nodes) > 0:
        n = nodes.pop(0)
        if n not in G:
            G[n] = {'in': set(), 'out': set()}
        for m in n.outbound_nodes:
            if m not in G:
                G[m] = {'in': set(), 'out': set()}
            G[n]['out'].add(m)
            G[m]['in'].add(n)
            nodes.append(m)
    L = []
    S = set(input_nodes)
    while len(S) > 0:
        n = S.pop()
        if isinstance(n, Input):
            n.value = feed_dict[n]
        L.append(n)
        for m in n.outbound_nodes:
            G[n]['out'].remove(m)
            G[m]['in'].remove(n)
            # if no other incoming edges add to S
            if len(G[m]['in']) == 0:
                S.add(m)
    return L

def forward_pass(output_node, sorted_nodes):
    """
    Performs a forward pass through a list of sorted nodes.
    Arguments:
        `output_node`: A node in the graph, should be the output node (have no outgoing edges).
        `sorted_nodes`: A topologically sorted list of nodes.
    Returns the output Node's value
    """
    val = 0
    for n in sorted_nodes:
       # print(n.value)
        if n.value  is not None:
            val += n.value
        n.forward()
    output_node.value = val
    return output_node.value

Мы попробуем реализовать приведенный ниже график на python, импортировав нашу недавно созданную библиотеку мини-потока.

from miniflow import *
x, y = Input(), Input()
f = Add(x, y)
feed_dict = {x: 10, y: 5}
sorted_nodes = topological_sort(feed_dict)
output = forward_pass(f, sorted_nodes)
# NOTE: because topological_sort sets the values for the `Input` nodes we could also access
# the value for x with x.value (same goes for y).
print("{} + {} = {} (according to miniflow)".format(feed_dict[x], feed_dict[y], output))

Обучение и потеря

Подобно MiniFlow в его текущем состоянии, нейронные сети принимают входные данные и производят выходные данные. Но в отличие от MiniFlow в его нынешнем состоянии, нейронные сети могут улучшать точность своих выходных данных с течением времени (трудно представить, что со временем можно улучшить точность Add!). Чтобы понять, почему важна точность, я хочу, чтобы вы сначала реализовали более сложный (и более полезный!) узел, чем Add: узел Linear.

Линейная функция

Простой искусственный нейрон зависит от трех компонентов:

  • входы, x (вектор)
  • веса, w (вектор)
  • смещение, b (скалярное)

Выход, o, представляет собой взвешенную сумму входных данных плюс смещение.

Помните, что, меняя веса, вы можете варьировать степень влияния любого входного сигнала на результат. Аспект обучения нейронных сетей происходит во время процесса, известного как обратное распространение. При обратном распространении сеть изменяет веса, чтобы повысить точность выходных данных сети. Вы будете применять все это в ближайшее время.

В следующем разделе вы попытаетесь построить линейный нейрон, генерирующий выходные данные, применяя упрощенную версию уравнения (1). Linear должен принимать список входящих узлов длины n, список весов длины n и смещение.

class Linear(Node):
    def __init__(self, inputs, weights, bias):
        Node.__init__(self, [inputs, weights, bias])
        # NOTE: The weights and bias properties here are not
        # numbers, but rather references to other nodes.
        # The weight and bias values are stored within the
        # respective nodes.
    def forward(self):
        """
        Set self.value to the value of the linear function output.
        
        Your code goes here!
        """
        
        output = 0
        for i in range(0, len(self.inbound_nodes)):
            #print(self.inbound_nodes[i].value)
            
            inValue = self.inbound_nodes[0].value[i]
            weight = self.inbound_nodes[1].value[i]
            bias = self.inbound_nodes[2].value
            
            output += (inValue*weight + bias)
        
        self.value = output
        pass

В приведенном выше разделе я установил self.value для смещения, а затем перебрал входные данные и веса, добавляя каждый взвешенный вход к self.value. Обратите внимание, что вызов .valueonself.inbound_nodes[0]orself.inbound_nodes[1] дает нам список.

Преобразование

Линейная алгебра прекрасно отражает идею преобразования значений между слоями в графе. На самом деле концепция преобразования делает именно то, что должен делать слой — преобразовывает входные данные в выходные во многих измерениях.

Вернемся к нашему уравнению для выхода.

В оставшейся части этого раздела мы будем обозначать x как X, а w как W, поскольку теперь они являются матрицами, а b теперь является вектором, а не скаляром.

Рассмотрим узел Linear с 1 входом и k выходами (сопоставление 1 входа с k выходами). В этом контексте ввод/вывод является синонимом признака.

В этом случае X представляет собой матрицу 1 на 1.

Матрица 1 на 1, с 1 элементом. Нижний индекс представляет строку (1) и столбец (1) элемента в матрице.

W становится матрицей размером 1 на k (выглядит как строка).

Результатом матричного умножения X и W является матрица 1 на k. Поскольку bb также представляет собой матрицу строк размером 1 на k (1 смещение на выход), bb добавляется к выходным данным умножения матриц X и W.

Что, если мы сопоставляем n входных данных с k выходными данными?

Тогда X теперь представляет собой матрицу размером 1 на n, а W представляет собой матрицу n на k. Результатом матричного умножения по-прежнему является матрица 1 на k, поэтому использование смещений остается прежним.

Давайте рассмотрим пример входных функций n. Рассмотрим изображение в оттенках серого размером 28 на 28 пикселей, как в случае с изображениями в наборе данных MNIST. Мы можем изменить форму (сгладить) изображение таким образом, чтобы оно представляло собой матрицу 1 на 784, где n = 784. Каждый пиксель — это вход/функция. Вот анимированный пример, подчеркивающий, что пиксель является функцией.

Пиксели — это функции!

На практике принято передавать несколько примеров данных в каждом прямом проходе, а не только 1. Причина этого в том, что примеры могут обрабатываться параллельно, что приводит к значительному приросту производительности. Количество примеров называется размером пакета. Общие числа для размера пакета: 32, 64, 128, 256, 512. Как правило, это максимальное количество, которое мы можем удобно разместить в памяти.

Что это означает для X, W и b?

X становится матрицей m на n (где m — размер пакета по количеству входных функций, n), а W и b остаются прежними. Результат матричного умножения теперь равен m на k (размер пакета зависит от количества выходов), поэтому добавление bb транслируется по каждой строке.

В контексте MNIST каждая строка X представляет собой изображение, измененное/сплющенное с 28 на 28 до 1 на 784.

Уравнение (1) превращается в:

Уравнение (2) также можно рассматривать как Z=XW+B, где B — вектор смещения, b, суммированный m раз. Из-за широковещательной передачи это сокращено до Z = XW + b.

Нам придется перестроить Linear для обработки матриц и векторов, используя почтенный математический пакет Python numpy, чтобы облегчить вашу жизнь. numpy часто обозначается аббревиатурой np, поэтому при обращении к коду мы будем называть его np.

Я использовал np.array (документация) для создания матриц и векторов. Вы захотите использовать np.dot, который функционирует как умножение матриц для 2D-массивов (документация), чтобы умножить входные и весовые матрицы из уравнения (2). Также стоит отметить, что numpy на самом деле перегружает оператор __add__, поэтому вы можете использовать его напрямую с np.array (например, np.array() + np.array()).

class Linear(Node):
    def __init__(self, X, W, b):
        # Notice the ordering of the input nodes passed to the
        # Node constructor.
        Node.__init__(self, [X, W, b])

    def forward(self):
        """
        Set the value of this node to the linear transform output.

        Your code goes here!
        """
        #output = 0
        
        X = self.inbound_nodes[0].value
        W = self.inbound_nodes[1].value
        b = self.inbound_nodes[2].value
        
        Z = np.dot(X,W) + b
        #print(X)
        #print(W)
        #print(b)
        
        #output = np.dot(X,W)
        self.value = Z

Мы попробуем импортировать наш файл мини-потока в python и реализовать его.

import numpy as np
from miniflow import *

X, W, b = Input(), Input(), Input()

f = Linear(X, W, b)

X_ = np.array([[-1., -2.], [-1, -2]])
W_ = np.array([[2., -3], [2., -3]])
b_ = np.array([-3., -5])

feed_dict = {X: X_, W: W_, b: b_}

graph = topological_sort(feed_dict)
output = forward_pass(f, graph)

"""
Output should be:
[[-9., 4.],
[-9., 4.]]
"""
print(output)

Заключение

Мы на полпути к реализации miniflow — нашей собственной версии tensorflow. Здесь мы реализовали концепции графов, прямого распространения, обучения и потерь и линейного преобразования.

Что дальше!!!!

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

А пока давайте запрограммируем беспилотные автомобили, используя этот курс по самоуправляемым автомобилям.

Ссылки:

https://in.udacity.com/course/самостоятельное вождение-автомобиль-инженер-наноградус–nd013