Глубокое обучение в Python: математика, стоящая за нейронными сетями|Вызов августовского обновления

глубокое обучение Python

Эта статья — одна из серии заметок, которые я написал, изучая Deep Learning with Python (второе издание, Франсуа Шолле). Содержимое статьи конвертируется из блокнотов Jupyter в Markdown, и когда я закончу все статьи, я опубликую все написанные мной блокноты Jupyter на GitHub.

Вы можете прочитать оригинальный текст (на английском языке) этой книги онлайн на этом сайте:live book.manning.com/book/deep-come…

Автор этой книги также дает набор блокнотов Jupyter:GitHub.com/ Very OL T/…


Эта статьяГлава 2 Прежде чем вы начнете: математика нейронных сетей(Глава 2. Прежде чем мы начнем: математические строительные блоки нейронных сетей) Интеграция заметок.

Первый взгляд на нейронные сети

Изучение языка программирования начинается с «Hello World», а изучение глубокого обучения начинается сMINSTНачинать.

MNIST используется для обучения распознаванию рукописных цифр.Он содержит 28x28 рукописных изображений в градациях серого и метку (значение 0~9), соответствующую каждому изображению.

Импорт набора данных MNIST

# Loading the MNIST dataset in Keras
from tensorflow.keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

Взгляните на тренировочный набор:

print(train_images.shape)
print(train_labels.shape)
train_labels

вывод:

(60000, 28, 28)
(60000,)

array([5, 0, 4, ..., 5, 6, 8], dtype=uint8)

Вот тестовый набор:

print(test_images.shape)
print(test_labels.shape)
test_labels

вывод:

(10000, 28, 28)
(10000,)

array([7, 2, 1, ..., 4, 5, 6], dtype=uint8)

построение сети

Построим нейронную сеть для изучения набора MNIST:

from tensorflow.keras import models
from tensorflow.keras import layers

network = models.Sequential()
network.add(layers.Dense(512, activation='relu', input_shape=(28 * 28, )))
network.add(layers.Dense(10, activation='softmax'))

Нейронные сети состоят из «слоев». «Слой» подобен «фильтру дистилляции», он «фильтрует» входные данные, «уточняет» из них нужную информацию, а затем передает ее следующему слою.

Такая серия «слоев» объединяется для обработки данных, как в конвейере. Слой за слоем обработанные данные или «представление данных» становятся все более и более «полезными» для конечного желаемого результата.

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

Данные достигают последнего слоя (второго слоя), который является10 способслой софтмакс. Выход этого слоя представляет собой массив, содержащий 10 значений вероятности (их сумма равна 1), информация, которую «представляет» этот выход, весьма полезна для нас, чтобы предсказать число, соответствующее картинке. Фактически, каждое значение вероятности в этом выводе представляет собой вероятность того, что входное изображение принадлежит одному из 10 чисел (0-9)!

компилировать

Далее мы будемкомпилироватьЭта сеть, этот шаг должен дать 3 параметра:

  • Функция потерь: функция, которая оценивает, насколько хорошо работает ваша сеть.
  • Оптимизатор: как обновить (оптимизировать) вашу сеть
  • Показатели, которые необходимо отслеживать при обучении и тестировании.Например, в данном примере нас интересует только один показатель — точность прогноза
network.compile(loss="categorical_crossentropy",
                optimizer='rmsprop',
                metrics=['accuracy'])

предварительная обработка

обработка графики

Нам также необходимо обработать данные графика и превратить их в то, что распознает наша сеть.

Изображения в наборе данных MNIST имеют размер 28x28, и каждое значение представляет собой uint8, принадлежащий [0, 255]. И то, что нужно нашей нейронной сети, — это 28x28 float32 в [0, 1].

train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype('float32') / 255

test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype('float32') / 255

Обработка этикеток

Точно так же нужно иметь дело с этикетками.

from tensorflow.keras.utils import to_categorical

train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)

обучать сеть

network.fit(train_images, train_labels, epochs=5, batch_size=128)

вывод:

Train on 60000 samples
Epoch 1/5
60000/60000 [==============================] - 3s 49us/sample - loss: 0.2549 - accuracy: 0.9254
Epoch 2/5
60000/60000 [==============================] - 2s 38us/sample - loss: 0.1025 - accuracy: 0.9693
Epoch 3/5
60000/60000 [==============================] - 2s 35us/sample - loss: 0.0676 - accuracy: 0.9800
Epoch 4/5
60000/60000 [==============================] - 2s 37us/sample - loss: 0.0491 - accuracy: 0.9848
Epoch 5/5
60000/60000 [==============================] - 2s 42us/sample - loss: 0.0369 - accuracy: 0.9888

<tensorflow.python.keras.callbacks.History at 0x13a7892d0>

Как видите, обучение проходит быстро, с точностью 98%+ на тренировочном наборе через некоторое время.

Попробуйте еще раз с тестовым набором:

test_loss, test_acc = network.evaluate(test_images, test_labels, verbose=2)    # verbose=2 to avoid a looooong progress bar that fills the screen with '='. https://github.com/tensorflow/tensorflow/issues/32286
print('test_acc:', test_acc)

вывод:

10000/1 - 0s - loss: 0.0362 - accuracy: 0.9789
test_acc: 0.9789

Производительность нашей обученной сети в тестовом наборе не так хороша, как в предыдущем тренировочном наборе, который является источником «переоснащения».

Представление данных для нейронных сетей

Тензор, тензор, массив произвольной размерности (имею в виду такой массив, который запрограммирован). Матрица — это двумерный тензор.

Мы часто называем «размерность тензора» «осью».

Распознавать тензоры

Скаляр (тензоры 0D)

Скаляры, скаляры — это 0-мерные тензоры (0 осей), которые содержат число.

Скаляр может быть представлен как float32 или float64 в numpy.

import numpy as np

x = np.array(12)
x

вывод:

array(12)
x.ndim    # 轴数(维数)

вывод:

1

Вектор (одномерные тензоры)

Векторы, вектор — это одномерный тензор (с 1 осью), который содержит столбец скаляров (то есть массив скаляров).

x = np.array([1, 2, 3, 4, 5])
x

вывод:

array([1, 2, 3, 4, 5])
x.ndim

вывод:

1

Мы называем такой вектор с 5 элементами «5-мерным вектором». Но обратите внимание5D векторнет5D тензор!

  • 5D вектор: есть только 1 ось и 5 измерений на этой оси.
  • Тензор 5D: имеет 5 осей и может иметь произвольные размеры по каждой оси.

Это очень сбивает с толку.Это «размерность» иногда относится к количеству осей, а иногда к количеству элементов на оси.

Итак, нам лучше выразить это по-другому и использовать «порядок» для представления количества осей, говорятензоры 5 ранга.

Матрицы (двумерные тензоры)

Матрицы, матрица — это тензор ранга 2 (2 оси, которые мы называем «строка» и «столбец»), содержащий вектор-столбец (то есть вектор-массив).

x = np.array([[5, 78, 2, 34, 0],
              [6, 79, 3, 35, 1],
              [7, 80, 4, 36, 2]])
x

вывод:

array([[ 5, 78,  2, 34,  0],
       [ 6, 79,  3, 35,  1],
       [ 7, 80,  4, 36,  2]])
x.ndim

вывод:

2

Тензоры высшего порядка

Вы получаете массив матриц и тензор ранга 3.

Другой массив тензоров ранга 3 получит тензор ранга 4, и так далее, будет тензор более высокого ранга.

x = np.array([[[5, 78, 2, 34, 0],
               [6, 79, 3, 35, 1],
               [7, 80, 4, 36, 2]],
              [[5, 78, 2, 34, 0],
               [6, 79, 3, 35, 1],
               [7, 80, 4, 36, 2]],
              [[5, 78, 2, 34, 0],
               [6, 79, 3, 35, 1],
               [7, 80, 4, 36, 2]]])
x.ndim

вывод:

3

В глубоком обучении мы обычно используем тензоры порядка от 0 до 4.

Три элемента тензоров

  • Порядок (количество осей): 3, 5, ...
  • Форма (размеры каждой оси): (2, 1, 3), (6, 5, 5, 3, 6),...
  • Типы данных: float32, uint8, ...

Давайте посмотрим на тензорные данные в MNIST:

from tensorflow.keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

print(train_images.ndim)
print(train_images.shape)
print(train_images.dtype)

вывод:

3
(60000, 28, 28)
uint8

Итак, train_images — это тензор ранга 3 8-битных целых чисел без знака.

Взгляните на фотографии внутри:

digit = train_images[0]

import matplotlib.pyplot as plt

print("image:")
plt.imshow(digit, cmap=plt.cm.binary)
plt.show()
print("label: ", train_labels[0])

вывод:

一张图片,显示了一个位于图像中央的数字5

label:  5

Numpy тензорные операции

Тензорные срезы:

my_slice = train_images[10:100]
print(my_slice.shape)

вывод:

(90, 28, 28)

Эквивалентно:

my_slice = train_images[10:100, :, :]
print(my_slice.shape)

вывод:

(90, 28, 28)

также эквивалентно

my_slice = train_images[10:100, 0:28, 0:28]
print(my_slice.shape)

вывод:

(90, 28, 28)

избиратьНижний правый14x14:

my_slice = train_images[:, 14:, 14:]
plt.imshow(my_slice[0], cmap=plt.cm.binary)
plt.show()

вывод:

一张图片,在左上角有数字5的一部分

Выберите 14x14 в центре:

my_slice = train_images[:, 7:-7, 7:-7]
plt.imshow(my_slice[0], cmap=plt.cm.binary)
plt.show()

вывод:

一张图片,显示了一个数字5,占满了整个图片

пакет данных

В данных глубокого обучения первая ось (индекс = 0) обычно называется «осью выборки» (или «размером выборки»).

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

В MNIST один из наших пакетов состоит из 128 данных:

# 第一批
batch = train_images[:128]
# 第二批
batch = train_images[128:256]
# 第n批
n = 12
batch = train_images[128 * n : 128 * (n+1)]

Поэтому при использовании партии мы также называем первую ось «осью партии».

Общие представления тензора данных

данные тензорная размерность форма
векторные данные 2D (samples,features)
последовательно 3D (samples, timesteps, features)
изображение 4D (образцы, высота, ширина, каналы) или (образцы, каналы, высота, ширина)
видео 5D (сэмплы, кадры, высота, ширина, каналы) или (сэмплы, кадры, каналы, высота, ширина)

«Шестерни» нейронных сетей: тензорные операции

В нашем первом примере нейронной сети (MNIST) каждый из наших слоев на самом деле делает с входными данными что-то вроде следующего:

output = relu(dot(W, input) + b)

вход есть вход, W и b — свойства слоя, выход есть выход.

Соотнесите, расставьте точки, добавьте операции между этими вещами, Далее мы объясним эти операции.

По элементам

Поэлементная операция заключается в воздействии на каждый элемент тензора отдельно. Например, мы реализуем простуюrelu(relu(x) = max(x, 0)):

def naive_relu(x):
    assert len(x.shape) == 2    # x is a 2D Numpy tensor.
    x = x.copy()    # Avoid overwriting the input tensor.
    for i in range(x.shape[0]):
        for j in range(x.shape[1]):
            x[i, j] = max(x[i, j], 0)
    return x

Сложение также является поэлементной операцией:

def naive_add(x, y):
    # assert x and y are 2D Numpy tensors and have the same shape.
    assert len(x.shape) == 2
    assert x.shape == y.shape
    
    x = x.copy()    # Avoid overwriting the input tensor.
    for i in range(x.shape[0]):
        for j in range(x.shape[1]):
            x[i, j] += y[i, j]
    return x

В Numpy это все написано. Конкретная операция передается BLAS, написанному на C или Fortran, и скорость очень высока.

Вы можете проверить, установлен ли BLAS, следующим образом:

import numpy as np

np.show_config()

вывод:

blas_mkl_info:
  NOT AVAILABLE
blis_info:
  NOT AVAILABLE
openblas_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/usr/local/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]
blas_opt_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/usr/local/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]
lapack_mkl_info:
  NOT AVAILABLE
openblas_lapack_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/usr/local/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]
lapack_opt_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/usr/local/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]

Вот как использовать элементное relu numpy, добавьте:

a = np.array([[1, 2, 3],
              [-1, 2, -3],
              [3, -1, 4]])
b = np.array([[6, 7, 8], 
              [-2, -3, 1], 
              [1, 0, 4]])

c = a + b    # Element-wise addition
d = np.maximum(c, 0)    # Element-wise relu

print(c)
print(d)

вывод:

[[ 7  9 11]
 [-3 -1 -2]
 [ 4 -1  8]]
[[ 7  9 11]
 [ 0  0  0]
 [ 4  0  8]]

Вещание

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

В частности, можно транслировать в форме(a, b, ..., n, n+1, ..., m)и(n, n+1, ..., m)Выполните поэлементные операции над двумя тензорами .

Например:

x = np.random.random((64, 3, 32, 10))    # x is a random tensor with shape (64, 3, 32, 10).
y = np.random.random((32, 10))    # y is a random tensor with shape (32, 10).
z = np.maximum(x, y)    # The output z has shape (64, 3, 32, 10) like x.

Вещание работает следующим образом:

  1. Малая ось увеличения тензора (ось вещания), добавить к той же, что и большая (ndim)
  2. Элементы малого тензора повторяются на новой оси, добавляя к той же форме, что и большой.

E.g.

x: (32, 10), y: (10,)
Step 1: add an empty first axis to y: Y -> (1, 10)
Step 2: repeat y 32 times alongside this new axis: Y -> (32, 10)

После завершения естьY[i, :] == y for i in range(0, 32)

Конечно, в реальной реализации мы не копируем это так, это пустая трата места, Реализуем эту «копию» прямо в алгоритме. Например, давайте реализуем простое сложение вектора и матрицы:

def naive_add_matrix_and_vector(m, v):
    assert len(m.shape) == 2    # m is a 2D Numpy tensor.
    assert len(v.shape) == 1    # v is a Numpy vector.
    assert m.shape[1] == v.shape[0]
    
    m = m.copy()
    for i in range(m.shape[0]):
        for j in range(m.shape[1]):
            m[i, j] += v[j]
    return m

naive_add_matrix_and_vector(np.array([[1 ,2, 3], [4, 5, 6], [7, 8, 9]]), 
                            np.array([1, -1, 100]))

вывод:

array([[  2,   1, 103],
       [  5,   4, 106],
       [  8,   7, 109]])

Тензорное скалярное произведение (точка)

Тензорный точечный продукт или тензорный продукт используется в numpydot(x, y)Заканчивать.

Работу скалярного произведения можно увидеть в следующей простой программе:

# 向量点积
def naive_vector_dot(x, y):
    assert len(x.shape) == 1
    assert len(y.shape) == 1
    assert x.shape[0] == y.shape[0]
    
    z = 0.
    for i in range(x.shape[0]):
        z += x[i] * y[i]
    return z


# 矩阵与向量点积
def naive_matrix_vector_dot(x, y):
    z = np.zeros(x.shape[0])
    for i in range(x.shape[0]):
        z[i] = naive_vector_dot(x[i, :], y)
    return z


# 矩阵点积
def naive_matrix_dot(x, y):
    assert len(x.shape) == 2
    assert len(y.shape) == 2
    assert x.shape[1] == y.shape[0]
    
    z = np.zeros((x.shape[0], y.shape[1]))
    for i in range(x.shape[0]):
        for j in range(y.shape[1]):
            row_x = x[i, :]
            column_y = y[:, j]
            z[i, j] = naive_vector_dot(row_x, column_y)
    return z
a = np.array([[1, 2, 3],
              [-1, 2, -3],
              [3, -1, 4]])
b = np.array([[6, 7, 8], 
              [-2, -3, 1], 
              [1, 0, 4]])
naive_matrix_dot(a, b)

вывод:

array([[  5.,   1.,  22.],
       [-13., -13., -18.],
       [ 24.,  24.,  39.]])

То же верно и для многомерных тензорных точечных произведений. Например, (это говорит о форме, ха-ха):

(a, b, c, d) . (d,) -> (a, b, c)
(a, b, c, d) . (d, e) -> (a, b, c, e)

Тензорное преобразование (изменение формы)

Эта операция, короче говоря, все те же элементы, но способ их расположения изменился.

x = np.array([[0., 1.],
              [2., 3.],
              [4., 5.]])
print(x.shape)

вывод:

(3, 2)
x.reshape((6, 1))

вывод:

array([[0.],
       [1.],
       [2.],
       [3.],
       [4.],
       [5.]])
x.reshape((2, 3))

вывод:

array([[0., 1., 2.],
       [3., 4., 5.]])

«Транспозиция» (transposition) — это особый вид деформации матрицы, Транспонирование — это замена строки на столбец.

оригинальныйx[i, :], после транспонирования становитсяx[:, i].

x = np.zeros((300, 20))
y = np.transpose(x)
print(y.shape)

вывод:

(20, 300)

«Двигатель» нейронной сети: градиентная оптимизация

Снова взглянем на наш первый пример нейронной сети (MNIST) и посмотрим, что каждый слой делает с входными данными:

output = relu(dot(W, input) + b)

В этой формуле: W и b — свойства слоя (веса или обучаемые параметры). Конкретно,

  • Wявляется свойством ядра;
  • bявляется атрибутом смещения.

Эти «веса» — это то, что нейронная сеть узнает из данных.

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

Этот процесс улучшения осуществляется в «цикле обучения», который может продолжаться и продолжаться, если в этом нет необходимости:

  1. Извлеките пакет обучающих данных x и соответствующий y
  2. Распространение вперед, чтобы получить предсказание y_pred, рассчитанное сетью для x
  3. Рассчитайте потери от y_pred и y
  4. Настройте параметры каким-либо образом, чтобы уменьшить потери

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

Производная

В этом разделе объясняется определение производных.

(Прямо к книге.)

Зная производную, обновите x, чтобы минимизировать функциюf(x), на самом деле просто переместите производную x в противоположном направлении.

градиент

«Градиент» — это производная от тензорной операции. Другими словами, «градиент» — это обобщение «производной» многомерных функций. Градиент точки представляет кривизну этой точки.

учитывать:

y_pred = dot(W, x)
loss_value = loss(y_pred, y)

Если x и y фиксированы, loss_value будет функцией W:

loss_value = f(W)

Пусть текущая точка будетW0, Тогда производная (градиент) f в точке W0 записывается какgradient(f)(W0), Это значение градиента того же типа, что и W. каждый из элементовgradient(f) (W0)[i, j]представляет собой изменениеW0[i, j], изменение направления и величины f.

Таким образом, чтобы изменить значение W для достиженияmin f, вы можете пойти в направлении, противоположном градиенту (т.е.градиентный спускнаправление) двигаться:

W1 = W0 - step * gradient(f)(W0)

Стохастический градиентный спуск

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

Внедрение этого метода в нейронную сеть требует решения задачи оWуравнениеgradient(f)(W) = 0, это N-элементное уравнение (N = количество параметров в нейронной сети), а на самом деле N вообще не меньше 1k, что делает решение этого уравнения практически невозможным.

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

  1. Извлеките пакет обучающих данных x и соответствующий y
  2. Распространение вперед, чтобы получить предсказание y_pred, рассчитанное сетью для x
  3. Рассчитайте потери от y_pred и y
  4. Настройте параметры каким-либо образом, чтобы уменьшить потери
    1. Обратное распространение, вычисляет градиент функции потерь по отношению к параметрам сети.
    2. Потери можно уменьшить, слегка сдвинув параметры в направлении, противоположном градиенту (W -= шаг * градиент).

Этот метод называется «мини-пакетный стохастический градиентный спуск» (мини-пакетный SGD). Слово «случайный» означает, что данные, которые мы нарисовали на шаге 1, были взяты случайным образом.

Есть несколько вариантов SGD, которые обновляют значения не только по текущему градиенту, но и по последнему обновлению веса. Эти варианты называются «методами оптимизации» или «оптимизаторами». Во многих из этих вариантов используется понятие «импульс».

«Импульс» в основном имеет дело с двумя проблемами в SGD: скоростью сходимости и локальными минимумами. Импульс можно использовать, чтобы избежать сходимости к локальному оптимальному решению, когда скорость обучения относительно мала, вместо того, чтобы продолжать движение к глобальному оптимальному решению.

Импульс здесь — понятие импульса из физики. Мы можем представить себе небольшой шарик, катящийся вниз (в направлении градиентного спуска) по поверхности потерь, и, если у него будет достаточно импульса, он может «промчаться» через локальный минимум и не застрять там. В этом примере движение мяча определяется не только наклоном текущего положения (текущим ускорением), но и текущей скоростью (которая зависит от предыдущего ускорения).

Эта идея заложена в нейросеть, то есть обновление значения веса, причем не только текущего градиента, но и последнего обновления веса:

# naive implementation of Optimization with momentum
past_velocity = 0.
momentum = 0.1    # Constant momentum factor
while loss > 0.01:    # Optimization loop
    w, loss, gradient = get_current_parameters()
    velocity = past_velocity * momentum + learning_rate * gradient
    w = w + momentum * velocity - learning_rate * gradient
    past_velocity = velocity
    update_parameter(w)

Алгоритм обратного распространения: цепные производные

Нейронные сети представляют собой набор тензорных операций, связанных вместе, например:

f(W1, W2, W3) = a(W1, b(W2, c(W3)))    # 其中 W1, W2, W3 是权重

В исчислении существует «цепное правило», по которому можно дифференцировать такую ​​составную функцию:f(g(x)) = f'(g(x)) * g'(x)

Применение этого цепного правила к нейронным сетям создает алгоритм под названием «Обратное распространение». Этот алгоритм также называют «дифференцированием в обратном режиме».

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

Сегодняшние фреймворки, такие как TensorFlow, имеют возможность, называемую «символической дифференциацией». Это позволяет этим фреймворкам автоматически находить функцию градиента для заданной операции нейросети, и тогда нам не нужно вручную реализовывать обратное распространение (хотя и интересно, но писать действительно надоедает), просто берем значение прямо из функции градиента.