Эта статья перемещена в личный блог, пожалуйста, нажмитездесьПосмотрите исходный пост в блоге.
Текущая среда глубокого обучения становится все более и более зрелой, а степень инкапсуляции для пользователей становится все выше и выше.Преимущество в том, что эти структуры теперь можно очень быстро использовать в качестве инструментов, а эксперименты можно проводить с очень небольшим количеством кода. Недостаток в том, что могут быть скрыты Реализации все скрыто. В этой статье автор поможет вам спроектировать и реализовать tinynn, легкую и простую в расширении структуру глубокого обучения с нуля, используя Python, надеясь помочь вам понять принципы глубокого обучения и структуру глубокого обучения.
В этой статье сначала будет проанализирован процесс глубокого обучения, абстрагированы ключевые компоненты в нейронной сети и определена базовая структура; затем реализован код для каждого компонента в структуре; наконец, на основе этой структуры приведен пример классификации MNIST. реализовано.
содержание
Абстракция компонента
Сначала рассмотрим процесс работы нейронной сети.Работа нейронной сети в основном включает в себя два этапа: обучение обучение и прогнозирование (или вывод).Основной процесс обучения: входные данные -> прямое распространение сетевого уровня -> потери вычислений -> сетевой уровень обратное Чтобы распространять градиенты -> параметры обновления, основной процесс прогнозирования - это входные данные -> прямое распространение сетевого уровня -> выходные результаты. С операционной точки зрения в основном существует три типа вычислений:
-
Данные передаются непосредственно на сетевом уровне Прямое распространение и обратное распространение можно рассматривать как поток тензоров (многомерных массивов) между слоями сети (потоки прямого распространения вход и выход, градиент потоков обратного распространения), каждый слой сети выполняет определенные операции, а затем вводит результат в следующий слой
-
Рассчитать потери Промежуточный процесс соединения прямого и обратного распространения определяет разницу между выходными данными модели и истинным значением, которое используется для последующего предоставления информации, необходимой для обратного распространения.
-
обновление параметра Класс вычислений, которые обновляют параметры сети с использованием вычисленных градиентов.
На основе этих трех типов мы можем сделать абстракцию основных компонентов сети.
-
tensor
Тензор, который является базовой единицей данных в нейронной сети. -
layer
Сетевой слой отвечает за получение входных данных предыдущего слоя, выполнение операции этого слоя и вывод результата на следующий слой.Поскольку поток тензора имеет два направления, прямое и обратное, для каждого типа сетевого слоя мы необходимо одновременно реализовывать как прямые, так и обратные операции -
loss
потери, этот компонент выводит значение потерь и градиент по отношению к последнему слою с учетом прогнозируемого значения модели и истинного значения (для транзитной передачи градиента) -
optimizer
Оптимизатор, отвечающий за обновление параметров модели с помощью градиентов.
Затем нам нужны некоторые компоненты, чтобы интегрировать вышеупомянутые четыре основных компонента вместе, чтобы сформировать конвейер.
-
net
Компонент отвечает за управление прямым и обратным распространением тензора между слоями и предоставляет интерфейсы для получения параметров, установки параметров и получения градиентов. -
model
Компоненты отвечают за интеграцию всех компонентов для формирования всего конвейера. То есть компонент сети выполняет прямое распространение -> компонент потерь вычисляет потери и градиент -> компонент сети выполняет обратное распространение градиента -> компонент оптимизатора обновляет градиент до параметров.
Базовая схема кадра выглядит следующим образом
Реализация компонента
Согласно приведенной выше абстракции, мы можем написать весь код процесса следующим образом. Сначала определите сеть, вход сети - это несколько сетевых слоев, а затем передайте сеть, потери, оптимизатор в модель вместе. Модель реализует интерфейсы прямого, обратного и apply_grad, соответствующие прямому распространению, обратному распространению и обновлению параметров соответственно.
# define model
net = Net([layer1, layer2, ...])
model = Model(net, loss_fn, optimizer)
# training
pred = model.forward(train_X)
loss, grads = model.backward(pred, train_Y)
model.apply_grad(grads)
# inference
test_pred = model.forward(test_X)
Далее давайте посмотрим, как здесь реализована каждая часть.
-
tensor
Тензор тензор — это базовая единица данных в нейронной сети, мы используем его непосредственно здесь.numpy.ndarrayКласс используется как реализация тензорного класса (нижний слой numpy использует C и Fortran, и сделано много оптимизаций на уровне алгоритма, и скорость работы не медленная)
-
layer
В приведенном выше коде процесса модель выполняет прямое и обратное На самом деле нижний уровень является фактической операцией сетевого уровня, поэтому сетевой уровень должен предоставлять прямой и обратный интерфейсы для соответствующих операций. Параметры и градиенты слоя также должны быть записаны. Сначала реализуйте базовый класс следующим образом
# layer.py class Layer(object): def __init__(self, name): self.name = name self.params, self.grads = None, None def forward(self, inputs): raise NotImplementedError def backward(self, grad): raise NotImplementedError
Самым базовым сетевым уровнем является полносвязный сетевой уровень, который реализован следующим образом. Прямой метод получает входные данные верхнего уровня и реализуетОперация обратного метода получает градиент от верхнего слоя и вычисляет параметрыи градиент ввода, затем вернуть градиент относительно ввода. Вывод этих трех градиентов можно найти в приложении, а реализация непосредственно приведена здесь. w_init и b_init — это инициализаторы для веса и смещения параметров соответственно, которые мы храним в инициализаторе другой реализации.
initializer.py
Для реализации эта часть не является основным компонентом, поэтому она не будет здесь представлена.# layer.py class Dense(Layer): def __init__(self, num_in, num_out, w_init=XavierUniformInit(), b_init=ZerosInit()): super().__init__("Linear") self.params = { "w": w_init([num_in, num_out]), "b": b_init([1, num_out])} self.inputs = None def forward(self, inputs): self.inputs = inputs return inputs @ self.params["w"] + self.params["b"] def backward(self, grad): self.grads["w"] = self.inputs.T @ grad self.grads["b"] = np.sum(grad, axis=0) return grad @ self.params["w"].T
В то же время другой важной частью нейронной сети является функция активации. Функцию активации можно рассматривать как своего рода сетевой уровень, который также должен реализовывать прямые и обратные методы. Мы реализуем класс функции активации, наследуя класс Layer, где реализована наиболее часто используемая функция активации ReLU. Методы func и derivation_func реализуют прямой расчет и градиентный расчет соответствующей функции активации соответственно.
# layer.py class Activation(Layer): """Base activation layer""" def __init__(self, name): super().__init__(name) self.inputs = None def forward(self, inputs): self.inputs = inputs return self.func(inputs) def backward(self, grad): return self.derivative_func(self.inputs) * grad def func(self, x): raise NotImplementedError def derivative_func(self, x): raise NotImplementedError class ReLU(Activation): """ReLU activation function""" def __init__(self): super().__init__("ReLU") def func(self, x): return np.maximum(x, 0.0) def derivative_func(self, x): return x > 0.0
-
net
Как упоминалось выше, класс net отвечает за управление прямым и обратным распространением тензора между слоями. Прямой метод очень прост: он обходит все слои по порядку, и выходные данные, вычисленные каждым слоем, используются в качестве входных данных следующего слоя, обратный обходит все слои в обратном порядке и использует градиент каждого слоя в качестве входных данных для следующего слоя. следующий слой. Здесь мы также сохраняем градиент каждого параметра сетевого слоя и возвращаем его, который будет использоваться позже для обновления параметра. Кроме того, класс net также реализует интерфейсы для получения параметров, установки параметров и получения градиентов, которые также необходимы для последующих обновлений параметров.
# net.py class Net(object): def __init__(self, layers): self.layers = layers def forward(self, inputs): for layer in self.layers: inputs = layer.forward(inputs) return inputs def backward(self, grad): all_grads = [] for layer in reversed(self.layers): grad = layer.backward(grad) all_grads.append(layer.grads) return all_grads[::-1] def get_params_and_grads(self): for layer in self.layers: yield layer.params, layer.grads def get_parameters(self): return [layer.params for layer in self.layers] def set_parameters(self, params): for i, layer in enumerate(self.layers): for key in layer.params.keys(): layer.params[key] = params[i][key]
-
loss
Мы упоминали выше, что компонент потерь должен делать две вещи: учитывая прогнозируемое значение и истинное значение, он должен вычислять значение потерь и градиент по отношению к прогнозируемому значению. Мы реализуем методы loss и grad соответственно.Здесь мы реализуем потери SoftmaxCrossEntropyLoss, обычно используемые в регрессии с несколькими классами. Формулы расчета этих потерь потерь и градиента grad выведены в приложении в конце статьи, а результаты непосредственно приведены здесь:
Потеря многоклассовой кросс-энтропии softmax
$$ J_{CE}(y, \hat{y}) = -\sum_{i=1}^N \log \hat{y_i^{c}} $$Градиент немного сложнее, и формулы расчета для целевой категории и нецелевой категории разные. Для измерения целевой категории градиент представляет собой выходную вероятность соответствующей модели измерения минус единицу, а для измерения нецелевой категории градиент представляет собой выходную вероятность самого соответствующего измерения.
$$ \frac{\partial J_{ce}}{\partial o^c}= \begin{случаи} (\ шляпа {y} ^ c-1) / N & \ text {целевой класс} c \\ y ^ {\ тильда {c}} / N & \ text {нецелевой класс} \ тильда {c} \end{случаи} $$Код реализован следующим образом
# loss.py class BaseLoss(object): def loss(self, predicted, actual): raise NotImplementedError def grad(self, predicted, actual): raise NotImplementedError class CrossEntropyLoss(BaseLoss): def loss(self, predicted, actual): m = predicted.shape[0] exps = np.exp(predicted - np.max(predicted, axis=1, keepdims=True)) p = exps / np.sum(exps, axis=1, keepdims=True) nll = -np.log(np.sum(p * actual, axis=1)) return np.sum(nll) / m def grad(self, predicted, actual): m = predicted.shape[0] grad = np.copy(predicted) grad -= actual return grad / m
-
optimizer
Оптимизатор в основном реализует интерфейс calculate_step, этот метод вычисляет размер шага каждого изменения параметра при возврате к фактической оптимизации в соответствии с текущим градиентом. Здесь мы реализуем часто используемый оптимизатор Adam.
# optimizer.py class BaseOptimizer(object): def __init__(self, lr, weight_decay): self.lr = lr self.weight_decay = weight_decay def compute_step(self, grads, params): step = list() # flatten all gradients flatten_grads = np.concatenate( [np.ravel(v) for grad in grads for v in grad.values()]) # compute step flatten_step = self._compute_step(flatten_grads) # reshape gradients p = 0 for param in params: layer = dict() for k, v in param.items(): block = np.prod(v.shape) _step = flatten_step[p:p+block].reshape(v.shape) _step -= self.weight_decay * v layer[k] = _step p += block step.append(layer) return step def _compute_step(self, grad): raise NotImplementedError class Adam(BaseOptimizer): def __init__(self, lr=0.001, beta1=0.9, beta2=0.999, eps=1e-8, weight_decay=0.0): super().__init__(lr, weight_decay) self._b1, self._b2 = beta1, beta2 self._eps = eps self._t = 0 self._m, self._v = 0, 0 def _compute_step(self, grad): self._t += 1 self._m = self._b1 * self._m + (1 - self._b1) * grad self._v = self._b2 * self._v + (1 - self._b2) * (grad ** 2) # bias correction _m = self._m / (1 - self._b1 ** self._t) _v = self._v / (1 - self._b2 ** self._t) return -self.lr * _m / (_v ** 0.5 + self._eps)
-
model
Наконец, класс модели реализует три интерфейса forward, back и apply_grad, которые мы разработали в начале. Forward напрямую вызывает переадресацию net. градиент получается обратным распространением, а затем оптимизатор вычисляет размер шага, и, наконец, параметры обновляются с помощью apply_grad
# model.py class Model(object): def __init__(self, net, loss, optimizer): self.net = net self.loss = loss self.optimizer = optimizer def forward(self, inputs): return self.net.forward(inputs) def backward(self, preds, targets): loss = self.loss.loss(preds, targets) grad = self.loss.grad(preds, targets) grads = self.net.backward(grad) params = self.net.get_parameters() step = self.optimizer.compute_step(grads, params) return loss, step def apply_grad(self, grads): for grad, (param, _) in zip(grads, self.net.get_params_and_grads()): for k, v in param.items(): param[k] += grad[k]
весь кадр
Наконец, мы реализуем основную часть кода файловой структуры следующим образом.
tinynn
├── core
│ ├── __init__.py
│ ├── initializer.py
│ ├── layer.py
│ ├── loss.py
│ ├── model.py
│ ├── net.py
│ └── optimizer.py
вinitializer.py
Этот модуль не обсуждался выше и в основном реализует общие методы инициализации параметров для инициализации параметров сетевого уровня.
пример MNIST
После того, как фреймворк в основном настроен, мы находим пример использования фреймворка tinynn для его запуска. Базовая конфигурация этого примера выглядит следующим образом.
- набор данных:MNIST
- Тип задачи: мультиклассификация
- Структура сети: трехуровневое полное соединение
INPUT(784) -> FC(400) -> FC(100) -> OUTPUT(10)
, эта сеть получаетвход, гдеколичество выборок на вход, 784Сглаженный вектор изображения, выходной размер,в- количество образцов, а 10 - вероятность соответствующего изображения в 10 категориях. - Функция активации: ReLU
- Функция потерь: SoftmaxCrossEntropy
- оптимизатор: Адам (lr=1e-3)
- размер партии: 128
- Число_эпох: 20
Здесь мы игнорируем некоторые подготовительные коды, такие как загрузка данных и предварительная обработка, и вставляем только определение базовой структуры сети и обучающий код следующим образом.
# example/mnist/run.py
net = Net([
Dense(784, 400),
ReLU(),
Dense(400, 100),
ReLU(),
Dense(100, 10)
])
model = Model(net=net, loss=SoftmaxCrossEntropyLoss(), optimizer=Adam(lr=args.lr))
iterator = BatchIterator(batch_size=args.batch_size)
evaluator = AccEvaluator()
for epoch in range(num_ep):
for batch in iterator(train_x, train_y):
# training
pred = model.forward(batch.inputs)
loss, grads = model.backward(pred, batch.targets)
model.apply_grad(grads)
# evaluate every epoch
test_pred = model.forward(test_x)
test_pred_idx = np.argmax(test_pred, axis=1)
test_y_idx = np.asarray(test_y)
res = evaluator.evaluate(test_pred_idx, test_y_idx)
print(res)
Текущий результат выглядит следующим образом
# tinynn
Epoch 0 {'total_num': 10000, 'hit_num': 9658, 'accuracy': 0.9658}
Epoch 1 {'total_num': 10000, 'hit_num': 9740, 'accuracy': 0.974}
Epoch 2 {'total_num': 10000, 'hit_num': 9783, 'accuracy': 0.9783}
Epoch 3 {'total_num': 10000, 'hit_num': 9799, 'accuracy': 0.9799}
Epoch 4 {'total_num': 10000, 'hit_num': 9805, 'accuracy': 0.9805}
Epoch 5 {'total_num': 10000, 'hit_num': 9826, 'accuracy': 0.9826}
Epoch 6 {'total_num': 10000, 'hit_num': 9823, 'accuracy': 0.9823}
Epoch 7 {'total_num': 10000, 'hit_num': 9819, 'accuracy': 0.9819}
Epoch 8 {'total_num': 10000, 'hit_num': 9820, 'accuracy': 0.982}
Epoch 9 {'total_num': 10000, 'hit_num': 9838, 'accuracy': 0.9838}
Epoch 10 {'total_num': 10000, 'hit_num': 9825, 'accuracy': 0.9825}
Epoch 11 {'total_num': 10000, 'hit_num': 9810, 'accuracy': 0.981}
Epoch 12 {'total_num': 10000, 'hit_num': 9845, 'accuracy': 0.9845}
Epoch 13 {'total_num': 10000, 'hit_num': 9845, 'accuracy': 0.9845}
Epoch 14 {'total_num': 10000, 'hit_num': 9835, 'accuracy': 0.9835}
Epoch 15 {'total_num': 10000, 'hit_num': 9817, 'accuracy': 0.9817}
Epoch 16 {'total_num': 10000, 'hit_num': 9815, 'accuracy': 0.9815}
Epoch 17 {'total_num': 10000, 'hit_num': 9835, 'accuracy': 0.9835}
Epoch 18 {'total_num': 10000, 'hit_num': 9826, 'accuracy': 0.9826}
Epoch 19 {'total_num': 10000, 'hit_num': 9819, 'accuracy': 0.9819}
Можно видеть, что точность тестового набора медленно улучшается по мере прохождения обучения, что показывает, что данные действительно передаются и правильно рассчитываются в структуре. Чтобы сравнить эффект, я использовал Tensorflow (1.13.1) для реализации той же структуры сети, применения того же метода инициализации выборки, настройки оптимизатора и т. д. Результаты следующие.
# Tensorflow 1.13.1
Epoch 0 {'total_num': 10000, 'hit_num': 9591, 'accuracy': 0.9591}
Epoch 1 {'total_num': 10000, 'hit_num': 9734, 'accuracy': 0.9734}
Epoch 2 {'total_num': 10000, 'hit_num': 9706, 'accuracy': 0.9706}
Epoch 3 {'total_num': 10000, 'hit_num': 9756, 'accuracy': 0.9756}
Epoch 4 {'total_num': 10000, 'hit_num': 9722, 'accuracy': 0.9722}
Epoch 5 {'total_num': 10000, 'hit_num': 9772, 'accuracy': 0.9772}
Epoch 6 {'total_num': 10000, 'hit_num': 9774, 'accuracy': 0.9774}
Epoch 7 {'total_num': 10000, 'hit_num': 9789, 'accuracy': 0.9789}
Epoch 8 {'total_num': 10000, 'hit_num': 9766, 'accuracy': 0.9766}
Epoch 9 {'total_num': 10000, 'hit_num': 9763, 'accuracy': 0.9763}
Epoch 10 {'total_num': 10000, 'hit_num': 9791, 'accuracy': 0.9791}
Epoch 11 {'total_num': 10000, 'hit_num': 9773, 'accuracy': 0.9773}
Epoch 12 {'total_num': 10000, 'hit_num': 9804, 'accuracy': 0.9804}
Epoch 13 {'total_num': 10000, 'hit_num': 9782, 'accuracy': 0.9782}
Epoch 14 {'total_num': 10000, 'hit_num': 9800, 'accuracy': 0.98}
Epoch 15 {'total_num': 10000, 'hit_num': 9837, 'accuracy': 0.9837}
Epoch 16 {'total_num': 10000, 'hit_num': 9811, 'accuracy': 0.9811}
Epoch 17 {'total_num': 10000, 'hit_num': 9793, 'accuracy': 0.9793}
Epoch 18 {'total_num': 10000, 'hit_num': 9818, 'accuracy': 0.9818}
Epoch 19 {'total_num': 10000, 'hit_num': 9811, 'accuracy': 0.9811}
Видно, что эффект от двух неплох, и немного лучше, чем у Tensorflow в одном эксперименте.
Суммировать
Исходный код, связанный с Tinynn, находится здесьrepoвнутри. В настоящее время поддерживает:
- слой: полносвязный слой, слой 2D-свертки, слой 2D-деконволюции, слой MaxPooling, слой Dropout, слой BatchNormalization, слой RNN и функции активации, такие как ReLU, Sigmoid, Tanh, LeakyReLU, SoftPlus
- потери: SigmoidCrossEntropy, SoftmaxCrossEntroy, MSE, MAE, Huber
- оптимизатор: RAam, Adam, SGD, RMSProp, Momentum и другие оптимизаторы, а также добавить динамическую настройку скорости обучения LRScheduler
- Реализованы общие модели, такие как mnist (классификация), nn_paint (регрессия), DQN (обучение с подкреплением), AutoEncoder и DCGAN (неконтролируемая). Видетьtinynn/examples
Есть еще много областей, которые tinynn может продолжать улучшать в связи с тем, что время не истекло (реализация слоя рекуррентной нейронной сети, слоя BatchNorm, оптимизация эффективности вычислений и т. д.), автор будет поддерживать и обновлять в моем Свободное время.
Конечно, tinynn может быть плохим выбором с точки зрения продакшн-приложений, причина в том, что использование python неизбежно приведет к проблемам с производительностью в таких ресурсоемких сценариях, без поддержки GPU, без распределенной поддержки, многие алгоритмы еще не реализованы. реализовано и т. д. Подождите, этот маленький проектОтправной точкой является больше обучения, В процессе проектирования и реализации tinynn автор многому научился, в том числе, как абстрагироваться, как проектировать интерфейсы компонентов, как реализовывать более эффективно, специфические детали алгоритмов и так далее. Для автора написание этого небольшого фреймворка имеет еще одно преимущество, помимо понимания дизайна и реализации фреймворка глубокого обучения: последующие действия могут бытьБыстро реализовать некоторые новые алгоритмы на этом фреймворке, например, в новой статье предлагается новый метод инициализации параметров, новый алгоритм оптимизации и новый дизайн сетевой структуры, которые можно быстро опробовать на этой небольшой платформе.
Если вы также заинтересованы в самостоятельной разработке и внедрении фреймворка глубокого обучения, я надеюсь, что чтение этой статьи поможет вам, и вы можете отправить PR и внести свой код вместе ~ ?
приложение
Потеря кросс-энтропии Softmax и получение градиента
Перекрестная энтропийная потеря при множественной классификации выглядит следующим образом:
в- фактическое значение и значение, предсказанное моделью, соответственно,количество образцов,это количество категорий. Поскольку реальное значение обычно представляет собой однократный вектор (за исключением того, что размерность реальной категории равна 1, все остальные равны 0), поэтому приведенную выше формулу можно упростить как
внастоящая категория,представительобразцыПрогнозируемая вероятность класса. То есть то, что нам нужно рассчитать, - это сумма логарифмов прогнозируемых вероятностей каждого образца в реальной категории, а затем отрицать отрицатель потери кредита энтропии.
Затем выведите, как решить градиент этой потери по отношению к выходным данным модели, используяПредставляет выходные данные модели. В мультиклассификации Softmax обычно используется для нормализации выходных данных сети в распределение вероятности. Выходные данные после Softmax
Подставьте в функцию потерь выше
решатьО выходном вектореградиент , который может бытьРазделены на измерение целевой категориии параметры нецелевой категории. Сначала посмотрите на размер целевой категории
Посмотрите на размер нецелевой категории
Можно видеть, что для измерения целевой категории градиент представляет собой выходную вероятность соответствующей модели измерения минус единица, а для измерения нецелевой категории градиент представляет собой выходную вероятность самого соответствующего измерения.