Loves Me, Loves Me Not: Classify Texts with TensorFlow and Twilio

Lizzie Siegle - Feb 14 '20 - - Dev Community

cover
This blog post was written for Twilio and originally published on the Twilio blog.

Valentine's Day is coming up and both love and machine learning are in the air. Some would use flower petals to determine if someone loves them or not, but developers might use a tool like TensorFlow. This post will go over how to perform binary text classification with neural networks using Twilio and TensorFlow in Python.
sms example

Prerequisites

Setup

bear picking flowers loves me loves me not
Activate a virtual environment in Python 3 and download this requirements.txt file. Be sure to use Python 3.6.x for TensorFlow. On the command line run pip3 install -r requirements.txt to import all the necessary libraries and then to import nltk, make a new directory with mkdir nltk_data, cd into it and then run python3 -m nltk.downloader. You should see a window like this, select all packages as shown in the screenshot below:
nltk downloader
Your Flask app will need to be visible from the web so Twilio can send requests to it. Ngrok simplifies this. With Ngrok installed, run ngrok http 5000 in the directory your code is in.
ngrok url

You should see the screen above. Grab that ngrok URL to configure your Twilio number:
configure Twilio phone number

Prepare Training Data

Make a new file called data.json to contain two arrays of phrases corresponding to labels: either "loves me" or "loves me not". Feel free to modify phrases to the arrays or add your own (the more training data, the better--this is not close to being enough but it's a fun start.)

{
    "loves me": [
        "do you want some food",
        "you're so nice",
        "i got you some food",
        "I like your hair",
        "You looked nice today",
        "Let's dance",
        "I spent time on this for you",
        "i got this for you",
        "heyyyyyyy",
        "i got you pizza"
    ],
    "loves me not": [
        "I didn't have the time",
        "Can you get your own food",
        "You'll have to get your own food",
        "Do it yourself",
        "i can't",
        "next time",
        "i'm sorry",
        "you up",
        "hey",
        "wyd",
        "k", 
        "idk man",
        "cool"
    ]
}
Enter fullscreen mode Exit fullscreen mode

Make a Python file called main.py. At the top import the required libraries, then make a function open_file to save the data from data.json as a variable data.

import re
from nltk.tokenize import word_tokenize
from nltk.stem import WordNetLemmatizer
import numpy as np
import tflearn
import tensorflow as tf
import random
import json
from twilio.twiml.messaging_response import MessagingResponse
from flask import Flask, request

def open_file(file):
    with open(file, 'r') as f:
        data = json.load(f)
        return data
data = open_file('data.json')
print(data)
Enter fullscreen mode Exit fullscreen mode

Read Training Data

This post will use a lemmatizer to get to the base of a word, ie. turning "going" into "go". A stemmer, which also reduces words to their word stem, could be used for this task but would be unable to identify that "good" is the lemma of "better." Though lemmas take more time to use, they tend to be more efficient. You can experiment with both stemmers and lemmatizers when working with natural language processing (NLP).

Right underneath the data variable declaration, initialize the lemmatizer and make this function to stem each word:

lemma = WordNetLemmatizer()
def tokenize_and_stem_text(text):
    return [lemma.lemmatize(word.lower()) for word in text] 
binary_categories = list(data.keys())
training_words = []
json_data = []
Enter fullscreen mode Exit fullscreen mode

This next function will read the training data, remove punctuation, handle contractions, and extract words in each sentence, appending them to a word list.

Next, get the possible labels ("loves me" and "loves me not") that the model will train for and initialize an empty list json_data to hold tuples of words from the sentence and also the label name. The training_words list will contain all the unique stemmed words from the training data JSON and binary_categories contains the possible categories they could classify as.

def read_training_data(data):
    for label in data.keys(): 
        for text in data[label]:
            for word in text.split():
                if word.lower() in contractions:
                    text = text.replace(word, contractions[word.lower()])
            text = re.sub("[^a-zA-Z' ]+", ' ', text)
            training_words.extend(word_tokenize(text))
            json_data.append((word_tokenize(text), label))
    return json_data
Enter fullscreen mode Exit fullscreen mode

The json_data returned is a list of words from each sentence and either loves_me or loves_me_not; for example, one element of that list is ([“do”, “you”, “want”, "some", "food], “loves_me”). This list does not cover every possible contraction but you get the idea:

contractions = {
    "aren't": "are not",
    "can't": "cannot",
    "could've": "could have",
    "couldn't": "could not",
    "didn't": "did not",
    "don't": "do not",
    "hadn't": "had not",
    "hasn't": "has not",
    "haven't": "have not",
    "how'd": "how did",
    "how's": "how is",
    "i'd": "I had",
    "i'll": "I will",
    "i'm": "I am",
    "i've": "I have",
    "isn't": "is not",
    "let's": "let us",
    "should've": "should have",
    "shouldn't": "should not",
    "that'd": "that had",
    "that's": "that is",
    "there's": "there is",
    "wasn't": "was not",
    "we'd": "we would",
    "we'll": "we will",
    "we're": "we are",
    "we've": "we have",
    "what'll": "what  will",
    "what's": "what is",
    "when's": "when is",
    "where'd": "where did",
    "where's": "where is",
    "won't": "will not",
    "would've": "would have",
    "wouldn't": "would not",
    "you'd": "you had",
    "you'll": "you will",
    "you're": "you are",
}
Enter fullscreen mode Exit fullscreen mode

Then stem each word to remove duplicates and call the read_training_data function.

training_words = tokenize_and_stem_text(training_words)
print(read_training_data(data))
read_training_data(data)
Enter fullscreen mode Exit fullscreen mode

For TensorFlow to understand this data the strings must be converted into numbers. This can be done with the bag-of-words NLP model, which keeps a count of the total number of occurrences of the most commonly-used words. For example, the sentence "Never gonna give you up never gonna let you down" could be represented as:

For the loves_me and loves_me_not labels a bag-of-words is initiated as a list of tokenized words, called vector here. We loop through the words in the phrase, stemming them and comparing with each word in the vocabulary. If the sentence has a word in our training data or vocabulary, 1 is appended to the vector, signaling which label the word belongs to. If not, a 0 is appended.

At the end our training set has a bag-of-words model and the output row corresponding to the label the bag belongs to.

training = []
for item in json_data:
    bag_vector = []
    token_words = item[0]
    token_words = [lemma.lemmatize(word.lower()) for word in token_words]
    for word in training_words:
        if word in token_words:
            bag_vector.append(1) 
        else:
            bag_vector.append(0)
    output_row = list([0] * len(binary_categories)) 
    output_row[binary_categories.index(item[1])] = 1
    training.append([bag_vector, output_row])
Enter fullscreen mode Exit fullscreen mode

Convert training to a numpy array so TensorFlow can process it as well, and split it into two variables: data has the bag of words and labels has the label.

training = np.array(training)
data = list(training[:, 0])
labels = list(training[:, 1])
Enter fullscreen mode Exit fullscreen mode

Now reset the underlying graph data, and clear defined variables and operations from the previous cell each time the model is run. Next build a neural network with three layers:

  1. The input_data input layer is for inputting or feeding data to a network, and the input to the network has size len(data[0]) for the length of our encoded bag of words and labels.
  2. Then make two fully-connected intermediate layers with 32 hidden units or neurons. While some functions need more than one layer to run, more than three layers probably won't make a difference, so two layers is enough and shouldn't be too computationally-expensive. We use the softmax activation function in this case because the labels are exclusive.
  3. Lastly, we make the final net from the estimator layer, like regression. At a high level, regression (linear or logistic) helps predict the outcome of an event based on the data. Neural networks have multiple layers to better learn more complicated abstractions relationships from the input.
tf.reset_default_graph()
net = tflearn.input_data(shape=[None, len(data[0])]) 
net = tflearn.fully_connected(net, 32)
net = tflearn.fully_connected(net, len(labels[0]), activation='softmax') 
net = tflearn.regression(net)
Enter fullscreen mode Exit fullscreen mode

A deep neural network (DNN) automatically performs neural network classifier tasks like training the model and prediction based on input. Calling the fit method begins training and applies the gradient descent algorithm, a common first-order optimization deep learning algorithm. n_epoch is the number of times the network will see all the data and batch_size is the size data is sliced in to for the model to train on.

model = tflearn.DNN(net)
model.fit(data, labels, n_epoch=100, batch_size=16, show_metric=True)
Enter fullscreen mode Exit fullscreen mode

Similar to how the data for the bag-of-words model was processed, this data needs to be converted to a numerical form that can be passed to TensorFlow.

def clean_for_tf(text):
    input_words = tokenize_and_stem_text(word_tokenize(text))
    vector = [0]*len(training_words)
    for input_word in input_words:
        for ind, word in enumerate(training_words):
            if word == input_word:
                vector[ind] = 1
    return(np.array(vector))
Enter fullscreen mode Exit fullscreen mode

To test this without text messages you could add

tensor = model.predict([clean_for_tf(INSERT-TEXT-HERE)])
print(binary_categories[np.argmax(tensor)])
Enter fullscreen mode Exit fullscreen mode

This calls the predict method on the model, getting the position of the largest value which represents the prediction.

We will test this with text messages by building a Flask application.

Create a Flask App

Add the following code to make a Flask app, get the inbound text message, create a tensor, and call the model.

app = Flask(__name__)
@app.route("/sms", methods=['POST'])
def sms():
    resp = MessagingResponse()
    inbMsg = request.values.get('Body').lower().strip()
    tensor = model.predict([clean_for_tf(inbMsg)])
    resp.message(
        f'The message {inbMsg!r} corresponds to {binary_categories[np.argmax(tensor)]!r}.')
    return str(resp)
Enter fullscreen mode Exit fullscreen mode

Open a new terminal tab separate from the one running ngrok. In the folder housing your code run and text your Twilio number a phrase like "get someone else to do it" and you should see something like this:
classify
The complete code and requirements.txt can be found on GitHub here.

What's Next

Ariel doing loves me-loves me not with flowers underwater gif
What will you classify next? You could use TensorFlow's Universal Sentence Encoder to perform similar text classification in JavaScript, classify phone calls or emails, use a different activation function like sigmoid if you have categories that are mutually exclusive, and more. Let me know what you're building online or in the comments.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .