This repository was archived by the owner on May 7, 2021. It is now read-only.
Folders and files
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Repository files navigation
* Indentification of Semantic Duplicates, or Dilettante Dive into the Quora Data Set ** Introduction Finding and quantifying semantic patterns is key to successful natural language processing and knowledge organisation. One of the most important challenges in NLP is determining whether two texts have the same intent or not, and machine learning techniques provide us a necessary and diverse toolset to approach this problem. In January 2017, [[https://data.quora.com/First-Quora-Dataset-Release-Question-Pairs][Quora published its first public data set]] on duplicate and related human-labelled questions compiled from the actual Quora data. How can we use it to predict whether two question-strings are duplicate or not? *** Loading Modules and Data #+BEGIN_SRC ipython :session :tangle yes import numpy as np # linear algebra import pandas as pd # data processing, CSV file I/O import matplotlib.pyplot as plt # visualisation training_filename = "data/train_small.csv" testing_filename = "data/test_small.csv" training_data = pd.read_csv(training_filename, encoding = 'utf8') #+END_SRC Let's get a sneak peek at the training data at hand: #+BEGIN_SRC ipython :session :tangle no training_data.head(10) #+END_SRC We are given the following parameters: - *id* - the id of a training set question pair - *qid1*, *qid2* - unique ids of each question - *question1*, *question2* - the full text of each question - *is_duplicate* - the target variable, set to 1 if question1 and question2 have essentially the same meaning, and 0 otherwise. #+BEGIN_SRC ipython :session :results output drawer :tangle no print('There are {} question pairs available for training.'.format(len(training_data))) print('Among them, {}% are duplicates.'.format(round(training_data['is_duplicate'].mean()*100, 2))) # find unique question ids qids = pd.Series(training_data['qid1'].tolist() + training_data['qid2'].tolist()) print('There are {} unique questions considered.'.format(len(np.unique(qids)))) print('The number of questions that appear multiple times is {}.'.format(np.sum(qids.value_counts() > 1))) #+END_SRC Let's look at the distribution of question lengths: #+BEGIN_SRC ipython :session :results output drawer :tangle no questions = pd.Series(list(set(training_data['question1'].tolist() + training_data['question2'].tolist()))) question_lengths = questions.str.len().value_counts() plt.figure(figsize=(12, 5)) plt.scatter(question_lengths, question_lengths.index) plt.xscale('log', nonposy='clip') plt.title('Log-Histogram of Question Length Counts') plt.xlabel('Number of Occurences of Question Length') plt.ylabel('Question Length') plt.hlines(10, 0, 1.9e4) plt.show() #+END_SRC It seems that there is a minority of very short questions. We will see whether dropping them from the training data set will help us improve the accuracy of predictions. *** Balancing Classes We have already noticed that in the training data set the number of duplicates constitute about a third of the entire corpus. This may skew our predictions by increasing the number of false positives. There are 2 approaches I have tried to remediate this problem: - generate an additional set of duplicate questions by replacing each noun in a randomly chosen duplicate question with its synonym, increasing the percentage content of duplicate questions to a half of the entire data sets - apply up-down scaling to the training dataframe, decreasing the percentage content to about a fifth of the entire dataframe, which [[https://www.kaggle.com/badat0202/estimate-distribution-of-data-in-lb][was estimated]] to be close to the percentage content of duplicates in the test data set *** Cleaning Data Our cleaning-up process is threefold: 1. We remove unnecessary punctuation, pad the words, deal with non-ascii characters and fix simple typos. 2. SpaCy is used to replace quantities and other entities with something more amenable to later processing by our machine learning model. 3. We remove questions deemed too short to bear any meaningful content. 4. Finally, we oversample duplicates. *** Dealing with Missing Packages Since reproducible builds of [[https://nixos.org/][NixOS]] disallow the vanilla usage of ~pip~ for Python package management, we [[https://github.com/NixOS/nixpkgs/pull/25804][first]] [[https://github.com/NixOS/nixpkgs/pull/25791][add]] tools like ~gensim~ and ~spaCy~ to [[https://nixos.org/nixpkgs/manual/][Nixpkgs]]. Now we are ready to proceed: #+BEGIN_SRC ipython :session :results output drawer from preprocess import Preprocessor prep = Preprocessor(training_filename) training_data = prep.processed_questions() #+END_SRC We save the data sets after each step so that the corresponding models could be compared later. ** Modelling *** Building Naive Models To avoid the time-wasting bog of fine-tuning a deliberately chosen model which might not be the best choice to analyse the data, we build and improve upon a benchmark model. **** Naive LSTM (long short-term memory) recurrent neural network with a non-trainable word2vec embedding layer, inspired by [[https://www.kaggle.com/lystdo/lstm-with-word2vec-embeddings][@lystdo]], seems suitable, considering the success of LSTM models in a diverse range of natural language processing such as text compression and handwriting recognition. Note that we are going to train on the data set with oversampled and perturbated positive class. #+BEGIN_SRC ipython :session :results output drawer from models.naive_model import NaiveModel nm = NaiveModel() nm.predict() #+END_SRC Ten epochs of the model require substantial computing resources -- running the snippet above took almost 17 hours on my laptop. The model with minimal fine-tuning gives us a score of 0.31985, while the logloss of the model validated on the training dataframe was 0.2480, which we would now try to improve. Our strategy is to identify relevant features and build a faster model. **** Aleph We will try to use less computationally expensive techniques. First of all, we dispense with recurrent neural networks in our naive model and build upon [[https://github.com/bradleypallen/keras-quora-question-pairs][the work of Bradley Pallen]], constructing a Siamese neural network with 300-dimensional Google News Word2Vec embeddings branches passing through TimeDistributed Dense layers and merged to be processed via fully-connected Dense layers activated with ReLU. Then we reweigh the classes to account for the disparity in the class prevalence. - *Training data set*: oversampled - *Dense dimensions*: 137 - *Dropout rate for hidden layers*: 0.38 #+BEGIN_SRC ipython :session :results output drawer from models.aleph_model import Aleph aleph = Aleph() aleph.predict() #+END_SRC For the speed the price is logloss: Aleph scores only 0.32497 on the LB. [[file:img/accuracy_dynamics.png]] **** Bet - based on Aleph - added kernel constraints to Dense layers to ensure the maximum norm does not exceed 3 - ordered Dense layers in the form of an inverted pyramid - *Training data set*: oversampled - *Dense dimensions*: 126 - *Dropout rate for hidden layers*: 0.37 #+BEGIN_SRC ipython :session :results output drawer from models.bet_model import Bet bet = Bet() bet.predict() #+END_SRC The model seems to undertrain, judging by the fact that the validation accuracy is always bigger than the training accuracy, and gives us a worse score 0.33641. [[file:img/2017-05-26-1043-beta_126_0.37-accuracy_dynamics.png]] **** Vet #+BEGIN_SRC ipython :session :results output drawer from models.vet_model import Vet vet = Vet() vet.predict() #+END_SRC - based on Bet - made the choice of the dropout rate for hidden layers more uniform in the range 0.20 to 0.40 - increased the hidden layer dimensions to the range [280, 300] - changed ReLU to PReLU - removed the dropout after the batch normalisation of the merge layer - removed kernel constraints - flattened the pyramidal structure of the hidden layers - *Training data set*: oversampled - *Dense dimensions*: 282 - *Dropout rate for hidden layers*: 0.27 [[file:img/2017-05-26-1818-vet_282_0.27-accuracy_dynamics.png]] The model evidently overtrains after the third epoch, and scores 0.35179, even worse than all the other models we have looked at. **** Gimmel #+BEGIN_SRC ipython :session :results output drawer from models.gimmel_model import Gimmel gimmel = Gimmel() gimmel.predict() #+END_SRC - based on Vet - added Krzysztof Dziedzic's [[https://www.kaggle.com/c/quora-question-pairs/discussion/33287][magic feature II]], inspired by @justfor's [[https://www.kaggle.com/justfor/edges/code][implementation]] - added @tarobxl's [[https://www.kaggle.com/c/quora-question-pairs/discussion/33371][max kcore feature]]. - feeding custom features through a separate hidden layer with sandwiching dropout and batch normalisation - increased the range of the dropout rate to [0.25, 0.50] - decreased the range of the hidden dimensions to [190, 275] - organised the hidden layers into the pyramidal form - switched to adam with a clipnorm as an optimizer - changed the training data set - *Training data set*: spaCy-treated, with shorties trimmed - *Dense dimensions*: 214 - *Dropout rate for hidden layers*: 0.42 [[file:img/2017-05-27-1204-gimmel_214_0.42-accuracy_dynamics.png]] The magic features play in, giving us the Public LB score of 0.24602. **** Dalet #+BEGIN_SRC ipython :session :results output drawer from models.dalet_model import Dalet dalet = Dalet() dalet.predict() #+END_SRC - based on Gimmel - custom features as a /pure/ input layer with Dropout - added [[https://github.com/abhishekkrthakur/is_that_a_duplicate_quora_question][@abhishek's features]] [[https://www.kaggle.com/c/quora-question-pairs/discussion/31284][provided]] by @raddar - *Training data set*: spaCy-treated, with shorties trimmed - *Dense dimensions*: 242 - *Dropout rate for hidden layers*: 0.32 [[file:img/2017-05-27-1945-dalet_242_0.32-accuracy_dynamics.png]] The performance is very similar to Gimmel's, but Dalet seems to overfit more, as reflected in its score of 0.24801. **** He #+BEGIN_SRC ipython :session :results output drawer from models.he_model import He he = He() he.predict() #+END_SRC - based on Dalet - used the train data set with words stemmed and stopwords removed #+BEGIN_SRC ipython :session :results output drawer from helpers.stem import Stem s = Stem() s.stem_questions() #+END_SRC - removed @abhishek's features in the attempt to avoid overfitting - added @jturkewitz's [[https://www.kaggle.com/jturkewitz/magic-features-0-03-gain][magic feature]] #+BEGIN_SRC ipython :session :results output drawer from helpers.magic import Magic m = Magic() m.spell() #+END_SRC - added a hidden layer for custom features for feature detection - *Training data set*: spaCy-treated, with shorties trimmed, words stemmed and stopwords removed - *Dense dimensions*: 199 - *Dropout rate for hidden layers*: 0.49 [[file:img/2017-05-28-0605-he_199_0.49-accuracy_dynamics.png]] Now, the model converges well, and no significant overlearning seems to occur. However, the score jumps up to 0.26713, and I make a hypothesis that stemming is to blame. **** Vav #+BEGIN_SRC ipython :session :results output drawer from models.vav_model import Vav vav = Vav() vav.predict() #+END_SRC - based on the naive model (switched to LSTM) - removed stemming, with stopwords still excluded #+BEGIN_SRC ipython :session :results output drawer from helpers.stopword import StopwordTrim s = StopwordTrim() s.remove_stopwords() #+END_SRC - *Training data set*: spaCy-treated, with shorties trimmed and stopwords removed - *Dense dimensions*: 198 - *Dropout rate for hidden layers*: 0.46 [[file:img/2017-05-28-2256-vav_198_0.46-accuracy_dynamics.png]] The model converges quickly and seems to underlearn, but gives the best score so far -- 0.24360. **** Zayin #+BEGIN_SRC ipython :session :results output drawer from models.zayin_model import Zayin zayin = Zayin() zayin.predict() #+END_SRC - based on Vav - included Abhishek's features - increased the dimensionality of the Dense layer for custom features to the default for the others - *Training data set*: spaCy-treated, with shorties trimmed and stopwords removed - *Dense dimensions*: 215 - *Dropout rate for hidden layers*: 0.40 [[file:img/2017-05-29-1751-zayin_215_0.40-accuracy_dynamics.png]] As we can see on the graph, the model is not as stable as the others in the beginning, but converges after the 7 epoch. The disparity between the training and validation set is not as big now if compared with Vav's, but the public LB score of 0.25464 indicates that the model generalises a bit worse than Vav. **** Khet #+BEGIN_SRC ipython :session :results output drawer from models.khet_model import Khet khet = Khet() khet.predict() #+END_SRC - based on Zayin (switched from LSTM to the Convolution1D layers with with GlobalMaxPooling1D and PReLU activation for the embedding layers to improve the speed of training) - removed Abhishek's features in the attempt to improve generalisation - *Training data set*: spaCy-treated, with shorties trimmed and stopwords removed - *Dense dimensions*: 261 - *Dropout rate for hidden layers*: 0.41 [[file:img/2017-05-29-2059-khet_261_0.41-accuracy_dynamics.png]] We pay the price for delegating feature extraction to the model with speed, receiving the score of 0.27044. *** Comparing Models and Ensembling One of the ways to improve our prediction accuracy is to ensemble the different predictions we have. We resort to GM and AM averaging. First, let's combine all our predictions into one with the help of a geometric mean: #+BEGIN_SRC ipython :session :results output drawer import glob import pandas as pd from scipy.stats.mstats import gmean predPath = "ensembling/" models = [] for file_name in glob.glob(predPath + '*.csv'): df = pd.read_csv(file_name, encoding = "utf-8") models.append(df.ix[:, :1]) total = pd.concat(models, axis=1) gm_df = pd.DataFrame() gm_df['is_duplicate'] = gmean(total, axis=1) gm_df['test_id'] = df.ix[:,1] gm_df.to_csv("gm_ensemble.csv", index=False) #+END_SRC This simple procedure got us a score of 0.21736, which is an increase of 0.02624 from the score of Vav, our best model. Now, we attempt rank averaging to improve our result. #+BEGIN_SRC ipython :session :results output drawer GLOB_PREDICTIONS = "./ensembling/*" RK_AVG_OUTPUT = "./rk_avg_ensemble.csv" from collections import defaultdict from glob import glob def rk_avg_ensemble(glob_files, loc_outfile): # write to the chosen file with open(loc_outfile,"w") as outfile: # store the raw ranks # keys are in the form (<line number>, <id>) all_ranks = defaultdict(list) # i is needed to write the header of the output file for i, glob_file in enumerate( glob(glob_files) ): # define a container for the ranks found in a file file_ranks = [] print("Parsing now:", glob_file) # sort <glob_file> by the predicted probability, ignoring the first line lines = open(glob_file).readlines() lines = [lines[0]] + sorted(lines[1:]) for line_number, line in enumerate( lines ): if line_number == 0 and i == 0: outfile.write( line ) # if the line is not a header, process it elif line_number > 0: # store the row in a list row = line.strip().split(",") # <row[0]> is the predicted probability # <row[1]> is the id of the question file_ranks.append( (float(row[0]), int(row[1])) ) # sort by the initially predicted probability # and give it a rank for rank, container in enumerate( sorted(file_ranks) ): # Store the rank in the dictionary for further processing. # The key is in the form <id>. all_ranks[container[1]].append(rank) print("The number of lines with the stored ranks: {}.".format(len(all_ranks.keys()))) # define a list as a container for the average ranks average_ranks = [] # k is in the form (<line number>, <id>); # first sort by the id for identifier in sorted(all_ranks): # append the average rank together with the identifier average_ranks.append((sum(all_ranks[identifier])/len(all_ranks[identifier]), identifier)) # define a list as a container for the sorted ranks normalised_ranks = [] # the element of the <average_ranks> is in the format # (<average_rank>, <identifier>), where # <identifier> is <test_id> for rank, avg_rk_obj in enumerate(sorted(average_ranks)): # sort <average_ranks> by the average rank and # append (<id>, <normalized probability>) # to <sorted_ranks> normalised_ranks.append((avg_rk_obj[1], rank/(len(average_ranks)-1))) # norm_prob_obj is in the format # (<id>, <normalised probability>) # sort <normalised_ranks> by <test_id> and write to the file for norm_prob_obj in sorted(normalised_ranks): outfile.write("{},{}\n".format(norm_prob_obj[1], norm_prob_obj[0])) print("Saved the normalised probabilites to {}.".format(loc_outfile)) rk_avg_ensemble(GLOB_PREDICTIONS, RK_AVG_OUTPUT) #+END_SRC Unfortunately, this approach does not yield anything useful, scoring at 0.90852, which is probably due to the fact that the estimated number of duplicates is only a fifth of the entire data set. Now we will try the weighted arithmetic mean. First, let's define a WAM ensembling function which we are going to use throughout our analysis. #+BEGIN_SRC ipython :session :results output drawer def ensemble(weights, output): import glob import pandas as pd weighted_models = [] for file_name in glob.glob(predPath + '*.csv'): df = pd.read_csv(file_name, encoding = "utf-8") weighted_models.append((df['is_duplicate'],weights[file_name])) wam_df = df['is_duplicate'] * 0.0 for model, weight in weighted_models: wam_df += model * weight wam_df /= sum(weights.values()) wam_df = pd.concat([df['test_id'], wam_df], axis=1) wam_df.to_csv(output, index=False) #+END_SRC Now, we rank the models which we want to ensemble. #+BEGIN_SRC ipython :session :results output drawer predPath = "ensembling/" weights = { # PB Scores are given above the file name #0.25464 predPath + "0.2092_zayin_215_0.40.csv":2, #0.24360 predPath + "0.2200_vav_198_0.46.csv":5, #0.26713 predPath + "0.2217_he_199_0.49.csv":1, #0.24602 predPath + "0.2346_gimmel_214_0.42.csv":4, #0.24801 predPath + "0.2364_dalet_242_0.32.csv":3 } ensemble(weights, "wam_ensemble.csv") #+END_SRC Weighted AM turns out to yield the best score so far, giving 0.21623 -- we will try to improve it by building better models. *** Building Different Models What can we learn about the data set without embedding the texts into the 300-dimensional vector space and letting the neural net out on the embeddings which might not be representative of the semantics due to inevitable typos and other oddities of formatting? We will try to find out by building XGBoost models trained on hand-picked features. **** Tet - built metrics and XGBoost model [[https://www.kaggle.com/act444/lb-0-158-xgb-handcrafted-leaky][inspired by @act444]]. #+BEGIN_SRC ipython :session :results output drawer from custom.features import CustomFeatures cf = CustomFeatures() cf.run() #+END_SRC - removed Abhishek's features - *Training data set*: spaCy-treated, with shorties trimmed and stopwords removed - *Result*: 0.24224 - *Features Importance*: [[file:img/2017-05-30-1512-0.222026460989_tet_xgboost-feature_importance.png]] **** Yud #+BEGIN_SRC ipython :session :results output drawer from models.yud_model import Yud yud = Yud() yud.predict() #+END_SRC - based on Tet - changed base score to 0.175 from 0.2 - added Abhishek's features - *Training data set*: spaCy-treated, with shorties trimmed and stopwords removed - *Training result*: 0.2244, which is worse than the training result of Tet (0.22437), hence Yud's predictions were not submitted - *Features Importance*: [[file:img/2017-05-30-1553-0.224371846975_yud_xgboost-feature_importance.png]] Notice how significant Abhishek's features are, given that they provide vital information about the NLP features of questions. **** Kaf #+BEGIN_SRC ipython :session :results output drawer from models.kaf_model import Kaf kaf = Kaf() kaf.predict() #+END_SRC - based on Yud - changed the training data set to include stopwords #+BEGIN_SRC ipython :session :results output drawer from magic import Magic TRAIN_DATA_FILENAME = "2017-05-24-1818-shorties_trimmed_train" m = Magic(train_data_filename=TRAIN_DATA_FILENAME) m.spell() from custom.features import CustomFeatures cf = CustomFeatures(train_data_filename=TRAIN_DATA_FILENAME) cf.run() #+END_SRC - changed ~max_depth~ from 7 to 10 and ~subsample~ from 0.6 to 0.55 - *Training data set*: spaCy-treated, with shorties trimmed - *Result*: 0.24253 - *Features Importance*: [[file:img/2017-05-30-1801-0.214863010529_kaf_xgboost-feature_importance.png]] Notice how the priority of ~tfidf_wm~ feature increases with stopwords included. The new settings of the model, however, decrease its predictive power. **** Lamed #+BEGIN_SRC ipython :session :results output drawer from models.lamed_model import Lamed lamed = Lamed() lamed.predict() #+END_SRC - trying ensembling with 5 classifiers, inspired by [[https://github.com/emanuele/kaggle_pbr/blob/master/blend.py][@emanuele's work]] - *Training data set*: spaCy-treated, with shorties trimmed - *Result*: 0.24785 - The result is surprisingly good for the time spent training the model. **** Attempt at Repeated Ensembling We will try to ensemble again to see whether Lamed gives us any improvement. #+BEGIN_SRC ipython :session :results output drawer predPath = "ensembling/" weights = { # PB Scores are given above the file name #0.21623 predPath + "wam_ensemble.csv":8, #0.21736 predPath + "gm_ensemble.csv":7, #0.24224 predPath + "0.222026460989_tet_xgboost.csv":6, #0.24253 predPath + "0.214863010529_kaf_xgboost.csv":5, #0.24360 predPath + "0.2200_vav_198_0.46.csv":4, #0.24602 predPath + "0.2346_gimmel_214_0.42.csv":3, #0.24785 predPath + "0.319533794385_lamed_xgboost.csv":2, #0.24801 predPath + "0.2364_dalet_242_0.32.csv":1 } ensemble(weights, "wam_ensemble_1.csv") #+END_SRC Indeed, the second ensemble scores 0.20882, which is the best score so far. What if we ensemble again? #+BEGIN_SRC ipython :session :results output drawer predPath = "ensembling/" weights = { # PB Scores are given above the file name #0.20882 predPath + "wam_ensemble_1.csv":8, #0.21623 predPath + "wam_ensemble.csv":7, #0.21736 predPath + "gm_ensemble.csv":6, #0.24224 predPath + "0.222026460989_tet_xgboost.csv":5, #0.24253 predPath + "0.214863010529_kaf_xgboost.csv":4, #0.24360 predPath + "0.2200_vav_198_0.46.csv":3, #0.24602 predPath + "0.2346_gimmel_214_0.42.csv":2, #0.24785 predPath + "0.319533794385_lamed_xgboost.csv":1 } ensemble(weights, "wam_ensemble_2.csv") #+END_SRC The third ensemble is worse than the parental second, which is consistent with [[https://mlwave.com/kaggle-ensembling-guide/][the reports]] that weighted AM ensembling works better with the least correlated predictions. **** Mem #+BEGIN_SRC ipython :session :results output drawer from models.mem_model import Mem mem = Mem() mem.predict() #+END_SRC - based on Lamed - added NN MLPClassifier with pyramidal structure of three hidden layers - removed Lamed's GradientBoostingClassifier - changed the number of estimators to 180 each - *Training data set*: spaCy-treated, with shorties trimmed - *Result*: 0.24785 The model received the worst score of 6.55941 after very quick training, which indicates severe overfitting. **** Nun #+BEGIN_SRC ipython :session :results output drawer from models.nun_model import Nun nun = Nun() nun.predict() #+END_SRC - based on He - changed the training data set (included stopwords) - wrote the code for computing similarity measures based on NLTK WordNet and Brown corpora, but decided against their inclusion due to the speed of their computation #+BEGIN_SRC ipython :session :results output drawer from custom.similarity import SimilarityFeatures sf = SimilarityFeatures(train_data_filename="2017-05-24-1818-shorties_trimmed_train") sf.run() #+END_SRC - added Abhishek's features with ~euclidean_distance~ and ~jaccard_distance~ dropped, as suggested by their feature importance - added Dropout layers to ~Q1~, ~Q2~ and ~custom_features~ branches before the merging layer - swapped places of dropout and batch normalisation layers after merging - decreased the dropout rate for the question layers - decreased the dimension of the dense layer for custom features - added up-down sampling - changed the optimizer to nadam - added custom features - removed reweighing - *Training data set*: spaCy-treated, with shorties trimmed - *Base dense dimensions*: 407 - *Base dropout rate for hidden layers*: 0.47 [[file:img/2017-06-04-1743-nun_407_0.47-accuracy_dynamics.png]] The score obtained is 0.24987, which is an improvement from He's 0.26713. **** Samech #+BEGIN_SRC ipython :session :results output drawer from models.samech_model import Samech samech = Samech() samech.predict() #+END_SRC - based on Kaf - decreased ~eta~ to 0.01 to prevent overfitting - increased ~max_depth~ to 8 to make the model more complex - increased base score to 0.19 - removed Abhishek's features - *Training data set*: spaCy-treated, with shorties trimmed [[file:img/2017-06-02-2225-0.212772886818_samech_xgboost-feature_importance.png]] The score of 0.24505 is worse than Kaf's 0.24253, which is probably due to the lack of good features. We have already seen in Yud's predictions that including Abhishek's can boost the results, so this run was not productive -- the model does seem to learn more from less, however. **** Pe #+BEGIN_SRC ipython :session :results output drawer from models.pe_model import Pe pe = Pe() pe.predict() #+END_SRC - based on Mem without MLPClassifier - switched to mlxtend StackedClassifier without folding - *Training data set*: spaCy-treated, with shorties trimmed- - overfits significantly -- LB Score of 0.42179 **** Phe #+BEGIN_SRC ipython :session :results output drawer from models.phe_model import Phe phe = Phe() phe.predict() #+END_SRC - based on Pe - removed minimax regularization of predictions - Increased the dimensions of MLPClassifier and squashed the pyramidal architecture of hidden layers - Decreased the batch_size of MLPClassifier - *Training data set*: spaCy-treated, with shorties trimmed- - overfits extremely -- LB Score of 6.29678, consistent with Mem's poor result, which is probably due to the addition of MLPClassifier **** Tsadi #+BEGIN_SRC ipython :session :results output drawer from models.tsadi_model import Tsadi tsadi = Tsadi() tsadi.predict() #+END_SRC - based on Nun (switched to LSTM) - removed one Dense layer - decreased the dropout rate for the question branches - *Training data set*: spaCy-treated, with shorties trimmed - was not run due to the computational cost and realisation that more careful base data selection is necessary **** Quf #+BEGIN_SRC ipython :session :results output drawer from models.quf_model import Quf quf = Quf() quf.predict() #+END_SRC - based on Yud - changed the base dataframe to the train file with minimal preprocessing - excluded magic2 and kcore features - added Abhishek's ~euclidean_distance~ and ~jaccard_distance~ - *Training data set*: vanilla #+BEGIN_SRC ipython :session :results output drawer from magic import Magic TRAIN_DATA_FILENAME = "vanilla_train" m = Magic(train_data_filename=TRAIN_DATA_FILENAME) m.spell() from custom.features import CustomFeatures cf = CustomFeatures(train_data_filename=TRAIN_DATA_FILENAME) cf.run() #+END_SRC Indeed, Abhishek's ~euclidean_distance~ and ~jaccard_distance~ are the least significant features and may just add a noise to the model. The score received was 0.23158, which is an improvement from Tet's 0.24224. It seems that ~vanilla_train~ gives better results. [[file:img/2017-06-04-1949-0.180881032724_quf_xgboost-feature_importance.png]] **** Attempt at Ensembling We ensemble our best model for the fourth time. #+BEGIN_SRC ipython :session :results output drawer predPath = "ensembling/" n = 9 weights = { # PB Scores are given above the file name #0.23158 predPath + "0.180881032724_quf_xgboost.csv":n, #0.24224 predPath + "0.222026460989_tet_xgboost.csv":n-1, #0.24253 predPath + "0.214863010529_kaf_xgboost.csv":n-2, #0.24360 predPath + "0.2200_vav_198_0.46.csv":n-3, #0.24505 predPath + "0.212772886818_samech_xgboost.csv":n-4, #0.24602 predPath + "0.2346_gimmel_214_0.42.csv":n-5, #0.24785 predPath + "0.319533794385_lamed_xgboost.csv":n-6, #0.24801 predPath + "0.2364_dalet_242_0.32.csv":n-7, #0.24987 predPath + "0.1339_nun_407_0.47.csv":n-8 } ensemble(weights, "wam_ensemble_3.csv") #+END_SRC Again, this boosts us to 0.20767, which is an improvement from the ~wam_ensemble1.csv~ result of 0.20882. *** Feature Engineering The dilettante model meddling did not give satisfactory results. We need more features. **** NLTK Investigations - measuring shares of hypernyms and lemmas, as well as linear and smooth counts of synonyms - cross path similarity is not computed due to the high computational cost We will plot how good the features are for the discrimination between dupes and non-dupes. #+BEGIN_SRC ipython :session :results output drawer import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt BASE_DIR = 'data/' TRAIN_DATA_FILENAME = "vanilla_train" TRAIN_DATA_FILE = BASE_DIR + TRAIN_DATA_FILENAME + '.csv' CUSTOM_FEATURES_TRAIN = 'custom/' + \ TRAIN_DATA_FILENAME + \ "-nltk-train.csv" df_train = pd.read_csv(TRAIN_DATA_FILE, encoding="utf-8") nltk_features = pd.read_csv(CUSTOM_FEATURES_TRAIN, encoding="utf-8") ix_is_dup = np.where(df_train['is_duplicate'] == 1)[0] ix_not_dup = np.where(df_train['is_duplicate'] == 0)[0] def plot_feature_graph(data, fname): fig = plt.figure() ax1 = plt.subplot2grid((3, 2), (0, 0), colspan=2) ax2 = plt.subplot2grid((3, 2), (1, 0), colspan=2) ax1.set_title('Distribution of %s' % fname, fontsize=16) sns.distplot(data[fname], bins=50, ax=ax1) sns.distplot(data.loc[ix_is_dup][fname], bins=50, ax=ax2, label='duplicate') sns.distplot(data.loc[ix_not_dup][fname], bins=50, ax=ax2, label='non-duplicate') ax2.legend(loc='upper right', prop={'size': 14}) plt.savefig("img/{}.png".format(fname)) plt.show() #+END_SRC #+BEGIN_SRC ipython :session :results output drawer plot_feature_graph(nltk_features,"hypernyms_share") plot_feature_graph(nltk_features,"linear_synonyms_count") plot_feature_graph(nltk_features,"smooth_synonyms_count") plot_feature_graph(nltk_features,"lemmas_share") #+END_SRC [[file:img/hypernyms_share.png]] [[file:img/linear_synonyms_count.png]] [[file:img/smooth_synonyms_count.png]] [[file:img/lemmas_share.png]] We can see that ~linear_synonyms_count~ and ~smooth_synonyms_count~ allow almost no differentiation at all, so we exclude them from our models. **** Properties of words Texts with similar contents might have similar syllables and character frequences. #+BEGIN_SRC ipython :session :results output drawer wordies_features = pd.read_csv('custom/' + TRAIN_DATA_FILENAME + "-wordies-train.csv", encoding="utf-8") plot_feature_graph(wordies_features,"character_freq") plot_feature_graph(wordies_features,'syllable_similarity') #+END_SRC [[file:img/character_freq.png]] [[file:img/syllable_similarity.png]] Here we may discard the character frequencies, since they are almost completely overlapping. **** Testing word and synonym properties ***** Resh #+BEGIN_SRC ipython :session :results output drawer from models.resh_model import Resh resh = Resh() resh.predict() #+END_SRC - based on Quf, with magic2 and kcore features included - *Training data set*: vanilla [[file:img/2017-06-05-0748-0.180005762885_resh_xgboost-feature_importance.png]] - Resh model scores 0.23214, which is worse than Quf, and the ~character_freq~ seems to be important despite the massive overlap. Probably ~euclidean_distance~, ~jaccard_distance~ and ~character_freq~ contribute to the decline of the model generalisability. **** Counts Investigation Now we try to measure several set statistics: Wagner-Fischer, MostFreqKSDF, Tversky index, weighted difference, stem frequencies, and stem sharing. #+BEGIN_SRC ipython :session :results output drawer from custom.counts_investigation import CountsFeatures c = CountsFeatures() c.run() #+END_SRC #+BEGIN_SRC ipython :session :results output drawer env_features = pd.read_csv('custom/' + TRAIN_DATA_FILENAME + "-counts-train.csv", encoding="utf-8") plot_feature_graph(env_features,"wagner_fischer") plot_feature_graph(env_features,'wagner_fischer_reverse') plot_feature_graph(env_features,'most_freq_2_sdf') plot_feature_graph(env_features,'most_freq_2_sdf_reverse') plot_feature_graph(env_features,'most_freq_2_sdf_reverse') plot_feature_graph(env_features,'stems_freq') plot_feature_graph(env_features,'stems_share') plot_feature_graph(env_features,'stems_weighted_difference') plot_feature_graph(env_features,'stems_tversky_index') del env_features #+END_SRC [[file:img/wagner_fischer.png]] [[file:img/wagner_fischer_reverse.png]] [[file:img/most_freq_2_sdf_reverse.png]] [[file:img/most_freq_2_sdf_reverse.png]] [[file:img/most_freq_2_sdf_reverse.png]] [[file:img/stems_freq.png]] [[file:img/stems_share.png]] [[file:img/stems_weighted_difference.png]] [[file:img/stems_tversky_index.png]] Only the last four features seem to be worthy to be included in our next model. **** Environment Investigation Now let's represent each sentence as one continuous string of stemmed words and calculate several similarity statistics on them. #+BEGIN_SRC ipython :session :results output drawer from custom.environment_investigation import EnvFeatures e = EnvFeatures() e.run() #+END_SRC #+BEGIN_SRC ipython :session :results output drawer env_features = pd.read_csv('custom/' + TRAIN_DATA_FILENAME + "-env-train.csv", encoding="utf-8").fillna(0) plot_feature_graph(env_features,'kendall_tau') plot_feature_graph(env_features,'kendall_p_value') plot_feature_graph(env_features,'string_similarity') plot_feature_graph(env_features,'subset_count') del env_features #+END_SRC [[file:img/kendall_tau.png]] [[file:img/kendall_p_value.png]] [[file:img/string_similarity.png]] [[file:img/subset_count.png]] ~kendall_p_value~ and ~string_similarity~ seem to be worthy to be included into the model. **** Testing word, synonym, counts and environment properties ***** Shin #+BEGIN_SRC ipython :session :results output drawer from shin_model import Shin s = Shin() s.predict() #+END_SRC - based on Resh, with counts and environment features included - *Training data set*: vanilla [[file:img/2017-06-06-0147-0.178758220484_shin_xgboost-feature_importance.png]] The added features seem to be important and blend well with the rest, giving a score of 0.22808, which is an improvement from Quf and Resh. **** Az Investigation Questions with similar places mentioned seem to be more likely to share the same intent. We use the Movehub City Rankings [[https://www.kaggle.com/blitzr/movehub-city-rankings][data]] to fetch the country names together with some cities. #+BEGIN_SRC ipython :session :results output drawer from custom.investigation_az import AzFeatures az = AzFeatures() az.run() #+END_SRC #+BEGIN_SRC ipython :session :results output drawer az_features = pd.read_csv('custom/' + TRAIN_DATA_FILENAME + "-az-train.csv", encoding="utf-8").fillna(0.0) plot_feature_graph(az_features,'places_share') # All zeroes. #plot_feature_graph(az_features,'places_difference') plot_feature_graph(az_features,'places_prevalence') del az_features #+END_SRC [[file:img/places_share.png]] [[file:img/places_prevalence.png]] There is some differentiation, but not that much -- not many questions mention any countries at all. **** Buky Investigation What can Word2Vec embeddings trained on Google News tell us? We introduce multiple features, all based on the vectors corresponding to each word in the questions. #+BEGIN_SRC ipython :session :results output drawer from custom.investigation_buky import BukyFeatures buky = BukyFeatures() buky.run() #+END_SRC **** Collins & Duffy We feed [[https://www.kaggle.com/c/quora-question-pairs/discussion/32334][Collins and Duffy's data]] in the hopes of retrieving some information affecting the duplicity. #+BEGIN_SRC ipython :session :results output drawer from custom.duffy import Duffy d = Duffy() d.build() #+END_SRC **** Pagerank We compute pageranks, [[https://www.kaggle.com/zfturbo/pagerank-on-quora-feature-file-generator/code][inspired by @ZFTurbo]]. #+BEGIN_SRC ipython :session :results output drawer from custom import pagerank #+END_SRC *** New Models Now we are ready to combine the features and feed them to our model **** Tav #+BEGIN_SRC ipython :session :results output drawer from tav_model import Tav t = Tav() t.predict() #+END_SRC - based on Shin, with the computed features included - *Training data set*: vanilla [[file:img/2017-06-06-2327-0.174476734017_tav_xgboost-feature_importance.png]] Luckily, we have found features which are more important than the ones used previously. Tav scores us 0.20662. **** Sin With not much time under our belts, let's train something more lightweight than LSTM but hopefully more accurate than vanilla NN. #+BEGIN_SRC ipython :session :results output drawer from sin_model import Sin s = Sin() s.predict() #+END_SRC - based on Khet (Convolutional1D with the custom features fed through a hidden layer) - all the best features included - trained for 5 total epochs due to time constraints, with a break in-between to change the number of epochs - *Training data set*: vanilla [[file:img/2017-06-07-0229-sin_323_0.46-accuracy_dynamics.png]] The model seems to be very stable, with some overfitting occuring by the end of the third epoch. Sin yields 0.22727, which is worse than Tav. **** WAM Ensemble 4 While Sin is being evaluated, let's ensemble what we have so far and see what we get. #+BEGIN_SRC ipython :session :results output drawer def ensemble(weights, output): import glob import pandas as pd weighted_models = [] for file_name in glob.glob(predPath + '*.csv'): df = pd.read_csv(file_name, encoding = "utf-8") weighted_models.append((df['is_duplicate'],weights[file_name])) wam_df = df['is_duplicate'] * 0.0 for model, weight in weighted_models: wam_df += model * weight wam_df /= sum(weights.values()) wam_df = pd.concat([df['test_id'], wam_df], axis=1) wam_df.to_csv(output, index=False) predPath = "ensembling/" n = 9 weights = { # PB Scores are given above the file name #0.20662 predPath + "0.174476734017_tav_xgboost.csv":n+5, #0.20767 predPath + "wam_ensemble_3.csv":n+4, #0.21736 predPath + "gm_ensemble.csv":n+3, #0.22808 predPath + "0.178758220484_shin_xgboost.csv":n+2, #0.23158 predPath + "0.180881032724_quf_xgboost.csv":n+1, #0.23214 predPath + "0.180005762885_resh_xgboost.csv":n, #0.24224 predPath + "0.222026460989_tet_xgboost.csv":n-1, #0.24253 predPath + "0.214863010529_kaf_xgboost.csv":n-2, #0.24360 predPath + "0.2200_vav_198_0.46.csv":n-3, #0.24505 predPath + "0.212772886818_samech_xgboost.csv":n-4, #0.24602 predPath + "0.2346_gimmel_214_0.42.csv":n-5, #0.24785 predPath + "0.319533794385_lamed_xgboost.csv":n-6, #0.24801 predPath + "0.2364_dalet_242_0.32.csv":n-7, #0.24987 predPath + "0.1339_nun_407_0.47.csv":n-8 } ensemble(weights, "wam_ensemble_4.csv") #+END_SRC The ensembling gives us an improvement of more than 0.005. **** WAM Ensemble 5 Finally, let's apply the trick which we used in our first ensembling. First, let's compute the GM ensemble of all the models which scored under 0.25. #+BEGIN_SRC ipython :session :results output drawer import glob import pandas as pd from scipy.stats.mstats import gmean predPath = "ensembling/" models = [] for file_name in glob.glob(predPath + '*.csv'): df = pd.read_csv(file_name, encoding = "utf-8") models.append(df['is_duplicate']) total = pd.concat(models, axis=1) gm_df = pd.DataFrame() gm_df['is_duplicate'] = gmean(total, axis=1) gm_df['test_id'] = df['test_id'] gm_df.to_csv("gm_ensemble_1.csv", index=False) #+END_SRC Then we combine the three best models and take a weighted mean: #+BEGIN_SRC ipython :session :results output drawer def ensemble(weights, output): import glob import pandas as pd weighted_models = [] for file_name in glob.glob(predPath + '*.csv'): df = pd.read_csv(file_name, encoding = "utf-8") weighted_models.append((df['is_duplicate'],weights[file_name])) wam_df = df['is_duplicate'] * 0.0 for model, weight in weighted_models: wam_df += model * weight wam_df /= sum(weights.values()) wam_df = pd.concat([df['test_id'], wam_df], axis=1) wam_df.to_csv(output, index=False) predPath = "ensembling/" n = 3 weights = { # PB Scores are given above the file name #0.20662 predPath + "0.174476734017_tav_xgboost.csv":n, #0.20767 predPath + "wam_ensemble_4.csv":n-1, #0.21067 predPath + "gm_ensemble_1.csv":n-2 } ensemble(weights, "wam_ensemble_5.csv") #+END_SRC The final ensemble scores 0.19638 on the public LB and 0.20098 on the private LB. ** Conclusion QQP was a superb experience for me -- data, tips and knowledge shared by the Kaggle community and all the other researchers working on NLP were indispensable in the present investigation of the data set. Given the fact that the only experience of machine learning I had before was limited to using automatic translators, the competition turned out to give valuable lessons in natural language processing and powerful prediction methods. Although the final score is not the best, it evidently shows that a dilettante with some free time, programming experience and wonderful receptive community can indeed beat the three quarters of participants. Key aspects of analysis which I would like to improve in the future include the following: - some graphs look awful and not particularly informative, especially the diagrams on the feature importance -- using violin or scatter plots might be more useful - there must be a way to extract features found by different neural networks and inject them into the new model to boost accuracy - a diverse range of models should be tested -- only three frameworks (XGBoost, Keras NN, Scikit stacking) were tested in the present analysis -- and looking at, for example, H2o and LightGBM may help - methodical approach to tweaking the model parameters should be adopted -- predictions here were fine-tuned based on the shaky understanding of the skimmed documentation, and a lot of them were added in bulk There is a lot to learn, produce and share.