# -*- coding: utf-8 -*-
"""NLP BBC News Classification based LSTM Algorithm.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/1mwHTB_1ovU4zILoutv3Dsu8nMi-_QmVY

# NLP BBC News Classification based LSTM Algorithm

Predict emotion from textual data : Multi-class text classification

**Training Accuracy**
- loss: 0.41
- acc: 100.00

**Validation Accuracy**
- loss: 44.98
- acc: 91.01
"""

# install library needed
!pip install -q kaggle
!pip install unidecode
!pip install chart_studio

# import library needed
import nltk
import spacy
import string
from unidecode import unidecode
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize, TweetTokenizer

from tensorflow import keras
from keras.optimizers import Adam
from keras.models import Sequential, Model, load_model
from keras.layers import Input, LSTM, Bidirectional, SpatialDropout1D, GlobalMaxPooling1D, Dropout, Flatten, Dense, Embedding, BatchNormalization
from keras.callbacks import ModelCheckpoint, EarlyStopping, CSVLogger
from keras.preprocessing.text import Tokenizer, text_to_word_sequence
from keras.preprocessing.sequence import sequence, pad_sequences
from keras.utils.vis_utils import plot_model

from google.colab import files
from google.colab import drive
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score, precision_score, recall_score, f1_score
import seaborn as sn
import numpy as np
import pandas as pd

import os
import re
import time
import uuid
import warnings
from IPython.display import Markdown
from IPython.display import display
from slugify import slugify
import cufflinks as cf
import plotly.io as pio

# ignore the unwanted warning messages
warnings.filterwarnings('ignore')
# enambling iplot
cf.go_offline()
pio.renderers.default = 'colab'

# downloading nltk resource
nltk.download('wordnet')
nltk.download('stopwords')

# Define base path for worksheet
BASE_PATH = '/content/drive/MyDrive/Playground/BBCNews'
# No. of class
NUM_CLASS=5
# No. of number of samples that will be propagated through the network
BATCH_SIZE=128
# Learning rate
LEARNING_RATE=0.001
# Number of iterations
EPOCH=50

# mount our google drive
drive.mount('/content/drive')

"""## Helper"""

# Print in markdown format
def printmd(string):
    display(Markdown(string))

# Plot training loss and accuracy
def plot_training(history, title, save=True, save_path=os.path.join(BASE_PATH, 'visualization')):
    printmd("### Training Loss and Accuracy")
    
    acc = history.history['accuracy']
    val_acc = history.history['val_accuracy']
    loss = history.history['loss']
    val_loss = history.history['val_loss']
    epoch_list = range(1, len(acc) + 1)
    
    plt.figure(figsize=(8, 6))
    plt.plot(loss, label='Training Loss')
    plt.plot(val_loss, label='Validation Loss')
    plt.legend(loc=0, fontsize='large')
    plt.xlabel("Epoch #", fontsize=14)
    plt.ylabel("Loss", fontsize=14)
    plt.show()
    
    if save is True:
        filename = slugify(title) + '_loss_model.png'
        plt.savefig(save_path + '/' + filename)

    plt.figure(figsize=(8, 6))
    plt.plot(acc, label='Training Accuracy')
    plt.plot(val_acc, label='Validation Accuracy')
    plt.legend(loc=4, fontsize='large')
    plt.xlabel("Epoch #", fontsize=14)
    plt.ylabel("Accuracy", fontsize=14)
    plt.show()
    
    if save is True:
        filename = slugify(title) + '_acc_model.png'
        plt.savefig(save_path + '/' + filename)

# Table classification report
def table_classification_report(classes, preds):
    printmd('#### Classification Report')
    target_names = ['business', 'entertainment', 'politics', 'sport', 'tech']
    print(classification_report(classes, preds, target_names=target_names))

    print('Accuracy: {:.2f}\n'.format(accuracy_score(classes, preds)))

    print('Macro Precision: {:.2f}'.format(precision_score(classes, preds, average='macro')))
    print('Macro Recall: {:.2f}'.format(recall_score(classes, preds, average='macro')))
    print('Macro F1-score: {:.2f}\n'.format(f1_score(classes, preds, average='macro')))

    print('Weighted Precision: {:.2f}'.format(precision_score(classes, preds, average='weighted')))
    print('Weighted Recall: {:.2f}'.format(recall_score(classes, preds, average='weighted')))
    print('Weighted F1-score: {:.2f}\n'.format(f1_score(classes, preds, average='weighted')))

# Evaluate model, confusion matrix and classification report
def model_evaluation(title, model, x_train, x_test, y_train, y_test, save=True, save_path=os.path.join(BASE_PATH, 'visualization')):
    printmd('### Training Accuracy')
    loss, val_acc = model.evaluate(x_train, y_train, verbose=1)

    print("loss: %.2f" % (loss*100))
    print("acc: %.2f" % (val_acc*100))
    
    printmd('### Validation Accuracy')
    loss, val_acc = model.evaluate(x_test, y_test, verbose=1)

    print("loss: %.2f" % (loss*100))
    print("acc: %.2f" % (val_acc*100))
    
    printmd('### Evaluation of Model Performance')
    y_pred = model.predict(x_test, verbose=1)
    
    # Table classification report
    table_classification_report(y_test, y_pred.round())

"""## Preprocessing

### Data Gatering
"""

# upload kaggle.json
files.upload()

# make directory and change permission
!mkdir -p ~/.kaggle && cp kaggle.json ~/.kaggle/ && chmod 600 ~/.kaggle/kaggle.json && ls ~/.kaggle

# download the dataset
!kaggle datasets download -d yufengdev/bbc-fulltext-and-category

# unziping dataset and move to playground
!unzip bbc-fulltext-and-category.zip -d /content/drive/MyDrive/Playground/BBCNews/

# remove downloaded dataset
!rm -rf bbc-fulltext-and-category.zip

"""### Load Dataset"""

# load dataset
dataset = os.path.join(BASE_PATH, 'bbc-text.csv')
df = pd.read_csv(dataset, sep=',')
print('Dataset loaded ...')

"""### Data Exploration"""

# get the first rows on dataset
df.head()

# total data
df.count()

# data info
df.info()

# simple metric study
df['category'].describe()

# print distribution of category/class
df.groupby('category').count()['text'].iplot(
    kind='bar',
    xTitle='category',
    linecolor='black',
    yTitle='count',
    title='Category/Class Distribution')

"""### Data Cleaning & Normalization"""

PUNCT_TO_REMOVE = string.punctuation
STOPWORDS = stopwords.words("english")

def preprocess(text):
  # convert text to lower
  text = text.lower()
  # remove username (using @), URLs and hastag
  text = re.sub("@\S+|http\S+|#\S+", "", text)
  # get rid of non words and extra spaces
  text = re.sub('\\W', ' ', text)
  text = re.sub('(?<=[a-z])\'(?=[a-z])', '', text)
  text = re.sub("(?<=[a-z])'(?=[a-z])", "", text)
  text = re.sub('\n', '', text)
  text = re.sub(' +', ' ', text)
  text = re.sub('^ ', '', text)
  text = re.sub(' $', '', text)
  # remove punctuactions
  text.translate(str.maketrans('', '', PUNCT_TO_REMOVE))
  # convert into ascii
  text = unidecode(text)
  # tokenized
  tokenized = word_tokenize(text)
  # remove stopwords
  remove_stopwords = [token for token in tokenized if token.isalnum() and token not in STOPWORDS]
  # clean_token = [token for token in tokenized if token.isalnum()]
  clean_string = " ".join(remove_stopwords)
  return clean_string

df["text_preprocessed"] = df["text"].apply(lambda text: preprocess(text))
df.head()

# data category one-hot-encoding
category = pd.get_dummies(df.category)
df_category = pd.concat([df, category], axis=1)
df_category = df_category.drop(columns='category')
df_category.head()

# change dataframe value to numpy array
text_preprocessed = df_category['text_preprocessed'].values
label = df_category[['business', 'entertainment', 'politics', 'sport', 'tech']].values

# view text preprocessed array
text_preprocessed

# view label array
label

"""### Data Spliting"""

# split data into training and validation
x_train, x_test, y_train, y_test = train_test_split(text_preprocessed, label, test_size=0.2, shuffle=True)

"""### Tokenizing and Pad Sequence"""

# tokinizer
tokenizer = Tokenizer()
# preparing vocubulary
tokenizer.fit_on_texts(list(x_train))

# converting text into integer sequences
sekuens_train = tokenizer.texts_to_sequences(x_train)
sekuens_test = tokenizer.texts_to_sequences(x_test)

# padding to prepare sequences of same length
padded_train = pad_sequences(sekuens_train, maxlen=100)
padded_test = pad_sequences(sekuens_test, maxlen=100)

# get size of vocabulary for embedding input dimension
size_of_vocabulary=len(tokenizer.word_index) + 1 #+1 for padding
print('Size of vocabulary:', size_of_vocabulary)

"""## Modeling"""

def ModelLSTM():
  model = Sequential()

  model.add(Embedding(input_dim=size_of_vocabulary, output_dim=100, input_length=100))
  model.add(SpatialDropout1D(0.2))
  model.add(Bidirectional(LSTM(128, dropout=0.2, recurrent_dropout=0.2, return_sequences = True)))
  model.add(Bidirectional(LSTM(32)))
  model.add(Dense(64, activation = "relu"))
  model.add(Dense(NUM_CLASS, activation = "softmax"))

  optimizer = Adam()
  model.compile(optimizer=optimizer,
                loss='categorical_crossentropy',
                metrics=['accuracy'])

  return model

# Visualize LSTM model
plot_model(ModelLSTM(), to_file=os.path.join(BASE_PATH, 'visualization/LSTM.png'), show_shapes=True)

"""## Training"""

# Adding callbacks
train_id = 'LSTM-' + str(uuid.uuid1())
best_model = os.path.join(BASE_PATH, 'model/best_model-' + train_id + '.h5')
last_model = os.path.join(BASE_PATH, 'model/last_model-' + train_id + '.h5')
csv_log = os.path.join(BASE_PATH, 'monitor/logs-' + train_id + '.h5')

ES = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=5)
MC = ModelCheckpoint(best_model, monitor='val_accuracy', mode='max', save_best_only=True, verbose=1)
CSVL = CSVLogger(csv_log, separator=",", append=True)

callbacks = [MC, ES, CSVL]

# Training start time
start_time = time.time()

print("Training strated with Train ID: {}".format(train_id))

model = LSTMv1()
history = model.fit(np.array(padded_train), np.array(y_train),
                  validation_data=(np.array(padded_test),np.array(y_test)),
                  batch_size=BATCH_SIZE,
                  epochs=EPOCH,
                  verbose=1,
                  callbacks=callbacks)

# Training complete
time_elapsed = time.time() - start_time
# Save last model
model.save(last_model)

print("=" * 30)
print("Training Finished, took {:.0f}m {:.0f}s".format(time_elapsed // 60, time_elapsed % 60))
print("Best model:", best_model)
print("Last model:", last_model)

# Plot training and loss accuracy
plot_training(history, train_id)

# Load best model
print("Load best model to evaluate ...")
model = load_model(best_model)
model_evaluation(train_id, model, padded_train, padded_test, y_train, y_test)

"""## Prediction"""

def prediction(model, string):
  seq_str = tokenizer.texts_to_sequences(string)
  enc_str = sequence.pad_sequences(seq_str, maxlen=100)
  pred = model.predict_classes(enc_str)

  news_class = ['business', 'entertainment', 'politics', 'sport', 'tech']
  label = news_class[pred[0]]

  print('String:', string)
  print('Prediction:', label)

# try prediction from https://www.bbc.com/sport/rugby-union/57771397 (Published date: 10 Jul 2021)
string = ["Pumas full-back Juan Cruz Mallia was shown a red card after 29 minutes for a reckless tackle on Kieran Hardy. The Pumas were trailing 6-3 at the time, but rallied through tries from Pablo Matera and Jeronimo De La Fuente and the boot of Nicolas Sanchez. Tries from Will Rowlands and Tomos Williams helped Wales level the scores. Evans lined up the long-range effort in the final minute, but it went wide after Argentina had also missed three penalty attempts. The two sides will meet in the second Test in Cardiff next Saturday (15:00 BST). Wales were indebted to the 47th-minute introduction of replacement half-backs Tomos Williams and Evans, who helped change the game's complexion after Argentina had dominated the contact area."]

prediction(model=model, string=string)