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