the two halves of my tasty brain

Bayesian Classification

A Brief Distraction

After using the classifier I originally laid out in this post, I discovered that my method of calculating was very flawed. I have made the appropriate revisions.

The Overview

So, as everyone knows by now, Bayes’ Theorem is expressed as:

What I’m going to talk about isn’t new or novel, but it’s something that interests me, so there. I want to look at using Bayes’ theorem to classify documents. This has been done to death with regard to classifying spam, but I’m going to take a more general look. For our purposes, a document is a sequence of terms such as: “This cat isn’t made of butterscotch”; a paragraph, or the HTML source of this page. The idea is to classify a bunch of documents by hand to “train” our classifier so that when new documents come along, we can ask the classifier where they belong. One way to accomplish this is through the use of Bayes’ theorem.

The Real Work

To start, we’ll need a way to tokenize a document into terms. The tokenizer’s implementation will depend upon the type of documents under consideration. To keep things somewhat simple, let’s assume we’re working with English phrases; thus, the terms produced by our tokenizer are simply words. Let’s suppose we have a set of categories, . So, what we’re then after is the probability that a document belongs in a particular category, , given these extracted terms:

We’re letting represent our document, and then expanding the document into its individual terms, . You’ll notice that we’ve skipped the expansion of , the justification for this will be explained shortly.

We can expand this using the rules of conditional probability:

This formulation is available on the Wikipedia page on Naive Bayes classifiers. As we are taking a naive approach, we assume there is no interdependence between terms, ie:

Now, when it comes to words, this is a obviously poor assumption. For instance, it is far more likely that the word “poor” would appear before “assumption” than the word “Faustian” in day-to-day phrases. However, we’re going to go ahead with the naive assumption for now, which simplifies our equation:

For any term, , we take to mean the probability of that term occurring given we’re working with category , and we calculate it thusly:

where is the number of times term occurs in category . This brings us to , meaning the probability of choosing category without taking the document into consideration. There are a number of ways we could calculate , such as assuming that all categories are equally likely:

where denotes the number of categories in our classifier. While this works, a better measure of might take into account the number of documents a category has been trained with:

where indicates the number of documents belonging to category . All other things being equally, will favor categories that have been fed the most documents. If each category is trained with the same number of documents, then we’re back to the uniform probability given earlier.

Basing on document counts does pose a problem. Suppose the following documents belonged to the same category:

this document is pretty small
small this document is pretty
pretty small this document is
is pretty small this document
document is pretty small this

These are all distinct permutations of the same string, and thus distinct documents. The problem is that the classifier we are in the process of constructing deals with terms. Our assumption of the independence between terms means that their ordering has no bearing on the classifier. From this position, we could argue that each of the five documents listed above are actually the same document as far as our classifier is concerned, and if we relied on document counts for , this set of documents would give some category an unfair bias. So, let’s consider another alternative for calculating :

This approach is calculating the probability of a given category as the sum of occurrences of all terms in the category and the sum of occurrences of all terms across all categories. Unfortunately, this form may also be unfairly biased by document repetition (e.g. if we fed a category the five documents shown earlier.) Let’s consider one more alternative:

where and are defined as:

These definitions may seem complicated, but they are both very simple concepts: is just counting the number of distinct terms in category and is counting the total number of distinct terms from all categories.

While each of the four possibilities presented for are very different from one another, they are all valid approaches. Personally, I feel is too naive in its assumption that all categories are equally likely to be selected, and as I intend on keeping track of only terms and categories, I can also rule out . As mentioned earlier, can be biased by repeated terms, so I’m going to opt for . Again, I wish to stress that this is a personal choice, your Bayesian needs may differ from mine.

With that all taken care of, we can now expand our classifier to:

Our problem is now just one of counting. The expression may look horrible, but that is largely a result of each expression being explicitly defined. Now that we know what the expressions mean, let’s do an informal clean up of this warlock by substituting simpler symbols for our sweet mess of expressions. Before we do, we need to take a closer look at , which in earlier revisions of this post was explicitly, and incorrectly, defined. For a given document, will be the same, regardless of the category under consideration. When our classifier is asked to classify a document, it will iterate over each of its categories and perform this calculation, which each of the final category probabilities will be scaled by this same term. So, our first simplification will be to substitute with , which we will not explicitly define as we will eventually discard it.

Now, let’s let be the number of unique terms in the category we are considering and be the number of unique terms in all of our categories. Finally, we let be the number of occurrences of term in the current category and be the occurrences of all terms in all of our categories. With these simplifications, our equation becomes:

Hopefully it is now painfully obvious that each category will be scaled by a common factor, namely . The purpose of our classifier is to pick the category a given document most likely belongs in, and if they are all scaled by this common term we can disregard it. In an effort to preserve mathematical accuracy, I have opted to multiply both sides of the equation by .

All we’ve gained from this simplification is some mental manageability. I wanted to derive the explicit calculations involved in Bayesian classification, but products of fractions containing summations don’t tend to be very memorable. Any additional calculations we may need to perform on the explicit form would have made things even messier, which serves as a nice segue into our next step.

In a world where we are free to perform these calculations without loss of precision, this expression is just fine. However, given that we are multiplying a series of numbers all in the range , a computer may eventually round a product to 0, and totally ruin our day. We can correct this with a little help from a logarithm*:

Is this logarithm business really necessary? Consider a document with 324 unique terms where, thanks the awesome power of contrived examples, for each term. When we take the product of all these terms, we end up with . That’s a pretty small number, so small that Ruby mistakes it for 0:

0.1 ** 324 # => 0.0

Now, we did factor out of our product earlier, but is also bound to be very small. This post alone is up to 608 unique terms, just to give an example of how likely it is that our classifier might experience floating-point underflow.

Digging Deeper

There are a few edge cases to consider, here are five I can think of off of the top of my head:

  1. What if there are no known terms for a particular category?
  2. What if there are no known terms for the classifier as a whole?
  3. What if a term is unknown to a particular category?
  4. What if a term is unknown to the classifier as a whole?
  5. How do we handle repeated terms (i.e. ) in a document that our classifier is attempting to classify for us?

We can easily address the first two cases. If any category has not been trained, we assume , thus if none of our categories have been trained (case #2), we assume all categories are equally likely (or unlikely) and either return an arbitrary one or none at all. The third and fourth cases are consequences of the fact that we are estimating probabilities. Through training, our classifier is building approximations for these various probabilities. It is entirely possible, perhaps even very likely, that we will encounter documents we wish to classify containing terms that were not included in our training. If we rely solely on the calculations presented here, a foreign term will result in zeroing the overall probability or produce an error — either from a division by zero or from the undefined calculation. To prevent garbage results, we will need to assign a non-zero probability, , to terms unknown to a particular category.

The fifth case deserves its own paragraph. To handle this situation, we defer to simple rules for conditional probabilities. Consider the following:

If and are describing the same event, then the expression simplifies to:

The same holds true for documents we wish to classify. We could argue that repeated terms do not describe the same event because the terms are occurring in distinct positions; however, we have not worked positional information into our classifier. It is important to note that when we are training our classifier with documents, we do take repeated terms into consideration.

Final Business

Now that virtually every email client has, at one point or another, made use of some form of Naive Bayes classification, I realize it’s no longer the hot topic it once was. However, while reviewing the Ruby classifier, I discovered that their Bayes implementation was wrong. This was independently confirmed by Brian Muller. While working on a fork of it with Zeke, I discovered that while the approach is well known, there are nuances that can significantly impact the results — examples being the choice in and the default probability, .

Foot Noted

Note: Logarithms

I would like to thank Brian Muller for explaining the use of logarithms in a Naive Bayes classifier he’s developing in Ruby, even if we don’t share the same views on . My Numerical Analysis professors would probably be displeased that I forgot about this trick. [ jump back ]