Libsvm 数据 DNN 训练—从 Keras 到 Estimator-阿里云开发者社区

开发者社区> 幻大米> 正文

Libsvm 数据 DNN 训练—从 Keras 到 Estimator

简介: ## 背景 手上有个 Libsvm 格式数据集,已经跑过 LR 和 GBDT,想快速看下 DNN 的效果?那本文正适合你。 尽管深度学习研究和应用的热潮已持续高涨多年,TensorFlow 早已为算法同学所熟知,但并非所有人都对这个工具驾轻就熟,要在个人数据集上跑个简易 DNN 模型出来也不是顷刻间的事,特别是当数据集是 Libsvm 格式时。
+关注继续查看

背景

手上有个 Libsvm 格式数据集,已经跑过 LR 和 GBDT,想快速看下 DNN 的效果?那本文正适合你。
尽管深度学习研究和应用的热潮已持续高涨多年,TensorFlow 早已为算法同学所熟知,但并非所有人都对这个工具驾轻就熟,要在个人数据集上跑个简易 DNN 模型出来也不是顷刻间的事,特别是当数据集是 Libsvm 格式时。Libsvm 是机器学习常用格式,很多工具包括 Liblinear、XGBoost、LightGBM、ytk-learn、xlearn 都支持,但 Tensorflow 官方及民间均未见提供优雅的解决方案,这给新手造成了诸多不便,对应用如此广泛的工具来说是个遗憾。对此,本文提供了经过充分验证的解决方案(some code),相信可以帮助新同学节省些时间。

简介

本文代码可用于:

  • 快速验证 Libsvm 数据集在 DNN 上的效果,以与其它线性模型或树模型做对比,探索模型的极限。
  • 对高维特征做降维,可取第一隐层的输出作为 embedding,加入到其它训练过程中。
  • 新手入门,学习 Tensorflow keras、Estimator 和 Dataset 的使用。

本次编码遵循如下原则:

  • 尽量不自己造轮子,尽量用官方的或其它公认性能最好的代码,除非迫不得已。
  • 代码尽量精简。
  • 追求极致的时间复杂度和空间复杂度。

本文只介绍最初级的 DNN 多分类训练评估代码,其它更高阶复杂模型可参考 DeepCTR 等优秀的开源项目,后续会另外发文分享这些复杂模型在实际调研中的应用。

1

下面是 Tensorflow 针对 Libsvm 数据训练 DNN 的四个进阶代码及其思路,推荐使用后两者。

Keras generator

这里面临三个选择:

  • Tensorflow API:要用 Tensorflow 构建个 DNN 模型,对熟手来说很容易,用低阶 API 也能马上建个 DNN,只是代码略显杂乱,相比之下,高阶 API Keras 就贴心得多,代码极度精简,一目了然。
  • Libsvm 数据读取:手写个 Libsvm 格式数据的读取很容易,读取稀疏编码转成稠密编码,但既然 sklearn 已经有 load_svmlight_file 了为什么不用呢,该函数会读进整个文件,当然小数据量不是问题。
  • fit 和 fit_generator:Keras 模型训练只接收稠密编码,而 Libsvm 是稀疏编码,如果数据集不算太大,通过 load_svmlight_file 全部读进内存也能接受,但要先全部转成稠密编码再喂给 fit,那内存可能会爆掉;理想方案是用多少读多少,读进来再转换,此处图省事就先用 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)

以上即早前实际调研中使用的代码,完成当时的训练任务够使了,但该代码的缺点显而易见,一方面空间复杂度太差,大数据常驻内存会影响其它进程,当遇到大数据集时就无能为力了,另一方面可用性差,数据分批需在 batch_generator 手动编码实现,调试耗费时间,也容易出错。
Tensorflow Dataset 是个完美的解决方案,不过由于之前对 Dataset 不熟,也不知道如何用 TF 低阶 API 解析 libsvm 并把 SparseTensor 转成 DenseTensor,当时时间有限就搁置了,后来才解决该问题,重点即下面代码中的 decode_libsvm 函数。
把 libsvm 转成 Dataset 后,DNN 才得到解锁,可以自由运行在任意大数据集上了。
下面依次介绍了 Dataset 应用在 Keras model、Keras to estimator、DNNClassifier。
附 embedding 代码,第一个隐层的输出作为 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')

Keras Dataset

将 libsvm 数据读取从 load_svmlight_file 改成 dataset 并 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 fit 时需指定 steps_per_epoch,为了保证每一轮走完整批数据,需要实现计算 sample size,不合理,其实 dataset 的 repeat 就可以保证,用 Estimator 就没有必须指定 steps_per_epoch 的限制。
  • 需事先计算特征维度 feature_len,由于 libsvm 是稀疏编码,只读取一行或几行无法推断特征维度,可先离线用 load_svmlight_file 获取特征维度 feature_len=X_train.shape[1],然后写死在代码里。这是 libsvm 的固有特点,只能如此处理了。

Keras model to Estimator

Tensorflow 的另一个高阶 API 是 Estimator,更加灵活,据说单机和分布式代码一致,且不用考虑底层的硬件设施,可以比较方便地和一些分布式调度框架(e.g. xlearning)结合使用,在工作中也发现 Estimator 比 Keras 能得到平台更全面的支持。
2
(https://intranetproxy.alipay.com/skylark/lark/0/2019/png/189544/1559021809231-5553af74-6bcf-41ad-94b3-9095059f25a7.png)

Estimator 是跟 Keras 相互独立的高阶 API,如果之前用的是 Keras,一时半会不能全部重构成 Estimator, TF 还提供了 Keras 的 model_to_estimator 接口,也可以享受到 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 还是须事先计算。注意到 Estimator 的 input_fn 返回的 dict key 需要跟 model 的输入名保持一致,这里通过 input_name 传递该值。
用 Keras 的人很多,很多开源项目也用 Keras 来搭建复杂模型,由于 Keras 的模型格式特别,部分平台不支持保存,但提供了对 Estimator 的模型保存支持,这时正好可以使用 model_to_estimator 来保存 Keras 模型,非常方便。

DNNClassifier

最后来直接使用 Tensorflow 预创建的 Estimator:DNNClassifier。
3

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 的更多信息可参考官方文档,这里不再赘述。
以上方案除第一个不便处理大数据,其它均可在单机运行,使用时可根据需求修改网络结构、目标函数等。
本文代码源自一个调研,耗费数小时调试,调研完成代码即闲置,现不计鄙陋,抛砖引玉,希望能对其它同学有所帮助。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
MaxCompute Tunnel SDK数据上传利器——BufferedWriter使用指南
MaxCompute 的数据上传接口(Tunnel)定义了数据 block 的概念:一个 block 对应一个 http request,多个 block 的上传可以并发而且是原子的,一次同步请求要么成功要么失败,不会污染其他的 block。这种设计对于服务端来讲十分简洁,但是也把记录状态做 fa.
5227 0
浙江理工大学-阿里云“数据工程训练营”成功举行
2019年1月21日,浙江理工大学-阿里云“数据工程训练营”结营答辩在浙江理工大学下沙校区成功举行。作为由浙江理工大学与阿里云联合举办的为期10天的大数据实战训练营,举行结营答辩是为了检验40位同学们在集训期间的学习成果。
2001 0
怎么设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程
7435 0
连载:阿里巴巴大数据实践—数据建模综述
数据模型就是数据组织和存储方法,它强调从业务、数据存取和使用角度合理存储数据。
3884 0
DL之RNN:人工智能为你写小说——基于TF利用RNN算法训练数据集(William Shakespeare的《Coriolanus》)替代你写英语小说短文、训练&测试过程全记录
DL之RNN:人工智能为你写小说——基于TF利用RNN算法训练数据集(William Shakespeare的《Coriolanus》)替代你写英语小说短文、训练&测试过程全记录
14 0
DL之NN/Average_Darkness/SVM:手写数字图片识别(本地数据集50000训练集+数据集加4倍)比较3种算法Average_Darkness、SVM、NN各自的准确率
DL之NN/Average_Darkness/SVM:手写数字图片识别(本地数据集50000训练集+数据集加4倍)比较3种算法Average_Darkness、SVM、NN各自的准确率
39 0
使用OpenApi弹性释放和设置云服务器ECS释放
云服务器ECS的一个重要特性就是按需创建资源。您可以在业务高峰期按需弹性的自定义规则进行资源创建,在完成业务计算的时候释放资源。本篇将提供几个Tips帮助您更加容易和自动化的完成云服务器的释放和弹性设置。
8268 0
+关注
1
文章
2
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载