Как тренироваться минимум в 3 раза быстрее. Обучите большую сеть на своем ноутбуке!

(работает во всех глубоких нейронных сетях, особенно хорошо работает при наличии остаточных блоков, таких как большинство сетей НЛП и реснет). Хотя можно было бы ожидать потери точности, но пока этого не наблюдалось).

Основная идея корнета

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

Эта идея была взята ранее Nvida для создания прогрессивного Гана. Но хотя они добавляют слои, все, что находится под ними, остается поддающимся обучению и, следовательно, изменяется. Это помогает сократить время обучения, но можем ли мы сделать (намного) лучше…

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

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

Но верна ли интуитивная концепция?

Давайте проверим стандарт, 14 блоков, resnet для категоризации изображений и используем cifar10. Этот метод очень хорошо работает с архитектурой типа resnet, сетями, в которых мы добавляем результаты из более раннего блока, но, похоже, он хорошо работает во всех глубоких сетях (попробуйте с одной из этих глубоких сетей НЛП!)

во-первых, исходный код в качестве ссылки:

## Four simple functions to make a 2,5,5,2 resnet
# i do not know the original author but you can find versions of this code all over stackexchange.
# whoever made it is one populair man.
# (So to be specific, i do not claim this code to be mine)
#functions extreme self expanitory. But look up resnet if you want to understand this type of net.

def relu_bn(inputs):
    relu = ReLU()(inputs)
    bn = BatchNormalization()(relu)
    return bn



def residual_block(x: Tensor, downsample: bool, filters: int, kernel_size: int = 3,blocknr=1) -> Tensor:
    y = Conv2D(kernel_size=kernel_size,
               strides=(1 if not downsample else 2),
               filters=filters,
               padding="same")(x)
    y = relu_bn(y)
    y = Conv2D(kernel_size=kernel_size,
               strides=1,
               filters=filters,
               padding="same")(y)

    if downsample:
        x = Conv2D(kernel_size=1,
                   strides=2,
                   filters=filters,
                   padding="same"
                  )(x)
    out = Add()([x, y])
    out = relu_bn(out)
    return out

def compile_model(model):
    model.compile(
        optimizer='adam',
        loss='sparse_categorical_crossentropy',
        metrics=['accuracy']
    )
def create_res_net():
    inputs = Input(shape=(32, 32, 3))
    num_filters = 64

    t = BatchNormalization()(inputs)
    t = Conv2D(kernel_size=3,
               strides=1,
               filters=num_filters,
               padding="same")(t)
    t = relu_bn(t)
    num_blocks_list = [2, 5,5, 2]
    for i in range(len(num_blocks_list)):
        num_blocks = num_blocks_list[i]
        for j in range(num_blocks):
            t = residual_block(t, downsample=(j == 0 and i != 0), filters=num_filters)
        num_filters *= 2

    t = AveragePooling2D()(t)
    t = Flatten()(t)
    outputs = Dense(10, activation='softmax')(t)

    model = Model(inputs, outputs)
    compile_model(model)
    return model

Стандартный реснет, взятый из stackexchange (но вы найдете его повсюду).

Чтобы обучить его:

def train_batch(model,epochs=5,batchsize=64):
    """
    Very standard train loop
    :param model: the model
    :param epochs: number of epcos
    :param batchsize: batchsize
    :return:
    """
    model.summary()
    batchnr=int(len(x_train)/batchsize)+1
    start = time.time()
    for epoch in range(epochs):
        for batch in range(batchnr):
            xt=x_train[batch*batchsize:(batch+1)*batchsize]
            yt=y_train[batch*batchsize:(batch+1)*batchsize]

            res=model.train_on_batch(
                xt,
                yt,
                sample_weight=None,
                class_weight=None,
                reset_metrics=False,
                return_dict=False,
            )
            print(epoch+1, '/', epochs, '-', batch+1, '/', batchnr,'--',res[0],res[1],round(time.time()-start))
   model.evaluate(x_test,y_test,verbose=1)
    print("needed", time.time() - start)



#get cifar data (nice and preprocessed)
(x_train, y_train), (x_test, y_test) = cifar10.load_data()

#create our model
model = create_res_net()
#trainit
train_batch(model,epochs=10)

Давайте сначала обучим стандартную архитектуру реснета. См. base_ori (модель) и train_ori. (вы можете просто запустить train_ori) (см. ссылку на github ниже)

Теперь корнет

## Four simple functions to make a 2,5,5,2 resnet
#see base_ori for the original code as found all over stackexchange and other place.
#this if modified to be able to add bloakcs
#we can then train the whole network with added blocks (a progressive resnet) or only the last 2 blocks (a corrective neural net(cornet).
# the term cornet and corrective neural net are mine, see readme. But call it whatevr you like


def relu_bn(inputs):
    """
    Part of teh resnet block. Nothing changed here
    :param inputs:
    :return:
    """
    relu = ReLU()(inputs)
    bn = BatchNormalization()(relu)
    return bn



def residual_block(x, downsample, filters, kernel_size= 3):
    """
    the standard resnetblock. Nothing changed here!
    :param x:
    :param downsample:
    :param filters:
    :param kernel_size:
    :return:
    """
    y = Conv2D(kernel_size=kernel_size,
               strides=(1 if not downsample else 2),
               filters=filters,
               padding="same")(x)
    y = relu_bn(y)
    y = Conv2D(kernel_size=kernel_size,
               strides=1,
               filters=filters,
               padding="same")(y)

    if downsample:
        x = Conv2D(kernel_size=1,
                   strides=2,
                   filters=filters,
                   padding="same"
                  )(x)
    out = Add()([x, y])
    out = relu_bn(out)
    return out

def compile_model(model):
    """
    standard way to compile a keras network...nothing changed here as well!
    :param model:
    :return:
    """
    model.compile(
        optimizer='adam',
        loss='sparse_categorical_crossentropy',
        metrics=['accuracy']
    )


def makeblocklist():
    """
    Make a list of blocks with true/false for downsample and nr of filters. Not all blocks are equal and to keep this experiment meaningfull will stick to the design
    :return:
    """
    blocklist=[]
    num_filters=64
    num_blocks_list = [2, 5, 5, 2]
    for i in range(len(num_blocks_list)):
        num_blocks = num_blocks_list[i]
        for j in range(num_blocks):
           blocklist.append( ((j == 0 and i != 0),num_filters))
        num_filters *= 2
    return blocklist


def addBlock(x,curblock):
    """
    Add a resnet block to the network
    :param x: last layer
    :param curblock: blocknumber
    :return: new network
    """
    bl=makeblocklist()
    x=residual_block(x,downsample=bl[curblock][0],filters=bl[curblock][1])
    return x

def closeblock(t):
    """
    The way to end the network.
    :param t:
    :return: model
    """
    t = AveragePooling2D()(t)
    t = Flatten()(t)
    t = Dense(10, activation='softmax')(t)
    return t

def create_res_net():
    """
    Make a resnet with proper closure but only one residual block. rest added during training
    :return:  model
    """
    inputs = Input(shape=(32, 32, 3))
    num_filters = 64

    t = BatchNormalization()(inputs)
    t = Conv2D(kernel_size=3,
               strides=1,
               filters=num_filters,
               padding="same")(t)
    t = relu_bn(t)
    t=addBlock(t,0)
    outputs=closeblock(t)
    model = Model(inputs, outputs)
    compile_model(model)
    return model

def expand_res_net(model,curblock,trainold=True):
    """
    Add one extra block to the model
    :param model: current model
    :param curblock: current block number
    :param trainold: if yes then all old layers remain trainable, making this a progressive network. If no, only last 2 blocks can be trained. Making it a cornet)
    :return: model
    """
    inp=model.inputs
    for layer in model.layers[:-11]:
        layer.trainable=trainold
    x=model.layers[-4].output
    x=addBlock(x,curblock)
    x=closeblock(x)
    model = Model(inp, x)
    compile_model(model)
    model.summary()
    return model

То же, что и раньше, но теперь добавлена ​​дополнительная логика для добавления блока и последующей перекомпиляции модели. Функция создания теперь делает повторную сеть только с одним остаточным блоком.

Вызов expand_res_net добавляет блок и перекомпилирует сеть. Также всегда повторно добавляется «закрывающий блок». Если trainold=True, это в основном прогрессивная сеть (все блоки в модели остаются обучаемыми). Чтобы действительно увидеть, как он сияет, установите для него значение false. Как в этом обучающем примере:

def train_batch(model,epochs=10,batchsize=64,stepsperupdate=500):
    """
    Stanar training loop, makes graphs of loss en accuracy

    :param model: the model (dah)
    :param epochs: number of epoch
    :param batchsize: batchsize
    :param stepsperupdate: number of steps before putting in a new block (linear here, but in real live better solutions are available)
    :return: absolutely nothing. Just make to print out training data. But can give back to model to work with or inference offcourse
    """
    steps = 1
    block=1
   
    count = 0
    model.summary()
    batchnr=int(len(x_train)/batchsize)+1
    start = time.time()
    for epoch in range(epochs):
        for batch in range(batchnr):
            xt=x_train[batch*batchsize:(batch+1)*batchsize]
            yt=y_train[batch*batchsize:(batch+1)*batchsize]

            res=model.train_on_batch(
                xt,
                yt,
                sample_weight=None,
                class_weight=None,
                reset_metrics=False,
                return_dict=False,
            )
            steps+=1
            if steps>stepsperupdate:
                if block < 13:
                    model=expand_res_net(model,block,trainold=False)
                    block+=1
                    print("expanded!")
                steps=0
            print('\r',epoch+1, '/', epochs, '-', batch+1, '/', batchnr,'--',res[0],res[1],round(time.time()-start),block)
   
    model.evaluate(x_test,y_test,verbose=1)
    print("needed", time.time() - start)


#get cifar data (nice and preprocessed)
(x_train, y_train), (x_test, y_test) = cifar10.load_data()

#create our model
model = create_res_net()
#trainit
train_batch(model,epochs=10)

Все три сети обучались на cifar 10 в течение 10 эпох. Результаты взяты из тестового набора данных.

Корнет и пронет стартуют с 1 ресблока. Новый добавляется каждые 500 пакетов, чтобы закончить 14. То же, что и базовый реснет, который используется в качестве эталона.

NetworkTime за 10 эпох Результаты (проигрыш) результат (акк) реснет

(*) Чтобы попробовать прогрессивную сеть, установите trainold=False на trainold=True в строке 68 train_cornet. В этом корнете количество шагов до добавления нового блока линейное (фиксированное). Это не лучший способ сделать это, но мы все еще работаем над этим.

Время обучения на моем Lenovo ideapad 3 (ryzen 7, gtx 1650). (Люди из Lenovo, если вам нравится это упоминание, не стесняйтесь обращаться ко мне за пожертвованием ;-))

Эта идея/работа моя, код и идеи мои. Базовая реализация keras для реснета и остаточных блоков поступает из Интернета (обмен стеками). Первоначальный автор не знаю.

Если этот корнет уже существует под другим именем и/или имеет библиотеки и т. д., дайте мне знать.

Это помогает мне обучать огромные сети НЛП на моем ноутбуке за долю времени. Так что больше улучшений или советов очень приветствуются.

Ремко Вайнгартен

[email protected] (буду рад, если напишете, сейчас буду писать корнет-библиотеку)

ВЕСЬ КОД НА GITHUB https://github.com/remko66/cornet2

Вот график потерь для корнета. Посмотрите на его характерный узор:

По сравнению с оригинальной моделью реснета: