import pandas as pd
import numpy as np
import os

%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('ggplot')

sentences = pd.read_csv(
    'imdb_labelled.txt', sep='\|', engine='python',
    header=None, names=['sentence', 'sentiment_label']
)

sentences.head()

"""
Explanation: Sentence Level Sentiment Predictator using Word Embeddings
Generate Word Embedding
In the first section, we read sentiment labelled sentences from IMDB reviews and generate word embeddings.
End of explanation
"""


sentences.sentiment_label.unique()

"""
Explanation: These are the unique sentiment labels:
End of explanation
"""


sentences.sentiment_label.value_counts().plot(kind='bar', title='Samples per label')

"""
Explanation: Let's check the label distribution
End of explanation
"""


sentences.shape

"""
Explanation: Thus this dataset is almost balanced - so we don't have to worry about the class imbalance problem too much.
End of explanation
"""


from nltk.tokenize import word_tokenize

sentences = sentences.assign(word_list=sentences.sentence.apply(word_tokenize))
sentences = sentences.drop('sentence', axis=1)

sentences.head()

"""
Explanation: Each sentence is now a single string. Next we tokenize each sentences into a list of strings.
End of explanation
"""


from gensim.models.word2vec import Word2Vec

embeddings = Word2Vec(
    sentences.word_list.tolist(),
    # use skip-gram model,
    sg=1,
    # size of the embedding dimension
    size=10,
    # size of the context window    
    window=3,
    # for consistency, this affects the initial random embeddings
    seed=1234,
    # ignore words that appear less than this
    min_count=0,
    # Use negative sampling instead of hierarchical softmax
    hs=0
)

embedding_map = {w: embeddings.wv[w] for w in embeddings.wv.vocab}
embedding_df = pd.DataFrame.from_dict(embedding_map, orient='index')

embedding_df.head()

"""
Explanation: Now we'll use gensim to build the word embeddings and then convert the result into a DataFrame. The index of the dataframe are the words (vocabulary) in our dataset and each row contains the embedding vector.
End of explanation
"""


def average_embedding(list_of_words, embeddings):
    """Return the average of the embedding vectors in `embeddings` for all the words in `list_of_words`. 
    If a word does not exist in `embeddings`, consider it's embeddings to be all zeros."""
    
    # The fillna(0) handles the missing word case 
    return embeddings.reindex(list_of_words, axis=0).fillna(0).mean(axis=0)

def sentence_embeddings(sents, embeddings):
    """Calculate average embeddings for each sentence."""
    sentence_embeddings = sents.word_list.apply(lambda word_list: average_embedding(word_list, embeddings))
    sentence_embeddings = sentence_embeddings.assign(sentiment_label=sents.sentiment_label)
    return sentence_embeddings   

input_df = sentence_embeddings(sentences, embedding_df)

input_df.head()

"""
Explanation: Note that since our original dataset was a mixture of uppercase and lowercase, we have ended up with two different embedding vectors for 'A' and 'a'. This could potentially be a problem  - for now we just make a note of it.
Building the Classifier
In the first pass, we'll create a simple classifier using a Sequential model with one Dense layer followed by an output layer with sigmoid activation. In this classifier, we'll take the average of the embedding vectors for all the words that are in a given sentence (strictly speaking, the words that are common between the vocabulary and the list of words in the sentence, since we've filtered out the rare words in the embedding construction step).
End of explanation
"""


train_df = input_df.groupby('sentiment_label').apply(lambda g: g.sample(frac=0.7, replace=False, random_state=4231))
train_df.index = train_df.index.droplevel(level=0)
train_df.head()

test_df = input_df.drop(train_df.index, axis=0)
test_df.head()

"""
Explanation: Now we split our data into a train and test sets.
End of explanation
"""


counts = pd.concat({
    'train': train_df.sentiment_label.value_counts(), 
    'test': test_df.sentiment_label.value_counts(), 
}, axis=1)

ax = counts.plot(kind='bar', title='Label counts in train and test split')

"""
Explanation: Let's double-check the label distributions in the train and test datasets:
End of explanation
"""


from keras import regularizers, Sequential
from keras.layers import Dense

minibatch_size = 32
# subtract 1 because input_df also contains the label column
embedding_size = input_df.shape[1] - 1

model = Sequential()
model.add(Dense(minibatch_size, 
                activation='relu', 
                input_dim=embedding_size, 
                kernel_regularizer=regularizers.l2(0.01)))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='adam',
              loss='binary_crossentropy',
              metrics=['accuracy'])

model.summary()

n_epochs = 2000
y_train = pd.DataFrame(train_df.sentiment_label).astype(np.int32).values

# Scale the X values to zero mean and unit variance
X_train = train_df.drop('sentiment_label', axis=1)
X_train = X_train.sub(X_train.mean(axis=0)).div(X_train.std(axis=0)).astype(np.float32).values

history = model.fit(X_train, y_train, epochs=n_epochs, batch_size=minibatch_size, verbose=1)

losses = history.history['loss']
ax = pd.Series(losses).rolling(20).mean().plot()

"""
Explanation: Now we're ready to build the model using Keras.
End of explanation
"""


y_test = pd.DataFrame(test_df.sentiment_label).astype(np.int32).values

# Scale the X values to zero mean and unit variance
X_test = test_df.drop('sentiment_label', axis=1)
X_test = X_test.sub(X_test.mean(axis=0)).div(X_test.std(axis=0)).astype(np.float32).values

train_accuracy, test_accuracy = model.evaluate(X_test, y_test)
print('train_accuracy: {:.4f}, test_accuracy: {:.4f}'.format(train_accuracy, test_accuracy))

predictions = model.predict(X_test).squeeze()
predictions = np.where(predictions > 0.5, 1, 0)
pred_series = pd.Series(index=test_df.index.copy(), data=predictions, name='predictions')

results_df = pd.concat({'predictions': pred_series, 'actual': test_df.sentiment_label}, axis=1)
results_df.head()

"""
Explanation: Now let's evaluate our model against the test set.
End of explanation
"""


import seaborn as sns
v = pd.crosstab(results_df.predictions, results_df.actual)
sns.heatmap(v, annot=True)

"""
Explanation: Now we generate the Confusion Matrix.
End of explanation
"""


