<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Lucas Bernardi on Medium]]></title>
        <description><![CDATA[Stories by Lucas Bernardi on Medium]]></description>
        <link>https://medium.com/@lucas.bernardi?source=rss-fe16abb58a96------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*APzuhN5coAR5cYj-7BQCRQ.png</url>
            <title>Stories by Lucas Bernardi on Medium</title>
            <link>https://medium.com/@lucas.bernardi?source=rss-fe16abb58a96------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 30 May 2026 05:38:56 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@lucas.bernardi/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Mining the Stars: Learning Quality Ratings with User-facing Explanations for Vacation Rentals]]></title>
            <link>https://booking.ai/mining-the-stars-learning-quality-ratings-with-user-facing-explanations-for-vacation-rentals-1bb0f8f2027c?source=rss-fe16abb58a96------2</link>
            <guid isPermaLink="false">https://medium.com/p/1bb0f8f2027c</guid>
            <category><![CDATA[research]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Lucas Bernardi]]></dc:creator>
            <pubDate>Tue, 23 Mar 2021 14:26:00 GMT</pubDate>
            <atom:updated>2024-07-12T17:59:53.546Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Published in the 14th ACM International Conference on Web Search and Data Mining WSDM 2021</em></p><p><a href="https://dl.acm.org/doi/10.1145/3437963.3441812">Paper</a></p><p>Online Travel Platforms are virtual two-sided marketplaces where guests search for accommodations and accommodation providers list their properties such as hotels and vacation rentals. The large majority of hotels are rated by official institutions with a number of stars indicating the quality of service they provide. It is a simple and effective mechanism that contributes to match supply with demand by helping guests to find options meeting their criteria and accommodation suppliers to market their product to the right segment directly impacting the number of transactions on the platform. Unfortunately, no similar rating system exists for the large majority of vacation rentals, making it difficult for guests to search and compare options and hard for vacation rentals suppliers to market their product effectively. In this work we describe a machine learned quality rating system for vacation rentals. The problem is challenging, mainly due to explainability requirements and the lack of ground truth. We present techniques to address these challenges and empirical evidence of their efficacy. Our system was successfully deployed and validated through Online Controlled Experiments performed in Booking.com, a large Online Travel Platform, and running for more than one year, impacting more than a million accommodations and millions of guests.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1bb0f8f2027c" width="1" height="1" alt=""><hr><p><a href="https://booking.ai/mining-the-stars-learning-quality-ratings-with-user-facing-explanations-for-vacation-rentals-1bb0f8f2027c">Mining the Stars: Learning Quality Ratings with User-facing Explanations for Vacation Rentals</a> was originally published in <a href="https://booking.ai">Booking.com ML &amp; DS Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Machine Learning in production: the Booking.com approach]]></title>
            <link>https://booking.ai/https-booking-ai-machine-learning-production-3ee8fe943c70?source=rss-fe16abb58a96------2</link>
            <guid isPermaLink="false">https://medium.com/p/3ee8fe943c70</guid>
            <category><![CDATA[infrastructure]]></category>
            <category><![CDATA[platform-engineering]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[system-architecture]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Lucas Bernardi]]></dc:creator>
            <pubDate>Tue, 29 Oct 2019 13:22:42 GMT</pubDate>
            <atom:updated>2019-10-29T13:27:25.697Z</atom:updated>
            <content:encoded><![CDATA[<p>During the last five years, Machine Learning became a standard tool for Product Development in Booking.com. Today, it plays a role in every step of the customer journey. Hundreds of Data Scientists build, deploy and experiment with hundreds of machine-learned models exposing them to millions of users every day.</p><p>Supporting Machine Learning at scale involves many challenges, not least of which is shipping the models to production reliably, as fast as possible and accommodating a large variety of model types, invocation settings, libraries, data sources, monitoring approaches, etc. Inspired by one of the core values of Booking.com (<em>diversity gives us strength</em>), we built a system that supports a large variety of Machine Learning approaches. In this article we present <strong>RS</strong>, our Machine Learning <em>Productionization</em> System.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qD5AVSSex68CSyvD0-B6Uw.png" /><figcaption>A big part of Amsterdam’s charm comes from the diversity of bicycles running all over the city.</figcaption></figure><h3>Diversity gives us strength</h3><p>This is one of the main values of Booking.com. Nationalities, languages, ages, backgrounds, genders, hobbies, religions, diets… all are immensely diverse. Machine Learning does not escape this. People building Machine Learning models do it in many very different ways. Some use a small data set and R, others a huge data set and a command line tool like Vowpal Wabbit. Some like to write their own optimization algorithm in Java, others use sklearn or H2O. Some build Deep Learning models in Pytorch, others in Tensorflow, and so on. We believe in such diversity and therefore encourage it and support it with tools, courses, infrastructure and more, but…</p><h3>Diversity gives us… a challenge</h3><p>Most of the models we build are only valuable when integrated into a larger system like the main Booking.com website, our mobile apps, our partner-facing services, customer service systems, among others. The process of taking a working machine-learned model and integrating it with the relevant system, making it available to our customers, is what we call <em>productionization</em>. This is a critical step and it entails a set of requirements. Let’s look at the most important ones:</p><h4>Consistency</h4><p>This means two things: the online predictions must match the offline predictions and they must be the same regardless of which Data Center, Server, Pod, etc. is hit by the request. Not meeting these conditions brings many bad consequences like user experience degradation, difficulty to debug the models and invalidation of experiments to name a few.</p><h4>High availability</h4><p>Most of out systems are available 24/7, which means our machine-learned models must also serve requests seven days a week, 24 hours a day, worldwide. Upgrading a model, scaling it to a wider audience, maintaining the hosts where it runs, testing new tools, etc. must not interfere with the availability of the model in production. The stakes are high: a whole experiment might end up invalid if one of the competing models fails too often.</p><h4>Low latency</h4><p>The model must make predictions really fast. Many models only decide if an icon will be shown on top of an accommodation card in search results. This is a tiny part of the whole web page so we can’t really take a lot of time to do that. Furthermore, many models collaborate to construct a page, so even if individual models are not slow, the aggregated latency might become prohibitively high.</p><h4>Scalability</h4><p>Booking.com is constantly growing, not only in terms of customers and transactions but also in terms of number of listings available, and even number of products as we expand our offer beyond accommodations. As a consequence, our models must be ready to take in growing number of requests per second.</p><h4>Observability</h4><p>The environments in which our models operate are very volatile. For example, the FIFA World Cup might suddenly bring a higher number of visits to France. Or maybe the website changed, and the “great deal icon” is now displayed in a different location. Such volatility implies we must closely watch the behavior of our models. Has the model output changed? Are the inputs correct? Is the input space changing? We need tools to make sure we can observe our models to react appropriately and promptly.</p><h4>Reusability</h4><p>Many of our models solve a rather generic task. For example, one interesting model determines if a hotel is family-friendly, which means that the hotel has good amenities and an environment where families can enjoy together (as opposed to a business hotel for example). This model can be applied in many different product features. We can use it to highlight those family-friendly hotels in the details page or to create a filter in the search results page, or to reinforce a user decision in the booking process. These are all examples of <em>model reutilization</em>, which helps us a lot to make the most out of our models.</p><p>Meeting all these requirements for models built in such a diverse set of approaches is quite challenging. But it is exactly the type of challenge we constantly face.</p><h3>The Fantastic Four</h3><p>Let’s take a look at four basic mechanisms to <em>productionize</em> machine-learned models. These mechanisms form the basis of our system, and they embody the types of trade-offs we make when deploying our models. They work a bit like the Marvel heroes, they all have strengths and weaknesses but working together they achieve great things.</p><h4>Lookup tables</h4><p>Most of our models map an input vector to a prediction (or set of predictions in the case of recommender systems). A very simple way to deploy a model in production is to precompute all the predictions for all the possible inputs and store them in a key-value store. At prediction time all we need to do is lookup the prediction using the input as the key. This is a very naive approach but has many great advantages:</p><ul><li>Since no computation is done at prediction time and since most key-value stores are optimized for fast reading, latency is usually very low.</li><li>Horizontal scalability is quite straightforward, most key-value stores take care of it.</li><li>Huge modeling flexibility. It doesn’t really matter how the model was trained, whether it is a Linear model trained in R or a Dual Attention Network trained with Keras, as long as the predictions can be computed and written to the key-value store in time the model can be productionized reliably.</li></ul><p>It also has several drawbacks:</p><ul><li>When the input space is large, it might be difficult to store that many combinations, or impossible to precompute all the predictions within a reasonable time. Besides, chances are many of the input combinations will never actually happen in production, resulting in waste of computation and storage resources.</li><li>The process of writing the predictions to the key-value store might be cumbersome when we consider model versions, modifications to the feature space, etc.</li><li>Continuous inputs are not supported.</li></ul><p>For problems with a small and discrete feature space this method offers huge modeling flexibility together with all the nice attributes of a good key-value store: fast reads, high availability and horizontal scalability. In Booking.com this method is quite popular in the Front End, where we usually have discrete feature spaces, or we have a natural key like user / accommodation / destination identifiers.</p><h4>Generalized Linear Models (GLMs)</h4><p>In this method the model is represented by a scalar weight for each input plus a global bias; at prediction time, we just compute the inner product of the input with the weights, add the bias, apply a scalar function (the “inverted link”) and return. If we are interested in ranking items, we do the same for each item, and sort them by their score, more formally:</p><blockquote>Prediction(<em>X</em>) = 𝓕(<em>&lt;W</em>, 𝓣(<em>X)&gt;</em>)</blockquote><p>Where <strong>&lt;,&gt;</strong> means inner product, <em>X</em> is the input vector, <em>W</em> is the weight vector (the model), 𝓕 is the inverted link function (scalar to scalar), and 𝓣 is a vector to vector transformation applied before the inner product is computed, finally, the output is a single scalar.</p><p>Ranking items is very similar:</p><blockquote>Ranking(<em>X</em>) = <em>arg sortᵢ</em> 𝓕(<em>&lt;Wᵢ</em>, 𝓣(<em>X</em>, <em>i</em>)&gt;), <em>i</em> ∈ 𝓘</blockquote><p>Here, 𝓘 is a set of items, <em>Wᵢ</em> is a weight vector for the item <em>i</em> (<em>W</em>, the model, is a matrix now) and 𝓣 transforms the input vector <em>X</em> into some other vector that might depend on the specific item <em>i</em>. The output is a sorted list of items, of course one can attach the score itself for downstream purposes or even take the top <em>k</em> items as opposed to sorting which can be done in linear time.</p><p>Now, depending on how we instantiate all these variables we get very different models, all linear in the parameters <em>W</em>. For example:</p><ul><li>𝓕 and 𝓣 identity gives a plain linear regression.</li><li>𝓕 sigmoid and 𝓣 identity gives a plain logistic regression.</li><li>If <em>X</em> is a 1-dimensional vector with just a user id, 𝓕 is the identity, 𝓣 transforms <em>X</em> into a <em>d</em>-dimensional user vector (independent of <em>i</em>), and <em>Wᵢ</em> is a <em>d</em>-dimensional item-vector for item <em>i</em>, we get bi-linear models like Matrix Factorization and <a href="https://booking.ai/k-nearest-neighbours-from-slow-to-fast-thanks-to-maths-bec682357ccd">cosine similarity based k-Nearest Neighbors</a>.</li></ul><p>In general this simple prediction rule allows to serve predictions for many kinds of regressors, binary or multiclass classifiers, recommender systems or rankers, and even simple sequence models like Markov Chains.</p><p>Of course the actual model depends on how <em>W</em> is learned from data. The linear regression could actually be a SVM classifier, but we don’t need to care about this. It doesn’t matter which training algorithm is used as long as it can be represented by a weight vector, an input transformation and link function, we can run it in production.</p><p>This method directly addresses the issues of lookup tables. GLMs can easily model continuous inputs, large amount of inputs, and they are more efficient since only actually requested predictions are computed. This flexibility comes at a cost: we can only productionize linear models. Note that this means linear in <em>W</em>, nothing prevents us from introducing non-linearities through feature transformations (𝓣) like interactions, bucketing, clipping, or even using embeddings. One more disadvantage is that we need to transform our model from the library used to train it, to the linear predictor format, which can be error-prone and adds one more step in the deployment process.</p><p>Due to its flexibility and online performance, this method is very popular and applied to deploy many models in a variety of business cases like user preference models, user context models, destination recommendations, budget prediction, hotel attributes prediction and more.</p><h4>Native libraries</h4><p>This method consists of simply using the library used to train the model to make predictions in production. For example, if a model is trained using sklearn, we can save it in pickle format, upload to a production server, where it would be loaded using the sklearn and pickle APIs, making it ready to serve predictions. If we trained a model using H2O we can serialize it using the Java Serialization API, and just as with sklearn, upload it to a server, deserialize it and use it to make predictions. Most libraries offer some form of serialization of an already trained model, so this approach is quite general in principle.</p><p>This method brings a new dimension into the picture: ease of use. We can simply train our model and upload it without any transformation to intermediate formats. It also provides high consistency, the same code used to train is used to predict, no surprises in production. On the other hand, native libraries might be difficult to actually deploy, since they require a specific runtime environment. For example if our servers run on Java, deploying Python models is not straightforward, or if our servers run on Python, deploying an H2O MOJO can be really hard. In general, this leads to develop support for libraries compatible with the server runtime, which offers much less flexibility compared to lookup tables and GLMs. Another disadvantage is that native libraries might not be optimized for serving time but rather for training time, increasing the risk of latency.</p><p>We use this approach a lot when our models are Tree-based, like Random Forests or Gradient Boosted Trees or when they are Neural Networks.</p><h4>Scripted models</h4><p>A scripted model is simply a script with a predefined interface that is invoked for every request. The script is of course written by the model’s author and they can do pretty much anything they want. This approach gives huge flexibility since it allows, among other things, to control the run-time environment, and to perform complex tasks at prediction time. On the other hand, scripted models are a weak link in the online request life cycle: every line of code will have an impact at prediction time, increasing the risk of latency and failure.</p><p>We use this approach to deploy models built with unsupported libraries and models requiring some logic on top of one or several predictions.</p><h3>Trade-offs and the iterative hypothesis-driven approach</h3><p>There are many more mechanisms to deploy models, but these are the four<em> canonical</em> methods that allow us to make sensible trade-offs. Not all business cases require the same level of modeling <em>flexibility</em> or <em>robustness</em>, and for one single business case, this requirements are also not at the same level on different stages of the solution. For example, if we want to create a recommender system, the first model might very well be just a popularity model with two or three categorical features, built just using SQL, so a lookup table is a perfect approach at this stage since we don’t want latency to be an issue at all at the beginning of the project. The next step might be to test the hypothesis that more features make the recommendations more relevant. We can still do that with a lookup table, but if the amount of features is too big, a GLM allows us to do it without compromising latency and giving us the freedom to use the programming language and software library we are most comfortable with. As the project succeeds, the model evolves towards higher complexity, we might abandon our slick SKLearn linear model to be able to test the effect of non-linearities with Random Forests for which H2O deployed as a serialized model will do a great job. Finally, a mature model might evolve to a powerful RNN trained in Pytorch and we might even be willing to pay with a few milliseconds for much better recommendations.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iwh6WNhlHjJTMX_tkmxz3Q.jpeg" /><figcaption>Bakfiets are very heavy, but they can carry lots of people and things. Not the best choice for commuting to the office, but very handy to take four kids to school or a 20 people picnic to the park.</figcaption></figure><p>Following a rather informal analysis of the four productionization methods, we find that <strong>Flexibility</strong> and <strong>Robustness</strong> are two software attributes creating a trade-off plane: the more Flexibility a method offers, the less Robust it is and vice-versa.</p><p>Flexibility can be decomposed into flexibility with respect to the Input Space, to the Modelling Approach or to the Stack (programming language and libraries). Likewise, Robustness can be decomposed into Latency, Consistency (between the training model and the actually running artifact) and Observability.</p><p>The following table illustrates these trade offs, using a -1, 0 +1 (red, yellow, green) scoring system along these 6 aspects:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/647/1*opJHRKbIoX8LyDK3jJ2FuQ.png" /></figure><p>Adding up the scores (assuming they are all equally important) allows us to locate each method in the trade-off plane:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/393/1*0MAmu02kWjBzx9JSj8UNqw.png" /></figure><p>Lookup Tables and GLMs are both at the origin, offering about the same level of Flexibility and Robustness, but different flavors: Lookup Table’s Flexibility is about Modeling while GLMs is about Input Space. Lookup Table’s Robustness is about Latency, while GLM’s Robustness is about Observability. So we can choose which method depending on which flavor of Flexibility and Robustness we consider a better fit for our problem.</p><p>From the origin we can get <em>a bit</em> more Robustness if we are willing to give away <em>a bit</em> of Flexibility using Native Libraries. Or we get <em>a lot</em> more Flexibility if we are willing to loose <em>some</em> Robustness with Scripted models.</p><p>Of course this analysis is rather subjective. One could always argue that all these aspects are not equally important and that their weights actually depend on the specific application or even that there are more aspects to consider. But this chart is just an illustration of how these four basic mechanisms do a fairly good job at covering the Robustness vs Flexibility trade-off plane.</p><h3>RS, our Machine Learning productionization tool</h3><p>RS is our machine-learned models productionization tool. It provides support to productionize models using all four methods described before and adds a lot of functionality that is method-agnostic, helping model authors to achieve the requirements described at the beginning.</p><p>The main idea behind RS is to <em>decouple training from prediction. </em>It doesn’t matter how a model was built, or which productionization method is used, model consumers can use exactly the same API. This simple but powerful idea is what allows RS to support huge diversity.</p><p>But prediction is just one functionality, many others can also be decoupled from training and productionization method, like monitoring and A/B testing. The following diagram summarizes the services RS provides for <em>all</em> models, regardless of how they were trained or deployed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/960/0*K96UstNT-SQTLFI2" /><figcaption>RS in a nutshell</figcaption></figure><h4>Implementation</h4><p>Models can be uploaded through a programmatic interface or through a web portal. RS distributes the model across nodes in a cluster where a Java process takes care of loading them into memory and makes them available to serve predictions through a standard HTTP interface. One RS node serves many models and any given model is loaded in many nodes, this approach helps to achieve High Availability and Scalability requirements.</p><p>Lookup tables are implemented using the Cassandra key-value store (or in memory if they are small enough), users can point to a table in our Hadoop cluster and RS imports it into Cassandra. GLMs are served through an in-house developed linear prediction system that uses simple text files as model descriptors. Native Libraries are supported through H2O MOJOS, Tensorflow and Vowpal Wabbit binaries. These are the most popular libraries used in Booking.com, and they are Java friendly which matches the RS runtime environment. Finally, Scripted models are Python scripts, each running in its own virtual environment allowing users to upload additional modules and dependencies as needed.</p><p>On top of this, RS adds a lot of value through extra functionality, including a web portal that allows to search and browse all the available models. Each model has its own page with detailed information such as experiments using the model, monitoring tools, documentation, link to the training code, etc. It also provides a basic state machine that allows the author to transition the model through states like <em>in-testing</em>, <em>production-ready</em>, <em>disabled</em>, etc. Another interesting feature is the Playground that allows occasional users to just go ahead and use a model to see what it does. All these helps a lot with the Observability and Reusability requirements.</p><p>RS also mitigates many of the red flags identified by the trade-off analysis. For example, caching and batch requests help a lot with latency; the Linear Prediction system was extended to support Factorization Machines, mitigating the model flexibility red flag; test cases are enforced when models are uploaded to mitigate the Consistency red flag. The supported native libraries are friendly with several programming languages (Python, Java, R, C) mitigating the Stack Flexibility red flag. Altogether, RS equips model authors with high flexibility and robustness, regardless of the productionization method they choose.</p><h3>Lessons learned</h3><p>RS is one of the many tools in Booking.com that we use everyday. Adoption is growing and plays a fundamental role in scaling up our applications of Machine Learning across the whole organization.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/605/1*mBR39F10Zv-cKa1tDaD0qA.png" /><figcaption>Cumulative number of newly created models and experiments with Machine Learning and RS</figcaption></figure><p>Embracing diversity was by far they most important success-factor, but we also learned many other things along the way, so to wrap it up let’s see three of them:</p><h4>Solving common concrete problems</h4><p>RS started as a simple utility to run linear models in the website. This tiny utility achieved huge impact because it solved a common and concrete problem of those days. It removed <em>one</em> obstacle and open the path for what came later.</p><h4>Keeping the customer close</h4><p>Since the very beginning we kept our customers (model authors) as close as possible, brainstorming together, solving business cases together, building up a vision together. We like to think RS is built by our Machine Learning community and not only by the core RS team.</p><h4>Reinventing the wheel</h4><p>Reinventing the wheel is usually seen as a bad practice (although <a href="https://www.smithsonianmag.com/science-nature/a-salute-to-the-wheel-31805121/">it has been actually</a> re-invented many times). Asking ourselves what are the concrete requirements we want to satisfy, showed us that building our own system was a much more scalable approach giving us, among other things, the chance to integrate with other tools like our Experimentation Platform and fronted libraries, and to focus on the aspects we considered fundamental like latency and high availability. Reinventing the wheel gave us the chance to invent a <em>perfect-fit-wheel</em> for our requirements, plus high flexibility to adapt smoothly as they change.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6ziW3G0L4fGdsu2FVpKc2w.jpeg" /><figcaption>Reinventing the Bicycle —Firefighter Bicycle by Pivari.com [<a href="https://creativecommons.org/licenses/by-sa/3.0">CC BY-SA 3.0</a>]</figcaption></figure><h3>That is all folks!</h3><p>So there it is, the Booking.com approach to machine-learned models productionization. In future articles we might explore specifics about Monitoring, Feature Engineering, Experimentation and other topics. We hope you found this one interesting and stay tuned for more.</p><p>I want to thank Adolfo Mazorra and Jean Schmidt, two of the main developers behind RS, for their insightful input, <a href="https://medium.com/u/b5dc69191d6c">Themis Mavridis</a> for reviewing the whole article and <a href="https://medium.com/u/db6ba22189a0">Steven Baguley</a> for making it human readable.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3ee8fe943c70" width="1" height="1" alt=""><hr><p><a href="https://booking.ai/https-booking-ai-machine-learning-production-3ee8fe943c70">Machine Learning in production: the Booking.com approach</a> was originally published in <a href="https://booking.ai">Booking.com ML &amp; DS Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Don’t be tricked by the Hashing Trick]]></title>
            <link>https://booking.ai/dont-be-tricked-by-the-hashing-trick-192a6aae3087?source=rss-fe16abb58a96------2</link>
            <guid isPermaLink="false">https://medium.com/p/192a6aae3087</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[blog-posts]]></category>
            <dc:creator><![CDATA[Lucas Bernardi]]></dc:creator>
            <pubDate>Wed, 10 Jan 2018 16:14:23 GMT</pubDate>
            <atom:updated>2018-06-14T13:59:38.623Z</atom:updated>
            <content:encoded><![CDATA[<p>In Machine Learning, the Hashing Trick is a technique to encode categorical features. It’s been gaining popularity lately after being adopted by libraries like <a href="https://github.com/JohnLangford/vowpal_wabbit/wiki">Vowpal Wabbit</a> and <a href="https://www.tensorflow.org/">Tensorflow</a> (where it plays a key role) and others like <a href="http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.HashingVectorizer.html\">sklearn</a>, where support is provided to enable <a href="https://en.wikipedia.org/wiki/Out-of-core_algorithm\">out-of-core</a> learning.</p><p>Unfortunately, the Hashing Trick is not parameter-free; the hashing space size must be decided beforehand. In this article, the Hashing Trick is described in depth, the effects of different hashing space sizes are illustrated with real world data sets, and a criterion to decide the hashing space size is constructed.</p><h3>Out-of-core learning</h3><p>Consider the problem of learning a linear model: an out-of-core algorithm learns the model without loading the whole data set in memory. It reads and processes the data row by row, updating feature coefficients on the fly. This makes the algorithm very scalable since its memory footprint is independent of the number of rows, which is a very attractive property when dealing with data sets that don’t fit in memory.</p><p>One possible implementation uses a mapping from a feature name (and possibly a value) to its corresponding coefficient. For example, if the data contains ‘country’ as a feature with values like ‘Netherlands’, ’Argentina’ or ‘Nigeria’, the weights associated to each country are updated like this:</p><p><strong>W</strong>[‘country_argentina’] = <strong>W</strong>[‘country_argentina’] + delta(X, y, ‘country’)</p><p>This is implicitly applying <a href="https://en.wikipedia.org/wiki/One-hot">one-hot encoding</a> to the feature ‘country’. The fact that it happens implicitly is <em>great</em> — we don’t need to create a file with one column for each possible country, use sparse data file formats or any other preprocessing. We don’t even need to know how many categories there are; we can just take our .csv data as is, throw it into our learner and get back a coefficient for each feature. This also plays nicely with numerical features, for which we just need to skip the value:</p><p><strong>W</strong>[‘price’] = <strong>W</strong>[‘price’] + delta(X, y, ‘price’)</p><p>While this approach looks good at first glance, it relies heavily on the way we implement the mapping from features to coefficients. This takes us to the next topic: <em>Hashing</em>.</p><h3>Hashing</h3><p>In order to implement the coefficients table <strong>W,</strong> we could use a <a href="https://en.wikipedia.org/wiki/Hash_table">hash table</a>. This is a data structure that maps ‘keys’ of arbitrary length to ‘values’.<em> </em>In our case, the<em> </em>features are the keys and the coefficients are the values. This mapping is performed using a <a href="https://en.wikipedia.org/wiki/Hash_function">hash function</a>, which takes a key of arbitrary length as input and outputs an integer in a specific range. The hash table uses this integer as an index in a 1-dimensional array where the values are stored.</p><p>Good hash functions have an interesting property: they map the expected input evenly over the output range, which means that every hash value has roughly the same probability of being observed after hashing a typical sample of the key space.</p><p>Hash functions also come with a not-so-nice side effect: they can hash different keys to the same integer value (this is known as ‘collision’), which is a big problem for our hash table: we can’t simply store each coefficient in the array where the hash of the key points to, since it could be overriding a previously inserted value.</p><p>A <a href="https://en.wikipedia.org/wiki/Hash_table#Collision_resolution">collision-resolution strategy</a> is necessary. The most straightforward approach (known as ‘separate chaining’) is to store a list in each position of the array, containing all the keys and values hashed to that bucket. When the value associated to a key is requested, the key is hashed giving an index in the array, then the corresponding list is scanned until the requested key is found, at that point the associated value is returned. When a new key and value are added to the table, a similar procedure is followed, the only difference is that instead of returning the value, the key and value are added to the list.</p><p>This whole process makes reads and writes slower though, and, forces us to keep the keys in memory. This is bad news for our out-of-core learning algorithm, since it means the whole learning process is slower and its memory footprint is now linear in the amount of features.</p><p>Unless we are willing to accept collisions...</p><h3>The Ostrich Algorithm</h3><p>The <a href="https://en.wikipedia.org/wiki/Ostrich_algorithm">Ostrich Algorithm</a> is very simple: ignore the problem. In our case, this would mean we don’t look to solve collisions — we just go ahead and do:</p><p><strong>W</strong> = 1-dimensional array of length <em>n</em></p><p>…</p><p><strong>W</strong>[hash(‘country_’+X[‘country’])] = <strong>W</strong>[hash(‘country_’+X[’country’])] + delta(X, y, ‘country’)</p><p>This means that we don’t need to scan a list nor keep the keys in memory, making our out-of-core learning algorithm constant in memory and fast.</p><p>Collisions <em>do</em> happen though. What’s the effect of collisions on this approach?</p><p>Figures 1, 2 and 3 show the effect of collisions on predictive power for 3 data sets:</p><ol><li>Booking.com’s internal dataset containing ~200k features and ~250M examples for a 4 classes classification problem;</li><li>The <a href="https://www.kaggle.com/c/criteo-display-ad-challenge">Criteo Display Advertising Kaggle Challenge</a> data set, which contains ~30M features and ~40M examples;</li><li>The <a href="https://www.kaggle.com/c/avazu-ctr-prediction">Avazu CTR Prediction Kaggle Challenge</a> with about ~40M features and ~40M examples.</li></ol><p>For all data sets, the algorithm is a simple logistic regression trained with Vowpal Wabbit (one against all for the multi-class problem).</p><p>In all charts, the x-axis is the actual proportion of colliding features during training. The blue curve, associated with the left hand side y-axis is <em>n, </em>the hash size in number of buckets; the red curve, associated with the right hand side axis, is the log loss on a held out data set.</p><p>All charts are quite consistent: collisions are bad, the more, the worse the log loss, which is expected but (surprisingly) not <em>that</em> bad — even for 50% colliding features, the performance lost is much less than half percent. This could be considered a big win for the Ostrich Algorithm, but depending on the application such impact could be unacceptable. Furthermore, the impact also depends on the specific data set: at 50% colliding features the Booking.com dataset is impacted more than twice as much as the Criteo data set.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*W-YE2SzU4IQbQREVy3IwTw.png" /><figcaption>Figure 1 — Booking.com Dataset</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*bC7oDd62Mc_Ae8epME_KDw.png" /><figcaption>Figure 2 — Criteo Kaggle Challenge Data</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*fE8KUBgJDG1HqSX4EXGu-g.png" /><figcaption>Figure 3 — Avazu Kaggle Challenge Data</figcaption></figure><p>Another effect of the collisions is the potential loss of interpretability of the model. Even if collisions don’t impact predictive power, they might produce absurd models; for example, binary features like ‘logged in/logged out’ could share exactly the same weight, which wouldn’t make much sense. Another example would be colliding countries; if we want to estimate the price of a hotel and use ‘country’ as a feature, we might expect prices in Denmark being much higher than in Greece. But if Denmark and Greece are hashed to the same bucket, they would have exactly the same contribution to the price of a hotel, mistakenly challenging our intuition.</p><p>Why not just use as big a coefficients table as possible? That would certainly minimise collisions — but at the expense of memory, which defeats one of the purposes of the Hashing Trick: <em>to avoid the consumption of memory by the features</em>. Minimising memory consumption has many advantages, but one of particular interest is that it allows us to train many versions of the model in parallel in a single computer, which can be very helpful for many things like hyperparameter search for example.</p><p><em>Whether we care about a specific metric to the last bit, or how interpretable our model is, if we want to make the most out of our computational resources, understanding the dynamics of the </em><strong><em>hashing space size</em></strong><em>, the </em><strong><em>feature space size</em></strong><em> and the </em><strong><em>number of collisions</em></strong><em> is key to make principled trade-offs — and, as you’ll see, a lot of fun. Let’s dig deeper.</em></p><h3>Expecting Collisions</h3><p>If we know roughly how many features our data has (denoted by <em>k</em>) and we fix the hash space size (denoted by <em>n</em>), what’s the expected number of features colliding? This is a nice puzzle, and it is related to a very famous problem: <a href="https://en.wikipedia.org/wiki/Birthday_problem">The Birthday Paradox</a>. In the original version, it is described as follows:</p><p>In a group of <em>k</em> people, what is the probability of at least 2 persons having the same birthday?</p><p>But in our case we are interested in a slightly different problem:</p><p><em>In a group of k people, how many of them are expected to share the same birthday with at least one other person?</em></p><p>For example, consider a group of 5 people, 2 of them having their birthday on February 17th, and 3 of them on March 23rd, then everybody shares their birthday with at least someone else. But if 2 of them have their birthday on February 17th and the others on April 25th, August 2nd and December 18th, then only 2 out of 5 people share their birthday with someone else. That is 40% colliding people.</p><p>So let’s solve this puzzle step by step:</p><p><strong>1</strong> The probability of persons <em>X</em> and <em>Y</em> sharing their birthdays is simply <em>1/n</em></p><p><strong>2 </strong>The probability of persons <em>X</em> and <em>Y</em> <strong>not</strong> sharing their birthdays is then <em>1–1/n</em></p><p><strong>3 </strong>The probability of person <em>X</em> <strong>not</strong> sharing their birthday with any of the other <em>k-1</em> people in the group is therefore:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/117/1*_CcfcOaBdv0Jk6YT7Dno6A.png" /></figure><p><strong>4</strong> Then the probability of <em>X</em> sharing their birthday with at least one other person is:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/207/1*BJBnJ028YImrGaTbr1Z5qg.png" /></figure><p><strong>5</strong> Define one binary random variable for each person: it gets value <em>1</em> when the person does share their birthday with at least one other person in the group and <em>0</em> otherwise. It follows a Bernoulli distribution with probability of success <em>p</em>.</p><p><strong>6</strong> Define another random variable <em>H</em> as the sum of the <em>k </em>previously defined random variables:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/112/1*JWEVhZbDfSG1BGQobAEH1A.png" /></figure><p>which simply represents how many of the <em>k </em>people share their birthdays with at least someone else.</p><p><strong>7</strong> We want the expected value of <em>H. </em>By linearity of expectations we can write:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/740/1*NCv_vEBesqHiFLAI0Oxzyg.png" /></figure><p><strong>8</strong> Finally we have that in a group of<em> </em><strong><em>k</em></strong> people, the expected number of people sharing their birthdays with at least one other person, when we consider <strong><em>n</em></strong><em> </em>possible birthdays is</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/292/1*ULlLSkvFlsz7GJfNuvrkGA.png" /><figcaption>Equation 1</figcaption></figure><p>Back to hashing. The metaphor is quite haptic: the group of people is the feature space, the number of possible birthdays is the hashing space size, and having the same birthday is sharing the hash value, a collision. We assume that all birthdays are equally probable, which is actually <a href="https://www.panix.com/~murphy/bday.html">not true for real birthdays</a>, but it is very close to true for hashing: we are just assuming the hash function is a good one.</p><blockquote><em>one experiment is worth a thousand equations</em></blockquote><p>So equation 1 looks good, but, <em>one experiment is worth a thousand equations</em>. Does it actually work? For our 3 data sets, we know how many features there are, how big the hash space is, and how many collisions actually happened. Let’s compare what equation 1 says and what reality shows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*986p3a5xZvELlJGnP-0xFw.png" /></figure><p>The results are conclusive. They show that the model is correct, since it can successfully predict how many collisions will happen in all 3 data sets.</p><h3>Controlling Collisions</h3><p>We now have a good model for the expected number of collisions given feature space and hashing space sizes. A very natural next question follows: for a given feature space size <em>k</em>, how big should the hashing space <em>n</em> be to produce expected number of collisions <em>c</em>? All we need to do is to take equation 1, fix <em>c</em> and write <em>n</em> as a function of <em>k:</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/231/1*wz-ftEGtoxBeYZ5enxhnFg.png" /></figure><p>Pretty ugly function, let’s see if it works,</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*HAaz2CE89pfobfA4WIExBg.png" /><figcaption>Figure 5</figcaption></figure><p>Figure 5 shows the results of applying this formula. The x-axis represents the different feature space sizes (<em>k</em>), the y-axis represents the hashing space size (<em>n</em>), and each curve a specific number of collisions (<em>c</em>). The chart can be interpreted as a contour plot of equation 1. One surprising result is that for a fixed number of collisions, the hash size seems to grow quadratically with the number of features. It is also surprising that all curves can be perfectly fit by a parabola, which suggests that it should be possible to find a parabolic form for <em>n</em> given <em>c</em> and <em>k</em> (as an alternative to that ugly formula).</p><p>Indeed, under rather weak assumptions ( n&gt;1 and k ≪ n), we can apply a <a href="https://en.wikipedia.org/wiki/Binomial_approximation">binomial approximation</a> to the power term in equation 1:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/271/1*z2eV-LdZybGtJu9jYMvwMg.png" /></figure><p>and then plug it back to get:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/216/1*JzdEaJFVqUxfhK2rD-7M1g.png" /></figure><p>which allows us to write <em>n</em> as a function of <em>k</em> and <em>c</em>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/90/1*nAIzQ48NeWzopigMFKTxFQ.png" /><figcaption>Equation 2</figcaption></figure><p>To validate this approximation we can simply compare with the original formula:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*2ifOEQVnJzZ2rohDlsrLjQ.png" /></figure><p>Equation 2 is a very good approximation and can therefore be used directly to decide the hash space size (given the feature space size and the desired number of collisions). It’s worth noting that for the case of 0 collisions we can conveniently set <em>c=1</em> to get <em>n=k².</em></p><h3>Trade-offs</h3><p>In practice there are two main approaches to implement the hashing trick:</p><p><strong>Global hashing space:</strong> There’s only one hashing space and one single parameter to decide, but cross-field collisions can happen — countries can collide with user ids, for example. This is how Vowpal Wabbit works.</p><p><strong>Per-field hashing space</strong>: There’s a hashing space per feature, allowing finer grain control at the cost of some speed and more parameters to tune. This is how Tensorflow works.</p><p>Also, in practice, there are two main types of categorical features:</p><p><strong>Moderate cardinality and static</strong>: These are features with less than a thousand categories that don&#39;t change a lot, like country. Usually it’s important to have one weight for each category, so having 0 collisions is what we’re after.</p><p><strong>High cardinality and dynamic</strong>: These are features with more than a thousand categories and constantly getting new ones, like user id or hotel id. Usually it’s not that important to have a weight for each category; the weight distribution over the categories is what matters. Collisions are acceptable but it’s hard to tell how many we are willing to accept. 5% collisions is a pretty conservative ansatz, giving a simple rule: <em>n</em>=20<em>k</em> . Cross-validation techniques can also help, but require more time and resources.</p><p><strong>One criterion</strong>: After quite a bit of analysis, we have now all the elements to recommend a practical criterion to decide the size of the hashing space that gives a good balance of memory usage, interpretability and predictive power:</p><blockquote>If you can choose the hashing space on a per feature basis, use <strong>k²</strong> for features with less than a thousand categories and <strong>20k</strong> for the others.</blockquote><blockquote>If there is only one hashing space and less than twenty thousand features in total, use <strong>k²</strong>, otherwise use <strong>n=20k</strong>.</blockquote><blockquote>If you want to control for collisions as a proportion <strong>r</strong> of the features, then use <strong>n=k/r</strong>.</blockquote><h3>Conclusion</h3><p>Of course the proposed criterion is not absolutely general, that is not the intent of this article (text problems might show different behaviours for example). This work presents general principles that govern the Hashing Trick, the trade-offs involved, and an analysis that gives tools to construct heuristics and criteria to decide the size of the hash in many different regimes: <em>Your mileage may vary.</em></p><p>A couple of findings were surprising:</p><ul><li>In practice, the effect of collisions on predictive power is very low.</li><li>The minimum hash size required to expect a fixed number of collisions grows quadratically with the feature hash size (this is especially relevant for the case of 0 collisions, if we only want to control collisions as a percentage of the number of features, then the hash size <em>n</em> grows <em>linear</em> with the feature space size <em>k</em> ).</li></ul><p>I like to see the Hashing Trick as a successful example of the Ostrich Algorithm. Indeed algorithmically, the collisions issue is just ignored.</p><p>Finally, two things: all the code to reproduce these results are available in my <a href="https://github.com/lucjb/hteng">github</a>, which contains some useful scripts to deal with Vowpal Wabbit models and some freaky snippets like maths with big numbers in python. And, if you are interested in a formal analysis of the hashing trick, I recommend <a href="https://arxiv.org/abs/0902.2206"><strong>Feature Hashing for Large Scale Multitask</strong> <strong>Learning</strong></a> by Weinberger, Dasgupta, Attenberg, Langford and Smola.</p><p>I want to thank Stas Girkin for asking this (at that moment) awkward question, Kristian Holsheimer for figuring out the power expansion, Denis Bilenko for the fruitful discussions, <a href="https://medium.com/u/b5dc69191d6c">Themis Mavridis</a> for reviewing the article and <a href="https://medium.com/u/db6ba22189a0">Steven Baguley</a> and <a href="https://medium.com/u/ebb744af7935">Kristofer Barber</a> for making it readable.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=192a6aae3087" width="1" height="1" alt=""><hr><p><a href="https://booking.ai/dont-be-tricked-by-the-hashing-trick-192a6aae3087">Don’t be tricked by the Hashing Trick</a> was originally published in <a href="https://booking.ai">Booking.com ML &amp; DS Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[k-Nearest Neighbours: From slow to fast thanks to maths]]></title>
            <link>https://booking.ai/k-nearest-neighbours-from-slow-to-fast-thanks-to-maths-bec682357ccd?source=rss-fe16abb58a96------2</link>
            <guid isPermaLink="false">https://medium.com/p/bec682357ccd</guid>
            <category><![CDATA[recommendation-system]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[fraud-detection]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[homepage]]></category>
            <dc:creator><![CDATA[Lucas Bernardi]]></dc:creator>
            <pubDate>Wed, 31 Aug 2016 07:00:00 GMT</pubDate>
            <atom:updated>2018-10-28T20:05:23.804Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>Abstract:</strong> Building the best travel experience for our customers in Booking.com often involves solving very challenging problems. One that appears very frequently is the <em>k</em>-Nearest Neighbours problem (<em>k</em>-NN). In simple words it can be stated as follows: Given a thing, find the <em>k</em> most similar things. Depending on how thing and similar are defined, the problem becomes more or less complex. In this post we’ll treat the case where the things are represented by vectors, and the similarity of two things is defined by their angle. We’ll discuss solutions and present a practical trick to make it fast, scalable, and simple. All of it, thanks to maths.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ycHKKT2IcLZum8bEV6gFOQ.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/photos/ShoP4ESuIsY?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Jason Leung</a> on <a href="https://unsplash.com/?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></figcaption></figure><h3>1. Things and Similarity</h3><p>Suppose that we are given a database with pictures of handwritten digits, and the task of finding similar digit handwriting. Every picture contains exactly one digit, but we don’t know which one. For every given picture we want to find other pictures that contain the same digit written with a similar style.</p><p>First, we need to find a representation for the pictures that we can use to operate. In this case it will be simply a vector, the length d of the vector is the number of pixels in the picture, and the components are the RGB values of the pixels. This representation has the advantage of working both at the computational level, but also at the mathematical level.</p><p>Second, we need to define similarity. Since the pictures are represented by vectors, we can compute the angle between any two of them; if the angle is small, the vectors point in the same direction and the pictures are similar. On the other hand, if the angle is big, the vectors diverge from each other and the pictures are not similar. More formally, the similarity between two pictures represented by vectors X and Y is given by:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*g1i-2gpvxvqFmc-UZKxH7w.png" /></figure><p>This quantity has a very nice property: It will never be more than 1, or less than -1. Given two vectors, if their similarity is close to 1 then they are very similar; if it is close to -1 they are completely different. 0 is in the middle — not very similar, but not completely different either.</p><p>Let’s see this in action:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8S_pzzD3aHb-7vDSg-ZGhQ.png" /><figcaption>Two very similar 4s, their similarity according to equation 1 is 0.85</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/942/0*FumfzwCXQSemtR-M.png" /><figcaption>Two very different 4s, their similarity according to equation 1 is 0.11</figcaption></figure><p>Figure 1 shows a graphical representation of the vectors computed from two handwritten digit pictures. These two digits are very similar, and when their similarity is computed by their angle using equation 1 it gives 0.85, which is, accordingly, quite high. On the other hand, Figure 2 shows two quite different numbers; this time their similarity is 0.11, which is quite low but still positive — even though the writing style is very different, both pictures are still a 4.</p><p>These pictures were handpicked to illustrate the vector representation and the <a href="https://en.wikipedia.org/wiki/Cosine_similarity">cosine similarity</a>. We now move on to finding an algorithm that finds similarly handwritten digits.</p><h3>2. A simple solution</h3><p>Now that we know how to represent things and compute their similarity, let’s solve the k-NN problem:</p><ol><li>For every picture in our database, compute its associated vector.</li><li>When the <em>k</em>-Nearest Neighbours for a picture are requested, compute its similarity to every other picture in the database.</li><li>Sort the pictures by ascending similarity.</li><li>Return the last <em>k</em> elements.</li></ol><p>This is a very good solution (especially because it <em>works</em>). Figure 3 shows this algorithm in action. Every row shows the top 9 most similar pictures to the first picture in the row. The first line captures very rounded 3s, the second inclined 3’s, the fourth line shows 2’s with a loop on the bottom, and the 5th line shows Z like 2’s. Notice that this algorithm has no information about what digit is in the picture (nor, for that matter, anything about what kind of things the picture has), but it nevertheless succeeds to group by digit, and even by typographic style.</p><p>But let’s take a closer look by analysing its computational complexity. Consider <em>n</em> pictures with <em>d</em> pixels each:</p><ol><li>Computing a feature vector is <em>O(d)</em>. As this is done for every picture, the first step is <em>O(nd)</em>.</li><li>The similarity function is <em>O(d)</em>, and again this happens $n$ times, then the second step is also <em>O(nd)</em>.</li><li>The third step — sort <em>n </em>elements — is <em>O(nlogn)</em></li><li>Finally returning the last <em>k</em> elements could be constant, but let’s consider it <em>O(k).</em></li></ol><p>In total we get:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*O-foT6f_TecssMd2Ohc5Ww.png" /></figure><p><em>k</em> is very small compared to <em>n</em> and <em>d</em>, so we can neglect the last term. We can also collapse the two first terms into one single<em> O(nd)</em>. Now, note that <em>logn</em> is much smaller than <em>d</em>, for example, if we have 10 million pictures with 256 (<em>d</em>) pixels each, <em>logn</em> would be 7, much smaller than $d$. That means we can turn the <em>O(nlogn)</em> into another <em>O(nd)</em>. Therefore the computational complexity of this algorithm is <em>O(nd)</em>, that is, linear in both total the number of images in our database and the the number of pixels per picture.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*6pfzbMM6AwLdhFXV.png" /><figcaption>k nearest neighbours for some pictures. Every row depicts the top 9 most similar pictures to the first picture in the row</figcaption></figure><h3>3. Can we do better?</h3><p>If we do computational complexity analysis, it is natural to ask ourselves whether we can improve. So let’s try.</p><p>One idea to consider is to use a heap to keep track of the <em>k</em> most similar items. The heap would never be larger than <em>k</em> so every insertion involves<em> O(logk)</em> similarity computations <em>(O(d))</em>, so an insertion is <em>O(dlogk)</em>. Since there will be <em>n</em> insertions, in total we get <em>O(ndlogk)</em>, which is not an improvement.</p><p>We could also try to exploit the fact that we do not need to sort the $n$ elements, just to get the top <em>k</em>. The algorithm would be exactly the same, but step 3 would be replaced by applying <a href="https://en.wikipedia.org/wiki/Quickselect">quick-select</a> instead of sorting. This would change the <em>O(nlogn)</em> term to <em>O(n)</em>, that gives <em>O(nd) + O(n)</em> which is <em>O(nd)</em>, again, not an improvement.</p><p>The last idea we will consider is to use a <a href="https://en.wikipedia.org/wiki/Binary_space_partitioning">Space Partitioning Tree</a> (SPT). An SPT is a data structure that allows us to find the closest object to another object in logarithmic time. A priori this seems to be the right solution but there is a problem: SPTs can only operate under certain distance functions, specifically <a href="https://en.wikipedia.org/wiki/Metric_(mathematics)"><em>metric</em> distances</a>.</p><p>SPTs work with distances, not with similarities. But there is a very close relationship between similarity and distance. In the context of <em>k</em>-NN, for every similarity function there exists a distance function such that searching the <em>k</em> most <em>similar</em> items is equivalent to searching the <em>k</em> closest items using that distance function. Just multiplying the similarity by −1 gives such a distance. So now we have a <em>cosine</em> distance that we could use in an SPT, but unluckily this cosine distance is not a metric distance.</p><p>A metric distance is a distance that complies the following conditions:</p><ol><li><em>distance(x, y) </em>≥<em> </em>0</li><li><em>distance(x, y) </em>= 0 ⇔ <em>x</em> = <em>y</em></li><li><em>distance(x, y) = distance(x, y)</em></li><li><em>distance(x, z) </em>≤ <em>distance(x, y) + distance(y, z)</em></li></ol><p>Cosine distance clearly violates the first condition, but this is easy to fix by just adding 1. The second and the third conditions are met. Finally the fourth condition is violated and this time we cannot fix it. Here is an example of 3 vectors that violate the fourth condition:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_UEc_O5k7FImQeGppGmVEw.png" /></figure><p>And then:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DVK9Cgf8LUKPpf02y_ILZQ.png" /></figure><p>which proves that condition 4 is not met.</p><p>In the following sections we are going to show a trick to overcome this limitation.</p><h3>4. Maths</h3><p>Let’s introduce some properties of vectors that we’ll exploit later.</p><h4>Cosine distance is invariant under Normalization</h4><p>First, let’s make a few definitions:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EnoVQ3WgOpsD3cifu90OSA.png" /></figure><p>A consequence of these two definitions is the following:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gCod1XvOEusnP7skLcbUdQ.png" /></figure><p>Which says that the norm of a normalized vector is always 1. This property is quite obvious, but here is a proof:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PpIQlg9s-HlQM5xeAeX4Qw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3RHdhNnmQQ5ilHFiXWpwmg.png" /></figure><p>Another consequence is the following:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0rTEeEol0amy6Nrj8XCxsQ.png" /></figure><p>In words, this means that the angle between two vectors doesn’t change when the vectors are normalized. Normalization only changes the length (the norm of the vector), not its direction, and therefore the angle is always kept. Again, here is the proof:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kL9tjpoRM4c6-oRQ1Vytkw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Wg_hM70aa2WXV0SI-Vuc4g.png" /></figure><h3>From Euclidean to Cosine</h3><p>The second property we need for this trick is the following:</p><p>If <em>Χ </em>and <em>Υ</em> are vectors with norm 1 (unit vectors) then:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZXyKhbxFo1I_QR0JaSQPXg.png" /></figure><p>This states that if <em>Χ</em> and<em> Υ</em> are unit vectors then there is an exact relationship between the euclidean distance from <em>Χ</em> to <em>Υ</em> and the angle between them.</p><p>The proof:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0twD-YH0ZR3l3IyRqc_LbA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*W-B5CEJpO4QpvvVTLSodjA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sWbKgdUSO0ektvk8CnrmIg.png" /></figure><p>And since <em>X</em> and <em>Y</em> are unit vectors, dividing by their nor is dividing by one:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ifm36XK8XHsjQAum9hkIww.png" /></figure><h3>Cosine ranking is equivalent to Euclidean ranking</h3><p>By looking at equation <strong>5</strong> we can already see that if <strong><em>1-cos(X,Y)</em></strong> is bigger, then ‖X-Y‖ must be bigger. That means that if <em>Y</em> gets away from <em>Χ </em>in the euclidean space, it also does in the cosine space, provided both <em>X</em> and <em>Y</em> are unit vectors. This allows us to establish the following:</p><p>Consider three arbitrary <em>d</em>-dimensional vectors <em>X</em>, <em>A</em> and <em>B</em>. (they don’t need to be unit vectors). Then the following holds:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jJ4chltQCVd1aRAOQY4foQ.png" /></figure><p>This equation says that if the cosine distance between <em>X</em> and <em>A</em> is less than the cosine distance between <em>X</em> and <em>B</em> then the euclidean distance between <em>X</em> and <em>A</em> is also less than the euclidean distance between <em>X</em> and<em> B</em>. In other words, if <em>A</em> is closer to <em>X </em>than <em>B</em> in the cosine space, it is also closer in the euclidean space.</p><p>The proof: We start from the left hand side expression and apply operations to get to the right hand side expression.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*r1sL4IPlFoPpn4uQyQUiYg.png" /></figure><p>cosine is invariant under normalization (see equation 4)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rJ9uZQze7CWyFV-rYQcJVw.png" /></figure><p>doubling and taking squared root keeps the inequality</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZGKi12Buy34mjTwqdcB1UQ.png" /></figure><p>normalized vectors are unit vectors (see equation 5)</p><p>This is all the maths we need to apply the trick. Let’s see what is it about.</p><h3>The k-NN Trick</h3><p>The goal of this trick is to find a way to be able to use cosine similarity with a Space Partitioning Tree, that would give us <em>O(log n)</em> time complexity, which is a huge improvement.</p><p>The idea is actually very simple: Since cosine similarity is invariant under normalization, we can just normalize all our feature vectors and the<em> k</em>-nearest neighbours to <em>X</em> will be exactly the same; but now our vectors are all unit vectors, which means that sorting them by cosine distance to <em>X</em> is exactly the same as sorting them by Euclidean distance to <em>X</em>, and since Euclidean distance is a proper metric we can use a Space Partitioning Tree and enjoy the logarithm of <em>n</em>. Here’s the recipe:</p><ol><li>Normalize all the feature vectors in the database</li><li>Build a Space Partitioning Tree using the normalized vectors</li><li>When the <em>k</em> nearest neighbours to an input vector <em>X</em> are requested:<br> - Normalize <em>X<br> - </em>look up the <em>k</em>-NN from the Space Partitioning Tree</li></ol><h3>6. Experiments</h3><p>Experimentation is at the core of Product Development at Booking.com. Every idea is welcomed, turned into a <a href="https://en.wikipedia.org/wiki/Hypothesis">hypothesis</a> and validated through experimentation. And Data Science doesn’t escape that process.</p><p>In this case, the idea has been thoroughly described and supported with practical examples and even maths. But let’s see if reality agrees with our understanding. Our hypothesis is the following: We can improve the response time of the algorithm described in section 2 by applying the trick described in section 5 guaranteeing exactly the same results.</p><p>To test this hypothesis we designed an experiment that compares the time needed to solve the <em>k</em>-NN problem using the full scan solution with the time needed by the <em>k</em>-NN trick solution. The <em>k</em>-NN trick is implemented using two different Space Partitioning Trees: <a href="https://en.wikipedia.org/wiki/Ball_tree">Ball Tree</a>, and <a href="https://en.wikipedia.org/wiki/K-d_tree">KD-Tree</a>.</p><p>The database consists of handwritten digits pictures from <a href="https://en.wikipedia.org/wiki/MNIST_database">MNIST</a>. For n ranging from 5000 to 40000 randomly sampled <em>n</em> pictures from the original database; then applied the different solutions to the same sample, computing the 10 most similar pictures for 20 input pictures.</p><h3>7. Results</h3><p>The results of our experiment are summarized by Figure 4:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*fV5iAP5KTvR4Wz9w.png" /><figcaption>Comparison of the full scan solution (brute force) and the k-NN trick (norm euclidean ball tree, and kd tree) for different database sizes <em>n</em></figcaption></figure><p>From the chart we can make several conclusions: First, the time complexity of the full scan solution is indeed linear in n as suggested by the blue dots. This confirms the theoretical analysis in section 2. Second, although it is hard to say if the <em>k</em>-NN trick based solution is logarithmic, it is clearly much better than the full scan, as suggested by the green and red dots. Third, the Ball Tree based solution is better than KD-Tree solution, though the reason for this fact is not clear and requires further analysis and experimentation. Overall, the experiment strongly supports the hypothesis.</p><h3>8. The Trap</h3><p>Every trick sets up a trap, and every gain in one aspect hides a loss in another. Being aware of these traps is key to successfully apply these tricks. Let’s see what trap the <em>k</em>-NN trick sets, or, in more technical words, what kind of trade-off are we dealing with?</p><p>In the simple solution, before being able to answer a <em>k</em>-NN query all we need to do is to compute the feature vectors of each object in the database. On the other hand, when using the trick, before we are able to answer a query we not only need to compute the feature vectors, but also we need to build the Space Partitioning Tree. In the experiment we run, we also recorded the time it takes to be able to answer queries. The results are displayed in Figure 5 and show that the trick-based solutions scale much worse than the simple solution. This means that when using the trick we are trading off query response time with start-up time.</p><p>This trade-off must be taken carefully, and for big databases this can have very negative consequences. Consider an e-commerce website that goes down for whatever reason; imagine that this e-commerce uses <em>k</em>-NN to serve some recommendations, (a very important yet not critical part of the system). As soon as we fix the problem, we want the system to reboot as soon as possible, but if the booting process depends on the <em>k</em>-NN system we fall into the trap — users won’t be able to purchase anything until our Space Partitioning Tree is built. Not good.</p><p>This can be easily solved by breaking the dependence using parallel or asynchronous processes to boot different parts of the system, but the simple solution is clearly more robust in this instance, up to a point where we don’t even need to care. The <em>k</em>-NN trick forces us to consider this very carefully and act properly. For many applications, this isn’t a bad price to pay for the speed and scalability we get at query time.</p><h3>9. Conclusion</h3><p>In this post we described a trick to speed up the standard solution for the <em>k</em>-NN problem with cosine similarity. The mathematical rationale for the trick was presented, as well as experiments that prove its validity. We consider this as a good example of a scalability problem overcome by applying elementary maths. This is also a good example of <a href="https://en.wikipedia.org/wiki/Reductionism">Reductionism</a>: The trick is a reduction from cosine similarity <em>k</em>-NN problem to a Euclidean distance <em>k</em>-NN problem which is a much more studied and solved problem. Maths and Reductionism are two concepts sitting at the core of applied Data Science at Booking.com, always at the service of the best travelling experience.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0i5XhOlvycEHfIe4.png" /><figcaption>Ready-time comparison of the full scan solution and the <em>k</em>-NN trick for different database sizes <em>n</em></figcaption></figure><p>Would you like to be a Data Scientist at Booking.com? <a href="https://workingatbooking.com/vacancies/?filter-searchphrase=data+machine">Work with us</a>!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bec682357ccd" width="1" height="1" alt=""><hr><p><a href="https://booking.ai/k-nearest-neighbours-from-slow-to-fast-thanks-to-maths-bec682357ccd">k-Nearest Neighbours: From slow to fast thanks to maths</a> was originally published in <a href="https://booking.ai">Booking.com ML &amp; DS Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>