Since the advent of neural networks, vector embeddings for text processing have gained traction in both scientific and applied text classification problems, for example in text sentiment analysis. Using (pre-trained) embeddings has become a de facto standard for attaining a high rating in scientific sentiment analysis contests such as SemEval. However, vector embeddings are finding their way into many other fascinating applications as well.
In the current post, I want to provide a intuitive, easy to understand introduction to the subject. I will touch upon both vector embeddings in general, and applied to text analysis in particular. Furthermore, I’ll provide some quite simple code with Python and Gensim for generating vector embeddings from text. In further posts, I will apply vector embeddings to transforming categorical to continuous variables and generating vector definitions in N-dimensional space for entities.
I’ll cycle through the following points:
- What are vector embeddings?
- How do you use vector embeddings?
- Why do you need vector embeddings?
- How do you generate vector embeddings?
- How does the neural network optimize the vector matrix?
- A practical example (with code): word vectorization for tweets.
What are vector embeddings?
In a nutshell, a vector embedding for a certain item (a word, or a person, or any other object you can think of) is a mathematical definition for that specific item. This definition is comprised of a set of float numbers between -1 and 1, which is called a vector.
A vector is a specific location in N-dimensional space, in comparison to the zero-point in that space. So, a vector embedding for an item means, that the item is represented by a vector, embedded in a space with as many dimensions as there are numbers in the vector. Every position in the vector, indicates the location of the item on a certain axis; the first number represents the X-axis, the second represents the Y-axis, and so forth.
To keep it simple, let’s say the vector embedding for the word ‘bird’ equals [0.6, -0.2, 0.6]. Therefore, ‘bird’ is represented in a specific spot in 3-dimensional space. Simple, right?
Like the following image:
However, it gets more complicated when you start adding dimensions. As a rule of thumb, 50 dimensions is a usual vector length for representing things like grocery items, or people, or countries. For language-related tasks, somewhere in the range of 200-300 dimensions is more common. Our human brains are just not equipped for imagining anything beyond 3 to 4 dimensions, so we’ll have to trust the math.
Lastly, a constraint that is put on vector embeddings, is that they should always add up to 1.
How do you use vector embeddings?
Of course, a list of float numbers on its own does not mean anything. It only becomes interesting once you start factoring in that you could encode multiple items in high-dimensional space. Naturally, items that are closer to each other can be thought of as more similar or compatible. Consequently, items that are far away, are dissimilar. Items that are neither far away nor close by (think of it like a 90 degree angle, just in high-dimensional space) do not really have any discernible relationship to each other. A mathematical term for this is ‘orthogonal’.
The measure that is used for checking whether two vectors are similar / close, is called cosine similarity. Cosine similarity can be computed by calculating the dot product between two vectors. It goes like this; Given two vectors [A, B, C] and [D, E, F], the numbers in each position are multiplied by each other; similarity equals A times D plus B times E plus C times F. This outcome is called a scalar in linear algebra. Subsequently, the dot product equals the sum of the previously calculated scalar.
The trick here is that for each position in the two vectors that are closer to each other within the -1 to 1 space, the result of the multiplication is higher. Therefore, the dot product between two vectors increases as they resemble each other more.
To test this out, let’s add two more words:
‘bird’ = [0.6, -0.2, 0.6]
‘wing’ = [0.7, -0.3, 0.6]
‘lawnmower’ = [-0.2, 0.8, 0.4]
Now what we have here, is actually our first vector matrix, which is just a fancy name for a set of vectors.
Intuitively, ‘bird’ and ‘wing’ should be quite similar, so have a high dot product. Let’s try it out:
Dot([0.7, -0.3, 0.6], [0.6, -0.2, 0.6]) = 0.42 + 0.06 + 0.36 = 0.86
On the opposite, ‘bird’ and ‘lawnmower’ shouldn’t really be related in any way, so the dot product should be close to 0:
Dot([0.6, -0.2, 0.6], [-0.2, 0.8, 0.4]) = -0.12 + -0.16 + 0.24 = 0.06
When you start adding lots of words to your vector matrix, it will start becoming a word cloud in N-dimensional space, in which certain clusters of words can be recognized. Furthermore, vector embeddings do not only represent bilateral relationships between items, but also capture more complicated relationships; In our language use, we have concepts of how words relate to each other. We understand that a leg is to a human, what a paw is to a cat. If our model is correctly trained on sufficient data, these types of relationships should be available.
Why do you need vector embeddings?
In any language-related machine learning task, your results will be best when using an algorithm that actually speaks the language that you’re aiming at analysing. In other words, you might want to use some technique that somehow captures the meanings of the words and sentences you’re processing.
Now, all ML algorithms require numerical INPUT. So, a requirement for an algorithm to capture language meaning, is that it should be numerical; every word should be represented by either a number or a series of numbers.
Generally, for transforming categorical entities such as words to numerical representations, one would apply one-hot encoding; for each of the amount of entities, which is called the cardinality of the variable, a separate column is generated, filled with 1s and 0s, like this:
Essentially, one-hot encoding already transforms entities to vectors; each entity is represented by a list of numbers. However, this transformation has a number of drawbacks;
- If you apply one-hot encoding to a text corpus, you’ll need to create a massive amount of extra columns. That’s both very inefficient and complicated to analyse and interpret. Text datasets often contain thousands of unique words. In other words; the created vector is too large to be useful.
- Due to the Boolean nature of this transformation, each entity is equally as similar to all other entities. No possibility exists for somehow putting more similar items close to each other. Yet, we also want to capture meanings of words. Therefore, we need a certain way of representing whether two words are closely related or not.
Embedding entities as vectors solves both these problems. Summarized, the method lets you encode every word in your dataset as a short list of float numbers, which are all between 1 and -1. Given that two words are similar, their respective lists resemble each other more.
How do you generate vector embeddings?
As I described before, similarity is essential to understanding vector embeddings. In the case of actually generating the embeddings from scratch, we’ll need a measure for similarity. Usually, in text analysis, we derive that from word co-occurrence in a text corpus. Essentially, words that are close together often within sentences, are theorized to be quite similar. The opposite is true for dissimilar words; they are never close together in text.
This is where neural networks come in: you use the neural network to either make two vectors more similar to each other each time the underlying entities occur together, or to make two vectors for items more dissimilar given that they don’t occur together.
Firstly, we need to transform our dataset to a usable format. Since the neural network will require some number to be predicted, we need a binary output variable. all similar word combinations will be accompanied with a 1, and all dissimilar combinations will be accompanied by a 0, like this;
|First word||Second word||Binary output|
These word combinations, accompanied by the output variable, are fed to the neural network. Essentially, the neural network predicts whether a certain combination of words should be accompanied by a 1 or a 0, and if it’s wrong compared to the real value, subsequently adjusts the weights in its internal vector matrix to make a more correct prediction next time that specific combination comes around.
How does the neural network optimize the vector matrix?
I’ll try to keep it simple. The neural network is trained by feeding it lots of bilateral word combinations, accompanied by 0s and 1s. Once a certain word combination enters the model, the two respective word vectors are retrieved from the vector matrix. At first, the numbers in the vectors are randomly generated, close to 0.
Subsequently, the dot product for those specific two vectors is calculated. Based on the outcome, the model will predict either a 0 (non-combination) or a 1 (actual combination). A sigmoid transformation is applied to the dot product of each combination, so that the outcome of the model is either 0, for a prediction of a non-combination, or a 1, for the prediction of a true combination.
The learning of the model, as with all neural networks, happens through backpropagation; after feeding a word combination to the network, the model prediction is compared with the actual outcome value. If those don’t match, this is propagated back to the embedding layer, where the embeddings are adjusted accordingly. If two words occur together frequently as a true combination, their vector embeddings will be adjusted to resemble each other more. The opposite happens for false combinations; those embeddings are adjusted so that they resemble each other less.
After training, we end up with an embedding matrix containing optimized embeddings for all words in the embedding matrix. We discard the sigmoid layer so we can examine dot products between different words.
A practical example (with code): word vectorization for tweets.
So, neural network vector embeddings can be challenging to understand. However, there are some libraries available that do all the heavy lifting in terms of programming; I’ll be using Gensim.
Some library imports:
import pandas as pd import numpy as np import csv from gensim.models import word2vec import nltk import re import collections
Firstly, we need some textual data. I chose for a set of 1.6 million tweets that were provided with a sentiment analysis competition on Kaggle. The data were provided in .csv format. Let’s load them. I’ll be keeping only a subset, for quick training.
##open kaggle file. We only need the tweet text, which is in 5th position of every line with open('sentiment140\kaggle_set.csv', 'r') as f: r = csv.reader(f) tweets = [line for line in r] tweets = tweets[:400000]
Here’s what they look like.
print(tweets[:10]) #["@switchfoot http://twitpic.com/2y1zl - Awww, that's a bummer. You shoulda got David Carr of Third Day to do it. ;D", "is upset that he can't update his Facebook by texting it... and might cry as a result School today also. Blah!", '@Kenichan I dived many times for the ball. Managed to save 50% The rest go out of bounds', 'my whole body feels itchy and like its on fire ', "@nationwideclass no, it's not behaving at all. i'm mad. why am i here? because I can't see you all over there. ", '@Kwesidei not the whole crew ', 'Need a hug ', "@LOLTrish hey long time no see! Yes.. Rains a bit ,only a bit LOL , I'm fine thanks , how's you ?", "@Tatiana_K nope they didn't have it ", '@twittera que me muera ? ']
It appears that some data cleaning is in order.
##clean the data. We'll use nltk tokenization, which is not perfect. ##Therefore I remove some of the things nltk does not recognize with regex emoticon_str = r"[:=;X][oO\-]?[D\)\]pP\(/\\O]" tweets_clean =  counter = collections.Counter() for t in tweets: t = t.lower() ##remove all emoticons t = re.sub(emoticon_str, '', t) ##remove username mentions, they usually don't mean anything t = re.sub(r'(?:@[\w_]+)', '', t) ##remove urls t = re.sub(r"http\S+", '', t) ##remove all reading signs t = re.sub(r'[^\w\s]','',t) ##tokenize the remainder words = nltk.word_tokenize(t) tweets_clean.append(words)
print(tweets_clean[:10]) ##[['awww', 'thats', 'a', 'bummer', 'you', 'shoulda', 'got', 'david', 'carr', 'of', 'third', 'day', 'to', 'do', 'it', 'd'], ['is', 'upset', 'that', 'he', 'cant', 'update', 'his', 'facebook', 'by', 'texting', 'it', 'and', 'might', 'cry', 'as', 'a', 'result', 'school', 'today', 'also', 'blah'], ['i', 'dived', 'many', 'times', 'for', 'the', 'ball', 'managed', 'to', 'save', '50', 'the', 'rest', 'go', 'out', 'of', 'bounds'], ['my', 'whole', 'body', 'feels', 'itchy', 'and', 'like', 'its', 'on', 'fire'], ['no', 'its', 'not', 'behaving', 'at', 'all', 'im', 'mad', 'why', 'am', 'i', 'here', 'because', 'i', 'cant', 'see', 'you', 'all', 'over', 'there'], ['not', 'the', 'whole', 'crew'], ['need', 'a', 'hug'], ['hey', 'long', 'time', 'no', 'see', 'yes', 'rains', 'a', 'bit', 'only', 'a', 'bit', 'lol', 'im', 'fine', 'thanks', 'hows', 'you'], ['nope', 'they', 'didnt', 'have', 'it'], ['que', 'me', 'muera']]
Now building the actual model is super simple:
##now for the word2vec ##list of lists input works fine, you could train in batches as well if your set is too large for memory. model = word2vec.Word2Vec(tweets_clean, iter=5, min_count=30, size=300, workers=1)
And check out the results. The embeddings generate some cool results; you can clearly see that the most similar words to a certain word, are actually almost identical in semantic meaning:
print(model.wv.similarity('yellow','blue')) print(model.wv.similarity('yellow','car')) print(model.wv.similarity('bird','wing')) #0.725584 #0.25973 #0.484771 print(model.wv.most_similar('car')) print(model.wv.most_similar('bird')) print(model.wv.most_similar('face')) #[('truck', 0.7357473373413086), ('bike', 0.6849836707115173), ('apt', 0.6751911640167236), ('flat', 0.666846752166748), ('room', 0.6251213550567627), ('garage', 0.6184542775154114), ('van', 0.6047226190567017), ('house', 0.5993092060089111), ('license', 0.5938838720321655), ('passport', 0.5903675556182861)] #[('squirrel', 0.7508804202079773), ('spider', 0.7459014654159546), ('cat', 0.7174966931343079), ('kitten', 0.7117133140563965), ('nest', 0.7008006572723389), ('giant', 0.6956182718276978), ('frog', 0.6906625032424927), ('rabbit', 0.685787558555603), ('mouse', 0.6779413223266602), ('hamster', 0.6754754781723022)] #[('mouth', 0.6230158805847168), ('lip', 0.5884073972702026), ('butt', 0.5822890996932983), ('finger', 0.579236626625061), ('smile', 0.5790724754333496), ('eye', 0.5769941806793213), ('cheek', 0.5746228098869324), ('skin', 0.5726771354675293), ('arms', 0.5703110694885254), ('neck', 0.5571259260177612)]
That’s it for today! I’ll be elaborating more on how to generate vector embeddings by defining neural networks with Tensorflow and Keras, in a future post.