点 快速计算的未来即将来到你所在城市的活动。

加入我们,参加 Redis 发布会

Redis-ML 介绍(第五部分)

此博文是研究 Redis-ML 模块特性的一系列博文的第五篇。系列中第一篇可在 这里 中找到。本博文中包含的示例代码需要多个 Python 库和加载了 Redis-ML 模块的 Redis 实例。运行时环境的详细设置说明在系列的 第一部分第二部分 中均有提供。

决策树

决策树 是用于机器学习中分类和回归问题的预测模型。决策树将规则序列建模为二叉树。树的内部节点表示拆分或规则,而叶子节点表示分类或值。

树中的每条规则都处理数据集的一个单一特征。如果满足规则条件,则移至左子级;否则移至右子级。对于范畴特征(枚举),规则使用的测试是属于特定类别。

对于具有连续值的特征,测试为“小于”或“等于”。若要评估数据点,从根节点开始,通过评估内部节点中的规则遍历树,直到到达叶子节点。叶子节点标有要返回的决定。下面展示了一个决策树示例

维基百科决策树学习文章中的 CART 树

可以使用多种不同的算法(递归分区、自顶向下归纳等)来构建决策树,但评估过程始终相同。为了提高决策树的准确性,通常将它们聚合到随机森林中,该森林使用多棵树对数据点进行分类,并将所有树中的多数决策作为最终分类。

为了演示决策树的工作方式以及如何在 Redis 中表示决策树,我们将使用 scikit-learn Python 包和 Redis 构建泰坦尼克号生存预测器。

泰坦尼克号数据集

1912 年 4 月 15 日,泰坦尼克号与冰山相撞后沉没于北大西洋。超过 1500 名乘客死于此次碰撞,使其成为现代历史上最致命的商业海上灾难之一。尽管有一定的运气成分在灾难中幸存下来,但查看数据显示,一些群体乘客比其他人更有可能幸存下来的偏差。

泰坦尼克数据集,其中一个副本可在此处获得此处,是机器学习中使用的一个经典数据集。来自范德比尔特档案馆中该数据集的副本包含泰坦尼克号上 1309 名乘客的记录。这些记录包含 14 个不同的字段:乘客等级、是否幸存、姓名、性别、年龄、兄弟姐妹/配偶人数、船上的父母/子女人数、船票号、票价、船舱、登船港、救生艇、遗体编号和目的地。

我们用 Excel 对数据进行粗略的扫描,发现我们的数据集中有很多丢失的数据。丢失的字段会影响我们的结果,因此我们在构建决策树之前需要对我们的数据进行一些清理。我们会使用pandas库预处理我们的数据。你可以使用 Python 包管理器 pip 安装 pandas 库:

pip install pandas 

或你喜欢的包管理器。

借助 pandas,我们可以快速了解我们的数据中每个记录类别的取值计数

pclass       1309
survived     1309
name         1309
sex          1309
age          1046
sibsp        1309
parch        1309
ticket       1309
fare         1308
cabin         295
embarked     1307
boat          486
body          121
home.dest     745

由于船舱、救生艇、遗体和目的地记录有大量缺失的记录,我们将从我们的数据集中删除它们。由于乘客票具有很小的预测意义,我们也将删除它。对于我们的预测变量,我们最终使用乘客等级 (pclass)、幸存状态 (survived)、性别、年龄、兄弟姐妹/配偶人数 (sibsp)、船上的父母/子女人数 (parch)、船费和登船港 (“embarked”) 记录来构建一个特征集。即使删除了具有稀疏填充的列之后,仍有多行数据缺失,因此为了简单起见,我们将那些乘客记录从我们的数据集中删除。

使用以下代码完成数据的初始清理阶段

import pandas as pd

# load data from excel 
orig_df = pd.read_excel('titanic3.xls', 'titanic3', index_col=None)
# remove columns we aren't going to work with, drop rows with missing data
df = orig_df.drop([“name”, "ticket", "body", "cabin", "boat", "home.dest"], axis=1)
df = df.dropna()

我们需要对数据执行的最终预处理是使用整数常量对分类数据进行编码。pclass 和 survived 列已经被编码为整数常量,但性别列记录的是字符串值“男性”或“女性”,embarked 列使用字母代码来表示每个港口。scikit 包在 preprocessing 子包中提供了执行数据编码的实用程序。

数据的第二个清理阶段,转换非整数编码的分类特征,使用以下代码完成

from sklearn import preprocessing

# convert enumerated columns (sex,)
encoder = preprocessing.LabelEncoder()
df.sex = encoder.fit_transform(df.sex)
df.embarked = encoder.fit_transform(df.embarked)

在清理完我们的数据后,我们可以计算根据乘客等级 (pclass) 和性别分组的几个特征列的平均值。

               survived        age     sibsp     parch        fare
pclass sex                                                        
1      female  0.961832  36.839695  0.564885  0.511450  112.485402
       male    0.350993  41.029250  0.403974  0.331126   74.818213
2      female  0.893204  27.499191  0.514563  0.669903   23.267395
       male    0.145570  30.815401  0.354430  0.208861   20.934335
3      female  0.473684  22.185307  0.736842  0.796053   14.655758
       male    0.169540  25.863027  0.488506  0.287356   12.103374

请注意男性和女性在乘客等级基础上的存活率有显著差异。我们用于构建决策树的算法会发现这些统计差异并使用它们来选择要拆分的特征。

构建决策树

我们利用 scikit-learn 基于我们的数据构建一个决策树分类器。我们首先将我们清理后的数据拆分成训练集和测试集。使用以下代码,我们从数据(幸存)拆分出标签列作为特征集,并保留我们数据中的最后 20 条记录作为测试集。

X = df.drop(['survived'], axis=1).values
Y = df['survived'].values

X_train = X[:-20]
X_test  = X[-20:]
Y_train = Y[:-20]
Y_test  = Y[-20:] 

一旦有了我们的训练集和测试集,我们可以创建一个深度为 10 的决策树。

# Create the real classifier depth=10
cl_tree = tree.DecisionTreeClassifier(max_depth=10, random_state=0)
cl_tree.fit(X_train, Y_train)

我们在博客文章中很难可视化我们的深度 10 决策树,因此为了可视化决策树的结构,我们创建了一颗第二棵树并限制了树的深度为 3。下图显示了决策树的结构,该结构由分类器学习

由 Scikit 学习的泰坦尼克号决策树

加载 Redis 预测器

Redis-ML 模块提供了两个用于处理随机森林的命令:ML.FOREST.ADD 用于在森林的上下文中创建一个决策树,ML.FOREST.RUN 用于使用随机森林评估一个数据点。ML.FOREST 命令具有以下语法

ML.FOREST.ADD key tree path ((NUMERIC|CATEGORIC) attr val | LEAF val [STATS]) [...]
ML.FOREST.RUN key sample (CLASSIFICATION|REGRESSION)

Redis-ML 中的每个决策树都必须使用一个单独的 ML.FOREST.ADD 命令加载。ML.FOREST.ADD 命令由一个 Redis 密钥组成,后跟一个整数树 ID,再后跟节点规范。节点规范由一个路径组成,即一系列符号  .  (根),l 和 r,代表树中到节点的路径。内部节点是分流器或规则节点,并使用 NUMERIC 或 CATEGORIC 关键字来指定规则类型、所针对的属性以及要拆分的阈值。对于 NUMERIC 节点,将属性与阈值进行比较,如果该属性小于或等于阈值,则采用左路径;否则采用右路径。对于 CATEGORIC 节点,测试为相等性。等值采用左路径,不等值采用右路径。

scikit-learn 中的决策树算法将分类属性视为数值,因此当我们在 Redis 中表示这棵树时,我们将只使用 NUMERIC 节点类型。为了将 scikit 树加载到 Redis 中,我们需要执行一个例程来遍历该树。以下代码对 scikit 决策树执行先序遍历以生成一个 ML.FOREST.ADD 命令(因为我们只有一个树,所以我们生成一个只有单个树的简单森林)。

# scikit represents decision trees using a set of arrays,
# create references to make the arrays easy to access

the_tree = cl_tree
t_nodes = the_tree.tree_.node_count
t_left = the_tree.tree_.children_left
t_right = the_tree.tree_.children_right
t_feature = the_tree.tree_.feature
t_threshold = the_tree.tree_.threshold
t_value = the_tree.tree_.value
feature_names = df.drop(['survived'], axis=1).columns.values

# create a buffer to build up our command
forrest_cmd = StringIO()
forrest_cmd.write("ML.FOREST.ADD titanic:tree 0 ")

# Traverse the tree starting with the root and a path of “.”
stack = [ (0, ".") ]

while len(stack) > 0:
    node_id, path = stack.pop()
   
    # splitter node -- must have 2 children (pre-order traversal)
    if (t_left[node_id] != t_right[node_id]):
        stack.append((t_right[node_id], path + "r")) 
        stack.append((t_left[node_id], path + "l"))
        cmd = "{} NUMERIC {} {} ".format(path, feature_names[t_feature[node_id]], t_threshold[node_id])
        forrest_cmd.write(cmd)
      
    else:
        cmd = "{} LEAF {} ".format(path, np.argmax(t_value[node_id]))
        forrest_cmd.write(cmd)

# execute command in Redis
r = redis.StrictRedis('localhost', 6379)
r.execute_command(forrest_cmd.getvalue())

对比结果

将决策树加载到 Redis 后,我们可以创建两个向量来对比 Redis 的预测与 scikit-learn 的预测

# generate a vector of scikit-learn predictors 
s_pred = cl_tree.predict(X_test)

# generate a vector of Redis predictions
r_pred = np.full(len(X_test), -1, dtype=int)
for i, x in enumerate(X_test):
    cmd = "ML.FOREST.RUN titanic:tree "

    # iterate over each feature in the test record to build up the 
    # feature:value pairs
    for j, x_val in enumerate(x):
        cmd += "{}:{},".format(feature_names[j], x_val)  

    cmd = cmd[:-1]
    r_pred[i] = int(r.execute_command(cmd))

为了使用 ML.FOREST.RUN 命令,我们必须生成一个特征向量,其中包含用逗号分隔的 <feature>:<value> 对的列表。向量的 <feature> 部分是字符串特征名称,它必须对应 ML.FOREST.ADD 命令中使用的特征名称。

对比 r_pred 和 s_pred 预测值与实际标签值

Y_test: [0 0 0 0 1 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0]
r_pred: [1 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0]
s_pred: [1 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0]

Redis 的预测与 scikit-learn 软件包的预测相同,包括对测试项 0 和 14 的错误分类。

乘客的生存几率与等级和性别密切相关,因此有几起出乎意料的案例,一些实际上丧生的个体竟然有很高的生存几率。 调查其中一些异常值会发现那次不幸航行的迷人故事。 网上有很多资源讲述了泰坦尼克号乘客和船员的故事,向我们展示了数据背后的真实人物。 我鼓励您调查一些被错误分类的人员并了解他们的故事。

在下一篇也是最后一篇文章中,我们将把所有内容串联起来并结束这篇 Redis-ML 介绍。 在此期间,如果您对这篇文章或之前的文章有任何疑问,请通过 Twitter(@tague)与我联系。