~/masum
All posts

Dropping loan spam on Android without TensorFlow

· by Sk Masum Ali

Regex rules are great at pulling currency amounts and merchant names out of bank texts. Where they struggle is intent. A marketing notification from a bank saying 'Take a personal loan up to ₹40 lakh with EMIs starting from ₹1,000' contains a valid currency sign, clean numbers and an account keyword. Expanding deterministic regex rules to catch every promotional variation creates brittle heuristics and eventual false positives.

A gatekeeper, not an extractor

The solution in Cipher is an on-device intent classifier that acts as a front door. Incoming messages are classified into four intents: TRANSACTION_EXPENSE, TRANSACTION_INCOME, PROMOTIONAL_MARKETING, or INFORMATIONAL_OTP. If the model flags the message as promotional or an OTP with high confidence (at least 0.70), Cipher drops it immediately before running any extraction patterns.

Crucially, the ML model never extracts numbers or dates. Using small machine learning models to extract variable figures on mobile invites hallucinations and silent accounting errors. Deterministic rules extract the amounts with 100% precision; the ML model only decides whether extraction should happen at all.

Zero native runtimes

Bringing in TensorFlow Lite, ONNX Runtime, or PyTorch Mobile would add 2 to 4 MB of native C++ shared libraries (.so files) to the APK, along with ABI split management and extra memory overhead. For classifying short texts into four buckets, a deep neural network is overkill.

Instead, the model is a Multinomial Logistic Regression classifier trained over n-gram TF-IDF features (unigrams, bigrams, and trigrams). The vocabulary of 2,409 tokens, IDF lookup table, and weights matrix (2409 × 4 floats) are bundled into a single 307 KB JSON asset. The entire vector math and softmax inference are implemented in pure Kotlin in under 80 lines:

fun classify(text: String): IntentPrediction {
    val features = featureExtractor.extract(text)
    val scores = FloatArray(NUM_CLASSES)

    for (classIdx in 0 until NUM_CLASSES) {
        var dotProduct = biases[classIdx]
        for ((featureIdx, weight) in features) {
            dotProduct += weight * weights[classIdx][featureIdx]
        }
        scores[classIdx] = dotProduct
    }

    val probabilities = softmax(scores)
    val (bestClass, confidence) = argMax(probabilities)
    return IntentPrediction(bestClass, confidence)
}

Because the active features for a single SMS rarely exceed 30 tokens, inference is simply a sparse dot product (< 150 float operations). It executes in under two milliseconds on modern ARM chips, with zero native memory allocations.

Graceful fallback without silent drops

If the JSON asset fails to deserialize or an unusual message produces low confidence (< 0.70), Cipher does not discard the message. The classifier drops to an offline fallback with a fixed 0.50 score, which lets the message proceed directly into the standard deterministic regex suite. Real transactions are never silently dropped due to model uncertainty.

Command menu

Jump to a page, open a profile, or run an action