【问题标题】:Set thresholds in PySpark multinomial logistic regression在 PySpark 多项逻辑回归中设置阈值
【发布时间】:2018-04-29 18:03:24
【问题描述】:

我想执行多项逻辑回归,但我无法正确设置 thresholdthresholds 参数。考虑以下 DF:

from pyspark.ml.linalg import DenseVector

test_train_df = (
sqlc
.createDataFrame([(0, DenseVector([-1.0, 1.2, 0.7])),
                  (0, DenseVector([3.1, -2.0, -2.9])),
                  (1, DenseVector([1.0, 0.8, 0.3])),
                  (1, DenseVector([4.2, 1.4, -1.7])),
                  (0, DenseVector([-1.9, 2.5, -2.3])),
                  (2, DenseVector([2.6, -0.2, 0.2])),
                  (1, DenseVector([0.3, -3.4, 1.8])),
                  (2, DenseVector([-1.0, -3.5, 4.7]))],
                 ['label', 'features'])
)

我的标签有 3 个类,所以我必须设置 thresholds(复数,默认为 None)而不是 threshold(单数,默认为 0.5)。然后我写:

from pyspark.ml import classification as cl

test_logit_abst = (
    cl.LogisticRegression()
    .setFamily('multinomial')
    .setThresholds([.5, .5, .5])
)

然后我想在我的 DF 上拟合模型:

test_logit = test_logit_abst.fit(test_train_df)

但是在执行最后一条命令时我得到一个错误:

---------------------------------------------------------------------------
Py4JJavaError                             Traceback (most recent call last)
~/anaconda3/lib/python3.6/site-packages/pyspark/sql/utils.py in deco(*a, **kw)
     62         try:
---> 63             return f(*a, **kw)
     64         except py4j.protocol.Py4JJavaError as e:

~/anaconda3/lib/python3.6/site-packages/py4j/protocol.py in get_return_value(answer, gateway_client, target_id, name)
    318                     "An error occurred while calling {0}{1}{2}.\n".
--> 319                     format(target_id, ".", name), value)
    320             else:

Py4JJavaError: An error occurred while calling o3769.fit.
: java.lang.IllegalArgumentException: requirement failed: Logistic Regression found inconsistent values for threshold and thresholds.  Param threshold is set (0.5), indicating binary classification, but Param thresholds is set with length 3. Clear one Param value to fix this problem.

During handling of the above exception, another exception occurred:

IllegalArgumentException                  Traceback (most recent call last)
<ipython-input-211-8f3443f41b6b> in <module>()
----> 1 test_logit = test_logit_abst.fit(test_train_df)

~/anaconda3/lib/python3.6/site-packages/pyspark/ml/base.py in fit(self, dataset, params)
     62                 return self.copy(params)._fit(dataset)
     63             else:
---> 64                 return self._fit(dataset)
     65         else:
     66             raise ValueError("Params must be either a param map or a list/tuple of param maps, "

~/anaconda3/lib/python3.6/site-packages/pyspark/ml/wrapper.py in _fit(self, dataset)
263
    264     def _fit(self, dataset):
--> 265         java_model = self._fit_java(dataset)
    266         return self._create_model(java_model)
267

~/anaconda3/lib/python3.6/site-packages/pyspark/ml/wrapper.py in _fit_java(self, dataset)
    260         """
    261         self._transfer_params_to_java()
--> 262         return self._java_obj.fit(dataset._jdf)
263
    264     def _fit(self, dataset):

~/anaconda3/lib/python3.6/site-packages/py4j/java_gateway.py in __call__(self, *args)
   1131         answer = self.gateway_client.send_command(command)
   1132         return_value = get_return_value(
-> 1133             answer, self.gateway_client, self.target_id, self.name)
1134
   1135         for temp_arg in temp_args:

~/anaconda3/lib/python3.6/site-packages/pyspark/sql/utils.py in deco(*a, **kw)
     77                 raise QueryExecutionException(s.split(': ', 1)[1], stackTrace)
     78             if s.startswith('java.lang.IllegalArgumentException: '):
---> 79                 raise IllegalArgumentException(s.split(': ', 1)[1], stackTrace)
     80             raise
     81     return deco

IllegalArgumentException: 'requirement failed: Logistic Regression found inconsistent values for threshold and thresholds.  Param threshold is set (0.5), indicating binary classification, but Param thresholds is set with length 3. Clear one Param value to fix this problem.'

错误提示 threshold 已设置。这看起来很奇怪,因为documentation 表示设置thresholds(复数)会清除threshold(单数),因此应该删除值0.5。 那么,既然不存在clearThreshold(),如何清除threshold

为了实现这一点,我尝试以这种方式清除threshold

logit_abst = (
    cl.LogisticRegression()
    .setFamily('multinomial')
    .setThresholds([.5, .5, .5])
    .setThreshold(None)
)

这次fit命令起作用了,我什至得到了模型截距和系数:

test_logit.interceptVector
DenseVector([65.6445, 31.6369, -97.2814])

test_logit.coefficientMatrix
DenseMatrix(3, 3, [-76.4534, -19.4797, -79.4949, 12.3659, 4.642, 4.1057, 64.0876, 14.8377, 75.3892], 1)

但是,如果我尝试从 test_logit_abst 获取 thresholds(复数),则会收到错误消息:

test_logit_abst.getThresholds()

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-214-fc1c8617ce80> in <module>()
----> 1 test_logit_abst.getThresholds()

~/anaconda3/lib/python3.6/site-packages/pyspark/ml/classification.py in getThresholds(self)
    363         if not self.isSet(self.thresholds) and self.isSet(self.threshold):
    364             t = self.getOrDefault(self.threshold)
--> 365             return [1.0-t, t]
    366         else:
    367             return self.getOrDefault(self.thresholds)

TypeError: unsupported operand type(s) for -: 'float' and 'NoneType'

这是什么意思?


作为进一步的细节,奇怪(而且对我来说难以理解)颠倒参数设置的顺序会产生我上面发布的第一个错误:

logit_abst = (
    cl.LogisticRegression()
    .setFamily('multinomial')
    .setThreshold(None)
    .setThresholds([.5, .5, .5])
)

为什么改变“set”指令的顺序也会改变输出?

【问题讨论】:

  • 无法重现您在 Spark 2.1.1 或 2.2.0 中的任何错误。如您的帖子所示,您是在仅声明模型时得到它们,还是暗示您在尝试用数据实际拟合模型时得到它们?如果是第二个,请编辑您的帖子以澄清这一点并显示产生错误的实际命令
  • 所有错误都出现在我编写的代码之后:在前两种情况下,它们是通过声明模型来的,在第三种情况下,它们是在我执行命令 logit_abst.getThresholds() 时出现的。为了更清楚,我编辑了第三种情况。
  • 好的,什么 Spark 版本?
  • 我正在使用 Spark 2.2.0
  • 致大家:我更新了关于上面四个答案的问题。使用@desertnaut 查看下面的主题。

标签: apache-spark machine-learning pyspark logistic-regression apache-spark-ml


【解决方案1】:

确实是一团糟……

简短的答案是:

  1. setThresholds(复数)未清除阈值(单数)似乎是一个错误
  2. 对于多项分类(即类数 > 2),setThresholds 不符合您的预期(可以说您不需要它)
  3. 如果您只需要在“默认”值 0.5 中设置一些“阈值”,那么您没有问题 - 只需不要使用任何相关参数或 setThresholds 声明
  4. 如果您确实需要对多项式分类中的不同类别应用不同的决策阈值,则必须手动执行此操作,通过对相应概率进行后处理,即probability 列中的转换后的数据框(虽然使用 setThreshold(s) 进行 binary 分类,但它可以正常工作)

现在是答案...

让我们从二元分类开始,适配玩具数据from the docs

spark.version
# u'2.2.0'

from pyspark.ml.classification import LogisticRegression
from pyspark.sql import Row
from pyspark.ml.linalg import Vectors
bdf = sc.parallelize([
     Row(label=1.0, features=Vectors.dense(0.0, 5.0)),
     Row(label=0.0, features=Vectors.dense(1.0, 2.0)),

blor = LogisticRegression(threshold=0.7, thresholds=[0.3, 0.7])
     Row(label=1.0, features=Vectors.dense(2.0, 1.0)),
     Row(label=0.0, features=Vectors.dense(3.0, 3.0))]).toDF()

我们不需要在这里设置thresholds(复数) - threshold=0.7 就足够了,但在说明与下面的setThreshold 的差异时会很有用。

blorModel = blor.fit(bdf) # works OK
blor.getThreshold()
# 0.7
blor.getThresholds()
# [0.3, 0.7]
blorModel.transform(bdf).show(truncate=False) # transform the training data

结果如下:

+---------+-----+------------------------------------------+----------------------------------------+----------+
|features |label|rawPrediction                             |probability                             |prediction| 
+---------+-----+------------------------------------------+----------------------------------------+----------+
|[0.0,5.0]|1.0  |[-1.138455151184087,1.138455151184087]    |[0.242604109995602,0.757395890004398]   |1.0       |
|[1.0,2.0]|0.0  |[-0.6056346859838877,0.6056346859838877]  |[0.35305562698104337,0.6469443730189567]|0.0       | 
|[2.0,1.0]|1.0  |[0.26586039040308496,-0.26586039040308496]|[0.5660763559614698,0.4339236440385302] |0.0       | 
|[3.0,3.0]|0.0  |[1.6453673835702176,-1.6453673835702176]  |[0.8382639556951765,0.16173604430482344]|0.0       | 
+---------+-----+------------------------------------------+----------------------------------------+----------+

thresholds=[0.3, 0.7]是什么意思?答案在第二行,预测为0.0,尽管1.0 (0.65) 的概率更高:0.65 确实高于 0.35,但低于我们设置的阈值对于此类 (0.7),因此不属于此类。

现在让我们尝试看似相同的操作,但改用setThreshold(s)

blor2 = (LogisticRegression()
  .setThreshold(0.7)
  .setThresholds([0.3, 0.7]) ) # works OK

blorModel2 = blor2.fit(bdf)
[...]
IllegalArgumentException: u'requirement failed: Logistic Regression getThreshold found inconsistent values for threshold (0.5) and thresholds (equivalent to 0.7)'

不错,嗯?

setThresholds(复数)似乎确实清除了我们在上一行中设置的阈值(0.7),如文档中所述,但它似乎这样做只是为了将其恢复为默认值 0.5.. .

省略 .setThreshold(0.7) 会给出您自己报告的第一个错误(未显示)。

颠倒参数设置的顺序可以解决问题(!!!),此外,getThreshold(单数)和getThresholds(复数)都可以运行(与您的情况相反):

blor2 = (LogisticRegression()
  .setThresholds([0.3, 0.7])
  .setThreshold(0.7) )

blorModel2 = blor2.fit(bdf) # works OK
blor2.getThreshold()
# 0.7
blor2.getThresholds()
# [0.30000000000000004, 0.7]

现在让我们转到 多项式 的情况;我们将再次使用文档中的示例,使用来自 Spark Github repo 的数据(它们也应该在本地可用,在您的 $SPARK_HOME/data/mllib/sample_multiclass_classification_data.txt 中,但我正在使用 Databricks 笔记本);这是一个3类案例,标签在{0.0, 1.0, 2.0}

data_path ="/FileStore/tables/sample_multiclass_classification_data.txt"
mdf = spark.read.format("libsvm").load(data_path)

与上面的二进制情况类似,我们的 thresholds(复数)的元素总和为 1,让我们要求第 2 类的阈值为 0.8:

mlor = (LogisticRegression()
       .setFamily("multinomial")
       .setThresholds([0, 0.2, 0.8])
       .setThreshold(0.8) )
mlorModel= mlor.fit(mdf)  # works OK
mlor.getThreshold()
# 0.8
mlor.getThresholds()
# [0.19999999999999996, 0.8]

看起来不错,但让我们在(训练)数据集中请求一个预测

mlorModel.transform(mdf).show(truncate=False)

我只挑出了一行 - 它应该是完整输出末尾的第二行:

+-----+----------------------------------------------------+---------------------------------------------------------+---------------------------------------------------------------+----------+ 
|label|features                                            |rawPrediction                                            |probability                                                    |prediction| 
+-----+----------------------------------------------------+---------------------------------------------------------+---------------------------------------------------------------+----------+
[...]
|0.0  |(4,[0,1,2,3],[0.111111,-0.333333,0.38983,0.166667]) |[36.67790353804905,-74.71196613173531,38.034062593686244]|[0.20486526556822454,8.619113376801409E-50,0.7951347344317755] |2.0       | 
[...]
+-----+----------------------------------------------------+---------------------------------------------------------+---------------------------------------------------------------+----------+

向右滚动,您会看到尽管此处对 2.0 类的预测低于我们设置的阈值 (0.8),但该行确实被预测为 @ 987654354@ - 与上面演示的二进制情况相反...

那么,该怎么办?只需删除所有与阈值相关的语句;你不需要它们——即使setFamily 也是不必要的,因为算法会自行检测到你有两个以上的类。这将给出与上述相同的结果:

mlor = LogisticRegression() # works OK - no family, no threshold(s)

总结

  1. 在二进制和多项式情况下,算法实际返回的是一个概率向量,其长度等于类的数量,元素总和为 1。
  2. 仅在二进制情况下,Spark 允许您更进一步,而不是天真地选择最高的probability 类作为prediction,而是应用用户定义的阈值;此设置可能很有用,例如在数据不平衡的情况下。
  3. 这个threshold(s) 设置在多项式 情况下实际上没有效果,在这种情况下,Spark 将始终以prediction 返回具有最高probability 的类。

尽管文档中的混乱(关于I have argued elsewhere)和一些错误的可能性,让我说一下(3)这个设计选择不是没有道理的;正如elsewhere(强调原文)所说的那样:

当您为新样本的每个类别输出一个概率时,您的练习的统计部分就结束了。选择一个阈值,将新观察分类为 1 与 0 的阈值不再是 统计数据 的一部分。它是 decision 组件的一部分。

虽然上述论证是针对二元情况的,但它也完全适用于多项式...

【讨论】:

  • 非常感谢您详尽的回答。这是不值钱的。我做了一些测试,我能够从截距和系数开始重建 rawPredictionprobabilityprediction 列的内容。我现在可以手动处理阈值,选择合适的预测。
  • 作为进一步的考虑,我同意这种设计选择并非不合理,因为阈值设置是决策组件的一部分,但由于与文档的不一致,以及 setThresholds 命令确实存在的事实,我仍然认为这种行为是一个错误。所以我认为应该向 Spark 报告。他们应该删除setThresholds 方法或修复其行为。你有什么想法?你同意吗?是否可以通过某种方式举报?
  • @VanniRovera 非常欢迎您(现在您也有必要的声誉来支持答案)。我想自己报告这个问题,在这里提供一个链接;如果您想自己做,请告诉我。
  • 如果您想自己报告问题,我可以。只需提供链接,以便跟踪讨论的主题和发展。谢谢!
  • 我不是已经对答案投了赞成票吗?嗯...我看到橙色的向上箭头亮了
猜你喜欢
  • 2017-11-20
  • 2017-10-22
  • 2019-04-11
  • 2015-04-27
  • 2021-05-02
  • 1970-01-01
  • 2014-06-08
  • 1970-01-01
相关资源
最近更新 更多