Sequence Models : RNNs & their limitations

Business Scenario

Welcome!

Previously you cleaned, tokenized, and normalized customer queries/reviews, converted text, explored word vectors using an Embedding layer and performed an end-to-end text analysis on a new customer query

Today, your manager has assigned you a task to build a Simple RNN using the same preprocessed dataset from previous lab and check whether understanding the order of words in customer queries can improve prediction accuracy and reduce incorrect query routing.

Git Pull

git pull origin branchName

Pre-Lab Preparation

Topic : Recurrent Neural Network

1) Understand RNN fundamentals and hidden-state mechanism.

2) Learn different types of RNN architectures.
3) Understand the sequential processing bottleneck.
4) Explore the vanishing gradient problem.

Recurrent Neural Network

RNN

Task 1: Reload Lab 3's Preprocessed Query Dataset

import re
import numpy as np, pandas as pd

import nltk
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
from nltk.tokenize import word_tokenize

import tensorflow as tf
from tensorflow import keras
from keras.layers import TextVectorization, Embedding, SimpleRNN, Dense
from keras import Sequential
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder

RANDOM_SEED = 42
DATA_PATH = "smartcart_customer_queries.csv"
MAX_TOKENS = 2000
EMBEDDING_DIM = 16
RNN_UNITS = 16
TEST_SIZE = 0.2
EPOCHS = 30
BATCH_SIZE = 32

Dataset :

Dataset :

import re
import numpy as np, pandas as pd

import nltk
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
from nltk.tokenize import word_tokenize

import tensorflow as tf
from tensorflow import keras
from keras.layers import TextVectorization, Embedding, SimpleRNN, Dense
from keras import Sequential
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder

RANDOM_SEED = 42
DATA_PATH = "smartcart_customer_queries.csv"
MAX_TOKENS = 2000
EMBEDDING_DIM = 16
RNN_UNITS = 16
TEST_SIZE = 0.2
EPOCHS = 30
BATCH_SIZE = 32

np.random.seed(RANDOM_SEED)
tf.random.set_seed(RANDOM_SEED)

nltk.download("punkt"); nltk.download("punkt_tab")
nltk.download("stopwords"); nltk.download("wordnet"); nltk.download("omw-1.4")

lemmatizer = WordNetLemmatizer()
stop_words = set(stopwords.words("english"))

def preprocess(text: str) -> str:
    """Clean, tokenize, drop stopwords, and lemmatize one query."""
    text = str(text).lower()
    text = re.sub(r'[^a-z0-9\s]', ' ', text)
    text = re.sub(r'\s+', ' ', text).strip()
    tokens = word_tokenize(text)
    tokens = [t for t in tokens if t not in stop_words]
    tokens = [lemmatizer.lemmatize(t) for t in tokens]
    return ' '.join(tokens)

df = pd.read_csv(DATA_PATH)
df['processed_text'] = df['query_text'].apply(preprocess)

train_df, test_df = train_test_split(
    df, test_size=TEST_SIZE, random_state=RANDOM_SEED, stratify=df['category']
)

sequence_length = int(np.percentile(
    train_df['processed_text'].str.split().apply(len), 95))

import re
import numpy as np, pandas as pd

import nltk
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
from nltk.tokenize import word_tokenize

import tensorflow as tf
from tensorflow import keras
from keras.layers import TextVectorization, Embedding, SimpleRNN, Dense
from keras import Sequential
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder

RANDOM_SEED = 42
DATA_PATH = "smartcart_customer_queries.csv"
MAX_TOKENS = 2000
EMBEDDING_DIM = 16
RNN_UNITS = 16
TEST_SIZE = 0.2
EPOCHS = 30
BATCH_SIZE = 32

np.random.seed(RANDOM_SEED)
tf.random.set_seed(RANDOM_SEED)

nltk.download("punkt"); nltk.download("punkt_tab")
nltk.download("stopwords"); nltk.download("wordnet"); nltk.download("omw-1.4")

lemmatizer = WordNetLemmatizer()
stop_words = set(stopwords.words("english"))

def preprocess(text: str) -> str:
    """Clean, tokenize, drop stopwords, and lemmatize one query."""
    text = str(text).lower()
    text = re.sub(r'[^a-z0-9\s]', ' ', text)
    text = re.sub(r'\s+', ' ', text).strip()
    tokens = word_tokenize(text)
    tokens = [t for t in tokens if t not in stop_words]
    tokens = [lemmatizer.lemmatize(t) for t in tokens]
    return ' '.join(tokens)

df = pd.read_csv(DATA_PATH)
df['processed_text'] = df['query_text'].apply(preprocess)

train_df, test_df = train_test_split(
    df, test_size=TEST_SIZE, random_state=RANDOM_SEED, stratify=df['category']
)

sequence_length = int(np.percentile(
    train_df['processed_text'].str.split().apply(len), 95))

int_vectorizer = TextVectorization(
    output_mode='int',
    max_tokens=MAX_TOKENS,
    output_sequence_length=sequence_length
)
int_vectorizer.adapt(train_df['processed_text'].tolist())  # TRAIN text only

label_encoder = LabelEncoder().fit(df['category'])
class_names = label_encoder.classes_

X_train = int_vectorizer(train_df['processed_text'].tolist()).numpy()
y_train = label_encoder.transform(train_df['category'])
X_test = int_vectorizer(test_df['processed_text'].tolist()).numpy()
y_test = label_encoder.transform(test_df['category'])
vocab_size = int_vectorizer.vocabulary_size()

print(f"Train/test split: {len(train_df)} / {len(test_df)} queries")
print(f"Sequence length (training cutoff): {sequence_length}")
print(f"Vocabulary size (fit on train only): {vocab_size}")

Output

Task 2 : Build a Simple RNN

def build_rnn_model(vocab_size: int, num_classes: int) -> keras.Model:
    """Embedding -> SimpleRNN -> Dense classifier for query intent."""
    model = Sequential([
        Embedding(input_dim=vocab_size, output_dim=EMBEDDING_DIM),
        SimpleRNN(RNN_UNITS),
        Dense(num_classes, activation='softmax')])
    model.compile(
        optimizer='adam',
        loss='sparse_categorical_crossentropy',
        metrics=['accuracy'])
    return model

rnn_model = build_rnn_model(vocab_size, num_classes=len(class_names))
rnn_model.summary()

Embedding has no fixed input_length on purpose — Simple RNN shares its weights across every timestep, so it can accept sequences of any length at inference time

Task 3 : Train and Evaluate on Short Queries

early_stop = keras.callbacks.EarlyStopping(
    monitor='val_accuracy', patience=5, restore_best_weights=True)

history = rnn_model.fit(
    X_train, y_train,
    validation_split=0.2,
    epochs=EPOCHS,
    batch_size=BATCH_SIZE,
    callbacks=[early_stop],
    verbose=0)

test_loss, test_accuracy = rnn_model.evaluate(X_test, y_test, verbose=0)
print(f"Stopped after {len(history.history['loss'])} epochs "
      f"(best weights restored)")
print(f"Held-out test accuracy on short queries: {test_accuracy:.3f}")

Output

Task 4: Test on Longer Shopping Queries

long_query_samples = [
    {"ticket_id": "SC-4831-01", "expected_category": "Electronics",
     "query": "looking for a lightweight wireless bluetooth headphone "
              "with good battery life and noise cancelling under budget "
              "two thousand rupees with fast charging support"},
    {"ticket_id": "SC-4831-02", "expected_category": "Electronics",
     "query": "need a budget smartphone with good camera long battery "
              "backup fast charging support and at least six gb ram "
              "under fifteen thousand rupees"},
    {"ticket_id": "SC-4831-03", "expected_category": "Apparel",
     "query": "want a slim fit cotton casual shirt for men in navy blue "
              "color size large with full sleeves for daily office wear"},
    {"ticket_id": "SC-4831-04", "expected_category": "Apparel",
     "query": "looking for comfortable running shoes for women with good "
              "grip lightweight sole and breathable material for daily "
              "gym and outdoor jogging"},
    {"ticket_id": "SC-4831-05", "expected_category": "Grocery",
     "query": "want organic basmati rice five kilogram pack with long "
              "grain good aroma and no added chemicals for daily home "
              "cooking"},
    {"ticket_id": "SC-4831-06", "expected_category": "Grocery",
     "query": "looking for cold pressed olive oil one litre bottle with "
              "rich flavor and no preservatives for healthy daily cooking"},]
long_query_samples = [
    {"ticket_id": "SC-4831-01", "expected_category": "Electronics",
     "query": "looking for a lightweight wireless bluetooth headphone "
              "with good battery life and noise cancelling under budget "
              "two thousand rupees with fast charging support"},
    {"ticket_id": "SC-4831-02", "expected_category": "Electronics",
     "query": "need a budget smartphone with good camera long battery "
              "backup fast charging support and at least six gb ram "
              "under fifteen thousand rupees"},
    {"ticket_id": "SC-4831-03", "expected_category": "Apparel",
     "query": "want a slim fit cotton casual shirt for men in navy blue "
              "color size large with full sleeves for daily office wear"},
    {"ticket_id": "SC-4831-04", "expected_category": "Apparel",
     "query": "looking for comfortable running shoes for women with good "
              "grip lightweight sole and breathable material for daily "
              "gym and outdoor jogging"},
    {"ticket_id": "SC-4831-05", "expected_category": "Grocery",
     "query": "want organic basmati rice five kilogram pack with long "
              "grain good aroma and no added chemicals for daily home "
              "cooking"},
    {"ticket_id": "SC-4831-06", "expected_category": "Grocery",
     "query": "looking for cold pressed olive oil one litre bottle with "
              "rich flavor and no preservatives for healthy daily cooking"},]

def evaluate_on_queries(model, vectorizer, samples):
    """Run raw queries through preprocessing + vectorizer + model,
    returning a results table with predictions and confidence."""
    texts = [preprocess(s['query']) for s in samples]
    sequences = vectorizer(texts).numpy()
    probs = model.predict(sequences, verbose=0)
    results = pd.DataFrame(samples)
    results['predicted_category'] = class_names[np.argmax(probs, axis=1)]
    results['confidence'] = probs.max(axis=1).round(2)
    return results

# Re-vectorize without the 7-token training cutoff -- these queries run
# well past it, and we want to see how the RNN handles the full length.
eval_vectorizer = TextVectorization(output_mode='int', max_tokens=MAX_TOKENS)
eval_vectorizer.adapt(train_df['processed_text'].tolist())  # same vocab

results_df = evaluate_on_queries(rnn_model, eval_vectorizer,
                                  long_query_samples)
long_query_accuracy = (
    results_df['predicted_category'] == results_df['expected_category']
).mean()

print(results_df[['ticket_id', 'expected_category',
                   'predicted_category', 'confidence']])
print(f"\nAccuracy on {len(long_query_samples)} queries from SC-4831: "
      f"{long_query_accuracy:.2f}")

Output

 

Great job!

You have successfully reloaded Lab 3’s padded query sequences, built and trained a Simple RNN classifier, evaluated its accuracy and confidence on short and longer shopping queries, and explored its key limitations—sequential bottleneck, vanishing gradients, and limited long-range context—motivating the need for a better architecture

Checkpoint

   Git Push

git push origin branchName

Next-Lab Preparation

Topic : Long Short-term Memory

1) LSTM gating mechanism

2) GRU gating mechanism

3) Memory Retention

GenAI - 4

By Content ITV

GenAI - 4

  • 22