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.
Prerequisites
- A Twilio account - sign up for a free one here
- A Twilio phone number with SMS capabilities - configure one here
- Set up your Python and Flask developer environment - Make sure you have Python 3 downloaded as well as ngrok.
Setup
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:
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.
You should see the screen above. Grab that ngrok URL to configure your Twilio 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"
]
}
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)
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 = []
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
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",
}
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)
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])
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])
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:
- The
input_data
input layer is for inputting or feeding data to a network, and the input to the network has sizelen(data[0])
for the length of our encoded bag of words and labels. - 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. - 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)
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)
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))
To test this without text messages you could add
tensor = model.predict([clean_for_tf(INSERT-TEXT-HERE)])
print(binary_categories[np.argmax(tensor)])
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)
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:
The complete code and requirements.txt
can be found on GitHub here.
What's Next
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.
- GitHub: elizabethsiegle
- Twitter: @lizziepika
- email: lsiegle@twilio.com