This repository documents a binary text classification workflow that fine-tunes a pretrained BERT encoder in TensorFlow to distinguish sincere questions from insincere ones. The implementation uses TensorFlow 2, TensorFlow Hub, TensorFlow Model Garden BERT utilities, and a custom tf.data input pipeline built around the Quora Insincere Questions Classification dataset.
The notebook covers the full training path end to end: dataset ingestion, stratified sampling, BERT tokenization, TensorFlow input pipeline construction, model definition, supervised fine-tuning, and sample inference. The recorded run in the notebook uses a reduced stratified subset of the original dataset for faster experimentation, so the reported metrics should be interpreted as subset-level results rather than full-dataset benchmark numbers.
- Build TensorFlow input pipelines for text data with the
tf.dataAPI. - Tokenize and preprocess raw text into BERT-compatible features.
- Fine-tune a pretrained BERT encoder for binary text classification.
The project uses the Quora Insincere Questions Classification dataset, a large-scale binary classification dataset where each example contains a question and a target label indicating whether the question is sincere (0) or insincere (1).
- Notebook-loaded dataset shape:
1,306,122rows x3columns - Observed columns from the notebook output:
qidquestion_texttarget
- Dataset source:
- Original competition page: Quora Insincere Questions Classification
- Archived file used in the notebook: train.csv.zip
The target histogram shown in the notebook indicates a clear class imbalance, with sincere questions dominating the dataset. That imbalance is preserved during splitting by using stratified sampling.
The notebook records the following runtime details:
- TensorFlow version:
2.19.0 - TensorFlow Hub version:
0.16.1 - Eager execution: enabled
- GPU availability: detected
- Recorded accelerator: NVIDIA Tesla T4
The setup stage also performs several environment-specific dependency steps:
- installs
tf_keras - clones
tensorflow/modelsat tagv2.3.0 - installs a modified version of the Model Garden requirements file with
tensorflow-addonsexcluded - appends the cloned
modelsrepository tosys.path - adds a lightweight
tensorflow_addonsimport fallback so the notebook can import Model Garden utilities without failing when the package is unavailable
The dataset is loaded directly into a pandas DataFrame from the archived compressed CSV file. For training and validation, the notebook intentionally works on a small stratified subset rather than the full dataset:
- training split:
9,795rows - validation split:
972rows
This sampling is produced with train_test_split(..., stratify=df.target.values) to preserve label proportions across splits.
The notebook creates TensorFlow datasets on CPU with tf.data.Dataset.from_tensor_slices, using:
question_textas the raw text inputtargetas the binary label
This yields datasets of (text, label) pairs before BERT preprocessing is applied.
The pretrained encoder is downloaded from TensorFlow Hub:
The notebook resolves the model vocabulary and casing configuration directly from the Hub layer and constructs a FullTokenizer from TensorFlow Model Garden BERT utilities. Example tokenization from the notebook:
- tokens for
"hi, how are you doing?":['hi', '##,', 'how', 'are', 'you', 'doing', '##?'] - token ids:
[7632, 29623, 2129, 2024, 2017, 2725, 29632]
Raw text is converted into BERT-ready features in two steps:
classifier_data_lib.InputExamplewraps the raw text and label.classifier_data_lib.convert_single_example(...)produces:input_idsinput_masksegment_idslabel_id
Because Dataset.map runs in graph mode, the Python-side preprocessing logic is wrapped with tf.py_function. The resulting tensors are assigned static shapes and returned in the feature dictionary expected by the model:
{
"input_word_ids": input_ids,
"input_mask": input_mask,
"input_type_ids": segment_ids
}After mapping text into BERT features, the notebook builds the final input pipeline with:
map(..., num_parallel_calls=tf.data.experimental.AUTOTUNE)shuffle(1000)for training databatch(32, drop_remainder=True)prefetch(tf.data.experimental.AUTOTUNE)
The resulting dataset contract is:
- feature tensors:
input_word_ids: shape(batch_size, 128)input_mask: shape(batch_size, 128)input_type_ids: shape(batch_size, 128)
- label tensor:
- scalar class id before batching
- batched shape
(batch_size,)
The notebook output confirms the batched element_spec for both training and validation as:
input_word_ids:TensorSpec(shape=(32, 128), dtype=tf.int32)input_mask:TensorSpec(shape=(32, 128), dtype=tf.int32)input_type_ids:TensorSpec(shape=(32, 128), dtype=tf.int32)- labels:
TensorSpec(shape=(32,), dtype=tf.int32)
The classification model fine-tunes the full BERT encoder rather than freezing it.
- maximum sequence length:
128 - batch size:
32 - encoder:
hub.KerasLayer(..., trainable=True) - inputs:
input_word_idsinput_maskinput_type_ids
The Hub layer returns both:
pooled_outputwith shape(None, 768)sequence_outputwith shape(None, 128, 768)
For classification, the notebook uses the pooled representation only. The classification head is intentionally simple:
Dropout(0.4)Dense(1, activation="sigmoid")
According to the recorded model summary:
- total parameters:
109,483,010 - trainable parameters:
109,483,009 - non-trainable parameters:
1
The model therefore exposes a single sigmoid output for binary classification. In the notebook's sample inference block, predictions are interpreted with a threshold of 0.7.
The model is compiled with:
- optimizer:
Adam(learning_rate=2e-5) - loss:
BinaryCrossentropy() - metric:
BinaryAccuracy()
Training configuration from the notebook:
- epochs:
4 - full encoder fine-tuning: enabled (
trainable=True) - validation performed at the end of each epoch on the sampled validation split
The following metrics come from the stored notebook output and reflect training on the reduced stratified subset (9,795 train / 972 validation), not the full 1.3M+ row dataset.
| Epoch | Train Loss | Train Binary Accuracy | Validation Loss | Validation Binary Accuracy |
|---|---|---|---|---|
| 1 | 0.1583 | 0.9420 | 0.1216 | 0.9542 |
| 2 | 0.0970 | 0.9645 | 0.1212 | 0.9583 |
| 3 | 0.0482 | 0.9823 | 0.1838 | 0.9583 |
| 4 | 0.0207 | 0.9940 | 0.2144 | 0.9573 |
Two patterns stand out in the recorded run:
- training loss decreases steadily across all four epochs
- validation loss is lowest at epoch 2, then rises while training accuracy continues to improve
This suggests the model begins to overfit after the second epoch on the sampled subset, even though validation accuracy remains high.
The notebook also runs inference on five custom examples and classifies them with a 0.7 decision threshold:
| Example | Predicted Label |
|---|---|
May I please have your email id? |
Sincere |
The world is cruel, but it is also very beautiful. |
Sincere |
What a surprising turn of events! |
Sincere |
| An explicitly insulting sentence targeting others | Insincere |
The weather is nice today. |
Sincere |
This quick sanity check is consistent with the intended task: neutral or ordinary language is classified as sincere, while the hostile example is classified as insincere.
The fine-tuned model consumes a dictionary of BERT features:
{
"input_word_ids": tf.Tensor(shape=(batch_size, 128), dtype=tf.int32),
"input_mask": tf.Tensor(shape=(batch_size, 128), dtype=tf.int32),
"input_type_ids": tf.Tensor(shape=(batch_size, 128), dtype=tf.int32)
}It returns a single sigmoid score per example:
- output shape:
(batch_size, 1) - semantic meaning: probability-like score for the positive class (
insincere) - sample notebook threshold: values
>= 0.7are interpreted asInsincere
Text preprocessing is not implemented with a custom tokenizer; it depends on TensorFlow Model Garden BERT utilities and the vocabulary/configuration resolved from the TensorFlow Hub BERT layer.
Fine_Tune_BERT_for_Text_Classification.ipynb: the main notebook containing the full data preparation, BERT preprocessing, model definition, fine-tuning, evaluation, and inference workflow.