In [31]:
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
In [2]:
def indices_to_one_hot_encodings(index, vector_length):
return [[1, 0] if i == index else [0, 1] for i in xrange(vector_length)]
In [3]:
# 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)
Out[3]:
In [4]:
# Przykłady z danych treningowych:
for index in [2, 44, 111, 384, 69]:
print ' '.join(X_train[index]), '\n', y_train[index], '\n'
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).
In [5]:
w2v_model = Word2Vec.load('w2v_allwiki_nkjp300_200.model')
In [6]:
# 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]
In [7]:
# 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()}
In [8]:
w2v_vocabulary_size = len(w2indx) + 1
w2v_vocabulary_dimension = len(w2vec.values()[0])
In [9]:
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]
In [10]:
# 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]
In [11]:
# max sentence length
max(
len(max(X_train, key=lambda sentence: len(sentence))),
len(max(X_test, key=lambda sentence: len(sentence)))
)
Out[11]:
In [12]:
# 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]
In [13]:
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'])
Zasadniczą rolę odegrają dwie warstwy, same będące pełnoprawnymi sieciami neuronowymi:
In [14]:
model.summary()
In [15]:
from IPython.display import SVG
from keras.utils.visualize_util import model_to_dot
SVG(model_to_dot(model).create(prog='dot', format='svg'))
Out[15]:
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.
In [16]:
# w modelu, który wykorzystaliśmy, słowa są reprezentowane jako
# 200-elementowe wektory 32-bitowych liczb zmiennoprzecinkowych
w2v_model['filozofia']
Out[16]:
In [17]:
w2v_model['filozofia'].shape
Out[17]:
In [18]:
w2v_model.similarity(u'filozofia', u'inżynieria')
Out[18]:
In [19]:
w2v_model.similarity(u'filozofia', u'nauka')
Out[19]:
In [20]:
w2v_model.similarity(u'filozofia', u'literatura')
Out[20]:
In [21]:
# wskaż słowo niepasujące do pozostałych
w2v_model.doesnt_match(['Kant', 'Leibniz', 'Derrida', 'Wittgenstein'])
Out[21]:
In [22]:
# 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'])
Out[22]:
In [23]:
# Paryż - Francja + Polska = Warszawa
w2v_model.most_similar(positive=[u'Paryż', u'Polska'], negative=[u'Francja'])
Out[23]:
In [24]:
# filozofia - logika = literatura
w2v_model.most_similar(positive=[u'filozofia',], negative=[u'logika'])
Out[24]:
In [25]:
# filozofia - postmodernizm = wiedza
w2v_model.most_similar(positive=[u'filozofia',], negative=[u'postmodernizm'])
Out[25]:
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.
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.
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 |
In [26]:
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
In [5]:
# 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()
In [64]:
# Ułamek poprawnie sklasyfikowanych tokenów
score, acc = model.evaluate(X_test, y_test, batch_size=batch_size, verbose=0)
print 'Test accuracy:', acc
In [27]:
predictions = model.predict(X_test, verbose=1)
In [54]:
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
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)
In [65]:
# Ułamek tokenów-wydźwięków, które poprawnie rozpoznano jako wydźwięki
float(accurately_evaluated_samples())/y_test.shape[0]
Out[65]:
Nie wygląda imponująco, ale...
Zwiększenie trafności przewidywań
Rozszerzenie problemu o inne trenowanie innych klasyfikatorów:
<obiekt, wydźwięk>
Repozytorium jest dostępne pod adresem: https://github.com/tomekkorbak/lstm-for-aspect-based-sentiment-analysis
In [ ]:
In [29]:
hist.history
Out[29]:
In [30]:
score, acc = model.evaluate(X_test, y_test,
batch_size=batch_size)
print('Test score:', score)
print('Test accuracy:', acc)
In [ ]: