Transferência de aprendizado: como treinar rapidamente uma rede neural em seus dados

O aprendizado de máquina está se tornando mais acessível, há mais oportunidades de aplicar essa tecnologia usando "componentes prontos para uso". Por exemplo, o Transfer Learning permite que você use a experiência adquirida na solução de um problema para resolver outro problema semelhante. A rede neural é treinada primeiro em uma grande quantidade de dados e depois no conjunto de destino.

Reconhecimento de alimentos

Neste artigo, mostrarei como usar o método Transfer Learning usando o exemplo de reconhecimento de imagens com alimentos. Falarei sobre outras ferramentas de aprendizado de máquina no workshop de Aprendizado de Máquina e Redes Neurais para Desenvolvedores .

Se formos confrontados com a tarefa de reconhecimento de imagem, você pode usar o serviço pronto. No entanto, se você precisar treinar o modelo em seu próprio conjunto de dados, precisará fazer isso sozinho.

Para tarefas típicas como a classificação de imagens, você pode usar a arquitetura pronta (AlexNet, VGG, Inception, ResNet etc.) e treinar a rede neural em seus dados. Já existem implementações dessas redes usando várias estruturas; portanto, nesse estágio, você pode usar uma delas como uma caixa preta, sem se aprofundar no princípio de sua operação.

No entanto, redes neurais profundas estão exigindo grandes quantidades de dados para a convergência da aprendizagem. E, freqüentemente, em nossa tarefa específica, não há dados suficientes para treinar adequadamente todas as camadas da rede neural. O Transfer Learning resolve esse problema.

Transferência de aprendizado para classificação de imagens


As redes neurais usadas para classificação geralmente contêm N neurônios de saída na última camada, onde N é o número de classes. Esse vetor de saída é tratado como um conjunto de probabilidades de pertencer a uma classe. Em nossa tarefa de reconhecer imagens de alimentos, o número de classes pode diferir daquele no conjunto de dados original. Nesse caso, teremos que jogar completamente fora essa última camada e colocar uma nova, com o número certo de neurônios de saída

Transferência de aprendizado

Freqüentemente, no final das redes de classificação, uma camada totalmente conectada é usada. Como substituímos essa camada, o uso de pesos pré-treinados não funcionará. Você terá que treiná-lo do zero, inicializando seus pesos com valores aleatórios. Carregamos pesos para todas as outras camadas a partir de um instantâneo pré-treinado.

Existem várias estratégias para continuar treinando o modelo. Usaremos o seguinte: treinaremos toda a rede de ponta a ponta ( ponta a ponta ) e não corrigiremos os pesos pré-treinados para permitir que eles se ajustem um pouco e se ajustem aos nossos dados. Esse processo é chamado de ajuste fino .

Componentes estruturais


Para resolver o problema, precisamos dos seguintes componentes:

  1. Descrição do modelo de rede neural
  2. Pipeline de aprendizado
  3. Pipeline de interferência
  4. Pesos pré-treinados para este modelo
  5. Dados para treinamento e validação

Componentes

No nosso exemplo, pegarei os componentes (1), (2) e (3) do meu próprio repositório , que contém o código mais leve - você pode facilmente descobrir se quiser. Nosso exemplo será implementado na popular estrutura TensorFlow . Pesos pré-treinados (4) adequados para a estrutura selecionada podem ser encontrados se corresponderem a uma das arquiteturas clássicas. Como um conjunto de dados (5) para demonstração, tomarei o Food-101 .

Modelo


Como modelo, usamos a rede neural VGG clássica (mais precisamente, VGG19 ). Apesar de algumas desvantagens, este modelo demonstra uma qualidade bastante alta. Além disso, é fácil de analisar. No TensorFlow Slim, a descrição do modelo é bastante compacta:

 import tensorflow as tf import tensorflow.contrib.slim as slim def vgg_19(inputs, num_classes, is_training, scope='vgg_19', weight_decay=0.0005): with slim.arg_scope([slim.conv2d], activation_fn=tf.nn.relu, weights_regularizer=slim.l2_regularizer(weight_decay), biases_initializer=tf.zeros_initializer(), padding='SAME'): with tf.variable_scope(scope, 'vgg_19', [inputs]): net = slim.repeat(inputs, 2, slim.conv2d, 64, [3, 3], scope='conv1') net = slim.max_pool2d(net, [2, 2], scope='pool1') net = slim.repeat(net, 2, slim.conv2d, 128, [3, 3], scope='conv2') net = slim.max_pool2d(net, [2, 2], scope='pool2') net = slim.repeat(net, 4, slim.conv2d, 256, [3, 3], scope='conv3') net = slim.max_pool2d(net, [2, 2], scope='pool3') net = slim.repeat(net, 4, slim.conv2d, 512, [3, 3], scope='conv4') net = slim.max_pool2d(net, [2, 2], scope='pool4') net = slim.repeat(net, 4, slim.conv2d, 512, [3, 3], scope='conv5') net = slim.max_pool2d(net, [2, 2], scope='pool5') # Use conv2d instead of fully_connected layers net = slim.conv2d(net, 4096, [7, 7], padding='VALID', scope='fc6') net = slim.dropout(net, 0.5, is_training=is_training, scope='drop6') net = slim.conv2d(net, 4096, [1, 1], scope='fc7') net = slim.dropout(net, 0.5, is_training=is_training, scope='drop7') net = slim.conv2d(net, num_classes, [1, 1], scope='fc8', activation_fn=None) net = tf.squeeze(net, [1, 2], name='fc8/squeezed') return net 

Os pesos para o VGG19, treinados no ImageNet e compatíveis com o TensorFlow, são baixados do repositório no GitHub na seção Modelos pré-treinados .

 mkdir data && cd data wget http://download.tensorflow.org/models/vgg_19_2016_08_28.tar.gz tar -xzf vgg_19_2016_08_28.tar.gz 

Datacet


Como amostra de treinamento e validação, usaremos o conjunto de dados público Food-101 , que contém mais de 100 mil imagens de alimentos, divididas em 101 categorias.

Conjunto de dados Food-101

Faça o download e descompacte o conjunto de dados:

 cd data wget http://data.vision.ee.ethz.ch/cvl/food-101.tar.gz tar -xzf food-101.tar.gz 

O pipeline de dados em nosso treinamento foi projetado para que, a partir do conjunto de dados, precisamos analisar o seguinte:

  1. Lista de aulas (categorias)
  2. Tutorial: uma lista de caminhos para imagens e uma lista de respostas corretas
  3. Conjunto de validação: lista de caminhos para imagens e lista de respostas corretas

Se o seu conjunto de dados, para treinamento e validação, você precisará quebrar os conjuntos por conta própria. O Food-101 já possui essa partição e essas informações são armazenadas no diretório meta .

 DATASET_ROOT = 'data/food-101/' train_data, val_data, classes = data.food101(DATASET_ROOT) num_classes = len(classes) 

Todas as funções auxiliares responsáveis ​​pelo processamento de dados são movidas para um arquivo data.py separado:

data.py
 from os.path import join as opj import tensorflow as tf def parse_ds_subset(img_root, list_fpath, classes): ''' Parse a meta file with image paths and labels -> img_root: path to the root of image folders -> list_fpath: path to the file with the list (eg train.txt) -> classes: list of class names <- (list_of_img_paths, integer_labels) ''' fpaths = [] labels = [] with open(list_fpath, 'r') as f: for line in f: class_name, image_id = line.strip().split('/') fpaths.append(opj(img_root, class_name, image_id+'.jpg')) labels.append(classes.index(class_name)) return fpaths, labels def food101(dataset_root): ''' Get lists of train and validation examples for Food-101 dataset -> dataset_root: root of the Food-101 dataset <- ((train_fpaths, train_labels), (val_fpaths, val_labels), classes) ''' img_root = opj(dataset_root, 'images') train_list_fpath = opj(dataset_root, 'meta', 'train.txt') test_list_fpath = opj(dataset_root, 'meta', 'test.txt') classes_list_fpath = opj(dataset_root, 'meta', 'classes.txt') with open(classes_list_fpath, 'r') as f: classes = [line.strip() for line in f] train_data = parse_ds_subset(img_root, train_list_fpath, classes) val_data = parse_ds_subset(img_root, test_list_fpath, classes) return train_data, val_data, classes def imread_and_crop(fpath, inp_size, margin=0, random_crop=False): ''' Construct TF graph for image preparation: Read the file, crop and resize -> fpath: path to the JPEG image file (TF node) -> inp_size: size of the network input (eg 224) -> margin: cropping margin -> random_crop: perform random crop or central crop <- prepared image (TF node) ''' data = tf.read_file(fpath) img = tf.image.decode_jpeg(data, channels=3) img = tf.image.convert_image_dtype(img, dtype=tf.float32) shape = tf.shape(img) crop_size = tf.minimum(shape[0], shape[1]) - 2 * margin if random_crop: img = tf.random_crop(img, (crop_size, crop_size, 3)) else: # central crop ho = (shape[0] - crop_size) // 2 wo = (shape[0] - crop_size) // 2 img = img[ho:ho+crop_size, wo:wo+crop_size, :] img = tf.image.resize_images(img, (inp_size, inp_size), method=tf.image.ResizeMethod.AREA) return img def train_dataset(data, batch_size, epochs, inp_size, margin): ''' Prepare training data pipeline -> data: (list_of_img_paths, integer_labels) -> batch_size: training batch size -> epochs: number of training epochs -> inp_size: size of the network input (eg 224) -> margin: cropping margin <- (dataset, number_of_train_iterations) ''' num_examples = len(data[0]) iters = (epochs * num_examples) // batch_size def fpath_to_image(fpath, label): img = imread_and_crop(fpath, inp_size, margin, random_crop=True) return img, label dataset = tf.data.Dataset.from_tensor_slices(data) dataset = dataset.shuffle(buffer_size=num_examples) dataset = dataset.map(fpath_to_image) dataset = dataset.repeat(epochs) dataset = dataset.batch(batch_size, drop_remainder=True) return dataset, iters def val_dataset(data, batch_size, inp_size): ''' Prepare validation data pipeline -> data: (list_of_img_paths, integer_labels) -> batch_size: validation batch size -> inp_size: size of the network input (eg 224) <- (dataset, number_of_val_iterations) ''' num_examples = len(data[0]) iters = num_examples // batch_size def fpath_to_image(fpath, label): img = imread_and_crop(fpath, inp_size, 0, random_crop=False) return img, label dataset = tf.data.Dataset.from_tensor_slices(data) dataset = dataset.map(fpath_to_image) dataset = dataset.batch(batch_size, drop_remainder=True) return dataset, iters 


Modelo de treinamento


O código de treinamento do modelo consiste nas seguintes etapas:

  1. Construção de pipelines de dados de trem / validação
  2. Construção de gráficos de trem / validação (redes)
  3. Anexo da função de classificação de perdas ( perda de entropia cruzada ) sobre o gráfico de trens
  4. O código necessário para calcular a precisão das previsões na amostra de validação durante o treinamento
  5. Lógica para carregar balanças pré-treinadas a partir de uma captura instantânea
  6. Criação de várias estruturas para treinamento
  7. O próprio ciclo de aprendizado (otimização iterativa)

A última camada do gráfico é construída com o número necessário de neurônios e é excluída da lista de parâmetros carregados do instantâneo pré-treinado.

Código de treinamento modelo
 import numpy as np import tensorflow as tf import tensorflow.contrib.slim as slim tf.logging.set_verbosity(tf.logging.INFO) import model import data ########################################################### ### Settings ########################################################### INPUT_SIZE = 224 RANDOM_CROP_MARGIN = 10 TRAIN_EPOCHS = 20 TRAIN_BATCH_SIZE = 64 VAL_BATCH_SIZE = 128 LR_START = 0.001 LR_END = LR_START / 1e4 MOMENTUM = 0.9 VGG_PRETRAINED_CKPT = 'data/vgg_19.ckpt' CHECKPOINT_DIR = 'checkpoints/vgg19_food' LOG_LOSS_EVERY = 10 CALC_ACC_EVERY = 500 ########################################################### ### Build training and validation data pipelines ########################################################### train_ds, train_iters = data.train_dataset(train_data, TRAIN_BATCH_SIZE, TRAIN_EPOCHS, INPUT_SIZE, RANDOM_CROP_MARGIN) train_ds_iterator = train_ds.make_one_shot_iterator() train_x, train_y = train_ds_iterator.get_next() val_ds, val_iters = data.val_dataset(val_data, VAL_BATCH_SIZE, INPUT_SIZE) val_ds_iterator = val_ds.make_initializable_iterator() val_x, val_y = val_ds_iterator.get_next() ########################################################### ### Construct training and validation graphs ########################################################### with tf.variable_scope('', reuse=tf.AUTO_REUSE): train_logits = model.vgg_19(train_x, num_classes, is_training=True) val_logits = model.vgg_19(val_x, num_classes, is_training=False) ########################################################### ### Construct training loss ########################################################### loss = tf.losses.sparse_softmax_cross_entropy( labels=train_y, logits=train_logits) tf.summary.scalar('loss', loss) ########################################################### ### Construct validation accuracy ### and related functions ########################################################### def calc_accuracy(sess, val_logits, val_y, val_iters): acc_total = 0.0 acc_denom = 0 for i in range(val_iters): logits, y = sess.run((val_logits, val_y)) y_pred = np.argmax(logits, axis=1) correct = np.count_nonzero(y == y_pred) acc_denom += y_pred.shape[0] acc_total += float(correct) tf.logging.info('Validating batch [{} / {}] correct = {}'.format( i, val_iters, correct)) acc_total /= acc_denom return acc_total def accuracy_summary(sess, acc_value, iteration): acc_summary = tf.Summary() acc_summary.value.add(tag="accuracy", simple_value=acc_value) sess._hooks[1]._summary_writer.add_summary(acc_summary, iteration) ########################################################### ### Define set of VGG variables to restore ### Create the Restorer ### Define init callback (used by monitored session) ########################################################### vars_to_restore = tf.contrib.framework.get_variables_to_restore( exclude=['vgg_19/fc8']) vgg_restorer = tf.train.Saver(vars_to_restore) def init_fn(scaffold, sess): vgg_restorer.restore(sess, VGG_PRETRAINED_CKPT) ########################################################### ### Create various training structures ########################################################### global_step = tf.train.get_or_create_global_step() lr = tf.train.polynomial_decay(LR_START, global_step, train_iters, LR_END) tf.summary.scalar('learning_rate', lr) optimizer = tf.train.MomentumOptimizer(learning_rate=lr, momentum=MOMENTUM) training_op = slim.learning.create_train_op( loss, optimizer, global_step=global_step) scaffold = tf.train.Scaffold(init_fn=init_fn) ########################################################### ### Create monitored session ### Run training loop ########################################################### with tf.train.MonitoredTrainingSession(checkpoint_dir=CHECKPOINT_DIR, save_checkpoint_secs=600, save_summaries_steps=30, scaffold=scaffold) as sess: start_iter = sess.run(global_step) for iteration in range(start_iter, train_iters): # Gradient Descent loss_value = sess.run(training_op) # Loss logging if iteration % LOG_LOSS_EVERY == 0: tf.logging.info('[{} / {}] Loss = {}'.format( iteration, train_iters, loss_value)) # Accuracy logging if iteration % CALC_ACC_EVERY == 0: sess.run(val_ds_iterator.initializer) acc_value = calc_accuracy(sess, val_logits, val_y, val_iters) accuracy_summary(sess, acc_value, iteration) tf.logging.info('[{} / {}] Validation accuracy = {}'.format( iteration, train_iters, acc_value)) 


Após iniciar o treinamento, você pode observar seu progresso usando o utilitário TensorBoard, fornecido com o TensorFlow e serve para visualizar várias métricas e outros parâmetros.

 tensorboard --logdir checkpoints/ 

No final do treinamento no TensorBoard, vemos uma imagem quase perfeita: uma diminuição na perda de trens e um aumento na precisão da validação

Perda e precisão do TensorBoard

Como resultado, obtemos o instantâneo salvo em checkpoints/vgg19_food , que usaremos durante o teste do nosso modelo ( inferência ).

Teste de modelo


Agora teste nosso modelo. Para fazer isso:

  1. Construímos um novo gráfico projetado especificamente para inferência ( is_training=False )
  2. Carregar pesos treinados a partir de uma captura instantânea
  3. Faça o download e pré-processe a imagem de teste de entrada.
  4. Vamos conduzir a imagem através da rede neural e obter a previsão

inference.py
 import sys import numpy as np import imageio from skimage.transform import resize import tensorflow as tf import model ########################################################### ### Settings ########################################################### CLASSES_FPATH = 'data/food-101/meta/labels.txt' INP_SIZE = 224 # Input will be cropped and resized CHECKPOINT_DIR = 'checkpoints/vgg19_food' IMG_FPATH = 'data/food-101/images/bruschetta/3564471.jpg' ########################################################### ### Get all class names ########################################################### with open(CLASSES_FPATH, 'r') as f: classes = [line.strip() for line in f] num_classes = len(classes) ########################################################### ### Construct inference graph ########################################################### x = tf.placeholder(tf.float32, (1, INP_SIZE, INP_SIZE, 3), name='inputs') logits = model.vgg_19(x, num_classes, is_training=False) ########################################################### ### Create TF session and restore from a snapshot ########################################################### sess = tf.Session() snapshot_fpath = tf.train.latest_checkpoint(CHECKPOINT_DIR) restorer = tf.train.Saver() restorer.restore(sess, snapshot_fpath) ########################################################### ### Load and prepare input image ########################################################### def crop_and_resize(img, input_size): crop_size = min(img.shape[0], img.shape[1]) ho = (img.shape[0] - crop_size) // 2 wo = (img.shape[0] - crop_size) // 2 img = img[ho:ho+crop_size, wo:wo+crop_size, :] img = resize(img, (input_size, input_size), order=3, mode='reflect', anti_aliasing=True, preserve_range=True) return img img = imageio.imread(IMG_FPATH) img = img.astype(np.float32) img = crop_and_resize(img, INP_SIZE) img = img[None, ...] ########################################################### ### Run inference ########################################################### out = sess.run(logits, feed_dict={x:img}) pred_class = classes[np.argmax(out)] print('Input: {}'.format(IMG_FPATH)) print('Prediction: {}'.format(pred_class)) 


Inferência

Todo o código, incluindo recursos para criar e executar um contêiner do Docker com todas as versões necessárias das bibliotecas, está neste repositório - no momento da leitura do artigo, o código no repositório pode ter atualizações.

No workshop “Aprendizado de máquina e redes neurais para desenvolvedores” , analisarei outras tarefas do aprendizado de máquina e os alunos apresentarão seus projetos até o final da sessão intensiva.

Source: https://habr.com/ru/post/pt428255/


All Articles