Mạng nơ-ron là công cụ hấp dẫn và rất hiệu quả đối với các nhà khoa học dữ liệu, nhưng chúng có một lỗ hổng rất lớn: chúng là những hộp đen không thể giải thích được. Trên thực tế, nó không cung cấp cho chúng ta bất kỳ thông tin nào về tầm quan trọng của các thuộc tính. May mắn thay, có một cách tiếp cận mạnh mẽ mà chúng ta có thể sử dụng để diễn giải mọi mô hình, thậm chí cả mạng nơ-ron. Đó là cách tiếp cận SHAP.
Bài viết trình bày cách sử dụng SHAP để giải thích và diễn giải mạng nơ-ron bằng Python.
SHAP là gì?
SHAP là viết tắt của SHapley Additive exPlanation. Đó là một cách để tính toán tác động của một feature đến giá trị của biến mục tiêu. Ý tưởng là bạn phải coi mỗi tính năng như một người chơi và tập dữ liệu như một đội. Mỗi người chơi đóng góp vào kết quả của đội.
Tổng của những đóng góp này cho chúng ta giá trị của biến mục tiêu với một số giá trị của các đối tượng.
Ý tưởng chính là tác động của một feature không chỉ dựa vào một feature duy nhất mà là toàn bộ tập hợp các feature trong tập dữ liệu.
Vì vậy, SHAP tính toán tác động của mọi đối tượng đến biến mục tiêu (được gọi là giá trị shap) bằng cách sử dụng phép tính tổ hợp và retraining mô hình trên tất cả sự kết hợp của các feature mà chúng ta đang xem xét.
Giá trị tuyệt đối trung bình của impact of a feature đối với một biến mục tiêu có thể được sử dụng làm thước đo tầm quan trọng của nó.
Lợi ích của SHAP là nó không quan tâm đến mô hình chúng ta sử dụng. Trên thực tế, đó là một cách tiếp cận theo mô hình bất khả tri. Vì vậy, thật hoàn hảo khi giải thích những mô hình không cung cấp cho chúng ta cách giải thích riêng của chúng về tầm quan trọng của tính năng, chẳng hạn như mạng thần kinh.
Ví dụ sử dụng SHAP trong Python với mạng nơ-ron
Trong ví dụ này, chúng ta sẽ tính toán tác động của feature bằng cách sử dụng SHAP cho một mạng nơ-ron sử dụng Python và Pytorch.
Đối với ví dụ này, chúng ta sẽ sử dụng tập dữ liệu bệnh tiểu đường của scikit-learning, là tập dữ liệu hồi quy.
Đầu tiên chúng ta hãy cài đặt thư viện shap.
!pip install shap
cài thêm cả sklearn
mình dùng vài hàm hỗ trợ việc chia dữ liệu train và test
pip install scikit-learn
Cài thêm cả thư viện pytourch vì mình sẽ xây dựng một mạng nơ ron từ nó
pip install torch torchvision
import các thư viện cần thiết
import shap
from sklearn.model_selection import train_test_split
import torch
import torch.nn as nn
import torch.nn.functional as F
shap.initjs() # để hiển thị đồ thị trên notebook nếu bạn dùng notebook
Chuẩn bị dữ liệu
Chúng ta sẽ sử dụng bộ dữ liệu adult, bộ dữ liệu phân loại một người có thu nhập > 50k$ hay không chi tiết dataset tại đây
Chúng ta thực hiện việc chuẩn hoá dữ liệu bằng cách trừ đi giá trị trung bình và chia cho độ lệch chuẩn, điều này cần cho quá trình hội tụ
X,y = shap.datasets.adult()
X_display,y_display = shap.datasets.adult(display=True)
# normalize data (this is important for model convergence)
dtypes = list(zip(X.dtypes.index, map(str, X.dtypes)))
for k,dtype in dtypes:
if dtype == "float32":
X[k] -= X[k].mean()
X[k] /= X[k].std()
X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.2, random_state=7)
tập dữ liệu của chúng ta trông sẽ như thế này.
Vì bộ dữ liệu này đang ở dạng pandas nên mình chuyển nó thành dạng numpy vì những function phía sau của mình nhận đầu vào như vậy:
X_train = X_train.to_numpy()
X_valid = X_valid.to_numpy()
Mình cũng chuyển dữ liệu dành dạng tensor
để có thể làm đầu vào cho pytorch
X_train = torch.FloatTensor(X_train)
X_test = torch.FloatTensor(X_valid)
y_train = torch.LongTensor(y_train)
y_test = torch.LongTensor(y_valid)
Khởi tạo mô hình mạng nơ ron ANN
mình khởi tạo một mạng nơ ron với input là 12
output là 2
và active function cuối cùng là một hàm softmax
trả về xác suất của mỗi lớp ở output
class ANN_Model(nn.Module):
def __init__(self, input_features = 12, hidden1 = 20, hidden2 = 20, out_features = 2):
super().__init__()
self.f_connected1 = nn.Linear(input_features, hidden1)
self.f_connected2 = nn.Linear(hidden1, hidden2)
self.out = nn.Linear(hidden2, out_features)
def forward(self, x):
x = F.relu(self.f_connected1(x))
x = F.relu(self.f_connected2(x))
x = F.softmax(self.out(x), dim=1)
return x
Tiếp theo bạn tạo một intanse của model, ngoài ra các bạn có thể xem thêm bài viết về cách khởi tạo tham số của mạng nơ ron của mình.
torch.manual_seed(20)
model = ANN_Model()
Train model
loss_function = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr = 0.01)
epochs = 10000
final_loss = []
for i in range(epochs):
i = i + 1
y_pred = model.forward(X_train)
loss = loss_function(y_pred, y_train)
final_loss.append(loss)
## after eveery 10 epochs print this
if i % 10 == 1:
print('Epoch number: {} and the loss: {}'.format(i, loss.item()))
optimizer.zero_grad()
loss.backward()
optimizer.step()
Log training sẽ như này
.....
Epoch number: 9881 and the loss: 0.4373455345630646
Epoch number: 9891 and the loss: 0.43858158588409424
Epoch number: 9901 and the loss: 0.4376521110534668
Epoch number: 9911 and the loss: 0.4376943111419678
Epoch number: 9921 and the loss: 0.4389132261276245
Epoch number: 9931 and the loss: 0.43802937865257263
Epoch number: 9941 and the loss: 0.4382016062736511
Epoch number: 9951 and the loss: 0.4388658404350281
Epoch number: 9961 and the loss: 0.43871062994003296
Epoch number: 9971 and the loss: 0.43797677755355835
Epoch number: 9981 and the loss: 0.43761739134788513
Epoch number: 9991 and the loss: 0.43811142444610596
Chúng ta thử dự đoán trên tập test
y_pred = model.forward(X_test)
y_pred
Kết quả sẽ được như này
tensor([[1.0000e+00, 1.3362e-25],
[1.0000e+00, 0.0000e+00],
[1.0000e+00, 2.3968e-39],
...,
[1.0000e+00, 0.0000e+00],
[1.0000e+00, 0.0000e+00],
[8.2758e-01, 1.7242e-01]], grad_fn=<SoftmaxBackward0>)
Mình chuyển kết quả ở trên thành nhãn
labels_again = torch.argmax(y_pred, dim=1)
l = labels_again.cpu().detach().numpy()
Rồi sau đó đánh giá model mình vừa đào tạo
# calculate accuracy
from sklearn import metrics
print(metrics.accuracy_score(y_test, l))
Kết quả training vớ vẩn của mình vừa tạo mà nó cũng đc > 80% chắc do tập data này sạch sẽ chứ thực tế thì data nó hỗn loạn hơn nhiều :3
0.8473821587594043
Sử dụng shap
Shap có thể giải thích bất kỳ một hàm nào, nên mình viết một hàm f(x)
hàm này có nhiệm vụ dự đoán và thực hiện chuyển đổi dữ liệu đầu vào để phù hợp hơn với shap. Mình chỉ chuyển đổi dữ liệu input thành dạng Tensor
để có thể đưa nó cho mạng nơ ron trong pytorch
def f(X):
a = torch.FloatTensor(X)
y_pred = model.forward(a)
labels_again = torch.argmax(y_pred, dim=1)
return labels_again.flatten().cpu().detach().numpy()
sử dụng shap kernel Explainer
Mình nhấn mạnh là các bạn có thể giải thích bất kỳ hàm nào với shap và cái mạng nơ ron thì cũng chỉ coi nó như một hàm thôi
explainer = shap.KernelExplainer(f, X_test.cpu().detach().numpy())
Tiếp theo là các bạn lấy shap value và in đồ thị của một điểm dữ liệu
shap_values = explainer.shap_values(X.iloc[299,:], nsamples=500)
shap.force_plot(explainer.expected_value, shap_values, X_display.iloc[299,:])
Biểu đồ này cho bạn biết 0.0 là giá trị dự đoán của mô hình, và các lực nào đã đóng góp theo từng thuộc tính để kéo kết quả như vậy. Màu xanh thể hiện những biến đã tác động làm cho giá trị dự đoán nhỏ hơn và màu đỏ thì làm cho nó cao hơn. Độ rộng của một thuộc tính cũng thể hiện nó đã đóng góp mạnh như thế nào
Xem chi tiết sự ảnh hưởng của từng feature
shap.force_plot(explainer.expected_value, shap_values50, X_display.iloc[280:330,:])
Qua cái này bạn có thể thấy những ông chồng đã đóng góp đẩy thu nhập gia đình cao hơn (màu đỏ husband), . Và chỉ có một số ít bà vợ đóng góp làm cho thu nhập gia đình tăng lên. Những người độc thân chủ yếu làm cho nhãn thu nhập dưới 50k$ là chủ yếu
Kết luận
Như vậy mình vừa hướng dẫn cho các bạn sử dụng shap để giải thích dữ liệu giúp bạn hiểu rõ hơn thuộc tính nào đã đóng góp và đóng góp bao nhiêu vào kết quả của mô hình.