diff --git a/.github/workflows/actions-demo.yml b/.github/workflows/actions-demo.yml index 50b25c6..bc5ca7b 100644 --- a/.github/workflows/actions-demo.yml +++ b/.github/workflows/actions-demo.yml @@ -20,9 +20,5 @@ jobs: - run: echo " This job's status is ${{ job.status }}." - name: Install dep run: pip3 install --no-cache-dir -r requirements.txt - - name: Run pytest cases - run: pytest - - name: Run bash script to build and run docker image - run: | - chmod +x ./docker_run.sh - ./docker_run.sh + - name: Run Docker image + run: ./docker_run.sh \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..685d615 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.8 +WORKDIR /app +COPY app.py /app/ +#COPY utils.py /app/ +COPY requirements.txt /app/ +COPY saved_model /app/saved_model/ +RUN apt-get install libsm6 libxext6 -y +RUN pip install -r requirements.txt +VOLUME /app/saved_model +EXPOSE 80 +CMD ["python", "app.py"] \ No newline at end of file diff --git a/api/app.py b/api/app.py index b4fd84e..539c4ed 100644 --- a/api/app.py +++ b/api/app.py @@ -1,10 +1,11 @@ from flask import Flask,request - +from joblib import load +import numpy as np app = Flask(__name__) @app.route("/") def hello_world(): - return "

Hello World!

" + return "

Hello, World!

" @app.route('/user/') def profile(username): @@ -20,6 +21,21 @@ def sum_two_numbers(x,y): @app.route("/predict", methods = ['POST']) def predict_fn(): input_data = request.get_json() - x = input_data['x'] - y = input_data['y'] - return f"sum of {x} and {y} is {x + y}" \ No newline at end of file + img1 = input_data['img1'] + img2 = input_data['img2'] + + img1 = list(map(float, img1)) + img2 = list(map(float, img2)) + + img1 = np.array(img1).reshape(1,-1) + img2 = np.array(img2).reshape(1,-1) + + model = load("models/svm_gamma:0.01_C:0.1.joblib") + predicted1 = model.predict(img1) + predicted2 = model.predict(img2) + if predicted1[0] == predicted2[0]: + print("Both images are of same digit") + return "True" + else: + print("Both images are not of same digit") + return "False" \ No newline at end of file diff --git a/api/app_2.py b/api/app_2.py new file mode 100644 index 0000000..539c4ed --- /dev/null +++ b/api/app_2.py @@ -0,0 +1,41 @@ +from flask import Flask,request +from joblib import load +import numpy as np +app = Flask(__name__) + +@app.route("/") +def hello_world(): + return "

Hello, World!

" + +@app.route('/user/') +def profile(username): + return f'{username}\'s profile' + + +@app.route("/sum//") +def sum_two_numbers(x,y): + res = int(x) + int(y) + return f"sum of {x} and {y} is {res}" + + +@app.route("/predict", methods = ['POST']) +def predict_fn(): + input_data = request.get_json() + img1 = input_data['img1'] + img2 = input_data['img2'] + + img1 = list(map(float, img1)) + img2 = list(map(float, img2)) + + img1 = np.array(img1).reshape(1,-1) + img2 = np.array(img2).reshape(1,-1) + + model = load("models/svm_gamma:0.01_C:0.1.joblib") + predicted1 = model.predict(img1) + predicted2 = model.predict(img2) + if predicted1[0] == predicted2[0]: + print("Both images are of same digit") + return "True" + else: + print("Both images are not of same digit") + return "False" \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..62360d7 --- /dev/null +++ b/app.py @@ -0,0 +1,46 @@ +import pickle +from flask import Flask, request, jsonify +import numpy as np +from PIL import Image +import json + +app = Flask("assigmnet6") + +with open("saved_model/mnist_classification_svm.pkl", "rb") as f: + model = pickle.load(f) + +def preprocess(image_file): + with Image.open(image_file) as img: + img = img.convert('L') + img = img.resize((8, 8), Image.Resampling.LANCZOS) + image_array = np.array(img, dtype=np.float64) + image_array = image_array.reshape(1, -1) + image_array *= (16 / 255) + return image_array + +def predict_digit(image): + prediction = model.predict(image) + return prediction + +@app.route('/run_test', methods=['GET']) +def test(): + return jsonify({'run_test': 'OK'}) + +@app.route('/predict', methods=['POST']) +def check_same_digit(): + try: + print(request.files.keys()) + image1 = request.files['image1'] + image2 = request.files['image2'] + + digit1 = predict_digit(preprocess(image1)) + digit2 = predict_digit(preprocess(image2)) + + same_digit = digit1[0] == digit2[0] + + return json.dumps({'digit match': bool(same_digit)}) + except Exception as e: + return jsonify({'error': str(e)}) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=80, debug=True) \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 868fab1..a5c01dc 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -25,6 +25,8 @@ WORKDIR /digits #create volume to mount it on host VOLUME /digits/models #run pytest -CMD ["pytest"] +#CMD ["pytest"] +ENV FLASK_APP=api/app2.py +CMD ["flask","run","--host=0.0.0.0"] #run python script to train model -ENTRYPOINT ["python","exp.py"] \ No newline at end of file +# ENTRYPOINT ["python","exp.py"] diff --git a/docker_run.sh b/docker_run.sh index d7727d7..d99fe73 100644 --- a/docker_run.sh +++ b/docker_run.sh @@ -1,12 +1,3 @@ -docker build -t mlops_assignment_4:v1 -f docker/Dockerfile . -echo "=======================================BEFORE EXECUTING CONTAINERS RUN================================================" -ls -lh models -echo "======================================================================================================================" - -docker run -v /mnt/c/Users/soura/Desktop/ML-OPS/Digits-Classification/models:/digits/models mlops_assignment_4:v1 --total_run 1 --dev_size 0.3 --test_size 0.2 --model_type 'svm' -docker run -v /mnt/c/Users/soura/Desktop/ML-OPS/Digits-Classification/models:/digits/models mlops_assignment_4:v1 --total_run 1 --dev_size 0.3 --test_size 0.2 --model_type 'tree' - -echo "=======================================AFTER EXECUTING CONTAINERS RUN================================================" -ls -lh models -echo "======================================================================================================================" +docker build -t flask_app . --no-cache +docker run --mount source=saved_model,destination=/app/saved_model flask_app \ No newline at end of file diff --git a/flask_app.py b/flask_app.py new file mode 100644 index 0000000..6f5df31 --- /dev/null +++ b/flask_app.py @@ -0,0 +1,41 @@ +import pickle +from flask import Flask, request, jsonify +import numpy as np +from PIL import Image +import json + +app = Flask("quiz4") + +with open("saved_model/mnist_classification_svm.pkl", "rb") as f: + model = pickle.load(f) + +def preprocess(image_file): + with Image.open(image_file) as img: + img = img.convert('L') + img = img.resize((8, 8), Image.Resampling.LANCZOS) + image_array = np.array(img, dtype=np.float64) + image_array = image_array.reshape(1, -1) + image_array *= (16 / 255) + return image_array + +def predict_digit(image): + prediction = model.predict(image) + return prediction + +@app.route('/test', methods=['GET', 'POST']) +def test(): + print("test") + return jsonify({'test': 'hit'}) + +@app.route('/predict', methods=['POST']) +def predict(): + try: + image = request.files['image'] + digit = predict_digit(preprocess(image))[0] + + return json.dumps({'prediction': int(digit)}) + except Exception as e: + return jsonify({'error': str(e)}) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file diff --git a/image1.jpg b/image1.jpg new file mode 100644 index 0000000..1b4261f Binary files /dev/null and b/image1.jpg differ diff --git a/image2.png b/image2.png new file mode 100644 index 0000000..d2b18bd Binary files /dev/null and b/image2.png differ diff --git a/mnist_classification.py b/mnist_classification.py new file mode 100644 index 0000000..3d59e67 --- /dev/null +++ b/mnist_classification.py @@ -0,0 +1,31 @@ +import numpy as np +from sklearn import datasets +from sklearn.model_selection import train_test_split +from sklearn import svm +from sklearn import metrics +import pickle + +digits = datasets.load_digits() + +n_samples = len(digits.images) +data = digits.images.reshape((n_samples, -1)) + +X_train, X_test, y_train, y_test = train_test_split(data, digits.target, test_size=0.2, random_state=42) + +clf = svm.SVC(kernel='linear') + +clf.fit(X_train, y_train) + +y_pred = clf.predict(X_test) + +accuracy = metrics.accuracy_score(y_test, y_pred) +print(f"Accuracy: {accuracy * 100:.2f}%") + +print("Classification Report:") +print(metrics.classification_report(y_test, y_pred)) + +print("Confusion Matrix:") +print(metrics.confusion_matrix(y_test, y_pred)) + +with open("./saved_model/mnist_classification_svm.pkl", "wb") as f: + pickle.dump(clf, f) \ No newline at end of file diff --git a/models/svm_gamma:0.0001_C:0.1.joblib b/models/svm_gamma:0.0001_C:0.1.joblib new file mode 100644 index 0000000..04b9083 Binary files /dev/null and b/models/svm_gamma:0.0001_C:0.1.joblib differ diff --git a/models/svm_gamma:0.0001_C:0.5.joblib b/models/svm_gamma:0.0001_C:0.5.joblib new file mode 100644 index 0000000..0d7c9d6 Binary files /dev/null and b/models/svm_gamma:0.0001_C:0.5.joblib differ diff --git a/models/svm_gamma:0.0001_C:1.joblib b/models/svm_gamma:0.0001_C:1.joblib new file mode 100644 index 0000000..efbdce8 Binary files /dev/null and b/models/svm_gamma:0.0001_C:1.joblib differ diff --git a/models/svm_gamma:0.001_C:0.1.joblib b/models/svm_gamma:0.001_C:0.1.joblib new file mode 100644 index 0000000..ae5162e Binary files /dev/null and b/models/svm_gamma:0.001_C:0.1.joblib differ diff --git a/models/svm_gamma:0.001_C:0.5.joblib b/models/svm_gamma:0.001_C:0.5.joblib new file mode 100644 index 0000000..0600c50 Binary files /dev/null and b/models/svm_gamma:0.001_C:0.5.joblib differ diff --git a/models/svm_gamma:0.001_C:1.joblib b/models/svm_gamma:0.001_C:1.joblib new file mode 100644 index 0000000..3024463 Binary files /dev/null and b/models/svm_gamma:0.001_C:1.joblib differ diff --git a/models/svm_gamma:0.005_C:0.1.joblib b/models/svm_gamma:0.005_C:0.1.joblib new file mode 100644 index 0000000..dec4181 Binary files /dev/null and b/models/svm_gamma:0.005_C:0.1.joblib differ diff --git a/models/svm_gamma:0.005_C:0.5.joblib b/models/svm_gamma:0.005_C:0.5.joblib new file mode 100644 index 0000000..ff94095 Binary files /dev/null and b/models/svm_gamma:0.005_C:0.5.joblib differ diff --git a/models/svm_gamma:0.005_C:1.joblib b/models/svm_gamma:0.005_C:1.joblib new file mode 100644 index 0000000..42fb89e Binary files /dev/null and b/models/svm_gamma:0.005_C:1.joblib differ diff --git a/models/svm_gamma:0.01_C:0.1.joblib b/models/svm_gamma:0.01_C:0.1.joblib new file mode 100644 index 0000000..9e878b0 Binary files /dev/null and b/models/svm_gamma:0.01_C:0.1.joblib differ diff --git a/models/svm_gamma:0.01_C:0.5.joblib b/models/svm_gamma:0.01_C:0.5.joblib new file mode 100644 index 0000000..b28edb1 Binary files /dev/null and b/models/svm_gamma:0.01_C:0.5.joblib differ diff --git a/models/svm_gamma:0.01_C:1.joblib b/models/svm_gamma:0.01_C:1.joblib new file mode 100644 index 0000000..0808726 Binary files /dev/null and b/models/svm_gamma:0.01_C:1.joblib differ diff --git a/models/tree_max_depth:10.joblib b/models/tree_max_depth:10.joblib new file mode 100644 index 0000000..94e97f0 Binary files /dev/null and b/models/tree_max_depth:10.joblib differ diff --git a/models/tree_max_depth:20.joblib b/models/tree_max_depth:20.joblib new file mode 100644 index 0000000..9e1b426 Binary files /dev/null and b/models/tree_max_depth:20.joblib differ diff --git a/models/tree_max_depth:5.joblib b/models/tree_max_depth:5.joblib new file mode 100644 index 0000000..70a4b53 Binary files /dev/null and b/models/tree_max_depth:5.joblib differ diff --git a/models/tree_max_depth:50.joblib b/models/tree_max_depth:50.joblib new file mode 100644 index 0000000..101ab31 Binary files /dev/null and b/models/tree_max_depth:50.joblib differ diff --git a/requirements.txt b/requirements.txt index ed96694..d0ec7bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ -Flask==3.0.0 +matplotlib==3.7.2 +pandas==2.1.1 matplotlib==3.7.2 scikit-learn==1.3.0 pytest==7.4.2 -pandas==2.1.1 \ No newline at end of file +flask==3.0.0 \ No newline at end of file diff --git a/saved_model/mnist_classification_svm.pkl b/saved_model/mnist_classification_svm.pkl new file mode 100644 index 0000000..59555b2 Binary files /dev/null and b/saved_model/mnist_classification_svm.pkl differ diff --git a/test_flask_app.py b/test_flask_app.py new file mode 100644 index 0000000..4dfda68 --- /dev/null +++ b/test_flask_app.py @@ -0,0 +1,39 @@ +import unittest +from flask import Flask +from flask_app import app # Import your Flask app instance +import json +import requests +import io +import logging +import tensorflow as tf +from PIL import Image + + +app = app.test_client() + +def convert_to_image(array): + image = Image.fromarray((array * 255).astype('uint8'), 'L') + img_byte_arr = io.BytesIO() + image.save(img_byte_arr, format='PNG') + img_byte_arr = img_byte_arr.getvalue() + return img_byte_arr + +def test_post_predict(): + + mnist = tf.keras.datasets.mnist + (_, _), (x_test, y_test) = mnist.load_data() + + for digit in range(10): + index = list(y_test).index(digit) + sample_data = x_test[index] + + image_data = convert_to_image(sample_data) + + response = app.post("/predict", data={'image': (io.BytesIO(image_data), f'{digit}.png')}) + predicted_digit = json.loads(response.text)['prediction'] + + assert response.status_code == 200 + try: + assert predicted_digit == digit + except Exception as e: + print(f"predicted_digit: {predicted_digit}\t digit: {digit}") \ No newline at end of file