MSBD5003 Project2.0: Decision Tree Model

书接上回,我们构建了id3的决策树,能够按照我们所指定的k列生成最多k层的决策树。本文在扩展使用方法之外还试图加快了运行速度(未完全果)。

决策树的使用还存在一些小问题,比如说预测的时候只能对单条输入的数据进行,写个for循环批量操作一下,虽然会所好像用map方法似乎更快一点,这里先偷个懒:

k = 5
columns = data.columns[:k]
data1 = data.selectExpr(columns).tail(n-a)
tag = data.selectExpr('booking_status').tail(n-a)
predict_all = []
count = 0
for row in data1:
    #print(row)
    count += 1
    predict_all.append(predict_one(row, res))
predict_all[:100]

然后就发生了神奇的一幕:

'Not_Canceled',
 'Not_Canceled',
 'Not_Canceled',
 'Not_Canceled',
 'Not_Canceled',
 'Not_Canceled',
 'Not_Canceled',
 'Not_Canceled',
 'Not_Canceled',
......

所有的预测结果都无一例外的是同一个,原因可能是因为分的还是太粗糙了,两条指标最多能分出M(第一条指标取值数量)*N(第二...)类,但这几类中可能都是Not_Canceled占比大,所以预测结果就肯定全都是Not_Canceled了。为了验证上述的猜想,我们检查一下groupBy的结果:

check = data.groupBy([columns[0],'required_car_parking_space',label_col]).agg(count("*").alias("freq")).orderBy('no_of_adults','required_car_parking_space')
check.show()

 确实是无论哪种情况下Not_Canceled的都更多,这棵树构造的没问题。说明分的还是不够细,得加大剂量。不过要意识到id3也是C4.5的一个极大的限制性就在此时出现了,因为是多叉树,树的节点增长的非常快。上面的的叶节点数可能只有几个十几个,但我们每多一个指标都以为着扩张对应倍数的节点数量。在我的小破电脑上,这3w条数据处理5条指标的时候就要10min,而且5条指标似乎也是远远不够的,分出来的结果仍然全部都是Not_Canceled.

这里假设数据是均匀分布的,我先不妨取部分数据试验一下,究竟怎么样才能养预测结果出现Canceled呢?

1. 数据的预处理

1.1 随机取1k条数据

取前1k个数据,观察了一下,靠,竟然不是均匀分布的。前1k条数据中只有65条是Canceled;后1200条中有1k条是cancel的(3w条中有1w条canceled),所以我们得重新随机排序一下数据:

shuffledData = data.orderBy(rand())

这样取的1k条数据就很有代表性了,这个故事告诉我们,无论什么时候我们都应该随机取1k条数据,而不是想当然取前1k条数据。

我们对1k条数据的所有列运行我们的上述代码,不难发现:根本运行不出来。。。因为这速度就好比乌龟在爬。考虑到有一些原因是有些属性的取值太过于分散了,我们把一些属性的值合并到一个区间以期望能爬的快一点。

1.2 使用少量数据所有标签的实验结果

功夫不负有心人,在我可以接受的时间内,决策树对500条数据的构造总算可以以751.3s的代价做了。好消息是,我们用同样的代码去看预测的结果,这次结果里面终于有Canceled了。

['Canceled',
 'Canceled',
 'Not_Canceled',
 'Canceled',
 'Canceled',
 'Canceled',
 'Not_Canceled',
 'Canceled',
 'Not_Canceled',
 'Not_Canceled']

当然我也知道放这个上去没有用。因为模型的稳定性其实很差,如果你们随机到的数据跟我不一样,很有可能会跟我复现出来的是一颗完全不同的树。我知道你很急,但你先别急,这里面还有太多的问题需要解决了,抛开刚才说的所有问题,回归到主线来:不管好坏,我怎么对所有的数据所有属性去做构造这个决策树?

2. 计算速度慢的原因分析

观察实验过程——>时间都去哪了?==》information_gain这个函数

这个函数计算的非常慢,而且往往调用的次数还会非常多,决策树的每一个走到叶节点的分支大概都需要调用这个函数(k+k-1+k-2+...+1)次,更何况我们还有这么多叶节点。不仅如此,它还非常非常慢。

split_col = 'avg_price_per_room'
def information_gain(data, split_col, label_col):
    print(split_col)
    ex_entropy = entropy(data, label_col)
    groups = data.groupBy(split_col).agg(count("*").alias("count")).collect()
    n = data.count()
    gain = ex_entropy
    for group in groups:
        a = data.filter(col(split_col) == str(group[0]))
        group_entropy = entropy(data.filter(col(split_col) == str(group[0])), label_col)
        group_weight = group[1] / n
        gain -= group_weight * group_entropy
    return gain
import time
T1 = time.time()
b = information_gain(data, split_col, label_col)
T2 = time.time()
print('程序运行时间:%s秒' % (T2 - T1))
print('end!')
avg_price_per_room
程序运行时间:4.3002166748046875秒
end!

对于3w条数据,它单计算一个attribut一次的耗时就直接高达4s,更何况我们需要以大概k^3的高强度去调用它 。我于是尝试了一些其他的方法来降低一些耗时。用broadcast函数来广播小数据集,以减少网络传输开销:

split_col = 'avg_price_per_room'
def information_gain_v2(data, split_col, label_col):
    ex_entropy = entropy(data, label_col)
    # 将小数据集广播出去
    groups = broadcast(data.select(split_col).distinct())
    n = data.count()
    # 使用Spark SQL中的函数
    gain = ex_entropy - data.join(groups, split_col).groupBy(split_col)\
        .agg(count("*").alias("count"))\
        .selectExpr("sum(-(count/{n})*log2(count/{n})) as entropy")\
        .selectExpr("sum(entropy) as entropy_sum").first()['entropy_sum']
    return gain
import time
T1 = time.time()
b = information_gain(data, split_col, label_col)
T2 = time.time()
print('程序运行时间:%s秒' % (T2 - T1))
print('end!')

虽然说并没能实现数量级上的降低,但效果还是非常显著的:

avg_price_per_room
程序运行时间:3.432748794555664秒
end

 但是这还是不够啊,3s对于我们来说还是太久了。然后进一步测试,问题自然只能出在entropy的计算上了。测试了一下,还真是有点慢......

label_col='booking_status'
def entropy(data, label_col):
    n = data.count()
    label_freqs = data.groupBy(label_col).agg(count("*").alias("freq"))
    label_freqs = label_freqs.withColumn("prob", col("freq") / n)    
    entropy = (label_freqs.selectExpr("prob * log2(prob) as product")
                 .selectExpr("-1 * sum(product) as entropy").first()["entropy"])
    #print('Entropy in this class:',entropy)
    return entropy

import time
T1 = time.time()
a = entropy(data, label_col)
T2 = time.time()
print('程序运行时间:%s秒' % (T2 - T1))
print('end!')
程序运行时间:0.4898509979248047秒
end!

算一次信息熵就要0.5s,当然这是对全部数据计算熵需要的时间,测试100条的时间大概在0.14s。这么一看确实冤枉了信息增益函数了。因为它要多次调用计算信息熵的函数,熵函数算不出来,它就得等着。

这里笔者使用了几种不同版本的方法去计算信息熵,除了上篇文章中的方法,还有:

def entropy_v2(data, label_col):
    n = data.count()
    label_freqs = data.groupBy(label_col).agg(count("*").alias("freq"))
    label_freqs.createOrReplaceTempView("label_freqs")
    sql_query = f"SELECT -1 * SUM(prob * LOG2(prob)) AS entropy FROM (SELECT freq/{n} AS prob FROM label_freqs) subquery"
    entropy = spark.sql(sql_query).first()["entropy"]
    return entropy
def entropy_v3(data, label_col):
    n = data.count()
    freqs = data.groupBy(label_col).count()
    prob = freqs.withColumn("prob", freqs["count"] / n).select("prob")
    entropy = -prob.rdd.map(lambda x: x[0] * log2(x[0])).sum()
    return entropy

只能说没有丝毫的进步,可能对于并行计算的方法掌握的仍然不够,笔者目前确实没有更好的办法去解决这个问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值