Skip to content
This repository has been archived by the owner on Nov 16, 2023. It is now read-only.

V chguan/add icml ex nlp code #90

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
234 changes: 234 additions & 0 deletions scenarios/interpret_NLP_models/explain_saved_model.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @Frozenmad, these two notebooks are pretty small. Do you think it make sense to merge them?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll try to merge them together~

"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"*Copyright (c) Microsoft Corporation. All rights reserved.*\n",
"\n",
"*Licensed under the MIT License.*\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Explain a saved pytorch NLP model\n",
"\n",
"This is a torturial on how to explain any layer in a saved pytorch model. We use the 3rd layer of the pre-trained BERT-base (12 layers) model for simplicity as an example.\n",
"\n",
"This torturial assumes you've already read another [Torturial on explain simple model](./explain_simple_model.ipynb). If you haven't, you may need to go through that first."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from pytorch_pretrained_bert import BertModel, BertTokenizer\n",
"import logging\n",
"from urllib import request\n",
"import torch\n",
"import json\n",
"import ssl\n",
"import sys\n",
"\n",
"sys.path.append(\"../../\")\n",
"\n",
"from utils_nlp.interpreter.Interpreter import Interpreter\n",
"\n",
"logging.getLogger().setLevel(logging.WARNING)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 0 Prepare Phi, x, words and regularization term\n",
"\n",
"we first load the pre-trained model we need to explain and define the sentence we use in our case. Suppose the sentence we want to study is `rare bird has more than enough charm to make it memorable.`, and the layer we need to explain is the 3rd layer."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
"\n",
"# suppose the sentence is as following\n",
"text = \"rare bird has more than enough charm to make it memorable.\"\n",
"\n",
"# get the tokenized words.\n",
"tokenizer = BertTokenizer.from_pretrained(\"bert-base-uncased\")\n",
"words = [\"[CLS]\"] + tokenizer.tokenize(text) + [\"[SEP]\"]\n",
"\n",
"# load BERT base model\n",
"model = BertModel.from_pretrained(\"bert-base-uncased\").to(device)\n",
"for param in model.parameters():\n",
" param.requires_grad = False\n",
"model.eval()\n",
"\n",
"# get the x (here we get x by hacking the code in the pytorch_pretrained_bert package)\n",
"tokenized_ids = tokenizer.convert_tokens_to_ids(words)\n",
"segment_ids = [0 for _ in range(len(words))]\n",
"token_tensor = torch.tensor([tokenized_ids], device=device)\n",
"segment_tensor = torch.tensor([segment_ids], device=device)\n",
"x = model.embeddings(token_tensor, segment_tensor)[0]\n",
"\n",
"# extract the Phi we need to explain, suppose the layer we are interested in is layer 3\n",
"def Phi(x):\n",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor suggestion here, maybe we can add a parameter called layer to make this function general, instead of harcoding the layer 3 inside the code

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea!

" global model\n",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is making global the model needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that the code can still run without this line hmmmmm.
I'll remove this line 0.0. Originally, I was thinking to make the model global so that we don't need to load the model we need every time we call Phi().

" x = x.unsqueeze(0)\n",
" attention_mask = torch.ones(x.shape[:2]).to(x.device)\n",
" extended_attention_mask = attention_mask.unsqueeze(1).unsqueeze(2)\n",
" extended_attention_mask = extended_attention_mask.to(dtype=torch.float)\n",
" extended_attention_mask = (1.0 - extended_attention_mask) * -10000.0\n",
" # extract the 3rd layer\n",
" model_list = model.encoder.layer[:3]\n",
" hidden_states = x\n",
" for layer_module in model_list:\n",
" hidden_states = layer_module(hidden_states, extended_attention_mask)\n",
" return hidden_states[0]"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"# here, we load the regularization we already calculated for simplicity\n",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here could you just print the regularization value? out of curiosity, how did you get this value?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add the print here. We get this by sampling. We first sample part of the training data of BERT, then pass them to BERT and collect the 3rd hidden state and calculate the std values of every dimension of them (the std values are the regular values).

"data = request.urlopen(\"https://nlpbp.blob.core.windows.net/data/regular.json\").read()\n",
"regularization = json.loads(data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1 Explain the Phi and x\n",
"\n",
"We first initialize an interpreter class, and pass necessary parameters in it."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"interpreter = Interpreter(x=x, Phi=Phi, regularization=regularization, words=words).to(\n",
" device\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We optimize the interpreter to get the result."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"interpreter.optimize(iteration=5000, lr=0.01, show_progress=False)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Then, we can get the sigma we have optimized"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"array([0.17282525, 0.14378461, 0.1557987 , 0.2305536 , 0.20187221,\n",
" 0.21250647, 0.19196394, 0.13568228, 0.26310277, 0.23032397,\n",
" 0.24152805, 0.13371354, 0.2737328 , 0.36158693], dtype=float32)"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interpreter.get_sigma()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Finally, we visualize the explained result."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 432x288 with 2 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"interpreter.visualize()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can see that the word 'rare', 'bird', 'charm', 'memorable' is important to the third layer."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.2"
}
},
"nbformat": 4,
"nbformat_minor": 2
}