Text Similarity and Document Clustering
Text Similarity and Document Clustering
Previous chapters have covered several techniques of analyzing text and extracting interesting
insights. We have looked at supervised machine learning (ML) techniques that are used to
classify or categorize text documents into several pre-assumed categories. Unsupervised
techniques like topic models and document summarization have also been also covered,
which involved trying to extract and retrieve key themes and information from large text
documents and corpora. In this chapter, we will be looking at several other techniques and
use-cases that leverage unsupervised learning and information retrieval concepts.
If you refresh your memory of Chapter 4, text categorization is indeed an interesting
problem that has several applications, most notably in the classification of news articles
and email. But one constraint in text classification is that we need some training data with
manually labeled categories because we use supervised learning algorithms to build our
classification model. The efforts of building this dataset are definitely not easy, because
to build a good model, you need a sizeable amount of training data. For this, we need to
spend time and manual effort in labeling data, building a model, and then finally using it to
classify new documents. Can we instead make the machine do it? Yes, as a matter of fact, we
can. This chapter specifically addresses looking at the content of text documents, analyzing
their similarity using various measures, and clustering similar documents together.
Text data is unstructured and highly noisy. We get the benefits of well-labeled
training data and supervised learning when performing text classification. But document
clustering is an unsupervised learning process, where we are trying to segment and
categorize documents into separate categories by making the machine learn about the
various text documents, their features, similarities, and the differences among them.
This makes document clustering more challenging, albeit interesting. Consider having
a corpus of documents that talk about various different concepts and ideas. Humans are
wired in such a way that we use our learning from the past and apply it to distinguish
documents from each other. For example, the sentence The fox is smarter than the
dog is more similar to The fox is faster than the dog than it is to Python is an excellent
programming language. We can easily spot and intuitively figure out specific keyphrases
like Python, fox, dog, programming, and so on, which help us determine which sentences
or documents are more similar. But can we do that programmatically? In this chapter,
we will focus on several concepts related to text similarity, distance metrics, and
unsupervised ML algorithms to answer the following questions:
Important Concepts
Our main objectives in this chapter are to understand text similarity and clustering.
Before moving on to the actual techniques and algorithms, this section will discuss some
important concepts related to information retrieval, document similarity measures, and
machine learning. Even though some of these concepts might be familiar to you from the
previous chapters, all of them will be useful to us as we gradually journey through this
chapter. Without further ado, let’s get started.
266
Chapter 6 ■ Text Similarity and Clustering
string can have different order, context, and semantics in the collection of documents
(entities), and these words can even have multiple different resolutions or possibilities
based on synonyms, antonyms, and negation modifiers.
Feature Engineering
Feature engineering or feature extraction is something which you know quite well by
now. Methods like Bag of Words, TF-IDF, and word vectorization models are typically
used to represent or model documents in the form of numeric vectors so that applying
mathematical or machine learning techniques become much easier. You can use various
document representations using these feature-extraction techniques or even map each
letter or a word to a corresponding unique numeric identifier.
Similarity Measures
Similarity measures are used frequently in text similarity analysis and clustering. Any
similarity or distance measure usually measures the degree of closeness between two
entities, which can be any text format like documents, sentences, or even terms. This
measure of similarity can be useful in identifying similar entities and distinguishing
clearly different entities from each other. Similarity measures are very effective, and
sometimes choosing the right measure can make a lot of difference in the performance
of your final analytics system. Various scoring or ranking algorithms have also been
invented based on these distance measures. Two main factors determine the degree of
similarity between entities:
• Inherent properties or features of the entities
• Measure formula and properties
There are several distance measures that measure similarity, and we will be covering
several of them in future sections. However, an important thing to remember is that all
distance measures of similarity are not distance metrics of similarity. The excellent paper
by A. Huang, “Similarity Measures for Text Document Clustering,” talks about this in
detail. Consider a distance measure d and two entities (say they are documents in our
context) x and y. The distance between x and y, which is used to determine the degree of
similarity between them, can be represented as d(x, y), but the measure d can be called as
a distance metric of similarity if and only if it satisfies the following four conditions:
1. The distance measured between any two entities, say x and y,
must be always non-negative, that is, d ( x , y ) ³ 0 .
2. The distance between two entities should always be zero if
and only if they are both identical, that is, d ( x , y ) ³ 0 iff x = y .
3. This distance measure should always be symmetric, which
means that the distance from x to y is always the same as the
distance from y to x. Mathematically this is represented as
d (x, y ) = d ( y, x ) .
267
Chapter 6 ■ Text Similarity and Clustering
Text Normalization
We will need to normalize our text documents and corpora as usual before we perform
any further analyses or NLP. For this we will reuse our normalization module from
Chapter 5 but with a few more additions specifically aimed toward this chapter. The
complete normalization module is available in the code files for this chapter in the file
[Link], but I will still be highlighting the new additions in our normalization
module in this section for your benefit.
To start, we have updated our stopwords list with several new words that have been
carefully selected after analyzing many corpora. The following code snippet illustrates:
stopword_list = [Link]('english')
stopword_list = stopword_list + ['mr', 'mrs', 'come', 'go', 'get', 'tell',
'listen', 'one', 'two', 'three', 'four', 'five',
'six', 'seven', 'eight',
'nine', 'zero', 'join', 'find', 'make', 'say', 'ask',
'tell', 'see', 'try', 'back', 'also']
268
Chapter 6 ■ Text Similarity and Clustering
You can see the new additions are words that are mostly generic verbs or nouns without
a lot of significance. This will be useful to us in feature extraction during text clustering. We
also add a new function in our normalization pipeline, which is to only extract text tokens
from a body of text for which we use regular expressions, as depicted in the following function:
import re
def keep_text_characters(text):
filtered_tokens = []
tokens = tokenize_text(text)
for token in tokens:
if [Link]('[a-zA-Z]', token):
filtered_tokens.append(token)
filtered_text = ' '.join(filtered_tokens)
return filtered_text
We add this in our final normalization function along with the other functions that
we have reused from previous chapters, including expanding contractions, unescaping
HTML, tokenization, removing stopwords, special characters, and lemmatization. The
updated normalization function is shown in the following snippet:
normalized_corpus = []
for text in corpus:
text = html_parser.unescape(text)
text = expand_contractions(text, CONTRACTION_MAP)
if lemmatize:
text = lemmatize_text(text)
else:
text = [Link]()
text = remove_special_characters(text)
text = remove_stopwords(text)
if only_text_chars:
text = keep_text_characters(text)
if tokenize:
text = tokenize_text(text)
normalized_corpus.append(text)
else:
normalized_corpus.append(text)
return normalized_corpus
Thus, as you can see, the preceding function is very similar to the one from Chapter 5
with only the addition of keeping text characters using the keep_text_characters()
function, which can be executed by setting the only_text_chars parameter to True.
269
Chapter 6 ■ Text Similarity and Clustering
Feature Extraction
We will also be using a feature-extraction function similar to the one used in Chapter 5.
The code will be very similar to our previous feature extractor, except we will be adding
some new parameters in this chapter. The function can be found in the [Link] file and
is also shown in the following snippet:
feature_type = feature_type.lower().strip()
feature_matrix = vectorizer.fit_transform(documents).astype(float)
You can see from the function definition that we have capabilities for Bag of Words
frequency, occurrences, and also TF-IDF–based features. The new additions in this
function include the addition of the min_df, max_df and ngram_range parameters and
also accepting them as optional arguments. The ngram_range is useful when we want
to add bigrams, trigrams, and so on as additional features. The min_df parameter can
be expressed by a threshold value within a range of [0.0, 1.0] and it will ignore terms
as features that will have a document frequency strictly lower than the input threshold
value. The max_df parameter can also be expressed by a threshold value within a range
of [0.0, 1.0] and it will ignore terms as features that will have a document frequency
strictly higher than the input threshold value. The intuition behind this would be that
these words, if they occur in almost all the documents, tend to have little value that would
help us in distinguishing among various types of documents. We will now deep dive into
the various techniques for text similarity.
270
Chapter 6 ■ Text Similarity and Clustering
Text Similarity
The main objective of text similarity is to analyze and measure how two entities of text are
close or far apart from each other. These entities of text can be simple tokens or terms,
like words, or whole documents, which may include sentences or paragraphs of text.
There are various ways of analyzing text similarity, and we can classify the intent of text
similarity broadly into the following two areas:
• Lexical similarity: This involves observing the contents of the
text documents with regard to syntax, structure, and content and
measuring their similarity based on these parameters.
• Semantic similarity: This involves trying to find out the semantics,
meaning, and context of the documents and then trying to see
how close they are to each other. Dependency grammars and
entity recognition are handy tools that can help in this.
Note that the most popular area is lexical similarity, because the techniques
are more straightforward, easy to implement, and you can also cover several parts of
semantic similarity using simple models like the Bag of Words. Usually distance metrics
will be used to measure similarity scores between text entities, and we will be mainly
covering the following two broad areas of text similarity:
• Term similarity: Here we will measure similarity between
individual tokens or words.
• Document similarity: Here we will be measuring similarity
between entire text documents.
The idea is to implement and use several distance metrics and see how we can
measure and analyze similarity among entities that are just simple words, and then
how things change when we measure similarity among documents that are groups of
individual words.
271
Chapter 6 ■ Text Similarity and Clustering
import numpy as np
def vectorize_terms(terms):
terms = [[Link]() for term in terms]
terms = [[Link](list(term)) for term in terms]
terms = [[Link]([ord(char) for char in term])
for term in terms]
return terms
The function takes input a list of words or terms and returns the corresponding
character vectors for the words. Bag of Characters vectorization is very similar to the Bag
of Words model except here we compute the frequency of each character in the word.
Sequence or word orders are not taken into account. The following function helps in
computing this:
def boc_term_vectors(word_list):
word_list = [[Link]() for word in word_list]
unique_chars = [Link](
[Link]([list(word)
for word in word_list]))
word_list_term_counts = [{char: count for char, count in
itemfreq(list(word))}
for word in word_list]
In that function, we take in a list of words or terms and then extract the unique
characters from all the words. This becomes our feature list, just like we do in Bag of
Words, where instead of characters, unique words are our features. Once we have this list
of unique_chars, we get the count for each of the characters in each word and build our
Bag of Characters vectors.
We can now see our previous functions in action in the following snippet. We will be
using a total of four example terms and computing the similarity among them later on:
root = 'Believe'
term1 = 'beleive'
term2 = 'bargain'
term3 = 'Elephant'
272
Chapter 6 ■ Text Similarity and Clustering
# Character vectorization
vec_root, vec_term1, vec_term2, vec_term3 = vectorize_terms(terms)
# show vector representations
In [103]: print '''
...: root: {}
...: term1: {}
...: term2: {}
...: term3: {}
...: '''.format(vec_root, vec_term1, vec_term2, vec_term3)
root: [ 98 101 108 105 101 118 101]
term1: [ 98 101 108 101 105 118 101]
term2: [ 98 97 114 103 97 105 110]
term3: [101 108 101 112 104 97 110 116]
root: [0 1 3 0 0 1 1 0 0 0 0 1]
term1: [0 1 3 0 0 1 1 0 0 0 0 1]
term2: [2 1 0 1 0 1 0 1 0 1 0 0]
term3: [1 0 2 0 1 0 1 1 1 0 1 0]
Thus you can see how we can easily transform text terms into numeric vector
representations. We will now be using several distance metrics to compute similarity
between the root word and the other three words mentioned in the preceding snippet.
There are a lot of distance metrics out there that you can use to compute and measure
similarities. We will be covering the following five metrics in this section:
• Hamming distance
• Manhattan distance
• Euclidean distance
• Levenshtein edit distance
• Cosine distance and similarity
273
Chapter 6 ■ Text Similarity and Clustering
We will be looking at the concepts for each distance metric and using the power of
numpy arrays to implement the necessary computations and mathematical formulae.
Once we do that, we will put them in action by measuring the similarity of our example
terms. First, though, we will set up some necessary variables storing the root term,
the other terms with which its similarity will be measures, and their various vector
representations using the following snippet:
root_term = root
root_vector = vec_root
root_boc_vector = boc_root
We are now ready to start computing similarity metrics and will be using the
preceding terms and their vector representations to measure similarities.
Hamming Distance
The Hamming distance is a very popular distance metric used frequently in information
theory and communication systems. It is distance measured between two strings under
the assumption that they are of equal length. Formally, it is defined as the number of
positions that have different characters or symbols between two strings of equal length.
Considering two terms u and v of length n, we can mathematically denote Hamming
distance as
n
hd ( u , v ) = å ( ui ¹ vi )
i =1
and you can also normalize it if you want by dividing the number of mismatches by the
total length of the terms to give the normalized hamming distance, which is represented
as
å (u i ¹ vi )
norm _ hd ( u , v ) = i =1
n
274
Chapter 6 ■ Text Similarity and Clustering
We will now measure the Hamming distance between our root term and the other
terms using the following code snippet:
Normalized Hamming distance between root: Believe and term: believe is 0.29
Normalized Hamming distance between root: Believe and term: bargain is 0.86
Traceback (most recent call last):
File "<ipython-input-117-7dfc67d08c3f>", line 4, in <module>
round(hamming_distance(root_vector, vector_term, norm=True), 2))
ValueError: The vectors must have equal lengths
You can see from the preceding output that terms 'Believe' and 'believe'
ignoring their case are most similar to each other with the Hamming distance of 2 or 0.29,
compared to the term 'bargain' giving scores of 6 or 0.86 (here, the smaller the score,
the more similar are the terms). The term 'Elephant' throws an exception because the
length of that term (term3) is 8 compared to length 7 of the root term 'Believe', hence
Hamming distance can’t be computed because the base assumption of strings being of
equal length is violated.
Manhattan Distance
The Manhattan distance metric is similar to the Hamming distance conceptually, where
instead of counting the number of mismatches, we subtract the difference between each
pair of characters at each position of the two strings. Formally, Manhattan distance is
also known as city block distance, L1 norm, taxicab metric and is defined as the distance
275
Chapter 6 ■ Text Similarity and Clustering
between two points in a grid based on strictly horizontal or vertical paths instead of
the diagonal distance conventionally calculated by the Euclidean distance metric.
Mathematically it can be denoted as
n
md ( u , v ) = u - v 1 = å ui - vi
i =1
where u and v are the two terms of length n. The same assumption of the two terms
having equal length from Hamming distance holds good here. We can also compute the
normalized Manhattan distance by dividing the sum of the absolute differences by the
term length. This can be denoted by
u -v å u -v i i
norm _ md ( u , v ) = 1
= i =1
n n
where n is the length of each of the terms u and v. The following function helps us in
implementing Manhattan distance with the capability to also compute the normalized
Manhattan distance:
We will now compute the Manhattan distance between our root term and the other
terms using the previous function, as shown in the following code snippet:
276
Chapter 6 ■ Text Similarity and Clustering
...: term,
...: round(manhattan_distance(root_vector, vector_term,
norm=True),2))
...:
...:
Normalized Manhattan distance between root: Believe and term: believe is 1.14
Normalized Manhattan distance between root: Believe and term: bargain is 5.43
Traceback (most recent call last):
File "<ipython-input-122-d13a48d56a22>", line 4, in <module>
round(manhattan_distance(root_vector, vector_term, norm=True),2))
ValueError: The vectors must have equal lengths.
From those results you can see that as expected, the distance between 'Believe'
and 'believe' ignoring their case is most similar to each other, with a score of 8 or
1.14, as compared to 'bargain', which gives a score of 38 or 5.43 (here the smaller the
score, the more similar the words). The term 'Elephant' yields an error because it has
a different length compared to the base term just as we noticed earlier when computing
Hamming distances.
Euclidean Distance
We briefly mentioned the Euclidean distance when comparing it with the Manhattan
distance in the earlier section. Formally, the Euclidean distance is also known as the
Euclidean norm, L2 norm, or L2 distance and is defined as the shortest straight-line
distance between two points. Mathematically this can be denoted as
n
ed ( u , v ) = u - v 2 = å (u - v )
2
i i
i =1
where the two points u and v are vectorized text terms in our scenario, each having
length n. The following function helps us in computing the Euclidean distance between
two terms:
We can now compare the Euclidean distance among our terms by using the
preceding function as depicted in the following code snippet:
From the preceding outputs you can see that the terms 'Believe' and 'believe'
are the most similar with a score of 5.66 compared to 'bargain' giving us a score of 17.94,
and 'Elephant' throws a ValueError because the base assumption that strings being
compared should have equal lengths holds good for this distance metric also.
So far, all the distance metrics we have used work on strings or terms of the same
length and fail when they are not of equal length. So how do we deal with this problem?
We will now look at a couple of distance metrics that work even with strings of unequal
length to measure similarity.
ìmax ( i ,j ) if min ( i ,j ) = 0 ü
ï ï
ï ì ldu ,v ( i - 1, j ) + 1 ü ï
ldu ,v ( i ,j ) = í ï ï ý
ïmin í ldu ,v ( i , j - 1 ) + 1 ý otherwise ï
ï ïld ( i - 1, j - 1) + C ï ï
î î u ,v ui ¹ v j þ þ
where i and j are basically indices for the terms u and v. The third equation in the minimum
above has a cost function denoted by Cui ¹ v j such that it has the following conditions
ïì 1 if ui ¹ v j ïü
Cui ¹ v j = í ý
îï0 if ui = v j þï
and this denotes the indicator function, which depicts the cost associated with two
characters being matched for the two terms (the equation represents the match or
mismatch operation). The first equation in the previous minimum stands for the deletion
278
Chapter 6 ■ Text Similarity and Clustering
operation, and the second equation represents the insertion operation. The function
ldu, v(i, j) thus covers all the three operations of insertion, deletion, and addition as we
mentioned earlier and it denotes the Levenshtein distance as measured between the first
i characters for the term u and the first j characters of the term v. There are also several
interesting boundary conditions with regard to the Levenshtein edit distance:
• The minimum value that the edit distance between two terms can
take is the difference in length of the two terms.
• The maximum value of the edit distance between two terms can
be the length of the term that is larger.
• If the two terms are equal, the edit distance is zero.
• Hamming distance between two terms is an upper bound for
Levenshtein edit distance if and only if the two terms have equal
length.
• This being a distance metric also satisfies the triangle inequality
property, discussed earlier when we talked about distance
metrics.
There are various ways of implementing Levenshtein distance computations for
terms. Here we will start with an example of two of our terms. Considering the root term
'believe' and another term 'beleive' (we ignore case in our computations). The edit
distance would be 2 because we would need the following two operations:
• 'beleive' → 'beliive' (substitution of e to i)
• 'beliive' → 'believe' (substitution of i to e)
To implement this, we build a matrix that will basically compute the Levenshtein
distance between all the characters of both terms by comparing each character of the
first term with the characters of the second term. For computation, we follow a dynamic
programming approach to get the edit distance between the two terms based on the
last computed value. For the given two terms, the Levenshtein edit distance matrix our
algorithm should generate is shown in Figure 6-1.
279
Chapter 6 ■ Text Similarity and Clustering
You can see in Figure 6-1 that the edit distances are computed for each pair of
characters in the terms, as mentioned earlier, and the final edit distance value highlighted
in the figure gives us the actual edit distance between the two terms. This algorithm is
also known as the Wagner-Fischer algorithm and is available in the paper by R. Wagner
and M. Fischer titled “The String-to-String Correction Problem,” which you can refer to
if you are more interested in the details. The pseudocode for the same is shown in the
snippet below, courtesy of the paper:
# target prefixes can be reached from empty source prefix by inserting every
character
for j from 1 to n:
d[0, j] := j
# the final value of the matrix is the edit distance between the terms
return d[m, n]
You can see from the preceding function definition pseudocode how we have
captured the necessary formulae we used earlier to define Levenshtein edit distance.
We will now implement this pseudocode in Python. The preceding algorithm uses
O(mn) space because it stores the entire distance matrix, but it is enough to just store the
previous and current row of distances to get to the final result. We will do the same in our
code but we will also store the results in a matrix so that we can visualize it in the end. The
following function implements Levenshtein edit distance as mentioned:
280
Chapter 6 ■ Text Similarity and Clustering
import copy
import pandas as pd
That function returns both the final Levenshtein edit distance and the complete edit
matrix between the two terms u and v, which are taken as input. Remember, we need to
pass the terms directly in their raw string format and not their vector representations.
Also, we do not consider case of strings here and convert them to lowercase.
The following snippet computes the Levenshtein edit distance between our example
terms using the preceding function:
281
Chapter 6 ■ Text Similarity and Clustering
282
Chapter 6 ■ Text Similarity and Clustering
You can see from the preceding outputs that 'Believe' and 'beleive' are the
closest to each other, with an edit distance of 2 and the distances between 'Believe',
'bargain', and 'Elephant' are 6, indicating a total of 6 edit operations needed. The edit
distance matrices provide a more detailed insight into how the algorithm computes the
distances per iteration.
Thus you can see from the position of the vectors, the plots show more clearly how
the vectors are close or far apart from each other, and the cosine of the angle between
them gives us the Cosine similarity metric. Now we can formally define Cosine similarity
as the dot product of the two term vectors u and v, divided by the product of their L2
norms. Mathematically, we can represent the dot product between two vectors as
u × v = u v cos (q )
where θ is the angle between u and v and u represents the L2 norm for vector u and v
is the L2 norm for vector v. Thus we can derive the Cosine similarity from the above
formula as
283
Chapter 6 ■ Text Similarity and Clustering
u ×v åu v i i
cs ( u , v ) = cos (q ) = = i =1
u v n n
åu åv
i =1
2
i
i =1
2
i
where cs(u, v) is the Cosine similarity score between u and v. Here ui and vi are the
various features or components of the two vectors, and the total number of these features
or components is n. In our case, we will be using the Bag of Characters vectorization to
build these term vectors, and n will be the number of unique characters across the terms
under analysis. An important thing to note here is that the Cosine similarity score usually
ranges from –1 to +1, but if we use the Bag of Characters–based character frequencies for
terms or Bag of Words–based word frequencies for documents, the score will range from 0
to 1 because the frequency vectors can never be negative, and hence the angle between
the two vectors cannot exceed 90 . The Cosine distance is complimentary to the
similarity score can be computed by the formula,
u ×v åu v i i
cd ( u , v ) = 1 - cs ( u , v ) = 1 - cos (q ) = 1 - = 1- i =1
u v n n
åu åv
i =1
2
i
i =1
2
i
where cd(u, v) denotes the Cosine distance between the term vectors u and v. The
following function implements computation of Cosine distance based on the preceding
formulae:
We will now test the similarity between our example terms using their Bag of
Character representations, which we created earlier, available in the boc_root_vector
and the boc_vector_terms variables, as depicted in the following code snippet:
284
Chapter 6 ■ Text Similarity and Clustering
These vector representations do not take order of characters into account, hence
the similarity between the terms "Believe" and "believe" is 1.0 or a perfect 100 percent
because it contains the same characters with the same frequency. You can see how this
can be used in combination with a semantic dictionary like WordNet to provide correct
spelling suggestions by suggesting semantically and syntactically correct words from
a vocabulary when users type a misspelled word, by measuring the similarity between
the words. You can even try our different features here instead of single character
frequencies, like taking two characters at a time and computing their frequencies to build
the term vectors. This takes into account some of the sequences that characters maintain
in various terms. Try out different possibilities and compare the results! This distance
measure works very well when measuring similarity between large documents or
sentences, and we will see that in the next section when we discuss document similarity.
285
Chapter 6 ■ Text Similarity and Clustering
As usual, we will cover the concepts behind each metric, look at its mathematical
representations and definitions, and then implement it using Python. We will also test
our metrics on a toy corpus here with nine documents and a separate corpus with three
documents, which will be our query documents. For each of these three documents, we
will try to find out the most similar documents from the corpus of nine documents, which
will act as our index. Consider this to be a mini-simulation of what happens in a search
engine when you search with a sentence and the most relevant results are returned to you
from its index of web pages. In our case, the queries are in the form of three documents,
and relevant documents for each of these three will be returned from the index of nine
documents based on similarity metrics.
We will start with loading the necessary dependencies and the corpus of documents
on which we will be testing our various metrics, as shown in the following code snippet:
From that snippet you can see that we have various documents in our corpus index
that talk about the sky, programming languages, and animals. We also have three query
documents for which we want to get the most relevant documents from the toy_corpus
index, based on similarity computations. Before we start looking at metrics, we will
normalize the documents and vectorize them by extracting their TF-IDF features, as
shown in the following snippet:
286
Chapter 6 ■ Text Similarity and Clustering
Now that we have our documents normalized and vectorized with TF-IDF–based
vector representations, we will look at how to compute similarity for each of the metrics
we specified at the beginning of this section.
Cosine Similarity
We have seen the concepts with regards to computing Cosine similarity and also
implemented the same for term similarity. Here, we will reuse the same concepts to
compute the Cosine similarity scores for documents instead of terms. The document
vectors will be the Bag of Words model–based vectors with TF-IDF values instead of
term frequencies. We have also taken only unigrams here, but you can experiment with
bigrams and so on as document features during the vectorization process. For each of
the three query documents, we will compute its similarity with the nine documents in
toy_corpus and return the n most similar documents where n is a user input parameter.
We will define a function that will take in the vectorized corpus and the document
corpus for which we want to compute similarities. We will get the similarity scores using the
dot product operation as before and finally we will sort them in reverse order and get the
top n documents with the highest similarity score. The following function implements this:
287
Chapter 6 ■ Text Similarity and Clustering
...:
...: doc_tfidf = query_docs_tfidf[index]
...: top_similar_docs = compute_cosine_similarity(doc_tfidf,
...: tfidf_features,
...: top_n=2)
...: print 'Document',index+1 ,':', doc
...: print 'Top', len(top_similar_docs), 'similar docs:'
...: print '-'*40
...: for doc_index, sim_score in top_similar_docs:
...: print 'Doc num: {} Similarity Score: {}\nDoc: {}'.
format(doc_index+1,
...:
sim_score, toy_corpus[doc_index])
...: print '-'*40
...: print
288
Chapter 6 ■ Text Similarity and Clustering
The preceding output depicts the top two most relevant documents for each of the
query documents based on Cosine similarity scores, and you can see that the outputs
are quite what were expected. Documents about animals are similar to the document
that mentions the fox and the dog; documents about Python and Java are most similar to
the query document talking about them; and the beautiful blue sky is indeed similar to
documents that talk about the sky being blue and beautiful!
Also note the Cosine similarity scores in the preceding outputs, where 1.0 indicates
perfect similarity, 0.0 indicates no similarity, and any score between them indicates some
level of similarity based on how large that score is. For instance, in the last example,
the main document vectors are ['sky', 'blue', 'beautiful'] and because they all
match with the first document from the toy corpus, we get a 1.0 or 100 percent similarity
score, and only ['sky', 'blue'] match from the second most similar document, and
we get a 0.72 or 72 percent similarity score. And you should remember our discussion
from earlier where I mentioned briefly that Cosine similarity using Bag of Words–based
vectors only looks at token weights and does not consider order or sequence of the terms,
which is quite desirable in large documents because the same content may be depicted
in different ways, and capturing sequences there might lead to loss of information due to
unwanted mismatches.
We recommend using scikit-learn’s cosine_similarity() utility function, which
you can find under the [Link] module. It uses similar logic as our
implementation but is much more optimized and performs well on large corpora of
documents. You can also use gensim’s similarities module or the cossim() function
directly available in the [Link] module.
Hellinger-Bhattacharya Distance
The Hellinger-Bhattacharya distance (HB-distance) is also called the Hellinger distance or the
Bhattacharya distance. The Bhattacharya distance, originally introduced by A. Bhattacharya,
is used to measure the similarity between two discrete or continuous probability
distributions. E. Hellinger introduced the Hellinger integral in 1909, which is used in the
computation of the Hellinger distance. Overall, the Hellinger-Bhattacharya distance is an
f-divergence, which in the theory of probability is defined as a function D f ( P || Q ) , which
can be used to measure the difference between P and Q probability distributions. There are
many instances of f-divergences, including KL-divergence and HB-distance. Remember that
KL-divergence is not a distance metric because it violates the symmetric condition from the
four conditions necessary for a distance measure to be a metric.
HB-distance is computable for both continuous and discrete probability
distributions. In our case, we will be using the TF-IDF–based vectors as our document
distributions. This makes it discrete distributions because we have specific TF-IDF values
for specific feature terms, unlike continuous distributions. We can define the Hellinger-
Bhattacharya distance mathematically as
1
hbd ( u , v ) = u- v
2 2
289
Chapter 6 ■ Text Similarity and Clustering
å( )
n
1 2
hbd ( u , v ) = ui - vi
2 i =1
such that u = ( u1 , u2 ,¼, un ) and v = (v1 , v2 ,¼, vn ) are the document vectors having
length n indicating n features, which are the TF-IDF weights of the various terms in the
documents. As with the previous computation of Cosine similarity, we will build our
function on the same principles; basically we will accept as input a corpus of document
vectors and a single document vector for which we want to get the n most similar
documents from the corpus based on their HB-distances. The function implements the
preceding concepts in Python in the following snippet:
From the preceding implementation, you case see that we sort the documents based
on their scores in ascending order, unlike Cosine similarity, where 1.0 indicates perfect
similarity—since this is a distance metric between distributions, a value of 0 indicates
perfect similarity, and higher values indicate some dissimilarity being present. We can
now apply this function to our example corpora, compute their HB-distances, and see the
results in the following snippet:
290
Chapter 6 ■ Text Similarity and Clustering
...:
...: doc_tfidf = query_docs_tfidf[index]
...: top_similar_docs = compute_hellinger_bhattacharya_
distance(doc_tfidf,
...: tfidf_features,
...: top_n=2)
...: print 'Document',index+1 ,':', doc
...: print 'Top', len(top_similar_docs), 'similar docs:'
...: print '-'*40
...: for doc_index, sim_score in top_similar_docs:
...: print 'Doc num: {} Distance Score: {}\nDoc: {}'.
format(doc_index+1,
...: sim_score, toy_corpus[doc_
index])
...: print '-'*40
...: print
...:
...:
Document Similarity Analysis using Hellinger-Bhattacharya distance
============================================================
Document 1 : The fox is definitely smarter than the dog
Top 2 similar docs:
----------------------------------------
Doc num: 8 Distance Score: 0.0
Doc: The dog is smarter than the fox
----------------------------------------
Doc num: 7 Distance Score: 0.96
Doc: The fox is quicker than the lazy dog
----------------------------------------
291
Chapter 6 ■ Text Similarity and Clustering
You can see from the preceding outputs that documents with lower HB-distance
scores are more similar to the query documents, and the result documents are quite
similar to what we obtained using Cosine similarity. Compare the results and try out
these functions with larger corpora! I recommend using gensim’s hellinger() function,
available in the [Link] module (which uses the same logic as our preceding
function) when building large-scale systems for analyzing similarity.
n
f ( qi , CD ) × ( k1 + 1)
bm 25 (CD , QD ) = åidf ( qi ) ×
æ CD ö
f ( qi , CD ) + k1 × ç 1 - b + b ×
i =1
÷
è avgdl ø
where the function bm25(CD, QD) computes the BM25 rank or score of the document
CD based on the query document QD. The function idf(qi) gives us the inverse document
frequency (IDF) of the term qi in the corpus that contains CD and from which we want
to retrieve the relevant documents. If you remember, we computed IDFs in Chapter 4
when we implemented the TF-IDF feature extractor. Just to refresh your memory, it can
represented by
C
idf ( t ) = 1 + log
1 + df ( t )
292
Chapter 6 ■ Text Similarity and Clustering
where idf(t) represents the idf for the term t and C represents the count of the total
number of documents in our corpus and df(t) represents the frequency of the number
of documents in which the term t is present. There are various other methods of
implementing IDF, but we will be using this one, and on a side note the end outcome
from the different implementations is very similar. The function f(qi, CD) gives us the
frequency of the term qi in the corpus document CD. The expression |CD| indicates the
total length of the document CD which is measured by its number of words, and the
term avgdl represents the average document length of the corpus from which we will be
retrieving documents. Besides that, you will also observe there are two free parameters,
k1, which is usually in the range of [1.2, 2.0], and b, which is usually taken as 0.75. We
will be taking the value of k1 to be 1.5 in our implementation.
There are several steps we must go through to successfully implement and compute
BM25 scores for documents:
1. Build a function to get inverse document frequency (IDF)
values for terms in corpus.
2. Build a function for computing BM25 scores for query
document and corpus documents.
3. Get Bag of Words–based features for corpus documents and
query documents.
4. Compute average length of corpus documents and IDFs of the
terms in the corpus documents using function from point 1.
5. Compute BM25 scores, rank relevant documents, and fetch
the n most relevant documents for each query document
using the function in point 2.
We will start with implementing a function to extract and compute inverse document
frequencies of all the terms in a corpus of documents by using its Bag of Words features,
which will contain the term frequencies, and then convert them to IDFs using the formula
mentioned earlier. The following function implements this:
import [Link] as sp
We will now implement the main function for computing BM25 score for all
the documents in our corpus based on the query document and retrieving the top n
relevant documents from the corpus based on their BM25 score. The following function
implements the BM25 scoring framework:
293
Chapter 6 ■ Text Similarity and Clustering
The comments in the function are self-explanatory and explain how the BM25
scoring function is implemented. In simple terms, we first compute the numerator
expression in the BM25 mathematical equation we specified earlier and then compute
the denominator expression. Finally, we divide the numerator by the denominator to get
the BM25 scores for all the corpus documents. Then we sort them in descending order
and return the top n relevant documents with the highest BM25 score. In the following
snippet, we will test our function on our example corpora and see how it performs for
each of the query documents:
294
Chapter 6 ■ Text Similarity and Clustering
295
Chapter 6 ■ Text Similarity and Clustering
Doc: Among Programming languages, both Python and Java are the most used in
Analytics
----------------------------------------
You can now see how for each query document, we get expected and relevant
documents that have similar concepts just like the query documents. You can see that
the results are quite similar to the previous methods—because, of course, they are
all similarity and ranking metrics and are expected to return similar results. Notice
the BM25 scores of the relevant documents. The higher the score, the more relevant
is the document. Unfortunately, I was not able to find any production-ready scalable
implementation of the BM25 ranking framework in nltk or scikit-learn. However,
gensim seems to have a bm25 module under the [Link] package and if
you are interested you can give it a try. But the core of the algorithm is based on what we
implemented, and this should work pretty well on its own!
Try loading a bigger corpus of documents and test out these functions on some
sample query strings and documents. In fact, information retrieval frameworks like Solr
and Elasticsearch are built on top of Lucene, which use these types of ranking algorithms
to return relevant documents from an index of stored documents—and you can build
your own search engine using them! Interested readers can check out [Link]/
blog/found-bm-vs-lucene-default-similarity by [Link], the company behind the
popular Elasticsearch product, which tells that the performance of BM25 is much better
than the default similarity ranking implementation of Lucene.
Document Clustering
Document clustering or cluster analysis is an interesting area in NLP and text analytics
that applies unsupervised ML concepts and techniques. The main premise of document
clustering is similar to that of document categorization, where you start with a whole
corpus of documents and are tasked with segregating them into various groups based
on some distinctive properties, attributes, and features of the documents. Document
classification needs pre-labeled training data to build a model and then categorize
documents. Document clustering uses unsupervised ML algorithms to group the
documents into various clusters. The properties of these clusters are such that documents
inside one cluster are more similar and related to each other compared to documents
belonging to other clusters. Figure 6-3, courtesy of scikit-learn, visualizes an example
of clustering data points into three clusters based on its features.
296
Chapter 6 ■ Text Similarity and Clustering
The cluster analysis in Figure 6-3 depicts three clusters among the data points,
which are visualized using different colors. An important thing to remember here is
that clustering is an unsupervised learning technique, and from Figure 6-3 it is pretty
clear that there will always be some overlap among the clusters because there is no such
definition of a perfect cluster. All the techniques are based on math, heuristics, and some
inherent attributes toward generating clusters, and they are never a 100 percent perfect.
Hence, there are several techniques or methods for finding clusters. Some popular
clustering algorithms are briefly described as follows:
• Hierarchical clustering models: These clustering models are also
known as connectivity-based clustering methods and are based on
the concept that similar objects will be closer to related objects
in the vector space than unrelated objects, which will be farther
away from them. Clusters are formed by connecting objects based
on their distance and they can be visualized using a dendrogram.
The output of these models is a complete, exhaustive hierarchy
of clusters. They are mainly subdivided into agglomerative and
divisive clustering models.
297
Chapter 6 ■ Text Similarity and Clustering
298
Chapter 6 ■ Text Similarity and Clustering
import pandas as pd
import numpy as np
Title Synopsis
0 The Godfather In late summer 1945, guests are gathered...
1 The Shawshank Redemption In 1947, Andy Dufresne (Tim Robbins),...
2 Schindler's List The relocation of Polish Jews from...
3 Raging Bull The film opens in 1964, where an older...
4 Casablanca In the early years of World War II...
299
Chapter 6 ■ Text Similarity and Clustering
Movie Synopsis: In late summer 1945, guests are gathered for the wedding
reception of Don Vito Corleone's daughter Connie (Talia Shire) and Carlo
Rizzi (Gianni Russo). Vito (Marlon Brando), the head of the Corleone Mafia
family, is known to friends and associates as "Godfather." He and Tom Hagen
(Robert Duvall), the Corleone family lawyer, are hearing requests for favors
because, according to Italian tradition, "no Sicilian can refuse a request
on his daughter's wedding day." One of the men who asks the Don for a favor
is Amerigo Bonasera, a successful mortician and acquaintance of the Don,
whose daughter was brutally beaten by two young men because she refused
their advances; the men received minimal punishment. The Don is disappointed
in Bonasera, who'd avoided most contact with the Don due to Corleone's
nefarious business dealings. The Don's wife is godmother to Bonasera's
shamed daughter, a relationship the Don uses to extract new loyalty from the
undertaker. The Don agrees to have his men punish
You can see that we have our movie titles and their corresponding synopses, which
we load into a data frame and then store them in variables. A sample movie and a part of
its corresponding synopsis are also depicted in the preceding output. The main idea is to
cluster these movies into groups using their synopsis as raw input. We will extract features
from these synopses and use unsupervised learning algorithms on them to cluster them
together. The movie titles are just for representation and will be useful when we would
want to visualize and display clusters and their statistics. The data to be fed to the clustering
algorithms will be features extracted from the movie synopses just to make things clearer.
Before we can jump into each of the clustering methods, we will follow the same process of
normalization and feature extraction that we have followed in all our other processes:
# normalize corpus
norm_movie_synopses = normalize_corpus(movie_synopses,
lemmatize=True,
only_text_chars=True)
300
Chapter 6 ■ Text Similarity and Clustering
We keep text tokens in our normalized text and extract TF-IDF–based features
for unigrams and bigrams such that each feature occurs in at least in 25 percent of the
documents and at most 85 percent of the documents using the terms min_df and max_df.
We can see that we have a total of 100 rows for the 100 movies and a total of 307 features
for each movie. Some sample features are also printed in the preceding snippet. We will
start our clustering analysis next, now that we have our features and documents ready.
K-means Clustering
The k-means clustering algorithm is a centroid-based clustering model that tries to cluster
data into groups or clusters of equal variance. The criteria or measure that this algorithm
tries to minimize is inertia, also known as within-cluster sum-of-squares. Perhaps the one
main disadvantage of this algorithm is that the number of clusters k need to be specified
in advance, as is the case with all other centroid-based clustering models. This algorithm
is perhaps the most popular clustering algorithm out there and is frequently used due to
its ease of use as well as the fact that it is scalable with large amounts of data.
We can now formally define the k-means clustering algorithm along with its
mathematical notations. Consider that we have a dataset X with N data points or samples
and we want to group them into K clusters where K is a user-specified parameter. The
k-means clustering algorithm will segregate the N data points into K disjoint separate
clusters Ck, and each of these clusters can be described by the means of the cluster
samples. These means become the cluster centroids μk such that these centroids are not
bound by the condition that they have to be actual data points from the N samples in
X. The algorithm chooses these centroids and builds the clusters in such a way that the
inertia or within-cluster sums of squares are minimized. Mathematically, this can be
represented as
K
min å å
2
x n - mi
i =1 xn ÎCi
with regard to clusters Ci and centroids μi such that i Î{1, 2 , ¼, k} . This optimization is
an NP hard problem for all you algorithm enthusiasts out there. Lloyd’s algorithm is a
solution to this problem, which is an iterative procedure consisting of the following steps.
1. Choose initial k centroids μk by taking k random samples from
the dataset X.
2. Update clusters by assigning each data point or sample to its
nearest centroid point. Mathematically, we can represent this
as C k = {x n : x n - mk £ all x n - ml } where Ck denotes the
clusters.
301
Chapter 6 ■ Text Similarity and Clustering
1
mk =
Ck
åx
xn ÎC k
n
That snippet uses our implemented k-means function to cluster the movies based
on the TF-IDF features from the movie synopses, and we assign the cluster label for each
movie from the outcome of this cluster analysis by storing it in the movie_data dataframe
in the 'Cluster' column. You can see that we have taken k to be 5 in our analysis. We can
now see the total number of movies for each of the 5 clusters using the following snippet:
You can see that there are five cluster labels as expected, from 0 to 5, and each of
them has some movies belonging to the cluster whose counts are mentioned as the
302
Chapter 6 ■ Text Similarity and Clustering
second element of each tuple in the preceding list. But can we do more than just see
cluster counts? Of course we can! We will now define some functions to extract detailed
cluster analysis information, print them, and then visualize the clusters. We will start by
defining a function to extract important information from our cluster analysis:
cluster_details = {}
# get cluster centroids
ordered_centroids = clustering_obj.cluster_centers_.argsort()[:, ::-1]
# get key features for each cluster
# get movies belonging to each cluster
for cluster_num in range(num_clusters):
cluster_details[cluster_num] = {}
cluster_details[cluster_num]['cluster_num'] = cluster_num
key_features = [feature_names[index]
for index
in ordered_centroids[cluster_num, :topn_features]]
cluster_details[cluster_num]['key_features'] = key_features
return cluster_details
The preceding function is pretty self-explanatory. What it does is basically extract the
key features per cluster that were essential in defining the cluster from the centroids. It also
retrieves the movie titles that belong to each cluster and stores everything in a dictionary.
We will now define a function that uses this data structure and prints the results in a
clear format:
def print_cluster_data(cluster_data):
# print cluster details
for cluster_num, cluster_details in cluster_data.items():
print 'Cluster {} details:'.format(cluster_num)
print '-'*20
print 'Key features:', cluster_details['key_features']
print 'Movies in this cluster:'
print ', '.join(cluster_details['movies'])
print '='*40
Before we analyze the results of our k-means clustering algorithm, we will also
define a function to visualize the clusters. If you remember, we talked earlier about
challenges associated with visualizing clusters. This happens because we deal with
multidimensional feature spaces and unstructured text data. Numeric feature vectors
303
Chapter 6 ■ Text Similarity and Clustering
themselves may not make any sense to readers if they were visualized directly. So, there
are some techniques like principal component analysis (PCA) or multidimensional scaling
(MDS) to reduce the dimensionality such that we can visualize these clusters in 2- or
3-dimensional plots. We will be using MDS in our implementation for visualizing clusters.
MDS is an approach towards non-linear dimensionality reduction such that the
results can be visualized better in lower dimensional systems. The main idea is having a
distance matrix such that distances between various data points are captured. We will be
using Cosine similarity for this. MDS tries to build a lower-dimensional representation
of our data with higher numbers of features in the vector space such that the distances
between the various data points obtained using Cosine similarity in the higher
dimensional feature space is still similar in this lower-dimensional representation.
The scikit-learn implementation for MDS has two types of algorithms: metric and
non-metric. We will be using the metric approach because we will use the Cosine
similarity–based distance metric to build the input similarity matrix between the various
movies. Mathematically, MDS can be defined as follows: Let S be our similarity matrix
between the various data points (movies) obtained using Cosine similarity on the feature
matrix and X be the coordinates of the n input data points (movies). Disparities are
represented by d̂ij = t ( Sij ) , which is usually some optimal transformation of the similarity
values or could even be the raw similarity values themselves. The objective function for
MDS, called stress, is defined as sumi< j dij ( X ) - dˆij ( X ) . We implement MDS-based
visualization for clusters in the following function:
304
Chapter 6 ■ Text Similarity and Clustering
cluster_color_map = {}
cluster_name_map = {}
for cluster_num, cluster_details in cluster_data.items():
# assign cluster features to unique label
cluster_color_map[cluster_num] = generate_random_color()
cluster_name_map[cluster_num] = ', '.join(cluster_details['key_
features'][:5]).strip()
# map each unique cluster label with its coordinates and movies
cluster_plot_frame = [Link]({'x': x_pos,
'y': y_pos,
'label': movie_data['Cluster'].
[Link](),
'title': movie_data['Title'].values.
tolist()
})
grouped_plot_frame = cluster_plot_frame.groupby('label')
# set plot figure size and axes
fig, ax = [Link](figsize=plot_size)
[Link](0.05)
# plot each cluster using co-ordinates and movie titles
for cluster_num, cluster_frame in grouped_plot_frame:
marker = markers[cluster_num] if cluster_num < len(markers) \
else [Link](markers, size=1)[0]
[Link](cluster_frame['x'], cluster_frame['y'],
marker=marker, linestyle='', ms=12,
label=cluster_name_map[cluster_num],
color=cluster_color_map[cluster_num], mec='none')
ax.set_aspect('auto')
ax.tick_params(axis= 'x', which='both', bottom='off', top='off',
labelbottom='off')
ax.tick_params(axis= 'y', which='both', left='off', top='off',
labelleft='off')
fontP = FontProperties()
fontP.set_size('small')
[Link](loc='upper center', bbox_to_anchor=(0.5, -0.01),
fancybox=True,
shadow=True, ncol=5, numpoints=1, prop=fontP)
#add labels as the film titles
for index in range(len(cluster_plot_frame)):
[Link](cluster_plot_frame.ix[index]['x'],
cluster_plot_frame.ix[index]['y'],
cluster_plot_frame.ix[index]['title'], size=8)
# show the plot
[Link]()
The function is quite big, but the self-explanatory comments explain each step
clearly. We build our similarity matrix first using the Cosine similarity between
documents, get the cosine distances, and then transform the high dimensional feature
305
Chapter 6 ■ Text Similarity and Clustering
space into 2 dimensions using MDS. Then we plot the clusters using matplotlib with
a bit of necessary formatting to view the results in a nice way. This function is a generic
function and will work with any clustering algorithm with a dynamic number of clusters.
Each cluster will have its own color, symbol, and label in the terms of top distinguishing
features in the legend. The actual plot will plot each movie with its corresponding cluster
label with its own color and symbol.
We are now ready to analyze the cluster results of our k-means clustering using the
preceding functions. The following code snippet depicts the detailed analysis results for
k-means clustering:
Cluster 0 details:
--------------------
Key features: [u'car', u'police', u'house', u'father', u'room']
Movies in this cluster:
Psycho, Sunset Blvd., Vertigo, West Side Story, E.T. the Extra-Terrestrial,
2001: A Space Odyssey, The Silence of the Lambs, Singin' in the Rain, It's
a Wonderful Life, Some Like It Hot, Gandhi, To Kill a Mockingbird, Butch
Cassidy and the Sundance Kid, The Exorcist, The French Connection, It
Happened One Night, Rain Man, Fargo, Close Encounters of the Third Kind,
Nashville, The Graduate, American Graffiti, Pulp Fiction, The Maltese
Falcon, A Clockwork Orange, Rebel Without a Cause, Rear Window, The Third
Man, North by Northwest
========================================
Cluster 1 details:
--------------------
Key features: [u'water', u'attempt', u'cross', u'death', u'officer']
Movies in this cluster:
Chinatown, Apocalypse Now, Jaws, The African Queen, Mutiny on the Bounty
========================================
Cluster 2 details:
--------------------
Key features: [u'family', u'love', u'marry', u'war', u'child']
Movies in this cluster:
The Godfather, Gone with the Wind, The Godfather: Part II, The Sound of
Music, A Streetcar Named Desire, The Philadelphia Story, An American in
Paris, Ben-Hur, Doctor Zhivago, High Noon, The Pianist, Goodfellas, The
King's Speech, A Place in the Sun, Out of Africa, Terms of Endearment,
Giant, The Grapes of Wrath, Wuthering Heights, Double Indemnity, Yankee
Doodle Dandy
306
Chapter 6 ■ Text Similarity and Clustering
========================================
Cluster 3 details:
--------------------
Key features: [u'apartment', u'new', u'woman', u'york', u'life']
Movies in this cluster:
Citizen Kane, Titanic, 12 Angry Men, Rocky, The Best Years of Our Lives, My
Fair Lady, The Apartment, City Lights, Midnight Cowboy, Mr. Smith Goes to
Washington, Annie Hall, Good Will Hunting, Tootsie, Network, Taxi Driver
========================================
Cluster 4 details:
--------------------
Key features: [u'kill', u'soldier', u'men', u'army', u'war']
Movies in this cluster:
The Shawshank Redemption, Schindler's List, Raging Bull, Casablanca, One
Flew Over the Cuckoo's Nest, The Wizard of Oz, Lawrence of Arabia, On the
Waterfront, Forrest Gump, Star Wars, The Bridge on the River Kwai, Dr.
Strangelove or: How I Learned to Stop Worrying and Love the Bomb, Amadeus,
The Lord of the Rings: The Return of the King, Gladiator, From Here to
Eternity, Saving Private Ryan, Unforgiven, Raiders of the Lost Ark, Patton,
Braveheart, The Good, the Bad and the Ugly, The Treasure of the Sierra
Madre, Platoon, Dances with Wolves, The Deer Hunter, All Quiet on the
Western Front, Shane, The Green Mile, Stagecoach
========================================
Figure 6-4. Visualizing the output of K-means clustering on IMDb movie data
307
Chapter 6 ■ Text Similarity and Clustering
The preceding output shows the key features for each cluster and the movies in each
cluster, and you can also see the same in the visualization in Figure 6-4 (there is a lot
in that figure—if the text appears too small, check out the kmeans_clustering.png file,
available along with the code files for this chapter). Each cluster is depicted by the main
themes that define that cluster by its top features, and you can see popular movies like
The Godfather and The Godfather: Part II in the same cluster along with other movies
like Ben-Hur and so on which talk about 'family', 'love', 'war', and so on. Movies
like Star Wars, The Lord of the Rings, The Deer Hunter, Gladiator, Forrest Gump, and so
on are clustered together associated with themes like 'kill', 'soldier', 'army', and
'war'. Definitely interesting results considering the data used for clustering was just a few
paragraphs of synopsis per movie. Look more closely at the results and the visualization.
Can you notice any other interesting patterns?
Affinity Propagation
The k-means algorithm, although very popular, has the drawback that the user has to pre-
define the number of clusters. What if in reality there are more clusters or lesser clusters?
There are some ways of checking the cluster quality and seeing what the value of the
optimum k might be. Interested readers can check out the elbow method and the silhouette
coefficient, which are popular methods of determining the optimum k. Here we will talk
about an algorithm that tries to build clusters based on inherent properties of the data
without any pre-assumptions about the number of clusters. The affinity propagation (AP)
algorithm is based on the concept of “message passing” among the various data points to
be clustered, and no pre-assumption is needed about the number of possible clusters.
AP creates these clusters from the data points by passing messages between pairs
of data points until convergence is achieved. The entire dataset is then represented by a
small number of exemplars that act as representatives for samples. These exemplars are
analogous to the centroids you obtain from k-means or k-medoids. The messages that
are sent between pairs represent how suitable one of the points might be in being the
exemplar or representative of the other data point. This keeps getting updated in every
iteration until convergence is achieved, with the final exemplars being the representatives
of each cluster. Remember, one drawback of this method is that it is computationally
intensive because messages are passed between each pair of data points across the entire
dataset and can take substantial time to converge for large datasets.
We can now define the steps involved in the AP algorithm (courtesy of Wikipedia and
scikit-learn). Consider that we have a dataset X with n data points such that
X = {x1 , x 2 , ¼, x n } , and let sim(x, y) be the similarity function that quantifies the similarity
between two points x and y. In our implementation, we will be using Cosine similarity
again for this. The AP algorithm iteratively proceeds by executing two message-passing
steps as follows:
1. Responsibility updates are sent around, which can be
mathematically represented as
308
Chapter 6 ■ Text Similarity and Clustering
def affinity_propagation(feature_matrix):
sim = feature_matrix * feature_matrix.T
sim = [Link]()
ap = AffinityPropagation()
[Link](sim)
clusters = ap.labels_
return ap, clusters
We will now use this function to cluster our movies based on their synopses and
then we will print the number of movies in each cluster and the total number of clusters
formed by this algorithm:
From the preceding results, we can see that a total of 17 clusters have been created
by AP on our movie data containing 100 movies. Each cluster has movies ranging from as
low as 2 to as high as 12 movies. We shall now extract detailed cluster information, display
cluster statistics, and visualize the clusters similar to what we did for k-means clustering,
using our utility functions that we implemented in the K-means clustering section:
310
Chapter 6 ■ Text Similarity and Clustering
311
Chapter 6 ■ Text Similarity and Clustering
312
Chapter 6 ■ Text Similarity and Clustering
Figure 6-5. Visualizing the output of Affinity Propagation clustering on IMDb movie data
The preceding outputs show the contents of the different clusters and their
visualization. If the visual text in Figure 6-5 is too small, you can always refer to the file
affinity_prop_clustering.png, which contains the plot depicted in higher resolution.
You can see from the results that we now have a total of 17 clusters, and there are some
similarities where you will see similar movies that were grouped together in k-means
clustering are in similar clusters here also, and there are also notable differences where
many movies now have their own cluster. Are these clustering results better than the
previous one? Well a lot depends on human perspective, and since I have yet to watch
several of these movies, I leave this decision to you, dear reader! An important point to
note here is that a few keywords from the exemplars or centroids for each cluster may not
always depict the true essence or theme of that cluster, so a good idea here would be to
build topic models on each cluster and see the kind of topics you can extract from each
cluster that would make a better representation of each cluster (another example where
you can see how we can connect various text analytics techniques together).
313
Chapter 6 ■ Text Similarity and Clustering
Figure 6-6 clearly highlights how six separate data points start off as six clusters,
and then we slowly start grouping them in each step following a bottom-up approach.
We will be using an agglomerative hierarchical clustering algorithm in this section. In
agglomerative clustering, for deciding which clusters we should combine when starting
from the individual data point clusters, we need two things:
• A distance metric to measure the similarity or dissimilarity degree
between data points. We will be using the Cosine distance/
similarity in our implementation.
• A linkage criterion that determines the metric to be used for the
merging strategy of clusters. We will be using Ward’s method here.
314
Chapter 6 ■ Text Similarity and Clustering
The Ward’s linkage criterion minimizes the sum of squared differences within all the
clusters and is a variance minimizing approach. This is also known as Ward’s minimum
variance method and was initially presented by J. Ward. The idea is to minimize the
variances within each cluster using an objective function like the L2 norm distance
between two points. We can start with computing the initial cluster distances between
each pair of points using the formula
( )
dij = d {Ci , C j } = Ci - C j
2
where initially Ci indicates cluster i with one document, and at each iteration, we find the
pairs of clusters that lead to the least increase in variance for that cluster once merged. A
weighted squared Euclidean distance or L2 norm as depicted in the preceding formula
would suffice for this algorithm. We use Cosine similarity to compute the cosine distances
between each pair of movies for our dataset. The following function implements Ward’s
agglomerative hierarchical clustering.:
def ward_hierarchical_clustering(feature_matrix):
cosine_distance = 1 - cosine_similarity(feature_matrix)
linkage_matrix = ward(cosine_distance)
return linkage_matrix
To view the results of the hierarchical clustering, we need to plot a dendrogram using
the preceding linkage matrix, and so we implement the following function to build and
plot a dendrogram from the hierarchical clustering linkage matrix:
We are now ready to perform hierarchical clustering on our movie data! The
following code snippet shows Ward’s clustering in action:
315
Chapter 6 ■ Text Similarity and Clustering
316
Chapter 6 ■ Text Similarity and Clustering
The dendrogram in Figure 6-7 shows the clustering analysis results. The colors
indicate that there are three main clusters, which further get subdivided into more
granular clusters maintaining a hierarchy. (If you have trouble reading the small fonts, look
at the file ward_hierachical_clusters.png available with the code files in this chapter).
You will notice a lot of similarities with the results of the previous clustering algorithms.
The green colored movies like Raiders of the Lost Ark, The Lord of the Rings, Star
Wars, The Godfather, The Godfather: Part II, Pulp Fiction, A Clockwork Orange, and
Platoon are definitely some of the top movies and in fact classics in the action, adventure,
war, and crime-based genres.
The red colored movies include comedy-based movies like City Lights, The
Apartment, and My Fair Lady, and also several movies that belong to the drama genre
including Mutiny on the Bounty, 12 Angry Men, Annie Hall, Midnight Cowboy, Titanic,
and An American in Paris, with several of them having romantic plots too. Several of them
are even musicals, including Yankee Doodle Dandy, An American in Paris, Singin' in the
Rain, and My Fair Lady. It is definitely interesting indeed that with just movie synopses,
our algorithm has clustered movies with similar attributes and genres together!
The blue colored movies give us similar results, in that Braveheart and Gladiator are
action, drama, and war classics. We also have some classics related to drama, romance,
and biographies like The Sound of Music, Wuthering Heights, Terms of Endearment, and
Out of Africa. Toward the top of the dendrogram you will observe movies related to
science fiction and fantasy, like 2001: A Space Odyssey, Close Encounters of the Third Kind,
and E.T. the Extra-Terrestrial, all close to each other.
Can you find more interesting patterns? Which movies do you think do not belong
together in the same clusters? Can we build better clusters? Can we recommend similar
movies to watch based on clustering movies together? These are some interesting
questions to ponder, and I will leave them for you to look at and explore further.
Summary
I would like to really commend your efforts on staying with me till the end of this
chapter. We covered a lot here, including several topics in the challenging but very
interesting unsupervised machine learning domain. You now know how text similarity
can be computed and you learned about various kinds of distance measures and
metrics. We also looked at important concepts related to distance metrics and measures
and properties that make a measure into a metric. We explored concepts related to
unsupervised ML and saw how we can incorporate such techniques in document
clustering. Various ways of measuring term and document similarity were also
covered, and we implemented several of these techniques by successfully converting
mathematical equations into code using the power of Python and several open source
libraries. We touched on document clustering in detail, looking at the various concepts
and types of clustering models. Finally, we took a real-world example of clustering the top
hundred greatest movies of all time using IMDb movie synopses data and used different
clustering models like k-means, affinity propagation, and Ward’s hierarchical clustering
to build, analyze, and visualize clusters. This should be enough for you to get started
with analyzing document similarity and clustering, and you can even start combining
various techniques from the chapters covered so far. (Hint: Topic models with clustering,
building classifiers by combining supervised and unsupervised learning, and augmenting
recommendation systems using document clusters—just to name a few!)
317
CHAPTER 7
Natural language understanding has gained significant importance in the last decade
with the advent of machine learning (ML) and further advances like deep learning and
artificial intelligence. Computers and other machines can be programmed to learn
things and perform specific operations. The key limitation is their inability to perceive,
understand, and comprehend things like humans do. With the resurgence in popularity
of neural networks and advances made in computer architecture, we now have deep
learning and artificial intelligence evolving rapidly to make some efforts into trying to
engineer machines into learning, perceiving, understanding, and performing actions on
their own. You may have seen or heard several of these efforts, such as self-driving cars,
computers beating experienced players in games like chess and Go, and the proliferation
of chatbots on the Internet.
In Chapters 4–6, we have looked at various computational, language processing, and
ML techniques to classify, cluster, and summarize text. Back in Chapter 3 we developed
certain methods and programs to analyze and understand text syntax and structure.
This chapter will deal with methods that try to answer the question Can we analyze and
understand the meaning and sentiment behind a body of text?
Natural Language Processing (NLP) has a wide variety of applications that try to use
natural language understanding to infer the meaning and context behind text and use it to
solve various problems. We discussed several of these applications briefly in Chapter 1.
To refresh your memory, the following applications require extensive understanding of
text from the semantic perspective:
• Question Answering Systems
• Contextual recognition
• Speech recognition (for some applications)
Text semantics specifically deals with understanding the meaning of text or language.
When combined into sentences, words have lexical relations and contextual relations
between them lead to various types of relationships and hierarchies, and semantics sits
at the heart of all this in trying to analyze and understand these relationships and infer
meaning from them. We will be exploring various types of semantic relationships in natural
language and look at some NLP-based techniques for inferring and extracting meaningful
semantic information from text. Semantics is purely concerned with context and meaning,
and the structure or format of text holds little significance here. But sometimes even the
syntax or arrangement of words helps us in inferring the context of words and helps us
differentiate things like lead as a metal from lead as in the lead of a movie.
Sentiment analysis is perhaps the most popular application of text analytics, with a
vast number of tutorials, web sites, and applications that focus on analyzing sentiment of
various text resources ranging from corporate surveys to movie reviews. The key aspect of
sentiment analysis is to analyze a body of text for understanding the opinion expressed by
it and other factors like mood and modality. Usually sentiment analysis works best on text
that has a subjective context than on that with only an objective context. This is because
when a body of text has an objective context or perspective to it, the text usually depicts some
normal statements or facts without expressing any emotion, feelings, or mood. Subjective
text contains text that is usually expressed by a human having typical moods, emotions, and
feelings. Sentiment analysis is widely used, especially as a part of social media analysis for
any domain, be it a business, a recent movie, or a product launch, to understand its reception
by the people and what they think of it based on their opinions or, you guessed it, sentiment.
In this chapter, we will be covering several aspects from both semantic and
sentiment analysis for textual data. We will start with exploring WordNet, a lexical
database, and introduce a new concept called synsets. We will also explore various
semantic relationships and representations in natural language and we will cover
techniques such as word sense disambiguation and named entity recognition. In
sentiment analysis, we will be looking at how to use supervised ML techniques to analyze
sentiment and also at several unsupervised lexical techniques with more detailed insights
into natural language sentiment, mood, and modality.
Semantic Analysis
We have seen how terms or words get grouped into phrases that further form clauses
and finally sentences. Chapter 3 showed various structural components in natural
language, including parts of speech (POS), chunking, and grammars. All these concepts
fall under the syntactic and structural analysis of text data. Whereas we do explore
relationships of words, phrases, and clauses, these are purely based on their position,
syntax, and structure. Semantic analysis is more about understanding the actual context
and meaning behind words in text and how they relate to other words to convey some
information as a whole. As mentioned in Chapter 1, the definition of semantics itself is
the study of meaning, and linguistic semantics is a complete branch under linguistics
that deals with the study of meaning in natural language, including exploring various
relationships between words, phrases and symbols. Besides this, there are also various
ways to represent semantics associated with statements and propositions. We will be
broadly covering the following topics under semantic analysis:
• Exploring WordNet and synsets
• Analyzing lexical semantic relations
• Word sense disambiguation
• Named entity recognition
• Analyzing semantic representations
320
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
The main objective of these topics is to give you a clear understanding of the
resources you can leverage for semantic analysis as well as how to use these resources.
We will explore various concepts related to semantic analysis, which was covered
in Chapter 1, with actual examples. You can refresh your memory by revisiting the
“Language Semantics” section in Chapter 1. Without any further delay, let's get started!
Exploring WordNet
WordNet is a huge lexical database for the English Language. The database is a part of
Princeton University, and you can read more about it at [Link]
It was originally created in around 1985, in Princeton University’s Cognitive Science
Laboratory under the direction of Professor G. A. Miller. This lexical database consists
of nouns, adjective, verbs, and adverbs, and related lexical terms are grouped together
based on some common concepts into sets, known as cognitive synonym sets or synsets.
Each synset expresses a unique, distinct concept. At a high level, WordNet can be
compared to a thesaurus or a dictionary that provides words and their synonyms. On a
lower level, it is much more than that, with synsets and their corresponding terms having
detailed relationships and hierarchies based on their semantic meaning and similar
concepts. WordNet is used extensively as a lexical database, in text analytics, NLP, and
artificial intelligence (AI)-based applications.
The WordNet database consists of over 155,000 words, represented in more than
117,000 synsets, and contains over 206,000 word-sense pairs. The database is roughly 12
MB in size and can be accessed through various interfaces and APIs. The official web site
has a web application interface for accessing various details related to words, synsets,
and concepts related to the entered word. You can access it at [Link]
[Link]/perl/webwn or download it from [Link]
wordnet/download/. The download contains various packages, files, and tools related to
WordNet. We will be accessing WordNet programmatically using the interface provided
by the nltk package. We will start by exploring synsets and then various semantic
relationships using synsets.
Understanding Synsets
We will start exploring WordNet by looking at synsets since they are perhaps one of the
most important concepts and structures that tie everything together. In general, based on
concepts from NLP and information retrieval, a synset is a collection or set of data entities
that are considered to be semantically similar. This doesn’t mean that they will be exactly
the same, but they will be centered on similar context and concepts. Specifically in the
context of WordNet, a synset is a set or collection of synonyms that are interchangeable
and revolve around a specific concept. Synsets not only consist of simple words, but
also collocations. Polysemous word forms (words that sound and look the same but
have different but relatable meanings) are assigned to different synsets based on their
meaning. Synsets are connected to other synsets using semantic relations, which we shall
explore in a future section. Typically each synset has the term, a definition explaining
the meaning of the term, and some optional examples and related lemmas (collection
of synonyms) to the term. Some terms may have multiple synsets associated with them,
where each synset has a particular context.
321
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Let’s look at a real example by using nltk’s WordNet interface to explore synsets
associated with the term, 'fruit'. We can do this using the following code snippet:
term = 'fruit'
synsets = [Link](term)
# display total synsets
In [75]: print 'Total Synsets:', len(synsets)
Total Synsets: 5
We can see that there are a total of five synsets associated with the term 'fruit'.
What can these synsets indicate? We can dig deeper into each synset and its components
using the following code snippet:
Synset: Synset('yield.n.03')
Part of speech: [Link]
Definition: an amount of a product
Lemmas: [u'yield', u'fruit']
Examples: []
Synset: Synset('fruit.n.03')
Part of speech: [Link]
Definition: the consequence of some effort or action
Lemmas: [u'fruit']
Examples: [u'he lived long enough to see the fruit of his policies']
Synset: Synset('fruit.v.01')
Part of speech: [Link]
Definition: cause to bear fruit
Lemmas: [u'fruit']
Examples: []
322
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Synset: Synset('fruit.v.02')
Part of speech: [Link]
Definition: bear fruit
Lemmas: [u'fruit']
Examples: [u'the trees fruited early this year']
The preceding output shows us details pertaining to each synset associated with
the term 'fruit', and the definitions give us the sense of each synset and the lemma
associated with it. The part of speech for each synset is also mentioned, which includes
nouns and verbs. Some examples are also depicted in the preceding output that show
how the term is used in actual sentences. Now that we understand synsets better, let’s
start exploring various semantic relationships as mentioned.
Entailments
The term entailment usually refers to some event or action that logically involves or is
associated with some other action or event that has taken place or will take place. Ideally
this applies very well to verbs indicating some specific action. The following snippet
shows how to get entailments:
# entailments
In [80]: for action in ['walk', 'eat', 'digest']:
...: action_syn = [Link](action, pos='v')[0]
...: print action_syn, '-- entails -->', action_syn.entailments()
Synset('walk.v.01') -- entails --> [Synset('step.v.01')]
Synset('eat.v.01') -- entails --> [Synset('chew.v.01'),
Synset('swallow.v.01')]
Synset('digest.v.01') -- entails --> [Synset('consume.v.02')]
You can see how related synsets depict the concept of entailment in that output.
Related actions are depicted in entailment, where actions like walking involve or entail
stepping, and eating entails chewing and swallowing.
323
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
The preceding output shows a part of the result obtained for the various homographs
for the term 'bank'. You can see that there are various different meanings associated with
the word 'bank', which is the core intuition behind homographs.
324
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Synonym: rich.a.01
Definition: possessing material wealth
Antonym: poor.a.02
Definition: having little money or few possessions
Synonym: rich.a.02
Definition: having an abundant supply of desirable qualities or substances
(especially natural resources)
Antonym: poor.a.04
Definition: lacking in specific resources, qualities or substances
The preceding outputs show sample synonyms and antonyms for the term 'large'
and the term 'rich'. Additionally, we explore several synsets associated with the term
or concept 'rich', which rightly give us distinct synonyms and their corresponding
antonyms.
325
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
term = 'tree'
synsets = [Link](term)
tree = synsets[0]
# print the entity and its meaning
In [86]: print 'Name:', [Link]()
...: print 'Definition:', [Link]()
Name: tree.n.01
Definition: a tall perennial woody plant having a main trunk and branches
forming a distinct elevated crown; includes both gymnosperms and angiosperms
# print total hyponyms and some sample hyponyms for 'tree'
In [87]: hyponyms = [Link]()
...: print 'Total Hyponyms:', len(hyponyms)
...: print 'Sample Hyponyms'
...: for hyponym in hyponyms[:10]:
...: print [Link](), '-', [Link]()
The preceding output tells us that there are a total of 180 hyponyms for 'tree',
and we see some of the sample hyponyms and their definitions. We can see that each
hyponym is a specific type of tree, as expected. Hyponyms are entities or concepts that act
as the superclass to hyponyms and have a more generic sense or context. The following
snippet shows the immediate superclass hyponym for 'tree':
You can even navigate up the entire entity/concept hierarchy depicting all the
hyponyms or parent classes for 'tree' using the following code snippet:
326
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
From the preceding output, you can see that 'entity' is the most generic concept
in which 'tree' is present, and the complete hypernym hierarchy showing the
corresponding hypernym or superclass at each level is shown. As you navigate further
down, you get into more specific concepts/entities, and if you go in the reverse direction
you will get into more generic concepts/entities.
From the output, we can see that 'forest' is a holonym for 'tree', which is
semantically correct because, of course, a forest is a collection of trees. Meronyms are
semantic relationships that relate a term or entity as a part or constituent of another term
or entity. The following snippet depicts different types of meronyms for 'tree':
327
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
limb.n.02 - any of the main branches arising from the trunk or a bough of a
tree
stump.n.01 - the base part of a tree that remains standing after the tree
has been felled
trunk.n.01 - the main stem of a tree; usually covered with bark; the bole is
usually the part that is commercially useful for lumber
The preceding output shows various meronyms that include various constituents of
trees like stump and trunk and also various derived substances from trees like heartwood
and sapwood.
tree = [Link]('tree.n.01')
lion = [Link]('lion.n.01')
tiger = [Link]('tiger.n.02')
cat = [Link]('cat.n.01')
dog = [Link]('dog.n.01')
# create entities and extract names and definitions
entities = [tree, lion, tiger, cat, dog]
entity_names = [[Link]().split('.')[0] for entity in entities]
entity_definitions = [[Link]() for entity in entities]
328
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
tree - a tall perennial woody plant having a main trunk and branches forming
a distinct elevated crown; includes both gymnosperms and angiosperms
lion - large gregarious predatory feline of Africa and India having a tawny
coat with a shaggy mane in the male
tiger - large feline of forests in most of Asia having a tawny coat with
black stripes; endangered
cat - feline mammal usually having thick soft fur and no ability to roar:
domestic cats; wildcats
dog - a member of the genus Canis (probably descended from the common wolf)
that has been domesticated by man since prehistoric times; occurs in many
breeds
Now that we know our entities a bit better from these definitions explaining them, we
will try to correlate the entities based on common hypernyms. For each pair of entities,
we will try to find the lowest common hypernym in the relationship hierarchy tree.
Correlated entities are expected to have very specific hypernyms, and unrelated entities
should have very abstract or generic hypernyms. The following code snippet illustrates:
common_hypernyms = []
for entity in entities:
# get pairwise lowest common hypernyms
common_hypernyms.append([entity.lowest_common_hypernyms(compared_entity)[0]
.name().split('.')[0]
for compared_entity in entities])
# build pairwise lower common hypernym matrix
common_hypernym_frame = [Link](common_hypernyms,
index=entity_names,
columns=entity_names)
# print the matrix
In [101]: print common_hypernym_frame
...:
tree lion tiger cat dog
tree tree organism organism organism organism
lion organism lion big_cat feline carnivore
tiger organism big_cat tiger feline carnivore
cat organism feline feline cat carnivore
dog organism carnivore carnivore carnivore dog
Ignoring the main diagonal of the matrix, for each pair of entities, we can see their
lowest common hypernym which depicts the nature of relationship between them. Trees are
unrelated to the other animals except that they are all living organisms. Hence we get the
'organism' relationship amongst them. Cats are related to lions and tigers with respect to
being feline creatures, and we can see the same in the preceding output. Tigers and lions are
connected to each other with the 'big cat' relationship. Finally, we can see dogs having the
relationship of 'carnivore' with the other animals since they all typically eat meat.
We can also measure the semantic similarity between these entities using various
semantic concepts. We will use 'path similarity', which returns a value between [0, 1]
based on the shortest path connecting two terms based on their hypernym/hyponym based
taxonomy. The following snippet shows us how to generate this similarity matrix:
329
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
similarities = []
for entity in entities:
# get pairwise similarities
[Link]([round(entity.path_similarity(compared_entity), 2)
for compared_entity in entities])
# build pairwise similarity matrix
similarity_frame = [Link](similarities,
index=entity_names,
columns=entity_names)
# print the matrix
print similarity_frame
From the preceding output, as expected, lion and tiger are the most similar with a
value of 0.33, followed by their semantic similarity with cat having a value of 0.25. And
tree has the lowest semantic similarity values when compared with other animals.
This concludes our discussion on analyzing lexical semantic relations. I encourage
you to try exploring more concepts with different examples by leveraging WordNet.
330
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Sentence: He finally reaped the fruit of his hard work as he won the race
Word synset: Synset('fruit.n.03')
Corresponding definition: the consequence of some effort or action
Sentence: John is the actor who plays the lead in that movie
Word synset: Synset('star.n.04')
Corresponding definition: an actor who plays a principal role
331
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
We try to disambiguate two words, 'fruit' and 'lead' in various text documents
in the preceding examples. You can see how we use the Lesk algorithm to get the correct
word sense for the word we are disambiguating based on its usage and context in each
document. This tells you how fruit can mean both an entity that is consumed as well as
some consequence one faces on applying efforts. We also see how lead can mean the soft
metal, causing something/someone to go somewhere, or even an actor who plays the
main role in a play or movie.
There is some overlap between GPE and LOCATION. The GPE entities are usually more
generic and represent geo-political entities like cities, states, countries, and continents.
LOCATION can also refer to these entities (it varies across different NER systems) along
with very specific locations like a mountain, river, or hill-station. FACILITY on the other
hand refers to popular monuments or artifacts that are usually man-made. The remaining
categories are pretty self-explanatory from their names and the examples depicted in
Figure 7-1.
The Bundesliga is perhaps the most popular top-level professional association
football league in Germany, and FC Bayern Munchen is one of the most popular clubs
in this league with a global presence. We will now take a sample description of this club
332
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
from Wikipedia and try to extract named entities from it. We will reuse our normalization
module (accessible as [Link] in the code files) from the last chapter in
this section to parse the document to remove unnecessary new lines. We will start by
leveraging nltk’s Named Entity Chunker:
# sample document
text = """
Bayern Munich, or FC Bayern, is a German sports club based in Munich,
Bavaria, Germany. It is best known for its professional football team,
which plays in the Bundesliga, the top tier of the German football
league system, and is the most successful club in German football
history, having won a record 26 national titles and 18 national cups.
FC Bayern was founded in 1900 by eleven football players led by Franz John.
Although Bayern won its first national championship in 1932, the club
was not selected for the Bundesliga at its inception in 1963. The club
had its period of greatest success in the middle of the 1970s when,
under the captaincy of Franz Beckenbauer, it won the European Cup three
times in a row (1974-76). Overall, Bayern has reached ten UEFA Champions
League finals, most recently winning their fifth title in 2013 as part
of a continental treble.
"""
import nltk
from normalization import parse_document
import pandas as pd
# tokenize sentences
sentences = parse_document(text)
tokenized_sentences = [nltk.word_tokenize(sentence) for sentence in
sentences]
333
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
The Named Entity Chunker identifies named entities from the preceding text
document, and we extract these named entities from the tagged annotated sentences
and display them in the data frame as shown. You can clearly see how it has correctly
identified PERSON, ORGANIZATION, and GPE related named entities, although a few of them
are incorrectly identified.
We will now use the Stanford NER tagger on the same text and compare the results.
For this, you need to have Java installed and then download the Stanford NER resources
from [Link] Unzip them
to a location of your choice (I used E:/stanford in my system). Once done, you can use
nltk’s interface to access this, similar to what we did in Chapter 3 for constituency and
dependency parsing. For more details on Stanford NER, visit [Link]
software/[Link], the official web site, which also contains the latest version of
their Named Entity Recognizer (I used an older version):
334
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
# tag sentences
ne_annotated_sentences = [[Link](sent) for sent in tokenized_sentences]
# display results
In [118]: print entity_frame
Entity Name Entity Type
0 Franz John PERSON
1 Franz Beckenbauer PERSON
2 Germany LOCATION
3 Bayern ORGANIZATION
4 Bavaria LOCATION
5 Munich LOCATION
6 FC Bayern ORGANIZATION
7 UEFA ORGANIZATION
8 Bayern Munich ORGANIZATION
The preceding output depicts various named entities obtained from our document.
You can compare this with the results obtained from nltk’s NER chunker. The results here
are definitely better—there are no misclassifications and each category is also assigned
correctly. Some really interesting points: It has correctly identified Munich as a LOCATION
and Bayern Munich as an ORGANIZATION. Does this mean the second NER tagger is better?
Not really. It depends on the type of corpus you are analyzing, and you can even build
your own NER tagger using supervised learning by training on pre-tagged corpora similar
to what we did in Chapter 3. In fact, both the taggers just discussed have been trained on
pre-tagged corpora like CoNLL, MUC, and Penn Treebank.
335
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Propositional Logic
We have already discussed propositional logic (PL) as the study of propositions,
statements, and sentences. A proposition is usually declarative, having a binary value
of being either true or false. There also exist various logical operators like conjunction,
disjunction, implication, and equivalence, and we also study the effects of applying these
operators on multiple propositions to understand their behavior and outcome.
Let us consider our example from Chapter 1 with regard to two propositions P and Q
such that they can be represented as follows:
P: He is hungry
Q: He will eat a sandwich
We will now try to build the truth tables for various operations on these propositions
using nltk based on the various logical operators discussed in Chapter 1 (refer to the
“Propositional Logic” section for more details) and derive outcomes computationally:
import nltk
import pandas as pd
import os
336
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
# display results
In [125]: print 'P:', proposition_P
...: print 'Q:', propositon_Q
...: print
...: print 'Expression Outcomes:-'
...: print result_frame
P: He is hungry
Q: He will eat a sandwich
Expression Outcomes:-
P Q (P & Q) (P | Q) (P -> Q) (P <-> Q)
0 False False False False True True
1 False True False True True False
2 True False False True False False
3 True True True True True True
The preceding output depicts the various truth values of the two propositions, and
when we combine them with various logical operators, you will find the results matching
with what we manually evaluated in Chapter 1. For example, P & Q indicates He is hungry
and he will eat a sandwich is True only when both of the individual propositions is True.
We use nltk’s Valuation class to create a dictionary of the propositions and their various
outcome states. We use the Model class to evaluate each expression, where the evaluate()
function internally calls the recursive function satisfy(), which helps in evaluating the
outcome of each expression with the propositions based on the assigned truth values.
337
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
import nltk
import os
# for reading FOL expressions
read_expr = [Link]
# initialize theorem provers (you can choose any)
[Link]['PROVER9'] = r'E:/prover9/bin'
prover = nltk.Prover9()
# I use the following one for our examples
prover = [Link]()
Now that we have our dependencies ready, let us evaluate a few FOL expressions.
Consider a simple expression that If an entity jumps over another entity, the reverse cannot
happen. Assuming the entities to be x and y, we can represent this is FOL as x y
(jumps_over(x, y) → ¬jumps_over(y, x)) which signifies that for all x and y, if x jumps
over y, it implies that y cannot jump over x. Consider now that we have two entities fox
and dog such that the fox jumps over the dog is an event which has taken place and can
be represented by jumps_over(fox, dog). Our end goal or objective is to evaluate the
outcome of jumps_over(dog, fox) considering the preceding expression and the event
that has occurred. The following snippet shows us how we can do this:
338
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Out[132]: False
The preceding output depicts the final result for our goal test_outcome is False, that
is, the dog cannot jump over the fox if the fox has already jumped over the dog based on
our rule expression and the events assigned to the assumptions parameter in the prover
already given. The sequence of steps that lead to the result is also shown in the output.
Let us now consider another FOL expression rule x studies(x, exam) → pass(x,
exam), which tells us that for all instances of x, if x studies for the exam, he/she will pass
the exam. Let us represent this rule and consider two students, John and Pierre, such
that John does not study for the exam and Pierre does. Can we then find out the outcome
whether they will pass the exam based on the given expression rule? The following
snippet shows us how:
# get results
In [134]: [Link](goal=test_outcome1,
...: assumptions=[event1, rule],
...: verbose=True)
[1] {-pass(John,exam)} A
[2] {-studies(John,exam)} A
[3] {-studies(z6,exam), pass(z6,exam)} A
[4] {-studies(John,exam)} (1, 3)
Out[134]: False
In [135]: [Link](goal=test_outcome2,
...: assumptions=[event2, rule],
...: verbose=True)
339
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
[1] {-pass(Pierre,exam)} A
[2] {studies(Pierre,exam)} A
[3] {-studies(z8,exam), pass(z8,exam)} A
[4] {-studies(Pierre,exam)} (1, 3)
[5] {pass(Pierre,exam)} (2, 3)
[6] {} (1, 5)
Out[135]: True
Thus you can see from the above evaluations that Pierre does pass the exam
because he studied for the exam, unlike John who doesn't pass the exam since he did not
study for it.
Let us consider a more complex example with several entities. They perform several
actions as follows:
• There are two dogs rover (r) and alex (a)
• There is one cat garfield (g)
• There is one fox felix (f)
• Two animals, alex (a) and felix (f) run, denoted by function
runs()
• Two animals rover (r) and garfield (g) sleep, denoted by
function sleeps()
• Two animals, felix (f) and alex (a) can jump over the other two,
denoted by function jumps_over()
Taking all these assumptions, the following snippet builds an FOL-based model
with the previously mentioned domain and assignment values based on the entities
and functions. Once we build this model, we evaluate various FOL-based expressions to
determine their outcome and prove some theorems like we did earlier:
340
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
The preceding snippet depicts the evaluation of various expressions based on the
valuation of different symbols based on the rules and domain. We create various FOL-
based expressions and see their outcome based on the predefined assumptions. For
example, the first expression gives us False because rover never runs() and the second
and third expressions are True because they satisfy all the conditions like felix and alex
can jump over rover or garfield and rover is a dog that does not run and garfield is
a cat. The second set of expressions is evaluated based on assigning felix and rover to
specific symbols in our domain (dom), and we pass that variable (g) when evaluating the
expressions. We can even satisfy open formulae or expressions using the satisfiers()
function as shown here:
341
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Sentiment Analysis
We will now discuss several concepts, techniques, and examples with regard to our second
major topic in this chapter, sentiment analysis. Textual data, even though unstructured,
mainly has two broad types of data points: factual based (objective) and opinion based
(subjective). We briefly talked about these two categories at the beginning of this chapter
when I introduced the concept of sentiment analysis and how it works best on text that has
a subjective context. In general, social media, surveys, and feedback data all are heavily
opinionated and express the beliefs, judgement, emotion, and feelings of human beings.
Sentiment analysis, also popularly known as opinion analysis/mining, is defined as the
process of using techniques like NLP, lexical resources, linguistics, and machine learning
(ML) to extract subjective and opinion related information like emotions, attitude, mood,
modality, and so on and try to use these to compute the polarity expressed by a text
document. By polarity, I mean to find out whether the document expresses a positive,
negative, or a neutral sentiment. More advanced analysis involves trying to find out more
complex emotions like sadness, happiness, anger, and sarcasm.
Typically, sentiment analysis for text data can be computed on several levels,
including on an individual sentence level, paragraph level, or the entire document as a
whole. Often sentiment is computed on the document as a whole or some aggregations
are done after computing the sentiment for individual sentences. Polarity analysis usually
involves trying to assign some scores contributing to the positive and negative emotions
expressed in the document and then finally assigning a label to the document based on
the aggregate score. We will depict two major techniques for sentiment analysis here:
• Supervised machine learning
• Unsupervised lexicon-based
The key idea is to learn the various techniques typically used to tackle sentiment
analysis problems so that you can apply them to solve your own problems. We will
see how to re-use the concepts of supervised machine learning based classification
algorithms from Chapter 4 here to classify documents to their associated sentiment. We
will also use lexicons, which are dictionaries or vocabularies specially constructed to
be used for sentiment analysis, and compute sentiment without using any supervised
techniques. We will be carrying out our experiments on a large real-world dataset
pertaining to movie reviews, which will make this task more interesting. We will compare
the performance of the various algorithms and also try to perform some detailed analytics
besides just analyzing polarity, which includes analyzing the subjectivity, mood, and
modality of the movie reviews. Without further delay, let’s get started!
342
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Setting Up Dependencies
There are several utility functions, data, and package dependencies that we need to set
up before we jump into sentiment analysis. We will need our movie review dataset, some
specific packages that we will be using in our implementations, and we will be defining
some utility functions for text normalization, feature extracting, and model evaluation,
similar to what we have used in previous chapters.
Text Normalization
We will be normalizing and standardizing our text data similar to what we did in Chapter
6 as a part of text pre-processing and normalization. For this we will be re-using our
[Link] module from Chapter 6 with a few additions. This mainly includes
343
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
adding an HTML stripper to remove unnecessary HTML characters from text documents,
as shown here:
class MLStripper(HTMLParser):
def __init__(self):
[Link]()
[Link] = []
def handle_data(self, d):
[Link](d)
def get_data(self):
return ' '.join([Link])
def strip_html(text):
html_stripper = MLStripper()
html_stripper.feed(text)
return html_stripper.get_data()
We also add a new function to normalize special accented characters and convert
them into regular ASCII characters so as to standardize the text across all documents. The
following snippet helps us achieve this:
def normalize_accented_characters(text):
text = [Link]('NFKD',
[Link]('utf-8')
).encode('ascii', 'ignore')
return text
The overall text normalization function is depicted in the following snippet and it
re-uses the expand contractions, lemmatization, HTML unescaping, special characters
removal, and stopwords removal functions from the previous chapter's normalization
module:
normalized_corpus = []
for index, text in enumerate(corpus):
text = normalize_accented_characters(text)
text = html_parser.unescape(text)
text = strip_html(text)
text = expand_contractions(text, CONTRACTION_MAP)
if lemmatize:
text = lemmatize_text(text)
else:
344
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
text = [Link]()
text = remove_special_characters(text)
text = remove_stopwords(text)
if only_text_chars:
text = keep_text_characters(text)
if tokenize:
text = tokenize_text(text)
normalized_corpus.append(text)
else:
normalized_corpus.append(text)
return normalized_corpus
To re-use this code, you can make use of the [Link] and contractions.
py files provided with the code files of this chapter.
Feature Extraction
We will be reusing the same feature-extraction function we used in Chapter 6, and it is
available as a part of the [Link] module. The function is shown here for the sake of
completeness:
feature_type = feature_type.lower().strip()
if feature_type == 'binary':
vectorizer = CountVectorizer(binary=True, min_df=min_df,
max_df=max_df, ngram_range=ngram_range)
elif feature_type == 'frequency':
vectorizer = CountVectorizer(binary=False, min_df=min_df,
max_df=max_df, ngram_range=ngram_range)
elif feature_type == 'tfidf':
vectorizer = TfidfVectorizer(min_df=min_df, max_df=max_df,
ngram_range=ngram_range)
else:
raise Exception("Wrong feature type entered. Possible values:
'binary', 'frequency', 'tfidf'")
feature_matrix = vectorizer.fit_transform(documents).astype(float)
return vectorizer, feature_matrix
345
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
You can experiment with various features provided by this function, which include
Bag of Words-based frequencies, occurrences, and TF-IDF based features.
We will also define a function to help us build the confusion matrix for evaluating
the model predictions against the actual sentiment labels for the reviews. The following
function will help us achieve that:
346
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Finally, we will define a function for getting a detailed classification report per
sentiment category (positive and negative) by displaying the precision, recall, F1-score,
and support (number of reviews) for each of the classes:
You will find all the preceding functions in the [Link] module along with the other
code files for this chapter and you can re-use them as needed. Besides this, you need to
make sure you have nltk and pattern installed—which you should already have by this
point of time because we have used them numerous times in our previous chapters.
Preparing Datasets
We will be loading our movie reviews data and preparing two datasets, namely training
and testing, similar to what we did in Chapter 4. We will train our supervised model on
the training data and evaluate model performance on the testing data. For unsupervised
models, we will directly evaluate them on the testing data so as to compare their
performance with the supervised model. Besides that, we will also pick some sample
positive and negative reviews to see how the different models perform on them:
import pandas as pd
import numpy as np
# load movie reviews data
dataset = pd.read_csv(r'E:/aclImdb/movie_reviews.csv')
# print sample data
In [235]: print [Link]()
review sentiment
0 One of the other reviewers has mentioned that ... positive
1 A wonderful little production. <br /><br />The... positive
2 I thought this was a wonderful way to spend ti... positive
347
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
train_reviews = [Link](train_data['review'])
train_sentiments = [Link](train_data['sentiment'])
test_reviews = [Link](test_data['review'])
test_sentiments = [Link](test_data['sentiment'])
We have taken a total of 35,000 reviews out of the 50,000 to be our training dataset
and we will evaluate our models and test them on the remaining 15,000 reviews. This is in
line with a typical 70:30 separation used for training and testing dataset building. We have
also extracted a total of eight reviews from the test dataset and we will be looking closely
at the results for these documents as well as evaluating the model performance on the
complete test dataset in the following sections.
1. Model training
a. Normalize training data
b. Extract features and build feature set and feature
vectorizer
c. Use supervised learning algorithm (SVM) to build a
predictive model
348
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
2. Model testing
a. Normalize testing data
b. Extract features using training feature vectorizer
c. Predict the sentiment for testing reviews using training
model
d. Evaluate model performance
To start, we will be building our training model using the steps in point 1. We will be
using our normalization and feature-extraction modules discussed in previous sections:
# normalization
norm_train_reviews = normalize_corpus(train_reviews, lemmatize=True, only_
text_chars=True)
# feature extraction
vectorizer, train_features = build_feature_matrix(documents=norm_train_
reviews,
feature_type='tfidf',
ngram_range=(1, 1),
min_df=0.0, max_df=1.0)
We will now build our model using the support vector machine (SVM) algorithm which
we used for text classification in Chapter 4. Refer to the “Support Vector Machines” subsection
under the “Classification Algorithms” section in Chapter 4 to refresh your memory:
The preceding snippet trainings the classifier and builds the model that is in the
svm variable, which we can now use for predicting sentiment for new movie reviews (not
used for training) from the test dataset. Let us normalize and extract features from the test
dataset first as mentioned in step 2 in our workflow:
# normalize reviews
norm_test_reviews = normalize_corpus(test_reviews, lemmatize=True, only_
text_chars=True)
# extract features
test_features = [Link](norm_test_reviews)
Now that we have our features for the entire test dataset, before we predict the
sentiment and measure model prediction performance for the entire test dataset, let us
look at some of the predictions for the sample documents we extracted earlier:
349
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Review:-
I hope this group of film-makers never re-unites.
Actual Labeled Sentiment: negative
Predicted Sentiment: negative
Review:-
no comment - stupid movie, acting average or worse... screenplay - no sense
at all... SKIP IT!
Actual Labeled Sentiment: negative
Predicted Sentiment: negative
Review:-
Add this little gem to your list of holiday regulars. It is<br /><br
/>sweet, funny, and endearing
Actual Labeled Sentiment: positive
Predicted Sentiment: positive
Review:-
a mesmerizing film that certainly keeps your attention... Ben Daniels is
fascinating (and courageous) to watch.
Actual Labeled Sentiment: positive
Predicted Sentiment: positive
Review:-
This movie is perfect for all the romantics in the world. John Ritter has
never been better and has the best line in the movie! "Sam" hits close to
home, is lovely to look at and so much fun to play along with. Ben Gazzara
was an excellent cast and easy to fall in love with. I'm sure I've met
Arthur in my travels somewhere. All around, an excellent choice to pick up
any evening.!:-)
350
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Review:-
I don't care if some people voted this movie to be bad. If you want the
Truth this is a Very Good Movie! It has every thing a movie should have. You
really should Get this one.
Actual Labeled Sentiment: positive
Predicted Sentiment: negative
Review:-
Worst horror film ever but funniest film ever rolled in one you have got
to see this film it is so cheap it is unbeliaveble but you have to see it
really!!!! P.s watch the carrot
Actual Labeled Sentiment: positive
Predicted Sentiment: negative
You can look at each review, its actual labeled sentiment, and our predicted sentiment
in the preceding output and see that we have some negative and positive reviews, and our
model is able to correctly identify the sentiment for most of the sampled reviews except
the last two reviews. If you look closely at the last two reviews, some part of the review has
a negative sentiment ("worst horror film", "voted this movie to be bad") but the
general sentiment or opinion of the person who wrote the review was intended positive.
These are the examples I mentioned earlier about the overlap of positive and negative
emotions, which makes it difficult for the model to predict the actual sentiment!
Let us now predict the sentiment for all our test dataset reviews and evaluate our
model performance:
351
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Predicted:
positive negative
Actual: positive 6770 740
negative 912 6578
The preceding outputs show the various performance metrics that depict the
performance of our SVM model with regard to predicting sentiment for movie reviews.
We have an average sentiment prediction accuracy of 89 percent, which is really good if
you compare it with standard baselines for text classification using supervised techniques.
The classification report also shows a per-class detailed report, and we see that our F1-
score (harmonic mean of precision and recall) is 89 percent for both positive and negative
sentiment. The support metric shows the number of reviews having positive (7510)
sentiment and negative (7490) sentiment. The confusion matrix shows how many reviews
for which we predicted the correct sentiment (positive: 6770/7510, negative: 6578/7490)
and the number of reviews for which we predicted the wrong sentiment (positive: 740/7510,
negative: 912/7490). Do try out building more models with different features (Chapter
4 talks about different feature-extraction techniques) and different supervised learning
algorithms. Can you get a better model which predicts sentiment more accurately?
• AFINN lexicon
• Bing Liu’s lexicon
• MPQA subjectivity lexicon
• SentiWordNet
• VADER lexicon
• Pattern lexicon
This is not an exhaustive list of lexicons that can be leveraged for sentiment analysis,
and there are several other lexicons which can be easily obtained from the Internet.
We will briefly discuss each lexicon and will be using the last three lexicons to analyze
the sentiment for our testing dataset in more detail. Although these techniques are
unsupervised, you can also use them to analyze and evaluate the sentiment for the
training dataset too, but for the sake of consistency and to compare model performances
with the supervised model, we will be performing all our analyses on the testing dataset.
AFINN Lexicon
The AFINN lexicon was curated and created by Finn Årup Nielsen, and more details are
mentioned in his paper “A New ANEW: Evaluation of a Word List for Sentiment Analysis
in Microblogs.” The latest version, known as AFINN-111, consists of a total of 2477 words
and phrases with their own scores based on sentiment polarity. The polarity basically
indicates how positive, negative, or neutral the term might be with some numerical
score. You can download it from [Link]/pubdb/views/publication_details.
php?id=6010. It also talks about the lexicon in further details. The author of this lexicon
has also built a Python wrapper over the AFINN lexicon, which you can directly use to
predict the sentiment of text data. The repository is available from GitHub at https://
[Link]/fnielsen/afinn. You can install the afinn library directly and start
analyzing sentiment. This library even has support for emoticons and smileys. Following
is a sample of the AFINN-111 lexicon:
abandon -2
abandoned -2
abandons -2
abducted -2
abduction -2
...
...
youthful 2
yucky -2
yummy 3
zealot -2
zealots -2
zealous 2
353
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
The basic idea is to load the entire list of polar words and phrases in the lexicon
along with their corresponding score (sample shown above) in memory and then find the
same words/phrases and score them accordingly in a text document. Finally, these scores
are aggregated, and the final sentiment and score can be obtained for a text document.
Following is an example snippet based on the official documentation:
Thus you can use the score() function directly to evaluate the sentiment of your text
documents, and from the preceding output you can see that they even give proper weightage
to emoticons, which are used extensively in social media like Twitter and Facebook.
354
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
To understand this data, you can refer to the readme file provided along with the
dataset. Basically, the clues in this dataset were curated and collected manually with
efforts by the above-mentioned maintainers of this project. The various parameters
mentioned above are explained briefly as follows:
• type: This has values that are either strongsubj indicating the
presence of a strongly subjective context or weaksubj which
indicates the presence of a weak/part subjective context.
• len: This points to the number of words in the term of the clue (all
are single words of length 1 for now).
• word1: The actual term present as a token or a stem of the actual
token.
• pos1: The part of speech for the term (clue) and it can be noun,
verb, adj, adverb, or anypos.
• stemmed1: This indicates if the clue (term) is stemmed (y) or not
stemmed (n). If it is stemmed, it can match all its other variants
having the same pos1 tag.
• priorpolarity: This has values of negative, positive, both, or
neutral, and indicates the polarity of the sentiment associated
with this clue (term).
The idea is to load this lexicon into a database or memory (hint: Python dictionary
works well) and then use it similarly to the previous lexicons to analyze the sentiment
associated with any text document.
355
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
SentiWordNet
We know that WordNet is perhaps one of the most popular corpora for the English
language, used extensively in semantic analysis, and it introduces the concept of synsets.
The SentiWordNet lexicon is a lexical resource used for sentiment analysis and opinion
mining. For each synset present in WordNet, the SentiWordNet lexicon assigns three
sentiment scores to it, including a positive polarity score, a negative polarity score,
and an objectivity score. You can find more details on the official web site http://
[Link], which includes research papers explaining the lexicon in
detail and also a link to download the lexicon. The nltk package in Python provides an
interface directly for accessing the SentiWordNet lexicon, and we will be using this to
analyze the sentiment of our movie reviews. The following snippet shows an example
synset and its sentiment scores using SentiWordNet:
import nltk
from [Link] import sentiwordnet as swn
# get synset for 'good'
good = swn.senti_synsets('good', 'n')[0]
# print synset sentiment scores
In [287]: print 'Positive Polarity Score:', good.pos_score()
...: print 'Negative Polarity Score:', good.neg_score()
...: print 'Objective Score:', good.obj_score()
Positive Polarity Score: 0.5
Negative Polarity Score: 0.0
Objective Score: 0.5
Now that we know how to use the sentiwordnet interface, we define a function
that can take in a body of text (movie review in our case) and analyze its sentiment by
leveraging sentiwordnet:
def analyze_sentiment_sentiwordnet_lexicon(review,
verbose=False):
# pre-process text
review = normalize_accented_characters(review)
review = html_parser.unescape(review)
review = strip_html(review)
# tokenize and POS tag text tokens
text_tokens = nltk.word_tokenize(review)
tagged_text = nltk.pos_tag(text_tokens)
pos_score = neg_score = token_count = obj_score = 0
# get wordnet synsets based on POS tags
# get sentiment scores if synsets are found
for word, tag in tagged_text:
ss_set = None
356
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
return final_sentiment
357
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
SENTIMENT STATS:
Predicted Sentiment Objectivity Positive Negative Overall
0 negative 0.83 0.08 0.09 -0.01
------------------------------------------------------------
Review:
I hope this group of film-makers never re-unites.
SENTIMENT STATS:
Predicted Sentiment Objectivity Positive Negative Overall
0 negative 0.71 0.04 0.25 -0.21
------------------------------------------------------------
Review:
no comment - stupid movie, acting average or worse... screenplay - no sense
at all... SKIP IT!
SENTIMENT STATS:
Predicted Sentiment Objectivity Positive Negative Overall
0 negative 0.81 0.04 0.15 -0.11
------------------------------------------------------------
Review:
Add this little gem to your list of holiday regulars. It is<br /><br
/>sweet, funny, and endearing
358
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
SENTIMENT STATS:
Predicted Sentiment Objectivity Positive Negative Overall
0 positive 0.76 0.18 0.06 0.13
------------------------------------------------------------
Review:
a mesmerizing film that certainly keeps your attention... Ben Daniels is
fascinating (and courageous) to watch.
SENTIMENT STATS:
Predicted Sentiment Objectivity Positive Negative Overall
0 positive 0.84 0.14 0.03 0.11
------------------------------------------------------------
Review:
This movie is perfect for all the romantics in the world. John Ritter has
never been better and has the best line in the movie! "Sam" hits close to
home, is lovely to look at and so much fun to play along with. Ben Gazzara
was an excellent cast and easy to fall in love with. I'm sure I've met
Arthur in my travels somewhere. All around, an excellent choice to pick up
any evening.!:-)
SENTIMENT STATS:
Predicted Sentiment Objectivity Positive Negative Overall
0 positive 0.75 0.2 0.05 0.15
------------------------------------------------------------
Review:
I don't care if some people voted this movie to be bad. If you want the
Truth this is a Very Good Movie! It has every thing a movie should have. You
really should Get this one.
SENTIMENT STATS:
Predicted Sentiment Objectivity Positive Negative Overall
0 positive 0.73 0.21 0.06 0.15
------------------------------------------------------------
Review:
Worst horror film ever but funniest film ever rolled in one you have got
to see this film it is so cheap it is unbeliaveble but you have to see it
really!!!! P.s watch the carrot
359
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
SENTIMENT STATS:
Predicted Sentiment Objectivity Positive Negative Overall
0 positive 0.79 0.13 0.08 0.05
------------------------------------------------------------
You can see detailed statistics related to each sentiment score and also the overall
sentiment and compare it with the actual labeled sentiment for each review in the
preceding output. Interestingly, we were able to predict the sentiment correctly for all
our sampled reviews as compared to the supervised learning technique. But how well
does this technique perform for our complete test movie reviews dataset? The following
snippet will give us the answer!
Confusion Matrix:
Predicted:
positive negative
Actual: positive 6941 569
negative 5510 1980
Classification report:
360
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Our model has a sentiment prediction accuracy of around 60% and an F1-score of
70% approximately. If you look at the detailed classification report and the confusion
matrix, you will observe that we correctly classify 6941/7510 positive movie reviews as
positive, but we incorrectly classify 5510/7490 negative movie reviews as positive—which
is quite high! A way to redress this would be to change our logic slightly in our function
and relax the threshold for overall sentiment score to decide whether a document will
have an overall positive or negative sentiment from 0 to maybe 0.1 or higher. Experiment
with this threshold and see what kind of results you get.
VADER Lexicon
VADER stands for Valence Aware Dictionary and sEntiment Reasoner. It is a lexicon
with a rule-based sentiment analysis framework that was specially built for analyzing
sentiment from social media resources. This lexicon was developed by C. J. Hutto and
Eric Gilbert, and you will find further details in the paper, “VADER: A Parsimonious Rule-
based Model for Sentiment Analysis of Social Media Text.” You can read more about it
and even download the dataset or install the library from [Link]
vaderSentiment, which contains all the resources pertaining to the VADER lexicon.
The file vader_sentiment_lexicon.txt contains all the necessary sentiment scores
associated with various terms, including words, emoticons, and even slang language-
based tokens (like lol, wtf, nah, and so on). There are over 9000 lexical features from
which it was further curated to 7500 lexical features in this lexicon with proper validated
valence scores. Each feature was rated on a scale from "[-4] Extremely Negative" to
"[4] Extremely Positive", with allowance for "[0] Neutral (or Neither, N/A)".
This curation was done by keeping all lexical features which had a non-zero mean rating
and whose standard deviation was less than 2.5, which was determined by the aggregate
of ten independent raters. A sample of the VADER lexicon is depicted as follows:
)-:< -2.2 0.4 [-2, -2, -2, -2, -2, -2, -3, -3, -2, -2]
)-:{ -2.1 0.9434 [-1, -3, -2, -1, -2, -2, -3, -4, -1, -2]
): -1.8 0.87178 [-1, -3, -1, -2, -1, -3, -1, -3, -1, -2]
...
...
resolved 0.7 0.78102 [1, 2, 0, 1, 1, 0, 2, 0, 0, 0]
resolvent 0.7 0.78102 [1, 0, 1, 2, 0, -1, 1, 1, 1, 1]
resolvents 0.4 0.66332 [2, 0, 0, 1, 0, 0, 1, 0, 0, 0]
...
...
}:-( -2.1 0.7 [-2, -1, -2, -2, -2, -4, -2, -2, -2, -2]
}:-) 0.3 1.61555 [1, 1, -2, 1, -1, -3, 2, 2, 1, 1]
361
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Each line in the preceding lexicon depicts a unique term, which can be a word
or even an emoticon. The first term indicates the word/emoticon, the second column
indicates the mean or average score, the third column indicates the standard deviation,
and the final column indicates a list of scores given by ten independent scorers. The nltk
package has a nice interface for leveraging the VADER lexicon, and the following function
makes use of the same for analyzing sentiment for any text document:
def analyze_sentiment_vader_lexicon(review,
threshold=0.1,
verbose=False):
# pre-process text
review = normalize_accented_characters(review)
review = html_parser.unescape(review)
review = strip_html(review)
# analyze the sentiment for review
analyzer = SentimentIntensityAnalyzer()
scores = analyzer.polarity_scores(review)
# get aggregate scores and final sentiment
agg_score = scores['compound']
final_sentiment = 'positive' if agg_score >= threshold\
else 'negative'
if verbose:
# display detailed sentiment statistics
positive = str(round(scores['pos'], 2)*100)+'%'
final = round(agg_score, 2)
negative = str(round(scores['neg'], 2)*100)+'%'
neutral = str(round(scores['neu'], 2)*100)+'%'
sentiment_frame = [Link]([[final_sentiment, final, positive,
negative, neutral]],
columns=[Link](levels=[['SENTIMENT STATS:'],
['Predicted Sentiment',
'Polarity Score',
'Positive', 'Negative',
'Neutral']],
labels=[[0,0,0,0,0],[0,1,2,3,4]]))
print sentiment_frame
return final_sentiment
That function helps in computing the sentiment and various statistics associated with
it for any text document (movie reviews in our case). The comments explain the main
sections of the function, which include text-preprocessing, getting the necessary sentiment
scores using the VADER lexicon, aggregating them, and computing the final sentiment
(positive/negative) using a specific threshold we talked about earlier. A threshold of 0.1
362
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
seemed to work best on an average, but you can experiment further with it. The following
snippet shows us how to use this function on our sampled test movie reviews:
Review:
Worst movie, (with the best reviews given it) I've ever seen. Over the top
dialog, acting, and direction. more slasher flick than [Link] all the
great reviews this movie got I'm appalled that it turned out so silly. shame
on you martin scorsese
SENTIMENT STATS:
Predicted Sentiment Polarity Score Positive Negative Neutral
0 negative 0.03 20.0% 18.0% 62.0%
------------------------------------------------------------
Review:
I hope this group of film-makers never re-unites.
SENTIMENT STATS:
Predicted Sentiment Polarity Score Positive Negative Neutral
0 positive 0.44 33.0% 0.0% 67.0%
------------------------------------------------------------
Review:
no comment - stupid movie, acting average or worse... screenplay - no sense
at all... SKIP IT!
SENTIMENT STATS:
Predicted Sentiment Polarity Score Positive Negative Neutral
0 negative -0.8 0.0% 40.0% 60.0%
------------------------------------------------------------
Review:
Add this little gem to your list of holiday regulars. It is<br /><br />sweet,
funny, and endearing
363
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
SENTIMENT STATS:
Predicted Sentiment Polarity Score Positive Negative Neutral
0 positive 0.82 40.0% 0.0% 60.0%
------------------------------------------------------------
Review:
a mesmerizing film that certainly keeps your attention... Ben Daniels is
fascinating (and courageous) to watch.
SENTIMENT STATS:
Predicted Sentiment Polarity Score Positive Negative Neutral
0 positive 0.71 31.0% 0.0% 69.0%
------------------------------------------------------------
Review:
This movie is perfect for all the romantics in the world. John Ritter has
never been better and has the best line in the movie! "Sam" hits close to
home, is lovely to look at and so much fun to play along with. Ben Gazzara
was an excellent cast and easy to fall in love with. I'm sure I've met
Arthur in my travels somewhere. All around, an excellent choice to pick up
any evening.!:-)
SENTIMENT STATS:
Predicted Sentiment Polarity Score Positive Negative Neutral
0 positive 0.99 37.0% 2.0% 61.0%
------------------------------------------------------------
Review:
I don't care if some people voted this movie to be bad. If you want the
Truth this is a Very Good Movie! It has every thing a movie should have. You
really should Get this one.
SENTIMENT STATS:
Predicted Sentiment Polarity Score Positive Negative Neutral
0 negative -0.16 17.0% 14.0% 69.0%
------------------------------------------------------------
Review:
Worst horror film ever but funniest film ever rolled in one you have got
to see this film it is so cheap it is unbeliaveble but you have to see it
really!!!! P.s watch the carrot
364
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
SENTIMENT STATS:
Predicted Sentiment Polarity Score Positive Negative Neutral
0 positive 0.49 11.0% 11.0% 77.0%
------------------------------------------------------------
The preceding statistics are similar to our previous function except the Positive,
Negative, and Neutral columns indicate the percentage or proportion of the document
that is positive, negative, or neutral, and the final score is determined based on the
polarity score and the threshold. The following snippet shows the model sentiment
prediction performance on the entire test movie reviews dataset:
Confusion Matrix:
Predicted:
positive negative
Actual: positive 6434 1076
negative 3410 4080
Classification report:
precision recall f1-score support
365
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
The preceding metrics depict that our model has a sentiment prediction accuracy of
around 70 percent and an F1-score close to 75 percent, which is definitely better than our
previous model. Also notice that we are able to correctly predict positive sentiment for
6434 out of 7510 positive movie reviews, and negative sentiment correctly for 4080 out of
7490 negative movie reviews.
Pattern Lexicon
The pattern package is a complete package for NLP, text analytics, and information
retrieval. We discussed it in detail in previous chapters and have also used it several
times to solve several problems. This package is developed by CLiPS (Computational
Linguistics & Psycholinguistics), a research center associated with the Linguistics
Department of the Faculty of Arts of the University of Antwerp. It has a sentiment module
associated with it, along with modules for analyzing mood and modality of a body of text.
For sentiment analysis, it analyzes any body of text by decomposing it into sentences
and then tokenizing it and tagging the various tokens with necessary parts of speech.
It then uses its own subjectivity-based sentiment lexicon, which you can access from
its official repository at [Link]
text/en/[Link]. It contains scores like polarity, subjectivity, intensity, and
confidence, along with other tags like the part of speech, WordNet identifier, and so
on. It then leverages this lexicon to compute the overall polarity and subjectivity score
associated with a text document. A threshold of 0.1 is recommended by pattern itself to
compute the final sentiment of a document as positive, and anything below it as negative.
You can also analyze the mood and modality of text documents by leveraging the
mood and modality functions provided by the pattern package. The mood function
helps in determining the mood expressed by a particular text document. This function
returns INDICATIVE, IMPERATIVE, CONDITIONAL, or SUBJUNCTIVE for any text based on its
content. The table in Figure 7-2 talks about each type of mood in further detail, courtesy
of the official documentation provided by CLiPS pattern. The column Use talks about
the typical usage patterns for each type of mood, and the examples provide some actual
examples from the English language.
Figure 7-2. Different types of mood and their examples (figure courtesy of CLiPS pattern)
Modality for any text represents the degree of certainty expressed by the text as
a whole. This value is a number that ranges between 0 and 1. Values > 0.5 indicate
factual texts having a high certainty, and < 0.5 indicate wishes and hopes and have a low
366
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
certainty associated with them. We will define a function now to analyze the sentiment
for text documents using the pattern lexicon:
return final_sentiment
367
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
We will now test the function we defined to analyze the sentiment of our sample
test movie reviews and observe the results. We take a threshold of 0.1 as the cut-off to
decide between positive and negative sentiment for a document based on the aggregated
sentiment polarity score, based on several experiments and recommendations from the
official documentation:
Review:
Worst movie, (with the best reviews given it) I've ever seen. Over the top
dialog, acting, and direction. more slasher flick than [Link] all the
great reviews this movie got I'm appalled that it turned out so silly. shame
on you martin scorsese
SENTIMENT STATS:
Predicted Sentiment Polarity Score Subjectivity Score
0 negative 0.06 0.62
DETAILED ASSESSMENT STATS:
Key Terms Polarity Score Subjectivity Score Type
0 [worst] -1.0 1.000 None
1 [best] 1.0 0.300 None
2 [top] 0.5 0.500 None
3 [acting] 0.0 0.000 None
4 [more] 0.5 0.500 None
5 [great] 0.8 0.750 None
6 [appalled] -0.8 1.000 None
7 [silly] -0.5 0.875 None
------------------------------------------------------------
Review:
I hope this group of film-makers never re-unites.
368
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
SENTIMENT STATS:
Predicted Sentiment Polarity Score Subjectivity Score
0 negative 0.0 0.0
Empty DataFrame
Columns: [(DETAILED ASSESSMENT STATS:, Key Terms), (DETAILED ASSESSMENT
STATS:, Polarity Score), (DETAILED ASSESSMENT STATS:, Subjectivity Score),
(DETAILED ASSESSMENT STATS:, Type)]
Index: []
------------------------------------------------------------
Review:
no comment - stupid movie, acting average or worse... screenplay - no sense
at all... SKIP IT!
SENTIMENT STATS:
Predicted Sentiment Polarity Score Subjectivity Score
0 negative -0.36 0.5
DETAILED ASSESSMENT STATS:
Key Terms Polarity Score Subjectivity Score Type
0 [stupid] -0.80 1.0 None
1 [acting] 0.00 0.0 None
2 [average] -0.15 0.4 None
3 [worse, !] -0.50 0.6 None
------------------------------------------------------------
Review:
Add this little gem to your list of holiday regulars. It is<br /><br
/>sweet, funny, and endearing
SENTIMENT STATS:
Predicted Sentiment Polarity Score Subjectivity Score
0 positive 0.19 0.67
DETAILED ASSESSMENT STATS:
Key Terms Polarity Score Subjectivity Score Type
0 [little] -0.1875 0.5 None
1 [funny] 0.2500 1.0 None
2 [endearing] 0.5000 0.5 None
------------------------------------------------------------
Review:
a mesmerizing film that certainly keeps your attention... Ben Daniels is
fascinating (and courageous) to watch.
369
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
SENTIMENT STATS:
Predicted Sentiment Polarity Score Subjectivity Score
0 positive 0.4 0.71
DETAILED ASSESSMENT STATS:
Key Terms Polarity Score Subjectivity Score Type
0 [mesmerizing] 0.300000 0.700000 None
1 [certainly] 0.214286 0.571429 None
2 [fascinating] 0.700000 0.850000 None
------------------------------------------------------------
Review:
This movie is perfect for all the romantics in the world. John Ritter has
never been better and has the best line in the movie! "Sam" hits close to
home, is lovely to look at and so much fun to play along with. Ben Gazzara
was an excellent cast and easy to fall in love with. I'm sure I've met
Arthur in my travels somewhere. All around, an excellent choice to pick up
any evening.!:-)
SENTIMENT STATS:
Predicted Sentiment Polarity Score Subjectivity Score
0 positive 0.66 0.73
DETAILED ASSESSMENT STATS:
Key Terms Polarity Score Subjectivity Score Type
0 [perfect] 1.000000 1.000000 None
1 [better] 0.500000 0.500000 None
2 [best, !] 1.000000 0.300000 None
3 [lovely] 0.500000 0.750000 None
4 [much, fun] 0.300000 0.200000 None
5 [excellent] 1.000000 1.000000 None
6 [easy] 0.433333 0.833333 None
7 [love] 0.500000 0.600000 None
8 [sure] 0.500000 0.888889 None
9 [excellent, !] 1.000000 1.000000 None
10 [:-)] 0.500000 1.000000 mood
------------------------------------------------------------
Review:
I don't care if some people voted this movie to be bad. If you want the
Truth this is a Very Good Movie! It has every thing a movie should have.
You really should Get this one.
370
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
SENTIMENT STATS:
Predicted Sentiment Polarity Score Subjectivity Score
0 positive 0.17 0.55
DETAILED ASSESSMENT STATS:
Key Terms Polarity Score Subjectivity Score Type
0 [bad] -0.7 0.666667 None
1 [very, good, !] 1.0 0.780000 None
2 [really] 0.2 0.200000 None
------------------------------------------------------------
Review:
Worst horror film ever but funniest film ever rolled in one you have got
to see this film it is so cheap it is unbeliaveble but you have to see it
really!!!! P.s watch the carrot
SENTIMENT STATS:
Predicted Sentiment Polarity Score Subjectivity Score
0 negative -0.04 0.63
DETAILED ASSESSMENT STATS:
Key Terms Polarity Score Subjectivity Score Type
0 [worst] -1.000000 1.0 None
1 [cheap] 0.400000 0.7 None
2 [really, !, !, !, !] 0.488281 0.2 None
------------------------------------------------------------
The preceding analysis shows the sentiment, polarity, and subjectivity scores for
each sampled review. Besides this, we also see key terms and emotions and their polarity
scores, which mainly contributed to the overall sentiment of each review. You can see
that even exclamations and emoticons are also given importance and weightage when
computing sentiment and polarity. The following snippet depicts the mood and modality
for the sampled test movie reviews:
371
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Review:
Worst movie, (with the best reviews given it) I've ever seen. Over the top
dialog, acting, and direction. more slasher flick than [Link] all the
great reviews this movie got I'm appalled that it turned out so silly. shame
on you martin scorsese
Labeled Sentiment: negative
Mood: indicative
Modality Score: 0.75
Certainty: Strong
------------------------------------------------------------
Review:
I hope this group of film-makers never re-unites.
Labeled Sentiment: negative
Mood: subjunctive
Modality Score: -0.25
Certainty: Low
------------------------------------------------------------
Review:
no comment - stupid movie, acting average or worse... screenplay - no sense
at all... SKIP IT!
Labeled Sentiment: negative
Mood: indicative
Modality Score: 0.75
Certainty: Strong
------------------------------------------------------------
Review:
Add this little gem to your list of holiday regulars. It is<br /><br
/>sweet, funny, and endearing
Labeled Sentiment: positive
Mood: imperative
Modality Score: 1.0
Certainty: Strong
------------------------------------------------------------
Review:
a mesmerizing film that certainly keeps your attention... Ben Daniels is
fascinating (and courageous) to watch.
Labeled Sentiment: positive
Mood: indicative
Modality Score: 0.75
Certainty: Strong
------------------------------------------------------------
Review:
This movie is perfect for all the romantics in the world. John Ritter has
never been better and has the best line in the movie! "Sam" hits close to
home, is lovely to look at and so much fun to play along with. Ben Gazzara
was an excellent cast and easy to fall in love with. I'm sure I've met
Arthur in my travels somewhere. All around, an excellent choice to pick up
any evening.!:-)
372
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
The preceding output depicts the mood, modality score, and the certainty factor
expressed by each review. It is interesting to see phrases like "Add this little gem…"
are correctly associated with the right mood, which is an imperative, and "I hope
this…" is correctly associated with subjunctive mood. The other reviews have more of an
indicative disposition, which is quite obvious since it expresses the beliefs of the review
who wrote the movie review. Certainty is lower in cases of reviews that use words like
"hope", "if", and higher in case of strongly opinionated reviews.
Finally, we will evaluate the sentiment prediction performance of this model on our
entire test review dataset as we have done before for our other models. The following
snippet achieves the same:
373
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Confusion Matrix:
Predicted:
positive negative
Actual: positive 5958 1552
negative 1924 5566
Classification report:
precision recall f1-score support
This model gives a better and more balanced performance toward predicting the
sentiment of both positive and negative classes. We have an average sentiment prediction
accuracy of 77 percent and an average F1-score of 77 percent for this model. Although
the number of correct positive predictions has dropped from our previous model to
5958/7510 reviews, the number of correct predictions for negative reviews has increased
significantly to 5566/7490 reviews.
374
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
From the visualization and the table in Figure 7-3, it is clear that the supervised
model using SVM gives us the best results, which are expected because it was trained on
35,000 training movie reviews. Pattern lexicon performs the best among the unsupervised
techniques for our test movie reviews. Does this mean these models will always perform
the best? Absolutely not. It depends on the data you are analyzing. Remember to consider
various models and also to evaluate all the metrics when evaluating any model, and not
just one or two. Some of the models in the chart have really high recall but low precision,
which indicates these models have a tendency to make more wrong predictions or
false positives. You can re-use these benchmarks and evaluate more sentiment analysis
models as you experiment with different features, lexicons, and techniques.
375
CHAPTER 7 ■ SEMANTIC AND SENTIMENT ANALYSIS
Summary
In this final chapter, we have covered a variety of topics focused on semantic and
sentiment analysis of textual data. We revisited several of our concepts from Chapter
1 with regard to language semantics. We looked at the WordNet corpus in detail and
explored the concept of synsets with practical examples. We also analyzed various lexical
semantic relations from Chapter 1 here, using synsets and real-world examples. We
looked at relationships including entailments, homonyms and homographs, synonyms
and antonyms, hyponyms and hypernyms, and holonyms and meronyms. Semantic
relations and similarity computation techniques were also discussed in detail, with
examples that leveraged common hypernyms among various synsets. Some popular
techniques widely used in semantic and information extraction were discussed, including
word sense disambiguation and named entity recognition, with examples. Besides
semantic relations, we also revisited concepts related to semantic representations,
namely propositional logic and first order logic. We leveraged the use of theorem provers
and evaluated actual propositions and logical expressions computationally.
Next, we introduced the concept of sentiment analysis and opinion mining and saw
how it is used in various domains like social media, surveys, and feedback data. We took
a practical example of analyzing sentiment on actual movie reviews from IMDb and built
several models that included supervised machine learning and unsupervised lexicon-
based models. We looked at each technique and its results in detail and compared the
performance across all our models.
This brings us to the end of this book. I hope the various concepts and techniques
discussed here will be helpful to and that you can use the knowledge and techniques
from this book when you tackle challenging problems in the world of text analytics and
natural language processing. You may have seen by now that there is a lot of unexplored
territory out there in the world of analyzing unstructured text data. I wish you the very
best and would like to leave you with the parting thought from Occam’s razor: Sometimes
the simplest solution is the best solution.
376