<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[The AI Engineer’s Lab – by Tarneem Alaa]]></title><description><![CDATA[The AI Engineer’s Lab – by Tarneem Alaa]]></description><link>https://tarneem.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 22 Sep 2026 11:07:58 GMT</lastBuildDate><atom:link href="https://tarneem.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building an End-to-End Sentiment Classifier: Classical ML vs. BERT on IMDb Review]]></title><description><![CDATA[Introduction
In Natural Language Processing (NLP), one of the most classic problems is Sentiment Analysis, where the goal is to determine the emotion behind a text. In this project, I address this problem by using the IMDb Movie Reviews dataset aimin...]]></description><link>https://tarneem.hashnode.dev/building-an-end-to-end-sentiment-classifier-classical-ml-vs-bert-on-imdb-review</link><guid isPermaLink="true">https://tarneem.hashnode.dev/building-an-end-to-end-sentiment-classifier-classical-ml-vs-bert-on-imdb-review</guid><category><![CDATA[nlp]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[BERT]]></category><category><![CDATA[Sentiment analysis]]></category><category><![CDATA[Deep Learning]]></category><category><![CDATA[Python]]></category><category><![CDATA[transformers]]></category><category><![CDATA[gradio]]></category><category><![CDATA[huggingface]]></category><category><![CDATA[scikit learn]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Tarneem Alaa Abdelreheem]]></dc:creator><pubDate>Sun, 03 Aug 2025 05:20:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754197766495/847240b5-2fca-422b-9c07-41dc880ff5fe.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>In Natural Language Processing (NLP), one of the most classic problems is Sentiment Analysis, where the goal is to determine the emotion behind a text. In this project, I address this problem by using the IMDb Movie Reviews dataset aiming to classify whether a given review has a <em>Positive</em> or <em>Negative</em> sentiment.</p>
<p>I approach this task by analyzing and comparing different strategies which are:</p>
<ol>
<li><p>Classical Machine Learning Models: Logistic Regression, Support Vector Machine (SVM), Naive Bayes, Random Forest. All using TF-IDF Vectorization.</p>
</li>
<li><p>Modern Deep Learning with BERT: a transformer-based model fine-tuned on the IMDb Dataset.</p>
</li>
</ol>
<p>This project demonstrates the whole process, starting from data preprocessing, moving to training and evaluating multiple classical ML models, to fine-tuning BERT and finally deploying the models. The objective is to create a strong end-to-end sentiment classifier and compare how the different approaches approaches perform on real examples.</p>
<h2 id="heading-dataset-amp-preprocessing">Dataset &amp; Preprocessing</h2>
<h3 id="heading-dataset">Dataset</h3>
<p>The dataset used in this project is <a target="_blank" href="https://www.kaggle.com/datasets/lakshmi25npathi/imdb-dataset-of-50k-movie-reviews">IMDB Dataset of 50K Movie Reviews</a> by <strong>Lakshmipathi N</strong> on Kaggle. It consists of 50,000 movie reviews labeled as either positive or negative, with an equal distribution of 25,000 reviews per class, as shown in the following plot.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753835270165/8c5fb8ed-bf2d-4aec-a2cb-755a2fa13caf.png" alt="Sentiment Class Distribution Plot" class="image--center mx-auto" /></p>
<p>In addition to analyzing class distribution, I also explored the <strong>length of the reviews</strong> (in number of characters).<br />As shown in the plot below, most reviews are under 2000 characters, with the peak around 750 characters.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753836455178/601c9ed2-fe1f-4d4f-a5fa-a6c4cc81c8ec.png" alt class="image--center mx-auto" /></p>
<p>As a way to visualize the tokens and data included, I generated a wordcloud for each of the classes, positive and negative, as shown in the following plot:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754168898477/a1a3cdb5-5a3a-42d8-84ef-d29030d0188b.png" alt class="image--center mx-auto" /></p>
<p>For further analysis and a way to make insights about the tokens and the movie reviews, we approached that by checking the most frequent words found in the reviews. The most frequent 5 words were: movie, film, one, like and good. Here is a plot that shows the most frequent 20 words:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754169320863/e0e6fcbf-4d43-4bd8-ac3b-7c09405b251b.png" alt class="image--center mx-auto" /></p>
<p>We divided the data to be 70% training, 15% validation and 15% test data, as shown in the following code snippet:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Split into train set with 70% and temporary set with 30% to be further split to test and validation sets</span>
X_train, X_temp, y_train, y_temp = train_test_split(
    X, y, test_size=<span class="hljs-number">0.3</span>, stratify=y, random_state=<span class="hljs-number">42</span>
)

<span class="hljs-comment"># Split temp into validation set with 15% and test set with 15%</span>
X_val, X_test, y_val, y_test = train_test_split(
    X_temp, y_temp, test_size=<span class="hljs-number">0.5</span>, stratify=y_temp, random_state=<span class="hljs-number">42</span>
)
</code></pre>
<h3 id="heading-preprocessing">Preprocessing</h3>
<p>Before going into any of the models, I preprocessed the data by performing a sequence of steps to prepare the IMDb reviews text for analysis and models.</p>
<p>The preprocessing steps I did are:</p>
<ol>
<li><p><strong>HTML Tag Removal</strong><br /> Due to noticing that some reviews contain HTML formatting like <em>&lt;br&gt;,</em> I used BeautifulSoup to remove these tags and just keep the clean text content.</p>
</li>
<li><p><strong>Remove Non-letter Characters</strong><br /> I removed all non-alphanumeric such as digits and symbols to reduce noise in the text.</p>
</li>
<li><p><strong>Lowercase</strong><br /> All text was converted to lowercase to ensure consistency and and avoid treating the same word differently based on case.</p>
</li>
<li><p><strong>Tokenization</strong><br /> Splitting the text to tokens, such that each review is split into individual words for further processing.</p>
</li>
<li><p><strong>Stopword Removal</strong><br /> I removed common English stopwords such as "and", "the" and "was" using NLTK's built-in stopword list, as they often don’t add much to the semantic analysis.</p>
</li>
</ol>
<p>Here is the function <em>clean_reviews_text</em> that I used to apply these steps:</p>
<pre><code class="lang-python">stop_words = set(stopwords.words(<span class="hljs-string">'english'</span>))

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">clean_reviews_text</span>(<span class="hljs-params">text</span>):</span>
    <span class="hljs-comment"># Remove HTML formatting</span>
    text = BeautifulSoup(text, <span class="hljs-string">"html.parser"</span>).get_text()

    <span class="hljs-comment"># Remove non-letter characters</span>
    text = re.sub(<span class="hljs-string">r'[^a-zA-Z]'</span>, <span class="hljs-string">' '</span>, text)

    <span class="hljs-comment"># lowercase</span>
    text = text.lower()

    <span class="hljs-comment"># Split to tokens</span>
    tokens = text.split()

    <span class="hljs-comment"># Remove stopwords</span>
    tokens = [word <span class="hljs-keyword">for</span> word <span class="hljs-keyword">in</span> tokens <span class="hljs-keyword">if</span> word <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> stop_words]

    <span class="hljs-keyword">return</span> <span class="hljs-string">' '</span>.join(tokens)
</code></pre>
<p>Here is an example of a movie review before and after cleaning:</p>
<p>*Before: “*Being a huge street fighter fan and thoroughly enjoying the previous film, Street Fighter II: The Animated Movie, I was really looking forward to this one!&lt;br /&gt;&lt;br /&gt;However, it seemed that the film had no real sense of direction or purpose. Most of the characters I could not associate with and it just lacked the intense action that made the other mentioned street fighter film so superior.&lt;br /&gt;&lt;br /&gt;There are some good points however, the Animation is superb!!!”</p>
<p>*After: “*huge street fighter fan thoroughly enjoying previous film street fighter ii animated movie really looking forward one however seemed film real sense direction purpose characters could associate lacked intense action made mentioned street fighter film superior good points however animation superb”</p>
<p>After applying the cleaning, the vocabulary size decreased significantly by 76.92% from 438729 To 101246 words, which is demonstrated in the following plot:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754176620938/ee59d605-37f6-4957-ba3c-3040dc771eea.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-encoding">Encoding</h3>
<p>I encoded the labels such that “Negative” is mapped to 0 and “Positive” is mapped to 1, to make the data ready for ML models training.</p>
<pre><code class="lang-python">imdb_data_df[<span class="hljs-string">'label'</span>] = imdb_data_df[<span class="hljs-string">'sentiment'</span>].map({<span class="hljs-string">'positive'</span>:<span class="hljs-number">1</span>, <span class="hljs-string">'negative'</span>:<span class="hljs-number">0</span>})
</code></pre>
<hr />
<h2 id="heading-classical-machine-learning-models-with-tf-idf">Classical Machine Learning Models with TF-IDF:</h2>
<p>I started with classical machine learning models to create a baseline for sentiment classification and to compare between the different ones.</p>
<p>Since these models can’t work directly with raw text, I first transformed the text into numerical features using vectorization. I experimented with <strong>CountVectorizer</strong> and <strong>TF-IDF</strong> using <strong>Logistic Regression</strong> as a test model to compare the performance between them both.</p>
<h3 id="heading-countvectorizer">CountVectorizer</h3>
<p><strong>CountVectorizer</strong> is a simple vectorization method that converts text to a matrix of token (word) counts by firstly building a vocabulary of all unique words, then for each it counts the number of times each word appears treating each word equally. For example:</p>
<ul>
<li><p>Review 1: “I love this movie”</p>
</li>
<li><p>Review 2: “I hated this movie”</p>
</li>
</ul>
<p>Vocabulary: [ “I”, “love”, “this”, “movie”, “hated”]</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td>I</td><td>love</td><td>this</td><td>movie</td><td>hated</td></tr>
</thead>
<tbody>
<tr>
<td>Review 1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>0</td></tr>
<tr>
<td>Review 2</td><td>1</td><td>0</td><td>1</td><td>1</td><td>1</td></tr>
</tbody>
</table>
</div><p>This is the code for applying the CountVectorizer to our data, by getting the vocabulary and calculating the matrix on the training data using <em>.fit_transform()</em> then applying the same vocabulary on the validation and test sets using <em>.transform().</em></p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sklearn.feature_extraction.text <span class="hljs-keyword">import</span> CountVectorizer
<span class="hljs-comment"># Initialize CountVectorizer</span>
count_vectorizer = CountVectorizer(max_features=<span class="hljs-number">10000</span>, ngram_range=(<span class="hljs-number">1</span>,<span class="hljs-number">2</span>))

<span class="hljs-comment"># We apply to train data to learn vocabulary &amp; computes CountVectorizer values for train set</span>
X_train_count = count_vectorizer.fit_transform(X_train)

<span class="hljs-comment"># We apply same vocabulary &amp; weights to the val set</span>
X_val_count = count_vectorizer.transform(X_val)

<span class="hljs-comment"># We apply same vocabulary &amp; weights to the test set</span>
X_test_count = count_vectorizer.transform(X_test)
</code></pre>
<p>For the CountVectorizer, we visualized it by getting the top 30 terms by frequency, as shown in the following plot:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754170466734/691979b8-b7a5-4636-ba5a-84286c51da36.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-tf-idf-term-frequency-inverse-document-frequency">TF-IDF (Term Frequency Inverse Document Frequency)</h3>
<p><strong>TF-IDF</strong> is an another vectorization method. Like CountVectorizer, it also counts the number of occurrences of a term (word) “Term Frequency”, in addition to calculating how rare the word is across all text “Inverse Document Frequency”. This gives a weighted score of how important a word is in a text relative to the entire vocabulary.</p>
<p>$$TF-IDF = TF(Word) * IDF(Word)$$</p><p>That makes it very useful as it reduces the weight of common words, and highlights the words that might be important and informative.</p>
<p>Similarly to what we did in the CountVectorizer, we applied TF-IDF on the train set to learn vocabulary and compute TF-IDF values using <em>fit_transform()</em> then for the validation and test sets we use <em>transform().</em></p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sklearn.feature_extraction.text <span class="hljs-keyword">import</span> TfidfVectorizer
<span class="hljs-comment"># Initializing the TF-IDF Vectorizer</span>
tfidf = TfidfVectorizer(max_features=<span class="hljs-number">10000</span>, ngram_range=(<span class="hljs-number">1</span>,<span class="hljs-number">2</span>))

<span class="hljs-comment"># We apply to train data to learn vocabulary &amp; computes TF-IDF values for train set</span>
X_train_tfidf = tfidf.fit_transform(X_train)

<span class="hljs-comment"># We apply same vocabulary &amp; weights to the val set</span>
X_val_tfidf = tfidf.transform(X_val)

<span class="hljs-comment"># We apply same vocabulary &amp; weights to the test set</span>
X_test_tfidf = tfidf.transform(X_test)
</code></pre>
<p>To visualize the TF-IDF vectorizer, I calculated the average TF-IDF across all terms, then showed the top 30 as shown in the following plot:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754175439294/cebb04bf-a22f-4269-a8c4-1bd3efd580f0.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-logistic-regression">Logistic Regression</h3>
<p>The first classical machine learning model I used is <strong>Logistic Regression</strong>. It is a supervised machine learning algorithm which is commonly used with binary classification tasks, which makes it very efficient for our sentiment analysis task of classifying reviews to <em>Positive</em> or <em>Negative.</em></p>
<p>Logistic Regression uses <strong>sigmoid function</strong> to convert inputs into a probability value between 0 and 1. If the output is closer to 0, the review is predicted as <em>negative</em>; if closer to 1, it's predicted as <em>positive</em> (as we previously encoded).</p>
<p>Here is how I initialized my logistic regression model for training:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sklearn.linear_model <span class="hljs-keyword">import</span> LogisticRegression

<span class="hljs-comment"># Initializing the Logistic Regression Model</span>
lr = LogisticRegression(
    max_iter=<span class="hljs-number">1000</span>,
    random_state=<span class="hljs-number">42</span>
)
</code></pre>
<p>Then, I trained the model using both CountVectorizer and TF-IDF to compare their performance. In the table below, we see the results:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Metric</strong></td><td><strong>LR with CountVectorizer</strong></td><td><strong>LR with TF-IDF</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Training Accuracy</strong></td><td>92.53%</td><td><strong><mark>98.55%</mark></strong></td></tr>
<tr>
<td><strong>Validation Accuracy</strong></td><td><strong><mark>89.04%</mark></strong></td><td>86.87%</td></tr>
<tr>
<td><strong>Testing Accuracy</strong></td><td><strong><mark>90.21%</mark></strong></td><td>88.05%</td></tr>
</tbody>
</table>
</div><p>From the previous results, it shows that CountVectorizer has higher training accuracy, but this is often a sign for overfitting. It memorized patterns from training data too well but failed to generalize as well on unseen data. However, TF-IDF has better validation and test accuracies, meaning it generalizes better. This is expected, as TF-IDF down-weights common terms and focuses on more meaningful and rare ones.</p>
<p>To further evaluate the model’s predictions, I generated a <strong>confusion matrix</strong> for the Logistic Regression model trained with TF-IDF on the test set:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754178240765/fe1d19cd-3bbb-4145-9274-125122ae10c1.png" alt class="image--center mx-auto" /></p>
<p>Therefore, based on the better generalization performance, I decided to use <strong>TF-IDF</strong> for training and comparing the rest of classical machine learning as well.</p>
<h3 id="heading-training-function">Training Function</h3>
<p>To streamline the training process across different classical machine learning models, I created a reusable function called <em>train_model</em>. This function handles the training and returns important metrics such as training time and training accuracy. By passing in different model instances and training data, I could easily compare performance across various models.</p>
<p>Here is the code for the function:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">train_model</span>(<span class="hljs-params">model, X_train, y_train, model_name= <span class="hljs-string">"Model"</span></span>):</span> 
    <span class="hljs-comment"># Train the model</span>
    start = time.time()
    model.fit(X_train, y_train)
    end = time.time()
    train_time = end - start

    print(<span class="hljs-string">f"Training Time: <span class="hljs-subst">{train_time:<span class="hljs-number">.2</span>f}</span> seconds"</span>)

    <span class="hljs-comment"># Training Accuracy</span>
    train_acc = model.score(X_train_tfidf, y_train)
    print(<span class="hljs-string">f"Training Accuracy: <span class="hljs-subst">{train_acc*<span class="hljs-number">100</span>:<span class="hljs-number">.2</span>f}</span> %"</span>)

    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">"Model"</span>: model,
        <span class="hljs-string">"Model Name"</span>: model_name,
        <span class="hljs-string">"Train Accuracy"</span>: train_acc,
        <span class="hljs-string">"Training Time (s)"</span>: train_time
    }
</code></pre>
<h3 id="heading-evaluate-function">Evaluate Function</h3>
<p>Similarly, I created a reusable function called <em>evaluate_model</em> to handle evaluation for different models on either the <strong>validation</strong> or <strong>test</strong> set. This helped avoid repeating code and made it easy to generate consistent metrics and visualizations for every model. The function prints and returns important evaluation metrics as:</p>
<ul>
<li><p>Accuracy</p>
</li>
<li><p>Classification Report: Precision, Recall, F1-Score</p>
</li>
<li><p>Confusion Matrix</p>
</li>
</ul>
<p>Here is the code for the function:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sklearn.metrics <span class="hljs-keyword">import</span> accuracy_score, classification_report, confusion_matrix

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">evaluate_model</span>(<span class="hljs-params">model, X, y, set_name=<span class="hljs-string">"Test"</span></span>):</span>
    <span class="hljs-comment"># Predict on val/test set</span>
    y_pred = model.predict(X)

    <span class="hljs-comment"># Accuracy</span>
    acc = accuracy_score(y, y_pred)
    print(<span class="hljs-string">f"<span class="hljs-subst">{set_name}</span> Accuracy: <span class="hljs-subst">{acc*<span class="hljs-number">100</span>:<span class="hljs-number">.2</span>f}</span>%\n"</span>)

    <span class="hljs-comment"># Classification Report</span>
    print(<span class="hljs-string">f"Classification Report for <span class="hljs-subst">{set_name}</span>:\n"</span>)
    print(classification_report(y, y_pred))

    <span class="hljs-comment"># Confusion Matrix</span>
    cm = confusion_matrix(y, y_pred)
    labels = [<span class="hljs-string">'Negative'</span>, <span class="hljs-string">'Positive'</span>]

    plt.figure(figsize=(<span class="hljs-number">6</span>,<span class="hljs-number">5</span>))
    sns.heatmap(cm, annot=<span class="hljs-literal">True</span>, fmt=<span class="hljs-string">'d'</span>, cmap=<span class="hljs-string">'Blues'</span>, xticklabels=labels, yticklabels=labels)
    plt.xlabel(<span class="hljs-string">'Predicted Label'</span>)
    plt.ylabel(<span class="hljs-string">'True Label'</span>)
    plt.title(<span class="hljs-string">f'Confusion Matrix - <span class="hljs-subst">{set_name}</span>'</span>)
    plt.show()

    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">"Set"</span>: set_name,
        <span class="hljs-string">"Accuracy"</span>: acc,
        <span class="hljs-string">"Predictions"</span>: y_pred
    }
</code></pre>
<p>This function was used for evaluating all classical ML models after training. It made it easy to consistently measure and compare their performance on both the <strong>validation</strong> and <strong>test sets</strong>.</p>
<p>Now, let’s explore how each of the remaining classical models performed using this training and evaluation setup.</p>
<h3 id="heading-support-vector-machine-svm">Support Vector Machine (SVM)</h3>
<p><strong>Support Vector Machine (SVM)</strong> is a supervised machine learning algorithm that classifies data by finding an optimal line or hyperplane that best separates the classes in feature space. To have a better performing SVM, the goal is to maximize the margin which is the distance between the closest points of each class to the hyperplane. A larger margin generally leads to a better generalization on unseen data.</p>
<p>In this project I used a Linear Support Vector Classifier (SVC) which works well with TF-IDF vectors as it is high-dimensional text data. The SVM tries to find the best line between the two classes: Positive and Negative.</p>
<p>Here is how I initialized and trained the model:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sklearn.svm <span class="hljs-keyword">import</span> LinearSVC

<span class="hljs-comment"># Initialize the Linear SVM Model</span>
svm = LinearSVC()

<span class="hljs-comment"># Train Model</span>
svm_results = train_model(svm, X_train_tfidf, y_train, model_name=<span class="hljs-string">"SVM"</span>)
</code></pre>
<p>Then, I evaluated the model on the validation and test sets:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Get trained model and evaluate</span>
svm_model = svm_results[<span class="hljs-string">"Model"</span>]
svm_val = evaluate_model(svm_model, X_val_tfidf, y_val, <span class="hljs-string">"Validation"</span>)
svm_test = evaluate_model(svm_model, X_test_tfidf, y_test, <span class="hljs-string">"Test"</span>)
</code></pre>
<p>Here are the <strong>SVM Results:</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Training Accuracy</strong></td><td>96.37 %</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Training Time</strong></td><td>0.85 seconds</td></tr>
<tr>
<td><strong>Validation Accuracy</strong></td><td>88.32%</td></tr>
<tr>
<td><strong>Test Accuracy</strong></td><td>89.11%</td></tr>
</tbody>
</table>
</div><p>These results show that the SVM model trained on TF-IDF features performs consistently well across training, validation, and test sets showing good generalization and robustness.</p>
<p>Here is the <strong>confusion matrix</strong> for the test set predictions:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754182622215/8de00e7d-a403-435f-84dc-e4325a5712aa.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-multinominal-naive-bayes">Multinominal Naive Bayes:</h3>
<p><strong>Naive Bayes</strong> is a probabilistic supervised machine learning algorithm, commonly used for classification task. It assumes that all features (in our case words/tokens) are independent given the class label, which is why called “naive” assumption.</p>
<p>In this project, I used <strong>Multinominal Naive Bayes</strong> Variant, which is suitable for text classification problems. It works well when the features represent word counts or frequencies, such as those produced by TF-IDF or CountVectorizer. In our case, we continued using TF-IDF.</p>
<p>Here is how I initialized and trained the model:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sklearn.naive_bayes <span class="hljs-keyword">import</span> MultinomialNB

<span class="hljs-comment"># Initalizing the Model</span>
nb = MultinomialNB()

<span class="hljs-comment"># Training the Model</span>
nb_results = train_model(nb, X_train_tfidf, y_train, model_name=<span class="hljs-string">"Multinomial Naive Bayes"</span>)
</code></pre>
<p>Then, I evaluated the model on the validation and test sets:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Get trained model and evaluate</span>
nb_model = nb_results[<span class="hljs-string">"Model"</span>]
nb_val = evaluate_model(nb_model, X_val_tfidf, y_val, <span class="hljs-string">"Validation"</span>)
nb_test = evaluate_model(nb_model, X_test_tfidf, y_test, <span class="hljs-string">"Test"</span>)
</code></pre>
<p>Here are the <strong>Multinomial Naive Bayes Results:</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Training Accuracy</strong></td><td>88.09 %</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Training Time</strong></td><td>0.02 seconds</td></tr>
<tr>
<td><strong>Validation Accuracy</strong></td><td>86.23%</td></tr>
<tr>
<td><strong>Test Accuracy</strong></td><td>87.33%</td></tr>
</tbody>
</table>
</div><p>Despite being a very fast and lightweight model, Multinominal Naive Bayes still achieved competitive results. It serves as a strong baseline for text classification for our sentiment analysis task.</p>
<p>Here is the <strong>confusion matrix</strong> for the test set predictions:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754184061203/c2341c77-7b52-4ced-9c53-5db92dcf3b04.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-random-forest-rf">Random Forest (RF)</h3>
<p><strong>Random Forest (RF)</strong> is an ensemble machine learning algorithm used for classification and regression tasks. It builds multiple decision trees during training and merges them together, through majority voting in case of classification, to get more accurate and stable predictions.</p>
<p>In our case, we are working with sparse and high-dimensional TF-IDF vectors, which represent text data as long feature vectors with many zeros. Random Forest models, being tree-based, are not always ideal for this kind of data which they tend to work better on dense, structured datasets. As a result, we might expect slightly lower performance compared to linear models like SVM or Logistic Regression, which are better suited for high-dimensional text classification. However, it's still worth evaluating as part of our comparison.</p>
<p>Here is how I initialized and trained the model:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sklearn.ensemble <span class="hljs-keyword">import</span> RandomForestClassifier

<span class="hljs-comment"># Initializing the Model</span>
rf = RandomForestClassifier()
<span class="hljs-comment"># Training the Model</span>
rf_results = train_model(rf, X_train_tfidf, y_train, model_name=<span class="hljs-string">"Random Forest (RF)"</span>)
</code></pre>
<p>Then, I evaluated the model on the validation and test sets:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Get trained model and evaluate</span>
rf_model = rf_results[<span class="hljs-string">'Model'</span>]
rf_val = evaluate_model(rf_model, X_val_tfidf, y_val, <span class="hljs-string">"Validation"</span>)
rf_test = evaluate_model(rf_model, X_test_tfidf, y_test, <span class="hljs-string">"Test"</span>)
</code></pre>
<p>Here are the <strong>Random Forest (RF) Results:</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Training Accuracy</strong></td><td>100.00%</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Training Time</strong></td><td>150.90</td></tr>
<tr>
<td><strong>Validation Accuracy</strong></td><td>85.00%</td></tr>
<tr>
<td><strong>Test Accuracy</strong></td><td>85.87%</td></tr>
</tbody>
</table>
</div><p>The model clearly overfits on the training data, achieving an accuracy of 100%, but performs worse on unseen validation and test sets which was expected based on the characteristics we mentioned earlier about the Random Forest algorithm characteristics.</p>
<p>Here is the <strong>confusion matrix</strong> for the test set predictions:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754185120810/c48faaa5-17b6-4727-9ba3-743ed39f148e.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-bert">BERT</h2>
<p><strong>BERT</strong>, which stands for <strong>Bidirectional Encoder Representations from Transformers,</strong> is a transformer-based language model developed by Google. It captures context from both directions in text (left-to-right and right-to-left) which makes it highly effective in language understanding tasks.</p>
<p>In this project, I fine-tuned the pretrained model <em>bert-base-uncased</em> to classify IMDb reviews as positive or negative for sentiment analysis.</p>
<h3 id="heading-preprocessing-amp-tokenization">Preprocessing &amp; Tokenization</h3>
<p>Unlike the classical machine learning models, BERT does not require manual text cleaning. We work directly with raw text as the BERT tokenizer, handles lowercasing, punctuation and special tokens internally.</p>
<p>Similar as before, we split the dataset to 70% training, 15% validation and 15% testing.</p>
<p>We tokenized the text using the pretrained BertTokenizer with a max sequence length of 256 to balance the performance and memory.</p>
<p>Then, we loaded the BERT Tokenizer and tokenized each of the sets:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> transformers <span class="hljs-keyword">import</span> BertTokenizer

tokenizer = BertTokenizer.from_pretrained(<span class="hljs-string">'bert-base-uncased'</span>)
MAX_LEN = <span class="hljs-number">256</span>

<span class="hljs-comment"># Tokenizing the train set</span>
train_encodings = tokenizer(
    X_train,
    truncation = <span class="hljs-literal">True</span>,
    padding = <span class="hljs-literal">True</span>,
    max_length = MAX_LEN,
    return_tensors = <span class="hljs-string">"pt"</span>
)

<span class="hljs-comment"># Tokenizing the val set</span>
val_encodings = tokenizer(
    X_val,
    truncation=<span class="hljs-literal">True</span>,
    padding=<span class="hljs-literal">True</span>,
    max_length=MAX_LEN,
    return_tensors=<span class="hljs-string">"pt"</span>
)

<span class="hljs-comment"># Tokenizing the test set</span>
test_encodings = tokenizer(
    X_test,
    truncation = <span class="hljs-literal">True</span>,
    padding = <span class="hljs-literal">True</span>,
    max_length = MAX_LEN,
    return_tensors = <span class="hljs-string">"pt"</span>
)
</code></pre>
<p>After tokenizing, now we have the data as ‘input_ids’ and ‘attention_masks’.</p>
<p>We also converted the labels and created a custom dataset class for PyTorch.</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">IMDbDataset</span>(<span class="hljs-params">Dataset</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, encodings, labels</span>):</span>
        self.encodings = encodings
        self.labels = labels

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__len__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> len(self.labels)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__getitem__</span>(<span class="hljs-params">self, index</span>):</span>
        item = {key: val[index] <span class="hljs-keyword">for</span> key, val <span class="hljs-keyword">in</span> self.encodings.items()}
        item[<span class="hljs-string">'labels'</span>] = self.labels[index]
        <span class="hljs-keyword">return</span> item
</code></pre>
<p>For efficient training, we prepared DataLoader for all the sets we have as shown in the code:</p>
<pre><code class="lang-python">BATCH_SIZE = <span class="hljs-number">16</span>
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=<span class="hljs-literal">True</span>)
val_loader   = DataLoader(val_dataset, batch_size=BATCH_SIZE)
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE)
</code></pre>
<h3 id="heading-fine-tuning-the-bert-model">Fine-Tuning the BERT Model</h3>
<p>We used BertForSequenceClassification with 2 output classes (positive &amp; negative), trained for <strong>3 epochs</strong>, with a <strong>learning rate</strong> of <strong>2e-5</strong> on Google Colab GPUs.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> transformers <span class="hljs-keyword">import</span> BertForSequenceClassification

<span class="hljs-comment"># Load pretrained BERT model with a classification head for 2 classes (positive/negative)</span>
model = BertForSequenceClassification.from_pretrained(<span class="hljs-string">'bert-base-uncased'</span>, num_labels=<span class="hljs-number">2</span>)
</code></pre>
<h3 id="heading-training-and-evaluating-functions">Training and Evaluating Functions</h3>
<p>I implemented three reusable functions:</p>
<ol>
<li><strong>get_accuracy:</strong> computes prediction accuracy</li>
</ol>
<pre><code class="lang-python"><span class="hljs-comment"># Function to get accuracy</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_accuracy</span>(<span class="hljs-params">preds, labels</span>):</span>
    <span class="hljs-comment"># Get the predicted class by getting the index of the highest score</span>
    pred_labels = torch.argmax(preds, dim=<span class="hljs-number">1</span>)
    <span class="hljs-comment"># calculates the total number of correct predictions comparing with the labels</span>
    correct = (pred_labels == labels).sum().item()
    <span class="hljs-comment"># Calculates the accuracy of correct predictions over total</span>
    <span class="hljs-keyword">return</span> correct / len(labels)
</code></pre>
<ol start="2">
<li><strong>train_epoch():</strong> trains the model for one epoch</li>
</ol>
<pre><code class="lang-python"><span class="hljs-comment"># Training one epoch</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">train_epoch</span>(<span class="hljs-params">model, dataloader, optimizer, scheduler</span>):</span>
    <span class="hljs-comment"># Training mode for the model</span>
    model.train()

    <span class="hljs-comment"># Initializing to accumulate the training loss and accuracy over the epoch</span>
    total_loss = <span class="hljs-number">0</span>
    total_acc = <span class="hljs-number">0</span>

    <span class="hljs-comment"># Looping through each batch of data of input-output pairs</span>
    <span class="hljs-keyword">for</span> batch_idx, batch <span class="hljs-keyword">in</span> enumerate(dataloader):
        <span class="hljs-comment"># Clears previous gradients</span>
        optimizer.zero_grad()

        <span class="hljs-comment"># To make sure they are on the device we are training on</span>
        input_ids = batch[<span class="hljs-string">'input_ids'</span>].to(device)
        attention_mask = batch[<span class="hljs-string">'attention_mask'</span>].to(device)
        labels = batch[<span class="hljs-string">'labels'</span>].to(device)

        <span class="hljs-comment"># Forward pass which computes predictions and loss</span>
        outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=labels)
        loss = outputs.loss
        logits = outputs.logits

        <span class="hljs-comment"># Getting accuracy for current batch</span>
        acc = get_accuracy(logits, labels)

        <span class="hljs-comment"># Backpropagation</span>
        loss.backward()
        <span class="hljs-comment"># Update weights and optimizing</span>
        optimizer.step()
        scheduler.step()

        <span class="hljs-comment"># Accumulate loss and accuracy</span>
        total_loss += loss.item()
        total_acc += acc

        <span class="hljs-comment"># Print progress every 500 batches</span>
        <span class="hljs-keyword">if</span> batch_idx % <span class="hljs-number">500</span> == <span class="hljs-number">0</span>:
            print(<span class="hljs-string">f"Batch <span class="hljs-subst">{batch_idx}</span>/<span class="hljs-subst">{len(dataloader)}</span>, Loss: <span class="hljs-subst">{loss.item():<span class="hljs-number">.4</span>f}</span>, Acc: <span class="hljs-subst">{acc:<span class="hljs-number">.4</span>f}</span>"</span>)

    <span class="hljs-comment"># Returning the epoch loss and accuracy over all batches</span>
    avg_loss = total_loss / len(dataloader)
    avg_acc = total_acc / len(dataloader)
    <span class="hljs-keyword">return</span> avg_loss, avg_acc
</code></pre>
<ol start="3">
<li><strong>eval_model():</strong> evaluates model on validation/test sets</li>
</ol>
<pre><code class="lang-python"><span class="hljs-comment"># Evaluate the model (for validation or testing)</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">eval_model</span>(<span class="hljs-params">model, dataloader</span>):</span>
    <span class="hljs-comment"># Evaluating mode for the model</span>
    model.eval()

    <span class="hljs-comment"># Initializing to accumulate the training loss and accuracy over the epoch</span>
    total_loss = <span class="hljs-number">0</span>
    total_acc = <span class="hljs-number">0</span>

    <span class="hljs-comment"># No gradient calculation in evaluating, saving memory</span>
    <span class="hljs-keyword">with</span> torch.no_grad():
        <span class="hljs-keyword">for</span> batch <span class="hljs-keyword">in</span> dataloader:
            input_ids = batch[<span class="hljs-string">'input_ids'</span>].to(device)
            attention_mask = batch[<span class="hljs-string">'attention_mask'</span>].to(device)
            labels = batch[<span class="hljs-string">'labels'</span>].to(device)

            <span class="hljs-comment"># Forward pass</span>
            outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=labels)

            loss = outputs.loss
            logits = outputs.logits

            <span class="hljs-comment"># Calculate accuracy per batch</span>
            acc = get_accuracy(logits, labels)

            <span class="hljs-comment"># Accumulate loss and accuracy</span>
            total_loss += loss.item()
            total_acc += acc

    <span class="hljs-comment"># Returning avg loss and accuracy</span>
    avg_loss = total_loss / len(dataloader)
    avg_acc = total_acc / len(dataloader)
    <span class="hljs-keyword">return</span> avg_loss, avg_acc
</code></pre>
<h3 id="heading-bert-fine-tuning-results">BERT Fine-Tuning Results</h3>
<p>We got the following results for training</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td><strong>Training Accuracy</strong></td><td><strong>Training Loss</strong></td><td><strong>Validation Accuracy</strong></td><td><strong>Validation Loss</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Epoch 1</strong></td><td>87.95%</td><td>0.2779</td><td>91.57%</td><td>0.2164</td></tr>
<tr>
<td><strong>Epoch 2</strong></td><td>95.16%</td><td>0.1354</td><td>92.36%</td><td>0.2142</td></tr>
<tr>
<td><strong>Epoch 3</strong></td><td>98.33%</td><td>0.0563</td><td>92.61%</td><td>0.2519</td></tr>
</tbody>
</table>
</div><p>Total BERT Training Time: 4818.46 seconds</p>
<p>We also plotted the <strong>loss and accuracy curves</strong> for both training and validation as shown in the following plot:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754187476967/4215e875-bc87-4d64-9eb5-5f1b5a29732c.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-deployment-to-hugging-face">Deployment to Hugging Face</h3>
<p>After training, I saved the model locally, then uploaded it to <strong>Hugging Face Hub</strong> for public use.<br />You can find it here: <a target="_blank" href="https://huggingface.co/tarneemalaa/bert_imdb_model">Fine-tuned BERT IMDb Model</a>.</p>
<p>To load and use the model, use the following code snippet:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> transformers <span class="hljs-keyword">import</span> BertTokenizer, BertForSequenceClassification

model = BertForSequenceClassification.from_pretrained(<span class="hljs-string">"tarneemalaa/bert_imdb_model"</span>)
tokenizer = BertTokenizer.from_pretrained(<span class="hljs-string">"tarneemalaa/bert_imdb_model"</span>)
</code></pre>
<h3 id="heading-testing-the-model">Testing the Model</h3>
<p>We got those results, applying the fine-tuned model on the test set:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Test Accuracy</strong></td><td><strong>93.08%</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Test Loss</strong></td><td>0.2334</td></tr>
</tbody>
</table>
</div><p>The test accuracy of 93.08% is a great result, shows that the model is performing really well on unseen data. There is a possibility if the model has been trained for more epochs, it would have achieved even better results.</p>
<p>Here is the <strong>confusion matrix</strong> on the test set:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754187733977/17b7def1-44bc-460c-87d1-dc9e32bbf963.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-interactive-movie-sentiment-classifier-gradio-app">Interactive Movie Sentiment Classifier - Gradio App:</h2>
<p>To make the project more engaging and interactive, I built a <strong>Gradio web app</strong> where users can test and compare all the trained models live. The app allows users to enter any movie review, select the model they want (e.g., BERT, Logistic Regression), and instantly get the <strong>sentiment analysis</strong>(Positive or Negative) along with confidence where applicable.</p>
<p>This allows the user to actually test and try, experiencing how each model interprets the data, instead of just looking at accuracy percentages of models.</p>
<p>It supports all of the previously discussed models:</p>
<ol>
<li><p>Fine-tuned BERT</p>
</li>
<li><p>Logistic Regression</p>
</li>
<li><p>Support Vector Machine</p>
</li>
<li><p>Naive Bayes</p>
</li>
<li><p>Random Forest</p>
</li>
</ol>
<p>If you choose <strong>BERT</strong>, the app tokenizes the text and feeds it into the <a target="_blank" href="https://huggingface.co/tarneemalaa/bert_imdb_model">fine-tuned model from Hugging Face</a> that I uploaded. If you choose any <strong>classical ML model</strong>, the review is vectorized using the same TF-IDF vectorizer used in training, and the corresponding model makes the prediction.</p>
<hr />
<h2 id="heading-try-it-out">Try it Out:</h2>
<h3 id="heading-light-mode">Light Mode</h3>
<p><a target="_blank" href="https://tarneemalaa-imdb-sentiment-classifier.hf.space/?__theme=light">https://tarneemalaa-imdb-sentiment-classifier.hf.space/?__theme=light</a></p>
<h3 id="heading-dark-mode">Dark Mode</h3>
<p><a target="_blank" href="https://tarneemalaa-imdb-sentiment-classifier.hf.space/?__theme=dark">https://tarneemalaa-imdb-sentiment-classifier.hf.space/?__theme=dark</a></p>
<hr />
<p>Here is a few screenshots from the app:</p>
<ol>
<li>Home Page</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754194664560/0f5359cc-f9ec-476d-bacc-f5df26fb1497.png" alt class="image--center mx-auto" /></p>
<ol start="2">
<li>Choose Model</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754194912889/704725ce-6e99-4979-acdb-4e0b9ccd4d36.png" alt class="image--center mx-auto" /></p>
<ol start="3">
<li>Enter Review and Submit</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754194747102/fe137ab1-c82e-4733-8932-0f5e77693de2.png" alt class="image--center mx-auto" /></p>
<p>For the movie review:<br />“I was really looking forward to this movie, but it turned out to be a huge letdown. The story was slow and lacked direction, and the characters felt flat and uninteresting. Some scenes looked visually nice, but that wasn't enough to save it. The dialogue was awkward, and the plot just didn’t go anywhere meaningful. I kept waiting for something to happen, but it never did. Overall, it was boring and forgettable I wouldn’t recommend it.”</p>
<p>Fine-tuned BERT detected that it is a negative review with confidence of 99.86%.</p>
<hr />
<h2 id="heading-summary">Summary</h2>
<p>Here is a summary of all the models used with all the accuracies for comparison and analysis:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754195740452/777bd5d4-8f94-47ac-b793-0146e92165e2.jpeg" alt class="image--center mx-auto" /></p>
<p>It clearly shows how BERT, while being the most accurate, requires significantly more training time. On the other hand, classical models like Logistic Regression and Naive Bayes offer fast and reasonably good performance, making them a good choice when compute or time is limited.</p>
<hr />
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>This project was a deep dive into both classical and modern NLP techniques for sentiment analysis. It allowed me to explore and compare the strengths and trade-offs between traditional machine learning models and transformer-based models like BERT.</p>
<p>Building the interactive Gradio app brought the project to life making it easy for anyone to try the models hands-on and see how each one performs in real time.</p>
<p>If you have any feedback, suggestions, or just want to connect feel free to reach out!</p>
<p><strong>GitHub Repository</strong>:<br />All the code for data preprocessing, classical models, BERT fine-tuning, evaluation, and the deployed app is available here:<br /><a target="_blank" href="https://github.com/TarneemAlaa1/imdb-sentiment-classifier">github.com/TarneemAlaa1/imdb-sentiment-classifier</a></p>
]]></content:encoded></item></channel></rss>