Задний план

Предположим, у вас есть набор данных в формате LibSVM, в котором запущены LR и GBDT, и вы хотите быстро узнать влияние DNN. Тогда эта статья для вас.

Хотя популярность исследований и приложений глубокого обучения растет уже много лет, а TensorFlow хорошо известен среди пользователей, специализирующихся на алгоритмах, не все знакомы с этим инструментом. Кроме того, речь не идет о мгновенном построении простой модели DNN на основе личного набора данных, особенно когда набор данных в формате LibSVM. LibSVM — это распространенный формат для машинного обучения, который поддерживается многими инструментами, включая Liblinear, XGBoost, LightGBM, ytk-learn и xlearn. Тем не менее, TensorFlow не предоставил элегантного решения для этого ни официально, ни в частном порядке, что вызвало много неудобств для новых пользователей и досадно, учитывая, что это такой широко используемый инструмент. С этой целью в этой статье представлено полностью проверенное решение (некоторый код), которое, как я считаю, может помочь новым пользователям сэкономить время.

Введение

Код в этой статье можно использовать:

  • Чтобы быстро проверить влияние набора данных в формате LibSVM на модель DNN, сравнить ее с другими линейными моделями или древовидными моделями и изучить ограничения модели.
  • Чтобы уменьшить размерность многомерных объектов. Вывод первого скрытого слоя можно использовать как встраивание и добавлять в другие обучающие процессы.
  • Чтобы начать работу с TensorFlow Keras, Estimator и набором данных для начинающих.

Кодирование следует следующим принципам:

  • Вместо того, чтобы разрабатывать свой собственный код, вы должны попытаться использовать официальный или другой код, признанный наиболее производительным, если в этом нет необходимости.
  • Код должен быть максимально оптимизирован.
  • Преследуется предельная временная и пространственная сложность.

В этой статье представлен только самый простой код оценки обучения многоклассовой классификации DNN. Более продвинутые сложные модели см. в выдающихся проектах с открытым исходным кодом, таких как DeepCTR. В последующих статьях будет рассказано о применении этих сложных моделей в практических исследованиях.

Ниже приведены четыре расширенных кода и концепции TensorFlow для обучения DNN для данных в формате LibSVM. Последние два рекомендуются.

Генератор Кераса

Вот три варианта:

  • TensorFlow API: создание модели DNN с использованием TensorFlow не составит труда для опытных пользователей. Модель DNN можно построить сразу с помощью низкоуровневого API, но код будет несколько запутанным. Напротив, высокоуровневый API Keras более «внимателен», а код чрезвычайно оптимизирован и понятен с первого взгляда.
  • Чтение данных в формате LibSVM: легко написать код, считывающий данные в формате LibSVM. Вы можете просто преобразовать разреженное кодирование в плотное кодирование. Однако, поскольку sklearn уже предоставил файл load_svmlight_file, почему бы не использовать его? Эта функция будет считывать весь файл в память, что возможно для небольших объемов данных.
  • fit и fit_generator: обучение модели Keras получает только плотное кодирование, в то время как LibSVM использует разреженное кодирование. Если набор данных не слишком велик, его можно считать в память через load_svmlight_file. Однако, если вы преобразуете все данные в плотное кодирование, а затем подадите их по размеру, память может дать сбой. Идеальное решение — считывать данные в память по требованию, а затем преобразовывать их. Здесь для удобства все данные считываются в память с помощью load_svmlight_file, и сохраняются в разреженном коде. И затем он передается в fit_generator партиями во время использования.

Код выглядит следующим образом:

import numpy as np
from sklearn.datasets import load_svmlight_file
from tensorflow import keras
import tensorflow as tf
feature_len = 100000 # 特征维度,下面使用时可替换成 X_train.shape[1]
n_epochs = 1
batch_size = 256
train_file_path = './data/train_libsvm.txt'
test_file_path = './data/test_libsvm.txt'
def batch_generator(X_data, y_data, batch_size):
    number_of_batches = X_data.shape[0]/batch_size
    counter=0
    index = np.arange(np.shape(y_data)[0])
    while True:
        index_batch = index[batch_size*counter:batch_size*(counter+1)]
        X_batch = X_data[index_batch,:].todense()
        y_batch = y_data[index_batch]
        counter += 1
        yield np.array(X_batch),y_batch
        if (counter > number_of_batches):
            counter=0
def create_keras_model(feature_len):
    model = keras.Sequential([
        # 可在此添加隐层
        keras.layers.Dense(64, input_shape=[feature_len], activation=tf.nn.tanh),
        keras.layers.Dense(6, activation=tf.nn.softmax)
    ])
    model.compile(optimizer=tf.train.AdamOptimizer(),
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
    return model
if __name__ == "__main__":
    X_train, y_train = load_svmlight_file(train_file_path)
    X_test, y_test = load_svmlight_file(test_file_path)
    keras_model = create_keras_model(X_train.shape[1])
    keras_model.fit_generator(generator=batch_generator(X_train, y_train, batch_size = batch_size),
                    steps_per_epoch=int(X_train.shape[0]/batch_size),
                    epochs=n_epochs)
    
    test_loss, test_acc = keras_model.evaluate_generator(generator=batch_generator(X_test, y_test, batch_size = batch_size),
                    steps=int(X_test.shape[0]/batch_size))
    print('Test accuracy:', test_acc)

Приведенный выше код — это код, использовавшийся ранее в практических исследованиях. На тот момент он смог выполнить обучающую задачу, но недостатки кода очевидны. С одной стороны, сложность пространства низкая. Резидентная память больших данных повлияет на другие процессы. Когда дело доходит до больших наборов данных, это неэффективно. С другой стороны, доступность плохая. Пакетный_генератор необходимо вручную закодировать для реализации пакета данных, что отнимает много времени и подвержено ошибкам.

Набор данных TensorFlow — идеальное решение. Однако я не был знаком с набором данных и не знал, как использовать низкоуровневый API TF для разбора LibSVM и преобразования SparseTensor в DenseTensor, поэтому я отложил его из-за ограниченного времени на тот момент, и проблема была решена позже. Ключевым моментом является функция decode_libsvm в следующем коде.

После преобразования данных в формате LibSVM в набор данных DNN разблокируется и может свободно работать с любым большим набором данных.

Далее, в свою очередь, описывается применение набора данных в модели Keras, Keras для оценки и DNNClassifier.

Ниже приведен код внедрения. Вывод первого скрытого слоя используется как Embedding:

def save_output_file(output_array, filename):
    result = list()
    for row_data in output_array:
        line = ','.join([str(x) for x in row_data.tolist()])
        result.append(line)
    with open(filename,'w') as fw:
        fw.write('%s' % '\n'.join(result))
        
X_test, y_test = load_svmlight_file("./data/test_libsvm.txt")
model = load_model('./dnn_onelayer_tanh.model')
dense1_layer_model = Model(inputs=model.input, outputs=model.layers[0].output)
dense1_output = dense1_layer_model.predict(X_test)
save_output_file(dense1_output, './hidden_output/hidden_output_test.txt')

Набор данных Керас

Данные в формате LibSVM, прочитанные load_svmlight_file, заменяются на набор данных, прочитанный decode_libsvm.

import numpy as np
from sklearn.datasets import load_svmlight_file
from tensorflow import keras
import tensorflow as tf
feature_len = 138830
n_epochs = 1
batch_size = 256
train_file_path = './data/train_libsvm.txt'
test_file_path = './data/test_libsvm.txt'
def decode_libsvm(line):
    columns = tf.string_split([line], ' ')
    labels = tf.string_to_number(columns.values[0], out_type=tf.int32)
    labels = tf.reshape(labels,[-1])
    splits = tf.string_split(columns.values[1:], ':')
    id_vals = tf.reshape(splits.values,splits.dense_shape)
    feat_ids, feat_vals = tf.split(id_vals,num_or_size_splits=2,axis=1)
    feat_ids = tf.string_to_number(feat_ids, out_type=tf.int64)
    feat_vals = tf.string_to_number(feat_vals, out_type=tf.float32)
    # 由于 libsvm 特征编码从 1 开始,这里需要将 feat_ids 减 1
    sparse_feature = tf.SparseTensor(feat_ids-1, tf.reshape(feat_vals,[-1]), [feature_len])
    dense_feature = tf.sparse.to_dense(sparse_feature)
    return dense_feature, labels
def create_keras_model():
    model = keras.Sequential([
        keras.layers.Dense(64, input_shape=[feature_len], activation=tf.nn.tanh),
        keras.layers.Dense(6, activation=tf.nn.softmax)
    ])
    model.compile(optimizer=tf.train.AdamOptimizer(),
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
    return model
if __name__ == "__main__":
    dataset_train = tf.data.TextLineDataset([train_file_path]).map(decode_libsvm).batch(batch_size).repeat()
    dataset_test = tf.data.TextLineDataset([test_file_path]).map(decode_libsvm).batch(batch_size).repeat()
    keras_model = create_keras_model()
    sample_size = 10000 # 由于训练函数必须要指定 steps_per_epoch,所以这里需要先获取到样本数
    keras_model.fit(dataset_train, steps_per_epoch=int(sample_size/batch_size), epochs=n_epochs)
    
    test_loss, test_acc = keras_model.evaluate(dataset_test, steps=int(sample_size/batch_size))
    print('Test accuracy:', test_acc)

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

Однако с точки зрения доступности все же существуют два неудобства:

  • Когда используется функция «подгонки» Keras, необходимо указать steps_per_epoch. Чтобы гарантировать, что весь пакет данных заполняется в каждом раунде, размер выборки должен быть рассчитан заранее, что нецелесообразно. Фактически, dataset.repeat может гарантировать, что весь пакет данных заполняется в каждом раунде. Если используется Estimator, указывать steps_per_epoch не нужно.
  • Измерение объекта feature_len необходимо вычислить заранее. LibSVM использует разреженное кодирование, поэтому невозможно определить размер объекта, прочитав только одну или несколько строк данных. Вы можете использовать load_svmlight_file в автономном режиме, чтобы получить измерение функции feature_len=X_train.shape[1], а затем жестко закодировать его в коде. Это неотъемлемая особенность LibSVM. Поэтому это единственный способ справиться с ним.

Модель Кераса для оценки

Еще одним высокоуровневым API TensorFlow является Estimator, который является более гибким. Его автономный код согласуется с распределенным кодом, и базовые аппаратные средства не нужно учитывать, поэтому его можно удобно комбинировать с некоторыми распределенными структурами планирования (такими как xlearning). Кроме того, Estimator, похоже, получает более полную поддержку от TensorFlow, чем от Keras.

Estimator — это высокоуровневый API, независимый от Keras. Если раньше использовался Keras, невозможно за короткое время восстановить все данные в Estimator. TensorFlow также предоставляет интерфейс model_to_estimator для моделей Keras, которые также могут извлечь выгоду из Estimator.

from tensorflow import keras
import tensorflow as tf
from tensorflow.python.platform import tf_logging
# 打开 estimator 日志,可在训练时输出日志,了解进度
tf_logging.set_verbosity('INFO')
feature_len = 100000
n_epochs = 1
batch_size = 256
train_file_path = './data/train_libsvm.txt'
test_file_path = './data/test_libsvm.txt'
# 注意这里多了个参数 input_name,返回值也与上不同
def decode_libsvm(line, input_name):
    columns = tf.string_split([line], ' ')
    labels = tf.string_to_number(columns.values[0], out_type=tf.int32)
    labels = tf.reshape(labels,[-1])
    splits = tf.string_split(columns.values[1:], ':')
    id_vals = tf.reshape(splits.values,splits.dense_shape)
    feat_ids, feat_vals = tf.split(id_vals,num_or_size_splits=2,axis=1)
    feat_ids = tf.string_to_number(feat_ids, out_type=tf.int64)
    feat_vals = tf.string_to_number(feat_vals, out_type=tf.float32)
    sparse_feature = tf.SparseTensor(feat_ids-1, tf.reshape(feat_vals,[-1]),[feature_len])
    dense_feature = tf.sparse.to_dense(sparse_feature)
    return {input_name: dense_feature}, labels
def input_train(input_name):
    # 这里使用 lambda 来给 map 中的 decode_libsvm 函数添加除 line 之的参数
    return tf.data.TextLineDataset([train_file_path]).map(lambda line: decode_libsvm(line, input_name)).batch(batch_size).repeat(n_epochs).make_one_shot_iterator().get_next()
def input_test(input_name):
    return tf.data.TextLineDataset([train_file_path]).map(lambda line: decode_libsvm(line, input_name)).batch(batch_size).make_one_shot_iterator().get_next()
def create_keras_model(feature_len):
    model = keras.Sequential([
        # 可在此添加隐层
        keras.layers.Dense(64, input_shape=[feature_len], activation=tf.nn.tanh),
        keras.layers.Dense(6, activation=tf.nn.softmax)
    ])
    model.compile(optimizer=tf.train.AdamOptimizer(),
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
    return model
def create_keras_estimator():
    model = create_keras_model()
    input_name = model.input_names[0]
    estimator = tf.keras.estimator.model_to_estimator(model)
    return estimator, input_name
if __name__ == "__main__":
    keras_estimator, input_name = create_keras_estimator(feature_len)
    keras_estimator.train(input_fn=lambda:input_train(input_name))
    eval_result = keras_estimator.evaluate(input_fn=lambda:input_train(input_name))
    print(eval_result)

Здесь sample_size не нужно вычислять, но feature_len все равно нужно вычислять заранее. Обратите внимание, что «ключ dict», возвращаемый input_fn Estimator, должен соответствовать входному имени модели. Это значение передается через input_name.

Многие люди используют Keras, и Keras также используется во многих проектах с открытым исходным кодом для создания сложных моделей. Из-за особого формата моделей Keras их нельзя сохранить на некоторых платформах, но эти платформы поддерживают сохранение моделей Estimator. В этом случае очень удобно использовать model_to_estimator для сохранения моделей Keras.

DNNКлассификатор

Наконец, давайте напрямую воспользуемся Estimator, предварительно созданным TensorFlow: DNNClassifier.

import tensorflow as tf
from tensorflow.python.platform import tf_logging
# 打开 estimator 日志,可在训练时输出日志,了解进度
tf_logging.set_verbosity('INFO')
feature_len = 100000
n_epochs = 1
batch_size = 256
train_file_path = './data/train_libsvm.txt'
test_file_path = './data/test_libsvm.txt'
def decode_libsvm(line, input_name):
    columns = tf.string_split([line], ' ')
    labels = tf.string_to_number(columns.values[0], out_type=tf.int32)
    labels = tf.reshape(labels,[-1])
    splits = tf.string_split(columns.values[1:], ':')
    id_vals = tf.reshape(splits.values,splits.dense_shape)
    feat_ids, feat_vals = tf.split(id_vals,num_or_size_splits=2,axis=1)
    feat_ids = tf.string_to_number(feat_ids, out_type=tf.int64)
    feat_vals = tf.string_to_number(feat_vals, out_type=tf.float32)
    sparse_feature = tf.SparseTensor(feat_ids-1,tf.reshape(feat_vals,[-1]),[feature_len])
    dense_feature = tf.sparse.to_dense(sparse_feature)
    return {input_name: dense_feature}, labels
def input_train(input_name):
    return tf.data.TextLineDataset([train_file_path]).map(lambda line: decode_libsvm(line, input_name)).batch(batch_size).repeat(n_epochs).make_one_shot_iterator().get_next()
def input_test(input_name):
    return tf.data.TextLineDataset([train_file_path]).map(lambda line: decode_libsvm(line, input_name)).batch(batch_size).make_one_shot_iterator().get_next()
def create_dnn_estimator():
    input_name = "dense_input"
    feature_columns = tf.feature_column.numeric_column(input_name, shape=[feature_len])
    estimator = tf.estimator.DNNClassifier(hidden_units=[64],
                                           n_classes=6,
                                           feature_columns=[feature_columns])
    return estimator, input_name
if __name__ == "__main__":
    dnn_estimator, input_name = create_dnn_estimator()
    dnn_estimator.train(input_fn=lambda:input_train(input_name))
    eval_result = dnn_estimator.evaluate(input_fn=lambda:input_test(input_name))
    print('\nTest set accuracy: {accuracy:0.3f}\n'.format(**eval_result))

Логика кода Estimator ясна, проста в использовании и очень мощна. Для получения дополнительной информации об Estimator см. официальную документацию.

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

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

Оригинальный источник