import json
from itertools import chain
from pprint import pprint
from time import time
import os

import numpy as np

%matplotlib inline
import matplotlib.pyplot as plt

from sklearn.metrics import accuracy_score

from gensim.models import Word2Vec
from gensim.corpora.dictionary import Dictionary


os.environ['THEANO_FLAGS'] = "device=gpu1"    
import theano
# theano.config.device = 'gpu' # Compute using GPU
# theano.config.floatX = 'float32'

from keras.preprocessing import sequence
from keras.models import Sequential, Model
from keras.layers import Input
from keras.layers.embeddings import Embedding
from keras.layers.recurrent import LSTM
from keras.layers.core import Dense, Dropout
from keras.layers.wrappers import TimeDistributed
from keras.utils.visualize_util import plot

np.random.seed(1337)

print theano.config.device

def indices_to_one_hot_encodings(index, vector_length):
    return [[1, 0] if i == index else [0, 1] for i in xrange(vector_length)]

# Load and process treebank data

treebank_file1 = open('json/OPTA-treebank-0.1.json')
treebank_file2 = open('skladnica_output.json')
treebank = chain(list(json.load(treebank_file1)), list(json.load(treebank_file2)))

X = []
y = []
labels = []
for entry in treebank:
    tree = entry['parsedSent']
    words = []
    sentiment = None
    for index, node in enumerate(tree):
        word = node.split('\t')[1].lower()
        words.append(word)
        if node.split('\t')[10] == 'S':
            sentiment = index
    if sentiment:
        labels.append(words[sentiment])
        X.append(words)
        y.append(indices_to_one_hot_encodings(sentiment, len(words)))

dataset_length = len(X)
slicing_point = int(dataset_length*0.9)

X_train_raw = X[:slicing_point]
y_train_raw = y[:slicing_point]
X_test_raw = X[slicing_point+1:]
y_test_raw = y[slicing_point+1:]

treebank_vocabulary = set(chain(*X))
print len(treebank_vocabulary)

X_train = X_train_raw 
y_train = labels

len(X_train) + len(X_test_raw)

# Przykłady z danych treningowych:

for index in [2, 44, 111, 384, 69]:
    print ' '.join(X_train[index]), '\n', y_train[index], '\n'

"""
Explanation: Głęboka sieć neuronowa w aspektowej analizie wydźwięku
Tomek Korbak
24 maja 2016
Problem
Wytrenować klasyfikator, który dostając na wejściu zdanie języka polskiego, zwróci jego wydźwięk, to znaczy słowo wyrażające opinię.
End of explanation
"""


w2v_model = Word2Vec.load('w2v_allwiki_nkjp300_200.model')

# Import w2v's dictionary to a bag-of-words model
w2v_vocabulary = Dictionary()
w2v_vocabulary.doc2bow(w2v_model.vocab.keys(), allow_update=True)
print w2v_vocabulary.items()[:10]

# Initialize dicts for representing w2v's dictionary as indices and 200-dim vectors
w2indx = {v: k+1 for k, v in w2v_vocabulary.items()}
w2vec = {word: w2v_model[word] for word in w2indx.keys()}

w2v_vocabulary_size = len(w2indx) + 1
w2v_vocabulary_dimension = len(w2vec.values()[0])

def map_treebank_words_to_w2v_indices(treebank_data, w2indx):
    treebank_data_vec = []
    for sentence in treebank_data:
        vectorized_sentence = []
        for word in sentence:
            try:
                vectorized_sentence.append(w2indx[word])
            except KeyError:  # words absent in w2v model will be indexed as 0s
                vectorized_sentence.append(0)
        treebank_data_vec.append(vectorized_sentence)
    return treebank_data_vec 

X_train = map_treebank_words_to_w2v_indices(X_train_raw, w2indx)
X_test = map_treebank_words_to_w2v_indices(X_test_raw, w2indx)

print X_test[4]

# Define numpy weights matrix for embedding layer
embedding_weights = np.zeros((w2v_vocabulary_size , w2v_vocabulary_dimension))
for word, index in w2indx.items():
    embedding_weights[index, :] = w2vec[word]

# max sentence length
max(
    len(max(X_train, key=lambda sentence: len(sentence))),
    len(max(X_test, key=lambda sentence: len(sentence)))
)

# Normalize sequences length to 40 (will be extended with 0s)
sentence_length = 40
X_train = sequence.pad_sequences(X_train, maxlen=sentence_length)
X_test = sequence.pad_sequences(X_test, maxlen=sentence_length)

y_train = sequence.pad_sequences(y_train_raw, maxlen=sentence_length, value=[0, 1])
y_test = sequence.pad_sequences(y_test_raw, maxlen=sentence_length, value=[0, 1])

# print X_train[2]
# print y_train[2]

inputs = Input(shape=(sentence_length,), dtype='int32')

x = Embedding(
    input_dim=w2v_vocabulary_size, 
    output_dim=w2v_vocabulary_dimension,
    input_length=sentence_length,
    mask_zero=True,
    weights=[embedding_weights]
)(inputs)

lstm_out = LSTM(200, return_sequences=True)(x)

regularized_data = Dropout(0.3)(lstm_out)

predictions = TimeDistributed(Dense(2, activation='sigmoid'))(regularized_data)

model = Model(input=inputs, output=predictions)


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

"""
Explanation: Dane pochodzą z ręcznie tagowanego treebanku (korpusu anotowanego składniowo) opracowanego przez Zespół Inżynierii Lingwistycznej IPI PAN na bazie Narodowego Korpusu Języka Polskiego (Wawer, 2015).
Treebank liczy około 1431 zdania. (To dość mało).
End of explanation
"""


model.summary()

from IPython.display import SVG
from keras.utils.visualize_util import model_to_dot

SVG(model_to_dot(model).create(prog='dot', format='svg'))

"""
Explanation: Architektura sieci
Zasadniczą rolę odegrają dwie warstwy, same będące pełnoprawnymi sieciami neuronowymi:
* Warstwa embedding, mapująca słowa na wektory liczb zmiennoprzecinkowych w sposób spełniający pewne kryteria
* Warstwa LSTM, sieć rekurencyjna szczególnie dobrze radzącą sobie z przetwarzaniem szeregów czasowych
End of explanation
"""


# w modelu, który wykorzystaliśmy, słowa są reprezentowane jako
# 200-elementowe wektory 32-bitowych liczb zmiennoprzecinkowych
w2v_model['filozofia']

w2v_model['filozofia'].shape

w2v_model.similarity(u'filozofia', u'inżynieria')

w2v_model.similarity(u'filozofia', u'nauka')

w2v_model.similarity(u'filozofia', u'literatura')

# wskaż słowo niepasujące do pozostałych
w2v_model.doesnt_match(['Kant', 'Leibniz', 'Derrida', 'Wittgenstein'])

# Kobieta + król - mężczyzna = królowa
# Medialny przykład z (Mikolov et al., 2013b)
w2v_model.most_similar(positive=[u'kobieta', u'król'], negative=[u'mężczyzna'])

# Paryż - Francja + Polska = Warszawa
w2v_model.most_similar(positive=[u'Paryż', u'Polska'], negative=[u'Francja'])

# filozofia - logika = literatura
w2v_model.most_similar(positive=[u'filozofia',], negative=[u'logika'])

# filozofia - postmodernizm = wiedza
w2v_model.most_similar(positive=[u'filozofia',], negative=[u'postmodernizm'])

"""
Explanation: Word embeddings
Word embedding to rodzaj statystycznego modelu językowego, który reprezentuje słowa (rzadziej złożone frazy lub całe dokumenty) jako punkty w n-wymiarowej przestrzeni liniowej.
Bardzo pożądaną cechą tego mapowania jest to, że relacje geometryczne między punktami tej przestrzeni odwzorowują relacje semantyczne między kodowanymi słowami.
Model taki trenuje się na bardzo dużych korpusach, każąc mu rozpoznawać wzorce współwystępowania słów. Najczęściej używanym algorytmem jest Word2Vec, opracowany przez Google (Mikolov et al., 2013a)
Skorzystaliśmy z gotowego embeddingu opracowanego przez IPI PAN, wytrenowanego (z użyciem Word2Vec) na całej polskojęzycznej Wikipedii oraz trzystumilionowym zbalansowanym podkorpusie NKJP.
End of explanation
"""


batch_size = 5
n_epoch = 5


hist = model.fit(X_train, y_train, batch_size=batch_size, nb_epoch=n_epoch, 
                 validation_data=(X_test, y_test), verbose=2)

# epochs = 10

# for i in range(epochs):
#     print('Epoch', i, '/', epochs)
#     model.fit

"""
Explanation: Różnica wektorów „Paryż” i „Francja” reprezentuje pojęcie STOLICA?
<img src="capitals.png">
<center>Rzut (przez analizę składowych głównych) 1000-wymiarowego word embeddingu dla języka angielskiego. Przedruk z (Mikolov et al., 2013).</center>
Warstwa LSTM
Long Short-Term Memory to bardzo popularna architektura rekurencyjnych sieci neuronowych, często używana do etykietowania lub predykcji szeregów czasowych (Hochreiter i Schmidhuber, 1997).
LSTM, dzięki połączeniom rekurencyjnym, utrzymuje coś w rodzaju pamięci roboczej, którą w każdej iteracji może aktualizować.
Zdolność do zapamiętywania odległych zależności (long-term dependecies), takich jak związek zgody, czyni ją najpopularniejszą architekturą do przetwarzania języka naturalnego.
Przetwarzanie danych sekwencyjnych przez rekurencyjną sieć neuronową
Przy n-tej iteracji sieć dostaje na wejście n-ty element ciągu uczącego oraz pewien wektor zapamiętany z (n-1)-tej iteracji.
<img src="RNN-unrolled.png"> <br />
<center><small>Przedruk z http://colah.github.io/posts/2015-08-Understanding-LSTMs/</small></center>
Przy każdej iteracji sieć decyduje, która informacje usunąć z pamięci roboczej, a które od niej dodać. Reguły aktualizacji pamięci roboczej (jako macierz wag połączeń) także podlegają uczeniu.
<img src="LSTM3-chain.png"> <br />
<center><small>Przedruk z http://colah.github.io/posts/2015-08-Understanding-LSTMs/</small></center>
Przepływ danych przez sieć
<img align="right" src="model.png">
Przykład | Opis
--- | ---
'Kotek' | token
 89762 | indeks tokenu w modelu w2v
array([ 0.21601944, ..., dtype=float32) | 200-elementowy wektor
 ...kolejne wektory... | dalsze etapy przetwarzania
 [0.9111, 0.0999] | zero-jedynkowy rozkład prawdopodobieństwa przynależności do klasy wydźwięk lub nie-wydźwięk
End of explanation
"""


# plt.rcParams['figure.figsize'] = (10,10)

# axes = plt.gca()
# x_min = hist.epoch[0]
# x_max = hist.epoch[-1]+1
# axes.set_xlim([x_min,x_max])

# plt.scatter(hist.epoch, hist.history['acc'], color='r')
# plt.plot(hist.history['acc'], color='r', label=u'Trafność mierzona na zbiorze treningowym')
# plt.scatter(hist.epoch, hist.history['val_acc'], color='c')
# plt.plot(hist.history['val_acc'], color='c', label=u'Trafność mierzona na zbiorze walidacyjnym')
# plt.xlabel('epoki')
# plt.ylabel(u'Trafność')
# plt.title(u'Trafność w kolejnych epokach')
# plt.legend()
# plt.show()

"""
Explanation: Uczenie
<img src="plot.png">
End of explanation
"""


# Ułamek poprawnie sklasyfikowanych tokenów
score, acc = model.evaluate(X_test, y_test, batch_size=batch_size, verbose=0)
print 'Test accuracy:', acc

predictions = model.predict(X_test, verbose=1)

def change_encoding_word(word):
    return 1 if list(np.rint(word)) == [1, 0] else 0

def change_encoding(one_hot_encoded_sentence):
    # Switch from ndarray([[0.88, 0.11], [0.34, 0.98]]) encoding to [1, 0] encoding 
    # and finally index number
    normalized_sentence = []
    for word in one_hot_encoded_sentence:
        normalized_sentence.append(change_encoding_word(word))
    return normalized_sentence

def accurately_evaluated_samples():
    total_accuracy = 0
    for n, sentence in enumerate(predictions):
        index_of_sentiment = np.argmax(change_encoding(sentence))
#         print change_encoding_word(y_test[n][index_of_sentiment])
        total_accuracy += change_encoding_word(y_test[n][index_of_sentiment])
    return total_accuracy
    
      


"""
Explanation: Ocena trafności
Oceny trafności dokonano na 143-zdaniowym podzbiorze treebanku, niewykorzystanym podczas treningu.
End of explanation
"""


# Ułamek tokenów-wydźwięków, które poprawnie rozpoznano jako wydźwięki
float(accurately_evaluated_samples())/y_test.shape[0]

"""
Explanation: Wartość bardzo przeszacowana ze względu na nierównomierną częśtość występowania klas (1:39 dla wydźwięku vs nie-wydźwięku)
Bardziej adekwatna metryka
End of explanation
"""


hist.history

score, acc = model.evaluate(X_test, y_test,
                            batch_size=batch_size)
print('Test score:', score)
print('Test accuracy:', acc)

"""
Explanation: Nie wygląda imponująco, ale...
* Przy zgadywaniu trafność wynosiłaby 0,0625% (= $1/40^2$),
* Maksymalny możliwy wynik oscyluje wokół 80%, bo taka jest średnia zgodność ludzkich anotacji (Ogneva, 2012).
Plany na przyszłość


Zwiększenie trafności przewidywań

Dodanie drugiej (i kolejnych) warstwy LSTM powinno umożliwić budowanie przez sieć bardziej złożonych hierarchicznych reprezentacji zależności w zdaniach
Wydłużenie treningu sieci przy wykonywaniu obliczeń przez GPU lub szybszą maszynę



Rozszerzenie problemu o inne trenowanie innych klasyfikatorów:

Ekstrakcja obiektów, których aspektom przypisywany jest wydźwięk
Klasyfikowanie wydźwięku (pozytywny lub negatywny) dla pary &lt;obiekt, wydźwięk&gt;



Kod źródłowy
Repozytorium jest dostępne pod adresem: https://github.com/tomekkorbak/lstm-for-aspect-based-sentiment-analysis
Użyte oprogramowanie

Keras (wysokopoziomowy wrapper na Theano)
Theano (implementacja sieci i jej treningu)
Scikit-learn (walidacja)
Gensim (model językowy Word2Vec)
Numpy (pomocnicze obliczenia numeryczne)

Bibliografia

Hochreiter, S. i Schmidhuber, J, (1997). Long Short-Term Memory, „Neural Computation”, 9 (8), ss. 1735-1780.
Mikolov, T.,  Sutskever, I.,  Chen, K., Corrado, G., i Dean, J. (2013a). Distributed Representations of Words and Phrases and their Compositionality. „Advances in Neural Information Processing Systems 26”, ss. 3111-3119.
Mikolov, T., Chen, K., Corrado, G. i Dean, J. (2013b). Efficient estimation of word representations in vector space. 
Ogneva, M. (2012). How Companies Can Use Sentiment Analysis to Improve Their Business
Wawer, A. (2015). Towards Domain-Independent Opinion Target Extraction. „IEEE 15th International Conference on Data Mining Workshops”, ss. 1326-1331.

<center>Dziękuję za uwagę</center>
End of explanation
"""


