# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# # Creating a Sentiment Analysis Web App
#
# I built a simple web page that a user can use to enter a movie review. The web page will then send the review off to the deployed recurrent neural network, which will predict the sentiment of the entered review.
#
# ## Project Steps
#
# 1. Download the IMDb dataset.
# 2. Preparing and Processing the data.
# 3. Upload the processed data to S3.
# 4. Train a recurrent neural network model.
# 5. Deploy the model. 
# 6. Test the trained model.
# 7. Deploy the model for the web app.

# ## Step 1: Download the IMDb dataset
#
# I will be using the [IMDb dataset](http://ai.stanford.edu/~amaas/data/sentiment/)
#
# > Maas, <NAME>., et al. [Learning Word Vectors for Sentiment Analysis](http://ai.stanford.edu/~amaas/data/sentiment/). In _Proceedings of the 49th Annual Meeting of the Association for Computational Linguistics: Human Language Technologies_. Association for Computational Linguistics, 2011.

# %mkdir ../data
# !wget -O ../data/aclImdb_v1.tar.gz http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
# !tar -zxf ../data/aclImdb_v1.tar.gz -C ../data

# ## Step 2: Preparing and Processing the data
#
# I will read in each of the reviews and combine them into a single input structure. Then, I will split the dataset into a training set and a testing set.

# +
import os
import glob

def read_imdb_data(data_dir='../data/aclImdb'):
    data = {}
    labels = {}
    
    for data_type in ['train', 'test']:
        data[data_type] = {}
        labels[data_type] = {}
        
        for sentiment in ['pos', 'neg']:
            data[data_type][sentiment] = []
            labels[data_type][sentiment] = []
            
            path = os.path.join(data_dir, data_type, sentiment, '*.txt')
            files = glob.glob(path)
            
            for f in files:
                with open(f) as review:
                    data[data_type][sentiment].append(review.read())
                    # Here we represent a positive review by '1' and a negative review by '0'
                    labels[data_type][sentiment].append(1 if sentiment == 'pos' else 0)
                    
            assert len(data[data_type][sentiment]) == len(labels[data_type][sentiment]), \
                    "{}/{} data size does not match labels size".format(data_type, sentiment)
                
    return data, labels


# -

data, labels = read_imdb_data()
print("IMDB reviews: train = {} pos / {} neg, test = {} pos / {} neg".format(
            len(data['train']['pos']), len(data['train']['neg']),
            len(data['test']['pos']), len(data['test']['neg'])))

# Now that I've read the raw training and testing data from the downloaded dataset, I will combine the positive and negative reviews and shuffle the resulting records.

# +
from sklearn.utils import shuffle

def prepare_imdb_data(data, labels):
    """Prepare training and test sets from IMDb movie reviews."""
    
    #Combine positive and negative reviews and labels
    data_train = data['train']['pos'] + data['train']['neg']
    data_test = data['test']['pos'] + data['test']['neg']
    labels_train = labels['train']['pos'] + labels['train']['neg']
    labels_test = labels['test']['pos'] + labels['test']['neg']
    
    #Shuffle reviews and corresponding labels within training and test sets
    data_train, labels_train = shuffle(data_train, labels_train)
    data_test, labels_test = shuffle(data_test, labels_test)
    
    # Return a unified training data, test data, training labels, test labets
    return data_train, data_test, labels_train, labels_test


# -

train_X, test_X, train_y, test_y = prepare_imdb_data(data, labels)
print("IMDb reviews (combined): train = {}, test = {}".format(len(train_X), len(test_X)))

# Now that I have our training and testing sets unified and prepared, I should do a quick check and see an example of the data our model will be trained on. 

print(train_X[100])
print(train_y[100])

# The first step in processing the reviews is to make sure that any html tags that appear should be removed. In addition I will tokenize the input, that way words such as *entertained* and *entertaining* are considered the same with regard to sentiment analysis.

# +
import nltk
from nltk.corpus import stopwords
from nltk.stem.porter import *

import re
from bs4 import BeautifulSoup

def review_to_words(review):
    nltk.download("stopwords", quiet=True)
    stemmer = PorterStemmer()
    
    text = BeautifulSoup(review, "html.parser").get_text() # Remove HTML tags
    text = re.sub(r"[^a-zA-Z0-9]", " ", text.lower()) # Convert to lower case
    words = text.split() # Split string into words
    words = [w for w in words if w not in stopwords.words("english")] # Remove stopwords
    words = [PorterStemmer().stem(w) for w in words] # stem
    
    return words


# -

# The `review_to_words` method defined above uses `BeautifulSoup` to remove any html tags that appear and uses the `nltk` package to tokenize the reviews. 

# Apply review_to_words to a review (train_X[100] or any other review)
review_to_words(train_X[100])

# The method below applies the `review_to_words` method to each of the reviews in the training and testing datasets. In addition it caches the results. 

# +
import pickle

cache_dir = os.path.join("../cache", "sentiment_analysis")  # where to store cache files
os.makedirs(cache_dir, exist_ok=True)  # ensure cache directory exists

def preprocess_data(data_train, data_test, labels_train, labels_test,
                    cache_dir=cache_dir, cache_file="preprocessed_data.pkl"):
    """Convert each review to words; read from cache if available."""

    # If cache_file is not None, try to read from it first
    cache_data = None
    if cache_file is not None:
        try:
            with open(os.path.join(cache_dir, cache_file), "rb") as f:
                cache_data = pickle.load(f)
            print("Read preprocessed data from cache file:", cache_file)
        except:
            pass  # unable to read from cache, but that's okay
    
    # If cache is missing, then do the heavy lifting
    if cache_data is None:
        # Preprocess training and test data to obtain words for each review
        words_train = [review_to_words(review) for review in data_train]
        words_test = [review_to_words(review) for review in data_test]
        
        # Write to cache file for future runs
        if cache_file is not None:
            cache_data = dict(words_train=words_train, words_test=words_test,
                              labels_train=labels_train, labels_test=labels_test)
            with open(os.path.join(cache_dir, cache_file), "wb") as f:
                pickle.dump(cache_data, f)
            print("Wrote preprocessed data to cache file:", cache_file)
    else:
        # Unpack data loaded from cache file
        words_train, words_test, labels_train, labels_test = (cache_data['words_train'],
                cache_data['words_test'], cache_data['labels_train'], cache_data['labels_test'])
    
    return words_train, words_test, labels_train, labels_test


# -

# Preprocess data
train_X, test_X, train_y, test_y = preprocess_data(train_X, test_X, train_y, test_y)

# ## Transform the data
#
# I will construct a feature representation. To start, I will represent each word as an integer. Some of the words that appear in the reviews occur very infrequently and so likely don't contain much information for the purposes of sentiment analysis. The way I will deal with this problem is that I will fix the size of the working vocabulary and I will only include the words that appear most frequently. I will then combine all of the infrequent words into a single category and, in this case, I will label it as `1`.
#
# Since I will be using a recurrent neural network, it will be convenient if the length of each review is the same. To do this, I will fix a size for the reviews and then pad short reviews with the category 'no word' (which we will label `0`) and truncate long reviews.

# ### Create a word dictionary
#
# To begin with, I need to construct a way to map words that appear in the reviews to integers. Here I fix the size of our vocabulary (including the 'no word' and 'infrequent' categories) to be `5000`.
#
# > Even though the vocab_size is set to `5000`, I only want to construct a mapping for the most frequently appearing `4998` words. This is because I want to reserve the special labels `0` for 'no word' and `1` for 'infrequent word'.

# +
import numpy as np

def build_dict(data, vocab_size = 5000):
    """Construct and return a dictionary mapping each of the most frequently appearing words to a unique integer."""
    
    #       Determine how often each word appears in `data`. `data` is a list of sentences and that a
    #       sentence is a list of words.
    word_count = {} # A dict storing the words that appear in the reviews along with how often they occur
    for sentence in data:
        for word in sentence:
            word_count[word] = word_count.get(word, 0) + 1
    
    
    #  Sort the words found in `data` so that sorted_words[0] is the most frequently appearing word and
    #       sorted_words[-1] is the least frequently appearing word.
    
    sorted_words = sorted(word_count.keys(), key = lambda x: word_count[x], reverse=True)
    
    word_dict = {} # This is what we are building, a dictionary that translates words into integers
    for idx, word in enumerate(sorted_words[:vocab_size - 2]): # The -2 is so that we save room for the 'no word'
        word_dict[word] = idx + 2                              # 'infrequent' labels
        
    return word_dict
# -

word_dict = build_dict(train_X)

# Determine the five most frequently appearing words in the training set.
sorted(word_dict.keys(), key = lambda x: word_dict[x], reverse=False)[:5]

# ### Save `word_dict`
#
# Later on when I construct an endpoint which processes a submitted review I will need to make use of the `word_dict` which I have created. As such, I will save it to a file now for future use.

data_dir = '../data/pytorch' # The folder we will use for storing data
if not os.path.exists(data_dir): # Make sure that the folder exists
    os.makedirs(data_dir)

with open(os.path.join(data_dir, 'word_dict.pkl'), "wb") as f:
    pickle.dump(word_dict, f)


# ### Transform the reviews
#
# Now that I have our word dictionary which allows me to transform the words appearing in the reviews into integers, it is time to make use of it and convert our reviews to their integer sequence representation, making sure to pad or truncate to a fixed length, which in this case is `500`.

# +
def convert_and_pad(word_dict, sentence, pad=500):
    NOWORD = 0 # We will use 0 to represent the 'no word' category
    INFREQ = 1 # and we use 1 to represent the infrequent words, i.e., words not appearing in word_dict
    
    working_sentence = [NOWORD] * pad
    
    for word_index, word in enumerate(sentence[:pad]):
        if word in word_dict:
            working_sentence[word_index] = word_dict[word]
        else:
            working_sentence[word_index] = INFREQ
            
    return working_sentence, min(len(sentence), pad)

def convert_and_pad_data(word_dict, data, pad=500):
    result = []
    lengths = []
    
    for sentence in data:
        converted, leng = convert_and_pad(word_dict, sentence, pad)
        result.append(converted)
        lengths.append(leng)
        
    return np.array(result), np.array(lengths)


# -

train_X, train_X_len = convert_and_pad_data(word_dict, train_X)
test_X, test_X_len = convert_and_pad_data(word_dict, test_X)

# Use this cell to examine one of the processed reviews to make sure everything is working as intended.
train_X[0]

# ## Step 3: Upload the processed data to S3
#
# I will upload the training dataset to S3 in order for the training code to access it. For now I will save it locally and I will upload to S3 later on.
#
# ### Save the processed training dataset locally
#
# It is important to note the format of the data that I am saving as I will need to know it when I write the training code. In this case, each row of the dataset has the form `label`, `length`, `review[500]` where `review[500]` is a sequence of `500` integers representing the words in the review.

# +
import pandas as pd
    
pd.concat([pd.DataFrame(train_y), pd.DataFrame(train_X_len), pd.DataFrame(train_X)], axis=1) \
        .to_csv(os.path.join(data_dir, 'train.csv'), header=False, index=False)
# -

# ### Uploading the training data
#
#
# Next, I need to upload the training data to the SageMaker default S3 bucket so that I can provide access to it while training the model.

# +
import sagemaker

sagemaker_session = sagemaker.Session()

bucket = sagemaker_session.default_bucket()
prefix = 'sagemaker/sentiment_rnn'

role = sagemaker.get_execution_role()
# -

input_data = sagemaker_session.upload_data(path=data_dir, bucket=bucket, key_prefix=prefix)

# **NOTE:** The cell above uploads the entire contents of the data directory. This includes the `word_dict.pkl` file. This is fortunate as I will need this later on when I create an endpoint that accepts an arbitrary review. For now, I will just take note of the fact that it resides in the data directory (and so also in the S3 training bucket) and that I will need to make sure it gets saved in the model directory.

# ## Step 4: Build and train a recurrent neural network with PyTorch
#
# A model in the SageMaker framework comprises three objects: 
#
#  - Model Artifacts,
#  - Training Code, and
#  - Inference Code,  
#  
# each of which interact with one another. I will be using containers provided by Amazon with the added benefit of being able to include my own custom code.
#
# I will start by implementing my own neural network in PyTorch along with a training script. 

# !pygmentize train/model.py

# There are three parameters to tweak in order to improve the performance of the model. These are the embedding dimension, the hidden dimension and the size of the vocabulary. I want to make these parameters configurable in the training script so that if I want to modify them I do not need to modify the script itself. To start I will write some of the training code in the notebook so that I can more easily diagnose any issues that arise.
#
# First I will load a small portion of the training data set to use as a sample. It would be very time consuming to try and train the model completely in the notebook as I do not have access to a gpu and the compute instance that I am using is not particularly powerful. However, I can work on a small bit of the data to get a feel for how our training script is behaving.

# +
import torch
import torch.utils.data

# Read in only the first 250 rows
train_sample = pd.read_csv(os.path.join(data_dir, 'train.csv'), header=None, names=None, nrows=250)

# Turn the input pandas dataframe into tensors
train_sample_y = torch.from_numpy(train_sample[[0]].values).float().squeeze()
train_sample_X = torch.from_numpy(train_sample.drop([0], axis=1).values).long()

# Build the dataset
train_sample_ds = torch.utils.data.TensorDataset(train_sample_X, train_sample_y)
# Build the dataloader
train_sample_dl = torch.utils.data.DataLoader(train_sample_ds, batch_size=50)


# -

# ### Writing the training method

def train(model, train_loader, epochs, optimizer, loss_fn, device):
    for epoch in range(1, epochs + 1):
        model.train()
        total_loss = 0
        for batch in train_loader:         
            batch_X, batch_y = batch
            
            batch_X = batch_X.to(device)
            batch_y = batch_y.to(device)
            
            optimizer.zero_grad()

            batch_output = model.forward(batch_X)
            loss = loss_fn(batch_output, batch_y)
            loss.backward()
            optimizer.step()

            total_loss += loss.item()
        print("Epoch: {}, BCELoss: {}".format(epoch, total_loss / len(train_loader)))


# I will test the training method by writing a bit of code in the notebook that executes the training method on the small sample training set that I loaded earlier. The reason for doing this in the notebook is so that I have an opportunity to fix any errors that arise early when they are easier to diagnose.

# +
import torch.optim as optim
from train.model import LSTMClassifier

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = LSTMClassifier(32, 100, 5000).to(device)
optimizer = optim.Adam(model.parameters())
loss_fn = torch.nn.BCELoss()

train(model, train_sample_dl, 5, optimizer, loss_fn, device)
# -

# In order to construct a PyTorch model using SageMaker I must provide SageMaker with a training script. I may optionally include a directory which will be copied to the container and from which the training code will be run. When the training container is executed it will check the uploaded directory (if there is one) for a `requirements.txt` file and install any required Python libraries, after which the training script will be run.

# ### Training the model
#
# When a PyTorch model is constructed in SageMaker, an entry point must be specified. This is the Python file which will be executed when the model is trained. Inside of the `train` directory is a file called `train.py` which has been provided and which contains most of the necessary code to train our model. 
#
# The way that SageMaker passes hyperparameters to the training script is by way of arguments. These arguments can then be parsed and used in the training script. 

# +
from sagemaker.pytorch import PyTorch

estimator = PyTorch(entry_point="train.py",
                    source_dir="train",
                    role=role,
                    framework_version='0.4.0',
                    train_instance_count=1,
                    train_instance_type='ml.c4.xlarge',
                    hyperparameters={
                        'epochs': 10,
                        'hidden_dim': 200,
                    })
# -

estimator.fit({'training': input_data})

# ## Step 5: Deploy the model 
#
# Now that I have trained our model, I would like to test it to see how it performs. Currently the model takes input of the form `review_length, review[500]` where `review[500]` is a sequence of `500` integers which describe the words present in the review, encoded using `word_dict`. Fortunately, SageMaker provides built-in inference code for models with simple inputs such as this.
#
# There is one thing that I need to provide, however, and that is a function which loads the saved model. This function must be called `model_fn()` and takes as its only parameter a path to the directory where the model artifacts are stored. This function must also be present in the python file which we specified as the entry point. 
#
# **NOTE**: When the built-in inference code is run it must import the `model_fn()` method from the `train.py` file. This is why the training code is wrapped in a main guard ( ie, `if __name__ == '__main__':` )

# +
# Deploy the trained model

predictor = estimator.deploy(initial_instance_count=1, instance_type='ml.c4.xlarge')
# -

# ## Step 6: Test the model

test_X = pd.concat([pd.DataFrame(test_X_len), pd.DataFrame(test_X)], axis=1)


# +
# I split the data into chunks and send each chunk seperately, accumulating the results.

def predict(data, rows=512):
    split_array = np.array_split(data, int(data.shape[0] / float(rows) + 1))
    predictions = np.array([])
    for array in split_array:
        predictions = np.append(predictions, predictor.predict(array))
    
    return predictions


# -

predictions = predict(test_X.values)
predictions = [round(num) for num in predictions]

from sklearn.metrics import accuracy_score
accuracy_score(test_y, predictions)

# ### More testing
#
# I now have a trained model which has been deployed and which I can send processed reviews to and which returns the predicted sentiment. However, ultimately I would like to be able to send my model an unprocessed review. That is, I would like to send the review itself as a string. For example, suppose I wish to send the following review to the model.

test_review = 'The simplest pleasures in life are the best, and this film is one of them. Combining a rather basic storyline of love and adventure this movie transcends the usual weekend fair with wit and unmitigated charm.'

# The question we now need to answer is, how to send this review to our model?
#
# There are two specific things to the provided reviews.
#  - Removed any html tags and stemmed the input
#  - Encoded the review as a sequence of integers using `word_dict`

# +
# Convert test_review into a form usable by the model and save the results in test_data

words = review_to_words(test_review)
test_data = [np.array(convert_and_pad(word_dict, words, pad=500)[0])]
# -

# Now that I have processed the review, I can send the resulting array to our model to predict the sentiment of the review.

predictor.predict(test_data)

# ### Delete the endpoint

estimator.delete_endpoint()

# ## Step 7 - Deploy the model for the web app
#
# Now that the model is working, it's time to create some custom inference code so that I can send the model a review which has not been processed and have it determine the sentiment of the review.
#
# Bby default the estimator which I created, when deployed, will use the entry script and directory which I provided when creating the model. However, since I now wish to accept a string as input and the model expects a processed review, I need to write some custom inference code.
#
# I will store the code in the `serve` directory. In this directory is the `model.py` file that I used to construct our model, a `utils.py` file which contains the `review_to_words` and `convert_and_pad` pre-processing functions which I used during the initial data processing, and `predict.py`, the file which will contain the custom inference code. The `requirements.txt` is present which will tell SageMaker what Python libraries are required by the custom inference code.
#
# Four functions are required for the SageMaker inference container to use.
#  - `model_fn`: This function is the same function used in the training script and it tells SageMaker how to load the model.
#  - `input_fn`: This function receives the raw serialized input that has been sent to the model's endpoint and its job is to de-serialize and make the input available for the inference code.
#  - `output_fn`: This function takes the output of the inference code and its job is to serialize this output and return it to the caller of the model's endpoint.
#  - `predict_fn`: The heart of the inference script, this is where the actual prediction is done and is the function which you will need to complete.
#
# For the simple website that I'm constructing, the `input_fn` and `output_fn` methods are relatively straightforward. The site only accepts a string as input and returns a single value as output. 

# !pygmentize serve/predict.py

# ### Deploying the model
#
# Now that the custom inference code has been written, I will create and deploy the model. To begin with, I need to construct a new PyTorchModel object which points to the model artifacts created during training and also points to the inference code that I want to use. Then I can call the deploy method to launch the deployment container.
#
# **NOTE**: The default behaviour for a deployed PyTorch model is to assume that any input passed to the predictor is a `numpy` array. In this case I want to send a string so I need to construct a simple wrapper around the `RealTimePredictor` class to accomodate simple strings. 

# +
from sagemaker.predictor import RealTimePredictor
from sagemaker.pytorch import PyTorchModel

class StringPredictor(RealTimePredictor):
    def __init__(self, endpoint_name, sagemaker_session):
        super(StringPredictor, self).__init__(endpoint_name, sagemaker_session, content_type='text/plain')

model = PyTorchModel(model_data=estimator.model_data,
                     role = role,
                     framework_version='0.4.0',
                     entry_point='predict.py',
                     source_dir='serve',
                     predictor_cls=StringPredictor)
predictor = model.deploy(initial_instance_count=1, instance_type='ml.c4.xlarge')
# -

# ### Testing the model
#
# Now that we have deployed our model with the custom inference code, we should test to see if everything is working. Here we test our model by loading the first `250` positive and negative reviews and send them to the endpoint, then collect the results. The reason for only sending some of the data is that the amount of time it takes for our model to process the input and then perform inference is quite long and so testing the entire data set would be prohibitive.

# +
import glob

def test_reviews(data_dir='../data/aclImdb', stop=250):
    
    results = []
    ground = []
    
    # We make sure to test both positive and negative reviews    
    for sentiment in ['pos', 'neg']:
        
        path = os.path.join(data_dir, 'test', sentiment, '*.txt')
        files = glob.glob(path)
        
        files_read = 0
        
        print('Starting ', sentiment, ' files')
        
        # Iterate through the files and send them to the predictor
        for f in files:
            with open(f) as review:
                # First, we store the ground truth (was the review positive or negative)
                if sentiment == 'pos':
                    ground.append(1)
                else:
                    ground.append(0)
                # Read in the review and convert to 'utf-8' for transmission via HTTP
                review_input = review.read().encode('utf-8')
                # Send the review to the predictor and store the results
                results.append(int(float(predictor.predict(review_input))))
                
            # Sending reviews to our endpoint one at a time takes a while so we
            # only send a small number of reviews
            files_read += 1
            if files_read == stop:
                break
            
    return ground, results


# -

ground, results = test_reviews()

from sklearn.metrics import accuracy_score
accuracy_score(ground, results)

predictor.predict("it's awesome")

# Now that I know the endpoint is working as expected, I can set up the web page that will interact with it. 

# ## Step 7 (again): Use the model for the web app
#
# So far I have been accessing our model endpoint by constructing a predictor object which uses the endpoint and then just using the predictor object to perform inference. What if I wanted to create a web app which accessed the model? The way things are set up currently makes that not possible since in order to access a SageMaker endpoint the app would first have to authenticate with AWS using an IAM role which included access to SageMaker endpoints. 
#
# <img src="Web App Diagram.svg">
#
# The diagram above gives an overview of how the various services will work together. On the far right is the model which I trained above and which is deployed using SageMaker. On the far left is our web app that collects a user's movie review, sends it off and expects a positive or negative sentiment in return.
#
# In the middle is where some of the magic happens. I will construct a Lambda function, which can be thought of as a straightforward Python function that can be executed whenever a specified event occurs. i will give this function permission to send and recieve data from a SageMaker endpoint.
#
# Lastly, the method I will use to execute the Lambda function is a new endpoint that we will create using API Gateway. This endpoint will be a url that listens for data to be sent to it. Once it gets some data it will pass that data on to the Lambda function and then return whatever the Lambda function returns. Essentially it will act as an interface that lets our web app communicate with the Lambda function.
#
# ### Setting up a Lambda function
#
# The first thing I'm going to do is set up a Lambda function. This Lambda function will be executed whenever the public API has data sent to it. When it is executed it will receive the data, perform any sort of processing that is required, send the data (the review) to the SageMaker endpoint I've created and then return the result.
#
# #### Part A: Create an IAM Role for the Lambda function
#
# Since I want the Lambda function to call a SageMaker endpoint, I need to make sure that it has permission to do so. To do this, I will construct a role that we can later give the Lambda function.
#
# Using the AWS Console, navigate to the **IAM** page and click on **Roles**. Then, click on **Create role**. Make sure that the **AWS service** is the type of trusted entity selected and choose **Lambda** as the service that will use this role, then click **Next: Permissions**.
#
# In the search box type `sagemaker` and select the check box next to the **AmazonSageMakerFullAccess** policy. Then, click on **Next: Review**.
#
# Lastly, give this role a name, for example `LambdaSageMakerRole`. Then, click on **Create role**.
#
# #### Part B: Create a Lambda function
#
# Now it is time to actually create the Lambda function.
#
# Using the AWS Console, navigate to the AWS Lambda page and click on **Create a function**. When you get to the next page, make sure that **Author from scratch** is selected. Now, name your Lambda function, using a name that you will remember later on, for example `sentiment_analysis_func`. Make sure that the **Python 3.6** runtime is selected and then choose the role that you created in the previous part. Then, click on **Create Function**.
#
# On the next page you will see some information about the Lambda function you've just created. If you scroll down you should see an editor in which you can write the code that will be executed when your Lambda function is triggered. In this example, I will use the code below. 
#
# ```python
# # We need to use the low-level library to interact with SageMaker since the SageMaker API
# # is not available natively through Lambda.
# import boto3
#
# def lambda_handler(event, context):
#
#     # The SageMaker runtime is what allows us to invoke the endpoint that we've created.
#     runtime = boto3.Session().client('sagemaker-runtime')
#
#     # Now we use the SageMaker runtime to invoke our endpoint, sending the review we were given
#     response = runtime.invoke_endpoint(EndpointName = '**ENDPOINT NAME HERE**',# The name of the endpoint
#                                        ContentType = 'text/plain',             # The data format expected
#                                        Body = event['body'])                   # The actual review
#
#     # The response is an HTTP response whose body contains the result of our inference
#     result = response['Body'].read().decode('utf-8')
#
#     return {
#         'statusCode' : 200,
#         'headers' : { 'Content-Type' : 'text/plain', 'Access-Control-Allow-Origin' : '*' },
#         'body' : result
#     }
# ```
#
# Once you have copy and pasted the code above into the Lambda code editor, replace the `**ENDPOINT NAME HERE**` portion with the name of the endpoint that we deployed earlier, which can be determined using the code cell below.

predictor.endpoint

# Once you have added the endpoint name to the Lambda function, click on **Save**. Your Lambda function is now up and running. Next we need to create a way for our web app to execute the Lambda function.
#
# ### Setting up API Gateway
#
# Now that our Lambda function is set up, it is time to create a new API using API Gateway that will trigger the Lambda function we have just created.
#
# Using AWS Console, navigate to **Amazon API Gateway** and then click on **Get started**.
#
# On the next page, make sure that **New API** is selected and give the new api a name, for example, `sentiment_analysis_api`. Then, click on **Create API**.
#
# Now we have created an API, however it doesn't currently do anything. What we want it to do is to trigger the Lambda function that we created earlier.
#
# Select the **Actions** dropdown menu and click **Create Method**. A new blank method will be created, select its dropdown menu and select **POST**, then click on the check mark beside it.
#
# For the integration point, make sure that **Lambda Function** is selected and click on the **Use Lambda Proxy integration**. This option makes sure that the data that is sent to the API is then sent directly to the Lambda function with no processing. It also means that the return value must be a proper response object as it will also not be processed by API Gateway.
#
# Type the name of the Lambda function you created earlier into the **Lambda Function** text entry box and then click on **Save**. Click on **OK** in the pop-up box that then appears, giving permission to API Gateway to invoke the Lambda function you created.
#
# The last step in creating the API Gateway is to select the **Actions** dropdown and click on **Deploy API**. You will need to create a new Deployment stage and name it anything you like, for example `prod`.
#
# You have now successfully set up a public API to access your SageMaker model. Make sure to copy or write down the URL provided to invoke your newly created public API as this will be needed in the next step. This URL can be found at the top of the page, highlighted in blue next to the text **Invoke URL**.

# Now that I have a publicly available API, I can start using it in a web app. 
#
# In the `website` folder there is a file called `index.html`. There is a line which contains **\*\*REPLACE WITH PUBLIC API URL\*\***. Replace this string with the url obtained in the last step and then save the file.
#
# Now, if you open `index.html` on your local computer, your browser will behave as a local web server and you can use the provided site to interact with your SageMaker model.
#
# If you'd like to go further, you can host this html file anywhere you'd like, for example using github or hosting a static site on Amazon's S3. Once you have done this you can share the link with anyone you'd like and have them play with it too!
#
# <img src="App Test 1.png">
# <img src="App Test 2.png">

# ### Delete the endpoint

predictor.delete_endpoint()



StarcoderdataJupyterScriptsDedupFiltered