LESSON 9.5 随机森林在巨量数据上的增量学习

五 随机森林在巨量数据上的增量学习

集成学习是工业领域中应用最广泛的机器学习算法。实际工业环境下的数据量往往十分巨大,一个训练好的集成算法的复杂程度与训练数据量高度相关,因此企业在应用机器学习时通常会提供强大的计算资源作为支持,也因此当代的大部分集成算法都是支持GPU运算的(相对的,如果你发现一个算法在任何机器学习库中,都没有接入GPU运算的选项,这可能说明该算法在工业应用中基本不会被使用)。

sklearn作为早期开源的机器学习算法库,难以预料到如今人工智能技术走进千家万户的应用状况,因此并未开放接入GPU进行运算的接口,即sklearn中的所有算法都不支持接入更多计算资源。因此当我们想要使用随机森林在巨量数据上进行运算时,很可能会遭遇计算资源短缺的情况。幸运的是,我们有两种方式解决这个问题:

  • 使用其他可以接入GPU的机器学习算法库实现随机森林,比如xgboost。
  • 继续使用sklearn进行训练,但使用增量学习(incremental learning)。

增量学习是机器学习中非常常见的方法,在有监督和无监督学习当中都普遍存在。增量学习允许算法不断接入新数据来拓展当前的模型,即允许巨量数据被分成若干个子集,分别输入模型进行训练

1 普通学习 vs 增量学习

  • 普通学习

通常来说,当一个模型经过一次训练之后,如果再使用新数据对模型进行训练,原始数据训练出的模型会被替代掉。举个例子,我们原本的数据集X与y是kaggle房价数据集,结构为:

X.shape
#(1460, 80)

y.shape
#(1460,)

现在,我们导入sklearn中非常常用的另一个数据集,加利福尼亚房价数据集:

from sklearn.datasets import fetch_california_housing
from sklearn.metrics import mean_squared_error

X_fc = fetch_california_housing().data
y_fc = fetch_california_housing().target

X_fc.shape #可以看到,加利福尼亚房价数据集的特征量为8
#(20640, 8)

建模,并在X_,y_基础上进行训练:

model = RFR(n_estimators=3, warm_start=False) #不支持增量学习的
model1 = model.fit(X_fc,y_fc)

#RMSE
(mean_squared_error(y_fc,model1.predict(X_fc)))**0.5
#0.30123985583215596
#使用.estimators_查看森林中所有树的情况,可以看到每一棵树的随机数种子
model1.estimators_
#[DecisionTreeRegressor(max_features='auto', random_state=1785210460),
# DecisionTreeRegressor(max_features='auto', random_state=121562514),
# DecisionTreeRegressor(max_features='auto', random_state=1271073231)]

此时,如果让model1继续在kaggle房价数据集X,y上进行训练:

model1 = model1.fit(X.iloc[:,:8],y)
#注意,X有80个特征,X_fc只有8个特征,输入同一个模型的数据必须结构一致

model1.estimators_ #你发现了吗?model1中原始的树消失了,新的树替代了原始的树
#[DecisionTreeRegressor(max_features='auto', random_state=349555903),
# DecisionTreeRegressor(max_features='auto', random_state=1253222501),
# DecisionTreeRegressor(max_features='auto', random_state=2145441582)]

再让model1对加利福尼亚房价数据集进行训练,会发生什么呢?别忘了model1之前训练过加利福尼亚房价数据集:

#RMSE
(mean_squared_error(y_fc,model1.predict(X_fc)))**0.5
#188517.0427626784

RMSE异常巨大,模型现在已经不具备任何预测y_fc的能力了。非常明显,model1中原始的树消失了,基于kaggle数据集训练的树覆盖了原始的树,因此model1不再对本来见过的加利福尼亚房价数据报有记忆。

sklearn的这一覆盖规则是交叉验证可以进行的基础,正因为每次训练都不会受到上次训练的影响,我们才可以使用模型进行交叉验证,否则就会存在数据泄露的情况。但在增量学习中,原始数据训练的树不会被替代掉,模型会一致记得之前训练过的数据,我们来看看详细情况:

  • 增量学习

我们还是可以使用X,y以及X_fc,y_fc作为例子,这一次,我们让warm_start参数取值为True,允许随机森林进行增量学习:

model = RFR(n_estimators=3, warm_start=True) #支持增量学习
model2 = model.fit(X_fc,y_fc)
(mean_squared_error(y_fc,model2.predict(X_fc)))**0.5
#0.30099931130927154
model2.estimators_
#[DecisionTreeRegressor(max_features='auto', random_state=338470642),
# DecisionTreeRegressor(max_features='auto', random_state=1545812511),
# DecisionTreeRegressor(max_features='auto', random_state=740599321)]

让X和y在model2上继续进行训练:

model2 = model2.fit(X.iloc[:,:8],y)
(mean_squared_error(y_fc,model2.predict(X_fc)))**0.5
#0.30099931130927154

你发现了吗?即便已经对X和y进行了训练,但是model2中对加利福尼亚房价数据集的记忆还在,因此在对X_fc与y_fc进行预测时,依然能够取得不错的分数。

model2.estimators_ #在增量学习当中,树没有发生变化
#[DecisionTreeRegressor(max_features='auto', random_state=338470642),
# DecisionTreeRegressor(max_features='auto', random_state=1545812511),
# DecisionTreeRegressor(max_features='auto', random_state=740599321)]

所以在增量学习当中,已经训练过的结果会被保留。对于随机森林这样的Bagging模型来说,这意味着之前的数据训练出的树会被保留,新数据会训练出新的树,新旧树互不影响。对于逻辑回归、神经网络这样不断迭代以求解权重的算法来说,新数据训练时w的起点是之前的数据训练完毕之后的w。

不过,这里存在一个问题:虽然原来的树没有变化,但增量学习看起来并没有增加新的树——事实上,对于随机森林而言,我们需要手动增加新的树:

model2.estimators_ #属性,反映训练完毕的模型的一些特点、一些客观存在的性质
#调用模型的参数,可以通过这种方式修改模型的参数,而不需要重新实例化模型
model2.n_estimators += 2 #增加2棵树,用于增量学习
model2
#RandomForestRegressor(n_estimators=5, warm_start=True)
model2.fit(X.iloc[:,:8],y)
#RandomForestRegressor(n_estimators=5, warm_start=True)
model2.estimators_ #原来的树还是没有变化,新增的树是基于新输入的数据进行训练的
#[DecisionTreeRegressor(max_features='auto', random_state=338470642),
# DecisionTreeRegressor(max_features='auto', random_state=1545812511),
# DecisionTreeRegressor(max_features='auto', random_state=740599321),
# DecisionTreeRegressor(max_features='auto', random_state=1633155700),
# DecisionTreeRegressor(max_features='auto', random_state=623929223)]

2 增量学习在Kaggle数据上的应用

  • 实际应用

现在我们使用一个385MB的csv文件作为例子,进行巨量数据的导入和训练(当然,在实际中csv文件往往是5G以上,基本不可能使用excel打开进行简单分析或观察)。该数据是来自Kaggle的五大人格心理特质回归数据集。五大人格心理特质是心理学当中常见的人格分类法,也称为FFM模型或OCEAN模型。这种人格分类法是通过给与被调查者一些描述性格方面的句子,让被调查者选择自己符合的项目,例如:

  • 考试前我总是提前准备好一切,尽全力避免出错
  • 考试前我会花几天时间准备
  • 我在考试前临时抱佛脚
  • 我考试前从不准备
  • 我不在意考试,甚至不记得考试的时间

你从中选择最像你的选项,和最不像你的选项,选择结果最终被用于性格分类。该数据集通过收集100w人群在大约80个问题当中的选项,得出最终性格分数和分类。训练集大约有一百万样本,测试集则有2w样本,更详细的状况可以查看:Big Five Personality Test | Kaggle

在面对大型数据时,我们采用循环模式分批读取巨大csv或数据库文件中的内容,并将数据分批进行预处理、再增量学习到一个模型当中。在我们的例子中,由于学习的重点是增量学习,因此课堂上使用的数据是经过我处理完毕、可以直接被随机森林处理的数据,在实际业务中,我们往往需要写好一个可以预处理所有数据的pipeline,然后在循环的过程中不断调用改pipeline。

现在,我们在干净的数据上来看看增量学习具体的步骤吧:

1、定义训练和测试数据地址

trainpath = r"D:\Pythonwork\2021ML\PART 2 Ensembles\datasets\Big data\bigdata_train.csv"
testpath = r"D:\Pythonwork\2021ML\PART 2 Ensembles\datasets\Big data\bigdata_test.csv"

2、设法找出csv中的总数据量

当我们决定使用增量学习时,数据应该是巨大到不可能直接打开查看、不可能直接训练、甚至不可能直接导入的(比如,超过20个G)。但如果我们需要对数据进行循环导入,就必须知道真实的数据量大概有多少,因此我们可以从以下途径获得无法打开的csv中的数据量:

  • 如果是比赛数据集,一般可以在比赛页面找到相应的说明
  • 如果是数据库数据集,则可以在数据库中进行统计
  • 如果无法找到相应的说明,可以使用deque库导入csv文件的最后几行,查看索引
  • 如果数据没有索引,就只能够靠pandas尝试找出大致的数据范围了
#使用deque与StringIO辅助,导入csv文件最后的n行
from collections import deque #deque:双向队列
from io import StringIO

with open(trainpath, 'r') as data:
    q = deque(data, 5)

q
# deque(['995029,3.0,3.0,5.0,5.0,2.0,3.0,2.0,5.0,5.0,5.0,2.0,2.0,4.0,4.0,1.0,1.0,2.0,4.0,4.0,2.0,4.0,3.0,1.0,4.0,1.0,4.0,2.0,4.0,4.0,5.0,4.0,3.0,4.0,3.0,3.0,4.0,4.0,1.0,2.0,5.0,3.0,3.0,3.0,1.0,3.0,4.0,5.0,2.0,5.0,3.0,82719.0,5474.0,7131.0,27265.0,12898.0,18537.0,13712.0,9704.0,9312.0,10824.0,17332.0,25771.0,21437.0,39362.0,29041.0,16015.0,12711.0,12114.0,11141.0,10610.0,34767.0,23585.0,2453.0,23004.0,4677.0,31609.0,11498.0,24396.0,8758.0,11288.0,18892.0,31976.0,5874.0,23840.0,38838.0,13131.0,8298.0,15644.0,7292.0,8649.0,8513.0,18259.0,34832.0,3168.0,3306.0,17459.0,12079.0,9565.0,6310.0,24019.0,291658.0,666.0,469.0,37.0,1954.0,33.0,0.0,41.0,865.0,-70.6503\n',
#        '995030,2.0,4.0,4.0,2.0,4.0,2.0,4.0,4.0,4.0,4.0,3.0,2.0,2.0,4.0,2.0,2.0,2.0,2.0,2.0,2.0,1.0,0.0,2.0,4.0,2.0,4.0,1.0,4.0,4.0,4.0,5.0,2.0,5.0,1.0,4.0,1.0,5.0,1.0,5.0,4.0,4.0,1.0,5.0,2.0,5.0,1.0,4.0,0.0,4.0,4.0,5470.0,2913.0,2137.0,2967.0,2060.0,2499.0,4645.0,7550.0,4682.0,5440.0,6976.0,5326.0,1556.0,4765.0,4029.0,2760.0,4450.0,5211.0,1623.0,4274.0,2652.0,14.0,5820.0,4383.0,9358.0,1826.0,4699.0,2441.0,5734.0,5116.0,1797.0,4038.0,2386.0,6465.0,3693.0,3399.0,5281.0,3727.0,1413.0,9482.0,2911.0,3693.0,2766.0,3782.0,1958.0,4180.0,7876.0,14.0,5032.0,2003.0,968800.0,666.0,469.0,6.0,208.0,30.0,0.0,208.0,19838.0,-123.0867\n',
#        '995031,2.0,1.0,3.0,2.0,5.0,1.0,5.0,4.0,4.0,3.0,2.0,5.0,3.0,4.0,1.0,1.0,1.0,3.0,2.0,2.0,3.0,4.0,1.0,4.0,3.0,5.0,1.0,3.0,5.0,5.0,4.0,4.0,5.0,2.0,4.0,3.0,4.0,1.0,3.0,4.0,3.0,2.0,4.0,2.0,3.0,2.0,3.0,1.0,4.0,4.0,74490.0,6721.0,19705.0,8894.0,11176.0,27505.0,12282.0,12209.0,7985.0,7434.0,34860.0,9848.0,9200.0,13043.0,995.0,5621.0,3592.0,4657.0,7069.0,5196.0,40330.0,6027.0,6590.0,5617.0,6727.0,14579.0,7065.0,10650.0,1350.0,8252.0,33374.0,7564.0,4720.0,14695.0,15016.0,9359.0,9410.0,53991.0,14756.0,5976.0,31580.0,6783.0,24779.0,41707.0,8803.0,8412.0,4472.0,7805.0,5635.0,5801.0,567037.0,93.0,541.0,596.0,2892.0,1602.0,0.0,144.0,2745.0,112.5\n',
#        '995032,1.0,4.0,1.0,5.0,2.0,2.0,1.0,5.0,2.0,4.0,5.0,1.0,4.0,2.0,4.0,4.0,3.0,4.0,5.0,3.0,2.0,2.0,2.0,4.0,2.0,4.0,2.0,4.0,3.0,3.0,4.0,4.0,5.0,2.0,2.0,3.0,3.0,2.0,2.0,3.0,4.0,1.0,3.0,1.0,4.0,1.0,3.0,4.0,4.0,3.0,14075.0,6301.0,3611.0,4143.0,5949.0,4900.0,10292.0,3254.0,3896.0,4297.0,5380.0,8667.0,6565.0,3442.0,4335.0,10107.0,15301.0,6697.0,4305.0,3574.0,44820.0,15434.0,7953.0,6857.0,9700.0,5049.0,6720.0,4937.0,12857.0,6981.0,10642.0,8187.0,3713.0,15412.0,4186.0,7606.0,3064.0,4122.0,3769.0,6718.0,5027.0,6272.0,7023.0,8974.0,8198.0,4774.0,11819.0,8736.0,3880.0,4768.0,989963.0,57.0,441.0,13.0,520.0,29.0,0.0,208.0,10546.0,-97.0\n',
#        '995033,3.0,2.0,4.0,3.0,4.0,2.0,4.0,3.0,4.0,3.0,3.0,3.0,4.0,4.0,3.0,3.0,4.0,5.0,3.0,3.0,2.0,5.0,2.0,5.0,2.0,5.0,2.0,4.0,5.0,4.0,3.0,5.0,3.0,3.0,2.0,4.0,3.0,3.0,1.0,4.0,3.0,3.0,5.0,3.0,3.0,2.0,3.0,2.0,4.0,4.0,7811.0,12321.0,728.0,2997.0,5020.0,515.0,10110.0,11314.0,4200.0,3473.0,2808.0,10826.0,7022.0,1590.0,917.0,2999.0,3919.0,661.0,2962.0,7594.0,4430.0,2462.0,7012.0,3126.0,4808.0,3359.0,623.0,5745.0,580.0,4144.0,2358.0,4829.0,735.0,3742.0,6546.0,5603.0,5158.0,6435.0,7207.0,6272.0,2231.0,5253.0,2480.0,18923.0,18792.0,14734.0,7294.0,7964.0,4358.0,4733.0,443675.0,36.0,272.0,3.0,285.0,15.0,0.0,208.0,9322.0,-76.3729\n'])

pd.read_csv(StringIO(''.join(q)), header=None)
#>> a=['1','2','3','4','5']
#>> '  '.join(a)
#1 2 3 4 5

LESSON 9.5 随机森林在巨量数据上的增量学习_第1张图片

可以看到最后一行的索引是995033,因此训练集中有99w条数据。 

#如果数据没有索引,则使用pandas中的skiprows与nrows进行尝试
#skiprows: 本次导入跳过前skiprows行
#nrows:本次导入只导入nrows行
#例如,当skiprows=1000, nrows=1000时,pandas会导入1001~2000行
#当skiprows超出数据量时,就会报空值错误EmptyDataError

for i in range(0,10**7,100000):
    df = pd.read_csv(trainpath,skiprows=i, nrows=1)
    print(i)
#0
#100000
#200000
#300000
#400000
#500000
#600000
#700000
#800000
#900000

可以看到90w顺利导入了,但是100w报错了,所以数据量在90-100w之间。如果我们想,我们可以继续精确数据量的具体范围,但通常来说我们只要确认10w以内的区域就可以了。

3、确认数据量后,准备循环范围

[*range(0,10**6,50000)]
#[0,
# 50000,
# 100000,
# 150000,
# 200000,
# 250000,
# 300000,
# 350000,
# 400000,
# 450000,
# 500000,
# 550000,
# 600000,
# 650000,
# 700000,
# 750000,
# 800000,
# 850000,
# 900000,
# 950000]
looprange = range(0,10**6,50000)

4、建立增量学习使用的模型,定义测试集

reg = RFR(n_estimators=10
          ,random_state=1412
          ,warm_start=True
          ,verbose=True #增量学习的过程总是很漫长的,你可以选择展示学习过程
          ,n_jobs=-1 #调用你全部的资源进行训练
         )

#定义测试集
test = pd.read_csv(testpath,header="infer",index_col=0)
Xtest = test.iloc[:,:-1]
Ytest = test.iloc[:,-1]

Xtest.head()

LESSON 9.5 随机森林在巨量数据上的增量学习_第2张图片

5、开始循环导入与增量学习

#当skiprows+nrows超出数据量的时候,会发生什么?
trainsubset = pd.read_csv(trainpath, header=None, index_col=0
                          , skiprows=950000
                          , nrows=50000)
trainsubset.tail(5) #会导出全部剩下的数据,即便不足200w

LESSON 9.5 随机森林在巨量数据上的增量学习_第3张图片

trainsubset = pd.read_csv(trainpath, header=None, index_col=0
                          , skiprows=950000
                          , nrows=50000)

trainsubset.tail(5) #会导出全部剩下的数据,即便不足200w

LESSON 9.5 随机森林在巨量数据上的增量学习_第4张图片

trainsubset.shape 
#(45035, 110)

for line in looprange:
    if line == 0:
        #首次读取时,保留列名,并且不增加树的数量
        header = "infer"
        newtree = 0
    else:
        #非首次读取时,不要列名,每次增加10棵树
        header = None
        newtree = 10
    
    trainsubset = pd.read_csv(trainpath, header = header, index_col=0, skiprows=line, nrows=50000)
    Xtrain = trainsubset.iloc[:,:-1]
    Ytrain = trainsubset.iloc[:,-1]
    reg.n_estimators += newtree
    reg = reg.fit(Xtrain,Ytrain)
    print("DONE",line+50000)
        
    #当训练集的数据量小于50000时,打断循环
    if Xtrain.shape[0] < 50000:
        break
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.4s finished
# DONE 50000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.3s finished
# DONE 100000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.2s finished
# DONE 150000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.3s finished
# DONE 200000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.4s finished
# DONE 250000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.3s finished
# DONE 300000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.0s finished
# DONE 350000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.1s finished
# DONE 400000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.1s finished
# DONE 450000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.2s finished
# DONE 500000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.4s finished
# DONE 550000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.1s finished
# DONE 600000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.4s finished
# DONE 650000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.2s finished
# DONE 700000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.3s finished
# DONE 750000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.1s finished
# DONE 800000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.2s finished
# DONE 850000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.1s finished
# DONE 900000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.3s finished
# DONE 950000
# [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 12 concurrent workers.
# DONE 1000000
# [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    3.8s finished

 现在,全部的99w数据都已经训练完毕了,我们可以在测试集上进行测试:

reg.score(Xtest,Ytest) #R2 99%,这可能与测试集上的数据太少有关
#0.9903482355083931

当使用增量学习时,如果需要调参,我们则需要将增量学习循环打包成一个评估器或函数,以便在调参过程中不断调用,这个过程所需的计算量是异常大的,不过至少我们拥有了在CPU上训练巨大数据的方法。在后续的课程当中,我们将会讲解如何将随机森林或其他集成算法接入GPU进行训练,进一步提升我们可以训练的数据体量、进一步减少我们所需的训练时间。

你可能感兴趣的:(机器学习,随机森林,算法,机器学习,增量学习)