It’s an honor to present at Red Hat Summit again this year! I’m giving a brief introduction to machine learning concepts for developers. Of course, one can’t do justice to such a broad topic in a forty-minute session, but I have some materials for people who’d like to experiment with some fundamental ML techniques on their own time.
These materials are all presented as Jupyter notebooks, which combine code, narrative explanations, and output. These notebooks mean that you can inspect code, run it, change it, and experiment with it. The main thing to know about Jupyter is that notebooks are made up of cells, and pressing shift+enter will run the cell you’re currently on and move to the next one. If you get stuck, you can go up to the “Kernel” menu, and select “Restart and clear output.”
First up, this notebook can be run directly in your browser through the mybinder.org service – it presents an introduction to the scalable analytic techniques I mentioned in the beginning of the session.
If you’d like to dive deeper into specific machine learning techniques, you’ll need to fire up OpenShift:
- log in to an OpenShift cluster, or create a temporary one on your local machine with
oc cluster up.
- create a pod to serve some more example notebooks with
oc new-app radanalyticsio/workshop-notebook -e JUPYTER_NOTEBOOK_PASSWORD=developer, and
- expose a route to that pod with
oc expose workshop-notebook.
When you visit the route for the Jupyter pod, you’ll need to log in. The password is
developer. After you log in, you’ll be presented a with a list of notebook files. Here’s what each of them contain:
ml-basics.ipynbcontains visual explanations and examples of clustering, classification, and regression using Apache Spark,
pyspark.ipynbintroduces data engineering and data cleaning using Apache Spark and shows you how to train a natural language model on a data set from an open-source project,
var.ipynbshows you how to model data and run Monte Carlo simulations with Apache Spark using an example from the financial domain.
You’re at the beginning of a really exciting journey! I hope these resources are helpful as you get started.
I’m thrilled to be in Brno for DevConf again! This year I’m speaking about structures and techniques for scalable data processing, and this post is a virtual handout for my talk.
- Here’s a Jupyter notebook containing all of the code I discussed in the talk, complete with a few exercises and some links to other resources.
- There are lots of implementations of these techniques that you can use in production. Apache Spark, for example, uses some of these structures to support library operations like aggregates in structured queries. Algebird provides scalable and parallel implementations of all of the techniques I discussed and it is especially nice if you’re an algebraically-inclined fan of functional programming (guilty as charged).
- A very cool data structure that I didn’t have time to talk about is the t-digest, which calculates approximate cumulative distributions (so that you can take a stream of metric observations and ask, e.g., what’s the median latency, or the latency at the 99th percentile?) My friend Erik Erlandson has a really elegant scala implementation of the t-digest and has also given several great talks on the t-digest and some really clever applications for scalable cumulative distribution estimates. Start with this one.
- radanalytics.io is a community effort to enable scalable data processing and intelligent applications on OpenShift, including tooling to manage compute resources in intelligent applications and a distribution of Apache Spark for OpenShift.
My slides are available here.
My team recently agreed that it would improve the usability of our main Trello board if we moved lists containing cards we’d completed in previous years to archival boards. The idea was that those lists and cards would still be searchable and accessible but that they wouldn’t be cluttering our view of our current work. I moved very old lists late in November, moved all of our lists from 2017 at the beginning of this week, and prepared to bask in a web page containing only recent virtual index cards.
My basking ended abruptly, as baskings are wont to do. In this case, the abrupt end was occasioned by an offhand question from a colleague:
“By the way, what’s the deal with me getting removed from all of my old cards?”
I looked at the Trello board I’d created to archive activity from 2017 and saw that the only cards that had a member attached were my cards. Even though I’d made the archive board visible to the whole team, every other person on the team was removed from her cards when I moved the lists.
Now, I’m not a Trello expert and hope I’ll never become one. It may be that removing users from cards on boards they don’t belong to is actually the correct behavior. However, having such a drastic side effect occur without warning is absolutely user-hostile.1
Since software is rarely content to injure without also insulting, Trello also insinuated that I had explicitly removed my colleagues from their cards, like this:
So not only had I screwed up our team’s task history, but I looked like a jerk with too much free time.
Fortunately, all of those spurious explicit removals gave me a way to start unwinding the mess. Those member removals were captured in the
actions log for each card as
removeMemberFromCard actions; I was able to see them by exporting cards as JSON:2
Trello provides a pretty decent API, so I got to work. (The official Trello Python client appears to lack support for Python 3; I used py-trello instead.) My basic approach was to look for
removeMemberFromCard actions that had happened since just before I moved the lists, identify the removed members from each card, and then add them back to the card.
I was able to get our history restored pretty quickly. Here are some of the minor snags I hit with the Trello API and how I worked around them:
- By default, querying for actions on cards only returns card-creation actions and comments. You will need to specify an explicit action type filter to the API (e.g.,
all) in order to get all relevant actions.
- Even though I cached results and thought I took adequate care to avoid Trello’s rate limits, I found myself regularly getting rate-limited at the
/1/membersendpoint while resolving from member IDs to
Memberobjects to pass to the
add_memberfunction on a card. I was able to work around this by converting the
dictcorresponding to the member in the action to a
namedtupleinstance, which acted enough like a
Memberobject to do the trick.3
- Some cards didn’t have the
removeMemberFromCardactions. This actually seems like a Trello bug, but I was able to work around it by adding everyone who had ever been added to a card but wasn’t currently on it. This means that there may be some people spuriously ascribed to cards now (i.e., people who should have been explicitly removed from cards), but I think it’s better to have slightly lower precision than virtually zero recall in this application. (Also, our team’s practice is to only add members to cards when they’re in progress or complete, which minimizes the potential impact here.)
My code, which is quick, dirty, and profoundly underengineered, is available for your review. To use it, you’ll need a Trello API key, OAuth secret, and token, all of which you can get from Trello’s developer site.
The code is certainly not that broadly useful but hopefully the takeaway lesson is: you can recover from a lot of application bugs and misfeatures if your data model explicitly tracks state changes.4 It may even be worth going to a data representation that explicitly allows rollback in some cases. Finally, If you expose a way to inspect history with your API, users can even recover from your bugs without your help.
Could Trello have asked if I wanted to invite users to the board? Told me I’d be removing member ascriptions from the cards before moving? It certainly seems like it should have. ↩
I’ve redacted unnecessary information, including usernames, member IDs, and other IDs. ↩
Hooray for the Wild West of untyped languages, eh? ↩
It’s almost as if those wacky functional programming zealots have a point about their persistent data structures. ↩
This post is also available as an interactive notebook.
Consider the following problem: you’d like to enable users to automatically extract a function from a Jupyter notebook and publish it as a service. Actually serializing a closure from a function in a given environment is not difficult, given the
cloudpickle module. But merely constructing a serialized closure isn’t enough, since in general this function may require other modules to be available to run in another context.
Therefore, we need some way to identify the modules required by a function (and, ultimately, the packages that provide these modules). Since engineering time is limited, it’s probably better to have an optimistic-but-incomplete (or unsound) estimate and allow users to override it (by supplying additional dependencies when they publish a function) than it is to have a sound and conservative module list.1
modulefinder module in the Python standard library might initially seem like an attractive option, but it is unsuitable because it operates at the level of scripts. In order to use
modulefinder on a single function from a notebook, we’d either have an imprecise module list (due to running the whole notebook) or we’d need to essentially duplicate a lot of its effort in order to slice backwards from the function invocation so we could extract a suitably pruned script.
Fortunately, you can interrogate nearly any property of any object in a Python program, including functions. If we could inspect the captured variables in a closure, we could identify the ones that are functions and figure out which modules they were declared in. That would look something like this:
inspect module provides a friendly interface to inspecting object metadata. In the above function, we’re constructing a worklist of all of the captured variables in a given function’s closure. We’re then constructing a set of all of the modules directly or transitively referred to by those captured variables, whether these are modules referred to directly, modules declaring functions referred to by captured variables, or modules declaring other values referred to by captured variables (e.g., native functions). Note that we add any functions we find to the worklist (although we don’t handle
eval or other techniques), so we’ll capture at least some of the transitive call graph in this case.
This approach seems to work pretty sensibly on simple examples:
It also works on itself, which is a relief:
While these initial experiments are promising, we shouldn’t expect that a simple approach will cover everything we might want to do. Let’s look at a (slightly) more involved example to see if it breaks down.
We’ll use the k-means clustering implementation from scikit-learn to optimize some cluster centers in a model object. We’ll then capture that model object in a closure and analyze it to see what we might need to import to run it elsewhere.
So far, so good. Let’s say we want to publish this simple model as a lighter-weight service (without a scikit-learn dependency). We can get that by reimplementing the
predict method from the k-means model:
What do we get if we analyze the second method?
This is a problem! We’d expect that
norm would be a captured variable in the body of
km_predict_two (and thus that
numpy.linalg would be listed in its module frontier), but that isn’t the case. We can inspect the closure variables:
We can see the cluster centers as well as the
min function and the
enumerate type. But
norm isn’t in the list. Let’s dive deeper. We can use the
dis module (and some functionality that was introduced in Python 3.4) to inspect the Python bytecode for a given function:
Ah ha! The body of our list comprehension, which contains the call to
norm, is a separate code object that has been stored in a constant. Let’s look at the constants for our function:
We can see the code object in the constant list and use
dis to disassemble it as well:
Once we’ve done so, we can see that the list comprehension has loaded
norm from a global, which we can then resolve and inspect:
Nested functions and lambda expressions
We can see a similar problem if we look at a function with local definitions (note that there is no need for the nesting in this example other than to expose a limitation of our technique):
In this case, we can see that Python compiles these nested functions in essentially the same way it compiles the bodies of list comprehensions:
But we can inspect the nested function just as we did the list comprehension. Let’s do that but just look at the load instructions; we’ll see that we’ve loaded
norm as we’d expect.
Predictably, we can also see a similar problem if we analyze a function with lambda expressions:
Let’s look at what happens when we import modules inside the function we’re analyzing. Because of the semantics of
import in Python, the module dependency list for this function will depend on whether or not we’ve already imported
sys in the global namespace. If we have, we’ll get a reasonable module list; if we haven’t, we’ll get an empty module list. (If you’re running this code in a notebook, you can try it out by restarting the kernel, re-executing the cell with the definition of
module_frontier, and then executing this cell.)
We can see this more clearly by importing a module in a function’s scope that we haven’t imported into the global namespace:
json is an unbound variable (since it isn’t bound in the enclosing environment of the closure). If it were bound in the global namespace, however, the
json we’re referring to in
example_six would be captured as a global variable:
Obviously, we’d like to return the same module-dependency results for functions that import modules locally independently of whether those modules have been imported into the global namespace. We can look at the bytecode for this function to see what instructions might be relevant:
To address the cases that the closure-inspecting approach misses, we can inspect the bytecode of each function. (We could also inspect abstract syntax trees, using the
ast module, but in general it’s easier to do this sort of work with a lower-level, more regular representation. ASTs have more cases to treat than bytecode.)
Python bytecode is stack-based, meaning that each instruction may take one or more arguments from the stack (in addition to explicit arguments encoded in the instruction). For a more involved analysis, we’d probably want to convert Python bytecode to a representation with explicit operands (like three-address code; see section 2 of Vallée-Rai et al. for a reasonable approach), but let’s see how far we can get by just operating on bytecode.
Identifying interesting bytecodes
We know from the problem cases we examined earlier that we need to worry about a few different kinds of bytecode instructions to find some of the modules that our
inspect-based approach missed:
LOAD_CONSTinstructions that load code objects (e.g., list comprehension bodies, lambda expressions, or nested functions);
LOAD_instructions that might do the same; and
IMPORT_NAMEinstructions that import a module into a function’s namespace.
Let’s extend our technique to also inspect relevant bytecodes. We’ll see how far we can get by just looking at bytecodes in isolation (without modeling the stack or value flow). First, we’ll identify the “interesting” bytecodes and return the modules, functions, or code blocks that they implicate:
Now we can make a revised version of our
module_frontier function. This starts with the same basic approach as the initial function but it also:
- processes the bytecode for each code block transitively referred to in each function,
- processes any modules explicitly imported in code.
As you can see, this new approach produces sensible results for all of our examples, including the ones that had confounded the closure-variable approach.
This is ongoing work and I hope to cover refinements to and extensions of this technique in future posts; as I mentioned at the beginning of the post, the ultimate goal is a tool to publish functions in notebook cells as self-contained services. It has been fun to learn about the power (and limitations) of the
inspect module – as well as a little more about how Python compiles code blocks and nested functions.
Thanks to my friend and colleague Erik Erlandson for suggesting improvements to the presentation of this post.
Sound program analyses present conservative overapproximations of program behavior. Consider a may-alias analysis, which determines if two reference variables may refer to the same location in memory. Precise may-alias analysis is undecidable, but certain kinds of imprecision are acceptable. Often we’re interested in sound analyses to support verification or semantics-preserving program transformations, so false positives are acceptable but false negatives are not. Put another way, the worst that can come of spuriously identifying a pair of variables as potentially-aliasing is that we’d miss an opportunity to optimize our program; the worst that can come of not identifying a pair of potentially-aliasing variables as such is a program tranformation that introduces a behavior change. By contrast, unsound analyses are imprecise but not conservative: both false positives and false negatives are possible. These analyses can still be useful for program understanding (e.g., in linters or static bug detectors) even if they are not sufficient to support safe program transformations. ↩
Dublin is a charming city and a burgeoning technology hub, but it also has special significance for anyone whose work involves making sense of data, since William Sealy Gosset was working as the head brewer at Guinness when he developed the t-statistic. Last week, Dublin had extra special significance for anyone whose work involves using Apache Spark for data processing. Our group at Red Hat gave three talks at Spark Summit EU this year, and videos of these are now online. You should check them out!
- One Pass Data Science In Apache Spark With Generative T-Digests , by Erik Erlandson,
- Fire in the Sky: An Introduction to Monitoring Apache Spark in the Cloud, by Mike McCune, and
- Building Machine Learning Algorithms on Apache Spark, by Will Benton.
A lot of the work we discussed is available from radanalytics.io or from the Isarn project; if you’d like to see other talks about data science, distributed computing, and best practices for contemporary intelligent applications, you should see our team’s list of presentations.
I’m giving a talk this afternoon at Spark Summit EU on extending Spark with new machine learning algorithms. Here are some additional resources and links:
- Our team’s Silex library is where I’ve published my ongoing work to develop a self-organizing map implementation for Spark and to extend it with support for data frames and ML pipelines
- I gave a talk about using self-organizing maps in Spark last year at Spark Summit
- If you like the idea of developing new ML techniques on Spark, you’ll also want to attend a session tomorrow in which my friend and teammate Erik Erlandson will be talking about using his parallel t-digest implementation to support feature importance and other applications.
- Finally, if you’re doing anything where parallelism and scale matter, especially in a cloud-native environment, you should also check out Mike McCune’s talk on Spark monitoring and metrics.
I’m speaking this morning at the OpenShift Commons Gathering about my team’s experience running Apache Spark on Kubernetes and OpenShift. Here are some links to learn more:
- My colleague Mike McCune put together a great video demo of the developer experience building and deploying a data-driven application on OpenShift.
- We’re doing all of our work in the radanalytics.io GitHub organization.
- I gave a talk covering some of the same material (but with more of a Spark focus) at Spark Summit EU a couple of weeks ago. Here is the accompanying post, and here is the video of my talk.
I’ll be speaking about Spark on Kubernetes at Spark Summit EU this week. The main thesis of my talk is that the old way of running Spark in a dedicated cluster that is shared between applications makes sense when analytics is a separate workload. However, analytics is no longer a separate workload – instead, analytics is now an essential part of long-running data-driven applications. This realization motivated my team to switch from a shared Spark cluster to multiple logical clusters that are co-scheduled with the applications that depend on them.
I’m glad for the opportunity to get together with the Spark community and present on some of the cool work my team has done lately. Here are some links you can visit to learn more about our work and other topics related to running Spark on Kubernetes and OpenShift:
- You can download a PDF of my slide deck, but it doesn’t include animations, so you may want to wait for the video (shortly after the event).
- We’re doing all of our work in the radanalytics.io GitHub organization. In particular, check out:
- openshift-spark, our container image for running Spark under OpenShift as a non-root user,
- Oshinko (REST service, web UI), a management console for containerized Spark clusters,
- source-to-image builders for PySpark applications so you can have a seamless developer workflow: commit, push, and see your changes built and deployed to production, and
- scorpion-stare, some prototypes of integrating Spark’s dynamic resource allocation with the Kubernetes API.
- You may also be interested in the Kubernetes Spark example, which enables standalone Spark clusters on vanilla Kubernetes.
- Finally, there has recently been some discussion in the Kubernetes community about developing first-class support in Spark for Kubernetes-managed clusters.
I’m delighted to have a chance to present at HTCondor Week this year and am looking forward to seeing some old friends and collaborators. The thesis of my talk is that HTCondor users who aren’t already leading data science initiatives are well-equipped to start doing so. The talk is brief and high-level, so here are a few quick links to learn more if you’re interested:
- Contemporary data processing frameworks like Apache Spark and Apache Flink offer superior programmability, flexibility, and performance. Both projects have really excellent documentation and vibrant user communities.
- I’ve written regularly about Spark in particular but the best place to start here is probably my ApacheCon EU ‘14 talk on Spark performance, which both introduces Spark and shows how to use its fundamental abstractions idiomatically and efficiently.
I also gave a quick overview of some of my team’s recent data science projects; visit these links to learn more:
- Diagnosing open-source community health with Spark by William Benton,
- Insights into Customer Behavior from Clickstream Data by RJ Nowling (also see the video),
- Using a Relative Index of Performance (RIP) to Determine Optimum Configuration Settings Compared to Random Forest Assessment Using Spark by Diane Feddema,
- Random Forest Clustering with Apache Spark by Erik Erlandson (see also Erik’s blog post), and
- Analyzing endurance-sports activity data with Spark by William Benton.
My team and I are pleased to announce the latest release of our Silex library, featuring cool new functionality from all of the core contributors. Silex is a library of reusable components for Apache Spark factored out of our data science work in Red Hat’s Emerging Technology group. You can:
- Include Silex in your projects,
- fork Silex on GitHub,
- read the API docs, or
- see what’s new in the project.
Enjoy, and let us know how you’re finding it useful!