Semantics and Word Vectors

Sometimes called "opinion mining", Wikipedia defines sentiment analysis as

"the use of natural language processing ... to systematically identify, extract, quantify, and study affective states and subjective information.
Generally speaking, sentiment analysis aims to determine the attitude of a speaker, writer, or other subject with respect to some topic or the overall contextual polarity or emotional reaction to a document, interaction, or event."

Up to now we've used the occurrence of specific words and word patterns to perform test classifications. In this section we'll take machine learning even further, and try to extract intended meanings from complex phrases. Some simple examples include:

  • Python is relatively easy to learn.
  • That was the worst movie I've ever seen.

However, things get harder with phrases like:

  • I do not dislike green eggs and ham. (requires negation handling)

The way this is done is through complex machine learning algorithms like word2vec. The idea is to create numerical arrays, or word embeddings for every word in a large corpus. Each word is assigned its own vector in such a way that words that frequently appear together in the same context are given vectors that are close together. The result is a model that may not know that a "lion" is an animal, but does know that "lion" is closer in context to "cat" than "dandelion".

It is important to note that building useful models takes a long time - hours or days to train a large corpus - and that for our purposes it is best to import an existing model rather than take the time to train our own.


Installing Larger spaCy Models

Up to now we've been using spaCy's smallest English language model, en_core_web_sm (35MB), which provides vocabulary, syntax, and entities, but not vectors. To take advantage of built-in word vectors we'll need a larger library. We have a few options:

en_core_web_md (116MB) Vectors: 685k keys, 20k unique vectors (300 dimensions)
or
en_core_web_lg (812MB) Vectors: 685k keys, 685k unique vectors (300 dimensions)

If you plan to rely heavily on word vectors, consider using spaCy's largest vector library containing over one million unique vectors:

en_vectors_web_lg (631MB) Vectors: 1.1m keys, 1.1m unique vectors (300 dimensions)

For our purposes en_core_web_md should suffice.

From the command line (you must run this as admin or use sudo):

activate spacyenvif using a virtual environment

python -m spacy download en_core_web_md
python -m spacy download en_core_web_lg   optional library
python -m spacy download en_vectors_web_lgoptional library

If successful, you should see a message like:


**Linking successful**
C:\Anaconda3\envs\spacyenv\lib\site-packages\en_core_web_md -->
C:\Anaconda3\envs\spacyenv\lib\site-packages\spacy\data\en_core_web_md

You can now load the model via spacy.load('en_core_web_md')

Of course, we have a third option, and that is to train our own vectors from a large corpus of documents. Unfortunately this would take a prohibitively large amount of time and processing power.


Word Vectors

Word vectors - also called word embeddings - are mathematical descriptions of individual words such that words that appear frequently together in the language will have similar values. In this way we can mathematically derive context. As mentioned above, the word vector for "lion" will be closer in value to "cat" than to "dandelion".

Vector values

So what does a word vector look like? Since spaCy employs 300 dimensions, word vectors are stored as 300-item arrays.

Note that we would see the same set of values with en_core_web_md and en_core_web_lg, as both were trained using the word2vec family of algorithms.


In [3]:
# Import spaCy and load the language library
import spacy
nlp = spacy.load('en_core_web_lg')  # make sure to use a larger model!

In [4]:
nlp(u'lion').vector


Out[4]:
array([ 1.8963e-01, -4.0309e-01,  3.5350e-01, -4.7907e-01, -4.3311e-01,
        2.3857e-01,  2.6962e-01,  6.4332e-02,  3.0767e-01,  1.3712e+00,
       -3.7582e-01, -2.2713e-01, -3.5657e-01, -2.5355e-01,  1.7543e-02,
        3.3962e-01,  7.4723e-02,  5.1226e-01, -3.9759e-01,  5.1333e-03,
       -3.0929e-01,  4.8911e-02, -1.8610e-01, -4.1702e-01, -8.1639e-01,
       -1.6908e-01, -2.6246e-01, -1.5983e-02,  1.2479e-01, -3.7276e-02,
       -5.7125e-01, -1.6296e-01,  1.2376e-01, -5.5464e-02,  1.3244e-01,
        2.7519e-02,  1.2592e-01, -3.2722e-01, -4.9165e-01, -3.5559e-01,
       -3.0630e-01,  6.1185e-02, -1.6932e-01, -6.2405e-02,  6.5763e-01,
       -2.7925e-01, -3.0450e-03, -2.2400e-02, -2.8015e-01, -2.1975e-01,
       -4.3188e-01,  3.9864e-02, -2.2102e-01, -4.2693e-02,  5.2748e-02,
        2.8726e-01,  1.2315e-01, -2.8662e-02,  7.8294e-02,  4.6754e-01,
       -2.4589e-01, -1.1064e-01,  7.2250e-02, -9.4980e-02, -2.7548e-01,
       -5.4097e-01,  1.2823e-01, -8.2408e-02,  3.1035e-01, -6.3394e-02,
       -7.3755e-01, -5.4992e-01,  9.9999e-02, -2.0758e-01, -3.9674e-02,
        2.0664e-01, -9.7557e-02, -3.7092e-01,  2.7901e-01, -6.2218e-01,
       -1.0280e-01,  2.3271e-01,  4.3838e-01,  3.2445e-02, -2.9866e-01,
       -7.3611e-02,  7.1594e-01,  1.4241e-01,  2.7770e-01, -3.9892e-01,
        3.6656e-02,  1.5759e-01,  8.2014e-02, -5.7343e-01,  3.5457e-01,
        2.2491e-01, -6.2699e-01, -8.8106e-02,  2.4361e-01,  3.8533e-01,
       -1.4083e-01,  1.7691e-01,  7.0897e-02,  1.7951e-01, -4.5907e-01,
       -8.2120e-01, -2.6631e-02,  6.2549e-02,  4.2415e-01, -8.9630e-02,
       -2.4654e-01,  1.4156e-01,  4.0187e-01, -4.1232e-01,  8.4516e-02,
       -1.0626e-01,  7.3145e-01,  1.9217e-01,  1.4240e-01,  2.8511e-01,
       -2.9454e-01, -2.1948e-01,  9.0460e-01, -1.9098e-01, -1.0340e+00,
       -1.5754e-01, -1.1964e-01,  4.9888e-01, -1.0624e+00, -3.2820e-01,
       -1.1232e-02, -7.9482e-01,  3.7275e-01, -6.8710e-03, -2.5772e-01,
       -4.7005e-01, -4.1387e-01, -6.4089e-02, -2.8033e-01, -4.0778e-02,
       -2.4866e+00,  6.2494e-03, -1.0210e-02,  1.2752e-01,  3.4965e-01,
       -1.2571e-01,  3.1570e-01,  4.1926e-01,  2.0056e-01, -5.5984e-01,
       -2.2801e-01,  1.2012e-01, -2.0518e-03, -8.9764e-02, -8.0373e-02,
        1.1969e-02, -2.6978e-01,  3.4829e-01,  7.3664e-03, -1.1137e-01,
        6.3410e-01,  3.8449e-01, -6.2248e-01,  4.1145e-02,  2.5922e-01,
        6.5811e-01, -4.9548e-01, -1.3030e-01, -3.8279e-01,  1.1156e-01,
       -4.3085e-01,  3.4473e-01,  2.7109e-02, -2.5108e-01, -2.8011e-01,
        2.1662e-01,  3.2660e-01,  5.5895e-02,  7.6077e-02, -5.2480e-02,
        4.5928e-02, -2.5266e-01,  5.2845e-01, -1.3145e-01, -1.2453e-01,
        4.0556e-01,  3.1877e-01,  2.4415e-02, -2.2620e-01, -6.1960e-01,
       -4.0886e-01, -3.5534e-02, -5.5123e-03,  2.3438e-01,  8.7854e-01,
       -2.5161e-01,  4.0600e-01, -4.4284e-01,  3.4934e-01, -5.6429e-01,
       -2.3676e-01,  6.2199e-01, -2.8175e-01,  4.2024e-01,  1.0043e-01,
       -1.4720e-01,  4.9593e-01, -3.5850e-01, -1.3998e-01, -2.7494e-01,
        2.3827e-01,  5.7268e-01,  7.9025e-02,  1.7872e-02, -2.1829e-01,
        5.5050e-02, -5.4200e-01,  1.6788e-01,  3.9065e-01,  3.0209e-01,
        2.3040e-01, -3.9351e-02, -2.1078e-01, -2.7224e-01,  1.6907e-01,
        5.4819e-01,  9.4888e-02,  7.9798e-01, -6.6158e-02,  1.9844e-01,
        2.0307e-01,  4.4808e-02, -1.0240e-01, -6.9909e-02, -3.6756e-02,
        9.5159e-02, -2.7830e-01, -1.0597e-01, -1.6276e-01, -1.8211e-01,
       -3.1897e-01, -2.1633e-01,  1.4994e-01, -7.2057e-02,  2.2264e-01,
       -4.5551e-01,  3.0341e-01,  1.8431e-01,  2.1681e-01, -3.1940e-01,
        2.6426e-01,  5.8106e-01,  5.4635e-02,  6.3238e-01,  4.3169e-01,
        9.0343e-02,  1.9494e-01,  3.5483e-01, -2.0706e-02, -7.3117e-01,
        1.2941e-01,  1.7418e-01, -1.5065e-01,  5.3355e-02,  4.4794e-02,
       -1.6600e-01,  2.2007e-01, -5.3970e-01, -2.4968e-01, -2.6464e-01,
       -5.5515e-01,  5.8242e-01,  2.2295e-01,  2.4433e-01,  4.5275e-01,
        3.4693e-01,  1.2255e-01, -3.9059e-02, -3.2749e-01, -2.7891e-01,
        1.3766e-01,  3.8392e-01,  1.0543e-03, -1.0242e-02,  4.9205e-01,
       -1.7922e-01,  4.1215e-02,  1.3547e-01, -2.0598e-01, -2.3194e-01,
       -7.7701e-01, -3.8237e-01, -7.6383e-01,  1.9418e-01, -1.5441e-01,
        8.9740e-01,  3.0626e-01,  4.0376e-01,  2.1738e-01, -3.8050e-01],
      dtype=float32)

What's interesting is that Doc and Span objects themselves have vectors, derived from the averages of individual token vectors.
This makes it possible to compare similarities between whole documents.


In [5]:
doc = nlp(u'The quick brown fox jumped over the lazy dogs.')

doc.vector


Out[5]:
array([-1.96635887e-01, -2.32740352e-03, -5.36607020e-02, -6.10564947e-02,
       -4.08843048e-02,  1.45266443e-01, -1.08268000e-01, -6.27789786e-03,
        1.48455709e-01,  1.90697408e+00, -2.57692993e-01, -1.95818534e-03,
       -1.16141019e-02, -1.62858292e-01, -1.62938282e-01,  1.18210977e-02,
        5.12646027e-02,  1.00078702e+00, -2.01447997e-02, -2.54611671e-01,
       -1.28316596e-01, -1.97198763e-02, -2.89733019e-02, -1.94347113e-01,
        1.26644447e-01, -8.69869068e-02, -2.20812604e-01, -1.58452198e-01,
        9.86308008e-02, -1.79210991e-01, -1.55290633e-01,  1.95643142e-01,
        2.66436003e-02, -1.64984968e-02,  1.18824698e-01, -1.17830629e-03,
        4.99809943e-02, -4.23077159e-02, -3.86111848e-02, -7.47400150e-03,
        1.23448208e-01,  9.60620027e-03, -3.32463719e-02, -1.77848607e-01,
        1.19390726e-01,  1.87545009e-02, -1.84173390e-01,  6.91781715e-02,
        1.28520593e-01,  1.48827005e-02, -1.78013414e-01,  1.10003807e-01,
       -3.35464999e-02, -1.52476998e-02, -9.41195935e-02,  1.58633105e-02,
       -1.29811959e-02,  1.40140295e-01, -1.47720069e-01, -3.81718054e-02,
        4.66808230e-02,  3.31423879e-02,  7.97965974e-02,  1.60014004e-01,
        8.90410226e-03, -1.01237908e-01,  7.39663988e-02,  2.47380026e-02,
        4.26153988e-02,  9.66729969e-02,  2.87616011e-02,  7.22841993e-02,
        1.76565602e-01,  7.55538046e-02,  1.10501610e-01, -1.02358103e-01,
       -5.43345436e-02, -4.12176028e-02,  3.98623049e-02, -2.98339734e-03,
       -5.32988012e-02,  1.90624595e-01, -6.42587021e-02, -1.76225007e-02,
        3.94165330e-02, -1.14773512e-01,  4.25241649e-01,  2.07243040e-01,
        2.60730416e-01,  1.31226778e-01, -8.00508037e-02,  6.88939020e-02,
        7.05293044e-02, -1.10744104e-01,  4.14580032e-02,  5.13269613e-03,
       -1.29179001e-01, -5.84542975e-02,  9.13560018e-02, -1.75975591e-01,
        9.52741057e-02,  1.37699964e-02, -1.30865201e-01, -4.76420000e-02,
        1.61670998e-01, -6.76959991e-01,  2.68619388e-01, -7.94106945e-02,
        8.56394917e-02, -5.94138019e-02,  7.44821057e-02, -1.67490095e-01,
        1.97447598e-01, -2.71580786e-01,  1.51915969e-02,  1.12019002e-01,
       -4.32585999e-02, -1.03554968e-02,  6.33272156e-02,  5.20200143e-03,
        4.94491048e-02, -1.07016601e-01, -6.45387918e-02, -1.76269561e-01,
       -1.98135704e-01,  4.17800918e-02,  1.23686995e-02, -1.13280594e-01,
       -4.03523073e-02, -4.21132054e-03, -9.65667963e-02, -7.12300017e-02,
       -2.19088510e-01,  6.41715974e-02,  1.11634992e-01, -7.12868944e-02,
       -8.27060193e-02,  1.53889004e-02,  6.84699565e-02, -5.50561920e-02,
       -1.84788990e+00, -4.75010052e-02,  1.31487206e-01,  1.03359401e-01,
        1.80857688e-01, -8.03041980e-02,  2.27739997e-02,  5.56868985e-02,
        9.20986086e-02,  6.22248054e-02,  4.86670025e-02, -4.06427011e-02,
        3.83703932e-02, -4.05869968e-02, -2.26339817e-01,  3.69174965e-02,
       -1.30066186e-01,  1.27621710e-01,  2.76701003e-02, -1.39992401e-01,
       -3.75526994e-02, -8.11104029e-02, -1.78196102e-01, -1.21652998e-01,
       -5.88919744e-02, -1.06128812e-01, -4.72390745e-03, -1.14130601e-01,
       -7.60087445e-02, -9.48704034e-02,  1.68780401e-01,  3.82669941e-02,
       -1.68303996e-01, -1.30991384e-01, -2.46409744e-01,  1.42855030e-02,
        1.23633012e-01,  7.95699935e-03, -3.22283022e-02,  3.75844017e-02,
       -4.48104031e-02, -2.00578898e-01, -2.86081016e-01, -1.83181003e-01,
       -5.46899159e-04,  6.52990937e-02,  2.34263036e-02, -7.60660022e-02,
        1.13897599e-01, -7.05380812e-02,  1.30277812e-01,  2.83973999e-02,
        1.73887815e-02, -1.71358977e-02,  1.78455990e-02,  1.86773703e-01,
        1.83613986e-01, -4.05438878e-02,  1.28929759e-03, -3.71900201e-03,
       -1.97373003e-01,  4.78463694e-02, -2.21408010e-01,  2.68826094e-02,
        2.40951017e-01,  7.42616802e-02,  7.53984973e-02, -7.67349079e-02,
       -5.37766796e-03, -8.06540065e-03,  1.88790001e-02,  8.31135064e-02,
       -5.20760007e-02,  1.29393607e-01,  4.09864075e-02,  7.31946975e-02,
       -1.64099425e-01,  1.17529690e-01, -6.96440935e-02,  1.91028208e-01,
        1.01721905e-01,  6.34808987e-02, -8.29815865e-02, -6.95784390e-03,
       -1.69757873e-01, -2.02478573e-01,  3.65395918e-02,  1.32345587e-01,
        3.53013016e-02,  2.27603033e-01, -1.52753398e-01,  7.80210178e-03,
        2.06879750e-02, -8.63540452e-03,  9.85722095e-02, -2.91380938e-02,
       -1.42988954e-02, -9.39018354e-02,  1.43968105e-01,  7.82396942e-02,
       -1.93540990e-01, -9.36544985e-02, -8.23533013e-02,  4.40272018e-02,
       -2.22195080e-03, -1.29856914e-01, -1.53841600e-01, -1.55329984e-02,
       -2.55266696e-01,  1.14425398e-01, -1.03161987e-02, -4.66439016e-02,
       -5.69390282e-02,  7.72780031e-02,  1.28908500e-01,  1.61679000e-01,
        1.50837511e-01,  6.18334934e-02, -9.06937942e-02, -3.52137014e-02,
        1.35956988e-01,  7.52059072e-02,  5.73905036e-02, -1.65402606e-01,
        1.68419987e-01, -1.83722824e-01,  5.91069926e-03, -1.25354990e-01,
        3.95771042e-02,  5.67352995e-02, -5.63519308e-03,  1.53597593e-01,
       -6.84822723e-02, -1.40976995e-01, -3.62732522e-02, -2.61475928e-02,
        2.50091963e-02,  1.18994810e-01, -2.66857035e-02,  7.50442073e-02,
        2.04583794e-01,  4.37736101e-02, -8.17096978e-02,  6.80228025e-02,
        5.50465994e-02, -2.39979066e-02,  7.68290013e-02, -5.76773956e-02,
        8.30340981e-02,  3.63199934e-02, -1.65820405e-01,  2.55408939e-02,
       -5.30679002e-02, -1.35961995e-01, -1.03501797e-01,  1.36406809e-01,
        9.66293067e-02,  7.33902007e-02, -1.83055893e-01, -2.73141060e-02],
      dtype=float32)

Identifying similar vectors

The best way to expose vector relationships is through the .similarity() method of Doc tokens.


In [6]:
# Create a three-token Doc object:
tokens = nlp(u'lion cat pet')

# Iterate through token combinations:
for token1 in tokens:
    for token2 in tokens:
        print(token1.text, token2.text, token1.similarity(token2))


lion lion 1.0
lion cat 0.5265437
lion pet 0.39923772
cat lion 0.5265437
cat cat 1.0
cat pet 0.7505456
pet lion 0.39923772
pet cat 0.7505456
pet pet 1.0

Note that order doesn't matter. `token1.similarity(token2)` has the same value as `token2.similarity(token1)`.

To view this as a table:


In [7]:
# For brevity, assign each token a name
a,b,c = tokens

# Display as a Markdown table (this only works in Jupyter!)
from IPython.display import Markdown, display
display(Markdown(f'<table><tr><th></th><th>{a.text}</th><th>{b.text}</th><th>{c.text}</th></tr>\
<tr><td>**{a.text}**</td><td>{a.similarity(a):{.4}}</td><td>{b.similarity(a):{.4}}</td><td>{c.similarity(a):{.4}}</td></tr>\
<tr><td>**{b.text}**</td><td>{a.similarity(b):{.4}}</td><td>{b.similarity(b):{.4}}</td><td>{c.similarity(b):{.4}}</td></tr>\
<tr><td>**{c.text}**</td><td>{a.similarity(c):{.4}}</td><td>{b.similarity(c):{.4}}</td><td>{c.similarity(c):{.4}}</td></tr>'))


lioncatpet
**lion**1.00.52650.3992
**cat**0.52651.00.7505
**pet**0.39920.75051.0

As expected, we see the strongest similarity between "cat" and "pet", the weakest between "lion" and "pet", and some similarity between "lion" and "cat". A word will have a perfect (1.0) similarity with itself.

If you're curious, the similarity between "lion" and "dandelion" is very small:


In [6]:
nlp(u'lion').similarity(nlp(u'dandelion'))


Out[6]:
0.18064451829601527

Opposites are not necessarily different

Words that have opposite meaning, but that often appear in the same context may have similar vectors.


In [7]:
# Create a three-token Doc object:
tokens = nlp(u'like love hate')

# Iterate through token combinations:
for token1 in tokens:
    for token2 in tokens:
        print(token1.text, token2.text, token1.similarity(token2))


like like 1.0
like love 0.657904
like hate 0.657465
love like 0.657904
love love 1.0
love hate 0.63931
hate like 0.657465
hate love 0.63931
hate hate 1.0

Vector norms

It's sometimes helpful to aggregate 300 dimensions into a Euclidian (L2) norm, computed as the square root of the sum-of-squared-vectors. This is accessible as the .vector_norm token attribute. Other helpful attributes include .has_vector and .is_oov or out of vocabulary.

For example, our 685k vector library may not have the word "nargle". To test this:


In [8]:
tokens = nlp(u'dog cat nargle')

for token in tokens:
    print(token.text, token.has_vector, token.vector_norm, token.is_oov)


dog True 7.03367 False
cat True 6.68082 False
nargle False 0.0 True

Indeed we see that "nargle" does not have a vector, so the vector_norm value is zero, and it identifies as out of vocabulary.

Vector arithmetic

Believe it or not, we can actually calculate new vectors by adding & subtracting related vectors. A famous example suggests

"king" - "man" + "woman" = "queen"
Let's try it out!


In [9]:
from scipy import spatial

cosine_similarity = lambda x, y: 1 - spatial.distance.cosine(x, y)

king = nlp.vocab['king'].vector
man = nlp.vocab['man'].vector
woman = nlp.vocab['woman'].vector

# Now we find the closest vector in the vocabulary to the result of "man" - "woman" + "queen"
new_vector = king - man + woman
computed_similarities = []

for word in nlp.vocab:
    # Ignore words without vectors and mixed-case words:
    if word.has_vector:
        if word.is_lower:
            if word.is_alpha:
                similarity = cosine_similarity(new_vector, word.vector)
                computed_similarities.append((word, similarity))

computed_similarities = sorted(computed_similarities, key=lambda item: -item[1])

print([w[0].text for w in computed_similarities[:10]])


['king', 'queen', 'commoner', 'highness', 'prince', 'sultan', 'maharajas', 'princes', 'kumbia', 'kings']

So in this case, "king" was still closer than "queen" to our calculated vector, although "queen" did show up!

Next up: Sentiment Analysis