摘要
使用tensorflow2建立深度学习模型的第一步,往往是将我们加工处理好的pandas的dataframe转化为tensor。按照官方指南的介绍,最佳转化方法是将 pd.DataFrame 转换为 dict,
并对该字典进行切片。本文将结合一个小例子具体描述一下转化方法。
实验说明
我们使用最经典的iris数据集,训练一个多分类器。数据就不再详细描述了,大家应该都熟悉这个数据集。我使用的是tensorflow v2.2 的cpu版本,运行环境就是自己的本了,i7+8G内存。
数据准备
#加载需要的模块
import pandas as pd
import tensorflow as tf
import joblib
import matplotlib.pyplot as plt
import model
from importlib import reload #方便修改model文件后可以立即生效
reload(model)
from model import mymodel
导入数据,为了简便特征名称就记为’a1’,‘a2’,‘a3’,‘a4’吧,类别记为’class’。
df=pd.read_csv('iris.txt',header=None)
df.columns=['a1','a2','a3','a4','class']
我对iris做了一下处理,加了一个离散特征,虽然这个特征造的不自然,但是能更好说明转化方法,毕竟我们希望得到一个比较通用的转化方法,数据集应当同时包含连续数值型特征和离散类别型特征。
def preprocess(df,cate_cols): #cate_cols是我们要编码的特征名称
#对样本类别进行编码
categories = {}
for c in cate_cols:
categories[c] = df[c].unique().tolist()
df[c] = pd.Categorical(df[c],
categories=categories[c]).codes
#我们保存一下离散特征的编码,方便以后使用。
joblib.dump(categories, 'cate_keys')
#将a4特征进行分箱,造一个离散特征
df['a1_bucket'] = pd.cut(df['a1'],
bins=[float('-Inf'),0,5.0,6.0,7.0,8.0,float('Inf')],
labels=[0,1,2,3,4,5])
#preprocess dataset
preprocess(df,cate_cols=['class'])
#target column
target=df.pop('class')
tensorflow2中对所有离散型特征都需要进行编码。
下面进行字典切片。这样在模型输入inputs中只需要指定列名就可以tensor的形式得到该列存储的所有数据,比较方便。
#transform dataframe to tensor
ds=tf.data.Dataset.from_tensor_slices((df.to_dict('list'),target.values))
将ds转成可以迭代的形式,以便进行mini-batch训练。
batch_size=32
epoch_num=100
ds=ds.shuffle(df.shape[0]).batch(batch_size).repeat(epoch_num)
模型训练
深度学习训练模型三板斧:
(1)定义模型结构
(2)定义损失函数
(3)定义优化器
模型结构
模型包含两个fully connected隐藏层和一个softmax输出层,我对离散型特征’a1_bucket’进行了向量嵌入(embedding),所以初始化时我们声明一个嵌入矩阵。
call方法就是定义模型的前向传播过程。在tensorflow中我们只需要将前向传播过程定义清楚,反向传播可以使用tf.GradientTape自动进行计算,非常方便。
call中我使用了tf.gather,从嵌入矩阵中查出对应编码的嵌入向量,所以将离散特征进行编码是十分有必要的。通过tf.concat将嵌入向量和其他维度的连续特征合并成一个长向量送入DNN中。
call的输入inputs就是我们之前得到的dataframe的字典切片了,从代码中可以看到字典切片可以很方便的进行tensor操作,非常灵活,能够胜任各种复杂的模型结构。后面我还会在其他文章中进一步说明。
class mymodel(tf.keras.Model):
def __init__(self,hidden,embed):
super().__init__()
self.dense1=tf.keras.layers.Dense(hidden,activation=tf.nn.relu,name='hidden_layer')
self.dense2 = tf.keras.layers.Dense(hidden, activation=tf.nn.relu, name='hidden_layer2')
self.output1 = tf.keras.layers.Dense(3, activation=tf.nn.softmax, name='output_layer')
self.embed=tf.Variable(tf.random.normal((6,embed)),name='a1_bucket_embed')
def call(self,inputs):
a1_buck_emb=tf.gather(self.embed,inputs['a1_bucket'])
a1=tf.expand_dims(inputs['a1'],axis=1)
a2 = tf.expand_dims(inputs['a2'], axis=1)
a3 = tf.expand_dims(inputs['a3'], axis=1)
a4 = tf.expand_dims(inputs['a4'], axis=1)
i_cct=tf.concat([a1_buck_emb,a1,a2,a3,a4], axis=1)
d1=self.dense1(i_cct)
d2=self.dense2(d1)
return self.output1(d2)
损失函数
对于多分类场景一般就是SparseCategoricalCrossentropy了。
#set loss func
loss=tf.losses.SparseCategoricalCrossentropy()
优化器
我一般使用adam,性能稳定。
#set optimizer
opt=tf.optimizers.Adam(learning_rate=0.01)
除了以上三板斧,我再定义一个metric,看一下模型分类的准确率。
#set evaluation metric
mtc=tf.metrics.SparseCategoricalAccuracy()
训练过程
训练过程就是常规套路了,这里有个坑就是loss()中的label最好定义成float32,不要使用整数型,不过本例中label是整型,训练好像也没有问题。
#record logloss of each step
logloss=[]
for ft,label in ds:
print("Step %d ...."%i)
with tf.GradientTape() as tape:
cur_loss = loss(label, mymodel(ft))
#compute gradient
grad=tape.gradient(cur_loss,mymodel.trainable_variables)
#update parameters
opt.apply_gradients(zip(grad,mymodel.trainable_variables))
mtc.update_state(label,mymodel(ft))
logloss.append(cur_loss)
i+=1
训练结果
print("Accuracy: %.4f"%mtc.result().numpy())
#plot logloss curve
img=plt.figure(figsize=(8,6))
plt.title("Iris Example")
plt.plot(range(len(logloss)),logloss)
plt.legend(['train logloss'])
img.show()
Accuracy: 0.9007
总结
本实验描述了对dataframe建模的过程,通过进行字典切片将dataframe转为tensor,这种处理方式非常灵活,能够适用于复杂的模型结构。最终我们训练了一个包含两层隐藏层的DNN分类器。
后续工作
DNN将输入特征进行高阶组合,却忽视了输入特征的低阶组合。后面我将实现DeepFM,将特征的高阶和低阶组合同时考虑进来,看看分类效果会如何。DeepFM的模型结构相比于这个两层的DNN就更加复杂了,我们看下如何使用字典切片开发DeepFM。
欢迎大家一起记录学习新技术的点点滴滴,共同进步。转载请注明原文出处。