A word insertion hack to fix parts of speech assigned to short strings in Swift

Gary Bartos
6 min readFeb 19, 2023
A graphic of a chef riding a scooter and holding up a pizza, behind which appear the words “Free Delivery!” Would you order a pizza that was delivered without a box?
“Is delivery no additional charge, or am I supposed to set free someone named Delivery?” (from clipartmax.com)

Apple’s NaturalLanguage library may not assign parts of speech accurately for strings of just two or three words. But that’s understandable, right? The two-word phrase “Order pizza” can be interpreted as two nouns rather than as “[Verb] [Noun].”

But maybe you’re writing a chatbot feature for a pizza ordering app that needs to work tomorrow. It’s four o’clock in the afternoon and you just found out that “Order pizza” is interpreted as “[Noun] [Noun],” but your app takes actions based on words being identified as verbs. Yikes!

For the TLDR crowd, this post proposes inserting one or more words to modify user input. Rather than call more NLP functions, or use some other library, we hack the string to be processed. This is a bit different from removing stop words or reducing text to just named entities or keywords.

The Right Solution vs. The Solution for Right Now

So you’ve identified a bug and you’re short on time. You think about ordering pizza for your all-night debug session, but your pizza ordering app doesn’t parse ‘Order pizza” correctly. Sigh.

There’s the right way to solve this problem. The right solution. The solution some NLP expert will post three days after you post in StackOverflow. Someone out there has solved this problem before. But you don’t have time to find The Right Solution.

Let’s go with a Right Now! Solution.

A function for parts of speech

You’ll already have a function to assign parts of speech to the words in a string. The parts of speech are Noun, Verb, Adjective, and so on. Being the diligent developer that you are, you copied the sample code from Apple, made a few tweaks, and after five minutes you came up with a partsOfSpeech(:) function like this:

import NaturalLanguage

public func partsOfSpeech(_ text: String) -> [(word: String, tag: NLTag)]
{
//https://developer.apple.com/documentation/naturallanguage/identifying_parts_of_speech
let tagger = NLTagger(tagSchemes: [.lexicalClass])
tagger.string = text

let options: NLTagger.Options = [.omitPunctuation, .omitWhitespace]

var taggedWords = [(String, NLTag)]()

//https://developer.apple.com/documentation/naturallanguage/nltagger/2976623-enumeratetags
tagger.enumerateTags(
in: text.startIndex ..< text.endIndex,
unit: .word,
scheme: .lexicalClass,
options: options)
{
tag, tokenRange in

if let tag = tag
{
let word = text[tokenRange]
// print("\(word): \(tag.rawValue)")
let pair = (String(word), tag)

taggedWords.append(pair)
}

return true
}

return taggedWords
}

And it works! You pass the very natural language-y sentence “Tell me the closest pizzeria with deep dish pizza” to the function and the parts of speech are labeled as you’d expect.

"Tell me the closest pizzeria with deep dish pizza"

Tell: Verb
me: Pronoun
the: Determiner
closest: Adjective
pizzeria: Noun
with: Preposition
deep: Adjective
dish: Adjective
pizza: Noun

You read Natural Language Processing Succinctly by Joe Booth and you applied what you learned. Your app does all sorts of cool things with tagged words.

Your app interprets the verb “tell” as a command to use text to speech. The noun “pizzeria” indicates what the user wants information about. The adjective “closest” becomes a prompt to use GPS coordinates.

The logic is solid. Your app is cool. Users will love this app, and by proxy they will love you. Or they would, if your app handled the simplest command.

The parts of speech in “Order pizza”

At 3:57 you pass the text “Order pizza” to your function partsOfSpeech(:) and you get this.

Order: Noun
pizza: Noun

No verb there. Maybe your app thinks the user is asking to check an existing order (noun) for pizza (noun). But there is no existing order for a pizza, because the user can’t order pizza using the command “order pizza.” There is no love.

The 5 o’clock Hack: insert “the”

A word like “order” can be a noun or a verb. You want “order” to be recognized as a verb in the phrase “order pizza” but as a noun in the phrase “my pizza order.”

Without the context of a sentence with many words, parts of speech may not be assigned correctly. But if you modify the user’s input, you might fix this problem without having to write custom NLP functions.

For the example “Order pizza”, we might try a very simple function that inserts the word “the” if text contains just two words:

func insert(_ text: String, inserted: String) -> String {
//simplified split!
let comps = text.description.components(separatedBy: " ")
return comps.count == 2 ? "\(comps[0]) \(inserted) \(comps[1])" : text
}

Perhaps you’ll only call this function if the text input is identified as two nouns.

Inserting “the” will change “Order pizza” to “Order the pizza.” Let’s pass the text “Order the pizza” to our partsOfSpeech(:) function.

 Order: Verb
the: Determiner
pizza: Noun

Aha! “Order” is now considered a verb. Maybe you don’t have to stay up all night finding The Right Way to fix the bug.

If you call the insert(:) function with “order pizza” reversed to become “pizza order,” the parts of speech remain noun and noun for “pizza” and “order.”

Pizza: Noun
the: Determiner
order: Noun

That’s good, because our gut feeling is that “pizza order” should be interpreted as the user asking about an order for a pizza.

“find zebra”

Users may use unnatural language as input to your natural language processing.

Let’s say you’re working on an iPhone app that processes images from webcams and camera traps to find animals and provide info about an animal currently in view.

Your app provides the example of finding a pileated woodpecker like this fine specimen from the Wikipedia entry:

A pileated (or “red-capped”) woodpecker grasping onto a tree, apparently in the about-to-peck position.
How pileated is this woodpecker? What would your chatbot say? “This bird is VERY pileated.”

The example command is “Find a pileated woodpecker.” Here are the raw results of passing “Find a pileated woodpecker” to the function partsOfSpeech(:)

 Find: Verb
a: Determiner
pileated: Verb
woodpecker: Noun

Confirming that “find” is meant as a verb is useful. The app processes verbs as commands to perform specific actions. The word “find” is sometimes used as a noun, as in “That pileated zebra is a rare find.”

Since your app is about animals, you may have “pileated woodpecker” as a lookup entry. Thus you can ignore “pileated” being identified as a verb. No need for the partsOfSpeech(:) function to be over-complicated.

And you use the example “Find a pileated woodpecker” because you know it works.

“Find zebra” and the curse of OtherWord

But some user may not want to type out or speak a full natural language sentence. Maybe all you get from the user is “find zebra.” Just two words. A native English speaker wouldn’t say “Find zebra” to another person, but might well use that clipped phrase in a search.

The result? Not what we’d like.

Find: OtherWord
zebra: OtherWord

Both “Find” and “zebra” are tagged with OtherWord. Ugh.

If we try our insert(:) function action to insert “the” between two nouns, the result is better:

 Find: Verb
the: Determiner
zebra: Noun

Find is a verb, and can be processed as an action.

Although we’ve inserted “the,” it’s a stop word which we can disregard.

And now that “zebra” is identified as a noun, your app logic knows what is meant to be acted upon.

Try Prepending and Appending Words

I had an example in which prepending “She said” to a short input phrase allowed the parts of speech to be tagged properly. I’m sure I’ll find that example again some day.

In my quick test, appending “please” as in “Find zebra, please” didn’t help with correct tagging of parts of speech, but it’s another hack to try if you’re in a hurry.

--

--

Gary Bartos

Founder of Echobatix, developing assistive technology for the blind. echobatix@gmail.com