Реализация пользовательской нейронной сети в MNIST с использованием Tensorflow 2.0?

Я пытался написать собственную реализацию базовой нейронной сети с двумя скрытыми слоями в наборе данных MNIST, используя *TensorFlow 2.0 beta* но я не уверен, что здесь пошло не так, но мои потери в тренировке и точность, похоже, застряли на 1,5 и около 85 соответственно. Но если я строю с использованием Keras, я получаю очень низкие потери при тренировках и точность выше 95% всего за 8-10 эпох.

Я считаю, что, может быть, я не обновляю свои веса или что-то? Так нужно ли мне присваивать мои новые веса, которые я вычисляю в спинах функций backprop, их соответствующим переменным веса/смещения?

Я действительно ценю, если кто-то может помочь мне с этим и этими несколькими вопросами, которые я упомянул ниже.

Еще несколько вопросов:

1) Как добавить слой Dropout и Batch Normalization в этой пользовательской реализации? (т.е. заставить его работать как в поезде, так и во время испытаний)

2) Как я могу использовать обратные вызовы в этом коде? то есть (используя обратные вызовы EarlyStopping и ModelCheckpoint)

3) Есть ли в моем коде что-то еще, что я могу оптимизировать в этом коде, например, используя декоратор tenorflow 2.x @tf.function и т.д.)

4) Я бы также потребовал извлечь окончательные веса, которые я получаю для построения и проверки их распределения. Для исследования таких вопросов, как исчезновение или взрыв градиента. (Например: Может быть, Тензорборд)

5) Мне также нужна помощь в написании этого кода в более обобщенном виде, чтобы я мог легко реализовать другие сети, такие как ConvNets (например, Conv, MaxPool и т.д.), На основе этого кода.

Вот мой полный код для легкой воспроизводимости :

Примечание: я знаю, что могу использовать высокоуровневый API, такой как Keras, для создания модели намного проще, но это не моя цель. Пожалуйста, поймите.

import numpy as np
import os
import logging
logging.getLogger('tensorflow').setLevel(logging.ERROR)
import tensorflow as tf
import tensorflow_datasets as tfds

(x_train, y_train), (x_test, y_test) = tfds.load('mnist', split=['train', 'test'], 
                                                  batch_size=-1, as_supervised=True)

# reshaping
x_train = tf.reshape(x_train, shape=(x_train.shape[0], 784))
x_test  = tf.reshape(x_test, shape=(x_test.shape[0], 784))

ds_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))
# rescaling
ds_train = ds_train.map(lambda x, y: (tf.cast(x, tf.float32)/255.0, y))

class Model(object):
    def __init__(self, hidden1_size, hidden2_size, device=None):
        # layer sizes along with input and output
        self.input_size, self.output_size, self.device = 784, 10, device
        self.hidden1_size, self.hidden2_size = hidden1_size, hidden2_size
        self.lr_rate = 1e-03

        # weights initializationg
        self.glorot_init = tf.initializers.glorot_uniform(seed=42)
        # weights b/w input to hidden1 --> 1
        self.w_h1 = tf.Variable(self.glorot_init((self.input_size, self.hidden1_size)))
        # weights b/w hidden1 to hidden2 ---> 2
        self.w_h2 = tf.Variable(self.glorot_init((self.hidden1_size, self.hidden2_size)))
        # weights b/w hidden2 to output ---> 3
        self.w_out = tf.Variable(self.glorot_init((self.hidden2_size, self.output_size)))

        # bias initialization
        self.b1 = tf.Variable(self.glorot_init((self.hidden1_size,)))
        self.b2 = tf.Variable(self.glorot_init((self.hidden2_size,)))
        self.b_out = tf.Variable(self.glorot_init((self.output_size,)))

        self.variables = [self.w_h1, self.b1, self.w_h2, self.b2, self.w_out, self.b_out]


    def feed_forward(self, x):
        if self.device is not None:
            with tf.device('gpu:0' if self.device=='gpu' else 'cpu'):
                # layer1
                self.layer1 = tf.nn.sigmoid(tf.add(tf.matmul(x, self.w_h1), self.b1))
                # layer2
                self.layer2 = tf.nn.sigmoid(tf.add(tf.matmul(self.layer1,
                                                             self.w_h2), self.b2))
                # output layer
                self.output = tf.nn.softmax(tf.add(tf.matmul(self.layer2,
                                                             self.w_out), self.b_out))
        return self.output

    def loss_fn(self, y_pred, y_true):
        self.loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y_true, 
                                                                  logits=y_pred)
        return tf.reduce_mean(self.loss)

    def acc_fn(self, y_pred, y_true):
        y_pred = tf.cast(tf.argmax(y_pred, axis=1), tf.int32)
        y_true = tf.cast(y_true, tf.int32)
        predictions = tf.cast(tf.equal(y_true, y_pred), tf.float32)
        return tf.reduce_mean(predictions)

    def backward_prop(self, batch_xs, batch_ys):
        optimizer = tf.keras.optimizers.Adam(learning_rate=self.lr_rate)
        with tf.GradientTape() as tape:
            predicted = self.feed_forward(batch_xs)
            step_loss = self.loss_fn(predicted, batch_ys)
        grads = tape.gradient(step_loss, self.variables)
        optimizer.apply_gradients(zip(grads, self.variables))

n_shape = x_train.shape[0]
epochs = 20
batch_size = 128

ds_train = ds_train.repeat().shuffle(n_shape).batch(batch_size).prefetch(batch_size)

neural_net = Model(512, 256, 'gpu')

for epoch in range(epochs):
    no_steps = n_shape//batch_size
    avg_loss = 0.
    avg_acc = 0.
    for (batch_xs, batch_ys) in ds_train.take(no_steps):
        preds = neural_net.feed_forward(batch_xs)
        avg_loss += float(neural_net.loss_fn(preds, batch_ys)/no_steps) 
        avg_acc += float(neural_net.acc_fn(preds, batch_ys) /no_steps)
        neural_net.backward_prop(batch_xs, batch_ys)
    print(f'Epoch: {epoch}, Training Loss: {avg_loss}, Training ACC: {avg_acc}')

# output for 10 epochs:
Epoch: 0, Training Loss: 1.7005115111824125, Training ACC: 0.7603832868262543
Epoch: 1, Training Loss: 1.6052448933478445, Training ACC: 0.8524806404020637
Epoch: 2, Training Loss: 1.5905528008006513, Training ACC: 0.8664196092868224
Epoch: 3, Training Loss: 1.584107405738905, Training ACC: 0.8727630912326276
Epoch: 4, Training Loss: 1.5792385798413306, Training ACC: 0.8773203844903037
Epoch: 5, Training Loss: 1.5759121985174716, Training ACC: 0.8804754322627559
Epoch: 6, Training Loss: 1.5739163148682564, Training ACC: 0.8826455712551251
Epoch: 7, Training Loss: 1.5722616605926305, Training ACC: 0.8840812018606812
Epoch: 8, Training Loss: 1.569699136307463, Training ACC: 0.8867688354803249
Epoch: 9, Training Loss: 1.5679460542742163, Training ACC: 0.8885049475356936

Ответ 1

Я задавался вопросом, с чего начать ваш вопрос, и решил сделать это с выражением:

Ваш код определенно не должен выглядеть так, и он далеко не соответствует современным рекомендациям Tensorflow.

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

Теперь перейдем к третьему пункту:

3) Есть ли в моем коде что-то еще, что я могу оптимизировать в этом коде, например, используя декоратор tenorflow 2.x @tf.function и т.д.)

Да, вы можете использовать функциональные возможности tensorflow2.0 и кажется, что вы tf.function от них (декоратор tf.function здесь на самом деле бесполезен, оставьте его пока).

Следование новым правилам также облегчит ваши проблемы с вашим 5-м пунктом, а именно:

5) Мне также нужна помощь в написании этого кода в более обобщенном виде, чтобы я мог легко реализовать другие сети, такие как ConvNets (например, Conv, MaxPool и т.д.), На основе этого кода.

как это было разработано специально для этого. После небольшого введения я попытаюсь познакомить вас с этими концепциями в несколько шагов:

1. Разделите вашу программу на логические части

Tensorflow принес много вреда, когда речь заходит о читабельности кода; все в tf1.x обычно хрустнуло в одном месте, за глобальными значениями следовало определение функций, за которыми следовали другие глобальные переменные или, возможно, загрузка данных, все в беспорядке. Это не совсем вина разработчиков, так как проект системы поощрял эти действия.

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

1.1 Загрузка данных

Вы были на хорошем пути с наборами данных Tensorflow, но отвернулись без видимой причины.

Вот ваш код с комментариями, что происходит:

# You already have tf.data.Dataset objects after load
(x_train, y_train), (x_test, y_test) = tfds.load('mnist', split=['train', 'test'], 
                                                  batch_size=-1, as_supervised=True)

# But you are reshaping them in a strange manner...
x_train = tf.reshape(x_train, shape=(x_train.shape[0], 784))
x_test  = tf.reshape(x_test, shape=(x_test.shape[0], 784))

# And building from slices...
ds_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))
# Unreadable rescaling (there are built-ins for that)

Вы можете легко обобщить эту идею для любого набора данных, поместив его в отдельный модуль, скажем, datasets.py:

import tensorflow as tf
import tensorflow_datasets as tfds


class ImageDatasetCreator:
    @classmethod
    # More portable and readable than dividing by 255
    def _convert_image_dtype(cls, dataset):
        return dataset.map(
            lambda image, label: (
                tf.image.convert_image_dtype(image, tf.float32),
                label,
            )
        )

    def __init__(self, name: str, batch: int, cache: bool = True, split=None):
        # Load dataset, every dataset has default train, test split
        dataset = tfds.load(name, as_supervised=True, split=split)
        # Convert to float range
        try:
            self.train = ImageDatasetCreator._convert_image_dtype(dataset["train"])
            self.test = ImageDatasetCreator._convert_image_dtype(dataset["test"])
        except KeyError as exception:
            raise ValueError(
                f"Dataset {name} does not have train and test, write your own custom dataset handler."
            ) from exception

        if cache:
            self.train = self.train.cache()  # speed things up considerably
            self.test = self.test.cache()

        self.batch: int = batch

    def get_train(self):
        return self.train.shuffle().batch(self.batch).repeat()

    def get_test(self):
        return self.test.batch(self.batch).repeat()

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

from datasets import ImageDatasetCreator

if __name__ == "__main__":
    dataloader = ImageDatasetCreator("mnist", batch=64, cache = True)
    train, test = dataloader.get_train(), dataloader.get_test()

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

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

1.2 Создание модели

Начиная с tf2.0 есть два рекомендуемых способа, в зависимости от сложности моделей:

  • tensorflow.keras.models.Sequential - этот способ показал @Stewart_R, нет необходимости повторять его пункты. Используется для простейших моделей (вы должны использовать это с прямой связью).
  • Наследование tensorflow.keras.Model и написание пользовательской модели. Это следует использовать, когда внутри вашего модуля есть какая-то логика или она более сложная (например, ResNets, многопутевые сети и т.д.). В целом более читабельным и настраиваемым.

Ваш класс Model попытался напомнить что-то подобное, но он снова пошел на юг; backprop определенно не является частью самой модели, не является ни loss ни accuracy, разделите их на другой модуль или функцию, не определяйте как член!

Тем не менее, позвольте кодировать сеть, используя второй подход (для краткости вы должны поместить этот код в model.py). Перед этим я с нуля YourDense слой YourDense связи YourDense, унаследовав его от tf.keras.Layers (этот может пойти в модуль layers.py):

import tensorflow as tf

class YourDense(tf.keras.layers.Layer):
    def __init__(self, units):
        # It Python 3, you don't have to specify super parents explicitly
        super().__init__()
        self.units = units

    # Use build to create variables, as shape can be inferred from previous layers
    # If you were to create layers in __init__, one would have to provide input_shape
    # (same as it occurs in PyTorch for example)
    def build(self, input_shape):
        # You could use different initializers here as well
        self.kernel = self.add_weight(
            shape=(input_shape[-1], self.units),
            initializer="random_normal",
            trainable=True,
        )
        # You could define bias in __init__ as well as it not input dependent
        self.bias = self.add_weight(shape=(self.units,), initializer="random_normal")
        # Oh, trainable=True is default

    def call(self, inputs):
        # Use overloaded operators instead of tf.add, better readability
        return tf.matmul(inputs, self.kernel) + self.bias

Относительно вашего

1) Как добавить слой Dropout и Batch Normalization в этой пользовательской реализации? (т.е. заставить его работать как в поезде, так и во время испытаний)

Я полагаю, вы хотели бы создать пользовательскую реализацию этих слоев. Если нет, то вы можете просто импортировать from tensorflow.keras.layers import Dropout и использовать его в любом месте, как вы хотите, как указывало @Leevo. Перевернутый отсев с различным поведением во время train и test ниже:

class CustomDropout(layers.Layer):
    def __init__(self, rate, **kwargs):
        super().__init__(**kwargs)
        self.rate = rate

    def call(self, inputs, training=None):
        if training:
            # You could simply create binary mask and multiply here
            return tf.nn.dropout(inputs, rate=self.rate)
        # You would need to multiply by dropout rate if you were to do that
        return inputs

Слои взяты отсюда и изменены, чтобы лучше соответствовать цели демонстрации.

Теперь вы можете создать свою модель окончательно (простая двойная прямая связь):

import tensorflow as tf

from layers import YourDense


class Model(tf.keras.Model):
    def __init__(self):
        super().__init__()
        # Use Sequential here for readability
        self.network = tf.keras.Sequential(
            [YourDense(100), tf.keras.layers.ReLU(), YourDense(10)]
        )

    def call(self, inputs):
        # You can use non-parametric layers inside call as well
        flattened = tf.keras.layers.Flatten()(inputs)
        return self.network(flattened)

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

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

Я думаю, что это соответствует вашему 5-му пункту:

5) Мне также нужна помощь в написании этого кода в более обобщенном виде, чтобы я мог легко реализовать другие сети, такие как ConvNets (например, Conv, MaxPool и т.д.), На основе этого кода.

Последнее, что вам, возможно, придется использовать model.build(shape) для построения графа вашей модели.

model.build((None, 28, 28, 1))

Это будет для 28x28x1 ввода MNIST 28x28x1, где None обозначает пакет.

1.3 Обучение

Еще раз, обучение может быть проведено двумя отдельными способами:

  • стандартный model.fit(dataset) - полезен в простых задачах, таких как классификация
  • tf.GradientTape - более сложные обучающие схемы, наиболее ярким примером являются генерирующие состязательные сети, где две модели оптимизируют ортогональные цели в игре minmax

Как еще раз отметили @Leevo, если вы будете использовать второй способ, вы не сможете просто использовать обратные вызовы, предоставляемые Keras, поэтому я бы посоветовал придерживаться первого варианта, когда это возможно.

Теоретически вы можете вызывать функции обратного вызова вручную, например on_batch_begin() и другие, где это необходимо, но это будет громоздко, и я не уверен, как это будет работать.

Когда дело доходит до первого варианта, вы можете использовать объекты tf.data.Dataset напрямую с помощью fit. Вот это внутри другого модуля (предпочтительно train.py):

def train(
    model: tf.keras.Model,
    path: str,
    train: tf.data.Dataset,
    epochs: int,
    steps_per_epoch: int,
    validation: tf.data.Dataset,
    steps_per_validation: int,
    stopping_epochs: int,
    optimizer=tf.optimizers.Adam(),
):
    model.compile(
        optimizer=optimizer,
        # I used logits as output from the last layer, hence this
        loss=tf.losses.SparseCategoricalCrossentropy(from_logits=True),
        metrics=[tf.metrics.SparseCategoricalAccuracy()],
    )

    model.fit(
        train,
        epochs=epochs,
        steps_per_epoch=steps_per_epoch,
        validation_data=validation,
        validation_steps=steps_per_validation,
        callbacks=[
            # Tensorboard logging
            tf.keras.callbacks.TensorBoard(
                pathlib.Path("logs")
                / pathlib.Path(datetime.datetime.now().strftime("%Y%m%d-%H%M%S")),
                histogram_freq=1,
            ),
            # Early stopping with best weights preserving
            tf.keras.callbacks.EarlyStopping(
                monitor="val_sparse_categorical_accuracy",
                patience=stopping_epochs,
                restore_best_weights=True,
            ),
        ],
    )
    model.save(path)

Более сложный подход очень похож (почти копирует и вставляет) на обучающие циклы PyTorch, поэтому, если вы знакомы с ними, они не должны представлять большой проблемы.

Вы можете найти примеры в документации tf2.0, например, здесь или здесь.

2. Другие вещи

2.1 неотвеченные вопросы

4) Есть ли что-то еще в коде, что я могу оптимизировать дальше в этом коде? то есть (с использованием tenorflow 2.x @tf.function decorator и т.д.)

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

Вы получите гораздо больше при правильном кэшировании данных (как описано в начале # 1.1) и хорошем конвейере, а не тех.

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

Как указано выше @Leevo,

weights = model.get_weights()

Получил бы ты весов. Вы можете преобразовать их в np.array и построить график, используя seaborn, matplotlib, анализировать, проверять или что угодно еще.

2.2 В целом

В общем, ваш main.py (или точка входа или что-то подобное) будет состоять из этого (более или менее):

from dataset import ImageDatasetCreator
from model import Model
from train import train

# You could use argparse for things like batch, epochs etc.
if __name__ == "__main__":
    dataloader = ImageDatasetCreator("mnist", batch=64, cache=True)
    train, test = dataloader.get_train(), dataloader.get_test()
    model = Model()
    model.build((None, 28, 28, 1))
    train(
        model, train, path epochs, test, len(train) // batch, len(test) // batch, ...
    )  # provide necessary arguments appropriately
    # Do whatever you want with those
    weights = model.get_weights()

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

3. Вопросы из комментариев

3.1 Как инициализировать пользовательские и встроенные слои

3.1.1 TL;DR, что вы собираетесь прочитать

  • Пользовательская функция инициализации Пуассона, но она принимает три аргумента
  • API tf.keras.initalization требуется два аргумента (см. последний пункт в их документах), поэтому один из них задается через lambda Python внутри пользовательского слоя, который мы написали ранее
  • Добавлен необязательный уклон для слоя, который можно отключить с помощью логического значения

Почему это так бесполезно сложно? Чтобы показать, что в tf2.0 вы, наконец, можете использовать функциональность Python, больше никаких хлопот с графиком, if вместо tf.cond и т.д.

3.1.2 От TL;DR до реализации

Инициализаторы Keras можно найти здесь, а Tensorflow - здесь.

Обратите внимание на несоответствия API (прописные буквы, такие как классы, строчные буквы с функциями подчеркивания, подобные функциям), особенно в tf2.0, но это не tf2.0.

Вы можете использовать их, передавая строку (как это было сделано в YourDense выше) или во время создания объекта.

Чтобы разрешить пользовательскую инициализацию в ваших пользовательских слоях, вы можете просто добавить дополнительный аргумент в конструктор (класс tf.keras.Model по-прежнему является классом Python, и его следует использовать __init__ же, как класс Python).

Перед этим я покажу вам, как создать пользовательскую инициализацию:

# Poisson custom initialization because why not.
def my_dumb_init(shape, lam, dtype=None):
    return tf.squeeze(tf.random.poisson(shape, lam, dtype=dtype))

Обратите внимание, что сигнатура принимает три аргумента, в то время как она должна принимать только (shape, dtype). Тем не менее, это можно легко исправить при создании своего собственного слоя, как YourLinear ниже (расширенный YourLinear):

import typing

import tensorflow as tf


class YourDense(tf.keras.layers.Layer):
    # It still Python, use it as Python, that the point of tf.2.0
    @classmethod
    def register_initialization(cls, initializer):
        # Set defaults if init not provided by user
        if initializer is None:
            # let make the signature proper for init in tf.keras
            return lambda shape, dtype: my_dumb_init(shape, 1, dtype)
        return initializer

    def __init__(
        self,
        units: int,
        bias: bool = True,
        # can be string or callable, some typing info added as well...
        kernel_initializer: typing.Union[str, typing.Callable] = None,
        bias_initializer: typing.Union[str, typing.Callable] = None,
    ):
        super().__init__()
        self.units: int = units
        self.kernel_initializer = YourDense.register_initialization(kernel_initializer)
        if bias:
            self.bias_initializer = YourDense.register_initialization(bias_initializer)
        else:
            self.bias_initializer = None

    def build(self, input_shape):
        # Simply pass your init here
        self.kernel = self.add_weight(
            shape=(input_shape[-1], self.units),
            initializer=self.kernel_initializer,
            trainable=True,
        )
        if self.bias_initializer is not None:
            self.bias = self.add_weight(
                shape=(self.units,), initializer=self.bias_initializer
            )
        else:
            self.bias = None

    def call(self, inputs):
        weights = tf.matmul(inputs, self.kernel)
        if self.bias is not None:
            return weights + self.bias

Я добавил my_dumb_initialization как значение по умолчанию (если пользователь не предоставил его) и сделал необязательным bias аргументом bias. Обратите внимание, вы можете использовать, if свободно, пока она не зависит от данных. Если это так (или как-то зависит от tf.Tensor), нужно использовать декоратор @tf.function который изменяет поток Python на его аналог tensorflow (например, if на tf.cond).

Смотрите здесь, чтобы узнать больше об автографе, за ним очень легко следить.

Если вы хотите включить вышеуказанные изменения инициализатора в вашу модель, вы должны создать соответствующий объект и все.

... # Previous of code Model here
self.network = tf.keras.Sequential(
    [
        YourDense(100, bias=False, kernel_initializer="lecun_uniform"),
        tf.keras.layers.ReLU(),
        YourDense(10, bias_initializer=tf.initializers.Ones()),
    ]
)
... # and the same afterwards

Со встроенными слоями tf.keras.layers.Dense можно делать то же самое (имена аргументов различаются, но идея верна).

3.2 Автоматическое дифференцирование с использованием tf.GradientTape

3.2.1 Введение

tf.GradientTape - предоставить пользователям нормальный поток управления Python и вычисление градиента переменных относительно другой переменной.

Пример взят здесь, но разбит на отдельные части:

def f(x, y):
  output = 1.0
  for i in range(y):
    if i > 1 and i < 5:
      output = tf.multiply(output, x)
  return output

Обычная функция python с операторами for и if

def grad(x, y):
  with tf.GradientTape() as t:
    t.watch(x)
    out = f(x, y)
  return t.gradient(out, x)

Используя градиентную ленту, вы можете записать все операции над Tensors (и их промежуточными состояниями) и "воспроизвести" их в обратном направлении (выполнить автоматическое обратное дифференцирование с использованием правила chaing).

Каждый Tensor в контекстном менеджере tf.GradientTape() записывается автоматически. Если какой-то Tensor находится вне области видимости, используйте метод watch() как показано выше.

Наконец, градиент output по x (ввод возвращается).

3.2.2 Связь с глубоким обучением

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

Давайте продолжим и предположим, что у вас уже установлены ваш tf.keras.Model, экземпляр оптимизатора, tf.data.Dataset и функция потерь.

Можно определить класс Trainer который будет выполнять обучение для нас. Пожалуйста, прочитайте комментарии в коде, если сомневаетесь:

class Trainer:
    def __init__(self, model, optimizer, loss_function):
        self.model = model
        self.loss_function = loss_function
        self.optimizer = optimizer
        # You could pass custom metrics in constructor
        # and adjust train_step and test_step accordingly
        self.train_loss = tf.keras.metrics.Mean(name="train_loss")
        self.test_loss = tf.keras.metrics.Mean(name="train_loss")

    def train_step(self, x, y):
        # Setup tape
        with tf.GradientTape() as tape:
            # Get current predictions of network
            y_pred = self.model(x)
            # Calculate loss generated by predictions
            loss = self.loss_function(y, y_pred)
        # Get gradients of loss w.r.t. EVERY trainable variable (iterable returned)
        gradients = tape.gradient(loss, self.model.trainable_variables)
        # Change trainable variable values according to gradient by applying optimizer policy
        self.optimizer.apply_gradients(zip(gradients, self.model.trainable_variables))
        # Record loss of current step
        self.train_loss(loss)

    def train(self, dataset):
        # For N epochs iterate over dataset and perform train steps each time
        for x, y in dataset:
            self.train_step(x, y)

    def test_step(self, x, y):
        # Record test loss separately
        self.test_loss(self.loss_function(y, self.model(x)))

    def test(self, dataset):
        # Iterate over whole dataset
        for x, y in dataset:
            self.test_step(x, y)

    def __str__(self):
        # You need Python 3.7 with f-string support
        # Just return metrics
        return f"Loss: {self.train_loss.result()}, Test Loss: {self.test_loss.result()}"

Теперь вы можете использовать этот класс в своем коде просто так:

EPOCHS = 5

# model, optimizer, loss defined beforehand
trainer = Trainer(model, optimizer, loss)
for _ in range(EPOCHS):
    trainer.train(train_dataset) # Same for training and test datasets
    trainer.test(test_dataset)
    print(f"Epoch {epoch}: {trainer})")

Печать скажет вам обучение и тестирование потерь для каждой эпохи. Вы можете комбинировать обучение и тестирование любым удобным для вас способом (например, 5 эпох для обучения и 1 тестирование), вы можете добавлять различные метрики и т.д.

Смотрите здесь, если вы хотите не ООП-ориентированный подход (ИМО менее читабелен, но каждому свой).

Ответ 2

Также, если есть что-то, что я мог бы улучшить в коде, дайте мне знать.

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

(x_train, y_train), (x_test, y_test) = tfds.load('mnist', split=['train', 'test'], 
                                                  batch_size=-1, as_supervised=True)

x_train = tf.cast(tf.reshape(x_train, shape=(x_train.shape[0], 784)), tf.float32)
x_test  = tf.cast(tf.reshape(x_test, shape=(x_test.shape[0], 784)), tf.float32)

model = tf.keras.models.Sequential([
  tf.keras.layers.Dense(512, activation='sigmoid'),
  tf.keras.layers.Dense(256, activation='sigmoid'),
  tf.keras.layers.Dense(10, activation='softmax')
])
model.fit(x_train, y_train, epochs=5)
model.evaluate(x_test, y_test)

Ответ 3

Я пытался написать пользовательскую реализацию базовой нейронной сети с двумя скрытыми слоями в наборе данных MNIST с использованием бета-версии tenorflow 2.0, но я не уверен, что здесь пошло не так, но мои потери в тренировке и точность, похоже, застряли на 1,5 и около 85 соответственно.

Где учебная часть? Обучение моделей TF 2.0 с использованием синтаксиса tf.GradientTape() или выполнения Eager с помощью tf.GradientTape(). Можете ли вы вставить код с плотными слоями и как вы его тренировали?


Другие вопросы:

1) Как добавить слой Dropout в этой пользовательской реализации? то есть (заставляя это работать и для поезда и для времени теста)

Вы можете добавить слой Dropout() с помощью:

from tensorflow.keras.layers import Dropout

И затем вы вставляете его в модель Sequential() просто с помощью:

Dropout(dprob)     # where dprob = dropout probability

2) Как добавить Batch Normalization в этот код?

То же, что и раньше, с:

from tensorflow.keras.layers import BatchNormalization

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


3) Как я могу использовать обратные вызовы в этом коде? то есть (используя обратные вызовы EarlyStopping и ModelCheckpoint)

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


4) Есть ли что-то еще в коде, что я могу оптимизировать дальше в этом коде? то есть (с использованием tenorflow 2.x @tf.function decorator и т.д.)

Это зависит. Если вы используете синтаксис Keras, я не думаю, что вам нужно добавлять к нему больше. Если вы тренируете модель в исполнении Eager, я бы посоветовал вам использовать декоратор @tf.function для некоторых функций, чтобы немного ускорить работу. Вы можете увидеть практический пример TF 2.0 о том, как использовать декоратор в этой записной книжке.

Кроме этого, я предлагаю вам поиграть с методами регуляризации, такими как инициализация весов, потеря L1-L2 и т.д.


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

После того, как модель обучена, вы можете извлечь ее вес:

weights = model.get_weights()

или же:

weights = model.trainable_weights

Если вы хотите сохранить только обучаемые.


6) Мне также нужна помощь в написании этого кода в более обобщенном виде, чтобы я мог легко реализовать другие сети, такие как сверточная сеть (например, Conv, MaxPool и т.д.), На основе этого кода.

Тогда вы можете упаковать весь свой код в функцию. В конце этой записной книжки я сделал что-то вроде этого (это для NN с прямой связью, что намного проще, но это начало, и вы можете изменить код в соответствии со своими потребностями).

---

ОБНОВЛЕНИЕ:

Пожалуйста, проверьте мою реализацию TensorFlow 2.0 классификатора CNN. Это может быть полезным советом: он обучается на наборе данных Fashion MNIST, что делает его очень похожим на вашу задачу.