This is an automated email from the ASF dual-hosted git repository. damccorm pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push: new b788d09bb37 Add NLP TensorFlow streaming use case (#27659) b788d09bb37 is described below commit b788d09bb37e9d81c42f585428fdc394b2a524c7 Author: Reeba Qureshi <64488642+reeba...@users.noreply.github.com> AuthorDate: Wed Jul 26 23:19:03 2023 +0530 Add NLP TensorFlow streaming use case (#27659) * Add NLP TensorFlow streaming use case This file contains an end-to-end workflow that performs the following: 1. Preprocesses the YouTube comments dataset 2. Trains a TensorFlow model to perform sentiment analysis 3. Extracts YouTube comments using YouTube API and sends them to Google Cloud Pub/Sub 4. Builds a Streaming Beam Pipeline to extract data from Pub/Sub, preprocess it, run inference on it and get the final result. * Delete nlp_tensorflow_streaming.ipynb * Add NLP TensorFlow streaming use case --- .../beam-ml/nlp_tensorflow_streaming.ipynb | 1110 ++++++++++++++++++++ 1 file changed, 1110 insertions(+) diff --git a/examples/notebooks/beam-ml/nlp_tensorflow_streaming.ipynb b/examples/notebooks/beam-ml/nlp_tensorflow_streaming.ipynb new file mode 100644 index 00000000000..44213f5d578 --- /dev/null +++ b/examples/notebooks/beam-ml/nlp_tensorflow_streaming.ipynb @@ -0,0 +1,1110 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "id": "CbCjxaCTpTI7" + }, + "outputs": [], + "source": [ + "# @title ###### Licensed to the Apache Software Foundation (ASF), Version 2.0 (the \"License\")\n", + "\n", + "# Licensed to the Apache Software Foundation (ASF) under one\n", + "# or more contributor license agreements. See the NOTICE file\n", + "# distributed with this work for additional information\n", + "# regarding copyright ownership. The ASF licenses this file\n", + "# to you under the Apache License, Version 2.0 (the\n", + "# \"License\"); you may not use this file except in compliance\n", + "# with the License. You may obtain a copy of the License at\n", + "#\n", + "# http://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing,\n", + "# software distributed under the License is distributed on an\n", + "# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n", + "# KIND, either express or implied. See the License for the\n", + "# specific language governing permissions and limitations\n", + "# under the License" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MQzPKKsd-LvV" + }, + "source": [ + "# Natural Language Processing using Streaming Beam Pipeline\n", + "\n", + "<table align=\"left\">\n", + " <td>\n", + " <a target=\"_blank\" href=\"https://colab.sandbox.google.com/github/apache/beam/blob/master/examples/notebooks/beam-ml/nlp_tensorflow_streaming.ipynb\"><img src=\"https://raw.githubusercontent.com/google/or-tools/main/tools/colab_32px.png\" />Run in Google Colab</a>\n", + " </td>\n", + " <td>\n", + " <a target=\"_blank\" href=\"https://github.com/apache/beam/blob/master/examples/notebooks/beam-ml/nlp_tensorflow_streaming.ipynb\"><img src=\"https://raw.githubusercontent.com/google/or-tools/main/tools/github_32px.png\" />View source on GitHub</a>\n", + " </td>\n", + "</table>\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m0xDYq_X-M18" + }, + "source": [ + "Natural Language Processing or NLP is a field of Artifical Intelligence that enables computers to interpret and understand human language. It involves multiple steps such as applying various preprocessing fuctions, getting predictions from a model, storing the predictions in a useful format, etc.\n", + "Sentiment Analysis is a popular use case of NLP, which allows computers to analyze the sentiment of a text. This notebook demonstrates the use of streaming pipelines in NLP.\n", + "* Extracts comments using [Youtube API](https://developers.google.com/youtube/v3) and publishing them to Pub/Sub\n", + "* Trains a TensorFlow model to predict the sentiment of text\n", + "* Stores the model in Google Cloud and creates a model handler\n", + "* Builds a Beam pipeline to:\n", + " 1. Read data from Pub/Sub\n", + " 2. Create a [PCollection](https://beam.apache.org/documentation/programming-guide/#pcollections) of input text\n", + " 3. Perform preprocessing [transforms](https://beam.apache.org/documentation/programming-guide/#transforms)\n", + " 4. RunInference to get predictions from the previously trained model\n", + " 5. Store the results\n", + "\n", + "For more information on using Apache Beam for machine learning, have a look at [AI/ML Pipelines using Beam](https://beam.apache.org/documentation/ml/overview/)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DH64zgK17BlJ" + }, + "source": [ + "## Installing necessary libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "id": "3CflkK6F2Zot" + }, + "outputs": [], + "source": [ + "!pip install apache-beam[interactive,gcp] --quiet\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2eRsXCuq7Fv-" + }, + "source": [ + "## Importing libraries\n", + "\n", + "Here's a brief overview of the libraries we have imported and what they do:\n", + "* **NumPy**: It provides support for multi-dimensional arrays, along with many mathematical functions to operate on these arrays efficiently.\n", + "* **Pandas**: It allows us to work efficiently with structured or tabular data. Here we have used pandas to import a dataset from a csv file and manipulate it.\n", + "* **TextBlob**: It is a library for processing textual data and common NLP tasks. Here we have used it to analyze comments and find their sentiment polarity.\n", + "* **Apache Beam**: It is used to build and execute data processing pipelines.\n", + "* **RunInference**: It uses a pretrained model to predict results for new unseen data.\n", + "* **TFModelHandlerNumpy**: It is used to manage trained TensorFlow models that take NumPy arrays as input." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "id": "Zj50mA55ULJ5" + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import tensorflow as tf\n", + "\n", + "from textblob import TextBlob\n", + "\n", + "import apache_beam as beam\n", + "from apache_beam.ml.inference.base import RunInference\n", + "from apache_beam.ml.inference.tensorflow_inference import TFModelHandlerNumpy\n", + "from apache_beam.options import pipeline_options\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BeyZU5fa6b1C" + }, + "source": [ + "## Sentiment Analysis\n", + "\n", + "Sentiment analysis is an NLP technique used to determine the sentiment or emotion expressed in a piece of text. The goal of sentiment analysis is to identify whether the text expresses a positive, negative, or neutral sentiment towards a particular subject or topic.\n", + "\n", + "Our goal is to build a streaming pipeline that ultimately tells us the sentiment of YouTube comments. For that, we need to have a pretrained model that can predict the sentiment of text." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pEnsIzu-3o_-" + }, + "source": [ + "## Training a model with labeled youtube comments dataset\n", + "The dataset can be found on [Kaggle](https://www.kaggle.com/datasets/datasnaek/youtube?select=UScomments.csv). It contains various statistics for comments on trending videos on YouTube. Since our goal is to perform a sentiment analysis on comments, we only have to consider the text.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uJMnkxSnByeL" + }, + "source": [ + "#### Reading the data from a csv file\n", + "Pandas allows us to load data from a CSV file and convert it into a DataFrame, which makes it easy to perform data analysis and manipulation." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "id": "WPzS1qRV2ft1" + }, + "outputs": [], + "source": [ + "comm = pd.read_csv('UScomments.csv',encoding='utf8',nrows = 1000, error_bad_lines=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W1veygZRCTA0" + }, + "source": [ + "The dataset has no labels, so we have used [TextBlob](https://textblob.readthedocs.io/en/dev/) to assign the appropriate labels by finding sentiment polarity. Polarity is a number between -1 to 1, which depicts the sentiment of a text. -1 represents most negative and 1 represents most positive sentiment." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "id": "h7tsSISv23F-" + }, + "outputs": [], + "source": [ + "pol=[]\n", + "for i in comm.comment_text.values:\n", + " try:\n", + " analysis =TextBlob(i)\n", + " pol.append(analysis.sentiment.polarity)\n", + "\n", + " except:\n", + " pol.append(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bLTIrypMCVmf" + }, + "source": [ + "We need to convert the continuous numerical values of sentiment polarity into categorical values. We will add a new column 'pol' to the DataFrame which contains the categorical labels.\n", + "* pol = 0 means positive comment (Sentiment polarity should be 0 or more)\n", + "* pol = 1 means negative comment (Sentiment polarity should be less than 0)" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "id": "LrKXREKh284D" + }, + "outputs": [], + "source": [ + "comm['pol']=pol\n", + "comm['pol'][comm.pol >= 0]= 0 #positive\n", + "comm['pol'][comm.pol < 0]= 1 #negative" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9eoFlczACsRR" + }, + "source": [ + "Next, we can drop unnecessary columns from the dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "id": "neZAKUkk2_Kp" + }, + "outputs": [], + "source": [ + "comm = comm.drop(['video_id','likes','replies'],axis=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8smzuTjUC0yE" + }, + "source": [ + "### Preprocessing\n", + "\n", + "Preprocessing refers to the series of steps taken to clean, transform, and prepare raw text data. This preprocessed data can easily be fed into our ML framework and provide better results." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "id": "fOG1MWhsIQW0" + }, + "outputs": [], + "source": [ + "#Dropping null values\n", + "comm = comm.dropna()" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "id": "EJ1lxVNP3vIz" + }, + "outputs": [], + "source": [ + "#Removing unnecessary characters\n", + "def remove_symbols(text):\n", + " return text.replace(\"[^a-zA-Z#]\", \" \")\n", + "comm['comment_text'] = comm['comment_text'].map(remove_symbols)" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "id": "cS9hhcrG4EVP" + }, + "outputs": [], + "source": [ + "#Removing words of length 3 or less\n", + "def remove_short_words(text):\n", + " return ' '.join([str(w) for w in text.split() if len(str(w))>3])\n", + "comm['comment_text'] = comm['comment_text'].map(remove_short_words)" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "id": "wopFqXpN4aBL" + }, + "outputs": [], + "source": [ + "#Converting to lowercase\n", + "def lower_case(text):\n", + " return text.lower()\n", + "comm['comment_text'] = comm['comment_text'].map(lower_case)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1GwE_Uufdrq9" + }, + "source": [ + "Next, we will divide our dataset into 2 parts:\n", + "* X = Array of string comments\n", + "* Y = Polarity Category (0 or 1)\n", + "\n", + "Here X is the unlabeled data which we will use for training and testing our model, and Y contains the corresponding labels." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "id": "i3zX3oLs5jU_" + }, + "outputs": [], + "source": [ + "X = np.array(comm['comment_text'])\n", + "Y = np.array(comm['pol'])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NRAyKOXHfEpU" + }, + "source": [ + "Now we need to split the data into training and testing splits. The train split will be used for training the model, and the test split will be used to check it's performance." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "id": "2oI-gFhs7RMf" + }, + "outputs": [], + "source": [ + "from sklearn.model_selection import train_test_split\n", + "X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=0)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3mgtFKPHJqHm" + }, + "source": [ + "TensorFlow's [Tokenizer](https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/text/Tokenizer) is used to convert text into an array of numbers that represent it based on the frequency of each word. This is done because an ML model can't process text directly, it can only process vectors of numbers. The [fit_on_texts](https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/text/Tokenizer#fit_on_texts) method updates the vocabulary of the tokenizer based on [...] + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "id": "Oi210fpELrnf" + }, + "outputs": [], + "source": [ + "tokenizer = tf.keras.preprocessing.text.Tokenizer(num_words=10000, oov_token='<UNK>')\n", + "tokenizer.fit_on_texts(comm['comment_text'])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8p39hoJKM0wZ" + }, + "source": [ + "Here we have defined a function that takes a tokenizer and array of strings as input, and returns an array of tokenized strings.\n", + "* [texts_to_sequences](https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/text/Tokenizer#texts_to_sequences): Converts text to a sequence of numbers, or tokens.\n", + "* [pad_sequences](https://www.tensorflow.org/api_docs/python/tf/keras/utils/pad_sequences): Transforms tokens of different sizes to the same size." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "id": "XxtYDlejLrrA" + }, + "outputs": [], + "source": [ + "maxlen=100\n", + "def get_sequences(comments):\n", + " sequences = tokenizer.texts_to_sequences(comments)\n", + " padded = tf.keras.utils.pad_sequences(sequences, truncating = 'post', padding='post', maxlen=maxlen)\n", + " return padded" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DF2qMnof37J3" + }, + "source": [ + "Using the function defined aboved, now we will tokenize the X_train and X_test datasets." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "id": "WLZd3QhHMyZQ" + }, + "outputs": [], + "source": [ + "padded_seq_train = get_sequences(X_train)\n", + "padded_seq_test = get_sequences(X_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1mT_Js8pUbs-" + }, + "source": [ + "### Building a simple TensorFlow model to predict polarity of comments\n", + "\n", + "Now that we have our preprocessed training and testing data, we need to build a model. This model will take input as strings and predict which category (positive or negative) the string belongs to. Here is a brief description of the layers we have used to build this model:\n", + "* **Embedding Layer**: It converts tokens into dense vector representations. This allows a neural network to capture semantic relationships between words and generalize better on unseen data.\n", + "* **Bidirectional Layer**: It enhances the information flow by processing input sequences in both forward and backward directions. It is used for sequential data like natural language.\n", + "* **Dense Layer**: It connects every neuron in the previous layer to the neurons in the next layer. The comments can either be positive or negative, so there are two categories. Thus, the Dense layer provides two outputs at the end, both of which correspond to the probabibility of the text belonging to that category." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "id": "5HuyQgQBNzaB" + }, + "outputs": [], + "source": [ + "model = tf.keras.models.Sequential([\n", + "tf.keras.layers.Embedding(10000,16,input_length=maxlen),\n", + "tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(20, return_sequences=True)),\n", + "tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(10)),\n", + "tf.keras.layers.Dense(2, activation='softmax')\n", + "])\n", + "model.compile(\n", + " loss='sparse_categorical_crossentropy',\n", + " optimizer='adam',\n", + " metrics=['accuracy']\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "u1cOz6wVXKwG" + }, + "source": [ + "Next, we will create a checkpoint to save the model with the best validation accuracy." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "id": "XrosBmBqX0yO" + }, + "outputs": [], + "source": [ + "checkpoint_acc = tf.keras.callbacks.ModelCheckpoint(\"weights_acc\", monitor=\"val_accuracy\",\n", + "save_best_only=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "O-lZlCWQb6ut" + }, + "source": [ + "Now we will train our model by fitting it with the training data and using testing data for validation." + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "aQwWsnwEPY82", + "outputId": "1d0e5578-04b7-42e6-8b17-b53f18e6f8c9" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Epoch 1/10\n", + "25/25 [==============================] - ETA: 0s - loss: 0.5931 - accuracy: 0.7650" + ] + }, + { + "output_type": "stream", + "name": "stderr", + "text": [ + "WARNING:absl:Found untraced functions such as _update_step_xla, lstm_cell_7_layer_call_fn, lstm_cell_7_layer_call_and_return_conditional_losses, lstm_cell_8_layer_call_fn, lstm_cell_8_layer_call_and_return_conditional_losses while saving (showing 5 of 9). These functions will not be directly callable after loading.\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r25/25 [==============================] - 60s 2s/step - loss: 0.5931 - accuracy: 0.7650 - val_loss: 0.3625 - val_accuracy: 0.8900\n", + "Epoch 2/10\n", + "25/25 [==============================] - 4s 169ms/step - loss: 0.4663 - accuracy: 0.8163 - val_loss: 0.3644 - val_accuracy: 0.8900\n", + "Epoch 3/10\n", + "25/25 [==============================] - 3s 120ms/step - loss: 0.4550 - accuracy: 0.8163 - val_loss: 0.3663 - val_accuracy: 0.8900\n", + "Epoch 4/10\n", + "25/25 [==============================] - 3s 112ms/step - loss: 0.4268 - accuracy: 0.8163 - val_loss: 0.3683 - val_accuracy: 0.8900\n", + "Epoch 5/10\n", + "25/25 [==============================] - 3s 111ms/step - loss: 0.3700 - accuracy: 0.8238 - val_loss: 0.3266 - val_accuracy: 0.8900\n", + "Epoch 6/10\n", + "25/25 [==============================] - 4s 179ms/step - loss: 0.2280 - accuracy: 0.9237 - val_loss: 0.3199 - val_accuracy: 0.8450\n", + "Epoch 7/10\n", + "25/25 [==============================] - 4s 152ms/step - loss: 0.1034 - accuracy: 0.9825 - val_loss: 0.4166 - val_accuracy: 0.8000\n", + "Epoch 8/10\n", + "25/25 [==============================] - 3s 108ms/step - loss: 0.0597 - accuracy: 0.9900 - val_loss: 0.4530 - val_accuracy: 0.8500\n", + "Epoch 9/10\n", + "25/25 [==============================] - 3s 114ms/step - loss: 0.0311 - accuracy: 0.9962 - val_loss: 0.5967 - val_accuracy: 0.8000\n", + "Epoch 10/10\n", + "25/25 [==============================] - 3s 106ms/step - loss: 0.0142 - accuracy: 0.9987 - val_loss: 0.8104 - val_accuracy: 0.7800\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "<keras.callbacks.History at 0x7eda55b3a4d0>" + ] + }, + "metadata": {}, + "execution_count": 57 + } + ], + "source": [ + "model.fit(\n", + " padded_seq_train, y_train,\n", + " validation_data=(padded_seq_test, y_test),\n", + " epochs=10,\n", + " callbacks=checkpoint_acc\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ow06ihbv7JkW" + }, + "source": [ + "## Authenticating for Google cloud\n", + "\n", + "We need to authenticate our google account for the following:\n", + "* Saving the model in Google cloud\n", + "* Publishing messages in Pub/Sub\n", + "* Accessing previously published messages using a subscription" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "id": "POlTUDj8D0_L" + }, + "outputs": [], + "source": [ + "from google.colab import auth\n", + "auth.authenticate_user()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TzpdiU39gA9v" + }, + "source": [ + "## Saving the model in Google cloud\n", + "\n", + "We will save the model in Google Cloud so that we can easily load it using RunInference. Then it can be used to predict results for the input data in our Beam pipeline." + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "2fpLQvm_ZEze", + "outputId": "35b6d356-c86d-43e5-e882-ddef6bc6d214" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "<tensorflow.python.checkpoint.checkpoint.CheckpointLoadStatus at 0x7eda3c5c42b0>" + ] + }, + "metadata": {}, + "execution_count": 59 + } + ], + "source": [ + "model.load_weights('weights_acc')" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "id": "-3H9m7NrZTs9" + }, + "outputs": [], + "source": [ + "save_model_dir = '' # Add the link to your GCS bucket here" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "cS5wxvxTZgcp", + "outputId": "999c6366-d887-49aa-8e40-0ebb995006d6" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "WARNING:absl:Found untraced functions such as _update_step_xla, lstm_cell_7_layer_call_fn, lstm_cell_7_layer_call_and_return_conditional_losses, lstm_cell_8_layer_call_fn, lstm_cell_8_layer_call_and_return_conditional_losses while saving (showing 5 of 9). These functions will not be directly callable after loading.\n" + ] + } + ], + "source": [ + "model.save(save_model_dir)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a-lXoeTqo7UG" + }, + "source": [ + "## Creating a model handler\n", + "\n", + "A model handler is used to save, load and manage trained ML models. Here we used TFModelHandlerNumpy as our input text is in the form of numpy arrays." + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "id": "mTR2SonmZiBQ" + }, + "outputs": [], + "source": [ + "model_handler = TFModelHandlerNumpy(save_model_dir)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "94aXKrR0Cnvw" + }, + "source": [ + "## Understanding Pub/Sub\n", + "\n", + "Google Cloud [Pub/Sub](https://cloud.google.com/pubsub/docs/overview) is a messaging service provided by Google Cloud Platform (GCP). It is designed to enable scalable, reliable, and real-time messaging between independent applications. Pub/Sub follows the publish-subscribe model, where messages are published by senders (publishers) to a topic, and then delivered to multiple receivers (subscribers) who have expressed interest in that topic. <br> <br>\n", + "Pub/Sub acts as an unbounded source, as it's constantly recieving and sending messages in real time. In such cases, we need to build a [Streaming Pipeline](https://beam.apache.org/documentation/sdks/python-streaming/)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QMPdmWS57Oww" + }, + "source": [ + "## Creating a publisher for a pubsub topic in Google Cloud Console\n", + "A publisher is a component that allows us to create and send messages to Google Cloud Pub/Sub. Learn more about publishing and recieved messages from Pub/Sub [here](https://cloud.google.com/pubsub/docs/publish-receive-messages-client-library)." + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": { + "id": "2UyYvEcz9IAx" + }, + "outputs": [], + "source": [ + "import os\n", + "from google.cloud import pubsub_v1\n", + "PROJECT_ID = '' # Add your project ID here\n", + "TOPIC = '' # Add your topic name here\n", + "publisher = pubsub_v1.PublisherClient()\n", + "topic_name = 'projects/{project_id}/topics/{topic}'.format(\n", + " project_id = PROJECT_ID,\n", + " topic = TOPIC,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2wAnY8zz7cIu" + }, + "source": [ + "## Extracting and sending comments to Pub/Sub\n", + "YouTube API provides an interface for accessing YouTube data. First, we need to enable YouTube API on the Google Cloud Console project. After that, we need to create a credential, which will further provide an API key. This API key, along with a video ID can be used to access data of that YouTube video. The Publisher created earlier is used to publish each comment to Pub/Sub.\n", + "\n", + "See examples of using the YouTube API [here](https://developers.google.com/youtube/v3/code_samples/code_snippets)." + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "frKiefXGHXve", + "outputId": "565d73f5-ec69-498b-91fc-211ce4c495e7" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Can’t wait to watch you guys grow . Harmonies are on point and the oversized early 90’s blazers are a great touch.\n", + "Amazing performance! Such an inspiring group ❤\n", + "Love the vibe\n", + "Your telling me this has less than 100 views???? Unreal\n", + "I'm happy that I lived long enough to see and hear music that tells the truth that millions of men and boys live every day! WELL DONE!\n", + "Love the unity of sound\n" + ] + } + ], + "source": [ + "from googleapiclient.discovery import build\n", + "\n", + "api_key = '' #Add your API key here\n", + "\n", + "def video_comments(video_id):\n", + " # Creating youtube resource object\n", + " youtube = build('youtube', 'v3',\n", + " developerKey=api_key)\n", + "\n", + " # Retrieve youtube video results\n", + " video_response=youtube.commentThreads().list(\n", + " part='snippet,replies',\n", + " videoId=video_id\n", + " ).execute()\n", + "\n", + " # Iterate video response\n", + " while video_response:\n", + "\n", + " # extracting required info from each object\n", + " for item in video_response['items']:\n", + "\n", + " # Extracting comments\n", + " comment = item['snippet']['topLevelComment']['snippet']['textDisplay']\n", + "\n", + " # Print comment\n", + " print(comment, end = '\\n')\n", + " data = comment.encode(\"utf-8\")\n", + "\n", + " # Publishing the comment to Pub/Sub\n", + " publisher.publish(topic_name, data)\n", + "\n", + " # Repeat until there are no next pages\n", + " if 'nextPageToken' in video_response:\n", + " video_response = youtube.commentThreads().list(\n", + " part = 'snippet,replies',\n", + " videoId = video_id\n", + " ).execute()\n", + " else:\n", + " return\n", + "\n", + "# The video ID can be extracted from the video URL, which can be represented like this\n", + "# https://www.youtube.com/watch?v=VIDEO_ID\n", + "# Enter here the desired video ID\n", + "video_id = \"fCXYrAH2gQI\"\n", + "\n", + "# Call function\n", + "video_comments(video_id)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "P51E6640pbw4" + }, + "source": [ + "## Defining utility functions\n", + "\n", + "Below we have defined some functions for our Beam pipeline to perform the following tasks:\n", + "* Print the messages recieved from Pub/Sub\n", + "* Tokenize the strings\n", + "* Save the predictions in a list\n", + "\n", + "These functions can be used in our pipeline by using [Map](https://beam.apache.org/documentation/transforms/python/elementwise/map/), which essentially calls the function on each element in the PCollection." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": { + "id": "kdzXM_ooco9-" + }, + "outputs": [], + "source": [ + "# Index 0 corresponds to positive comment while index 1 corresponds to negative comment\n", + "labels = ['positive','negative']" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": { + "id": "HKbvU544Cd6v" + }, + "outputs": [], + "source": [ + "# Printing values\n", + "def print_values(element):\n", + " print(element)\n", + " return element\n", + "\n", + "# Here along with printing, we have also returned the element.\n", + "# This is done so that the element is passed into the next functions or transforms after printing." + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": { + "id": "gNpyjcEwLny9" + }, + "outputs": [], + "source": [ + "# Tokenizing the strings\n", + "def tokenize(element):\n", + " padded_seq = get_sequences([element])\n", + " return padded_seq[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": { + "id": "58xSecy4Jfn8" + }, + "outputs": [], + "source": [ + "# Saving predictions in a list\n", + "predictions = []\n", + "from tensorflow.python.ops.numpy_ops import np_config\n", + "np_config.enable_numpy_behavior()\n", + "def save_predictions(element):\n", + " list_of_predictions = element.inference.tolist()\n", + " highest_prediction = max(list_of_predictions)\n", + " ans = labels[list_of_predictions.index(highest_prediction)]\n", + " predictions.append([list_of_predictions,ans])\n", + " print(ans)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YaqDqk9w0LlA" + }, + "source": [ + "## Building an Apache Beam Pipeline\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "OgH15nbK0Whw" + }, + "source": [ + "We need to build a streaming pipeline that takes data from Pub/Sub. A [Runner](https://beam.apache.org/documentation/#runners) is used to execute Beam pipelines in a distributed manner. We need to use a streaming runner to run a streaming pipeline. [InteractiveRunner](https://beam.apache.org/releases/pydoc/2.10.0/apache_beam.runners.interactive.interactive_runner.html) is suitable for this and allows developing and running Beam pipelines interactively in notebooks.\n", + "\n", + "See more details on how to use InteractiveRunner [here](https://cloud.google.com/dataflow/docs/guides/interactive-pipeline-development)." + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": { + "id": "Nq1N7JGR9Vwa" + }, + "outputs": [], + "source": [ + "# path to the topic\n", + "TOPIC_PATH = '' # Add the path to your topic here" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "id": "5-06sxMtwyWN" + }, + "outputs": [], + "source": [ + "# path to the subscription\n", + "SUBS_PATH = '' # Add the path to your subscription here" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Importing InteractiveRunner" + ], + "metadata": { + "id": "UliBhojEfxhq" + } + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": { + "id": "Nr6lPbQMhywM" + }, + "outputs": [], + "source": [ + "from apache_beam.runners.interactive.interactive_runner import InteractiveRunner\n", + "import apache_beam.runners.interactive.interactive_beam as ib" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": { + "id": "nuHyErBPZDVY" + }, + "outputs": [], + "source": [ + "ib.options.recording_duration = '2m' # This is how long Interactive Runner will listen to data from Pub/Sub\n", + "ib.options.recording_size_limit = 1e9 # This is the recording size limit set to 1 GB\n", + "options = pipeline_options.PipelineOptions()\n", + "options.view_as(pipeline_options.StandardOptions).streaming = True # Streaming mode is set True" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Wfo0UqQmz821" + }, + "source": [ + "The pipeline performs the following tasks:\n", + "* Reads messages from Cloud Pub Sub\n", + "* Prints the messages\n", + "* Performs preprocessing. We can reuse all of our previously defined preprocessing functions for training using [beam.Map](https://beam.apache.org/documentation/transforms/python/elementwise/map/).\n", + "* RunInference on the preprocessed data\n", + "* Prints the result and store in a list" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 627 + }, + "id": "unDFnXzcLkvF", + "outputId": "b20179e0-6bbd-407b-8752-1f282ff2c2ed" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "WARNING:apache_beam.options.pipeline_options:Discarding unparseable args: ['-f', '/root/.local/share/jupyter/runtime/kernel-85b81103-253c-4eab-9f0c-29c3184b9aab.json']\n" + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Can’t wait to watch you guys grow . Harmonies are on point and the oversized early 90’s blazers are a great touch.\n", + "Amazing performance! Such an inspiring group ❤\n", + "Love the vibe\n", + "Your telling me this has less than 100 views???? Unreal\n", + "I'm happy that I lived long enough to see and hear music that tells the truth that millions of men and boys live every day! WELL DONE!\n", + "Love the unity of sound\n", + "positive\n", + "positive\n", + "positive\n", + "positive\n", + "positive\n", + "positive\n" + ] + }, + { + "output_type": "error", + "ename": "KeyboardInterrupt", + "evalue": "ignored", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m<ipython-input-37-715e98c74e29>\u001b[0m in \u001b[0;36m<cell line: 1>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mwith\u001b[0m \u001b[0mbeam\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPipeline\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0moptions\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0moptions\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mp\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m [...] + "\u001b[0;32m/usr/local/lib/python3.10/dist-packages/apache_beam/pipeline.py\u001b[0m in \u001b[0;36m__exit__\u001b[0;34m(self, exc_type, exc_val, exc_tb)\u001b[0m\n\u001b[1;32m 599\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mexc_type\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 600\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \ [...] + "\u001b[0;32m/usr/local/lib/python3.10/dist-packages/apache_beam/runners/direct/direct_runner.py\u001b[0m in \u001b[0;36mwait_until_finish\u001b[0;34m(self, duration)\u001b[0m\n\u001b[1;32m 584\u001b[0m 'DirectRunner does not support duration argument.')\n\u001b[1;32m 585\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 586\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m. [...] + "\u001b[0;32m/usr/local/lib/python3.10/dist-packages/apache_beam/runners/direct/executor.py\u001b[0m in \u001b[0;36mawait_completion\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 430\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 431\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mawait_completion\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 432\u001b[0 [...] + "\u001b[0;32m/usr/local/lib/python3.10/dist-packages/apache_beam/runners/direct/executor.py\u001b[0m in \u001b[0;36mawait_completion\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 475\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 476\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mawait_completion\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 477\u001b[0 [...] + "\u001b[0;32m/usr/local/lib/python3.10/dist-packages/apache_beam/runners/direct/executor.py\u001b[0m in \u001b[0;36mtake\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 549\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 550\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 551\u001b[0;31m [...] + "\u001b[0;32m/usr/lib/python3.10/queue.py\u001b[0m in \u001b[0;36mget\u001b[0;34m(self, block, timeout)\u001b[0m\n\u001b[1;32m 178\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mremaining\u001b[0m \u001b[0;34m<=\u001b[0m \u001b[0;36m0.0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 179\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mEmpty\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b [...] + "\u001b[0;32m/usr/lib/python3.10/threading.py\u001b[0m in \u001b[0;36mwait\u001b[0;34m(self, timeout)\u001b[0m\n\u001b[1;32m 322\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 323\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mtimeout\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m [...] + "\u001b[0;31mKeyboardInterrupt\u001b[0m: " + ] + } + ], + "source": [ + "with beam.Pipeline(options=options) as p:\n", + " _ = (p | \"Read From Pub/Sub\" >> beam.io.ReadFromPubSub(subscription=SUBS_PATH)\n", + " | \"Convert to String\" >> beam.Map(lambda element: element.decode('utf-8'))\n", + " | \"Print\" >> beam.Map(print_values)\n", + " | \"Remove Symbols\" >> beam.Map(remove_symbols)\n", + " | \"Remove Short Words\" >> beam.Map(remove_short_words)\n", + " | \"Lower Case\" >> beam.Map(lower_case)\n", + " | \"Tokenize\" >> beam.Map(tokenize)\n", + " | \"RunInference\" >> RunInference(model_handler)\n", + " | \"Store Predictions\" >> beam.Map(save_predictions)\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bIhqhtyvzMXR" + }, + "source": [ + "The above pipeline is a streaming pipeline, which means it will run continuously, unless we stop it manually. This is why a keyboard interrupt can be seen here." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vEch9GkVzp9S" + }, + "source": [ + "Let us print the predictions made by the model and the corresponding sentiment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "YqNoxnd4qJAD", + "outputId": "bde9cf5b-97b6-4197-9629-41d0b53c043a" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "[[[0.852806806564331, 0.14719319343566895], 'positive'],\n", + " [[0.8602035045623779, 0.13979655504226685], 'positive'],\n", + " [[0.8670042753219604, 0.13299570977687836], 'positive'],\n", + " [[0.8574993014335632, 0.14250065386295319], 'positive'],\n", + " [[0.8401712775230408, 0.15982864797115326], 'positive'],\n", + " [[0.8648154735565186, 0.13518451154232025], 'positive']]" + ] + }, + "metadata": {}, + "execution_count": 38 + } + ], + "source": [ + "predictions" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file