Sentiment Analysis with Naive Bayes Classifier

Classification is a fundamental aspect of both human and machine intelligence. In this blog, we'll explore a specific text classification problem: sentiment analysis. The most common approach to text classification in natural language processing involves supervised machine learning. Formally, the task of supervised classification is to take an input x and a fixed set of output classes Y={y1,y2,,ym}, and return a predicted class yY. In the context of text classification, we often refer to the output variable as c (for "class") and the input variable as d (for "document"). In a supervised setting, we have a training set of N documents, each labeled with a class: {(d1,c1),,(dN,cN)}. The objective is to train a classifier that can accurately map a new document d to its correct class c, where C represents a set of relevant document classes. A probabilistic classifier not only predicts the class but also provides the probability of the document belonging to each class. This full probability distribution can be valuable for making more informed downstream decisions, as it allows us to delay making discrete decisions, which can be beneficial when integrating multiple systems.

Many kinds of machine learning algorithms are used to build classifiers. This blog we are going to use Naive Bayes it belongs to a family of Generative classifiers

It is called Naive Bayes beacuse it is a bayesian classifier that makes a naive assumption that the features are independent of each other

For this blog we are going to use Twitter sentiment analysis dataset from Kaggle

Before implementing the Naive Bayes classifier, let's first understand the theory behind it.

Naive Bayes is a probabilistic classifier, meaning that for a document d, out of all classes cC the classifier returns the class c^ which has the maximum posterior probability, posterior probability is the probability of the class given the document P(cd), which can be calculated using Bayes' theorem:

P(cd)=P(dc)P(c)P(d)

where:
P(cd) : Posterior probability of class c given document d
P(dc) : Likelihood of document d given class c
P(c) : Prior probability of class c
P(d) : Probability of document d (usually it is called evidence or marginal likelihood)

The denominator P(d) is a constant for all classes, so we can ignore it while calculating the maximum posterior probability. The class c^ that maximizes the posterior probability can be calculated as:

c^=argmaxcCP(cd)

Substituting the Bayes' theorem in the above equation, we get:

c^=argmaxcCP(cd)=argmaxcCP(dc)P(c)P(d)

the denominator P(d) is a constant for all classes, we are always asking for most likely class c given document d so we can ignore it, so the equation becomes:

c^=argmaxcCP(dc)P(c)

The likelihood P(dc) is the probability of observing document d given class c. In the context of text classification, we can calculate it as the product of the probabilities of observing each word in the document given the class. This is where the "naive" assumption comes into play: we assume that the features (words) are conditionally independent given the class. This assumption simplifies the calculation of the likelihood, as we can calculate the probability of observing each word independently and multiply them together. The likelihood can be calculated as:

P(dc)=P(w1,w2,,wnc)=i=1nP(wic)

where:
P(wic) : Probability of observing word wi given class c

The prior P(c) is the probability of class c occurring in the training set. It can be calculated as the fraction of documents in the training set that belong to class c:

P(c)=NcN

where:
Nc : Number of documents in the training set that belong to class c
N : Total number of documents in the training set

Now that we have the likelihood and the prior, we can calculate the posterior probability of each class for a given document. The class with the maximum posterior probability is the predicted class for the document.

To apply the naive Bayes classifier to text, we will use each word in the documents as a feature, as suggested above, and we consider each of the words in the document by walking an index through every word position in the document

cNB=argmaxcCP(c)i=1nP(wic)

where:
$ i \in \text{positions}:istheindexofthewordinthedocumentc_{NB}$ : Predicted class for the document using the naive Bayes classifier
P(c) : Prior probability of class c
P(wic) : Probability of observing word wi given class c

To calculate the probability of P(wic), we’ll assume a feature is just the existence of a word in the document’s bag of words, and so we’ll want P(wic), which we compute as the fraction of times the word wi appears among all words in all documents of topic c. we first concatenate all the documents of class c into one big document, and then we calculate the frequency of each word in the big document to give maximum likelihood estimate of the probability. The probability of observing word wi given class c can be calculated as:

P(wic)=count(wi,c)wVcount(w,c)

where:
count(wi,c) : Number of times word wi appears in documents of class c
wVcount(w,c) : Total number of words in documents of class c

But the above equation can be problematic when we encounter a word that is not present in the training set.
For example, Imagine we are trying to classify a document that contains the word "apple", but the word "apple" was not present in the training set. In this case, the probability P(wic) will be zero, which will make the entire likelihood zero. To avoid this problem, we can use Laplace smoothing, which adds a small constant α to the numerator and denominator of the probability calculation:

P(wic)=count(wi,c)+αwVcount(w,c)+αV

where:
α : Smoothing parameter
$ \mid V \mid $ : Size of the vocabulary (total number of unique words in the training set)

Let's walkthrough a simple example to understand how the Naive Bayes classifier works. Consider a training set with two classes: positive and negative. The training set contains the following documents:

Document Class
I love this movie positive
Just plain boring negative
most fun movie ever positive
no fun at all negative

vocabulary for positive class: {I, love, this, movie, most, fun, ever}
vocabulary for negative class: {just, plain, boring, no, fun, at, all}
complete vocabulary:

Given the above training set, we want to classify the document "I had fun". To do this, we first calculate the prior probabilities of each class:

P(positive)=Number of positive documentsTotal number of documents=24=0.5

P(negative)=Number of negative documentsTotal number of documents=24=0.5

Next, we calculate the likelihood of observing the document "I had fun" given each class. We calculate the likelihood for each word in the document and multiply them together:

For the positive class:

P(Ipositive)=I in positive documents+αTotal words in positive documents+αVocabulary=1+17+112=219

P(hadpositive)=had in positive documents+αTotal words in positive documents+αVocabulary=0+17+112=119

P(funpositive)=fun in positive documents+αTotal words in positive documents+αVocabulary=2+17+112=319

For the negative class:

P(Inegative)=I in negative documents+αTotal words in negative documents+αVocabulary=0+15+112=117

P(hadnegative)=had in negative documents+αTotal words in negative documents+αVocabulary=0+15+112=117

P(funnegative)=fun in negative documents+αTotal words in negative documents+αVocabulary=1+15+112=217

Finally, we calculate the posterior probability of each class for the document "I had fun" and predict the class with the maximum posterior probability:

P(positiveIhadfun)=P(I+)×P(had+)×P(fun+)×P(positive)=219×119×319×0.5=0.0006

P(negativeIhadfun)=P(I)×P(had)×P(fun)×P(negative)=117×117×217×0.5=0.0003

Since P(positiveIhadfun)>P(negativeIhadfun), we predict the document "I had fun" as belonging to the positive class.

Now that we have a good understanding of the theory behind the Naive Bayes classifier, let's implement it in Python using the Twitter sentiment analysis dataset.

First we need to load the dataset and preprocess it. We will also split the dataset into training and testing sets.

data = pd.read_csv('twitter_training.csv',header=None,usecols=[2,3],names=['sentiment','text'])
data = data.sample(frac=0.4).reset_index(drop=True)
data.dropna(inplace=True)

Next, we need to preprocess the text data by tokenizing the text and removing stopwords. and then we will convert the text data into a matrix of token counts using the CountVectorizer class from scikit-learn.(It is an implementation of the bag of words model)

def tokenize(text):
    return [word for word in word_tokenize(text) if word.isalpha() and word not in stop_words]

vectorizer = CountVectorizer(tokenizer=tokenize)
X = vectorizer.fit_transform(data.text).toarray()
y = (data.sentiment == 'Positive').astype(int)

Next, we will split the data into training and testing sets using the train_test_split function from scikit-learn.

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

Now we can train the Naive Bayes classifier using the training data. We will use the MultinomialNB class from scikit-learn to train the classifier.

nb = MultinomialNB()
nb.fit(X_train, y_train)

Finally, we can evaluate the performance of the classifier on the testing data using the score method.

nb.score(X_test, y_test)

The score method returns the mean accuracy of the classifier on the testing data. In this case, the accuracy represents the proportion of correctly classified documents in the testing set.

Although Accuracy is a good metric to evaluate the performance of the classifier, it is not always the best metric, especially when the classes are imbalanced. Other metrics such as precision, recall, and F1 score can provide more insights into the performance of the classifier.

Precision is the proportion of true positive predictions among all positive predictions:

Precision=True PositivesTrue Positives+False Positives

Recall is the proportion of true positive predictions among all actual positive instances:

Recall=True PositivesTrue Positives+False Negatives

The F1 score is the harmonic mean of precision and recall:

F1 Score=2×Precision×RecallPrecision+Recall

We can calculate these metrics using the precision_score, recall_score, and f1_score functions from scikit-learn.

print(f'Accuracy: {accuracy_score(y_test, nb.predict(X_test)):0.2f}')
print(f'F1: {f1_score(y_test, nb.predict(X_test)):0.2f}')
print(f'Precision: {precision_score(y_test, nb.predict(X_test)):0.2f}')
print(f'Recall: {recall_score(y_test, nb.predict(X_test)):0.2f}')

we get the following output:

Accuracy: 0.85
F1: 0.84
Precision: 0.84
Recall: 0.83

Before concluding, I want to highlight one more important aspect of Naive Bayes: it is a generative classifier. This means that it models the joint probability distribution of the features and the class, allowing it to generate new samples from the learned distribution. This capability can be particularly useful in scenarios where we need to create new samples that resemble the training data.

let's generate a new sample using the learned naive bayes model.

import numpy as np
generated_class = 1
generated_features = np.random.multinomial(n=1, pvals=np.exp(nb.feature_log_prob_[generated_class]))

generated_data_point = vectorizer.inverse_transform(generated_features.reshape(1, -1))

print("Generated Data Point:", generated_data_point[0][0])

output:

Generated Data Point: interesting

In this blog, we explored the theory behind the Naive Bayes classifier and implemented it in Python using the Twitter sentiment analysis dataset. We also evaluated the performance of the classifier using accuracy, precision, recall, and F1 score. Finally, we generated a new sample using the learned Naive Bayes model.