Thinking about fuzzy logic and implementing black boxes

Today, Google did a doodle about Dr. Lotfi Zadeh who published a paper that proposed fuzzy logic. It feels like it's been forever since I studied anything that has to do with machine learning so I decided why not, let me learn a bit about this. It turns out, fuzzy logic is actually really applicable to my job (which involves no machine learning at all) not because I need to implement a bunch of ambiguous things on the app (although dealing with ambiguity is a big part of the job) but it proposes a really neat way to think about constructing a black box.

First, my understanding of a fuzzy system. It is effectively a black box, with rules that you can configure. You feed in your raw data on one end, it goes through the "fuzzifier", the "inference engine", the "defuzzifier" and some new data emerges from the other end. The core of this box is the inference engine. Basically, this part of the system holds the rules that you and your product folks come up with. On the input end, the fuzzifier essentially transforms the raw data into a vector of useful characteristics for your inference engine to process. On the output end, the defuzzifier transforms the vector of useful resulting characters that the inference engine has returned into some data that the rest of your code can use.

One example of a fuzzy system might be some black box that determines whether or not a person should be eligible for a loan. On the input, you provide a person's income, credit score, age etc. The fuzzifier transforms it into a vector like [high_default_risk, medium_default_risk, low_default_risk]. Then, the inference engine might apply the following rules to it:
if high_default_risk then low_loan_eligibility
if medium_default_risk then low_loan_eligibility
if low_default_risk then high_loan_eligibility
This would return a vector like [low_loan_eligibility, high_loan_eligibility] which would then be fed into the defuzzifier that would return, maybe, a boolean on whether or not this person is eligible for the loan.

So how is this interesting for my work? Well, one thing that we should all do a lot at work is write code that abstracts away details like these black boxes. For example, let's say there is a black box that takes in a user's IP address, age, and social enable status and then returns which social features the user should be allowed to use. While this is not a fuzzy situation, it is useful to construct the black box in a similar way to the fuzzy system box. The core of our box is what would be the "inference engine", or in this case the "rules engine". These rules should be easy to understand. In this case, imagine in our rules engine we have the following rules:
if is_underage then chat_ineligible
if is_underage and in_usa then news_feed_ineligible
if social_disabled then chat_ineligible
if social_disabled then news_feed_ineligible
We might implement this box like so:
data class SocialFeatureEligibilityStatus(
  val newsFeedEligible: Boolean,
  val chatEligible: Boolean
)

object SocialFeatureEligibilityProvider {
  fun getValidSocialFeatures(ipAddr: Inet4Address, age: Int, socialEnabled: Boolean): SocialFeatureEligibilityStatus =
    encode(ipAddr, age, socialEnabled)
      .applyRulesEngine()
      .decode()
  
  private data class RulesEngineInput(
    val isUnderage: Boolean,
    val isInUSA: Boolean,
    val isSocialDisabled: Boolean
  )
  
  private data class RulesEngineOutput(
    val chatIneligible: Boolean,
    val newsFeedIneligible: Boolean
  )
  
  private fun RulesEngineInput.getRules(): List<(RulesEngineOutput) -> RulesEngineOutput> = listOf(
    { output ->
      if (isUnderage)
        output.copy(chatIneligible = true)
      else
        output
    },
    { output ->
      if (isUnderage && isInUSA)
        output.copy(newsFeedIneligible = true)
      else
        output
    },
    { output ->
      if (isSocialDisabled)
        output.copy(chatIneligible = true)
      else
        output
    },
    { output ->
      if (isSocialDisabled)
        output.copy(newsFeedIneligible = true)
      else
        output
    }
  )
  
  private fun encode(ipAddr: Inet4Address, age: Int, socialEnabled: Boolean): RulesEngineInput {
    val isUnderage = isUnderageDeterminer(age)
    val isInUSA = isInUSADeterminer(ipAddr)
    val isSocialDisabled = isSocialDisabledDeterminer(socialEnabled)
    
    return RulesEngineInput(isUnderage, isInUSA, isSocialDisabled)
  }
  
  private fun RulesEngineInput.applyRulesEngine(): RulesEngineOutput =
    getRules().fold(
      RulesEngineOutput(
        chatIneligible = false,
        newsFeedIneligible = false
      )
    ) { outputState, rule -> rule.invoke(outputState) }
  
  private fun RulesEngineOutput.decode(): SocialFeatureEligibilityStatus =
    SocialFeatureEligibilityStatus(
      newsFeedEligible = !newsFeedIneligible,
      chatEligible = !chatIneligible
    )
}
The structure of this code feels really great to me because it is feels so reusable. Just plug in your new encode, decode, and rules and you're good to go! Plus, it's super easy to read. In terms of the product folks, they'd just need to focus on that one function for "getRules()" and make sure everything is right in that list of rules. It's also very easy to A/B test on as well: Just change your encode to write in the experiment condition and include a new rule for your experiment. I think I'll be able to apply this new learning to stuff in my job very soon!

Comments

Popular posts from this blog

First-Principles Derivation of A Bank

A Play-by-play of the Mirai botnet source code - Part 1 (scanner.c)

You can control individual packets using WinDivert!