TensorFlow Sentiment Analysis

Sentiment analysis classifies text as expressing a positive or negative opinion. Product reviews, social media posts, customer feedback, and news articles are all examples of text where sentiment classification adds business value. This topic builds a complete sentiment analysis model on the IMDB movie review dataset — 50,000 reviews labeled positive or negative — using everything covered in the RNN section.

The Task

Input:  "The acting was superb and the story kept me hooked throughout."
Output: POSITIVE (probability: 0.94)

Input:  "Boring, predictable, and a complete waste of two hours."
Output: NEGATIVE (probability: 0.03)

Step 1 — Load the IMDB Dataset

import tensorflow as tf
import numpy as np

# IMDB: 25,000 training reviews, 25,000 test reviews
# Already tokenized — words replaced with integer IDs
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.imdb.load_data(
    num_words=10000   # Keep only the 10,000 most frequent words
)

print(f"Training samples: {len(x_train)}")   # 25,000
print(f"Test samples:     {len(x_test)}")    # 25,000
print(f"Label values:     {set(y_train)}")   # {0, 1}

# Reviews have variable length
print(f"Shortest review: {min(len(r) for r in x_train)} words")
print(f"Longest review:  {max(len(r) for r in x_train)} words")
print(f"Average review:  {np.mean([len(r) for r in x_train]):.0f} words")

Step 2 — Pad Sequences

MAXLEN = 200   # Reviews longer than 200 words get truncated

x_train = tf.keras.preprocessing.sequence.pad_sequences(
    x_train, maxlen=MAXLEN, padding='post', truncating='post'
)
x_test = tf.keras.preprocessing.sequence.pad_sequences(
    x_test,  maxlen=MAXLEN, padding='post', truncating='post'
)

print(x_train.shape)   # (25000, 200)
print(x_test.shape)    # (25000, 200)

Step 3 — Build the Model

VOCAB_SIZE = 10000
EMBEDDING_DIM = 128
MAXLEN = 200

model = tf.keras.Sequential([
    # Convert integer IDs to 128-dim vectors
    tf.keras.layers.Embedding(VOCAB_SIZE, EMBEDDING_DIM, input_length=MAXLEN),

    # Bidirectional LSTM to capture context from both directions
    tf.keras.layers.Bidirectional(
        tf.keras.layers.LSTM(64, return_sequences=True, dropout=0.2)
    ),
    tf.keras.layers.Bidirectional(
        tf.keras.layers.LSTM(32, dropout=0.2)
    ),

    # Dense classification head
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dropout(0.4),
    tf.keras.layers.Dense(1, activation='sigmoid')   # Binary output
])

model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
    loss='binary_crossentropy',
    metrics=['accuracy',
             tf.keras.metrics.AUC(name='auc'),
             tf.keras.metrics.Precision(name='precision'),
             tf.keras.metrics.Recall(name='recall')]
)

model.summary()

Step 4 — Train With Callbacks

callbacks = [
    tf.keras.callbacks.EarlyStopping(
        monitor='val_auc',
        patience=3,
        mode='max',
        restore_best_weights=True
    ),
    tf.keras.callbacks.ReduceLROnPlateau(
        monitor='val_loss',
        factor=0.5,
        patience=2,
        min_lr=1e-6
    )
]

history = model.fit(
    x_train, y_train,
    epochs=15,
    batch_size=64,
    validation_split=0.1,
    callbacks=callbacks
)

Step 5 — Evaluate on Test Set

results = model.evaluate(x_test, y_test, verbose=1)
print(f"\nTest Loss:      {results[0]:.4f}")
print(f"Test Accuracy:  {results[1]:.2%}")
print(f"Test AUC:       {results[2]:.4f}")
print(f"Test Precision: {results[3]:.4f}")
print(f"Test Recall:    {results[4]:.4f}")

Step 6 — Predict on Raw Text

# The IMDB dataset uses its own vocabulary encoding
# For raw text prediction, we need to use our own vectorization

word_index = tf.keras.datasets.imdb.get_word_index()
# Offset by 3 because 0=padding, 1=start, 2=unknown
word_index = {k: v + 3 for k, v in word_index.items()}
word_index['<PAD>'] = 0
word_index['<START>'] = 1
word_index['<UNK>'] = 2

def encode_review(text):
    tokens = text.lower().split()
    ids = [word_index.get(word, 2) for word in tokens]   # UNK=2 for unknown
    ids = [id for id in ids if id < 10000]               # Clip to vocab size
    padded = tf.keras.preprocessing.sequence.pad_sequences(
        [ids], maxlen=200, padding='post'
    )
    return padded

def predict_sentiment(text):
    encoded = encode_review(text)
    prob = model.predict(encoded, verbose=0)[0][0]
    sentiment = "POSITIVE" if prob > 0.5 else "NEGATIVE"
    print(f"Text: {text[:60]}...")
    print(f"Sentiment: {sentiment} ({prob:.2%} positive)")

predict_sentiment("This was an absolutely brilliant film. Loved every minute.")
predict_sentiment("Terrible movie. Boring plot and awful acting throughout.")

Understanding What the Model Learned

Diagram — How the Model Processes a Review:

"The plot was uninspired and the acting was wooden"

Token IDs:   [3, 105, 22, 847, 9, 3, 412, 22, 932, 0, 0...]
                 ↓
Embedding:   (200, 128) — each word becomes a 128-dim vector
                 ↓
Bi-LSTM 1:  (200, 128) — contextual representation at each word
             Forward pass: left→right context
             Backward pass: right→left context
                 ↓
Bi-LSTM 2:  (64,) — compressed sequence summary
                 ↓
Dense(64):  (64,) — further abstraction
                 ↓
Dense(1):   (1,) → sigmoid → 0.04 ← NEGATIVE (4% positive probability)

Comparing Models for Sentiment Analysis

Model                            IMDB Accuracy  Training Time
────────────────────────────────────────────────────────────────
SimpleRNN                        ~72%           Fast
LSTM (single layer)              ~85%           Medium
Bidirectional LSTM               ~88%           Medium
Stacked Bidirectional LSTM       ~89%           Slow
Transformer (custom)             ~91%           Slow
BERT (fine-tuned, pre-trained)   ~94%           Very slow initially
────────────────────────────────────────────────────────────────

Visualizing Attention Weights (Optional Enhancement)

# Add a simple attention mechanism to see which words the model focuses on
class AttentionLayer(tf.keras.layers.Layer):
    def __init__(self, units):
        super().__init__()
        self.W = tf.keras.layers.Dense(units, activation='tanh')
        self.V = tf.keras.layers.Dense(1)

    def call(self, lstm_output):
        score = self.V(self.W(lstm_output))           # (batch, steps, 1)
        weights = tf.nn.softmax(score, axis=1)        # Normalize across steps
        context = tf.reduce_sum(weights * lstm_output, axis=1)  # Weighted sum
        return context, weights

# High weight words in a negative review:
# "uninspired" (0.18), "wooden" (0.15), "terrible" (0.12)
# Low weight words: "the" (0.01), "and" (0.01), "was" (0.02)

Handling Class Imbalance in Sentiment Tasks

# If your dataset has unequal positive/negative samples
neg_count = sum(y_train == 0)
pos_count = sum(y_train == 1)

weight_for_neg = (1 / neg_count) * (len(y_train) / 2.0)
weight_for_pos = (1 / pos_count) * (len(y_train) / 2.0)

class_weight = {0: weight_for_neg, 1: weight_for_pos}
model.fit(x_train, y_train, class_weight=class_weight, epochs=10)

Sentiment analysis is one of the most commercially valuable NLP applications. Every customer service platform, product review system, and brand monitoring tool relies on some form of sentiment classification. With the skills from this topic — tokenization, embedding, bidirectional LSTM, and proper evaluation — you can build production-quality sentiment classifiers for any domain. The next topic begins the advanced section with pre-trained models and fine-tuning techniques.

Leave a Comment

Your email address will not be published. Required fields are marked *