1、问题
观看了李宏毅老师的机器学习进化课程之可解释的机器学习,课程中对主要是针对黑盒模型进行白盒模型转化的技巧和方法进行了简单介绍,详细细节可以参考《Interpretable Machine Learning》。像一些线性模型、树形模型是可解释的ML model,但是,深度学习一直被称为“黑盒子”,是end-to-end模型,即忽略内部计算,只关心输入端和输出端。然后就有不少人想要知道深度学习模型训练之后学到了什么,是否真的是人类设想的,学到了某些物体的轮廓和纹理,又是如何判别的,这些疑问可以从中间结果、或将模型转换为可解释的ML model等多种途径解决。
可解释性(非数学定义):可解释性是一个人能够理解决策原因的程度
可解释的机器学习是它捕获“从机器学习模型中提取与数据中包含的活模型学习的关系相关的知识”。
1.1、可解释模型的分类
机器学习的可解释性的方法可以根据各种标准进行分类
(1)Intrinsic or post hoc?
该标准区分了可解释性是通过限制机器学习模型的复杂性(Intrinsic)还是通过应用训练后分析模型的方法(Post hoc)来实现。Intrinsic是指由于结构简单而被认为是可解释的机器学习模型,如线性模型、树形模型,Post hoc是训练后解释方法的应用,如排列特征重要性。
(2)Model-specific or model-agnostic??
模型是特定的还是不可知的,特定于模型(Model-specific)的解释工具仅限于特定的模型类。线性模型中回归权重的解释是特定于模型的解释,因为根据定义,内在可解释模型的解释总是特定于模型的。仅用于解释例如神经网络的工具是特定于模型的。与模型无关的工具可以用在任何机器学习模型上,并且在模型已经被训练(事后)之后被应用。这些不可知的方法(model-agnostic)通常通过分析特征输入和输出对来工作。根据定义,这些方法不能访问模型内部,如重量或结构信息。
(3)Local or global?
模型是局部还是整体的,即是解释单个预测还是整体模型。
2、任务
3、解析
3.1 任务一:计算梯度
我们都知道神经网络模型是分为forward和backward两个部分,前向传播(forward)是从输入端到输出端,通过各个网络层的隐节点产生输出。后向传播是定义一个损失函数,将损失函数的信息向后传播用以计算梯度,从而达到优化网络参数的目的。
3.1.1 前提
(1)stop_gradient
查看一个Tensor是否计算并传播梯度,如果stop_gradinet为true,则该Tensor不会计算梯度,并会阻绝Autograd的梯度传播,反之,则进行梯度计算和传播
(2)grad
查看一个Tensor的梯度,数据类型是numpy.ndarray。
(3)backward
调用backward,自动计算梯度,并将结果存在grad属性中。backward()会累积梯度,还提供了clear_grad()函数来清除当前Tensor的梯度。
(4)自动微分运行机制
飞浆的神经网络核心是自动微分(Tensorflow和Pytorch也有自动微分机制),飞桨的自动微分是通过trace 的方式,记录前向OP 的执行,并自动创建反向var 和添加相应的反向OP ,然后来实现反向梯度计算的。以a=wx+b为例,细节参考。
3.1.2 实现
(1)处理数据
import paddle
import os
import pandas
import cv2
import numpy as np
from paddle.io import Dataset,DataLoader
from paddlenlp.datasets import MapDataset
train_path = 'data/food-11/training'
val_path = 'data/food-11/validation'
test_path = 'data/food-11/testing'
#图片大小不一致
def data_loader(path):
filelist = os.listdir(path)
data = []
for i in filelist[:1000]:
img = cv2.imread(path+'/'+i)
img = cv2.resize(img,(512,512))
# 读入的图像数据格式是[H, W, C]
# 使用转置操作将其变成[C, H, W]
img = np.transpose(img, (2,0,1))
img = np.array(img,dtype='float32')
label = int(i.split('_')[0])
data.append((img,label))
return data
train_data = data_loader(train_path)
train_loader = paddle.io.DataLoader(MapDataset(train_data), return_list=True, shuffle=True,
batch_size=5, drop_last=True)
(2)定义模型
import paddle
import numpy as np
from paddle.nn import Conv2D, MaxPool2D, Linear
## 组网
import paddle.nn.functional as F
# 定义 LeNet 网络结构
class LeNet(paddle.nn.Layer):
def __init__(self, num_classes=1):
super(LeNet, self).__init__()
# 创建卷积和池化层
# 创建第1个卷积层
self.conv1 = Conv2D(in_channels=3, out_channels=32, kernel_size=128)
self.max_pool1 = MaxPool2D(kernel_size=4, stride=2)
# 尺寸的逻辑:池化层未改变通道数;当前通道数为6
# 创建第2个卷积层
self.conv2 = Conv2D(in_channels=32, out_channels=64, kernel_size=128)
self.max_pool2 = MaxPool2D(kernel_size=4, stride=2)
# 创建第3个卷积层
self.conv3 = Conv2D(in_channels=64, out_channels=128, kernel_size=64)
self.max_pool3 = MaxPool2D(kernel_size=4, stride=2)
# 创建第4个卷积层
self.conv4 = Conv2D(in_channels=128, out_channels=256, kernel_size=32)
self.max_pool4 = MaxPool2D(kernel_size=4, stride=2)
# 创建第5个卷积层
self.conv5 = Conv2D(in_channels=256, out_channels=512, kernel_size=16)
self.max_pool5 = MaxPool2D(kernel_size=4, stride=2)
# 创建第6个卷积层
self.conv6 = Conv2D(in_channels=512, out_channels=1024, kernel_size=8)
self.max_pool6 = MaxPool2D(kernel_size=4, stride=2)
# 尺寸的逻辑:输入层将数据拉平[B,C,H,W] -> [B,C*H*W]
# 输入size是[28,28],经过三次卷积和两次池化之后,C*H*W等于120
self.fc1 = Linear(in_features=1024, out_features=64)
# 创建全连接层,第一个全连接层的输出神经元个数为64, 第二个全连接层输出神经元个数为分类标签的类别数
self.fc2 = Linear(in_features=64, out_features=num_classes)
# 网络的前向计算过程
def forward(self, x):
x = self.conv1(x)
# 每个卷积层使用Sigmoid激活函数,后面跟着一个2x2的池化
x = F.sigmoid(x)
x = self.max_pool1(x)
x = F.sigmoid(x)
x = self.conv2(x)
x = self.max_pool2(x)
x = self.conv3(x)
x = self.max_pool3(x)
x = self.conv4(x)
x = self.max_pool4(x)
x = self.conv5(x)
x = self.max_pool5(x)
x = self.conv6(x)
x = self.max_pool6(x)
# 尺寸的逻辑:输入层将数据拉平[B,C,H,W] -> [B,C*H*W]
x = paddle.reshape(x, [x.shape[0], -1])
x = self.fc1(x)
x = F.sigmoid(x)
x = self.fc2(x)
return x
model_pre = paddle.Model(model)
(3)训练,并保存梯度数据
def normalize(image):
return (image - image.min()) / (image.max() - image.min())
def compute_saliency_maps(x, y, model):
# 计算梯度
x.stop_gradient = False
#模型训练
y_pred = model(x)
loss_func = paddle.nn.loss.CrossEntropyLoss()
loss = loss_func(y_pred, y)
#反向传播
loss.backward()
#获取反向传播的梯度
saliencies = np.abs(x.grad)
# saliencies: (batches, channels, height, weight)
# 因为接下来我们要对每张图片画 saliency map,每张图片的 gradient scale 很可能有巨大落差
# 可能第一张图片的 gradient 在 100 ~ 1000,但第二张图片的 gradient 在 0.001 ~ 0.0001
# 如果我们用同样的色阶去画每一张 saliency 的话,第一张可能就全部都很亮,第二张就全部都很暗,
# 如此就看不到有意义的结果,我们想看的是「单一张 saliency 内部的大小关係」,
# 所以这边我们要对每张 saliency 各自做 normalize。手法有很多种,这边只採用最简单的
saliencies = np.stack([normalize(item) for item in saliencies])
return saliencies
#这里遍历了所有数据,但是只存最后一组,
for images,labels in train_loader:
# print(images,labels)
saliencies = compute_saliency_maps(images, labels, model)
(4)绘图
import cv2
import matplotlib.pyplot as plt
import paddle
# 使用 matplotlib 画出来,batch_size=5,默认画5张
fig, axs = plt.subplots(2, 5, figsize=(15, 8))
index = 0
saliencies = paddle.to_tensor(saliencies)
for row, target in enumerate([images, saliencies]):
for column, img in enumerate(target):
img = img.numpy()
axs[row][column].imshow(img[0])
plt.show()
plt.close()
3.2 任务二:中间结果展示
CNN模型在卷积的过程中,我们认为不同的滤波器能学习到不同的图像纹理或边缘特征,但是模型训练是一体化,并不展示中间结果,因此,hook机制应运而生。
3.2.1 前提
(1)model的执行模式
模型的执行模式有两种,如果需要训练的话调用?train() ?,如果只进行前向执行则调用?eval()
(2)Hook的应用
hook是一个作用于变量的自定义函数,在模型执行时调用。对于注册在层上的hook函数,可以分为pre_hook和post_hook两种。pre_hook可以对层的输入变量进行处理,用函数的返回值作为新的变量参与层的计算。post_hook则可以对层的输出变量进行处理,将层的输出进行进一步处理后,用函数的返回值作为层计算的输出。细节参考
? ? ? ? hook的实现步骤:
? ? ? ? 1. 定义一个对feature进行处理的函数,比如叫hook_fun
? ? ? ? 2. 注册hook:告诉模型,我将在哪些层使用hook_fun来处理feature
- register_forward_pre_hook(pre_hook)
import paddle
import numpy as np
# the forward_post_hook change the input of the layer: input = input * 2
def forward_pre_hook(layer, input):
# user can use layer and input for information statistis tasks
# change the input
input_return = (input[0] * 2)
return input_return
linear = paddle.nn.Linear(13, 5)
# register the hook
forward_pre_hook_handle = linear.register_forward_pre_hook(forward_pre_hook)
value0 = np.arange(26).reshape(2, 13).astype("float32")
in0 = paddle.to_tensor(value0)
out0 = linear(in0)
# remove the hook
forward_pre_hook_handle.remove()
value1 = value0 * 2
in1 = paddle.to_tensor(value1)
out1 = linear(in1)
# hook change the linear's input to input * 2, so out0 is equal to out1.
assert (out0.numpy() == out1.numpy()).any()
- register_forward_post_hook(post_hook)
import paddle
import numpy as np
# the forward_post_hook change the output of the layer: output = output * 2
def forward_post_hook(layer, input, output):
# user can use layer, input and output for information statistis tasks
# change the output
return output * 2
linear = paddle.nn.Linear(13, 5)
# register the hook
forward_post_hook_handle = linear.register_forward_post_hook(forward_post_hook)
value1 = np.arange(26).reshape(2, 13).astype("float32")
in1 = paddle.to_tensor(value1)
out0 = linear(in1)
# remove the hook
forward_post_hook_handle.remove()
out1 = linear(in1)
# hook change the linear's output to output * 2, so out0 is equal to out1 * 2.
assert (out0.numpy() == (out1.numpy()) * 2).any()
3.2.2 实现
模型是基于任务一,可以通过summary()打印网络的基础结构和参数信息,即
model_pre.summary()
模型结构是6层卷积池化层,2层全连接层,我们通过hook机制来输出中间结果,可以定一个全局变量,来记录中间结果值。
#定义hook函数
def hook(model,input,output):
global ll
ll = output
#对模型第三层卷积层进行输出
hook_ll = model.conv3.register_forward_post_hook(hook)
#遍历数据,并代入到模型中
for i in train_loader:
model(i[0])
#待执行完,ll变量里边存储中间结果数据,是一个四维数组,
#第一维是train_loader里边的batch_size个数,即图片个数
#第二维是滤波器的个数
#第三维和第四维是当前卷积层卷积之后的output的宽高
print(ll[:,1,:,:])
画图:
import cv2
import matplotlib.pyplot as plt
import paddle
fig, axs = plt.subplots(1, 5, figsize=(15, 6))
for i,img in enumerate(ll[:,1,:,:]) :
# print(i,img)
img = img.numpy()
axs[i].imshow(img)
plt.show()
plt.close()
3.3 任务三:LIME & SHAP 的应用
3.3.1 LIME & SHAP的工具使用
(1)LIME
LIME(Local?Interpretable?Model-AgnosticExplanations)算法是为了解释某个样本在模型中的信息,帮助理解模型。原理参考
以iris数据集为数据源的LIME实例:
import lime
import sklearn
import numpy as np
import sklearn.ensemble
import sklearn.metrics
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.ensemble import GradientBoostingClassifier
from lime.lime_tabular import LimeTabularExplainer
#读取数据
# categories = ['alt.atheism', 'soc.religion.christian']
# newsgroups_train = fetch_20newsgroups(subset='train', categories=categories)
# newsgroups_test = fetch_20newsgroups(subset='test', categories=categories)
# class_names = ['atheism', 'christian']
# data = pd.DataFrame(newsgroups_train.data)
data = load_iris()
feature_names = data.feature_names
class_names = data.target_names
#利用GBDT分类模型区分是否违约
x =data.data
y = data.target
gbdt = GradientBoostingClassifier()
gbdt = gbdt.fit(x[:140],y[:140])
#直接将训练数据作为预测数据
pred = gbdt.score(x[140:],y[140:])
#中文字体显示
plt.rc('font', family='SimHei', size=13)
#建立解释器
explainer = LimeTabularExplainer(x, feature_names=feature_names, class_names=class_names)
#解释第81个样本的规则
exp = explainer.explain_instance(x[81], gbdt.predict_proba)
#画图
fig = exp.as_pyplot_figure()
#画分析图
exp.show_in_notebook(show_table=True, show_all=False)
(2)SHAP
SHAP(SHapley?Additive exPlanation)是另一种可解释方法的模型 。具体实现细节就不深究了。但是,提供了多种模型的解释器:
细节参考
(1)特征重要性
使用XGboost模型去训练iris数据集,并通过特征重要性排序,来解释各项特征对模型的影响力。
import xgboost as xgb
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt; plt.style.use('seaborn')
import sklearn
import numpy as np
import sklearn.ensemble
import sklearn.metrics
from sklearn.datasets import load_iris
from sklearn.ensemble import GradientBoostingClassifier
#读取数据
# categories = ['alt.atheism', 'soc.religion.christian']
# newsgroups_train = fetch_20newsgroups(subset='train', categories=categories)
# newsgroups_test = fetch_20newsgroups(subset='test', categories=categories)
# class_names = ['atheism', 'christian']
# data = pd.DataFrame(newsgroups_train.data)
data = load_iris()
feature_names = data.feature_names
class_names = data.target_names
#利用GBDT分类模型区分是否违约
x =data.data
y = data.target
# 训练xgboost回归模型
model = xgb.XGBRegressor(max_depth=4, learning_rate=0.05, n_estimators=150)
model.fit(x, y)
# 获取feature importance
plt.figure(figsize=(15, 5))
plt.bar(range(len(feature_names)), model.feature_importances_)
plt.xticks(range(len(feature_names)), feature_names, rotation=-45, fontsize=14)
plt.title('Feature importance', fontsize=14)
(2)通过shap进行分析,计算shap_values值
import shap
# model是在第1节中训练的模型
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(x)
print(shap_values.shape)
获取单个样本的shape值
j = 30
f_explainer = pd.DataFrame()
f_explainer['feature'] = feature_names
f_explainer['feature_value'] = x[j]
f_explainer['shap_value'] = shap_values[j]
print(f_explainer)
确定模型的基线:
#基线值就是训练集的目标变量的拟合值的均值
#一个样本中各特征SHAP值的和加上基线值应该等于该样本的预测值
y_base = explainer.expected_value
print(y_base)
绘图
shap.initjs()
shap.force_plot(explainer.expected_value, shap_values[j], x[j])
#对特征总体进行分析
shap.summary_plot(shap_values, x)
#对特征总体进行分析,绘制柱形图
shap.summary_plot(shap_values,x , plot_type="bar")
#某个特征对shap_value的影响
shap.dependence_plot('Feature 2', shap_values, x, interaction_index=None, show=False)
?多变量之间的分析
#多个变量的交互分析
shap_interaction_values = shap.TreeExplainer(model).shap_interaction_values(x)
shap.summary_plot(shap_interaction_values, x, max_display=4)
#两个变量交互下变量对目标值的影响
shap.dependence_plot('Feature 2', shap_values,x , interaction_index='Feature 1', show=False)
3.3.2 实现
(1)LIME
from lime.lime_image import LimeImageExplainer
#处理数据
images=[]
labels = []
for image,label in train_loader:
for i in image:
i = i.numpy()
images.append(i)
for j in label:
j = j.numpy()[0]
# images[i]=image
labels.append(j)
def predict(images):
images = np.transpose(np.array(images), (0,3,1,2))
images = paddle.to_tensor(images)
output = model(images)
return output.detach().numpy()
#此处要注意顺序,model的data格式是[batch_size,channels,height,weight]
# lime的data顺序是[batch_size,height,weight,channels]
lime_ex = LimeImageExplainer().explain_instance(image= np.transpose(np.array(images), (0,2,3,1))[0],classifier_fn=predict,labels=labels[0])
lime_img, mask = lime_ex.get_image_and_mask(label=int(labels[0]))
import matplotlib.pyplot as plt
plt.imshow(lime_img)
plt.show()
(2)SHAP
?SHAP的DeepExplainer解释器,仅支持pytorch和tensorflow框架,需要用该框架定义model。鉴于下载数据集的麻烦,忽略。
参考:
李宏毅课程-机器学习进阶-作业1-卷积神经网络的可解释性 - 飞桨AI Studio - 人工智能学习与实训社区
|