Skip to content

Commit

Permalink
Release 0.4.88 (#64)
Browse files Browse the repository at this point in the history
* update lore init
* add support for downloading http
* add more keras base estimators
* rename estimators for consistency
* reverse transform the output after prediction
* update dependencies
* new command line feature: lore fit MODEL --score --epochs=10
* add scoring as an option with model fitting
* update timer logging to show nested timings
* handle towers in evaluation
* better unicode handling in logs
* Add a generic flask server to expose model predictions
  • Loading branch information
montanalow committed Feb 16, 2018
1 parent 8322e12 commit c371f94
Show file tree
Hide file tree
Showing 57 changed files with 922 additions and 260 deletions.
4 changes: 3 additions & 1 deletion LICENSE.txt
@@ -1,4 +1,6 @@
Copyright (c) 2017 Instacart
The MIT License (MIT)

Copyright (c) 2018 Instacart

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
4 changes: 2 additions & 2 deletions circle.yml
@@ -1,7 +1,7 @@
machine:
pre:
- cd /opt/circleci/.pyenv; git pull
- pyenv install -s 2.7.12
- pyenv install -s 2.7.14
- pyenv install -s 3.5.2
- pyenv install -s 3.6.2

Expand All @@ -13,7 +13,7 @@ dependencies:
- createdb lore_test
override:
- pip install tox tox-pyenv
- pyenv local 2.7.12 3.5.2 3.6.2
- pyenv local 2.7.14 3.5.2 3.6.2
- tox --notest

test:
Expand Down
10 changes: 6 additions & 4 deletions lore/__init__.py
Expand Up @@ -15,7 +15,7 @@
__copyright__ = 'Copyright © 2017, Instacart'
__credits__ = ['Montana Low', 'Jeremy Stanley', 'Emmanuel Turlay']
__license__ = 'MIT'
__version__ = '0.4.87'
__version__ = '0.4.88'
__maintainer__ = 'Montana Low'
__email__ = 'montana@instacart.com'
__status__ = 'Development Status :: 3 - Alpha'
Expand All @@ -28,16 +28,18 @@ def banner():
return '%s in %s on %s' % (
ansi.foreground(ansi.GREEN, env.project),
ansi.foreground(env.color, env.name),
ansi.foreground(ansi.CYAN,
getpass.getuser() + '@' + socket.gethostname())
ansi.foreground(
ansi.CYAN,
getpass.getuser() + '@' + socket.gethostname()
)
)


lore_no_env = False
if hasattr(sys, 'lore_no_env'):
lore_no_env = sys.lore_no_env

if len(sys.argv) > 1 and sys.argv[0][-4:] == 'lore' and sys.argv[1] in ['install', 'init']:
if len(sys.argv) > 1 and sys.argv[0][-4:] == 'lore' and sys.argv[1] in ['install', 'init', 'server']:
lore_no_env = True

if not lore_no_env:
Expand Down
256 changes: 232 additions & 24 deletions lore/__main__.py
Expand Up @@ -5,20 +5,28 @@
from builtins import input

import argparse
import datetime
import dateutil
import glob
from io import open
import importlib
import inspect
import json
import logging
import os
import sys
import platform
import re
import shutil
import subprocess
import logging
from io import open
import sys

import lore
from lore import ansi, env, util
from lore.util import timer, which

if not (sys.version_info.major == 3 and sys.version_info.minor >= 6):
ModuleNotFoundError = ImportError

logger = logging.getLogger(__name__)


Expand All @@ -33,11 +41,11 @@ def main(args=None):
parser.add_argument('--version', action='version',
version='lore %s' % lore.__version__)

commands = parser.add_subparsers( help='common commands')
commands = parser.add_subparsers(help='common commands')

init_parser = commands.add_parser('init', help='create a new lore project')
init_parser.set_defaults(func=init)
init_parser.add_argument('NAME', help='the name of the project')
init_parser.add_argument('name', metavar='NAME', help='the name of the project')
init_parser.add_argument('--git-ignore', default=True)
init_parser.add_argument('--python-version', default=None)

Expand Down Expand Up @@ -75,11 +83,91 @@ def main(args=None):
action='store_true'
)

task_parser = commands.add_parser(
'task',
help='run a task from the command line'
generate_parser = commands.add_parser(
'generate',
help='create a new model'
)
generators = generate_parser.add_subparsers()
scaffold_parser = generators.add_parser(
'scaffold',
)
scaffold_parser.set_defaults(func=generate_scaffold)
scaffold_parser.add_argument('name', metavar='NAME', help='name of the project')
scaffold_parser.add_argument(
'--keras',
help='create a keras scaffold',
action='store_true'
)

scaffold_parser.add_argument(
'--holdout',
help='create a holdout pipeline',
action='store_true'
)

model_parser = generators.add_parser(
'model',
)
model_parser.set_defaults(func=generate_model)
model_parser.add_argument('name', metavar='NAME', help='name of the model')
model_parser.add_argument(
'--keras',
help='inherit from lore.models.keras.Base',
action='store_true'
)

estimator_parser = generators.add_parser(
'estimator',
)
estimator_parser.set_defaults(func=generate_estimator)
estimator_parser.add_argument('name', metavar='NAME', help='name of the estimator')
estimator_parser.add_argument(
'--keras',
help='inherit from lore.estimators.keras.Base',
action='store_true'
)

pipeline_parser = generators.add_parser(
'pipeline',
)
pipeline_parser.set_defaults(func=generate_pipeline)
pipeline_parser.add_argument('name', metavar='NAME', help='name of the pipeline')
pipeline_parser.add_argument(
'--holdout',
help='inherit from lore.pipelines.holdout.Base',
action='store_true'
)

fit_parser = commands.add_parser(
'fit',
help="train models"
)
fit_parser.set_defaults(func=fit)
fit_parser.add_argument(
'model',
metavar='MODEL',
help='fully qualified model including module name. e.g. app.models.project.Model'
)
fit_parser.add_argument(
'--test',
help='calculate the loss on the prediction against the test set',
action='store_true'
)
fit_parser.add_argument(
'--score',
help='score the model, typically inverse of loss',
action='store_true'
)
fit_parser.add_argument(
'--upload',
help='upload model to store after fitting'
)

server_parser = commands.add_parser(
'server',
help='launch the flask server to provide an api to your models'
)
task_parser.set_defaults(func=task)
server_parser.set_defaults(func=server)

pip_parser = commands.add_parser(
'pip',
Expand Down Expand Up @@ -164,6 +252,101 @@ def api(parsed, unknown):
).start()


def fit(parsed, unknown):
module, klass = parsed.model.rsplit('.', 1)
try:
module = importlib.import_module('.', module)
Model = getattr(module, klass)
except (AttributeError, ModuleNotFoundError):
sys.exit(ansi.error() + ' "' + parsed.model + '" does not exist in this directoy! Are you sure you typed the fully qualified module.Class name correctly?')
print(ansi.success('FITTING ') + parsed.model)

model = Model()

def get_valid_args(method):
if hasattr(method, '__wrapped__'):
return get_valid_args(method.__wrapped__)
return inspect.getargspec(method)
valid_model_fit_args = get_valid_args(model.fit)
valid_estimator_fit_args = get_valid_args(model.estimator.fit)
valid_fit_args = valid_model_fit_args.args[1:] + valid_estimator_fit_args.args[1:]

def filter_private_attributes(dict):
return {k: v for k, v in dict.items() if k[0] != '_'}
model_attrs = filter_private_attributes(model.__dict__)
pipeline_attrs = filter_private_attributes(model.pipeline.__dict__)
estimator_attrs = filter_private_attributes(model.estimator.__dict__)
estimator_attrs.pop('model', None)

def cast_attr(value, default):
if isinstance(default, int):
return int(value)
elif isinstance(default, float):
return float(value)
elif isinstance(default, datetime.date):
return dateutil.parse(value).date()
elif isinstance(default, datetime.datetime):
return dateutil.parse(value)
else:
return value

# handle args passed with ' ' or '=' between name and value
attrs = [arg[2:] if arg[0:2] == '--' else arg for arg in unknown] # strip --
attrs = [attr.split('=') for attr in attrs] # split
attrs = [attr for sublist in attrs for attr in sublist] # flatten
grouped = list(zip(*[iter(attrs)] * 2)) # pair up

# assign args to their receivers
fit_args = {}
unknown_args = []
for name, value in grouped:
if name in model_attrs:
value = cast_attr(value, getattr(model, name))
setattr(model, name, value)
elif name in pipeline_attrs:
value = cast_attr(value, getattr(model.pipeline, name))
setattr(model.pipeline, name, value)
elif name in estimator_attrs:
value = cast_attr(value, getattr(model.estimator, name))
setattr(model.estimator, name, value)
elif name in valid_model_fit_args.args:
index = valid_model_fit_args.args.index(name)
from_end = index - len(valid_model_fit_args.args)
default = None
if from_end < len(valid_model_fit_args.defaults):
default = valid_model_fit_args.defaults[from_end]
fit_args[name] = cast_attr(value, default)
elif name in valid_estimator_fit_args.args:
index = valid_estimator_fit_args.args.index(name)
from_end = index - len(valid_estimator_fit_args.args)
default = None
if from_end < len(valid_estimator_fit_args.defaults):
default = valid_estimator_fit_args.defaults[from_end]
fit_args[name] = cast_attr(value, default)
else:
unknown_args.append(name)

if len(attrs) % 2 != 0:
unknown_args.append(attrs[-1])

if unknown_args:
msg = ansi.bold("Valid model attributes") + ": %s\n" % ', '.join(sorted(model_attrs.keys()))
msg += ansi.bold("Valid estimator attributes") + ": %s\n" % ', '.join(sorted(estimator_attrs.keys()))
msg += ansi.bold("Valid pipeline attributes") + ": %s\n" % ', '.join(sorted(pipeline_attrs.keys()))
msg += ansi.bold("Valid fit arguments") + ": %s\n" % ', '.join(sorted(valid_fit_args))

sys.exit(ansi.error() + ' Unknown arguments: %s\n%s' % (unknown_args, msg))

model.fit(score=parsed.score, test=parsed.test, **fit_args)
print(ansi.success() + ' Fitting: %i\n%s' % (model.fitting, json.dumps(model.stats, indent=2)))


def server(parsed, unknown):
args = [env.bin_flask, 'run'] + unknown
os.environ['FLASK_APP'] = os.path.join(os.path.dirname(__file__), 'www', '__init__.py')
os.execv(env.bin_flask, args)


def console(parsed, unknown):
install_jupyter_kernel()
sys.argv[0] = env.bin_jupyter
Expand All @@ -186,28 +369,27 @@ def execute(parsed, unknown):
def init(parsed, unknown):
template = os.path.join(os.path.dirname(__file__), 'template')

if os.path.exists(parsed.NAME):
sys.exit(ansi.error() + ' "' + parsed.NAME + '" already exists in this directoy!')
if os.path.exists(parsed.name):
sys.exit(ansi.error() + ' "' + parsed.name + '" already exists in this directoy! Lore can not create a new project with this name.')

shutil.copytree(template, parsed.NAME, symlinks=False, ignore=None)
os.chdir(parsed.NAME)
shutil.move('app', parsed.NAME)

shutil.copytree(template, parsed.name, symlinks=False, ignore=None)
os.chdir(parsed.name)
shutil.move('app', parsed.name)

requirements = '-e /Users/montanalow/repos/lore'
if unknown:
requirements += '[' + ','.join([r[2:] for r in unknown]) + ']'
with open('requirements.txt', 'wt') as file:
file.write('lore\n')
file.write(requirements)

if parsed.python_version:
python_version = parsed.python_version
else:
python_version = '3.6.3'
python_version = '3.6.4'

with open('runtime.txt', 'wt') as file:
file.write('python-' + python_version + '\n')
if sys.version_info[0] == 2:
reload(lore.env)
else:
import importlib
importlib.reload(lore.env)
importlib.reload(lore.env)
install(parsed, unknown)


Expand Down Expand Up @@ -235,6 +417,32 @@ def install(parsed, unknown):
install_tensorflow()


def generate_scaffold(parsed, unknown):
generate_model(parsed, unknown)
generate_estimator(parsed, unknown)
generate_pipeline(parsed, unknown)
generate_notebooks(parsed, unknown)


def generate_model(parsed, unknown):
pass


def generate_estimator(parsed, unknown):
pass


def generate_pipeline(parsed, unknown):
template = '''
'''
pass


def generate_notebooks(parsed, unknown):
pass


def task(parsed, unknown):
task = unknown[0]
print(ansi.error() + 'not implemented')
Expand Down Expand Up @@ -560,8 +768,7 @@ def install_requirements(args):


def install_jupyter_kernel():
import jupyter_core.paths
if os.path.exists(os.path.join(jupyter_core.paths.jupyter_data_dir(), 'kernels', env.project)):
if os.path.exists(env.jupyter_kernel_path):
return

print(ansi.success('INSTALL') + ' jupyter kernel')
Expand Down Expand Up @@ -597,6 +804,7 @@ def freeze_requirements():
needed = [m.group(1).lower() for l in missing for m in [regex.search(l)] if m]

added_index = present.index('## The following requirements were added by pip freeze:')
unsafe = None
if added_index:
added = present[added_index + 1:-1]
present = set(present[0:added_index])
Expand Down

0 comments on commit c371f94

Please sign in to comment.