• 10 min reading time

5 Jun 2020

How to Analyse Language Using ‘NLTagger’ in Swift

Embracing the AI power of Apple’s native Natural Language framework


I’ve written a small, AI-powered to-do app. The app is a simple list of to-do items that you can delete from your list — your standard to-do list app with one difference: the app recognises a user’s input text and automatically appends a relevant image next to it.


Image by Willi Heidelbach


As an example, if the user adds and a to-do item, “buy bananas,” the app will classify the word bananas, find and download the image of bananas. and append it next to the to-do item. It looks like a bit of magic, but it’s a pretty simple thing to do using Apple’s native Natural Language framework. (Reference to open-source App repository is at the bottom of this article.)


Want to know how I’ve done that? Read further.


Using Apple’s native Natural Language framework (available in Swift and Objective-C), you can extract a text’s lexical class (nouns, verbs, pronouns, and adjectives), the language of input text, and even a sentiment score that tells how positive/negative the inputted text is.


Now my example is a little to-do app, but you can easily use the framework in many more ways to enrich user experience in your app. As an example, how helpful would it be for you to know your user’s sentiment score based on their input text? You could customise the way your app responds if a user is feeling positive/negative.


Alternatively, you could do something similar to what I’ve done in my little app. You could use input pronouns/nouns as a search term using a remote image-search API to fetch a relevant image to display next to the user’s input item.


Code speaks more than an essay about it, so lets have a look at two examples.


Example 1: How Positive/Negative Is a String?

There are only a few lines of code needed to classify input strings in Swift, so let’s have a look at the code.


import NaturalLanguage

class LanguageProcessor {
    
    private let tagger: NLTagger
    
    init(tagger: NLTagger) {
        self.tagger = tagger
    }
    
    func getSentimentScore(from sentenceString: String, sentimentScore: @escaping (Double?)->()) {
        
        guard let range = sentenceString.range(of: sentenceString) else {
            sentimentScore(nil)
            return
        }

        tagger.string = sentenceString
        tagger.enumerateTags(in: range,
                             unit: .paragraph,
                             scheme: .sentimentScore,
                             options: [.omitWhitespace]) { (tag, range) -> Bool in
                                
                                let score = Double(tag?.rawValue ?? "")
                                sentimentScore(score)
                                return true
        }
    }
    
}

// EXAMPLES
let languageProcessor = LanguageProcessor(tagger: NLTagger(tagSchemes: [.sentimentScore]))
let sentence1 = "I love eating broccoli."
let sentence2 = "I don't like eating broccoli"

languageProcessor.getSentimentScore(from: sentence1) { sentimentScore in
    print(sentimentScore) // 1.0 (most positive)
}

languageProcessor.getSentimentScore(from: sentence2) { sentimentScore in
    print(sentimentScore) // -0.8 (very negative)
}

We need a way for a user to input text (you can pass that through a text field or text view as an example from your app).


Next, we need to import the NaturalLanguage framework and create an instance of the NLTagger class. I wrapped the whole thing into a LanguageProcessor class to make it easy to reuse.


As you can see, I added a function called getSentimentScore(from sentenceString: String, sentimentScore: @escaping (Double?)->()) that’ll give us an output of a sentiment score, somewhere between -1.0 (most negative) and 1.0 (most positive) to inform us about the sentiment of the inputted text.


Firstly, we need to get a range of our string; I wrapped the range in a guard statement at the top of the function just in case the input string doesn’t have any characters. In my example, I’m aiming to evaluate the whole input string, so my range will be from the first to the last index of the input string.


Next, we need to assign the input string to the NLTagger instance and call our main function, tagger.enumerateTags.


Let’s look at the parameters that the enumerateTags function is asking for in a bit more detail:


  1. First is the string range, which we already have.

  1. unit: of type NLTokenUnit. In my case, I used .paragraph. However, this could be anything from a word, sentence, and paragraph to document(the whole input string).

  1. scheme: of type NLTagScheme. This will be the .sentimentScore. Some other examples in here could be the language or lexical class (an example of that will come down below).

  1. options: an optional array of type [NLTagger.Options] which can help omit words, punctuation, or, in our case, .whitespace.

    In the closure, we’re interested in the tag?.rawValue string, which we typecast to Double to get our sentiment score.
    Now the only thing we have to do is to create an instance of our LanguageProcessor class and instantiate it with the [.sentimentScore] tag scheme. Now, we call getSentimentScore with two example sentences; the first one gives us a sentiment score of 1.0 (most positive), and the second one is -0.8 (pretty negative).

Example 2: Getting Verbs From a String

Now let’s have a look at another code example that’ll return all the verbs from the input text.


import NaturalLanguage

class LanguageProcessor {
    
    private let tagger: NLTagger
    
    init(tagger: NLTagger) {
        self.tagger = tagger
    }
    
    func getVerbs(from string: String, verbs: @escaping ([String]?) -> ()) {
        
        guard let range = string.range(of: string) else {
            verbs(nil)
            return
        }
        
        var verbsArray: [String] = []
        
        tagger.string = string
        tagger.enumerateTags(in: range,
                             unit: .word,
                             scheme: .lexicalClass,
                             options: []) { (tag, range) -> Bool in
                                
                                if tag?.rawValue == "Verb" {
                                    verbsArray.append(String(string[range]))
                                }
                                
                                return true
        }
        
        verbs(verbsArray)
    }
    
}

// EXAMPLE
let languageProcessor = LanguageProcessor(tagger: NLTagger(tagSchemes: [.lexicalClass]))
let sentenceWithVerbs = "I had gone for a run in the moonlight, then I have seen a unicorn."

languageProcessor.getVerbs(from: sentenceWithVerbs) { verbs in
    print(verbs) // ["had", "gone", "have", "seen"]
}

Our code is very similar to the above except that our NLTokenUnit is .word, and our NLTagScheme is .lexicalClass


As a lexical class can be anything from a noun, verb, pronoun, or number (the full list of possible lexical classes is here), we need to make sure only to parse verbs, so we check if tag?.rawValue == “Verb” and then append String(string[range]) to our outputVerbs array..


Now we call our functions with an example sentence, where you can see an array of verbs parsed.


In short, using the Natural Language framework is a great and easy way to evaluate strings that can add some magic to your app.


To-do App Reference



GitHub link: My AI-powered to-do app is open source on GitHub — written in Objective-C and MVVM, using Realm as a persistence framework due to therequirements of a project.


I hope you enjoyed this article and learned something new on the way.


If you have any thoughts on this article, I would love to hear them!
You can always get in touch with me via Twitter or LinkedIn 🙂


Cheers,


Tim