1. 前言

我在上篇博文《特征工程与机器学习在加油卡与车辆号牌关系识别业务上的实践》中着重分享了特征工程实践,就像在业界广泛流传的话一样:数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。

而在深度学习的世界里,特征工程就没有那么重要了,而所谓的特征工程也就是数据预处理,特征识别、分析的任务留给深度学习模型自行完成。例如典型的图像识别,卷积神经网络就做的很好。

以结果导向,直接使用深度学习技术做数据分析模型是不是也很好呢?如何使用深度学习模型进行传统的数据分析呢?

我们接下来,仍以“特征工程与机器学习在加油卡与车辆号牌关系识别业务上的实践”为例,使用深度学习中简单的LetNet-5卷积神经网路模型再实践检验我们的想法。

2. 数据预处理

我们首先参照图像识别方法,为卷积神经网络准备数据,模拟灰度图片的形式,预处理我们待分析数据。

模拟灰度图片(1层),横向为数据特征(字段),纵向为每组数据的行数。本例中为加油卡与车号牌进出站时段内相遇的数据,设定取20次相遇,超出舍弃,不足用“0”补足数据,类似一张图片。

一组数据(一张图)的效果如下:

神经网络分位数回归python 神经网络数据分析步骤_特征工程


对于分类问题,数据预处理过程主要分为三步:

(1)按图片数据矩阵格式构造二维数据;

(2)分离输出分类为OneHot编码;

(3)对构造出来的二维数据进行归一化处理。

数据预处理详细过程,详见如下处理数据代码:

import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler
from DataBase.Car_Info import Gas_Collection

class ComboData(object):
    def __init__(self,datas):
        self.df = datas
        # 统一量纲为小时
        self.df['fuel_time'] = round(self.df['fuel_time']/60,2)         

    def  set_datas(self,times=1):
        self.df.sort_values(by=['IC_ID','fuelle_date'],inplace=True,ascending=[True,True])
        # 类似SQL的having,筛选分组结果
        df_ID = self.df.groupby(['Flag','IC_ID', 'License_plate_R'], as_index=False).filter(lambda x:len(x)>times).groupby(['Flag','IC_ID', 'License_plate_R'], as_index=False).size()
        df_ID = df_ID[['Flag','IC_ID', 'License_plate_R']]
        df_feature = self.df[['Flag','IC_ID','License_plate_R','gas_station','year','month','day','weekday','IC_time','fuel_time','Entry_time','Shop_time','Dep_time']]
        df_feature = df_feature.rename(columns={'License_plate_R':'Lic_plate'})        
        y_df = df_ID[['Flag']]
        self.y_=pd.DataFrame(columns=('TrueIC','FalseIC'))
        self.y_['TrueIC']=y_df['Flag'].apply(lambda x:0 if x==0 else 1)
        self.y_['FalseIC']=y_df['Flag'].apply(lambda x:1 if x==0 else 0)
        self.y_= self.y_.values        
        # 取20个行数据,不足补零
        x_data = []    
        cols_name = ['IC_ID','Lic_plate','gas_station','year','month','day','weekday','IC_time','fuel_time','Entry_time','Shop_time','Dep_time']
        for index,row in df_ID.iterrows():
            IC_ID = row['IC_ID']
            Lic_plate = row['License_plate_R']    
            df_tmp = df_feature[(df_feature['IC_ID']==IC_ID) & (df_feature['Lic_plate']==Lic_plate)][cols_name]                
            num = len(df_tmp)
            if num > 20:
                num = num - 1               
                df_tmp = df_tmp.iloc[(num - 20):num]
            else:
                idx = [i for i in range(20-num)]
                df_null = pd.DataFrame(0,columns=cols_name,index=idx)
                df_null[['IC_ID','Lic_plate']] = IC_ID , Lic_plate
                df_tmp = pd.concat([df_tmp,df_null],axis=0,ignore_index=True).reset_index()
                df_tmp = df_tmp.drop('index',axis=1)            
            a = df_tmp.values
            print(df_tmp)
            a = a.reshape(a.shape[0]*a.shape[1],)
            x_data.append(a)
        
        x_data= np.array(x_data)
        # 归一化处理
        MM = MinMaxScaler()
        self.X = MM.fit_transform(x_data)
        return 
    # 获取返回训练集    
    def get_datas(self):
        return self.X, self.y_ 
           
if __name__ == '__main__':
    GC = Gas_Collection('study')    
    df = GC.get_combo_data()
    CbD = ComboData(df)
    CbD.set_datas(1)   
    pass

3. 卷积模型

由于数据样本量较少,特征较为突出,使用简易的深度学习模型将会达到要求,卷积模型采用最经典的LeNet-5例子,如下图逐层分析各层的参数及连接个数。

神经网络分位数回归python 神经网络数据分析步骤_特征工程_02


输入使用上节产生的数据,20×12数据集,20行加油卡与车号牌相遇数据,每行12个特征(字段),这样就可以使用处理图像的卷积神经网络。

import numpy as np
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import tensorflow as tf
from DataBase.Car_Info import Gas_Collection
from DataBase.DL_ComboData import ComboData

class TensorLetNet5(object):
    def __init__(self):
        return
    # 输入训练数据集并设置训练与测试比例
    def set_Datas(self,x,y,test_size=0.3):
        self.X_ = x
        self.y_= y
        #注意训练集、测试集返回参数顺序
        self.x_train,self.x_test, self.y_train, self.y_test = train_test_split(self.X_,self.y_,test_size=test_size)                    
        return        

    def train_Model_fit(self,num_class,normalization=True):
        #标准差为0.1的正态分布
        def weight_variable(shape,name=None):
            initial = tf.truncated_normal(shape,stddev=0.1)
            return tf.Variable(initial,name=name)        
        #0.1的偏差常数,为了避免死亡节点
        def bias_variable(shape,name=None):
            initial = tf.constant(0.1, shape=shape)
            return tf.Variable(initial,name=name)        
        #二维卷积函数,strides代表卷积模板移动的步长,全是1代表走过所有的值,padding设为SAME意思是保持输入输出的大小一样,使用全0补充
        def conv2d(x,W,name=None):
            return tf.nn.conv2d(x,W,strides=[1,1,1,1],padding='SAME',name=name)        
        #ksize [1, height, width, 1] 第一个和最后一个代表对batches和channel做池化,1代表不池化
        #strides [1, stride,stride, 1]意思是步长为2,我们使用的最大池化
        def max_pool_2x2(x,name=None):
            return tf.nn.max_pool(x,ksize=[1,2,2,1], strides=[1,2,2,1],padding='SAME',name=name)
        #计算卷积后的图像尺寸,其中pool池化为正方形
        def figure_size(height,width,pool,stride,layer):
            for i in range(0,layer):
                height = round((height-pool)/stride) + 1
                width = round((width-pool)/stride) + 1
            return height,width
        # 定义LetNet-5神经网络,输入每组数据220个,输出2个(正确与错误),x_shape为数据形状(任意行,20*11形状,1层通道),学习率0.01
        def LetNet5(in_units=220,out_units=2,x_shape=[-1,20,11,1], learning_rate = 0.01):  #,dropout=True):       
            #x为原始输入数据,即特征向量,None代表可以批量喂入数据
            #y_为对应输入数据的期望输出结果,即真实值
            x = tf.placeholder(tf.float32, [None,in_units],name='x')
            y_ = tf.placeholder(tf.float32, [None,out_units],name='y')
            #reshape图片成-1,20 * 16,1 大小,-1代表样本数量不固定,1代表channel
            x_image=tf.reshape(x,x_shape)
            height = x_shape[1]
            width = x_shape[2]
            print('height: {},width: {}'.format(height,width))
            pool = 2 #池化核2*2 ,见def max_pool_2x2(x,name=None):
            stride = 2 #步长 2
            layer = 2 #两层卷积
            #前面两个3代表卷积核的尺寸,1代表channel,16代表深度
            W_conv1 = weight_variable([3,3,1,16],name='w_conv1')
            b_conv1 = bias_variable([16],name='b_conv1')
            h_conv1 = tf.nn.relu(conv2d(x_image,W_conv1)+b_conv1,name='h_conv1')
            print('h_conv1:{}'.format(h_conv1.shape))
            #第一池化层,2*2最大值池化
            p_conv1 = max_pool_2x2(h_conv1,name='pool_1') 
            print('p_conv1:{}'.format(p_conv1.shape))
            #第二卷积层的卷积核:3x3卷积核滤波,32通道,32个特征映射
            W_conv2 = weight_variable([3,3,16,32], name='w_conv2')
            b_conv2 = bias_variable([32], name='b_conv2')
            h_conv2 = tf.nn.relu(conv2d(p_conv1,W_conv2)+b_conv2, name='h_conv2')
            print('h_conv2:{}'.format(h_conv2.shape))
            p_conv2 = max_pool_2x2(h_conv2, name='pool_2') 
            print('p_conv2:{}'.format(p_conv2.shape))
            #输入为5 * 3 * 32, 输出为480的一维向量(神经元)
            height,width = figure_size(height,width,pool,stride,layer)
            print('height: {},width: {}'.format(height,width))
            W_fc1 = weight_variable([height*width*32,80], name='w_fc1')
            b_fc1 = bias_variable([80], name='b_fc_1')
            #重构reshape第二池化层为1维,接下来要进入全连接层full_connecting
            h_pool1_flat = tf.reshape(p_conv2,[-1,height*width*32], name='pool_1_flat')
            h_fc1 = tf.nn.relu(tf.matmul(h_pool1_flat,W_fc1)+b_fc1, name='h_fc1')            
            keep_prob = tf.placeholder(tf.float32, name='keep_prob')
            h_fc1_drop = tf.nn.dropout(h_fc1,keep_prob, name='h_fc1_drop')            
            #第二全连接层的权重和偏置
            W_fc2 = weight_variable([80,40], name='w_fc2')
            b_fc2 = bias_variable([40], name='b_fc_2')
            h_fc2 = tf.nn.relu(tf.matmul(h_fc1_drop,W_fc2)+b_fc2, name='h_fc2')
            h_fc2_drop = tf.nn.dropout(h_fc2,keep_prob, name='h_fc2_drop')
            #第三全连接,输出层,使用柔性最大值函数softmax作为激活函数
            W_fc3 = weight_variable([40,2], name='w_fc3')
            b_fc3 = bias_variable([2], name='b_fc3')
            y_conv = tf.nn.softmax(tf.matmul(h_fc2_drop,W_fc3) + b_fc3, name='y_')            
            # 使用TensorFlow内置的交叉熵函数避免出现权重消失问题
            cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv), name='cost_func')            
            #使用优化器
            lr = tf.Variable(learning_rate,dtype=tf.float32)
            train_step = tf.train.AdamOptimizer(lr).minimize(cross_entropy)            
            # 正确的预测结果
            correct_pred = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1), name='Correct_pred')
            # 计算预测准确率
            accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32), name='Accuracy')
            return x,y_,cross_entropy,train_step,correct_pred,keep_prob,accuracy,y_conv                  
            
        batch_size = 20        
        # 使用from_tensor_slices将数据放入队列,使用batch和repeat划分数据批次,且让数据序列无限延续
        dataset = tf.data.Dataset.from_tensor_slices((self.x_train, self.y_train))
        dataset = dataset.batch(batch_size).repeat()        
        # 使用生成器make_one_shot_iterator和get_next取数据
        iterator = dataset.make_one_shot_iterator()
        next_iterator = iterator.get_next()        
        #定义神经网络的参数,in_units = 240  #输入12个特征,返回一个正确与错误
        x,y_,loss_,train_step,correct_pred,keep_prob,accuracy,y_conv = LetNet5(in_units=240,out_units=2,x_shape=[-1,20,12,1], learning_rate = 0.001) #[18,10,6,2]),dropout=True)
        log_loss= []
        log_acc = []
        #随机梯度下降算法训练参数
        with tf.Session() as sess:
            sess.run(tf.global_variables_initializer())
        
            for i in range(2000):
                batch_x,batch_y = sess.run(next_iterator)        
                _,loss,acc = sess.run([train_step,loss_,accuracy], feed_dict={x:batch_x,y_:batch_y,keep_prob:0.8})                
                if i%10 == 0:
                    print ("step: {}, total_loss: {},accuracy: {}".format(i, loss,acc))           # 用于趋势回归,预测值
                    log_loss.append(loss)
                    log_acc.append(acc)                                        
            # 训练集验证
            train_ret = sess.run(y_conv, feed_dict={x:self.x_train,keep_prob:1.0})
            train_y = sess.run(tf.argmax(train_ret,1))  # 用于分类问题,取最大概率,返回训练验证结果
            y_train = sess.run(tf.argmax(self.y_train,1))  # 用于分类问题,取最大概率,还原实际数据集(oneHot编码还原)
            train_correct = sess.run(tf.equal(train_y,y_train)) #测试集结果对比(真/假)           
            # 测试集验证
            ret = sess.run(y_conv, feed_dict={x:self.x_test,keep_prob:1.0})
            y = sess.run(tf.argmax(ret,1))  # 用于分类问题,取最大概率,返回测试集验证结果
            y_test = sess.run(tf.argmax(self.y_test,1))  # 用于分类问题,取最大概率 ,还原实际数据集(oneHot编码还原)           
            test_correct = sess.run(tf.equal(y,y_test)) #测试集结果对比(真/假)           
            
            print("测试集预测结果概率:{}".format(ret))
            print("测试集实际数据:{}".format(self.y_test))           
            print("预测结果:{}".format(y))            
            print('测试集:{}'.format(y_test))
            print('测试集准确率:{}'.format(sess.run(tf.reduce_mean(tf.cast(test_correct, tf.float32)))))
            print('训练集准确率:{}'.format(sess.run(tf.reduce_mean(tf.cast(train_correct, tf.float32)))))

        #plt.figure(111)
        fig, ax1 = plt.subplots() # 使用subplots()创建窗口
        plt.rcParams['font.sans-serif']=['SimHei'] #显示中文
        plt.grid()   
        # 设置横坐标刻度标示(最大循环中是2000/10次取数据)
        xx = [0,50,100,150,200]
        labels =['0','500','1000','1500','2000']
        ax1.set_ylabel('logloss')
        ax1.set_xticks(xx)
        ax1.set_xticklabels(labels)
        ax1.plot(log_loss,label = 'train-logloss',color='red')
        ax1.set_xlabel('训练次数')                
        ax2 = ax1.twinx() # 创建第二个坐标轴
        ax2.set_ylabel('correct')
        ax2.plot(log_acc,label = 'train-correct',color='blue')       
        # 显示图例
        ax1.legend(loc='center')
        ax2.legend(loc='center right')
        plt.show()
        return
 
if __name__ == '__main__':  
    GC = Gas_Collection('study')
    df = GC.get_combo_data()
    CbD = ComboData(df)
    CbD.set_datas(times=1)
    x,y = CbD.get_datas()
    TB = TensorLetNet5()
    TB.set_Datas(x,y, 0.3)
    TB.train_Model_fit(2, normalization=False)            
    pass

训练结果如图所示:

神经网络分位数回归python 神经网络数据分析步骤_深度学习_03


我们看模型训练过程,以及测试结果,深度学习效果很好,可以说超出机器学习的效果,怎么超出的、哪些因素影响的,这个模型并没有给出,仅仅给出模型和训练结果。

神经网络分位数回归python 神经网络数据分析步骤_深度学习_04

4. 开发中的小技巧与问题处理

4.1. 数据形状问题

我们在设置卷积神经网络或调参过程中,经常遇到数据(张量)的形状不匹配的问题,例如:

tensorflow.python.framework.errors_impl.InvalidArgumentError: Incompatible shapes: [5] vs. [20]
	 [[node correct_prediction (defined at

通过两种方式避免此类问题发生:

4.1.1. 输出各层卷积、池化的形状

例如上节中的代码:

print('h_conv1:{}'.format(h_conv1.shape))
            #第一池化层,2*2最大值池化
            p_conv1 = max_pool_2x2(h_conv1,name='pool_1') 
            print('p_conv1:{}'.format(p_conv1.shape))

神经网络分位数回归python 神经网络数据分析步骤_Tensorflow_05

4.1.2. 定义自动计算形状参数

一般情况下,池化参数的计算公式为:
设输入图像尺寸为神经网络分位数回归python 神经网络数据分析步骤_深度学习_06,其中神经网络分位数回归python 神经网络数据分析步骤_Tensorflow_07为图像宽,神经网络分位数回归python 神经网络数据分析步骤_深度学习_08为图像高,神经网络分位数回归python 神经网络数据分析步骤_深度学习_09:图像深度(通道数),卷积核的尺寸为神经网络分位数回归python 神经网络数据分析步骤_Tensorflow_10神经网络分位数回归python 神经网络数据分析步骤_深度学习_11为步长,则池化后输出图像大小:

神经网络分位数回归python 神经网络数据分析步骤_特征工程_12
神经网络分位数回归python 神经网络数据分析步骤_深度学习_13

注:本文案例中,卷积核尺寸为2x2,S(步长)= 2。

#计算卷积后的图像尺寸,其中pool池化为正方形
        def figure_size(height,width,pool,stride,layer):
            for i in range(0,layer):
                height = round((height-pool)/stride) + 1
                width = round((width-pool)/stride) + 1
            return height,width

4.2. Tensorflow输入输出参数重名问题

TypeError: Fetch argument 0.5 has invalid type <class 'numpy.float32'>, must be a string
 or Tensor. (Can not convert a float32 into a Tensor or Operation.)

错误出现在如下这行代码:

_,total_loss,accuracy= sess.run([train_step,loss_,accuracy], feed_dict={x:batch_x,y_:batch_y,keep_prob:0.8})

原因是 sess.run()返回的参数“accuracy”,与fetch_list传入的参数名称一致,导致代码第二次运行的时候,将第一次运行得到的值,直接代入,与之前的定义运算冲突。按如下修改即可:

_,total_loss,acc = sess.run([train_step,loss_,accuracy], feed_dict={x:batch_x,y_:batch_y,keep_prob:0.8})

代码基于python3.6、pandas1.1.4、Tensorflow1.13.2版本开发,请注意版本。
源代码参考GitHub/xiaoyw71

5. 总结

特征工程与机器学习、深度学习的关系及比较,我们可以从两个方面总结:

(1)数据分析需求
我们的需求,如果是要分析业务中的影响因素,相关性分析等,具体到特征分析,那么是必须进行特征工程,机器学习上的特征工程方法相对较多[1][2]的分享内容 。
如果我们只是分类识别、趋势预测需求,那么深度学习将更具优势。

(2)技术优势
“深度学习能自动获取特征”是对自动对输入的低阶特征进行组合、变换,得到高阶特征。对于图像处理之类的场景,像素值就可以作为低阶特征输入,组合、变换得到的高阶特征也有比较好的效果,所以看似可以自动获取特征。
而在其他应用场景,例如自然语言处理中,输入的字或词都是离散、稀疏的值,不适合对输入原始数据进行组合、变换得到的高阶特征。而且有的语义并不来自数据,而来自人们的先验知识,所以利用先验知识构造的特征是很有帮助的。所以在深度学习中,原来的特征选择方法仍然适用。

总的来说,特征工程的应用还是要与实际业务场景相结合,同样深度学习能为我们省去了手动构造高阶特征的工作量。