Задний план
Предположим, у вас есть набор данных в формате 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 см. официальную документацию.
Вышеуказанные решения, за исключением первого, которое неудобно обрабатывает большие данные, могут быть запущены на одной машине, а структура сети и целевые функции могут быть изменены по мере необходимости во время использования.
Код в этой статье получен из опроса, отладка которого занимает несколько часов. Код «простаивает» после завершения опроса. Этот код теперь доступен для справки, и я надеюсь, что он может быть полезен другим пользователям.