{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "_This notebook contains code and comments from Section 3.4 of the book [Ensemble Methods for Machine Learning](https://www.manning.com/books/ensemble-methods-for-machine-learning). Please see the book for additional details on this topic. This notebook and code are released under the [MIT license](https://github.com/gkunapuli/ensemble-methods-notebooks/blob/master/LICENSE)._\n",
    "\n",
    "---\n",
    "\n",
    "## 3.4 Case Study: Sentiment Analysis\n",
    "\n",
    "Sentiment analysis is a natural language processing (NLP) task for identifying the the polarity of opinion as positive, neutral or negative. This case study explores a supervised sentiment analysis task for movie reviews. The data set we will use is the [Large Movie Review Dataset](https://ai.stanford.edu/~amaas/data/sentiment/), which was originally collected and curated for a 2011 paper on sentiment analysis: \n",
    "\n",
    "Andrew L. Maas, Raymond E. Daly, Peter T. Pham, Dan Huang, Andrew Y. Ng, and Christopher Potts. (2011). _Learning Word Vectors for Sentiment Analysis_. The 49th Annual Meeting of the Association for Computational Linguistics (ACL 2011)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Data directory relative to this notebook\n",
    "sentiment_data_directory = './data/ch03/'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.4.1 Pre-processing\n",
    "This data set has already been pre-processed by [count vectorization](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html) to generate [bag-of-word features](https://en.wikipedia.org/wiki/Bag-of-words_model). These pre-processed term-document count features, our data set, can be found in ``./data/ch03/train/labeledBow.feat`` and ``./data/ch03/test/labeledBow.feat``.\n",
    "\n",
    "#### Stop-word Removal\n",
    "This step aims to remove common words such as “the”, “is”, “a”, “an”. Stop word removal can reduce the dimensionality of the data, (to make processing faster), and can improve classification performance. This is because words like “the” are often not really informative for information retrieval and text-mining tasks.\n",
    "\n",
    "**Listing 3.11**: Drop stop words from the vocabulary"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import nltk\n",
    "import time\n",
    "import numpy as np\n",
    "\n",
    "def prune_vocabulary(data_path, max_features=5000):\n",
    "    start_time = time.time()\n",
    "    with open('{0}/imdb.vocab'.format(data_path), 'r', encoding='utf8') as vocab_file:\n",
    "        vocabulary = vocab_file.read().splitlines()\n",
    "    print('Vocabulary load time = {0} seconds.'.format(time.time() - start_time))\n",
    "\n",
    "    # Convert the list of stopwords to a set for faster processing\n",
    "    # nltk.download('stopwords')   # **** UNCOMMENT THIS LINE IF YOU HAVEN'T USED NLTK BEFORE ***\n",
    "    stopwords = set(nltk.corpus.stopwords.words(\"english\"))\n",
    "\n",
    "    # Keep only those vocabulary words that are NOT stopwords\n",
    "    to_keep = [True if word not in stopwords else False for word in vocabulary]\n",
    "    feature_ind = np.where(to_keep)[0]\n",
    "\n",
    "    return feature_ind[:max_features]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Vocabulary load time = 0.04451417922973633 seconds.\n"
     ]
    }
   ],
   "source": [
    "features = prune_vocabulary(sentiment_data_directory, max_features=5000)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### TF-IDF Transformation\n",
    "Our second pre-processing step converts the count features to [tf-idf features](https://en.wikipedia.org/wiki/Tf%E2%80%93idf). These features represent the term frequency-inverse document frequency, a statistic that weights each feature in a document (in our case, a single review) relative to how often it appears in that document as well as how often it appears in the entire corpus (in our case, all the reviews). \n",
    "\n",
    "We can use scikit-learn’s pre-processing toolbox to convert our count features to tf-idf features using the [TfidfTransformer](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfTransformer.html).\n",
    "\n",
    "**Listing 3.12**: Extract tf-idf features and save the data set (**NOTE: _generates a 41 MB file_**)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "import h5py\n",
    "from sklearn.datasets import load_svmlight_files\n",
    "from scipy.sparse import csr_matrix as sp\n",
    "from sklearn.feature_extraction.text import TfidfTransformer\n",
    "\n",
    "def preprocess_and_save(data_path, feature_ind):\n",
    "    data_files = ['{0}/{1}/labeledBow.feat'.format(data_path, data_set) \n",
    "                  for data_set in ['train', 'test']]\n",
    "    [Xtrn, ytrn, Xtst, ytst] = load_svmlight_files(data_files)\n",
    "    n_features = len(feature_ind)\n",
    "\n",
    "    ytrn[ytrn <= 5], ytst[ytst <= 5] = 0, 0\n",
    "    ytrn[ytrn > 5], ytst[ytst > 5] = 1, 1\n",
    "\n",
    "    # Transform the bag-of-words\n",
    "    tfidf = TfidfTransformer()\n",
    "    Xtrn = tfidf.fit_transform(Xtrn[:, feature_ind])\n",
    "    Xtst = tfidf.transform(Xtst[:, feature_ind])\n",
    "      \n",
    "    # Save the data in HDF5 format with sparse matrix representation\n",
    "    with h5py.File('{0}/imdb-{1}k.h5'.format(data_path, \n",
    "                            round(n_features/1000)), 'w') as db:\n",
    "        db.create_dataset('Xtrn', \n",
    "                          data=sp.todense(Xtrn), compression='gzip')\n",
    "        db.create_dataset('ytrn', data=ytrn, compression='gzip')\n",
    "        db.create_dataset('Xtst', \n",
    "                          data=sp.todense(Xtst), compression='gzip')\n",
    "        db.create_dataset('ytst', data=ytst, compression='gzip')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "preprocess_and_save(sentiment_data_directory, features)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "### 3.4.2\tDimensionality Reduction\n",
    "\n",
    "We adopt the popular dimensionality reduction approach of [principal components analysis](https://en.wikipedia.org/wiki/Principal_component_analysis) (PCA), which aims to compress and embed the data into a lower-dimensional representation while while preserving as much of the information as possible. \n",
    "\n",
    "To avoid loading the entire data set into memory and to process the data more efficiently, we perform [Incremental PCA](https://scikit-learn.org/stable/auto_examples/decomposition/plot_incremental_pca.html) instead.\n",
    "\n",
    "**Listing 3.13**: Perform dimensionality reduction using Incremental PCA (**NOTE: _generates a 187 MB file_**)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.decomposition import IncrementalPCA\n",
    "\n",
    "def transform_sentiment_data(data_path, n_features=5000, n_components=500):\n",
    "    db = h5py.File('{0}/imdb-{1}k.h5'.format(data_path, round(n_features/1000)), 'r')\n",
    "\n",
    "    pca = IncrementalPCA(n_components=n_components)\n",
    "    chunk_size = 1000\n",
    "    n_samples = db['Xtrn'].shape[0] \n",
    "    for i in range(0, n_samples // chunk_size):\n",
    "        pca.partial_fit(db['Xtrn'][i*chunk_size:(i+1) * chunk_size])\n",
    "\n",
    "    Xtrn = pca.transform(db['Xtrn'])\n",
    "    Xtst = pca.transform(db['Xtst'])\n",
    "\n",
    "    with h5py.File('{0}/imdb-{1}k-pca{2}.h5'.format(data_path,\n",
    "                                                    round(n_features/1000), n_components), 'w') as db2:\n",
    "        db2.create_dataset('Xtrn', data=Xtrn, compression='gzip')\n",
    "        db2.create_dataset('ytrn', data=db['ytrn'], compression='gzip')\n",
    "        db2.create_dataset('Xtst', data=Xtst, compression='gzip')\n",
    "        db2.create_dataset('ytst', data=db['ytst'], compression='gzip')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "transform_sentiment_data(sentiment_data_directory, n_features=5000, n_components=500)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "###  3.4.3\tStacking classifiers\n",
    "\n",
    "Our goal now is to train a heterogeneous ensemble with meta-learning. Specifically, we will use ensemble several base estimators by blending them. Blending is a variant of stacking, where, instead of using cross validation, we use a single validation set."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "def load_sentiment_data(data_path,n_features=5000, n_components=500):\n",
    "\n",
    "    with h5py.File('{0}/imdb-{1}k-pca{2}.h5'.format(data_path, \n",
    "                                                    round(n_features/1000), n_components), 'r') as db:\n",
    "        Xtrn = np.array(db.get('Xtrn'))\n",
    "        ytrn = np.array(db.get('ytrn'))\n",
    "        Xtst = np.array(db.get('Xtst'))\n",
    "        ytst = np.array(db.get('ytst'))\n",
    "\n",
    "    return Xtrn, ytrn, Xtst, ytst"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "Xtrn, ytrn, Xtst, ytst = load_sentiment_data('./data/Ch03/', n_features=5000, n_components=500)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next, we use five base estimators: [``RandomForestClassifier``](https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html) with 100 randomized decision trees, [``ExtraTreesClassifier``] (https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.ExtraTreesClassifier.html#sklearn.ensemble.ExtraTreesClassifier) with 100 extremely randomized trees, [``Logistic Regression``](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html), Bernoulli [naïve Bayes](https://scikit-learn.org/stable/modules/classes.html#module-sklearn.naive_bayes) (``BernoulliNB``) and a linear SVM trained with stochastic gradient descent ([``SGDClassifier``](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDClassifier.html))."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier\n",
    "from sklearn.linear_model import LogisticRegression, SGDClassifier\n",
    "from sklearn.naive_bayes import BernoulliNB\n",
    "\n",
    "estimators = [('rf', RandomForestClassifier(n_estimators=100, n_jobs=-1)),\n",
    "              ('xt', ExtraTreesClassifier(n_estimators=100, n_jobs=-1)),\n",
    "              ('lr', LogisticRegression(C=0.01, solver='lbfgs')),\n",
    "              ('bnb', BernoulliNB()),\n",
    "              ('svm', SGDClassifier(loss='hinge', penalty='l2', alpha=0.01,\n",
    "                                    n_jobs=-1, max_iter=10, tol=None))]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To blend these base-estimators into a heterogeneous ensemble with meta-learning, we use the following procedure:\n",
    "1.\tSplit the training data into a training set ``(Xtrn, ytrn)`` with 80% of the data and a validation set ``(Xval, yval)``, with the remaining 20% of the data\n",
    "2.\tTrain the each of the level-1 estimators on the training set, ``(Xtrn, ytrn)``\n",
    "3.\tGenerate meta-features ``Xmeta`` with the trained estimators using ``Xval``; \n",
    "4.\tAugment the validation data with the meta-features: ``[Xval, Xmeta]``; this augmented validation set will have 500 original features + 5 meta-features\n",
    "5.\tTrain the level-2 estimator with the augmented validation set ``([Xval, Xmeta], yval)``\n",
    "\n",
    "This leaves one final decision: the choice of the level-2 estimator. Previously, we used simple linear classifiers. For this classification task, we utilize a neural network. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.neural_network import MLPClassifier\n",
    "meta_estimator = MLPClassifier(hidden_layer_sizes=(128, 64, 32),\n",
    "                               alpha=0.001)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Listing 3.14**: Blending models with a validation set"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "def blend_models(level1_estimators, level2_estimator, \n",
    "                 X, y , use_probabilities=False):    \n",
    "    Xtrn, Xval, ytrn, yval = train_test_split(X, y, test_size=0.2)\n",
    "\n",
    "    n_estimators = len(level1_estimators)\n",
    "    n_samples = len(yval)\n",
    "    Xmeta = np.zeros((n_samples, n_estimators))\n",
    "    for i, (model, estimator) in enumerate(level1_estimators):\n",
    "        estimator.fit(Xtrn, ytrn)\n",
    "        Xmeta[:, i] = estimator.predict(Xval)\n",
    "\n",
    "    Xmeta = np.hstack([Xval, Xmeta])\n",
    "    \n",
    "    level2_estimator.fit(Xmeta, yval)\n",
    "\n",
    "    final_model = {'level-1': level1_estimators, \n",
    "                   'level-2': level2_estimator, \n",
    "                   'use-proba': use_probabilities}\n",
    "\n",
    "    return final_model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Listing 3.2** for individual base estimator predictions and **Listing 3.9** for meta-estimator predictions are combined here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "def predict_stacking(X, stacked_model):\n",
    "    # Get level-1 predictions\n",
    "    level1_estimators = stacked_model['level-1']\n",
    "    n_samples, n_estimators = X.shape[0], len(level1_estimators)\n",
    "    use_probabilities = stacked_model['use-proba']\n",
    "\n",
    "    Xmeta = np.zeros((n_samples, n_estimators))  # Initialize meta-features\n",
    "    for i, (model, estimator) in enumerate(level1_estimators):\n",
    "        if use_probabilities:\n",
    "            Xmeta[:, i] = estimator.predict_proba(X)[:, 1]\n",
    "        else:\n",
    "            Xmeta[:, i] = estimator.predict(X)\n",
    "\n",
    "    level2_estimator = stacked_model['level-2']\n",
    "    Xmeta = np.hstack([X, Xmeta])\n",
    "\n",
    "    y = level2_estimator.predict(Xmeta)\n",
    "\n",
    "    return y"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Train a simple blending model with this code:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "stacked_model = blend_models(estimators, meta_estimator, Xtrn, ytrn)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "7.340000000000002\n",
      "16.959999999999997\n"
     ]
    }
   ],
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "ypred = predict_stacking(Xtrn, stacked_model)\n",
    "trn_err = (1 - accuracy_score(ytrn, ypred)) * 100\n",
    "print(trn_err)\n",
    "\n",
    "ypred = predict_stacking(Xtst, stacked_model)\n",
    "tst_err = (1 - accuracy_score(ytst, ypred)) * 100\n",
    "print(tst_err)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "### Overall Performance of the blended model\n",
    "We finally visualize and comparing the performance of each individual base classifier with the meta-classifier ensemble. Stacking/blending improves classification performance by ensembling diverse base classifiers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "rf: training error = 0.00%, test error = 22.41%, running time = 9.56 seconds.\n",
      "xt: training error = 0.00%, test error = 29.92%, running time = 4.26 seconds.\n",
      "lr: training error = 17.65%, test error = 18.03%, running time = 0.10 seconds.\n",
      "bnb: training error = 21.66%, test error = 22.84%, running time = 0.32 seconds.\n",
      "svm: training error = 27.34%, test error = 27.31%, running time = 0.39 seconds.\n"
     ]
    }
   ],
   "source": [
    "trn_errors, tst_errors = np.zeros((len(estimators) + 1, )), np.zeros((len(estimators) + 1, ))\n",
    "for i, (method, estimator) in enumerate(estimators):\n",
    "    start_time = time.time()\n",
    "    estimator.fit(Xtrn, ytrn)\n",
    "    run_time = time.time() - start_time\n",
    "\n",
    "    ypred = estimator.predict(Xtrn)\n",
    "    trn_errors[i] = (1 - accuracy_score(ytrn, ypred)) * 100\n",
    "    ypred = estimator.predict(Xtst)\n",
    "    tst_errors[i] = (1 - accuracy_score(ytst, ypred)) * 100\n",
    "\n",
    "    print('{0}: training error = {1:4.2f}%, test error = {2:4.2f}%, running time = {3:4.2f} seconds.'\n",
    "          .format(method, trn_errors[i], tst_errors[i], run_time))\n",
    "    \n",
    "trn_errors[-1] = trn_err\n",
    "tst_errors[-1] = tst_err"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "def autolabel(ax, rects):\n",
    "    for rect in rects:\n",
    "        height = np.round(rect.get_height(), 1)\n",
    "        ax.annotate('{}'.format(height),\n",
    "                    xy=(rect.get_x() + rect.get_width() / 2, height),\n",
    "                    xytext=(0, 3),  # 3 points vertical offset\n",
    "                    textcoords=\"offset points\",\n",
    "                    ha='center', va='bottom')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "%matplotlib inline\n",
    "\n",
    "labels = ['Random\\nForest', 'Extra\\nTrees', 'Logistic\\nRegression', \n",
    "      'Bernoulli\\n Naive Bayes', 'SVM', 'Blending with\\nNeural Nets']\n",
    "\n",
    "x = np.arange(len(labels))  # the label locations\n",
    "width = 0.35  # the width of the bars\n",
    "\n",
    "fig, ax = plt.subplots()\n",
    "rects1 = ax.bar(x - width / 2, trn_errors, width, label='Training Error')\n",
    "rects2 = ax.bar(x + width / 2, tst_errors, width, label='Test Error')\n",
    "\n",
    "# Add some text for labels, title and custom x-axis tick labels, etc.\n",
    "ax.set_ylabel('Model performance (error %)', fontsize=12)\n",
    "# ax.set_title('Scores by group and gender')\n",
    "ax.set_xticks(x)\n",
    "ax.set_xticklabels(labels, fontsize=10)\n",
    "ax.legend()\n",
    "\n",
    "ax.set_axisbelow(True)\n",
    "# ax.grid(linestyle='-', linewidth='0.5', color='gray')\n",
    "\n",
    "autolabel(ax, rects1)\n",
    "autolabel(ax, rects2)\n",
    "\n",
    "fig.tight_layout()\n",
    "# plt.savefig('./figures/CH03_F19_Kunapuli.png', format='png', dpi=300, bbox_inches='tight', pad_inches=0)\n",
    "# plt.savefig('./figures/CH03_F19_Kunapuli.pdf', format='pdf', dpi=300, bbox_inches='tight', pad_inches=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.10.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}