2018-06-20 13:38:18 +02:00
# coding: utf-8
# In[1]:
import pandas as pd
from IPython . display import clear_output , Markdown , Math
import ipywidgets as widgets
import os
import glob
import json
import numpy as np
import itertools
import sklearn . utils as sku
from sklearn . feature_extraction . text import CountVectorizer , TfidfVectorizer , HashingVectorizer
from sklearn . model_selection import train_test_split
2018-06-20 18:10:54 +02:00
from sklearn . preprocessing import MultiLabelBinarizer , LabelBinarizer
from sklearn . cluster import KMeans
2018-06-20 13:38:18 +02:00
import nltk
from keras . models import load_model
from sklearn . externals import joblib
import pickle
import operator
from sklearn . pipeline import Pipeline
2018-06-26 14:44:05 +02:00
import json
2018-07-19 16:46:19 +02:00
import datetime
2018-07-21 09:48:25 +02:00
import matplotlib . pyplot as plt
2018-07-19 16:46:19 +02:00
2018-06-20 13:38:18 +02:00
nltk . download ( ' punkt ' )
nltk . download ( ' averaged_perceptron_tagger ' )
nltk . download ( ' wordnet ' )
2018-07-10 15:59:28 +02:00
from keras import losses
2018-06-26 20:07:37 +02:00
# check whether the display function exists:
try :
display
except NameError :
print ( " no fancy display function found... using print instead " )
display = print
2018-06-20 13:38:18 +02:00
# In[2]:
import sys
sys . path . append ( " .. " )
2018-06-20 18:10:54 +02:00
import Tools . Emoji_Distance as edist
2018-07-03 14:20:31 +02:00
import Tools . sklearn_doc2vec as skd2v
2018-06-20 13:38:18 +02:00
def emoji2sent ( emoji_arr , only_emoticons = True ) :
2018-06-20 18:10:54 +02:00
return np . array ( [ edist . emoji_to_sentiment_vector ( e , only_emoticons = only_emoticons ) for e in emoji_arr ] )
2018-06-20 13:38:18 +02:00
def sent2emoji ( sent_arr , custom_target_emojis = None , only_emoticons = True ) :
2018-06-20 18:10:54 +02:00
return [ edist . sentiment_vector_to_emoji ( s , custom_target_emojis = custom_target_emojis , only_emoticons = only_emoticons ) for s in sent_arr ]
2018-06-20 13:38:18 +02:00
# In[3]:
SINGLE_LABEL = True
2018-07-22 10:55:31 +02:00
# top 20 emojis:
top_20 = list ( " 😳😋😀😌😏😔😒😎😢😅😁😉🙌🙏😘😊😩😍😭😂 " )
top_20_sents = emoji2sent ( top_20 )
# plotting function to evaluate stuff:
def sentiment_score ( s ) :
#(pos, neg, neu)^T
return s [ 0 ] - s [ 1 ]
2018-07-23 20:11:24 +02:00
def plot_sentiment_space ( predicted_sentiment_vectors , top_sentiments , top_emojis , style = ' bo ' , additional_patches = None ) :
2018-07-22 10:55:31 +02:00
# sentiment score axis
top_X = np . array ( [ sentiment_score ( x ) for x in top_sentiments ] )
pred_X = np . array ( [ sentiment_score ( x ) for x in predicted_sentiment_vectors ] )
# neutral axis:
top_Y = np . array ( [ x [ 2 ] for x in top_sentiments ] )
pred_Y = np . array ( [ x [ 2 ] for x in predicted_sentiment_vectors ] )
fig_1 , ax_1 = plt . subplots ( ) #figsize=(15,10))
plt . title ( " sentiment-score-plot " )
plt . xlabel ( " sentiment score " )
plt . ylabel ( " neutrality " )
plt . xlim ( [ - 1 , 1 ] )
plt . ylim ( [ 0 , 1 ] )
for i in range ( len ( top_X ) ) :
plt . text ( top_X [ i ] , top_Y [ i ] , top_emojis [ i ] )
2018-07-23 09:23:17 +02:00
plt . plot ( pred_X , pred_Y , style )
2018-07-23 20:11:24 +02:00
for p_tuple in additional_patches :
ax_1 . add_artist ( p_tuple [ 0 ] )
p_tuple [ 0 ] . set_alpha ( 0.4 )
2018-07-22 10:55:31 +02:00
plt . savefig ( " val-error_sentiment-plot " + str ( datetime . datetime . now ( ) ) + " .png " , bbox_inches = ' tight ' )
# sentiment score axis
top_X = np . array ( [ x [ 0 ] for x in top_sentiments ] )
pred_X = np . array ( [ x [ 0 ] for x in predicted_sentiment_vectors ] )
# neutral axis:
top_Y = np . array ( [ x [ 1 ] for x in top_sentiments ] )
pred_Y = np . array ( [ x [ 1 ] for x in predicted_sentiment_vectors ] )
fig_2 , ax_2 = plt . subplots ( ) #figsize=(15,10))
plt . title ( " positive-negative-plot " )
plt . xlabel ( " positive " )
plt . ylabel ( " negative " )
plt . xlim ( [ 0 , 1 ] )
plt . ylim ( [ 0 , 1 ] )
for i in range ( len ( top_X ) ) :
plt . text ( top_X [ i ] , top_Y [ i ] , top_emojis [ i ] )
2018-07-23 09:23:17 +02:00
plt . plot ( pred_X , pred_Y , style )
2018-07-23 20:11:24 +02:00
for p_tuple in additional_patches :
ax_2 . add_artist ( p_tuple [ 1 ] )
p_tuple [ 1 ] . set_alpha ( 0.4 )
2018-07-22 10:55:31 +02:00
plt . savefig ( " val-error_positive-negative-plot " + str ( datetime . datetime . now ( ) ) + " .png " , bbox_inches = ' tight ' )
plt . show ( )
2018-06-20 13:38:18 +02:00
# ----
# ## classes and functions we are using later:
# ----
# * functions for selecting items from a set / list
# In[4]:
def latest ( lst ) :
return lst [ - 1 ] if len ( lst ) > 0 else ' X '
def most_common ( lst ) :
# trying to find the most common used emoji in the given lst
return max ( set ( lst ) , key = lst . count ) if len ( lst ) > 0 else " X " # setting label to 'X' if there is an empty emoji list
# * our emoji blacklist (skin and sex modifiers)
# In[5]:
# defining blacklist for modifier emojis:
emoji_blacklist = set ( [
chr ( 0x1F3FB ) ,
chr ( 0x1F3FC ) ,
chr ( 0x1F3FD ) ,
chr ( 0x1F3FE ) ,
chr ( 0x1F3FF ) ,
chr ( 0x2642 ) ,
chr ( 0x2640 )
] )
# * lemmatization helper functions
# In[6]:
from nltk . stem . snowball import SnowballStemmer
from nltk . stem import WordNetLemmatizer
from nltk import pos_tag
from nltk import word_tokenize
from nltk . corpus import wordnet
def get_wordnet_pos ( treebank_tag ) :
if treebank_tag . startswith ( ' J ' ) :
return wordnet . ADJ
elif treebank_tag . startswith ( ' V ' ) :
return wordnet . VERB
elif treebank_tag . startswith ( ' N ' ) :
return wordnet . NOUN
elif treebank_tag . startswith ( ' R ' ) :
return wordnet . ADV
else :
return wordnet . NOUN
2018-06-26 20:07:37 +02:00
# global stemmer and lemmatizer function
stemmer = SnowballStemmer ( " english " )
def stem ( s ) :
stemmed_sent = [ ]
for word in s . split ( " " ) :
word_stemmed = stemmer . stem ( word )
stemmed_sent . append ( word_stemmed )
stemmed_sent = ( " " ) . join ( stemmed_sent )
return stemmed_sent
lemmatizer = WordNetLemmatizer ( )
def lemm ( s ) :
lemmatized_sent = [ ]
sent_pos = pos_tag ( word_tokenize ( s ) )
for word in sent_pos :
wordnet_pos = get_wordnet_pos ( word [ 1 ] . lower ( ) )
word_lemmatized = lemmatizer . lemmatize ( word [ 0 ] , pos = wordnet_pos )
lemmatized_sent . append ( word_lemmatized )
lemmatized_sent = ( " " ) . join ( lemmatized_sent )
2018-06-26 20:33:55 +02:00
return lemmatized_sent
2018-06-26 20:07:37 +02:00
def batch_stem ( sentences ) :
return [ stem ( s ) for s in sentences ]
def batch_lemm ( sentences ) :
return [ lemm ( s ) for s in sentences ]
2018-06-20 13:38:18 +02:00
# ### sample data manager
# the sample data manager loads and preprocesses data
# most common way to use:
#
#
# * `sdm = sample_data_manager.generate_and_read(path:str, only_emoticons=True, apply_stemming=True, n_top_emojis=-1, file_range=None)`
#
# * Generates a sample_data_manager object and preprocess data in one step
#
# In[7]:
class sample_data_manager ( object ) :
@staticmethod
2018-07-21 10:32:34 +02:00
def generate_and_read ( path : str , only_emoticons = True , apply_stemming = True , n_top_emojis = - 1 , file_range = None , n_kmeans_cluster = - 1 , read_progress_callback = None , stem_progress_callback = None , emoji_mean = False , custom_target_emojis = None , min_words = 0 ) :
2018-06-20 13:38:18 +02:00
"""
generate , read and process train data in one step .
@param path : folder containing json files to process
@param only_emoticons : if True , only messages containing emoticons ( provided by Tools . Emoji_Distance ) are used
@param apply_stemming : apply stemming and lemmatization on dataset
@param n_top_emojis : only use messages containing one of < ` n_top_emojis ` > - top emojis . set to ` - 1 ` to prevent top emoji filtering
@param file_range : range of file ' s indices to read (eg `range(3)` to read the first three files). If `None`: all files are read
2018-06-20 18:10:54 +02:00
@param n_kmeans_cluster : generating multilabeled labels with kmeans with these number of clusters . Set to - 1 to use the plain sentiment space as label
2018-06-20 13:38:18 +02:00
@return : sample_data_manager object
"""
sdm = sample_data_manager ( path )
2018-07-10 15:59:28 +02:00
sdm . read_files ( file_index_range = range ( sdm . n_files ) if file_range is None else file_range , only_emoticons = only_emoticons , progress_callback = read_progress_callback , emoji_mean = emoji_mean )
2018-06-20 13:38:18 +02:00
if apply_stemming :
2018-06-26 16:27:27 +02:00
sdm . apply_stemming_and_lemmatization ( progress_callback = stem_progress_callback )
2018-06-20 13:38:18 +02:00
sdm . generate_emoji_count_and_weights ( )
2018-07-20 12:49:55 +02:00
if custom_target_emojis is not None :
sdm . filter_by_emoji_list ( custom_target_emojis )
elif n_top_emojis > 0 :
2018-06-20 13:38:18 +02:00
sdm . filter_by_top_emojis ( n_top = n_top_emojis )
2018-06-20 18:10:54 +02:00
if n_kmeans_cluster > 0 :
sdm . generate_kmeans_binary_label ( only_emoticons = only_emoticons , n_clusters = n_kmeans_cluster )
2018-07-21 10:32:34 +02:00
if min_words > 0 :
sdm . filter_by_sentence_length ( min_words = min_words )
2018-06-20 13:38:18 +02:00
return sdm
def __init__ ( self , data_root_folder : str ) :
"""
constructor for manual initialization
@param data_root_folder : folder containing json files to process
"""
self . data_root_folder = data_root_folder
self . json_files = sorted ( glob . glob ( self . data_root_folder + " /*.json " ) )
self . n_files = len ( self . json_files )
self . emojis = None
self . plain_text = None
self . labels = None
self . emoji_count = None
self . emoji_weights = None
self . X = None
self . y = None
self . Xt = None
self . yt = None
self . top_emojis = None
2018-06-20 18:10:54 +02:00
self . binary_labels = None
self . use_binary_labels = False
self . kmeans_cluster = None
self . label_binarizer = None
2018-06-26 20:07:37 +02:00
self . use_stemming = False
self . use_lemmatization = False
2018-06-20 13:38:18 +02:00
2018-06-26 16:27:27 +02:00
def read_files ( self , file_index_range : list , only_emoticons = True , emoji_mean = False , progress_callback = None ) :
2018-06-20 13:38:18 +02:00
"""
reading ( multiple ) files to one panda table .
@param file_index_range : range of file ' s indices to read (eg `range(3)` to read the first three files)
@param only_emoticons : if True , only messages containing emoticons ( aka smileys ) are used . This classification is derived from Tools . Emoji_Distance
2018-06-26 16:27:27 +02:00
@param emoji_mean : if True , using mean of all emojis instead of the last one
2018-06-20 13:38:18 +02:00
"""
assert np . min ( file_index_range ) > = 0 and np . max ( file_index_range ) < self . n_files
2018-06-26 16:27:27 +02:00
n = len ( file_index_range )
2018-07-19 16:46:19 +02:00
2018-06-20 13:38:18 +02:00
for i in file_index_range :
print ( " reading file: " + self . json_files [ i ] + " ... " )
2018-06-26 16:27:27 +02:00
raw_data_i = pd . read_json ( self . json_files [ i ] , encoding = " utf-8 " )
emojis_i = raw_data_i [ ' EMOJI ' ]
plain_text_i = raw_data_i [ ' text ' ]
# replacing keywords. TODO: maybe these information can be extracted and used
plain_text_i = plain_text_i . str . replace ( " (<EMOJI>|<USER>|<HASHTAG>) " , " " ) . str . replace ( " [ " + " " . join ( list ( emoji_blacklist ) ) + " ] " , " " )
2018-07-19 15:58:06 +02:00
# filter empty labels
empty_labels = [ ]
for e in emojis_i :
if len ( e ) < 1 :
empty_labels . append ( True )
else :
empty_labels . append ( False )
empty_labels = np . array ( empty_labels , dtype = np . bool_ )
plain_text_i = plain_text_i [ np . invert ( empty_labels ) ]
emojis_i = emojis_i [ np . invert ( empty_labels ) ]
print ( " ignored " + str ( np . sum ( empty_labels ) ) + " empty labels " )
2018-06-26 16:27:27 +02:00
if not emoji_mean :
# so far filtering for the latest emoji. TODO: maybe there are also better approaches
labels_i = emoji2sent ( [ latest ( e ) for e in emojis_i ] , only_emoticons = only_emoticons )
2018-06-20 13:38:18 +02:00
else :
2018-07-19 15:58:06 +02:00
tmp = [ np . nanmean ( emoji2sent ( e , only_emoticons = only_emoticons ) , axis = 0 , dtype = float ) for e in emojis_i ]
c = 0
for t in tmp :
2018-07-19 16:46:19 +02:00
# only to find and debug wrong formatted data
2018-07-19 15:58:06 +02:00
if str ( type ( t ) ) != " <class ' numpy.ndarray ' > " :
print ( t , type ( t ) )
print ( emojis_i [ c ] )
print ( emoji2sent ( emojis_i [ c ] , only_emoticons = only_emoticons ) )
c + = 1
labels_i = np . array ( tmp , dtype = float )
2018-06-20 13:38:18 +02:00
2018-06-26 16:27:27 +02:00
# and filter out all samples we have no label for:
wrong_labels = np . isnan ( np . linalg . norm ( labels_i , axis = 1 ) )
labels_i = labels_i [ np . invert ( wrong_labels ) ]
plain_text_i = plain_text_i [ np . invert ( wrong_labels ) ]
emojis_i = emojis_i [ np . invert ( wrong_labels ) ]
print ( " imported " + str ( len ( labels_i ) ) + " samples " )
if self . labels is None :
self . labels = labels_i
else :
self . labels = np . append ( self . labels , labels_i , axis = 0 )
if self . emojis is None :
self . emojis = emojis_i
else :
self . emojis = pd . concat ( [ self . emojis , emojis_i ] , ignore_index = True )
if self . plain_text is None :
self . plain_text = plain_text_i
else :
self . plain_text = pd . concat ( [ self . plain_text , plain_text_i ] , ignore_index = True )
if progress_callback is not None :
progress_callback ( ( i + 1 ) / n )
2018-06-20 13:38:18 +02:00
2018-06-26 16:27:27 +02:00
def apply_stemming_and_lemmatization ( self , progress_callback = None ) :
2018-06-20 13:38:18 +02:00
"""
apply stemming and lemmatization to plain text samples
"""
2018-06-26 20:07:37 +02:00
self . use_stemming = True
self . use_lemmatization = True
2018-06-26 16:27:27 +02:00
print ( " apply stemming and lemmatization... " )
2018-06-20 13:38:18 +02:00
stemmer = SnowballStemmer ( " english " )
2018-06-26 16:27:27 +02:00
n = self . plain_text . shape [ 0 ] * 2 # 2 for loops
i = 0
2018-06-20 13:38:18 +02:00
for key in self . plain_text . keys ( ) :
stemmed_sent = [ ]
for word in self . plain_text [ key ] . split ( " " ) :
word_stemmed = stemmer . stem ( word )
stemmed_sent . append ( word_stemmed )
stemmed_sent = ( " " ) . join ( stemmed_sent )
self . plain_text [ key ] = stemmed_sent
2018-06-26 16:27:27 +02:00
i + = 1
if progress_callback is not None and i % 1024 == 0 :
progress_callback ( i / n )
2018-06-20 13:38:18 +02:00
lemmatizer = WordNetLemmatizer ( )
for key in self . plain_text . keys ( ) :
lemmatized_sent = [ ]
sent_pos = pos_tag ( word_tokenize ( self . plain_text [ key ] ) )
for word in sent_pos :
wordnet_pos = get_wordnet_pos ( word [ 1 ] . lower ( ) )
word_lemmatized = lemmatizer . lemmatize ( word [ 0 ] , pos = wordnet_pos )
lemmatized_sent . append ( word_lemmatized )
lemmatized_sent = ( " " ) . join ( lemmatized_sent )
self . plain_text [ key ] = lemmatized_sent
2018-06-26 16:27:27 +02:00
i + = 1
if progress_callback is not None and i % 1024 == 0 :
progress_callback ( i / n )
print ( " stemming and lemmatization done " )
2018-06-20 13:38:18 +02:00
def generate_emoji_count_and_weights ( self ) :
"""
counting occurences of emojis
"""
self . emoji_count = { }
for e_list in self . emojis :
for e in set ( e_list ) :
if e not in self . emoji_count :
self . emoji_count [ e ] = 0
self . emoji_count [ e ] + = 1
emoji_sum = sum ( [ self . emoji_count [ e ] for e in self . emoji_count ] )
self . emoji_weights = { }
for e in self . emoji_count :
# tfidf for emojis
self . emoji_weights [ e ] = np . log ( ( emoji_sum / self . emoji_count [ e ] ) )
weights_sum = sum ( [ self . emoji_weights [ x ] for x in self . emoji_weights ] )
# normalize:
for e in self . emoji_weights :
self . emoji_weights [ e ] = self . emoji_weights [ e ] / weights_sum
self . emoji_weights [ ' X ' ] = 0 # dummy values
self . emoji_count [ ' X ' ] = 0
2018-07-19 16:46:19 +02:00
# dump count data to json:
f = open ( " count_from_read_progress_ " + str ( datetime . datetime . now ( ) ) + " .json " , ' w ' )
f . write ( json . dumps ( self . emoji_count , ensure_ascii = False ) )
f . close ( )
2018-06-20 13:38:18 +02:00
def get_emoji_count ( self ) :
"""
@return : descending list of tuples in form ( < emoji as character > , < emoji count > )
"""
assert self . emoji_count is not None
sorted_emoji_count = list ( reversed ( sorted ( self . emoji_count . items ( ) , key = operator . itemgetter ( 1 ) ) ) )
#display(sorted_emoji_count)
return sorted_emoji_count
def filter_by_top_emojis ( self , n_top = 20 ) :
"""
2018-07-19 16:46:19 +02:00
filter out messages not containing one of the ` n_top ` emojis
2018-06-20 13:38:18 +02:00
@param n_top : number of top emojis used for filtering
"""
assert self . labels is not None # ← messages are already read in
self . top_emojis = [ x [ 0 ] for x in self . get_emoji_count ( ) [ : n_top ] ]
2018-06-20 18:10:54 +02:00
in_top = [ edist . sentiment_vector_to_emoji ( x ) in self . top_emojis for x in self . labels ]
2018-06-20 13:38:18 +02:00
self . labels = self . labels [ in_top ]
self . plain_text = self . plain_text [ in_top ]
self . emojis = self . emojis [ in_top ]
print ( " remaining samples after top emoji filtering: " , len ( self . labels ) )
2018-07-20 12:49:55 +02:00
def filter_by_emoji_list ( self , custom_target_emojis ) :
assert self . labels is not None
in_list = [ edist . sentiment_vector_to_emoji ( x ) in custom_target_emojis for x in self . labels ]
self . labels = self . labels [ in_list ]
self . plain_text = self . plain_text [ in_list ]
self . emojis = self . emojis [ in_list ]
print ( " remaining samples after custom emoji filtering: " , len ( self . labels ) )
2018-07-21 10:32:34 +02:00
def filter_by_sentence_length ( self , min_words ) :
assert self . plain_text is not None
is_long = [ True if len ( x . split ( ) ) > = min_words else False for x in self . plain_text ]
self . labels = self . labels [ is_long ]
self . plain_text = self . plain_text [ is_long ]
self . emojis = self . emojis [ is_long ]
print ( " remaining samples after sentence length filtering: " , len ( self . labels ) )
2018-06-20 18:10:54 +02:00
def generate_kmeans_binary_label ( self , only_emoticons = True , n_clusters = 5 ) :
"""
generate binary labels using kmeans .
@param only_emoticons : set whether we ' re using the full emoji set or only emoticons
@param n_clusters : number of cluster we ' re generating in emoji ' s sentiment space
"""
assert self . labels is not None
array_sentiment_vectors = edist . list_sentiment_emoticon_vectors if only_emoticons else edist . list_sentiment_vectors
array_sentiment_vectors = np . array ( array_sentiment_vectors )
list_emojis = edist . list_emoticon_emojis if only_emoticons else edist . list_emojis
self . use_binary_labels = True
print ( " clustering following emojis: " + " " . join ( list_emojis ) + " ... " )
self . kmeans_cluster = KMeans ( n_clusters = n_clusters ) . fit ( array_sentiment_vectors )
print ( " clustering done " )
self . label_binarizer = LabelBinarizer ( )
multiclass_labels = self . kmeans_cluster . predict ( self . labels )
# FIXME: we have to guarantee that in every dataset all classes occur.
# otherwise batch fitting is not possible!
# (or we have to precompute the mlb fitting process somewhere...)
self . binary_labels = self . label_binarizer . fit_transform ( multiclass_labels )
2018-06-20 13:38:18 +02:00
def create_train_test_split ( self , split = 0.1 , random_state = 4222 ) :
2018-06-20 18:10:54 +02:00
assert self . plain_text is not None and self . labels is not None
2018-06-20 13:38:18 +02:00
if self . X is not None :
sys . stderr . write ( " WARNING: overwriting existing train/test split \n " )
2018-06-20 18:10:54 +02:00
labels = self . binary_labels if self . use_binary_labels else self . labels
assert labels is not None
self . X , self . Xt , self . y , self . yt = train_test_split ( self . plain_text , labels , test_size = split , random_state = random_state )
2018-06-20 13:38:18 +02:00
# * the pipeline manager saves and stores sklearn pipelines. Keras models are handled differently, so the have to be named explicitly during save and load operations
# In[8]:
class pipeline_manager ( object ) :
2018-06-26 14:44:05 +02:00
@staticmethod
def load_from_pipeline_file ( pipeline_file : str ) :
"""
loading a json configuration file and using it ' s paramters to call ' load_pipeline_from_files '
"""
with open ( pipeline_file , ' r ' ) as f :
d = json . load ( f )
keras_models = d [ ' keras_models ' ]
all_models = d [ ' all_models ' ]
return pipeline_manager . load_pipeline_from_files ( pipeline_file . rsplit ( ' . ' , 1 ) [ 0 ] , keras_models , all_models )
2018-06-20 13:38:18 +02:00
@staticmethod
def load_pipeline_from_files ( file_prefix : str , keras_models = [ ] , all_models = [ ] ) :
"""
load a pipeline from files . A pipeline should be represented by multiple model files in the form ' <file_prefix>.<model_name> '
@param file_prefix : basename of all files ( without extension )
@param keras_models : list of keras models ( keras model files , only extension name ) . Leave this list empty if this is not a keras pipeline
@param all_models : list of all models ( including keras_models , only extension name ) .
@return a pipeline manager object
"""
pm = pipeline_manager ( keras_models = keras_models )
pm . load ( file_prefix , all_models )
return pm
@staticmethod
2018-07-10 13:48:51 +02:00
def create_keras_pipeline_with_vectorizer ( vectorizer , layers , sdm : sample_data_manager , loss = None , optimizer = None , fit_vectorizer = True ) :
2018-06-20 13:38:18 +02:00
'''
creates pipeline with vectorizer and keras classifier
@param vectorizer : Vectorizer object . will be fitted with data provided by sdm
@param layers : list of keras layers . One keras layer is a tuple in form : ( < #neurons:int>, <activation_func:str>)
@param sdm : sample data manager to get data for the vectorizer
2018-06-20 18:10:54 +02:00
@param loss : set keras loss function . Depending whether sdm use multiclass labels ` categorical_crossentropy ` or ` mean_squared_error ` is used as default
@param optimizer : set keras optimizer . Depending whether sdm use multiclass labels ` sgd ` or ` adam ` is used as default
2018-07-03 14:20:31 +02:00
2018-06-20 13:38:18 +02:00
@return : a pipeline manager object
'''
from keras . models import Sequential
from keras . layers import Dense
2018-07-10 13:48:51 +02:00
if fit_vectorizer :
if sdm . X is None :
sdm . create_train_test_split ( )
2018-07-22 10:55:31 +02:00
print ( " fit vectorizer... " )
2018-07-10 13:48:51 +02:00
vec_train = vectorizer . fit_transform ( sdm . X )
vec_test = vectorizer . transform ( sdm . Xt )
2018-07-22 10:55:31 +02:00
print ( " fitting done " )
2018-06-20 13:38:18 +02:00
# creating keras model:
model = Sequential ( )
keras_layers = [ ]
first_layer = True
for layer in layers :
if first_layer :
2018-07-03 14:20:31 +02:00
size = None
if " size " in dir ( vectorizer ) :
size = vectorizer . size
else :
size = vectorizer . transform ( [ " " ] ) [ 0 ] . _shape [ 1 ]
model . add ( Dense ( units = layer [ 0 ] , activation = layer [ 1 ] , input_dim = size ) )
2018-06-20 13:38:18 +02:00
first_layer = False
else :
model . add ( Dense ( units = layer [ 0 ] , activation = layer [ 1 ] ) )
2018-06-20 18:10:54 +02:00
if sdm . use_binary_labels :
loss_function = loss if loss is not None else ' categorical_crossentropy '
optimizer_function = optimizer if optimizer is not None else ' sgd '
model . compile ( loss = loss_function ,
optimizer = optimizer_function ,
metrics = [ ' accuracy ' ] )
else :
loss_function = loss if loss is not None else ' mean_squared_error '
optimizer_function = optimizer if optimizer is not None else ' adam '
model . compile ( loss = loss_function ,
optimizer = optimizer_function )
2018-06-20 13:38:18 +02:00
pipeline = Pipeline ( [
( ' vectorizer ' , vectorizer ) ,
( ' keras_model ' , model )
] )
return pipeline_manager ( pipeline = pipeline , keras_models = [ ' keras_model ' ] )
@staticmethod
def create_pipeline_with_classifier_and_vectorizer ( vectorizer , classifier , sdm : sample_data_manager = None ) :
'''
creates pipeline with vectorizer and non - keras classifier
@param vectorizer : Vectorizer object . will be fitted with data provided by sdm
@param classifier : unfitted classifier object ( should be compatible with all sklearn classifiers )
@param sdm : sample data manager to get data for the vectorizer
@return : a pipeline manager object
'''
if sdm is not None :
if sdm . X is None :
sdm . create_train_test_split ( )
vec_train = vectorizer . fit_transform ( sdm . X )
vec_test = vectorizer . transform ( sdm . Xt )
pipeline = Pipeline ( [
( ' vectorizer ' , vectorizer ) ,
( ' classifier ' , classifier )
] )
return pipeline_manager ( pipeline = pipeline , keras_models = [ ] )
def __init__ ( self , pipeline = None , keras_models = [ ] ) :
"""
constructor
@param pipeline : a sklearn pipeline
@param keras_models : list of keras steps in pipeline . Neccessary because saving and loading from keras models differs from the scikit ones
"""
self . pipeline = pipeline
self . additional_objects = { }
self . keras_models = keras_models
def save ( self , prefix : str ) :
"""
saving the pipeline . It generates one file per model in the form : ' <prefix>.<model_name> '
@param prefix : file prefix for all models
"""
2018-06-26 14:44:05 +02:00
2018-06-20 13:38:18 +02:00
print ( self . keras_models )
# doing this like explained here: https://stackoverflow.com/a/43415459
for step in self . pipeline . named_steps :
if step in self . keras_models :
self . pipeline . named_steps [ step ] . model . save ( prefix + " . " + step )
else :
joblib . dump ( self . pipeline . named_steps [ step ] , prefix + " . " + str ( step ) )
load_command = " pipeline_manager.load_pipeline_from_files( ' "
load_command + = prefix + " ' , " + str ( self . keras_models ) + " , "
load_command + = str ( list ( self . pipeline . named_steps . keys ( ) ) ) + " ) "
2018-06-26 14:44:05 +02:00
with open ( prefix + ' .pipeline ' , ' w ' ) as outfile :
json . dump ( { ' keras_models ' : self . keras_models , ' all_models ' : [ step for step in self . pipeline . named_steps ] } , outfile )
2018-06-20 13:38:18 +02:00
import __main__ as main
if not hasattr ( main , ' __file__ ' ) :
display ( " saved pipeline. It can be loaded the following way: " )
display ( Markdown ( " > ``` \n " + load_command + " \n ``` " ) ) # ← if we're in jupyter, print the fancy way :)
else :
print ( " saved pipeline. It can be loaded the following way: " )
print ( load_command )
def load ( self , prefix : str , models = [ ] ) :
"""
load a pipeline . A pipeline should be represented by multiple model files in the form ' <prefix>.<model_name> '
NOTE : keras model names ( if there are some ) have to be defined in self . keras_models first !
@param prefix : the prefix for all model files
@param models : model_names to load
"""
self . pipeline = None
model_list = [ ]
for model in models :
if model in self . keras_models :
model_list . append ( ( model , load_model ( prefix + " . " + model ) ) )
else :
model_list . append ( ( model , joblib . load ( prefix + " . " + model ) ) )
self . pipeline = Pipeline ( model_list )
def fit ( self , X , y ) :
""" fitting the pipeline """
self . pipeline . fit ( X , y )
2018-07-10 13:48:51 +02:00
def predict ( self , X , use_stemming = False , use_lemmatization = False ) :
2018-06-20 13:38:18 +02:00
""" predict """
2018-06-26 20:07:37 +02:00
if use_stemming :
2018-06-26 20:33:55 +02:00
X = np . array ( batch_stem ( X ) )
2018-06-26 20:07:37 +02:00
if use_lemmatization :
2018-06-26 20:33:55 +02:00
X = np . array ( batch_lemm ( X ) )
2018-06-20 13:38:18 +02:00
return self . pipeline . predict ( X )
# * the trainer class passes Data from the sample manager to the pipeline manager
# In[9]:
2018-07-03 14:20:31 +02:00
def to_dense_if_sparse ( X ) :
"""
little hepler function to make data dense ( if it is sparse ) .
is used in trainer . fit function
"""
if " todense " in dir ( X ) :
return X . todense ( )
return X
2018-06-20 13:38:18 +02:00
class trainer ( object ) :
def __init__ ( self , sdm : sample_data_manager , pm : pipeline_manager ) :
""" constructor """
self . sdm = sdm
self . pm = pm
2018-07-21 09:48:25 +02:00
self . acc = [ ]
self . val = [ ]
2018-06-20 13:38:18 +02:00
2018-07-10 13:48:51 +02:00
def fit ( self , max_size = 1000000 , disabled_fit_steps = [ ' vectorizer ' ] , keras_batch_fitting_layer = [ ' keras_model ' ] , batch_size = None , n_epochs = 1 , progress_callback = None ) :
2018-06-20 13:38:18 +02:00
"""
fitting data in the pipeline . Because we don ' t want to refit the vectorizer, the pipeline models containing the vectorizer have to be named explicitly
@param max_size : don ' t train more examples than that number
@param disabled_fit_steps : list of pipeline steps that we want to prevent to refit . Normally all vectorizer steps
"""
# TODO: make batch fitting available here (eg: continous waiting for data and fitting them)
if self . sdm . X is None :
self . sdm . create_train_test_split ( )
disabled_fits = { }
disabled_fit_transforms = { }
2018-06-25 19:17:38 +02:00
disabled_keras_fits = { }
2018-06-20 13:38:18 +02:00
named_steps = self . pm . pipeline . named_steps
for s in disabled_fit_steps :
2018-06-25 19:17:38 +02:00
# now it gets really dirty:
2018-06-20 13:38:18 +02:00
# replace fit functions we don't want to call again (e.g. for vectorizers)
disabled_fits [ s ] = named_steps [ s ] . fit
disabled_fit_transforms [ s ] = named_steps [ s ] . fit_transform
named_steps [ s ] . fit = lambda self , X , y = None : self
named_steps [ s ] . fit_transform = named_steps [ s ] . transform
2018-06-25 19:17:38 +02:00
2018-07-03 15:37:15 +02:00
if batch_size is not None :
2018-07-03 15:07:45 +02:00
for k in keras_batch_fitting_layer :
# forcing batch fitting on keras
disabled_keras_fits [ k ] = named_steps [ k ] . fit
2018-07-03 14:20:31 +02:00
2018-07-03 15:07:45 +02:00
named_steps [ k ] . fit = lambda X , y : named_steps [ k ] . train_on_batch ( to_dense_if_sparse ( X ) , y ) # ← why has keras no sparse support on batch progressing!?!?!
2018-06-25 19:17:38 +02:00
if batch_size is None :
2018-07-21 09:48:25 +02:00
self . acc = [ ]
self . val = [ ]
2018-07-10 15:59:28 +02:00
for e in range ( n_epochs ) :
print ( " epoch " , e )
self . pm . fit ( X = self . sdm . X [ : max_size ] , y = self . sdm . y [ : max_size ] )
pred , yt = self . test ( )
mean_squared_error = ( ( pred - yt ) * * 2 ) . mean ( axis = 0 )
print ( " # " + str ( e ) + " : validation loss: " , mean_squared_error , " scalar: " , np . mean ( mean_squared_error ) )
2018-07-21 09:48:25 +02:00
self . val . append ( np . mean ( mean_squared_error ) )
2018-07-22 10:55:31 +02:00
plot_sentiment_space ( pred , top_20_sents , top_20 )
2018-07-21 09:48:25 +02:00
plt . figure ( figsize = ( 10 , 5 ) )
plt . plot ( self . val )
plt . savefig ( " val_error " + str ( datetime . datetime . now ( ) ) + " .png " , bbox_inches = ' tight ' )
plt . show ( )
2018-07-22 10:55:31 +02:00
2018-06-25 19:17:38 +02:00
else :
n = len ( self . sdm . X ) / / batch_size
for i in range ( n_epochs ) :
for j in range ( n ) :
self . pm . fit ( X = np . array ( self . sdm . X [ j * batch_size : ( j + 1 ) * batch_size ] ) , y = np . array ( self . sdm . y [ j * batch_size : ( j + 1 ) * batch_size ] ) )
if progress_callback is not None :
2018-06-26 16:27:27 +02:00
progress_callback ( j / n )
2018-06-25 19:17:38 +02:00
pred , yt = self . test ( )
mean_squared_error = ( ( pred - yt ) * * 2 ) . mean ( axis = 0 )
print ( " # " + str ( j ) + " : loss: " , mean_squared_error )
2018-06-20 13:38:18 +02:00
# restore replaced fit functions:
for s in disabled_fit_steps :
named_steps [ s ] . fit = disabled_fits [ s ]
named_steps [ s ] . fit_transform = disabled_fit_transforms [ s ]
2018-06-25 19:17:38 +02:00
2018-07-03 15:37:15 +02:00
if batch_size is not None :
2018-07-03 15:07:45 +02:00
for k in keras_batch_fitting_layer :
named_steps [ k ] . fit = disabled_keras_fits [ k ]
2018-06-20 13:38:18 +02:00
2018-07-22 10:55:31 +02:00
def test ( self , use_lemmatization = False , use_stemming = False , emoji_subset = None , only_test_on_valid_set = True ) :
2018-06-20 13:38:18 +02:00
'''
2018-07-22 10:55:31 +02:00
@param use_lemmatization : boolean
@param use_stemming : boolean
@param emoji_subset : list if given , only make predictions on samples containing one of these emojis as teacher value
2018-06-20 13:38:18 +02:00
@return : prediction : list , teacher : list
'''
2018-07-22 10:55:31 +02:00
2018-06-20 13:38:18 +02:00
if self . sdm . X is None :
self . sdm . create_train_test_split ( )
2018-07-22 10:55:31 +02:00
Xt = self . sdm . Xt
yt = self . sdm . yt
print ( " original validation size: " + str ( len ( yt ) ) )
if emoji_subset is not None :
has_emoji = np . array ( [ True if edist . sentiment_vector_to_emoji ( y ) in emoji_subset else False for y in yt ] )
Xt = Xt [ has_emoji ]
yt = yt [ has_emoji ]
print ( " filtered validation size: " + str ( len ( yt ) ) )
return self . pm . predict ( Xt , use_lemmatization = use_lemmatization , use_stemming = use_stemming ) , yt