<jupyter_start><jupyter_text>KerasKeras это высокоуровневая обертка над tensorflow. Но в текущих реализациях библиотеке tf они живут очень рядом. Когда мы собираем свои нейронки мы берем уже готовые слои из keras и добавляем что-то свое, если нам требуется.Но keras можно использовать без явного использования TF пытаясь свести задачу к fit-predict.  SequentialСамое простое, что мы можем сделать это собирать слои последовательно друг за другом - займеся же этим!<jupyter_code>from sklearn.model_selection import train_test_split

import tensorflow as tf
from tensorflow.keras import Sequential, layers as L # подгружаем нужные модули. 
import tensorflow.keras as keras
# в keras лежит несколько наборов данных. Для примера возьмем fashion_mnist - как mnist, но про предметы одежды :)
fashion_mnist = tf.keras.datasets.fashion_mnist

(X_train, y_train), (X_test, y_test) = fashion_mnist.load_data()

X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=10**4, random_state=42)

X_train = X_train/ 255.
X_val = X_val/ 255.
X_test = X_test/ 255.

class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat',
               'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']
# для того, чтобы учить через cross_entropy нам нужно сделать OHE таргетам. И эта функция есть в keras!
from tensorflow.keras.utils import to_categorical

y_train_ohe = to_categorical(y_train)
y_test_ohe = to_categorical(y_test)
y_val_ohe = to_categorical(y_val)

## Первая простая нейронка

tf.random.set_seed(42) # фиксируем random_seed

model = Sequential(name = 'first_try')
model.add(L.Input(shape = (28,28))) # входной нейрон с данными. Его обычно можно опускать, сразу передавая 
# в нейрон размерность. Но Dense ячейки не умеют работать с картинками, поэтому оставляем Input
model.add(L.Flatten()) # разворачиваем картинку в вектор
model.add(L.Dense(100,  kernel_initializer='random_normal',name='First')) # можно именовать и потом брать слои по именам
model.add(L.ReLU()) # добавляем активацию
model.add(L.Dense(10, kernel_initializer = 'random_normal',name='Output'))
model.add(L.Softmax())
opt = keras.optimizers.Adam(learning_rate=1e-4) # так же нам нужно указать оптимайзер
model.compile(optimizer=opt,loss='categorical_crossentropy',
             metrics=["categorical_accuracy"]) # и собрать нашу модель, указав метрики,loss и оптимизатор


history1 = model.fit(X_train,y_train_ohe,batch_size=500,epochs=2,validation_data = (X_val,y_val_ohe))
# и процесс обучения. Задаем количество эпох, размер батча и валидационную часть наших данных

#history1.params
#history1.history
# Эту же модель можно записать чуть в меньшее количество строчек кода
model = Sequential(name = 'first_try')
model.add(L.Input(shape = (28,28))) 
model.add(L.Flatten()) 
model.add(L.Dense(100,  kernel_initializer='random_normal',name='First',activation='relu')) # можно именовать и потом брать слои по именам
model.add(L.Dense(10, kernel_initializer = 'random_normal',name='Output',activation='softmax'))
opt = keras.optimizers.Adam(learning_rate=1e-4) 
model.compile(optimizer=opt,loss='categorical_crossentropy',
             metrics=["categorical_accuracy"]) 

history1 = model.fit(X_train,y_train_ohe,batch_size=500,epochs=2,validation_data = (X_val,y_val_ohe))

# ?L.Dense<jupyter_output><empty_output><jupyter_text>Из приятного - все в таком подходе можно кастомизировать под себя!Но пока продолжим рассмотрение о следующем подходе сборки моделей.Класс Sequential не дает нам вообще никакой гибкости, позволяя набирать слои только последовательно.Что же у нас есть новый герой  - Model.Он позволяет собирать сетки практически любой архитектуры<jupyter_code>from tensorflow.keras import Model  # подгружаем нужные модули. 
init = 'uniform'
act = 'relu'

input_tensor = L.Input(shape=(28, 28)) # задаем вход
x = L.Flatten()(input_tensor)# применение нейрона к входу
x = L.Dense(100, kernel_initializer=init, activation=act)(x) # повторяем всю логику сколько нам надо
x = L.Dense(100, kernel_initializer=init, activation=act)(x)
output_tensor = L.Dense(10, kernel_initializer=init, activation='softmax')(x)

model = keras.Model(input_tensor, output_tensor) # Keras под копотом сам собирает граф.
# Если он может получить из входа выхода то вы великолепны.

model.compile(optimizer=opt,loss='categorical_crossentropy',
             metrics=["categorical_accuracy"]) 
history = model.fit(X_train,y_train_ohe,batch_size=500,epochs=2,validation_data = (X_val,y_val_ohe))<jupyter_output>Train on 50000 samples, validate on 10000 samples
Epoch 1/2
50000/50000 [==============================] - 1s 15us/sample - loss: 1.7141 - categorical_accuracy: 0.4943 - val_loss: 1.1252 - val_categorical_accuracy: 0.6275
Epoch 2/2
50000/50000 [==============================] - 0s 8us/sample - loss: 0.9337 - categorical_accuracy: 0.6636 - val_loss: 0.8333 - val_categorical_accuracy: 0.6795<jupyter_text>Такой подход позволяет делать практически любой гибкости нейронки. Как пример - двухголовая!<jupyter_code>input_1 = L.Input(shape=(28, 28))
input_2 = L.Input(shape=(28, 28))

x1 = L.Flatten()(input_1)
x1 = L.Dense(100, kernel_initializer=init, activation=act)(x1)
x1 = L.Dense(100, kernel_initializer=init, activation=act)(x1)

x2 = L.Flatten()(input_2)
x2 = L.Dense(100, kernel_initializer=init, activation=act)(x2)
x2 = L.Dense(100, kernel_initializer=init, activation=act)(x2)

x = L.concatenate([x1, x2]) # Волшебное слово, которое позволяет нам соеденять несколько потоков наших данных
output = L.Dense(10, kernel_initializer=init, activation='softmax')(x)

model = keras.Model([input_1, input_2], output)
model.summary()
model.compile(optimizer=opt,loss='categorical_crossentropy',
             metrics=["categorical_accuracy"]) 
history = model.fit([X_train,X_train],y_train_ohe,batch_size=500,epochs=2,validation_data = ([X_val,X_val],y_val_ohe))
# нужно для винды, если не видит путь до graphviz
import os
os.environ["PATH"] += os.pathsep + 'C:/Program Files (x86)/Graphviz2.38/bin/'
from tensorflow.keras.utils import plot_model

plot_model(model) # можно нарисовать модельку<jupyter_output>Failed to import pydot. You must install pydot and graphviz for `pydotprint` to work.<jupyter_text>4.2 Несколько выходов и функций потерь<jupyter_code>init = 'uniform'
act = 'relu'

input_tensor = L.Input(shape=(28, 28))

x = L.Flatten()(input_tensor)
x1 = L.Dense(100, kernel_initializer=init, activation=act)(x)
x2 = L.Dense(100, kernel_initializer=init, activation=act)(x)
x3 = L.Dense(100, kernel_initializer=init, activation=act)(x)

output_1 = L.Dense(1, kernel_initializer=init, activation='sigmoid',name='gender')(x1)
output_2 = L.Dense(10, kernel_initializer=init, activation='softmax',name='income')(x2)
output_3 = L.Dense(1, kernel_initializer=init,name='age')(x3)

model = keras.Model(input_tensor, [output_1, output_2, output_3])

model.summary()
# чтобы модель не переобучилась под самую большую функцию потерь
# их можно взвесить
model.compile(optimizer='adam', loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'],
                                    loss_weights=[0.25, 1., 10.])


# если дали выходам имена, можно вот так: 

model.compile(optimizer='adam',

              loss={'age': 'mse',
                    'income': 'categorical_crossentropy',
                    'gender': 'binary_crossentropy'},
                    
              loss_weights={'age': 0.25,
                            'income': 1.,
                            'gender': 10.})<jupyter_output><empty_output><jupyter_text>Помните статью про то, как люди рисовали функции потерь? [Теперь появилась галерея!](https://losslandscape.com/gallery/) Есть подход как skip-connection, он сильно меняет нашу функцию.![](https://i.stack.imgur.com/UDvbg.png)Такую модель нельзя собрать через `Sequence`-стиль, но можно через функциональный стиль. Давайте попробуем сделать это. Заодно посмотрим насколько сильно в нашей ситуации будет меняться траектория обучения. (Сравним обычный 6-ти слойный персептрон и с прокидыванием инфы, например со 2 слоя на 5ый). Ну и также сразу поэксперементируем с функциями активаций и batchnorm.<jupyter_code>### ╰( ͡° ͜ʖ ͡° )つ──☆*:・ﾟ  создайте сеть по инструкциям ниже
tf.random.set_seed(42)
## Соберите обычный 6ти слойный перспептрон


## Соберите обычный 6ти слойный перспептрон с функциями активации сигмоид

## Соберите с батчнормом и релу

## Соберите ну и наконец прокидывание данных

# Функция для удобной отрисовки всего

def plot_history(histories, key='loss', start=0):
    plt.figure(figsize=(16,10))

    for name, history in histories:
        val = plt.plot(history.epoch[start:], history.history['val_'+key][start:],
                       #'--', 
                       label=name.title()+' Val')
            #plt.plot(history.epoch[start:], history.history[key][start:], color=val[0].get_color(),
            #     label=name.title()+' Train')

    plt.xlabel('Epochs')
    plt.ylabel(key.replace('_',' ').title())
    plt.legend()

    plt.xlim([start, max(history.epoch)])

plot_history([('simple',    history_simple), 
              ('simple_sigmoid', history_simple_sigmoid),
              ('simple_BN_and_init', history_simple_BN_and_init),
              ('complex!!!',   history_complex)
             ],
             start=0)<jupyter_output><empty_output><jupyter_text>Более интересная часть - кастомизацияВ keras уже есть несколько удобных callback.<jupyter_code>from tensorflow.keras.callbacks import EarlyStopping,LearningRateScheduler,ReduceLROnPlateau,ModelCheckpoint
EarlyStopping # останавливает обучение если наша метрика не меняет n эпох
LearningRateScheduler # меняет наш learning_rate по расписанию
ReduceLROnPlateau # понижает на LR если не происходит улучшения
ModelCheckpoint # сохраняет нашу лучшую модель
early_stop = EarlyStopping(patience=3)
reduce_on_plateau = ReduceLROnPlateau(patience=3)
# filepath="checkpoint_path/weights-improvement-{epoch:02d}-{val_categorical_accuracy:.2f}.hdf5"
filepath="checkpoint_path/weights-improvement.hdf5"
model_checkpoing = ModelCheckpoint(filepath,
                                   save_best_only=True,
                                  save_weights_only=True)
def create_simple_model():
    model = Sequential(name = 'simple_model')
    model.add(L.Input(shape = (28,28))) 
    model.add(L.Flatten()) 
    model.add(L.Dense(100,  kernel_initializer='random_normal',name='First',activation='relu'))
    model.add(L.Dense(100,  kernel_initializer='random_normal',name='Second',activation='relu'))
    model.add(L.Dense(10, kernel_initializer = 'random_normal',name='Output',activation='softmax'))
    opt = keras.optimizers.Adam(learning_rate=1e-4) 
    model.compile(optimizer=opt,loss='categorical_crossentropy',
                 metrics=["categorical_accuracy"]) 
    return model


model = create_simple_model()

history = model.fit(X_train,y_train_ohe,batch_size=500,epochs=20,
                     validation_data = (X_val,y_val_ohe),
                    callbacks = [early_stop,reduce_on_plateau,model_checkpoing],verbose=0)

model.load_weights('checkpoint_path/weights-improvement.hdf5')
filepath="checkpoint_path/full_model_improvement.hdf5"
model_checkpoing = ModelCheckpoint(filepath,
                                   save_best_only=True,
                                  save_weights_only=False)
simple_model = create_simple_model()

history = simple_model.fit(X_train,y_train_ohe,batch_size=500,epochs=20,
                     validation_data = (X_val,y_val_ohe),
                    callbacks = [early_stop,reduce_on_plateau,model_checkpoing],verbose=0)

simple_model = keras.models.load_model(filepath)
simple_model.evaluate(x=X_val,y=y_val_ohe)
from tensorflow.keras import callbacks
 
class My_Callback(callbacks.Callback):     # Класс My_Callback унаследовал свойства класса Callback
    def on_train_begin(self, logs={}):           # Функция, которая выполняется в начале обучения 
        return
 
    def on_train_end(self, logs={}):             # Функция, которая выполняется в конце обучения 
        return
 
    def on_epoch_begin(self, logs={}):           # В начале каждой эпохи 
        return
 
    def on_epoch_end(self, epoch, logs={}): 
        # В конце каждой эпохи
        return
 
    def on_batch_begin(self, batch, logs={}):    # В начале батча
        return
 
    def on_batch_end(self, batch, logs={}):      # В конце батча 
        return
from tensorflow.keras import callbacks

class Printlogs(callbacks.Callback):

    def on_epoch_end(self, epoch, logs):
        if epoch==10:
            print(logs)
        
simple_model = create_simple_model()
our_callback = Printlogs()
history = simple_model.fit(X_train,y_train_ohe,batch_size=500,epochs=20,
                     validation_data = (X_val,y_val_ohe),
                    callbacks = [our_callback],
                           verbose=0)
import numpy as np
# Напишем изменение скорости обучения
INIT_LR=0.1
# Стратегия для понижения скорости
def lr_scheduler(epoch):
    drop = 0.5
    epochs_drop = 1.0
    lrate = INIT_LR * np.math.pow(drop, np.math.floor((epoch)/epochs_drop))
    return lrate
lrate = LearningRateScheduler(lr_scheduler)
# класс чтобы отслеживать бесчинства
class Print_lr(callbacks.Callback):
    def on_epoch_end(self, epoch, logs):
        print(float(tf.keras.backend.get_value(self.model.optimizer.lr)))
        # чтобы установить свой LR надо указать
        # LR_OUR = ....
        # tf.keras.backend.set_value(self.model.optimizer.lr, LR_OUR)


simple_model = create_simple_model()
history = simple_model.fit(X_train,y_train_ohe,batch_size=500,epochs=20,
                     validation_data = (X_val,y_val_ohe),
                    callbacks = [lrate,Print_lr()],
                           verbose=0)

# Давайте заставим модель посчитать метрики каждую эпоху, как пример
# Считать будем на X_val, y_val<jupyter_output><empty_output><jupyter_text>Кастомные loss и метрикиЧто уже естьhttps://keras.io/api/metrics/https://keras.io/api/losses/<jupyter_code>celsius    = np.array([-40, -10,  0,  8, 15, 22,  38],  dtype=float)
fahrenheit = np.array([-40,  14, 32, 46, 59, 72, 100],  dtype=float)


model = Sequential()
model.add(L.Dense(1))

opt = tf.keras.optimizers.Adam( )

model.compile(loss='mse', optimizer=opt)
model.fit(celsius, fahrenheit,  epochs=3, verbose=1)


def custom_loss_function(y_true, y_pred):
    squared_difference = tf.square(y_true - y_pred)
    return tf.reduce_mean(squared_difference, axis=-1)
%%time
model = Sequential()
model.add(L.Dense(1))

opt = tf.keras.optimizers.Adam( )

model.compile(loss=custom_loss_function, optimizer=opt)
model.fit(celsius, fahrenheit,  epochs=3,verbose=1)


## тоже самое можно делать и с метриками. Также можно следить сразу за несколькими метриками, что бывает полезно.
## Если хотим добавить совсем сложную логику то мы это будем делать через callback<jupyter_output><empty_output><jupyter_text>В этой тетрадке немного поработаем с градусами по цельсию и фаренгейту! Снова попробуем восстановить формулу $$ f = c \times 1.8 + 32 $$<jupyter_code>## возьмем срезы
tf.keras.backend.set_floatx('float64')
model = Sequential()
model.add(L.Dense(1,name='our_neural'))

opt = tf.keras.optimizers.Adam(0.1 )

model.compile(loss='mse', optimizer=opt)
model.fit(celsius, fahrenheit,  epochs=600, verbose=0)


## элементарная задача, но из-за того, что данные не скалированы сходились вечность
our_layer = model.get_layer(name='our_neural')
our_layer.variables
our_layer(np.array(celsius).reshape((7,1))) ## Берем прогнозы от слоя<jupyter_output><empty_output><jupyter_text>Есть понимание, как взять определить в callback на какой эпохе мы получили правильное значение весов? Проверить как ведет себя наша нейронка? (с учетом того, что мы точно знаем формулу) Ну и заодно быстро проверить гипотезу - а поможет ли нам batchnorm в данной ситуации?  Пишем класс нейронки с TF и keras вместе<jupyter_code># транспонировали выборку
x_train = celsius[:,None]
y_train = fahrenheit[:,None]
tf.keras.backend.set_floatx('float64')
class Super_puper_neural_net(Model):
    
    def __init__(self, n_hidden_neurons):
        super(Super_puper_neural_net, self).__init__()
        self.fc1 = L.Dense(n_hidden_neurons, kernel_initializer='glorot_uniform',
                           activation='relu', trainable=True)
        self.fc2 = L.Dense(n_hidden_neurons, kernel_initializer='glorot_uniform',
                           trainable=True)

    def encode(self, x):
        x = self.fc1(x)
        x = self.fc2(x)
        return x
model_super = Super_puper_neural_net(1)
model_super.encode(x_train)
# Ошибка для модели
def mean_square(y_pred, y_true):
    return tf.reduce_mean((y_pred-y_true)**2)

# оптимизатор 
optimizer = tf.optimizers.SGD(learning_rate=0.001)

# процесс оптимизации
def model_train(X, Y):

    # находим loss и пробрасываем градиент
    with tf.GradientTape() as g:
        pred = model_super.encode(X)
        loss = mean_square(pred, Y)

    # Вычисляем градиенты
    gradients = g.gradient(loss, model_super.variables)
    
    # Обновляем веса a и b в ходе одной итерации спуска 
    optimizer.apply_gradients(zip(gradients, model_super.variables))
#Обучение
epochs = 1000 # число эпох 

for i in range(epochs):
    
    # Делаем щаг градиентного спуска 
    model_train(x_train, y_train)
    
    # Каждую сотую итерацию следим за тем, что произошло
    if i%100 == 0:
        y_pred = model_super.encode(x_train)
        loss_val = mean_square(y_pred, y_train)
        print("step: %i, loss: %f" % (i, loss_val))<jupyter_output>step: 0, loss: 2452.378595
step: 100, loss: 1908.733339
step: 200, loss: 1512.925791
step: 300, loss: 1224.864457
step: 400, loss: 1015.159668
step: 500, loss: 862.328840
step: 600, loss: 750.747670
step: 700, loss: 669.096943
step: 800, loss: 654.442778
step: 900, loss: 630.576029<jupyter_text>Свой слой на Tensorflow для KerasНовые слои можно писать на основе керасовского класса `Layer`. Если прописать `help(tf.keras.layers.Layer)`, можно почитать про него. Если в кратце, нужно реализовать три части: * Конструктор, в нём мы описываем гиперпараметры * Метод `build`, в которм мы описываем все переменные * Метод `call`, который делает forward pass<jupyter_code>class MyLinear(L.Layer):
    
    # Задаём консруктор 
    def __init__(self, units=32):
        super(MyLinear, self).__init__()  # чтобы коректно унаследовались методы
        self.units = units                # число нейронов

    def build(self, input_shape):
        # add_weight внутри build то же самое что и Variable, но совместимо с Keras
        self.w = self.add_weight(shape=(input_shape[-1], self.units),
                                 initializer='random_normal', 
                                 trainable=True)
        
        self.b = self.add_weight(shape=(self.units,),
                                 initializer='random_normal', 
                                 trainable=True)

    # Применение 
    def call(self, inputs):
        # сразу делаем и линейное преобразование и ReLU (а почему бы и нет)
        return tf.nn.relu(tf.matmul(inputs, self.w) + self.b) 
model_custom = Sequential(name = 'simple_model')
model_custom.add(L.Input(shape = (28,28))) 
model_custom.add(L.Flatten()) 
model_custom.add(L.Dense(100,  kernel_initializer='random_normal',name='First',activation='relu'))
model_custom.add(MyLinear()) ### Самый красивый слой
model_custom.add(L.Dense(10, kernel_initializer = 'random_normal',name='Output',activation='softmax'))
opt = keras.optimizers.Adam(learning_rate=1e-4) 
model_custom.compile(optimizer=opt,loss='categorical_crossentropy',
             metrics=["categorical_accuracy"]) 
history = model_custom.fit(X_train,y_train_ohe,batch_size=500,epochs=20,
                     validation_data = (X_val,y_val_ohe),
                    callbacks = [early_stop,reduce_on_plateau],verbose=1)<jupyter_output>Train on 50000 samples, validate on 10000 samples
Epoch 1/20
50000/50000 [==============================] - 1s 12us/sample - loss: 0.6633 - categorical_accuracy: 0.7723 - val_loss: 0.6594 - val_categorical_accuracy: 0.7683
Epoch 2/20
50000/50000 [==============================] - 1s 12us/sample - loss: 0.6257 - categorical_accuracy: 0.7865 - val_loss: 0.6268 - val_categorical_accuracy: 0.7819
Epoch 3/20
50000/50000 [==============================] - 1s 11us/sample - loss: 0.5967 - categorical_accuracy: 0.7961 - val_loss: 0.6017 - val_categorical_accuracy: 0.7942
Epoch 4/20
50000/50000 [==============================] - 1s 11us/sample - loss: 0.5729 - categorical_accuracy: 0.8044 - val_loss: 0.5803 - val_categorical_accuracy: 0.8012
Epoch 5/20
50000/50000 [==============================] - 1s 12us/sample - loss: 0.5536 - categorical_accuracy: 0.8101 - val_loss: 0.5622 - val_categorical_accuracy: 0.8095
Epoch 6/20
50000/50000 [==============================] - 1s 12us/sample - loss: 0.53[...]<jupyter_text>Ну и нам остался пример, как взять срез модели. Посмотреть прогнозы в середине<jupyter_code>model = create_simple_model()
history = model.fit(X_train,y_train_ohe,batch_size=500,epochs=20,
                     validation_data = (X_val,y_val_ohe),
                    callbacks = [early_stop,reduce_on_plateau],verbose=0)
# Извлекаем выходы верхних 2х слоев
layer_outputs = [layer.output for layer in model.layers[1:3]]
# создаем модель, которая вернет эти выходы с учетом заданнаго входа
activation_model = Model(inputs=model.input, outputs=layer_outputs)
prediction = activation_model.predict(X_val)
prediction
layer_outputs<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11957785113208425, 'fraction_numerical': 0.03421823422753266, 'mean_word_length': 4.245062179956109, 'pattern_counts': {'":': 0, '<': 41, '<?xml version=': 0, '>': 41, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 11, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '11', 'dir': 'jupyter-structured-clean-dedup', 'id': '56841', 'license': 'MIT', 'n_tokens_mistral': 8488, 'n_tokens_neox': 8418, 'n_words': 1617, 'path': 'week07/Keras_tutorial.ipynb', 'repo_name': 'romankop/SiriusDL', 'size': '21509'}