机器学习-增量训练方法

机器学习-增量训练方法

机器学习-增量训练方法

1. 为什么要增量训练

做过机器学习的同学都知道,有时候训练数据是很多的,几十万几百万也是常有的事。虽然几十万几百万只看记录数不算多,但是如果有几百个特征呢,那数据集是很恐怖的,如果存成numpy.float类型,那绝对是把内存吃爆。我就是在这种情况下,开始考虑增量模型的增量训练。现在的机器都很便宜了,为什么不能放在服务器上面执行呢?我也有想过这个问题,但是在shell下操作和Windows下操作的开发测试速度差别很大啊,而且就经验来看,只要把数据准备好了,训练并不会占用太多的时间,就算内存装不下的数据,训练起来也就几分钟。没必要动用大数据集群吧。

在超大数据集上,一般有这么几种方法:1. 对数据进行降维,2. 增量训练,使用流式或类似流式处理,3. 上大机器,高内存的,或者用spark集群。

这里我要说的是增量学习。说到增量训练,其实和在线学习是一个意思,在线学习的典型代表是用SGD优化的logistics regress,先用数据初始化参数,线上来一个数据更新一次参数,虽然时间的推移,效果越来越好。这样就避免了离线更新模型的问题。

好了,说到这里你应该已经明白增量训练的主要用途了吧,主要有两个作用,一个是想办法利用全部的数据,另一个是想办法及时利用新的数据。

2. 特征的增量训练方法

特征的机器学习模型的粮食,对特征的处理同样的增量训练需要面对的问题。如果数据量很大,我们一般倾向于抽样训练,也就是在100万条数据里面抽取10万或者5万条数据做训练模型,多次抽样训练多个模型,然后对多模型的预测结果做加权。这是一个好方法,但是不是最佳的方法。

因为抽样后,数据的分布其实是被破坏了,举个例子,数据的分布是偏态,而且存在异常值,结果抽样后异常值丢失,此时数据不需要做异常值处理,而在下一次抽样因为存在异常值导致需要异常值处理,整个数据流程就会乱糟糟的。因此,需要对整个数据做数据分布分析,对特征的计算和变换应该是基于整个数据分布来,而不是抽样后的10万个样本的数据分布。

我们做特征变换,用到的主要是特征的最小值最大值,均值,方差,变异系数,k阶原点矩,k阶中心距,偏度,峰度等几个基本统计量,因此,我们需要算出每一个特征的这些统计量,在新的数据到来的时候,更新这个统计量,在训练模型之前,用全局统计量更新局部数据(抽样数据),以保证数据整体分布信息能够充分利用。

计算全量数据的基本统计量最大的问题是什么?

前面说了,内存不够啊。有什么解决办法呢?有啊,上大数据集群,spark一跑,全部出来了。

这不是我们这里要说的重点,我们要说的是,在有限的内存上,通过增量训练的方法实现特征的增量训练。核心方法是,如何只遍历一遍数据就能算出这些统计量

这就要参考阿里巴巴杨旭的著作《重构大数据统计》,在第二章中杨旭老师推导了如何只遍历一次数据就能计算大部分基本统计量的方法。下面是基本的基本统计量的计算方法:

n: 数据量sum: 数据加和sum2: 平方和sum3: 立方和sum4: 四次方和

简单求和: {\rm{sum}} = \sum\limits_{}^{} {{X_i}}
平方和:sum2 = \sum\limits_{}^{} {X_i^2}
立方和: sum3 = \sum\limits_{}^{} {{X_i}^3}
四次方和: sum4 = \sum\limits_{}^{} {X_i^4}

均值: \bar X = \frac{1}{n}\sum\limits_{}^{} {{X_i}} = \frac{{sum}}{n}

方差:  {\mathop{\rm var}} = \frac{1}{{n - 1}}\sum\limits_{i = 1}^n {{{({X_i} - \bar X)}^2}} = \frac{1}{{n - 1}}(sum2 - sum*\bar X)

标准差: std = \sqrt {{\mathop{\rm var}} }

变异系数: \frac{{std}}{{\bar X}}

2阶原点矩: \frac{1}{n}\sum\limits_{i = 1}^n {X_i^2} = \frac{{sum2}}{n}

2阶中心距: \frac{1}{n}\sum\limits_{i = 1}^n {{{({X_i} - \bar X)}^2}} = \frac{1}{n}(sum2 - sum*\bar X)

除了上面的基本统计量,最小值最大值 用得也很多,要计算也很简单,不再赘述。

好了,我们已经知道只遍历一遍数据就能计算这些统计量,接下来就是对数据进行分块,每次读取一块,然后进行特征计算,转换,之后将特征送到允许增量计算的模型中就好了。

下面是伪代码:

# 第一步:计算基本统计量的代码
# 每次读取10万行数据

statistic=None

for sub_data in pd.read_csv(file, chunksize=100000):
    sub_statistic = calculate_statistic(sub_data)  # 计算
    update_statistic(sub_statistic)                # 更新

# 第二步:用计算好的基本统计量进行特征转换,计算
for sub_data in pd.read_csv(file, chunksize=100000):
    new_feature = calculate_new_feature(sub_data, statistic)

    # 第三步:用计算好的基本统计量进行模型训练
    model.train_on_batchs(new_feature)

3. 模型的增量训练方法

理论上说,只要能是用SGD或者类似mini_batch SGD进行优化的模型都是可以实现增量训练的,其中以Linear ModelDeep Learning最为典型,下面就简要介绍现成工具的使用吧。

3.1. sklearn中的增量训练

sklearn中提供了很多增量学习算法。虽然不是所有的算法都可以增量学习,但是学习器提供了 partial_fit的函数的都可以进行增量学习。事实上,使用小batch的数据中进行增量学习是这种学习方式的核心,因为它能让任何一段时间内内存中只有少量的数据。
sklearn提供很多增量学习算法:

  • Classification
  • sklearn.naive_bayes.MultinomialNB
  • sklearn.naive_bayes.BernoulliNB
  • sklearn.linear_model.Perceptron
  • sklearn.linear_model.SGDClassifier
  • sklearn.linear_model.PassiveAggressiveClassifier


  • Regression
  • sklearn.linear_model.SGDRegressor
  • sklearn.linear_model.PassiveAggressiveRegressor


  • Clustering
  • sklearn.cluster.MiniBatchKMeans


  • Decomposition / feature Extraction
  • sklearn.decomposition.MiniBatchDictionaryLearning
  • sklearn.decomposition.IncrementalPCA
  • sklearn.decomposition.LatentDirichletAllocation
  • sklearn.cluster.MiniBatchKMeans

对于sklearn的学习方法,我们要做的主要就是实现上面说的特征增量训练的方法即可。

3.2. lightGBM的增量训练方法

lightGBM作为xgboost的有力竞争者,其增量训练用起来也是很方便呢,lightGBM设计之初也考虑到了超大数据集的应用,除了分布式训练支持外,也提供了增量训练的方法。
下面是回归预测的增量训练方法:

# 第一步,初始化模型为None,设置模型参数
gbm=None
params = {
        'task': 'train',
        'application': 'regression',
        'boosting_type': 'gbdt',
        'learning_rate': 0.2,
        'num_leaves': 31,
        'tree_learner': 'serial',
        'min_data_in_leaf': 100,
        'metric': ['l1','l2','rmse'],  # l1:mae, l2:mse
        'max_bin': 255,
        'num_trees': 300
    }
# 第二步,流式读取数据(每次10万)
i=1
for sub_data in pd.read_csv(file, chunksize=100000)
    # 区分特征x和结果Y
    x_data = sub_data[x_cols]
    y_data = sub_data[y_col]

    # 创建lgb的数据集
    lgb_train = lgb.Dataset(x_data, y_data.values)
    lgb_eval = lgb.Dataset(test[x_cols], test[y_col].values, reference=lgb_train)

    # 第三步:增量训练模型
    # 重点来了,通过 init_model 和 keep_training_booster 两个参数实现增量训练
    gbm = lgb.train(params,
                    lgb_train,
                    num_boost_round=1000,
                    valid_sets=lgb_eval,
                    init_model=gbm,             # 如果gbm不为None,那么就是在上次的基础上接着训练
                    feature_name=x_cols,
                    early_stopping_rounds=10,
                    verbose_eval=False,
                    keep_training_booster=True) # 增量训练 

    # 输出模型评估分数
    score_train = dict([(s[1], s[2]) for s in gbm.eval_train()])
    score_valid = dict([(s[1], s[2]) for s in gbm.eval_valid()])
    print('当前模型在训练集的得分是:mae=%.4f, mse=%.4f, rmse=%.4f'%(score_train['l1'], score_train['l2'], score_train['rmse']))
    print('当前模型在测试集的得分是:mae=%.4f, mse=%.4f, rmse=%.4f' % (score_valid['l1'], score_valid['l2'], score_valid['rmse']))
    i += 1

从打印结果看,效果并不理想,也许是特征不给力。在例子中我每次读取10万数据,也许这10万数据已经能代表整个数据集了吧。

[2018.08.06 15:08:30] 第 1 批次数据训练模型
[2018.08.06 15:08:30] 当前模型在训练集的得分是:mae=3.8034, mse=186.6290, rmse=13.6612
[2018.08.06 15:08:30] 当前模型在测试集的得分是:mae=4.1202, mse=376.4796, rmse=19.4031
[2018.08.06 15:08:46] 第 2 批次数据训练模型
[2018.08.06 15:08:46] 当前模型在训练集的得分是:mae=4.0651, mse=346.0248, rmse=18.6017
[2018.08.06 15:08:46] 当前模型在测试集的得分是:mae=4.1143, mse=370.8750, rmse=19.2581
[2018.08.06 15:09:02] 第 3 批次数据训练模型
[2018.08.06 15:09:02] 当前模型在训练集的得分是:mae=3.9073, mse=502.9016, rmse=22.4255
[2018.08.06 15:09:02] 当前模型在测试集的得分是:mae=4.1229, mse=373.6719, rmse=19.3306
[2018.08.06 15:09:19] 第 4 批次数据训练模型
[2018.08.06 15:09:19] 当前模型在训练集的得分是:mae=4.2073, mse=525.9846, rmse=22.9344
[2018.08.06 15:09:19] 当前模型在测试集的得分是:mae=4.1955, mse=415.1820, rmse=20.3760
[2018.08.06 15:09:37] 第 5 批次数据训练模型
[2018.08.06 15:09:37] 当前模型在训练集的得分是:mae=3.8308, mse=208.4748, rmse=14.4387
[2018.08.06 15:09:37] 当前模型在测试集的得分是:mae=4.1798, mse=389.9514, rmse=19.7472
[2018.08.06 15:09:55] 第 6 批次数据训练模型
[2018.08.06 15:09:55] 当前模型在训练集的得分是:mae=4.0334, mse=226.4893, rmse=15.0496
[2018.08.06 15:09:55] 当前模型在测试集的得分是:mae=4.1701, mse=374.0758, rmse=19.3410
[2018.08.06 15:10:14] 第 7 批次数据训练模型
[2018.08.06 15:10:14] 当前模型在训练集的得分是:mae=4.3154, mse=459.7565, rmse=21.4419
[2018.08.06 15:10:14] 当前模型在测试集的得分是:mae=4.1799, mse=382.8426, rmse=19.5664
[2018.08.06 15:10:26] 第 8 批次数据训练模型
[2018.08.06 15:10:26] 当前模型在训练集的得分是:mae=4.2520, mse=293.1202, rmse=17.1208
[2018.08.06 15:10:26] 当前模型在测试集的得分是:mae=4.1891, mse=382.2080, rmse=19.5501

3.3. keras的增量训练方法

keras作为深度学习的API封装,同样实现了增量训练的方法。在深度学习中,由于训练周期很长,几周甚至一两个月很常见,如果因为异常退出要重新训练是很可怕的。因此,在TensorFlow等框架中,都会有从上次异常退出的地方接着训练的方法。
其实也很好理解,比如每训练1万次,就把训练过程的网络结构参数和权重保存下来,如果没问题就接着训练,有问题就把最近一次保存的参数和权重加载进来初始化网络,接着训练。这样即使出错问题也不大。
keras中的增量训练方法也是一个道理,每次将一小部分数据丢进网络训练,然后保存网络,下次新的数据过来再加载网络接着训练。

下面是模型伪代码:

import keras

def normalize(array, min_, max_):
    """使用全局统计量对局部数据进行归一化"""
    write your normalize function here
 
def y_normaloize(array, min_, max_):
    """使用全局统计量对局部数据进行归一化"""
    write your normalize function here


# 第一步:初始化模型为None,设置模型保存路径
model = None
model_file = r'E:\data_tmp\nn_data\nn_model_last_train.h5'

# 第二步:每次读取10万行数据
i = 1
for train in pd.read_csv(file, chunksize=100000):
    
    # 第三步:
    # 区分特征X和目标Y
    x_data = train[x_cols]
    y_data = train[[y_col]]
    
    # 归一化,神经网络的输入需要归一化
    # 这里的归一化使用了特征增量训练的结果(特征全局基本统计信息)
    # (1)对X进行归一化
    for col in x_data.columns:
        min_, max_ = df_min[col], df_max[col]
        x_data[col] = normalize(x_data[col], min_, max_)
    # (2)对y进行归一化
    col = y_col
    min_, max_ = df_min[col], df_max[col]
    y_data[y_col] = y_normaloize(y_data[col], min_, max_)

    # 第四步:增量训练
    
    # 判断是否需要创建模型,如果网络不是None,那么加载上次保存的网络,接着训练就好了
    if model == None:
        k = len(x_cols)
        model = Sequential()
        model.add(Dense(80, activation='relu', input_shape=(k,), ))
        model.add(Dropout(0.5))
        model.add(Dense(32, activation='relu'))
        model.add((Dropout(0.5)))
        model.add(Dense(8, activation='relu'))
        model.add(Dropout(0.5))
        model.add(Dense(1, activation='linear'))
        model.summary()
        print('完成构建网络')
        model.compile(loss='mape', optimizer='adam', metrics=['mse', 'mae', 'mape'])
        print('编译网络')
    else:
        model = load_model(model_file)
        print('加载上次训练的网络')
    
    # 第五步:训练网络
    os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
    history = model.fit(x=x_data, y=y_data[y_col], batch_size=5000, epochs=20,verbose=10000)
    log.info('完成训练网络')
    # 第六步:保存网络
    model.save(model_file)
    
    # 第七步:查看模型评分
    # 查看模型评分
    loss = history.history['loss'][-1]
    mse = history.history['mean_squared_error'][-1]
    mae = history.history['mean_absolute_error'][-1]
    mape = history.history['mean_absolute_percentage_error'][-1]
    print('第%2d 批次数据,loss=%.4f, mse=%.4f, mae=%.4f, mape=%.4f' % (i, loss, mse, mae, mape))

    i += 1

可以看到,loss一直在下降,说明增量训练是有用的。

[2018.08.07 14:15:23] 第 1 批次数据,loss=0.0225, mse=0.0225, mae=0.1221, mape=13.5878
[2018.08.07 14:16:37] 第 2 批次数据,loss=0.0028, mse=0.0028, mae=0.0379, mape=4.4841
[2018.08.07 14:17:50] 第 3 批次数据,loss=0.0023, mse=0.0023, mae=0.0325, mape=3.7667
[2018.08.07 14:19:06] 第 4 批次数据,loss=0.0023, mse=0.0023, mae=0.0324, mape=3.7899
[2018.08.07 14:20:20] 第 5 批次数据,loss=0.0021, mse=0.0021, mae=0.0313, mape=3.5558
[2018.08.07 14:21:37] 第 6 批次数据,loss=0.0022, mse=0.0022, mae=0.0323, mape=3.7333
[2018.08.07 14:22:56] 第 7 批次数据,loss=0.0023, mse=0.0023, mae=0.0322, mape=5.5631
[2018.08.07 14:23:46] 第 8 批次数据,loss=0.0024, mse=0.0024, mae=0.0332, mape=3.8871

参考:

Strategies to scale computationally: bigger data

使用sklearn进行增量学习

lightGBM keep_training_boosterIncremental learning using Dataset.subset LightGBM 2.1.1 python API

如何加载训练好的Keras模型并继续训练

How to Check-Point Deep Learning Models in Keras

Keras中文文档

关注我的专栏(推公式到写代码),期待更有有意义的知识分享。

编辑于 2018-08-08

文章被以下专栏收录

    明明学过那么多专业知识却不知怎么应用在工作中,明明知道这样做可以解决问题却无可奈何。 你不仅仅需要学习专业数学模型,更需要学习怎么应用数学的方法。