李宏毅的可解释模型——三个任务
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 MapDatasettrain_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 datatrain_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等于120self.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 xmodel_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)
- register_forward_post_hook(post_hook)
3.2.2 實現
模型是基于任務一,可以通過summary()打印網絡的基礎結構和參數信息,即
model_pre.summary()模型結構是6層卷積池化層,2層全連接層,我們通過hook機制來輸出中間結果,可以定一個全局變量,來記錄中間結果值。
#定義hook函數 def hook(model,input,output):global llll = 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.targetgbdt = 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]=imagelabels.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 - 人工智能學習與實訓社區
總結
以上是生活随笔為你收集整理的李宏毅的可解释模型——三个任务的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 嵌入式系统与人工智能
- 下一篇: 壁纸服务的启动过程
