-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
readme zip removed unused function shiny doc removed parallel function shiny core toc renamed folder rename
- Loading branch information
1 parent
0da6eaa
commit 0e8608e
Showing
25 changed files
with
818 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# Shiny Core | ||
|
||
To deploy a [Shiny Core](https://shiny.posit.co/py/docs/overview.html) application to Ploomber Cloud you need: | ||
|
||
- A `Dockerfile` | ||
- A Shiny project | ||
|
||
## `Dockerfile` | ||
|
||
Use this [template](https://github.com/ploomber/doc/blob/main/examples/docker/shiny-core/Dockerfile) `Dockerfile`: | ||
|
||
```Dockerfile | ||
FROM python:3.11 | ||
|
||
COPY app-core.py app-core.py | ||
COPY requirements.txt requirements.txt | ||
RUN pip install -r requirements.txt | ||
|
||
ENTRYPOINT ["shiny", "run", "app-core.py", "--host", "0.0.0.0", "--port", "80"] | ||
``` | ||
|
||
## Testing locally | ||
|
||
To test your app, you can use `docker` locally: | ||
|
||
```sh | ||
# build the docker image | ||
docker build . -t shiny-core-app | ||
|
||
# run it | ||
docker run -p 5000:80 shiny-core-app | ||
``` | ||
|
||
Now, open [http://0.0.0.0:5000/](http://0.0.0.0:5000/) to see your app. | ||
|
||
|
||
## Deploy | ||
|
||
Once you have all your files, create a zip file. | ||
|
||
To deploy a Shiny Core app from the deployment menu, select the Docker option and follow the instructions: | ||
|
||
![](../static/docker.png) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
FROM python:3.11 | ||
|
||
COPY app-core.py app-core.py | ||
COPY requirements.txt requirements.txt | ||
RUN pip install -r requirements.txt | ||
|
||
ENTRYPOINT ["shiny", "run", "app-core.py", "--host", "0.0.0.0", "--port", "80"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# Model monitoring application | ||
|
||
This app is built on Shiny and allows you to visualize model training accuracy scores in realtime in 2 steps: | ||
|
||
* Train multiple models with different parameters locally, and store the accuracy scores to a remote database at the end of every epoch. | ||
* The visualisation application dynamically detects changes in the remote database and generates a plot as the training progresses. | ||
|
||
## Connecting to MongoDB Cloud | ||
|
||
For the purpose of this tutorial we will use the `MongoDB Atlas` cloud database. Let's see the steps for creating a cluster: | ||
|
||
First create an account on [MongoDB cloud](https://www.mongodb.com/atlas/database) and create a new project: | ||
|
||
![](./static/create-project.png) | ||
|
||
Then select `Create deployment` and select the free tier `M0` or your desired tier. | ||
|
||
In the `Security Quickstart` page, select your authentication type. For this tutorial we have generated a username and password. | ||
|
||
Select the `Cloud Environment` and open the `Network Access Page` in a new tab. | ||
|
||
![](./static/network-access.png) | ||
|
||
Now select `ALLOW ACCESS FROM ANYWHERE` and confirm to allow all IP addresses to connect to the cluster. | ||
|
||
![](./static/ip-access.png) | ||
|
||
Once you have completed the access rules setup, select the `CONNECT` button on the `Overview` page to get the connection details. Specifically, copy the connection string and replace the `URI` variable value in `app-core.py`, `train_one.py` and `train_two.py` | ||
|
||
![](./static/connect.png) | ||
|
||
## Deploy application | ||
|
||
Create a zip file using `Dockerfile`, `app-core.py` and `requirements.txt` and deploy this file to Ploomber cloud. | ||
Once the `VIEW APPLICATION` button is enabled, you can start the training script and monitor the training. | ||
|
||
## Train models | ||
|
||
We will use the [MNIST dataset](https://www.tensorflow.org/datasets/catalog/mnist) and train two different models on the data. The models differ in the number of units in the `Dense` layer. | ||
The training scripts write the accuracy values at the end of every epoch to the MongoDB database in a collection called `accuracy_scores`. | ||
|
||
First create a virtual environment and install the required packages: | ||
|
||
```bash | ||
pip install tensorflow scikit-learn "pymongo[srv]" | ||
``` | ||
|
||
Then run the training scripts parallely.: | ||
|
||
```bash | ||
python train_one.py & python train_two.py & wait | ||
``` | ||
|
||
Once the scripts are running you can monitor the accuracy scores as the training progresses on the Ploomber Cloud application deployed in above step: | ||
|
||
![](./static/monitor.png) | ||
|
||
Currently, the threshold for an accurate model (marked in green) is 0.9. You can modify it by setting `THRESHOLD_MID` in `app-core.py`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
from __future__ import annotations | ||
|
||
import pandas as pd | ||
import plotly.express as px | ||
from shinywidgets import output_widget, render_widget | ||
from faicons import icon_svg | ||
|
||
from pymongo.mongo_client import MongoClient | ||
from pymongo.server_api import ServerApi | ||
from shiny import App, Inputs, Outputs, Session, reactive, render, ui | ||
|
||
THRESHOLD_MID = 0.9 | ||
THRESHOLD_MID_COLOR = "rgb(0, 137, 26)" | ||
THRESHOLD_LOW = 0.5 | ||
THRESHOLD_LOW_COLOR = "rgb(193, 0, 0)" | ||
|
||
URI = "CONNECTION_STRING" | ||
|
||
# Create a new client and connect to the server | ||
client = MongoClient(URI, server_api=ServerApi('1')) | ||
|
||
try: | ||
client.admin.command('ping') | ||
print("Successfully connected to MongoDB!") | ||
except Exception as e: | ||
raise ConnectionError(str(e)) from e | ||
|
||
db = client.myDatabase | ||
my_collection = db["accuracy_scores"] | ||
|
||
|
||
def last_modified(my_collection): | ||
""" | ||
Get the timestamp of the most recent row in the MongoDB database. | ||
""" | ||
return my_collection.find().sort({'timestamp':-1}).limit(1) | ||
|
||
|
||
@reactive.poll(lambda: last_modified(my_collection)) | ||
def df(): | ||
""" | ||
@reactive.poll calls a cheap query (`last_modified()`) every 1 second to check if | ||
the expensive query (`df()`) should be run and downstream calculations should be | ||
updated. | ||
""" | ||
results = my_collection.find().sort({'timestamp': -1, 'model': -1}).limit(150) | ||
tbl = pd.DataFrame(list(results)) | ||
|
||
# Convert timestamp to datetime object, which SQLite doesn't support natively | ||
tbl["timestamp"] = pd.to_datetime(tbl["timestamp"], utc=True) | ||
# Create a short label for readability | ||
tbl["time"] = tbl["timestamp"].dt.strftime("%H:%M:%S") | ||
# Reverse order of rows | ||
tbl = tbl.iloc[::-1] | ||
|
||
return tbl | ||
|
||
|
||
model_names = ["model_1", "model_2"] | ||
model_colors = { | ||
name: color | ||
for name, color in zip(model_names, px.colors.qualitative.D3[0 : len(model_names)]) | ||
} | ||
|
||
|
||
def make_value_box(model, score): | ||
theme = "text-success" | ||
icon = icon_svg("check", width="50px") | ||
if score < THRESHOLD_MID: | ||
theme = "text-warning" | ||
icon = icon_svg("triangle-exclamation", width="50px") | ||
if score < THRESHOLD_LOW: | ||
theme = "bg-danger" | ||
icon = icon_svg("circle-exclamation", width="50px") | ||
|
||
return ui.value_box(model, ui.h2(score), theme=theme, showcase=icon) | ||
|
||
|
||
app_ui = ui.page_sidebar( | ||
ui.sidebar( | ||
ui.input_checkbox_group("models", "Models", model_names, selected=model_names), | ||
), | ||
ui.row( | ||
ui.h1("Model monitoring dashboard"), | ||
ui.output_ui("value_boxes"), | ||
), | ||
ui.row( | ||
ui.card(output_widget("plot_timeseries")), | ||
), | ||
) | ||
|
||
|
||
def server(input: Inputs, output: Outputs, session: Session): | ||
@reactive.Calc | ||
def filtered_df(): | ||
""" | ||
Return the data frame that should be displayed in the app, based on the user's | ||
input. This will be either the latest rows, or a specific time range. Also | ||
filter out rows for models that the user has deselected. | ||
""" | ||
data = df() | ||
|
||
# Filter the rows so we only include the desired models | ||
return data[data["model"].isin(input.models())] | ||
|
||
@reactive.Calc | ||
def filtered_model_names(): | ||
return filtered_df()["model"].unique() | ||
|
||
@output | ||
@render.ui | ||
def value_boxes(): | ||
data = filtered_df() | ||
models = data["model"].unique().tolist() | ||
scores_by_model = { | ||
x: data[data["model"] == x].iloc[-1]["score"] for x in models | ||
} | ||
# Round scores to 2 decimal places | ||
scores_by_model = {x: round(y, 2) for x, y in scores_by_model.items()} | ||
|
||
return ui.layout_columns( | ||
*[ | ||
# For each model, return a value_box with the score, colored based on | ||
# how high the score is. | ||
make_value_box(model, score) | ||
for model, score in scores_by_model.items() | ||
], | ||
width="135px", | ||
) | ||
|
||
@render_widget | ||
def plot_timeseries(): | ||
""" | ||
Returns a Plotly Figure visualization. Streams new data to the Plotly widget in | ||
the browser whenever filtered_df() updates, and completely recreates the figure | ||
when filtered_model_names() changes (see recreate_key=... above). | ||
""" | ||
fig = px.line( | ||
filtered_df(), | ||
x="time", | ||
y="score", | ||
labels=dict(score="accuracy"), | ||
color="model", | ||
color_discrete_map=model_colors, | ||
template="simple_white", | ||
) | ||
|
||
return fig | ||
|
||
|
||
app = App(app_ui, server) |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
pandas | ||
plotly | ||
shiny | ||
shinywidgets | ||
faicons | ||
pymongo[srv] |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
from datetime import datetime | ||
|
||
import tensorflow as tf | ||
from sklearn.model_selection import train_test_split | ||
|
||
from pymongo.errors import OperationFailure | ||
from pymongo.mongo_client import MongoClient | ||
from pymongo.server_api import ServerApi | ||
|
||
URI = "CONNECTION_STRING" | ||
|
||
# Create a new client and connect to the server | ||
client = MongoClient(URI, server_api=ServerApi('1')) | ||
|
||
# Send a ping to confirm a successful connection | ||
try: | ||
client.admin.command('ping') | ||
print("Successfully connected to MongoDB!") | ||
except Exception as e: | ||
raise ConnectionError(str(e)) from e | ||
|
||
db = client.myDatabase | ||
my_collection = db["accuracy_scores"] | ||
|
||
|
||
class TrackLossandAccuracyCallback(tf.keras.callbacks.Callback): | ||
|
||
def __init__(self, model_name): | ||
self.model_name = model_name | ||
|
||
def on_epoch_end(self, epoch, logs=None): | ||
my_collection.insert_one({'model': self.model_name, 'score': logs["accuracy"], 'timestamp': datetime.now()}) | ||
|
||
|
||
def train_model(train_images, train_labels, val_images, val_labels): | ||
model = tf.keras.Sequential( | ||
[ | ||
tf.keras.layers.Flatten(input_shape=(28, 28)), | ||
tf.keras.layers.Dense(128, activation="relu"), | ||
tf.keras.layers.Dense(10), | ||
] | ||
) | ||
|
||
model.compile( | ||
optimizer="adam", | ||
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), | ||
metrics=["accuracy"], | ||
) | ||
|
||
model.fit( | ||
train_images, | ||
train_labels, | ||
validation_data=(val_images, val_labels), | ||
epochs=50, | ||
verbose=0, | ||
callbacks=[TrackLossandAccuracyCallback('model_1')], | ||
) | ||
|
||
|
||
def begin(): | ||
try: | ||
my_collection.drop() | ||
except OperationFailure as e: | ||
raise ConnectionError(str(e)) from e | ||
|
||
fashion_mnist = tf.keras.datasets.fashion_mnist | ||
(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data() | ||
|
||
# create a validation set | ||
train_images, val_images, train_labels, val_labels = train_test_split( | ||
train_images, train_labels, test_size=0.2 | ||
) | ||
|
||
# Scale the images to range (0,1) | ||
train_images = train_images / 255.0 | ||
val_images = val_images / 255.0 | ||
train_model(train_images, train_labels, val_images, val_labels) | ||
|
||
|
||
if __name__ == "__main__": | ||
begin() |
Oops, something went wrong.