Display LIME TextExplainer with Streamlit

Dear All,

Please I am developing a Text Classifier and will like to display LIME TextExplainer. Unlike SHAP, the output is html/d3

My code so far:

import streamlit as st 
import sklearn
import lime
import numpy as np
import sklearn
import sklearn.ensemble
import sklearn.metrics
from sklearn.pipeline import make_pipeline
from lime.lime_text import LimeTextExplainer
from lime import lime_text
from matplotlib import pyplot as plt

from sklearn.datasets import fetch_20newsgroups
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']



vectorizer = sklearn.feature_extraction.text.TfidfVectorizer(lowercase=False)
train_vectors = vectorizer.fit_transform(newsgroups_train.data)
test_vectors = vectorizer.transform(newsgroups_test.data)


rf = sklearn.ensemble.RandomForestClassifier(n_estimators=500)
rf.fit(train_vectors, newsgroups_train.target)


c = make_pipeline(vectorizer, rf)
explainer = LimeTextExplainer(class_names=class_names)



idx = 83
exp = explainer.explain_instance(newsgroups_test.data[idx], c.predict_proba, num_features=6)


def main():

	st.title("Newsgroup Classifier")
	
	st.write(f"Document id = {idx}")
	st.write(f"Probability(christian) = {c.predict_proba([newsgroups_test.data[idx]])[0,1]}")
	st.write(f"True class:  {class_names[newsgroups_test.target[idx]]}")
	
	exp.as_pyplot_figure()
	st.pyplot()
	plt.clf()

	exp.show_in_notebook(text=True)

	with open("result.html", "r", encoding='utf-8') as f:
		html = f.read()
	
	st.markdown(html, unsafe_allow_html=True)
    

if __name__ == '__main__':
	main()

Hi @iEvidently, thanks a lot for your question. Can you point at what exactly is your problem? I am assuming it is in the

st.markdown(html, unsafe_allow_html=True)

We do advise not to use the unsafe_allow_html=True option if not to experiment. We are working at creating a better API for html injection that is more safe and easier to use. That said, the feature it is supposed to work. To give you an example, the following statement works on Streamlit 0.50.2:

import streamlit as st
st.markdown("""
<div style='background-color: red; border: 1px solid black; color: black;height:100px;width:100px; text-align: center;'>
This is a box with red background
</div>
""", unsafe_allow_html=True)

Can you narrow down your problem to a piece of html as above? If we find that this is breaking, I will open a bug on the unsafe_allow_html being broken.

Also, this is a link to a feature request to add support for better html injection: https://github.com/streamlit/streamlit/issues/686. Feel free to comment or contribute on that.

Best,
Matteo

Hi Matteo,

Thanks a lot for the reply.

Instead of showing the following, the app is not rendering the html properly, just showing the codes:

This is what is showing:

@iEvidently - yeah, this definitely looks like a bug. Can you post an example result.html that causes this to happen, so we can investigate further?

Hi Tim,

Thanks a lot. The result.html is very long and can not upload html file.

Please see below:

<html>
        <meta http-equiv="content-type" content="text/html; charset=UTF8">
        <head><script>var lime =
/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId])
/******/ 			return installedModules[moduleId].exports;
/******/
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			exports: {},
/******/ 			id: moduleId,
/******/ 			loaded: false
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.loaded = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

	/* WEBPACK VAR INJECTION */(function(global) {'use strict';
	
	Object.defineProperty(exports, "__esModule", {
	  value: true
	});
	exports.PredictedValue = exports.PredictProba = exports.Barchart = exports.Explanation = undefined;
	
	var _explanation = __webpack_require__(1);
	
	var _explanation2 = _interopRequireDefault(_explanation);
	
	var _bar_chart = __webpack_require__(3);
	
	var _bar_chart2 = _interopRequireDefault(_bar_chart);
	
	var _predict_proba = __webpack_require__(6);
	
	var _predict_proba2 = _interopRequireDefault(_predict_proba);
	
	var _predicted_value = __webpack_require__(7);
	
	var _predicted_value2 = _interopRequireDefault(_predicted_value);
	
	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
	
	if (!global._babelPolyfill) {
	  __webpack_require__(8);
	}
	
	__webpack_require__(339);
	
	exports.Explanation = _explanation2.default;
	exports.Barchart = _bar_chart2.default;
	exports.PredictProba = _predict_proba2.default;
	exports.PredictedValue = _predicted_value2.default;
	//require('style-loader');
	/* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))

/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {

	'use strict';
	
	Object.defineProperty(exports, "__esModule", {
	  value: true
	});
	
	var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
	
	var _d2 = __webpack_require__(2);
	
	var _d3 = _interopRequireDefault(_d2);
	
	var _bar_chart = __webpack_require__(3);
	
	var _bar_chart2 = _interopRequireDefault(_bar_chart);
	
	var _lodash = __webpack_require__(4);
	
	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
	
	function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
	
	var Explanation = function () {
	  function Explanation(class_names) {
	    _classCallCheck(this, Explanation);
	
	    this.names = class_names;
	    if (class_names.length < 10) {
	      this.colors = _d3.default.scale.category10().domain(this.names);
	      this.colors_i = _d3.default.scale.category10().domain((0, _lodash.range)(this.names.length));
	    } else {
	      this.colors = _d3.default.scale.category20().domain(this.names);
	      this.colors_i = _d3.default.scale.category20().domain((0, _lodash.range)(this.names.length));
	    }
	  }
	  // exp: [(feature-name, weight), ...]
	  // label: int
	  // div: d3 selection
	
	
	  Explanation.prototype.show = function show(exp, label, div) {
	    var svg = div.append('svg').style('width', '100%');
	    var colors = ['#5F9EA0', this.colors_i(label)];
	    var names = ['NOT ' + this.names[label], this.names[label]];
	    if (this.names.length == 2) {
	      colors = [this.colors_i(0), this.colors_i(1)];
	      names = this.names;
	    }
	    var plot = new _bar_chart2.default(svg, exp, true, names, colors, true, 10);
	    svg.style('height', plot.svg_height + 'px');
	  };
	  // exp has all ocurrences of words, with start index and weight:
	  // exp = [('word', 132, -0.13), ('word3', 111, 1.3)
	
	
	  Explanation.prototype.show_raw_text = function show_raw_text(exp, label, raw, div) {
	    var opacity = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
	
	    //let colors=['#5F9EA0', this.colors(this.exp['class'])];
	    var colors = ['#5F9EA0', this.colors_i(label)];
	    if (this.names.length == 2) {
	      colors = [this.colors_i(0), this.colors_i(1)];
	    }
	    var word_lists = [[], []];
	    var max_weight = -1;
	    var _iteratorNormalCompletion = true;
	    var _didIteratorError = false;
	    var _iteratorError = undefined;
	
	    try {
	      for (var _iterator = exp[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
	        var _step$value = _slicedToArray(_step.value, 3),
	            word = _step$value[0],
	            start = _step$value[1],
	            weight = _step$value[2];
	
	        if (weight > 0) {
	          word_lists[1].push([start, start + word.length, weight]);
	        } else {
	          word_lists[0].push([start, start + word.length, -weight]);
	        }
	        max_weight = Math.max(max_weight, Math.abs(weight));
	      }
	    } catch (err) {
	      _didIteratorError = true;
	      _iteratorError = err;
	    } finally {
	      try {
	        if (!_iteratorNormalCompletion && _iterator.return) {
	          _iterator.return();
	        }
	      } finally {
	        if (_didIteratorError) {
	          throw _iteratorError;
	        }
	      }
	    }
	
	    if (!opacity) {
	      max_weight = 0;
	    }
	    this.display_raw_text(div, raw, word_lists, colors, max_weight, true);
	  };
	  // exp is list of (feature_name, value, weight)
	
	
	  Explanation.prototype.show_raw_tabular = function show_raw_tabular(exp, label, div) {
	    div.classed('lime', true).classed('table_div', true);
	    var colors = ['#5F9EA0', this.colors_i(label)];
	    if (this.names.length == 2) {
	      colors = [this.colors_i(0), this.colors_i(1)];
	    }
	    var table = div.append('table');
	    var thead = table.append('tr');
	    thead.append('td').text('Feature');
	    thead.append('td').text('Value');
	    thead.style('color', 'black').style('font-size', '20px');
	    var _iteratorNormalCompletion2 = true;
	    var _didIteratorError2 = false;
	    var _iteratorError2 = undefined;
	
	    try {
	      for (var _iterator2 = exp[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
	        var _step2$value = _slicedToArray(_step2.value, 3),
	            fname = _step2$value[0],
	            value = _step2$value[1],
	            weight = _step2$value[2];
	
	        var tr = table.append('tr');
	        tr.style('border-style', 'hidden');
	        tr.append('td').text(fname);
	        tr.append('td').text(value);
	        if (weight > 0) {
	          tr.style('background-color', colors[1]);
	        } else if (weight < 0) {
	          tr.style('background-color', colors[0]);
	        } else {
	          tr.style('color', 'black');
	        }
	      }
	    } catch (err) {
	      _didIteratorError2 = true;
	      _iteratorError2 = err;
	    } finally {
	      try {
	        if (!_iteratorNormalCompletion2 && _iterator2.return) {
	          _iterator2.return();
	        }
	      } finally {
	        if (_didIteratorError2) {
	          throw _iteratorError2;
	        }
	      }
	    }
	  };
	
	  Explanation.prototype.hexToRgb = function hexToRgb(hex) {
	    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
	    return result ? {
	      r: parseInt(result[1], 16),
	      g: parseInt(result[2], 16),
	      b: parseInt(result[3], 16)
	    } : null;
	  };
	
	  Explanation.prototype.applyAlpha = function applyAlpha(hex, alpha) {
	    var components = this.hexToRgb(hex);
	    return 'rgba(' + components.r + "," + components.g + "," + components.b + "," + alpha.toFixed(3) + ")";
	  };
	  // sord_lists is an array of arrays, of length (colors). if with_positions is true,
	  // word_lists is an array of [start,end] positions instead
	
	
	  Explanation.prototype.display_raw_text = function display_raw_text(div, raw_text) {
	    var word_lists = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
	    var colors = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
	    var max_weight = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1;
	    var positions = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false;
	
	    div.classed('lime', true).classed('text_div', true);
	    div.append('h3').text('Text with highlighted words');
	    var highlight_tag = 'span';
	    var text_span = div.append('span').style('white-space', 'pre-wrap').text(raw_text);
	    var position_lists = word_lists;
	    if (!positions) {
	      position_lists = this.wordlists_to_positions(word_lists, raw_text);
	    }
	    var objects = [];
	    var _iteratorNormalCompletion3 = true;
	    var _didIteratorError3 = false;
	    var _iteratorError3 = undefined;
	
	    try {
	      var _loop = function _loop() {
	        var i = _step3.value;
	
	        position_lists[i].map(function (x) {
	          return objects.push({ 'label': i, 'start': x[0], 'end': x[1], 'alpha': max_weight === 0 ? 1 : x[2] / max_weight });
	        });
	      };
	
	      for (var _iterator3 = (0, _lodash.range)(position_lists.length)[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
	        _loop();
	      }
	    } catch (err) {
	      _didIteratorError3 = true;
	      _iteratorError3 = err;
	    } finally {
	      try {
	        if (!_iteratorNormalCompletion3 && _iterator3.return) {
	          _iterator3.return();
	        }
	      } finally {
	        if (_didIteratorError3) {
	          throw _iteratorError3;
	        }
	      }
	    }
	
	    objects = (0, _lodash.sortBy)(objects, function (x) {
	      return x['start'];
	    });
	    var node = text_span.node().childNodes[0];
	    var subtract = 0;
	    var _iteratorNormalCompletion4 = true;
	    var _didIteratorError4 = false;
	    var _iteratorError4 = undefined;
	
	    try {
	      for (var _iterator4 = objects[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
	        var obj = _step4.value;
	
	        var word = raw_text.slice(obj.start, obj.end);
	        var start = obj.start - subtract;
	        var end = obj.end - subtract;
	        var match = document.createElement(highlight_tag);
	        match.appendChild(document.createTextNode(word));
	        match.style.backgroundColor = this.applyAlpha(colors[obj.label], obj.alpha);
	        var after = node.splitText(start);
	        after.nodeValue = after.nodeValue.substring(word.length);
	        node.parentNode.insertBefore(match, after);
	        subtract += end;
	        node = after;
	      }
	    } catch (err) {
	      _didIteratorError4 = true;
	      _iteratorError4 = err;
	    } finally {
	      try {
	        if (!_iteratorNormalCompletion4 && _iterator4.return) {
	          _iterator4.return();
	        }
	      } finally {
	        if (_didIteratorError4) {
	          throw _iteratorError4;
	        }
	      }
	    }
	  };
	
	  Explanation.prototype.wordlists_to_positions = function wordlists_to_positions(word_lists, raw_text) {
	    var ret = [];
	    var _iteratorNormalCompletion5 = true;
	    var _didIteratorError5 = false;
	    var _iteratorError5 = undefined;
	
	    try {
	      for (var _iterator5 = word_lists[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
	        var words = _step5.value;
	
	        if (words.length === 0) {
	          ret.push([]);
	          continue;
	        }
	        var re = new RegExp("\\b(" + words.join('|') + ")\\b", 'gm');
	        var temp = void 0;
	        var list = [];
	        while ((temp = re.exec(raw_text)) !== null) {
	          list.push([temp.index, temp.index + temp[0].length]);
	        }
	        ret.push(list);
	      }
	    } catch (err) {
	      _didIteratorError5 = true;
	      _iteratorError5 = err;
	    } finally {
	      try {
	        if (!_iteratorNormalCompletion5 && _iterator5.return) {
	          _iterator5.return();
	        }
	      } finally {
	        if (_didIteratorError5) {
	          throw _iteratorError5;
	        }
	      }
	    }
	
	    return ret;
	  };
	
	  return Explanation;
	}();
	
	exports.default = Explanation;


Hi @iEvidently,

I’m moving this discussion to a new GitHub issue, please upload a zip file there containing the result.html file so we can debug it.

Thanks!