Log In

Natural Language Processing with RNNs - Toxic Comment Classification

Open In Colab

Outline:

  1. Download and explore the data
  2. Prepare the data for training
  3. Build a recurrent neural network
  4. Train & evaluate the model
  5. Make predictions & submit to Kaggle

Download & Explore Data

Outline:

  1. Download the data from Kaggle
  2. Load data into Pandas dataframes
  3. Explore the dataset
import os
os.environ['KAGGLE_CONFIG_DIR'] = '.'
!kaggle competitions download -c jigsaw-toxic-comment-classification-challenge
Warning: Your Kaggle API key is readable by other users on this system! To fix this, you can run 'chmod 600 ./kaggle.json' Downloading jigsaw-toxic-comment-classification-challenge.zip to /content 36% 19.0M/52.6M [00:00<00:00, 197MB/s] 100% 52.6M/52.6M [00:00<00:00, 281MB/s]
!unzip jigsaw-toxic-comment-classification-challenge.zip -d data
Archive: jigsaw-toxic-comment-classification-challenge.zip inflating: data/sample_submission.csv.zip inflating: data/test.csv.zip inflating: data/test_labels.csv.zip inflating: data/train.csv.zip
import pandas as pd
raw_df = pd.read_csv('data/train.csv.zip')
test_df = pd.read_csv('data/test.csv.zip')
sub_df = pd.read_csv('data/sample_submission.csv.zip')
raw_df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 159571 entries, 0 to 159570 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 159571 non-null object 1 comment_text 159571 non-null object 2 toxic 159571 non-null int64 3 severe_toxic 159571 non-null int64 4 obscene 159571 non-null int64 5 threat 159571 non-null int64 6 insult 159571 non-null int64 7 identity_hate 159571 non-null int64 dtypes: int64(6), object(2) memory usage: 9.7+ MB
raw_df.sample(10)
raw_df.comment_text.values[0]
"Explanation\nWhy the edits made under my username Hardcore Metallica Fan were reverted? They weren't vandalisms, just closure on some GAs after I voted at New York Dolls FAC. And please don't remove the template from the talk page since I'm retired now.89.205.38.27"
target_cols = ['toxic', 'severe_toxic', 'obscene', 'threat', 'insult', 'identity_hate']
for col in target_cols:
    print(raw_df[col].value_counts(normalize=True))
0 0.904156 1 0.095844 Name: toxic, dtype: float64 0 0.990004 1 0.009996 Name: severe_toxic, dtype: float64 0 0.947052 1 0.052948 Name: obscene, dtype: float64 0 0.997004 1 0.002996 Name: threat, dtype: float64 0 0.950636 1 0.049364 Name: insult, dtype: float64 0 0.991195 1 0.008805 Name: identity_hate, dtype: float64
test_df
sub_df

Prepare the Dataset for Training

Outline:

  • Create a vocabulary using TorchText
  • Create training & validation sets
  • Create PyTorch dataloaders
from torchtext.data.utils import get_tokenizer
tokenizer = get_tokenizer('basic_english')
sample_comment = raw_df.comment_text.values[0]
sample_comment
"Explanation\nWhy the edits made under my username Hardcore Metallica Fan were reverted? They weren't vandalisms, just closure on some GAs after I voted at New York Dolls FAC. And please don't remove the template from the talk page since I'm retired now.89.205.38.27"
sample_comment_tokens = tokenizer(sample_comment)
sample_comment_tokens[:10]
['explanation',
 'why',
 'the',
 'edits',
 'made',
 'under',
 'my',
 'username',
 'hardcore',
 'metallica']
from torchtext.vocab import build_vocab_from_iterator
comment_tokens = raw_df.comment_text.map(tokenizer)
VOCAB_SIZE = 1500
unk_token = '<unk>'
pad_token = '<pad>'
vocab = build_vocab_from_iterator(comment_tokens, 
                                  specials=[unk_token, pad_token], 
                                  max_tokens=VOCAB_SIZE)
vocab[unk_token]
0
vocab.set_default_index(vocab[unk_token])
vocab['this']
18
vocab['harcore']
0
sample_indices = vocab.lookup_indices(sample_comment_tokens)
sample_indices[:10]
[667, 85, 3, 140, 142, 185, 39, 663, 0, 0]
sample_comment_recovered = vocab.lookup_tokens(sample_indices)
sample_comment_recovered[:10]
['explanation',
 'why',
 'the',
 'edits',
 'made',
 'under',
 'my',
 'username',
 '<unk>',
 '<unk>']

Create Training & Validation Sets

  • Define a custom Pytorch Dataset
  • Pass raw data into the dataset
  • Split the PyTorch Dataset
raw_df.comment_text.sample(1000).map(tokenizer).map(len).plot(kind='hist')
<matplotlib.axes._subplots.AxesSubplot at 0x7f9253909cd0>
Notebook output
MAX_LENGTH = 150
def pad_tokens(tokens):
    if (len(tokens) >= MAX_LENGTH):
        return tokens[:MAX_LENGTH]
    else:
        return tokens + [pad_token] * (MAX_LENGTH - len(tokens))
import torch
from torch.utils.data import Dataset
class JigsawDataset(Dataset):
    def __init__(self, df, is_test=False):
        self.df = df
        self.is_test = is_test

    def __getitem__(self, index):
        comment_text = self.df.comment_text.values[index]
        comment_tokens = pad_tokens(tokenizer(comment_text))
        input = torch.tensor(vocab.lookup_indices(comment_tokens))
        if self.is_test:
            target = torch.tensor([0,0,0,0,0,0]).float()
        else:
            target = torch.tensor(self.df[target_cols].values[index]).float()
        return input, target

    def __len__(self):
        return len(self.df)
raw_ds = JigsawDataset(raw_df)
raw_df.head(5)
raw_ds[0]
(tensor([ 667,   85,    3,  140,  142,  185,   39,  663,    0,    0, 1287,   96,
          329,   27,   57,    0,    9,   31,    0,    4,   61,    0,   21,   71,
            0,  158,    6,    0,   45,  130, 1167,    0,    0,    2,    8,   55,
           70,    9,   31,  253,    3,  369,   41,    3,   50,   38,  155,    6,
            9,   81,    0,   99,    2,    0,    2,    0,    2,    0,    2, 1122,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1]),
 tensor([0., 0., 0., 0., 0., 0.]))
from torch.utils.data import random_split
VAL_FRAC = 0.25
train_ds, val_ds = random_split(raw_ds, [1-VAL_FRAC, VAL_FRAC])
len(raw_ds), len(train_ds), len(val_ds)
(159571, 119679, 39892)
test_ds = JigsawDataset(test_df, is_test=True)
test_df.head(5)
test_ds[0]
(tensor([   0,  636,    0,  649,   12,   69,    0,   93,   10,    9,  169,  359,
           23,    0,   89,   30,   10,    8,    0,   10, 1488,    0,    2,    2,
            2,    6,   68,  636,    0,    0,    0,  690,    0,    8,  105,   10,
            5,    0,   39,  419,   10,  699,    0,   46,    2,    0,  649,   12,
           47,    0,   15,    0,  668,  439,    2,  443,    0,   13,  332,   21,
          167,    2,    8,    0,   12,  251,    0,   62,    0,   59,   34,   11,
            0,  171,    2,    2,    2,    0,  690,    0,  105,  226,  160,  490,
           95,    2,    4,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1]),
 tensor([0., 0., 0., 0., 0., 0.]))
from torch.utils.data import DataLoader
BATCH_SIZE = 256
train_dl = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=8, pin_memory=True)
val_dl = DataLoader(val_ds, batch_size=BATCH_SIZE*2, num_workers=8, pin_memory=True)
test_dl = DataLoader(test_ds, batch_size=BATCH_SIZE*2, num_workers=8, pin_memory=True)
for batch in train_dl:
    b_inputs, b_targets = batch
    print('b_input.shape', b_inputs.shape)
    print('b_targets.shape', b_targets.shape)

    break
b_input.shape torch.Size([256, 150]) b_targets.shape torch.Size([256, 6])

Build a Recurrent Neural Network

Outline:

  1. Understand how recurrent neural networks work
  2. Create a recurrent neural network
  3. Pass some through the network
import torch.nn as nn
import torch.functional as F
vocab[pad_token]
1
emb_layer = nn.Embedding(VOCAB_SIZE, 256, 1)
rnn_layer = nn.RNN(256, 128, 1, batch_first=True)
for batch in train_dl:
    b_inputs, b_targets = batch
    print('b_input.shape', b_inputs.shape)
    print('b_targets.shape', b_targets.shape)

    emb_out = emb_layer(b_inputs)
    print('emb_out.shape', emb_out.shape)

    rnn_out, hn = rnn_layer(emb_out)
    print('rnn_out.shape', rnn_out.shape)
    print('hn.shape', hn.shape)

    break
b_input.shape torch.Size([256, 150]) b_targets.shape torch.Size([256, 6]) emb_out.shape torch.Size([256, 150, 256]) rnn_out.shape torch.Size([256, 150, 128]) hn.shape torch.Size([1, 256, 128])
!pip install pytorch_lightning --quiet
|████████████████████████████████| 800 kB 4.6 MB/s |████████████████████████████████| 125 kB 83.0 MB/s |████████████████████████████████| 512 kB 79.4 MB/s
import pytorch_lightning as pl
import torch.nn.functional as F
import numpy as np
class JigsawModel(pl.LightningModule):
    def __init__(self):
        super().__init__()
        self.emb = nn.Embedding(VOCAB_SIZE, 256, 1)
        self.lstm = nn.LSTM(256, 128, 1, batch_first=True)
        self.linear = nn.Linear(128, 6)
        self.learning_rate = 0.001

    def forward(self, x):
        out = self.emb(x)
        out, hn = self.lstm(out)
        out = F.relu(out[:,-1,:])
        out = self.linear(out)
        return out

    def training_step(self, batch, batch_idx):
        inputs, targets = batch
        outputs = self(inputs)
        probs = torch.sigmoid(outputs)
        loss = F.binary_cross_entropy(probs, targets)
        return loss

    def validation_step(self, batch, batch_idx):
        inputs, targets = batch
        outputs = self(inputs)
        probs = torch.sigmoid(outputs)
        loss = F.binary_cross_entropy(probs, targets)
        return loss.item()

    def validation_epoch_end(self, validation_step_outputs):
        loss = np.mean(validation_step_outputs)
        print("Epoch #{}; Loss: {:4f} ".format(self.current_epoch, loss)) 

    def predict_step(self, batch, batch_idx):
        inputs, targets = batch
        outputs = self(inputs)
        probs = torch.sigmoid(outputs)
        return probs       

    def configure_optimizers(self):
        return torch.optim.Adam(self.parameters(), lr=self.learning_rate)
model = JigsawModel()
for batch in train_dl:
    b_inputs, b_targets = batch
    print('b_input.shape', b_inputs.shape)
    print('b_targets.shape', b_targets.shape)

    outputs = model(b_inputs)
    print('outputs.shape', outputs.shape)

    probs = torch.sigmoid(outputs)
    loss = F.binary_cross_entropy(probs, b_targets)
    print('Loss', loss)
    break
b_input.shape torch.Size([256, 150]) b_targets.shape torch.Size([256, 6]) outputs.shape torch.Size([256, 6]) Loss tensor(0.6917, grad_fn=<BinaryCrossEntropyBackward0>)
trainer = pl.Trainer(max_epochs=3, accelerator='gpu', auto_lr_find=True)
INFO:pytorch_lightning.utilities.rank_zero:GPU available: True (cuda), used: True INFO:pytorch_lightning.utilities.rank_zero:TPU available: False, using: 0 TPU cores INFO:pytorch_lightning.utilities.rank_zero:IPU available: False, using: 0 IPUs INFO:pytorch_lightning.utilities.rank_zero:HPU available: False, using: 0 HPUs
trainer.tune(model, train_dl)
/usr/local/lib/python3.8/dist-packages/pytorch_lightning/trainer/configuration_validator.py:108: PossibleUserWarning: You defined a `validation_step` but have no `val_dataloader`. Skipping val loop. rank_zero_warn( INFO:pytorch_lightning.accelerators.cuda:LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]
Finding best initial lr:   0%|          | 0/100 [00:00<?, ?it/s]
INFO:pytorch_lightning.utilities.rank_zero:`Trainer.fit` stopped: `max_steps=100` reached. INFO:pytorch_lightning.tuner.lr_finder:Learning rate set to 0.00478630092322638 INFO:pytorch_lightning.utilities.rank_zero:Restoring states from the checkpoint path at /content/.lr_find_50b3f325-e61c-4318-8d53-dfd48c1ccde0.ckpt INFO:pytorch_lightning.utilities.rank_zero:Restored all states from the checkpoint file at /content/.lr_find_50b3f325-e61c-4318-8d53-dfd48c1ccde0.ckpt
{'lr_find': <pytorch_lightning.tuner.lr_finder._LRFinder at 0x7f9249b40550>}
model.learning_rate
0.00478630092322638
trainer.fit(model, train_dl, val_dl)
INFO:pytorch_lightning.accelerators.cuda:LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] INFO:pytorch_lightning.callbacks.model_summary: | Name | Type | Params ------------------------------------- 0 | emb | Embedding | 384 K 1 | lstm | LSTM | 197 K 2 | linear | Linear | 774 ------------------------------------- 582 K Trainable params 0 Non-trainable params 582 K Total params 2.330 Total estimated model params size (MB)
Sanity Checking: 0it [00:00, ?it/s]
Epoch #0; Loss: 0.691875
Training: 0it [00:00, ?it/s]
Validation: 0it [00:00, ?it/s]
Epoch #0; Loss: 0.073076
Validation: 0it [00:00, ?it/s]
Epoch #1; Loss: 0.065023
Validation: 0it [00:00, ?it/s]
INFO:pytorch_lightning.utilities.rank_zero:`Trainer.fit` stopped: `max_epochs=3` reached.
Epoch #2; Loss: 0.064172
test_df.head(5)
for batch in test_dl:
    b_inputs, b_targets = batch
    print('b_inputs.shape', b_inputs.shape)
    print('b_targets.shape', b_targets.shape)
    break
b_inputs.shape torch.Size([512, 150]) b_targets.shape torch.Size([512, 6])
test_preds = trainer.predict(model, test_dl)
INFO:pytorch_lightning.accelerators.cuda:LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]
Predicting: 468it [00:00, ?it/s]
test_preds = torch.cat(test_preds)
test_preds.shape
torch.Size([153164, 6])
test_preds
tensor([[9.8514e-01, 3.6960e-01, 9.3268e-01, 4.5492e-02, 8.6001e-01, 2.4571e-01],
        [6.6544e-03, 9.5455e-05, 1.4566e-03, 2.8526e-04, 2.3743e-03, 8.5336e-04],
        [9.5926e-03, 1.2386e-04, 2.3447e-03, 3.2115e-04, 2.8670e-03, 9.5028e-04],
        ...,
        [6.3757e-03, 1.0199e-04, 1.4886e-03, 3.1001e-04, 2.3857e-03, 8.5090e-04],
        [3.4499e-03, 5.9353e-05, 9.4897e-04, 1.3394e-04, 1.1450e-03, 4.0778e-04],
        [6.7744e-01, 2.3407e-02, 2.9260e-01, 1.6682e-02, 3.3222e-01, 6.3561e-02]])
test_probs = torch.sigmoid(test_preds)
sub_df[target_cols] = test_probs.detach().cpu().numpy()
sub_df
sub_df.to_csv('submission.csv', index=None)
!head submission.csv
id,toxic,severe_toxic,obscene,threat,insult,identity_hate 00001cee341fdb12,0.72812605,0.5913613,0.7176182,0.51137114,0.70266306,0.56111944 0000247867823ef7,0.5016636,0.50002384,0.5003641,0.5000713,0.5005936,0.5002134 00013b17ad220c46,0.50239813,0.500031,0.5005862,0.5000803,0.50071675,0.5002376 00017563c3f7919a,0.501505,0.50002253,0.5003276,0.50006527,0.5005442,0.500191 00017695ad8997eb,0.50200754,0.5000269,0.50047714,0.50007606,0.50064564,0.5002039 0001ea8717f6de06,0.50149107,0.50002205,0.50032103,0.50006306,0.5005344,0.50018644 00024115d4cbde0f,0.5014186,0.5000208,0.50028896,0.5000587,0.500487,0.5001697 000247e83dcc1211,0.5343304,0.50087047,0.5057776,0.50117475,0.5125339,0.503937 00025358d4737918,0.50083727,0.5000131,0.50024486,0.5000303,0.50024164,0.5000872
sample_df = raw_df.sample(10)
sample_df
sample_ds = JigsawDataset(sample_df)
sample_dl = DataLoader(sample_ds, batch_size=10)
sample_preds = trainer.predict(model, sample_dl)
INFO:pytorch_lightning.accelerators.cuda:LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0] /usr/local/lib/python3.8/dist-packages/pytorch_lightning/trainer/connectors/data_connector.py:224: PossibleUserWarning: The dataloader, predict_dataloader 0, does not have many workers which may be a bottleneck. Consider increasing the value of the `num_workers` argument` (try 12 which is the number of cpus on this machine) in the `DataLoader` init to improve performance. rank_zero_warn(
Predicting: 468it [00:00, ?it/s]
sample_probs = torch.sigmoid(sample_preds[0])
sample_probs
tensor([[0.5018, 0.5001, 0.5005, 0.5000, 0.5004, 0.5002],
        [0.5020, 0.5000, 0.5004, 0.5001, 0.5008, 0.5003],
        [0.5018, 0.5000, 0.5004, 0.5001, 0.5006, 0.5002],
        [0.5084, 0.5001, 0.5016, 0.5002, 0.5029, 0.5008],
        [0.5015, 0.5000, 0.5003, 0.5001, 0.5006, 0.5002],
        [0.5408, 0.5008, 0.5047, 0.5024, 0.5152, 0.5039],
        [0.5027, 0.5001, 0.5006, 0.5001, 0.5006, 0.5005],
        [0.5046, 0.5001, 0.5014, 0.5002, 0.5012, 0.5004],
        [0.7199, 0.5369, 0.6775, 0.5104, 0.6677, 0.5424],
        [0.7088, 0.5169, 0.6377, 0.5067, 0.6428, 0.5278]])
sample_df