Нативная нейронная сеть, часть III: критерии обучения

искусственный интеллект Нейронные сети NumPy
Нативная нейронная сеть, часть III: критерии обучения

оригинал:Deep Learning From Scratch III: Training Criterion - deep ideas

Перевод: Сунь Имэн



Training criterion

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

Тогда, естественно, у нас возникает проблема: как задать весовую матрицуWи предвзятостьbподходящее значение. В красно-синем примере мы просто посмотрели на обучающие данные и угадали линию, которая точно разделяет точку на плоскости на две части.

Но в целом мы не хотим находить разделительную линию вручную, мы хотим, чтобы после ввода обучающих точек в компьютер, он сам находил хорошую разделительную линию.

Так как же нам оценить, хороша или плоха разделительная линия?


Скорость ошибочной классификации

В идеале мы хотим найти разделительную линию с как можно меньшим количеством ошибок. Мы не знаем истинного распределения генерации данныхp_{data}(x, c(x))подобен, но для каждой точки x в нем и каждого класса c(x), которому он принадлежит, мы хотим минимизировать вероятность того, что персептрон неправильно классифицирует. то есть свести к минимуму错分率:

\underset{W,b}{argmin} p(\hat{c}(x) \neq c(x) ~ | ~ x, c(x) \sim p_{data})

Часто мы не знаем истинного распределения генерации данныхp_{data}, поэтому невозможно рассчитать точное значение частоты ошибок. Но у нас есть набор обучающих точек, содержащих значение x и категорию, к которой он принадлежит.

В дальнейшем будем использовать формуX \in \mathbb{R}^{N \times d}Матрица представляет собой набор обучающих точек, каждая строка представляет обучающую точку, количество столбцов матрицы равно размерности обучающих точек, а каждый столбец представляет размерность.

Кроме того, мы используем матрицу истинной правильной классификацииc \in \mathbb{R}^{N \times C}означает, что еслиiобучающие выборки имеют классификациюj,Так[math]$ c_{i,j} = 1 $[/math]. Точно так же мы также используем матрицу\hat{c} \in \mathbb{R}^{N \times C}представляет собой ожидаемую классификацию, если перваяiПрогнозируемые классы обучающих выборок:j,Так\hat{c}_{i,j} = 1.

Наконец, мы используем матрицуp \in \mathbb{R}^{N \times C}для представления вероятности, гдеp_{i,j}означает первыйiобучающие выборки относятся к категорииjВероятность.

С данными обучения мы можем найти классификатор с наименьшей частотой ошибочной классификации выборки:

\underset{W,b}{argmin} \frac{1}{N} \sum_{i=1}^N I(\hat{c_i} \neq c_i)

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

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


оценка максимального правдоподобия

Альтернативаоценка максимального правдоподобия, при оценке максимального правдоподобия мы пытаемся найти конкретные значения параметров, которые делают вероятностьp(\hat{c} = c)максимизировать.

`[math]\underset{W,b}{argmax} p(\hat{c} = c)[/math]`
`[math] = \underset{W,b}{argmax} \prod_{i=1}^N p(\hat{c}_i = c_i) [/math]`
`[math] = \underset{W,b}{argmax} \prod_{i=1}^N \prod_{j=1}^C p_{i,j}^{I(c_i=j)} [/math]`
`[math] = \underset{W,b}{argmax} \prod_{i=1}^N \prod_{j=1}^C p_{i,j}^{c_{i,j}} [/math]`
`[math] = \underset{W,b}{argmax} \log{\prod_{i=1}^N \prod_{j=1}^C p_{i,j}^{c_{i,j}}} [/math]`
`[math] = \underset{W,b}{argmax} \sum_{i=1}^N \sum_{j=1}^C c_{i,j} \cdot \log{p_{i,j}} [/math]`
`[math] = \underset{W,b}{argmin} - \sum_{i=1}^N \sum_{j=1}^C c_{i,j} \cdot \log{p_{i,j}} [/math]`
`[math] = \underset{W,b}{argmin} J [/math]`

Здесь мы представляем`[math]J = - \sum_{i=1}^N \sum_{j=1}^C c_{i,j} \cdot \log{p_{i,j}}[/math]`

Jназываетсяперекрестная потеря энтропии. Здесь мы желаемJминимизировать.

мы кладемJсчитается однимOperation, то его ввод: данныеX, реальная классификацияcи ожидаемая вероятностьp.pто есть операцияσВывод.

Это позволяет рассчитать истинное количество потерь:

J Operation 的计算图


построить один для вычисленийJизoperation

построить для расчетаJизoperaion, мы можем поставить несколько первичныхoperationКомбинированный. Во-первых,JМожно использовать матричные операции.⊙выражается следующим образом:

`[math]- \sum_{i=1}^N \sum_{j=1}^C (c \odot \log{p})_{i,j} [/math]`

Отсюда мы можем найти, что упомянутый выше «первичный»operationЕсть следующие:

  • log: операция логарифмирования (логарифма), выполняемая над каждым элементом матрицы или вектора.

  • ⊙: Умножение каждого элемента между двумя матрицами

  • \sum_{j=1}^C: сумма всех столбцов матрицы

  • \sum_{i=1}^N: сумма всех строк матрицы

  • −: Поэлементное отрицание

Затем реализуем их по отдельности.

1. log

Поэлементные логарифмические операции над тензорами:

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

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

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)

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)

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 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

class softmax(Operation):
    """返回 a 的 softmax 函数结果.
    """

    def __init__(self, a):
        """构造 softmax

        参数列表:
          a: 输入节点
        """
        super().__init__([a])

    def compute(self, a_value):
        """计算 softmax operation 的输出值

        参数列表:
          a_value: 输入值
        """
        return np.exp(a_value) / np.sum(np.exp(a_value), axis=1)[:, None]
class log(Operation):
    """ 对每一个元素进行对数运算
    """

    def __init__(self, x):
        """ 构造 log

        参数列表:
          x: 输入节点
        """
        super().__init__([x])

    def compute(self, x_value):
        """ 计算对数 operation 的输出

        参数列表:
          x_value: 输入值
        """
        return np.log(x_value)

2. Умножение / ⊙

Поэлементное умножение двух тензоров одинаковой формы

class multiply(Operation):
    """ 对每一个元素,返回 x * y 的值
    """

    def __init__(self, x, y):
        """ 构造乘法

        参数列表:
          x: 第一个乘数的输入节点
          y: 第二个乘数的输入节点
        """
        super().__init__([x, y])

    def compute(self, x_value, y_value):
        """ 计算乘法 operation 的输出

        Args:
          x_value: 第一个乘数的值
          y_value: 第二个乘数的值
        """
        return x_value * y_value

3. reduce_sum

Чтобы вычислить несколько сумм за одну операцию (например, сумму всех строк, сумму всех столбцов и т. д.), мы указываем ось значений, если ось = 0 означает вычисление суммы строки, ось = 1 означает вычислить сумму столбца, И т.д., и т.п. Нампи делает именно это.

class reduce_sum(Operation):
    """ 计算张量中元素延某一或某些维度的总和
    """

    def __init__(self, A, axis=None):
        """ 构造 reduce_sum

        参数列表:
          A: 要进行 reduce 运算的张量
          axis: 需要 reduce 的维度,如果 `None`(即缺省值),则延所有维度 reduce
        """
        super().__init__([A])
        self.axis = axis

    def compute(self, A_value):
        """ 计算 reduce_sum operation 的输出值

        参数列表:
          A_value: 输入的张量值
        """
        return np.sum(A_value, self.axis)

4. Отрицательная операция

Отмените каждый элемент тензора:

class negative(Operation):
    """ 逐元素计算负数
    """

    def __init__(self, x):
        """ 构造负运算

        参数列表:
          x: 输入节点
        """
        super().__init__([x])

    def compute(self, x_value):
        """ 计算负运算 operation 的输出

        参数列表:
          x_value: 输入值
        """
        return -x_value

5. Объедините этиoperation

Используя эти операции, описанные выше, теперь мы можем записать операцию для J следующим образом:

 J = negative(reduce_sum(reduce_sum(multiply(c, log(p)), axis=1))) 

### Пример Теперь давайте рассчитаем потери красного/синего персептрона.
import numpy as np
red_points = np.random.randn(50, 2) - 2*np.ones((50, 2))
blue_points = np.random.randn(50, 2) + 2*np.ones((50, 2))

# 创建一个新的 graph
Graph().as_default()

X = placeholder()
c = placeholder()

W = Variable([
    [1, -1],
    [1, -1]
])
b = Variable([0, 0])
p = softmax(add(matmul(X, W), b))

# 交叉熵损失
J = negative(reduce_sum(reduce_sum(multiply(c, log(p)), axis=1)))

session = Session()
print(session.run(J, {
    X: np.concatenate((blue_points, red_points)),
    c:
        [[1, 0]] * len(blue_points)
        + [[0, 1]] * len(red_points)

}))

Если у вас есть какие-либо вопросы, не стесняйтесь задавать их в комментариях.