DCausse has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/364638 )

Change subject: Add some feature engineering helper functions
......................................................................


Add some feature engineering helper functions

A couple small functions to assist with feature engineering,
and some documentation of how to create and test new features.

Change-Id: Ic87b0a48c94c5b8f449cebb496626cdb2cb90011
---
A docs/feature_engineering.rst
M mjolnir/feature_engineering.py
2 files changed, 218 insertions(+), 2 deletions(-)

Approvals:
  DCausse: Verified; Looks good to me, approved



diff --git a/docs/feature_engineering.rst b/docs/feature_engineering.rst
new file mode 100644
index 0000000..41416a0
--- /dev/null
+++ b/docs/feature_engineering.rst
@@ -0,0 +1,158 @@
+This document contains examples of testing new features with mjolnir to
+hopefully guide future experimentation with new features quickly, without
+having to import all the relevant data or create new fields in elasticsearch
+for the initial evaluation.
+
+Features that seem to be useful should then be implemented as part of the
+elasticsearch feature collection pipeline and re-verified.
+
+
+Evaluating new query independent features
+=========================================
+
+The example calculates a new query independent feature in mysql and appends it
+to an existing test and train dataset.
+
+Calculate a potential new feature from mysql. It is important to check a mysql 
`explain` before
+running your feature collction query to ensure it is not overly expensive. 
Visiting every row
+of the enwiki revisions table for example would take perhaps hours::
+
+    echo 'select rev_page, min(rev_timestamp) from enwiki.revision group by 
rev_page' | \
+        mysql -sN 
--defaults-extra-file=/etc/mysql/conf.d/analytics-research-client.cnf -h 
analytics-store > page_min_timestamp
+
+Open a pyspark shell with mjolnir::
+
+    PYSPARK_PYTHON=venv/bin/python SPARK_CONF_DIR=/etc/spark/conf 
~/spark-2.1.0-bin-hadoop2.6/bin/pyspark \
+        --jars /home/ebernhardson/mjolnir-0.1-jar-with-dependencies.jar \
+        --driver-class-path 
/home/ebernhardson/mjolnir-0.1-jar-with-dependencies.jar \
+        --master yarn \
+        --files /usr/lib/libhdfs.so.0.0.0 \
+        --archives 'mjolnir_venv.zip#venv'
+
+Load the feature file into a dataframe. Specifying the schema allows naming 
the columns
+and ensuring they are converted to the correc types::
+
+    import pyspark.sql.types
+    df_mwts = sqlContext.read.csv(
+        '/path/to/page_min_timestamp',
+        sep='\t', schema=pyspark.sql.types.StructType([
+            pyspark.sql.types.StructField('hit_page_id', 
pyspark.sql.types.IntegerType()),
+            pyspark.sql.types.StructField('mwts', 
pyspark.sql.types.StringType()),
+        ]))
+
+Munge the feature from a mediawiki timestamp into a normal unix timestamp::
+
+    import datetime
+    from pyspark.sql import functions as F
+    import time
+    mwts_to_ts_udf = F.udf(lambda mwts: 
time.mktime(datetime.datetime.strptime(mwts, '%Y%m%d%H%M%S').timetuple()), 
pyspark.sql.types.FloatType())
+    df_page_created = df_mwts.select('hit_page_id', 
mwts_to_ts_udf('mwts').alias('page_created'))
+
+Load existing df generated by data_pipeline.py and merge new feature::
+
+    df_train = 
sqlContext.read.parquet('hdfs://analytics-hadoop/user/ebernhardson/training_size/train_1193k')
+    df_test = 
sqlContext.read.parquet('hdfs://analytics-hadoop/user/ebernhardson/training_size/test')
+
+Append the new feature to existing features::
+
+    import mjolnir.feature_engineering
+    def merge(df):
+        df_joined = df.join(df_page_created, how='inner', on=['hit_page_id'])
+        return mjolnir.feature_engineering.append_features(df_joined, 
'page_created').drop('page_created')
+
+    
merge(df_train).write.parquet('hdfs://analytics-hadoop/user/ebernhardson/mjolnir/1193k_with_page_created')
+    
merge(df_test).write.parquet('hdfs://analytics-hadoop/user/ebernhardson/mjolnir/test_with_page_created')
+
+Now we have two new datasets that contain the new feature, one for training 
and one for evaluation. The training set can be fed into training_pipeline.py 
as the --input parameter. The test set can be provided as --test-dir to be 
evaluated against the model after training is complete.
+
+Calculating a new query dependent feature from the elasticsearch analyze api
+============================================================================
+
+Open a pyspark shell with mjolnir::
+
+    PYSPARK_PYTHON=venv/bin/python SPARK_CONF_DIR=/etc/spark/conf 
~/spark-2.1.0-bin-hadoop2.6/bin/pyspark \
+        --jars /home/ebernhardson/mjolnir-0.1-jar-with-dependencies.jar \
+        --driver-class-path 
/home/ebernhardson/mjolnir-0.1-jar-with-dependencies.jar \
+        --master yarn \
+        --files /usr/lib/libhdfs.so.0.0.0 \
+        --archives 'mjolnir_venv.zip#venv'
+
+Load all the relevant query strings into an rdd::
+
+    df_1193k = 
sqlContext.read.parquet('hdfs://analytics-hadoop/user/ebernhardson/mjolnir/training_size/train_1193k')
+    df_test = 
sqlContext.read.parquet('hdfs://analytics-hadoop/user/ebernhardson/mjolnir/training_size/test')
+    rdd_queries = sc.union([df.select('query').rdd for df in [df_1193k, 
df_test]).distinct()
+
+Define a function that will iterate over rows in a partition and collect our
+new feature. Because the query issued is increadibly light weight and the
+execution time is dominated by round trips between datacenters it utilizes a
+thread pool to further parallelize the work on each partition. Note that when
+using pyspark in a shell it has issues automagically importing from multiple
+levels, so the `import multiprocessing.dummy` has to go inside the function run
+on each worker. Additionally a `requests.Session` is used to ensure http
+keep-alive is used, and one is created per-thread as it is not thread safe.::
+
+    import json
+    import requests
+    import random
+    import threading
+    def gen_collect_partition(index, analyzer)
+        def f(rows):
+            import multiprocessing.dummy
+            url = 'http://elastic%d.codfw.wmnet:9200/%s/_analyze' % 
(random.randint(2001, 2035), index)
+            tl = threading.local()
+            def init():
+                tl.session = requests.Session()
+            def process_one(row):
+                res = tl.session.get(url, data=json.dumps({
+                    'analyzer': analyzer,
+                    'text': row.query,
+                })
+                return (row.query, len(res.json()['tokens']))
+            pool = multiprocessing.dummy.Pool(10, init)
+            return pool.imap_unordered(rows)
+        return f
+
+Apply that function to each partition of the rdd. This is somewhat expensive to
+compute, taking ~75s against 500k unique queries in ~30 partitions, and it will
+be used multiple times so we cache it in memory::
+
+    df_num_plain_tokens = 
rdd.mapPartitions(gen_collect_partition('enwiki_content', 
'text_search')).toDF(['query', 'num_text_tokens'])
+    df_num_text_tokens = 
rdd.mapPartitions(gen_collect_partition('enwiki_content', 
'plain_search')).toDF(['query', 'num_plain_tokens'])
+    df_num_tokens = df_num_plain_tokens.join(df_num_text_tokens, how='inner', 
on=['query']).cache()
+
+Merge our new feature into the existing data sets and write them out to hdfs::
+
+    import mjolnir.feature_engineering
+    def merge(df):
+        df_joined = df.join(df_num_tokens, how='inner', on=['query'])
+        return mjolnir.feature_engineering.append_features(df_joined, 
'num_text_tokens', 
'num_plain_tokens').drop('num_query_tokens').drop('num_plain_tokens')
+
+    
merge(df_1193k).write.parquet('hdfs://analytics-hadoop/user/ebernhardson/mjolnir/1193k_with_num_query_tokens')
+    
merge(df_test).write.parquet('hdfs://analytics-hadoop/user/ebernhardson/mjolnir/test_with_num_query_tokens')
+
+Again we have two new datasets with an additional feature that can be evaluated
+with training_pipeline.py. These new features can be tested together directly::
+
+    PYSPARK_PYTHON=venv/bin/python SPARK_CONF_DIR=/etc/spark/conf 
~/spark-2.1.0-bin-hadoop2.6/bin/spark-submit \
+        --jars /home/ebernhardson/mjolnir-0.1-jar-with-dependencies.jar \
+        --driver-class-path 
/home/ebernhardson/mjolnir-0.1-jar-with-dependencies.jar \
+        --master yarn \
+        --files /usr/lib/libhdfs.so.0.0.0 \
+        --conf spark.dynamicAllocation.maxExecutors=105 \
+        --conf spark.sql.autoBroadcastJoinThreshold=-1 \
+        --conf spark.task.cpus=4 \
+        --conf spark.yarn.executor.memoryOverhead=1536 \
+        --executor-memory 2G i\
+        --executor-cores 4 \
+        --archives 'mjolnir_venv.zip#venv' \
+        venv/lib/python2.7/site-packages/mjolnir/cli/training_pipeline.py \
+        -i 
hdfs://analytics-hadoop/user/ebernhardson/mjolnir/1193k_with_num_query_tokens \
+        -o /home/ebernhardson/training_size/1193k_with_num_query_tokens \
+        -t 
hdfs://analytics-hadoop/user/ebernhardson/mjolnir/test_with_num_query_tokens
+        -w 1 -c 100 -f 5 enwiki
+
+The new features can also be tested individually by using `--zero-feature`
+argument to training_pipeline.py to zero out the feature not being evaluated.
+
+
diff --git a/mjolnir/feature_engineering.py b/mjolnir/feature_engineering.py
index 8b942d2..8301547 100644
--- a/mjolnir/feature_engineering.py
+++ b/mjolnir/feature_engineering.py
@@ -1,12 +1,70 @@
+"""Helpful utilities for feature engineering"""
 import numpy as np
+import mjolnir.spark
 from pyspark.ml.linalg import Vectors, VectorUDT
+from pyspark.sql import functions as F
+import pyspark.sql.types
+
 
 def append_features(df, *cols):
+    """Append features from columns to the features vector.
+
+    Parameters
+    ----------
+    df : pyspark.sql.DataFrame
+    cols : list of str
+
+    Returns
+    -------
+    pyspark.sql.DataFrame
+    """
     def add_features(feat, *other):
         raw = feat.toArray()
         return Vectors.dense(np.append(raw, map(float, other)))
     add_features_udf = F.udf(add_features, VectorUDT())
     new_feat_list = df.schema['features'].metadata['features'] + cols
-    return df.withColumn('features', mjolnir.spark.add_meta(df._sc, 
add_features_udf('features', *cols),
-                                                            {'features': 
new_feat_list}))
+    return df.withColumn('features', mjolnir.spark.add_meta(
+        df._sc, add_features_udf('features', *cols), {'features': 
new_feat_list}))
 
+
+def zero_features(df, *feature_names):
+    """Zero out features in the feature vector.
+
+    Parameters
+    ----------
+    df : pyspark.sql.DataFrame
+    feature_names : list of str
+
+    Returns
+    -------
+    pyspark.sql.DataFrame
+    """
+    features = df.schema['features'].metadata['features']
+    idxs = [features.index(name) for name in feature_names]
+    def zero_features(feat):
+        raw = feat.toArray()
+        for idx in idxs:
+            raw[idx] = 0.
+        return Vectors.dense(raw)
+    zero_features_udf = F.udf(zero_features, VectorUDT())
+    return df.withColumn('features', mjolnir.spark.add_meta(
+        df._sc, zero_features_udf('features'), {'features': features}))
+
+
+def explode_features(df):
+    """Convert feature vector into individual columns
+
+    Parameters
+    ----------
+    df : pyspark.sql.DataFrame
+
+    Returns
+    -------
+    pyspark.sql.DataFrame
+    """
+    features = df.schema['features'].metadata['features']
+    def extract_feature(features, idx):
+        return features.toArray()[idx]
+    extract_feature_udf = F.udf(extract_feature, pyspark.sql.types.FloatType())
+    cols = [extract_feature_udf('features', F.lit(idx)).alias(name) for idx, 
name in enumerate(features)]
+    return df.select('*', *cols)

-- 
To view, visit https://gerrit.wikimedia.org/r/364638
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ic87b0a48c94c5b8f449cebb496626cdb2cb90011
Gerrit-PatchSet: 2
Gerrit-Project: search/MjoLniR
Gerrit-Branch: master
Gerrit-Owner: EBernhardson <[email protected]>
Gerrit-Reviewer: DCausse <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to