Fairlearn 是一个旨在帮助数据科学家提高人工智能系统公平性的开源项目。目前国内并没有相关教程来讲解这个库的使用方式,所以笔者用一系列博客尽可能详细地教学 Fairlearn 库的使用方法。「在官网教程的基础上加入自己的个人见解。」
本篇博客是此系列的第三篇,主要内容是 Fairlearn 中内置的公平性评估方法。
功能:Metrics 模块提供了评估模型的公平性相关指标的方法。
最简单的情况下,评估指标采取的是标签
Y
t
r
u
e
Y_{true}
Ytrue 和预测集
Y
p
r
e
d
Y_{pred}
Ypred 之间的关系。例如真阳性率
T
P
=
P
(
Y
p
r
e
d
=
1
∣
Y
t
r
u
e
=
1
)
TP = P(Y_{pred}=1|Y_{true}=1)
TP=P(Ypred=1∣Ytrue=1),即预测为正,实际也为正的概率。假阴性率
F
N
=
P
Y
p
e
r
d
=
0
∣
Y
t
r
u
e
=
1
FN = P{Y_{perd}=0|Y_{true}=1}
FN=PYperd=0∣Ytrue=1 ,即预测为负,实际为正的概率。召回率
R
e
c
a
l
l
=
T
P
T
P
+
F
N
Recall = \frac{TP}{TP+FN}
Recall=TP+FNTP。召回率在代码中的一个具体实现是 sklearn.metrics.recall_score()
代码展示 「Jupyter NoteBook」
import sklearn.metrics as skm
y_true = [0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1]
y_pred = [0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1]
skm.recall_score(y_true,y_pred)
输出结果如下
当考虑到公平时,我们想看到在不同组之间的指标有什么差异,这就涉及到群体性指标。
import numpy as np
import pandas as pd
group_membership_data = ['d', 'a', 'c', 'b', 'b', 'c', 'c', 'c',
'b', 'd', 'c', 'a', 'b', 'd', 'c', 'c']
pd.set_option('display.max_columns',20)
pd.set_option('display.width',80)
pd.DataFrame({'y_true':y_true,'y_pred':y_pred,'group_membership_data':group_membership_data})
我们将前一个例子的 y_true
和 y_pred
用 a,b,c,d
划分为四个不同的组。
from fairlearn.metrics import MetricFrame
grouped_metrics = MetricFrame(metrics= skm.recall_score,y_pred=y_pred,y_true=y_true,sensitive_features=group_membership_data)
print(grouped_metrics.overall)
print(grouped_metrics.by_group)
除了输出各个组件的评估指标,MetricFrame 还支持输出其他统计信息,例如最大最小精度,比例等等
print("min recall over groups = ", grouped_metrics.group_min()) # 最小召回率
print("max recall over groups = ", grouped_metrics.group_max()) # 最大召回率
print("difference in recall = ", grouped_metrics.difference(method='between_groups')) # 组间召回率的最大差异
print("ratio in recall = ", grouped_metrics.ratio(method='between_groups')) # 最小召回率比最大召回率
结果如下:
上述的例子只展示了召回率这一个指标,实际上 MetricFrame 可以一次评估多个指标
from fairlearn.metrics import count
multi_metrics = MetricFrame({'precision':skm.precision_score,'recall':skm.recall_score,'count':count},y_true=y_true,y_pred=y_pred,sensitive_features=group_membership_data) # precision_score 是差准率,是 TP/TP + TN
print(multi_metrics.overall)
print(multi_metrics.by_group)
结果如下:
通常情况下,不同个体的权重是不同的,得到带权重的组间评估指标只需要在 MetricFrame 中多加一个 sample_params
参数
s_w = [1, 2, 1, 3, 2, 3, 1, 2, 1, 2, 3, 1, 2, 3, 2, 3]
s_p = { 'sample_weight':s_w }
weighted_metrics = MetricFrame(metrics=skm.recall_score,
y_true=y_true,
y_pred=y_pred,
sensitive_features=group_membership_data,
sample_params=s_p)
print(weighted_metrics.overall)
print(weighted_metrics.by_group)
结果如下:
前面 MetricFrame 使用到的 metrics
都是不携带参数的,如果我们需要使用携带参数的 metric
该怎么办呢?
直接将携带参数的 metric
传给 metrics
会直接报错,如下
解决方法是,用functools.partial()
将 fbeta_score
和 beta
参数封装成一个方法,具体如下
import functools
fbeta_06 = functools.partial(skm.fbeta_score, beta=0.6)
metric_beta = MetricFrame(metrics=fbeta_06, y_true=y_true, y_pred=y_pred, sensitive_features=group_membership_data)
print(metric_beta.overall)
print(metric_beta.by_group)
注意:这里回顾一下 fbeta_score
,它是召回率和差准率的加权平均。召回率为
r
e
c
a
l
l
recall
recall,查准率是
p
r
e
c
i
s
i
o
n
precision
precision ,那么
F
−
B
e
t
a
=
(
1
+
β
2
)
⋅
p
r
e
c
i
s
i
o
n
⋅
r
e
c
a
l
l
(
β
2
⋅
p
r
e
c
i
s
i
o
n
)
+
r
e
c
a
l
l
F-Beta = (1+\beta^2)\cdot\frac{precision\cdot recall}{(\beta^2\cdot precision)+recall}
F−Beta=(1+β2)⋅(β2⋅precision)+recallprecision⋅recall。当
β
<
1
\beta < 1
β<1 时,偏向于召回率,
β
>
1
\beta > 1
β>1 时偏向于查全率。
输出结果如下:
前面,我们只使用了单个敏感属性,如果我们需要用多个敏感属性,该如何实现呢?
g_2 = [ 8,6,8,8,8,8,6,6,6,8,6,6,6,6,8,6]
s_f_frame = pd.DataFrame(np.stack([group_membership_data,g_2],axis=1),columns=['SX 0','SX 1'])
metric_2_f = MetricFrame(metrics=skm.recall_score,y_pred=y_pred,y_true=y_true,sensitive_features=s_f_frame)
print(metric_2_f.overall)
print(metric_2_f.by_group)
输出结果如下:
上述代码中,我们唯一需要了解的是 np.stack()
方法,它用于将两个相同维度 array()
在某一维度上合并**「axis = 0 按 x 轴,axis = 1 按 y 轴」。**
a = np.array([1,2,3])
b = np.array([4,5,6])
np.stack([a,b],axis=1)
合并结果如下:
高级的机器学习算法通常使用度量函数来指导它们的优化。这样的算法通常和标量结果一起工作,所以如果我们希望根据公平性指标进行调优,就要在 MetricFrame 上执行聚合。
这一小节,我们用 fairlearn
提供的 make_derived_metric
方法来自定义 metric
。通过 make_derived_metric
基于四种基本 metric
来自定义,它们分别是 group_min
、group_max
、difference
和 ratio
「四选一」。
from fairlearn.metrics import make_derived_metric
fbeta_difference = make_derived_metric(metric=skm.fbeta_score,transform='difference') # 选择的 Base Metric 是 differebce,即最大 F_Beta 与最小 F_Beta 的差
fbeta_difference(y_true,y_pred,beta=0.7,sensitive_features = group_membership_data)
输出结果如下:
上述方法可以用以下代码来替代,区别是后者需要使用 functools.partial
fbeta_07 = functools.partial(skm.fbeta_score, beta=0.7)
fbeta_07_metrics = MetricFrame(metrics=fbeta_07, y_true=y_true, y_pred=y_pred, sensitive_features=group_membership_data)
fbeta_07_metrics.difference()
个人感觉下面的方法更好,因为它可以很方便的调用其他方法。
控制属性也称为条件属性,通过提供将数据分成子组的进一步方法,实现更加详细的公平性洞察。当数据被划分为子组时,控制属性的作用类似于敏感属性。不同的是,度量的 overall
值是在控制属性划分的各个子组上进行的**「其他方法如 group_max
、group_min
也是如此」**。
控制属性对于属性的一些预期变化情况是有用的,所以我们需要在控制属性的同时计算差异。例如,在一个贷款场景中,我们希望不同收入的人以不同的利率获得批准。但是,在每个收入阶层中,我们仍然希望测量不同敏感特征之间的差异。
在 MetricFrame 的一个构造方法中,提供了 control_features
参数。
decision = [
0,0,0,1,1,0,1,1,0,1,
0,1,0,1,0,1,0,1,0,1,
0,1,1,0,1,1,1,1,1,0
]
prediction = [
1,1,0,1,1,0,1,0,1,0,
1,0,1,0,1,1,1,0,0,0,
1,1,1,0,0,1,1,0,0,1
]
control_feature = [
'H','L','H','L','H','L','L','H','H','L',
'L','H','H','L','L','H','L','L','H','H',
'L','H','L','L','H','H','L','L','H','L'
]
sensitive_feature = [
'A','B','B','C','C','B','A','A','B','A',
'C','B','C','A','C','C','B','B','C','A',
'B','B','C','A','B','A','B','B','A','A'
]
metric_c_f = MetricFrame(metrics=skm.accuracy_score,
y_true=decision,
y_pred=prediction,
sensitive_features={'SF' : sensitive_feature},
control_features={'CF' : control_feature})
print(metric_c_f.overall)
print(metric_c_f.by_group)
结果如下:
对于控制属性我是这么理解的,当我们需要比较不同组之间的差异时使用敏感属性,如不同地区人民的收入水平。但如果我们想看组内不同小群体,如某地区的男性和女性收入水平时,就可以用到控制属性。
绘图主要用到 MetricFrame 的 plot
方法,它内部调用了 matplotlib
from fairlearn.metrics import false_positive_rate, true_positive_rate, selection_rate
from sklearn.metrics import accuracy_score, recall_score, precision_score
metrics = {
'accuracy': accuracy_score,
'precision': precision_score,
'recall': recall_score,
'false positive rate': false_positive_rate,
'true positive rate': true_positive_rate,
'selection rate': selection_rate,
'count': count}
metric_frame = MetricFrame(metrics=metrics,
y_true=y_true,
y_pred=y_pred,
sensitive_features=group_membership_data)
metric_frame.by_group.plot.bar(
subplots=True,
layout=[3, 3],
legend=False,
figsize=[12, 8],
title="Show all metrics",
)
在上一小节的图中,我们可以看出,不同的图 y
轴的范围是不同的,如果我们需要让它们的范围一致(方便比较),可以使用 plot()
方法中规定 ylim
参数来调节。
metric_frame.by_group.plot.bar(
subplots=True,
ylim=[0,1],
layout=[3, 3],
legend=False,
figsize=[12, 8],
title="Show all metrics",
)
结果如下:
到这里,可能有人要吐槽,上面的图选的什么阴间配色!!就不能自己选好看的颜色画吗?
这一点,matplotlib
在设计时就有考虑到,我们可以通过 colormap
参数来调节。更多好康的色系可以参考这里: 好康的色系
这里我们选用 accent
色系
metric_frame.by_group.plot.bar(
subplots=True,
ylim=[0,1],
layout=[3, 3],
legend=False,
figsize=[12, 8],
colormap = 'Accent',
title="Show all metrics",
)
结果如下:
到这里,可能小伙伴们又要问了,你这画的都是柱状图啊,只能画柱状图也太垃了吧!!
唉~~,作为一个专业的机器学习公平库,fairlearn
怎么可能没考虑到这点呢?**「其实是 Pandas 考虑到的」**我们可以通过 kind
参数来调节绘图的类型
metric_frame.by_group.plot(
kind = 'pie',
subplots=True,
ylim=[0,1],
layout=[3, 3],
legend=False,
figsize=[12, 8],
colormap = 'Accent',
title="Show all metrics",
)
结果如下:
那我们该怎么知道有哪些图形可以画呢?可以参考如下链接: 可绘制的类型