Creating a Text Summarizer Using Natural Language Processing

There are several methods of generating summaries for text. In this tutorial, I will be covering an extractive method in which the top 2 most important sentences in an article serve as the summary. This method uses GloVe, cosine similarity and TextRank.

GloVe is an algorithm which creates vector (or numerical) representations for words. Cosine similarity is used to generate a similarity matrix n x n (n = number of sentences in the article) and the TextRank Algorithm is used to score the sentence similarities. The top scoring (most similar) sentences are used to generate the summary.

Before feeding the data into the algorithm, it should be cleaned, preprocessed and converted into a numerical format that it can understand (vectorized).

Using Jupyter Notebook:

Step 1: Create a new environment or use an existing environment to create a new Jupyter Notebook. Make sure the environment that you are running your Jupyter Notebook in has the following dependencies installed:

dill=0.2.9
networkx=2.4
nltk=3.4.5
numpy=1.18.1
numpy-base=1.18.1
pandas=1.0.3
pickleshare=0.7.5
pymongo=3.8.0
python=3.7.7
regex=2018.07.11
scikit-learn=0.22.1

Step 2: Download some needed files. This step only has to be done once.

Download and unzip the GloVe word vectors trained on Wikipedia and Gigaword 5 Data . This code applies to Jupyter Notebook.

!wget http://nlp.stanford.edu/data/glove.6B.zip
!unzip glove*.zip

Download nltk punkt

nltk.download('punkt')

Step 3: Connect to the database collection. Here I am connecting to a MongoDB collection. Replace <> with the relevant information.

import re

from pymongo import MongoClient

#create a MongoClient to the running mongoDB instance (make sure that MongoDB instance is currently running)

#specify the host and port to connect on
MONGO_ADDRESS = "mongodb://<IP Address>"
from bson import json_util
from collections import Counter

def connection():
    # Connect to the database
    client = MongoClient(MONGO_ADDRESS)
    db = client.<DBName>
    # Handle to the connection
    collection = db.<CollectionName>

    data = list(collection.find({},{'<col1>','<col2>','<col3>','<col4>','<col5>','<col6>'}))
        
    return data

data = connection()

Step 4: Convert the collection into a dataframe. Pickle the file to be stored on your local machine. You only have to run this code once, or as often as the database updates.

import pandas as pd

df = pd.DataFrame.from_records(data)

#Converts dataframe to a file
df.to_pickle('filename.pkl')

Step 5: Read the pickled file back into a pandas dataframe. Each time you run Jupyter Notebook, you only have to start from the code in this step (unless the MongoDB collection is updated).

import pandas as pd

df = pd.read_pickle('filename.pkl')

Step 6: Now let’s do some data cleaning on a particular column.

Remove whitespace:

df['<column name>'] = df['<column name>'].str.replace(r'\s+', ' ')

Remove chars \n and \t:

df['<column name>'] = df['<column name>'].str.replace(r'\\n', ' ')
df['<column name>'] = df['<column name>'].str.replace(r'\\t', ' ')

Next, let’s identify the number of non-empty rows for the target column:

len(list(filter(lambda x: not x, df['<column name>'] == '')))

Finally, the rows which are empty for the target column will be filtered out:

df = df[df['<column name>'] != '']

Let’s confirm that we now have the same number of non-empty rows as shown above:

len(df)

Remove non-English characters from text:

df['<column name>'] = df['<column name>'].str.replace(r'[^\x00-\x7F]+', '')

Remove some comment code, anything between and including <!– and –>:

df['<column name>'] = df['<column name>'].str.replace("<!--.*-->","")

Return only the rows which do not have NaN in <column name> column:

df = df[df['<column name>'].notna()]

Step 7: Create the text summarizer function.

import nltk

nltk.download('punkt') # one time execution

# Use the sent_tokenize() function of the nltk library to break the text into individual sentences
from nltk.tokenize import sent_tokenize

nltk.download('stopwords')

from nltk.corpus import stopwords

stop_words = stopwords.words('english')

from sklearn.metrics.pairwise import cosine_similarity

import numpy as np

from nltk.cluster.util import cosine_distance

import pandas as pd

import networkx as nx

# Define the summarizer function
def summarize_func(description):
    sentences = []
    sentences.append(sent_tokenize(description))

    # Flatten list
    sentences = [y for x in sentences for y in x] 

    # Extract word vectors
    word_embeddings = {}
    f = open('glove.6B.100d.txt', encoding='utf-8')
    for line in f:
        values = line.split()
        word = values[0]
        coefs = np.asarray(values[1:], dtype='float32')
        word_embeddings[word] = coefs
    f.close()

    # Remove punctuations, numbers and special characters
    clean_sentences = pd.Series(sentences).str.replace("[^a-zA-Z]", " ")

    # Make the text lowercase
    clean_sentences = [s.lower() for s in clean_sentences]

    # Define function to remove stopwords
    def remove_stopwords(sen):
        sen_new = " ".join([i for i in sen if i not in stop_words])
        return sen_new

    # Remove stopwords from the sentences
    clean_sentences = [remove_stopwords(r.split()) for r in clean_sentences]

    # Vectorize each word in a sentence and then take the mean of those vectors to get a consolidated vector for the sentence
    sentence_vectors = []
    for i in clean_sentences:
        if len(i) != 0:
            v = sum([word_embeddings.get(w, np.zeros((100,))) for w in i.split()])/(len(i.split())+0.001)
        else:
            v = np.zeros((100,))
        sentence_vectors.append(v)

    # Create empty similarity matrix of dimensions n * n where n is the number of sentences
    sim_mat = np.zeros([len(sentences), len(sentences)])

    # Fill matrix with cosine similarity scores
    for i in range(len(sentences)):
        for j in range(len(sentences)):
            if i != j:
                sim_mat[i][j] = cosine_similarity(sentence_vectors[i].reshape(1,100), sentence_vectors[j].reshape(1,100))[0,0]

    # Convert the similarity matrix into a graph
    nx_graph = nx.from_numpy_array(sim_mat)

    # Apply the TextRank algorithm on this graph to get the sentence rankings
    scores = nx.pagerank(nx_graph)

    ranked_sentences = sorted(((scores[i],s) for i,s in enumerate(sentences)), reverse=True)

    # Extract the top 2 sentences as the summary
    # If the article <column name> has only 1 sentence, return that sentence
    if len(sent_tokenize(description)) == 1:
        return sentences
    else:
        return ranked_sentences[0][1] + ' ' + ranked_sentences[1][1]

Step 8: Create a new “Summary” column which contains the results of applying the summary function to the first 10 rows of a particular column within the dataframe.

first_ten_df = df.head(10)

first_ten_df["Summary"] = first_ten_df.apply(lambda row: summarize_func(row['<column name>']), axis=1)

first_ten_df

Step 9: View the dataframe with the new column “Summary”.

pd.set_option('display.max_rows', None)
pd.set_option('display.max_columns', None)
pd.set_option('display.width', None)
pd.set_option('display.max_colwidth', None)

first_ten_df[['Summary']]

Step 10: Finally, write the dataframe to a csv file.

first_ten_df.to_csv(r'summarized_text.csv', index = False)

Using PyCharm:

Step 1: Create an environment (using conda, etc.). Install the following dependencies in the environment:

dill=0.2.9
networkx=2.4
nltk=3.4.5
numpy=1.18.1
numpy-base=1.18.1
pandas=1.0.3
pickleshare=0.7.5
pymongo=3.8.0
python=3.7.7
regex=2018.07.11
scikit-learn=0.22.1

Step 2: In PyCharm, go to File > Settings and add the interpreter (python.exe) within that environment. This will be necessary for running the code later.

Step 3: Create a module called clean.py which stores the function for cleaning each row of <column name> in the df dataframe. Any word such as ‘data’ can be passed in, as the variable the function is applied on will replace ‘data’.

def clean(data):
    """
    Cleans data
    """

    # Import statements
    import re
    import string

    # Remove whitespace
    data['<column name>'] = data['<column name>'].str.replace(r'\s+', ' ')

    # Remove chars \n and \t
    data['<column name>'] = data['<column name>'].str.replace(r'\\n', ' ')
    data['<column name>'] = data['<column name>'].str.replace(r'\\t', ' ')

    # Remove rows with Empty Descriptions
    data = data[data['<column name>'] != '']

    # Remove non-English characters from description
    data['<column name>'] = data['<column name>'].str.replace(r'[^\x00-\x7F]+', '')

    # Remove comment code
    data['<column name>'] = data['<column name>'].str.replace("<!--.*-->", "")

    # Return only the rows which do not have NaN in <column name> column:
    data = data[data['<column name>'].notna()]

    return data

Step 4: Create a module called summarizer.py which stores the setup and summarization functions which will later be called in main.py. Make sure it is stored in the same directory as clean.py.

The functions starting with ‘_’ are used by the setup and summarization functions, they will not be called in main.py.

# Import statements
import nltk
from nltk.tokenize import sent_tokenize
from nltk.corpus import stopwords
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
from nltk.cluster.util import cosine_distance
import pandas as pd
import networkx as nx
import urllib.request
import zipfile
from os import path


def _download_file(url, save_path):
    with urllib.request.urlopen(url) as dl_file:
        with open(save_path, 'wb') as out_file:
            out_file.write(dl_file.read())


def setup():
    """
    Get GloVe word vectorizer if doesn't exist and downloads nltk dependencies
    """
    if not path.exists('glove.6B.100d.txt'):
        _download_file('http://nlp.stanford.edu/data/glove.6B.zip', 'glove6B.zip')
        with zipfile.ZipFile('glove6B.zip', 'r') as zip_ref:
            zip_ref.extractall('.')

    # Run only once
    nltk.download('punkt')
    nltk.download('stopwords')


def _remove_stopwords(sen):
    """
    Function to remove stopwords
    """
    stop_words = stopwords.words('english')
    sen_new = " ".join([i for i in sen if i not in stop_words])
    return sen_new


def summarize(article_text):
    """
    Summarizer function
    :param article_text: Full article text for summarization
    :return:
    """
    sentences = []
    sentences.append(sent_tokenize(article_text))

    # Flatten list
    sentences = [y for x in sentences for y in x]

    # Extract word vectors
    word_embeddings = {}
    f = open('glove.6B.100d.txt', encoding='utf-8')
    for line in f:
        values = line.split()
        word = values[0]
        coefs = np.asarray(values[1:], dtype='float32')
        word_embeddings[word] = coefs
    f.close()

    # Remove punctuations, numbers and special characters
    clean_sentences = pd.Series(sentences).str.replace("[^a-zA-Z]", " ")

    # Make text lowercase
    clean_sentences = [s.lower() for s in clean_sentences]

    # Remove stopwords from the sentences
    clean_sentences = [_remove_stopwords(r.split()) for r in clean_sentences]

    # Create consolidated vector for each sentence
    sentence_vectors = []
    for i in clean_sentences:
        if len(i) != 0:
            v = sum([word_embeddings.get(w, np.zeros((100,))) for w in i.split()]) / (len(i.split()) + 0.001)
        else:
            v = np.zeros((100,))
        sentence_vectors.append(v)

    # Create empty similarity matrix of dimensions n * n where n is the number of sentences
    sim_mat = np.zeros([len(sentences), len(sentences)])

    # Fill matrix with cosine similarity scores
    for i in range(len(sentences)):
        for j in range(len(sentences)):
            if i != j:
                sim_mat[i][j] = \
                    cosine_similarity(sentence_vectors[i].reshape(1, 100), sentence_vectors[j].reshape(1, 100))[0, 0]

    # Convert the similarity matrix into a graph
    nx_graph = nx.from_numpy_array(sim_mat)

    # Apply the PageRank algorithm on this graph to get the sentence rankings
    scores = nx.pagerank(nx_graph)

    ranked_sentences = sorted(((scores[i], s) for i, s in enumerate(sentences)), reverse=True)

    # Extract the top 2 sentences as the summary
    # If the article <column name> has only 1 sentence, return that sentence
    if len(sent_tokenize(article_text)) == 1:
        return sentences
    else:
        return ranked_sentences[0][1] + ' ' + ranked_sentences[1][1]

Step 5: Create a module main.py. Make sure it is stored in the same directory as clean.py and summarizer.py. It first reads in the database collection and stores the first 10 rows as a dataframe df. Then it calls the cleaning function from clean.py. Next it calls the setup function from summarizer.py. Finally, it calls the summarize function from summarizer.py and applies it to each row of <column name> in the dataframe df.

# Import statements
# Note: If you just use 'import summarizer', you would have to call it
# using 'summarizer.summarize' instead of just 'summarize'
from clean import clean
from summarizer import summarize
import pandas as pd
import re
from pymongo import MongoClient

if __name__ == "__main__":

     # Read-in data from MongoDB collection

     #specify the host and port to connect on
     MONGO_ADDRESS = "mongodb://<IP Address>"

     def connection():
          # Connect to the database
          client = MongoClient(MONGO_ADDRESS)
          db = client.<DBName>
          # Handle to the connection
          collection = db.<CollectionName>

          data = list(collection.find({},{'<col1>','<col2>','<col3>','<col4>','<col5>','<col6>'}))
        
          return data

     data = connection()

     # Convert 'data' to pandas dataframe called 'df'
     df = pd.DataFrame.from_records(data)

     # Set 'df' variable as first 10 rows of 'df'
     df = df.head(10)

     # Calls clean function in clean.py to clean 'df' dataframe
     df = clean(data)

     # Calls setup() function in summarizer.py
     summarizer.setup()

     # Applies summarize() function in summarizer.py on each row of df under <column name>
     df['Summary'] = df.apply(lambda row: summarize(row['<column name>']), axis=1)

Step 6: You can check if the code in your summarizer.py module works by running it on some sample text. First set the script path to summarizer.py in Run > Edit Configurations. Then add the test code to the bottom of summarizer.py. Finally click Run > Run ‘summarizer’.

Alternatively, you can add the test code to the bottom of summarizer.py and then click the green play button to the left of if __name__ == “__main__” in summarizer.py.

# For testing
if __name__ == "__main__":
    print("Setting up")
    setup()

    print("Setup complete")

    article = """Insert article here."""

    print("Summarizing")
    print("Summary: " + summarize(article))

Step 7: Finally, to summarize each row of the target column in the MongoDB collection and then add them to a new column called ‘Summary’:, first set the script path to main.py in Run > Edit Configurations. Then, click Run > Run ‘main’.

You can instead click on the green play button to the left of if __name__ == “__main__” in main.py.

Leave a Reply

Your email address will not be published. Required fields are marked *