{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's get right into this post! But, before you read this post, be sure to check out my original post on [word embeddings](https://jrtechs.net/data-science/word-embeddings). In this post we will be using data from this blog to create and visualize a word embedding.\n",
    "\n",
    "To recap, let's first look at a small example using the [Gensim](https://pypi.org/project/gensim/) Word2Vec model:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 156,
   "metadata": {},
   "outputs": [],
   "source": [
    "from gensim.models import Word2Vec\n",
    "\n",
    "# defines some dummy data\n",
    "sentences = [[\"cat\", \"say\", \"meow\"], [\"dog\", \"say\", \"woof\"], [\"man\", \"say\", \"dam\"]]\n",
    "\n",
    "# creates and trains model\n",
    "model = Word2Vec(min_count=1, size=10)\n",
    "model.build_vocab(sentences)\n",
    "model.train(sentences, total_examples=model.corpus_count, epochs=model.epochs) \n",
    "\n",
    "# saves the model so that you can load it later with model.load\n",
    "model.save(\"basic-word2vec.model\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "With this word2vec model we can do things like get the most similar words and get the vectorized version of the word. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 114,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[('woof', 0.3232297897338867),\n",
       " ('dam', 0.14384251832962036),\n",
       " ('man', 0.11316978931427002),\n",
       " ('cat', -0.06251632422208786),\n",
       " ('say', -0.1781214326620102),\n",
       " ('meow', -0.21009384095668793)]"
      ]
     },
     "execution_count": 114,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model.wv.most_similar(\"dog\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ 0.04777663  0.01543251 -0.04632503  0.03601828 -0.00572644  0.00553683\n",
      " -0.04476452 -0.0274465   0.0047655   0.00508591]\n"
     ]
    }
   ],
   "source": [
    "print(model.wv.get_vector(\"dog\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Using the TSNE method, we can reduce the dimensionality so that we can visualize it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 158,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.decomposition import IncrementalPCA    # inital reduction\n",
    "from sklearn.manifold import TSNE                   # final reduction\n",
    "import numpy as np                      \n",
    "\n",
    "def reduce_dimensions(model):\n",
    "    num_dimensions = 2  # final num dimensions (2D, 3D, etc)\n",
    "\n",
    "    vectors = [] # positions in vector space\n",
    "    labels = [] # keep track of words to label our data again later\n",
    "    for word in model.wv.vocab:\n",
    "        vectors.append(model.wv[word])\n",
    "        labels.append(word)\n",
    "\n",
    "    # convert both lists into numpy vectors for reduction\n",
    "    vectors = np.asarray(vectors)\n",
    "    labels = np.asarray(labels)\n",
    "\n",
    "    # reduce using t-SNE\n",
    "    vectors = np.asarray(vectors)\n",
    "    tsne = TSNE(n_components=num_dimensions, random_state=0)\n",
    "    vectors = tsne.fit_transform(vectors)\n",
    "\n",
    "    x_vals = [v[0] for v in vectors]\n",
    "    y_vals = [v[1] for v in vectors]\n",
    "    return x_vals, y_vals, labels\n",
    "\n",
    "\n",
    "x_vals, y_vals, labels = reduce_dimensions(model)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['cat' 'say' 'meow' 'dog' 'woof' 'man' 'dam']\n",
      "[-29.594002, -45.996586, 20.368856, 53.92877, -12.437127, 3.9659712, 37.524284]\n",
      "[60.112713, 11.891685, 70.019325, 31.70431, -26.423267, 21.79772, -16.517805]\n"
     ]
    }
   ],
   "source": [
    "print(labels)\n",
    "print(x_vals)\n",
    "print(y_vals)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Due to simplicity, we are using a basic matplotlib scatter plot to visualize it. Since it is messy to visualize every label on larger models, we are reducing the number of labels we are picking to display on the graph. However, on this initial example we can visualize all the labels. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 161,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 10852 characters]\n",
      "text/plain": [
       "<Figure size 360x360 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import random\n",
    "\n",
    "def plot_with_matplotlib(x_vals, y_vals, labels, num_to_label):\n",
    "    plt.figure(figsize=(5, 5))\n",
    "    plt.scatter(x_vals, y_vals)\n",
    "    plt.title(\"Embedding Space\")\n",
    "    indices = list(range(len(labels)))\n",
    "    selected_indices = random.sample(indices, num_to_label)\n",
    "    for i in selected_indices:\n",
    "        plt.annotate(labels[i], (x_vals[i], y_vals[i]))\n",
    "    plt.savefig('ex.png')\n",
    "        \n",
    "plot_with_matplotlib(x_vals, y_vals, labels, 7)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Since my blog is written in markdown files, we need a way to extract all the code blocks out of it so that our model does not get fooled by things that are not \"English.\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 118,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      " The Ackermann function is a classic example of a function that is not primitive recursive – you cannot solve it using loops like Fibonacci. In other words, you have to use recursion to solve for values of the Ackermann function. For more information on the Ackermann function [click\n"
     ]
    }
   ],
   "source": [
    "def process_file(fileName):\n",
    "    result = \"\"\n",
    "    tempResult = \"\"\n",
    "    inCodeBlock = False\n",
    "\n",
    "    with open(fileName) as file:\n",
    "        for line in file:\n",
    "            if line.startswith(\"```\"):\n",
    "                inCodeBlock = not inCodeBlock\n",
    "            elif inCodeBlock:\n",
    "                pass\n",
    "            else:\n",
    "                for word in line.split():\n",
    "                    if \"http\" not in word and \"media/\"not  in word:\n",
    "                        result = result  + \" \" + word\n",
    "    return result\n",
    "\n",
    "print(process_file(\"data/ackermann-function-written-in-java.md\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Although this script takes us most of the way there with removing the code block, we still want to remove stopper words and punctuations. The gensim library has a function that already does that for us."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 119,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['the', 'ackermann', 'function', 'is', 'classic', 'example', 'of', 'function', 'that', 'is', 'not', 'primitive', 'recursive', 'you', 'cannot', 'solve', 'it', 'using', 'loops', 'like', 'fibonacci', 'in', 'other', 'words', 'you', 'have', 'to', 'use', 'recursion', 'to', 'solve', 'for', 'values', 'of', 'the', 'ackermann', 'function', 'for', 'more', 'information', 'on', 'the', 'ackermann', 'function', 'click']\n"
     ]
    }
   ],
   "source": [
    "from gensim import utils\n",
    "print(utils.simple_preprocess(process_file(\"data/ackermann-function-written-in-java.md\")))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Loading everything in as memory and feeding it into a large list like the dummy example, although possible, is not feasable or efficient for large data sets. In Gensim it is typical to create a Corpus: a corpus is a collection of documents used for training. To create the corpus baised on my prior blog posts, I need to process each blog post and store the cleaned version in a file."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "file = open(\"jrtechs.cor\", \"w+\")\n",
    "for file_name in os.listdir(\"data\"):\n",
    "    file.write(process_file(\"data/\" + file_name) + \"\\n\")\n",
    "file.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To read our text version corpus, we simply create an iterable source that just reads out each line from our text file. In our case each line is an entire blog post."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [],
   "source": [
    "from gensim.test.utils import datapath\n",
    "\n",
    "class MyCorpus(object):\n",
    "    \"\"\"An interator that yields sentences (lists of str).\"\"\"\n",
    "\n",
    "    def __iter__(self):\n",
    "        corpus_path = \"jrtechs.cor\"\n",
    "        for line in open(corpus_path):\n",
    "            # assume there's one document per line, tokens separated by whitespace\n",
    "            yield utils.simple_preprocess(line)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we can create a new model using our custom corpus. The things that we will want to change is the embedding size and the number of epochs we train our model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 128,
   "metadata": {},
   "outputs": [],
   "source": [
    "sentences = MyCorpus()\n",
    "model = Word2Vec(min_count=1, size=20, sentences=sentences)\n",
    "model.train(sentences, total_examples=model.corpus_count, epochs=500) \n",
    "model.save(\"jrtechs-word2vec-500.model\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 130,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[('that', 0.7954616546630859),\n",
       " ('also', 0.7654829025268555),\n",
       " ('but', 0.7392309904098511),\n",
       " ('since', 0.7380496859550476),\n",
       " ('although', 0.7112778425216675),\n",
       " ('why', 0.7025969624519348),\n",
       " ('encouragement', 0.6704230308532715),\n",
       " ('because', 0.6692429184913635),\n",
       " ('faster', 0.6633850932121277),\n",
       " ('so', 0.6504907011985779)]"
      ]
     },
     "execution_count": 130,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model.wv.most_similar(\"however\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 139,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[('function', 0.8110014200210571),\n",
       " ('grouping', 0.7134513854980469),\n",
       " ('import', 0.7052735090255737),\n",
       " ('select', 0.6987707614898682),\n",
       " ('max', 0.6856316328048706),\n",
       " ('authentication', 0.6762576103210449),\n",
       " ('pseudo', 0.663935124874115),\n",
       " ('namespace', 0.6549093723297119),\n",
       " ('matrix', 0.6515613198280334),\n",
       " ('zero', 0.6360148191452026)]"
      ]
     },
     "execution_count": 139,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model.wv.most_similar(\"method\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 140,
   "metadata": {},
   "outputs": [],
   "source": [
    "x_vals, y_vals, labels = reduce_dimensions(model)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you run the TSNE algorithm with different training parameters, you will get different shapes in your output. I noticed that for my data, If I ran it with 5 epochs I got a curvy shape where if I trained the model with over 100 epochs I just got a single cluster shape. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 155,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 32640 characters]\n",
      "text/plain": [
       "<Figure size 360x360 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "plot_with_matplotlib(x_vals, y_vals, labels, 10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If we zoom in on the embedding space, we can get a better idea of what clusters close to each other. This visualization of course is not perfect because we are trying to visualize a 15 dimensions down into a two dimensional graph. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 152,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 550960 characters]\n",
      "text/plain": [
       "<Figure size 360x360 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import random\n",
    "\n",
    "def plot_with_matplotlib(x_vals, y_vals, labels, xmin=3, xmax=7, ymin=3, ymax=7):\n",
    "    plt.figure(figsize=(5, 5))\n",
    "    plt.scatter(x_vals, y_vals)\n",
    "    plt.title(\"Embedding Space\")\n",
    "    plt.xlim(xmin, xmax)\n",
    "    plt.ylim(ymin, ymax)\n",
    "    \n",
    "    for x, y, l in zip(x_vals, y_vals, labels):\n",
    "        plt.annotate(l, (x, y))\n",
    "    plt.savefig('smallex.png')\n",
    "        \n",
    "plot_with_matplotlib(x_vals, y_vals, labels)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It was impressive the results I got on my limited data scope. The Google news dataset explored last time was far more accurate, however, the model was quite large and I was unable to run the TSNE algorithm on it without crashing my computer. Training your own word embedding model is useful because in different applications and contexts words can mean different things. Right now there is a large push at seeing how we can leverage pre-trained models. This enables you to take google's word2vec model trained on 100 billion words and tweak the model using your own data to better fit your own application. Maybe I'll make that a part three of this blog post series on embeddings.  "
   ]
  }
 ],
 "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.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
