├── _config.yml ├── docs ├── _config.yml ├── data │ ├── images │ │ ├── labeling.png │ │ ├── cloud_data.png │ │ ├── output_3_0.png │ │ ├── output_3_1.png │ │ ├── output_3_2.png │ │ ├── output_3_3.png │ │ ├── output_3_4.png │ │ ├── create_s3_bucket.png │ │ ├── label_main_page.png │ │ └── labeling_interface.png │ └── index.md ├── training │ ├── images │ │ ├── crnn.png │ │ ├── cometml_log.png │ │ ├── output_48_1.png │ │ └── cometml_api_key.png │ └── index.md ├── problem │ ├── images │ │ ├── problem.png │ │ └── service.png │ └── index.md ├── deployment │ ├── images │ │ └── gradio.png │ └── index.md ├── kubeflow │ ├── images │ │ ├── kubeflow.png │ │ └── pipeline.png │ └── index.md ├── overview │ ├── images │ │ └── ml_system.png │ └── index.md ├── terraform │ └── index.md ├── pipeline │ └── index.md └── index.md ├── mlpipeline ├── components │ ├── preprocess │ │ ├── Dockerfile │ │ ├── build_image.sh │ │ ├── component.yaml │ │ └── src │ │ │ └── main.py │ ├── train │ │ ├── build_image.sh │ │ ├── component.yaml │ │ ├── Dockerfile │ │ └── src │ │ │ └── main.py │ └── deployment │ │ ├── build_image.sh │ │ ├── component.yaml │ │ ├── Dockerfile │ │ └── src │ │ └── main.py ├── pipeline.py └── mlpipeline.yaml └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /mlpipeline/components/preprocess/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | COPY ./src /src 3 | -------------------------------------------------------------------------------- /docs/data/images/labeling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/data/images/labeling.png -------------------------------------------------------------------------------- /docs/training/images/crnn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/training/images/crnn.png -------------------------------------------------------------------------------- /docs/data/images/cloud_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/data/images/cloud_data.png -------------------------------------------------------------------------------- /docs/data/images/output_3_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/data/images/output_3_0.png -------------------------------------------------------------------------------- /docs/data/images/output_3_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/data/images/output_3_1.png -------------------------------------------------------------------------------- /docs/data/images/output_3_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/data/images/output_3_2.png -------------------------------------------------------------------------------- /docs/data/images/output_3_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/data/images/output_3_3.png -------------------------------------------------------------------------------- /docs/data/images/output_3_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/data/images/output_3_4.png -------------------------------------------------------------------------------- /docs/problem/images/problem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/problem/images/problem.png -------------------------------------------------------------------------------- /docs/problem/images/service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/problem/images/service.png -------------------------------------------------------------------------------- /docs/deployment/images/gradio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/deployment/images/gradio.png -------------------------------------------------------------------------------- /docs/kubeflow/images/kubeflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/kubeflow/images/kubeflow.png -------------------------------------------------------------------------------- /docs/kubeflow/images/pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/kubeflow/images/pipeline.png -------------------------------------------------------------------------------- /docs/overview/images/ml_system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/overview/images/ml_system.png -------------------------------------------------------------------------------- /docs/data/images/create_s3_bucket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/data/images/create_s3_bucket.png -------------------------------------------------------------------------------- /docs/data/images/label_main_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/data/images/label_main_page.png -------------------------------------------------------------------------------- /docs/training/images/cometml_log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/training/images/cometml_log.png -------------------------------------------------------------------------------- /docs/training/images/output_48_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/training/images/output_48_1.png -------------------------------------------------------------------------------- /docs/data/images/labeling_interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/data/images/labeling_interface.png -------------------------------------------------------------------------------- /docs/training/images/cometml_api_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanhhau097/mlpipeline/HEAD/docs/training/images/cometml_api_key.png -------------------------------------------------------------------------------- /mlpipeline/components/train/build_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | full_image_name=thanhhau097/ocrtrain 4 | 5 | docker build -t "${full_image_name}" . 6 | docker push "$full_image_name" -------------------------------------------------------------------------------- /mlpipeline/components/preprocess/build_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | full_image_name=thanhhau097/ocrpreprocess 4 | 5 | docker build -t "${full_image_name}" . 6 | docker push "$full_image_name" -------------------------------------------------------------------------------- /mlpipeline/components/deployment/build_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | full_image_name=thanhhau097/ocrdeployment:v1.0 4 | 5 | docker build -t "${full_image_name}" . 6 | docker push "$full_image_name" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Machine Learning Pipeline System 2 | Hướng dẫn xây dựng hệ thống trí tuệ nhân tạo/học máy dành cho các kĩ sư học máy. 3 | 4 | Blog: [thanhhau097.github.io/mlpipeline](https://thanhhau097.github.io/mlpipeline) -------------------------------------------------------------------------------- /docs/terraform/index.md: -------------------------------------------------------------------------------- 1 | ## Terraform 2 | 3 | -------------------------------------------------------------------------------- /docs/pipeline/index.md: -------------------------------------------------------------------------------- 1 | ## Machine Learning Pipeline 2 | 3 | 4 | -------------------------------------------------------------------------------- /mlpipeline/components/preprocess/component.yaml: -------------------------------------------------------------------------------- 1 | name: OCR Preprocessing 2 | description: Preprocess OCR model 3 | 4 | outputs: 5 | - {name: output_path, type: String, description: 'Output Path'} 6 | 7 | implementation: 8 | container: 9 | image: thanhhau097/ocrpreprocess 10 | command: [ 11 | python, /src/main.py, 12 | ] 13 | 14 | fileOutputs: 15 | output_path: /output_path.txt # path to s3 16 | -------------------------------------------------------------------------------- /mlpipeline/components/train/component.yaml: -------------------------------------------------------------------------------- 1 | name: OCR Train 2 | description: Training OCR model 3 | inputs: 4 | - {name: data_path, description: 's3 path to data'} 5 | outputs: 6 | - {name: output_path, type: String, description: 'Output Path'} 7 | 8 | implementation: 9 | container: 10 | image: thanhhau097/ocrtrain 11 | command: [ 12 | python, /src/main.py, 13 | --data_path, {inputValue: data_path}, 14 | ] 15 | 16 | fileOutputs: 17 | output_path: /output_path.txt -------------------------------------------------------------------------------- /mlpipeline/components/deployment/component.yaml: -------------------------------------------------------------------------------- 1 | name: OCR Deployment 2 | description: Deploy OCR model 3 | inputs: 4 | - {name: model_path, description: 's3 path to model'} 5 | outputs: 6 | - {name: output_path, type: String, description: 'Output Path'} 7 | 8 | implementation: 9 | container: 10 | image: thanhhau097/ocrdeployment:v1.0 11 | command: [ 12 | python, /src/main.py, 13 | --model_path, {inputValue: model_path}, 14 | ] 15 | 16 | fileOutputs: 17 | output_path: /output_path.txt # path to s3 18 | -------------------------------------------------------------------------------- /mlpipeline/components/train/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | # Install some common dependencies 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | RUN apt-get update -qqy && \ 5 | apt-get install -y \ 6 | build-essential \ 7 | cmake \ 8 | g++ \ 9 | git \ 10 | libsm6 \ 11 | libxrender1 \ 12 | locales \ 13 | libssl-dev \ 14 | pkg-config \ 15 | poppler-utils \ 16 | python3-dev \ 17 | python3-pip \ 18 | python3.6 \ 19 | software-properties-common \ 20 | && \ 21 | apt-get clean && \ 22 | apt-get autoremove && \ 23 | rm -rf /var/lib/apt/lists/* 24 | 25 | # Set default python version to 3.6 26 | RUN ln -sf /usr/bin/python3 /usr/bin/python 27 | RUN ln -sf /usr/bin/pip3 /usr/bin/pip 28 | RUN pip3 install -U pip setuptools 29 | 30 | # locale setting 31 | RUN locale-gen en_US.UTF-8 32 | ENV LC_ALL=en_US.UTF-8 \ 33 | LANG=en_US.UTF-8 \ 34 | LANGUAGE=en_US.UTF-8 \ 35 | PYTHONIOENCODING=utf-8 36 | 37 | RUN pip install torch numpy comet_ml boto3 requests opencv-python 38 | RUN pip install flask 39 | RUN apt update && apt install ffmpeg libsm6 libxext6 -y 40 | COPY ./src /src -------------------------------------------------------------------------------- /mlpipeline/components/deployment/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | # Install some common dependencies 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | RUN apt-get update -qqy && \ 5 | apt-get install -y \ 6 | build-essential \ 7 | cmake \ 8 | g++ \ 9 | git \ 10 | libsm6 \ 11 | libxrender1 \ 12 | locales \ 13 | libssl-dev \ 14 | pkg-config \ 15 | poppler-utils \ 16 | python3-dev \ 17 | python3-pip \ 18 | python3.6 \ 19 | software-properties-common \ 20 | && \ 21 | apt-get clean && \ 22 | apt-get autoremove && \ 23 | rm -rf /var/lib/apt/lists/* 24 | 25 | # Set default python version to 3.6 26 | RUN ln -sf /usr/bin/python3 /usr/bin/python 27 | RUN ln -sf /usr/bin/pip3 /usr/bin/pip 28 | RUN pip3 install -U pip setuptools 29 | 30 | # locale setting 31 | RUN locale-gen en_US.UTF-8 32 | ENV LC_ALL=en_US.UTF-8 \ 33 | LANG=en_US.UTF-8 \ 34 | LANGUAGE=en_US.UTF-8 \ 35 | PYTHONIOENCODING=utf-8 36 | 37 | RUN pip3 install torch numpy comet_ml boto3 requests opencv-python 38 | RUN pip3 install flask 39 | RUN apt update && apt install ffmpeg libsm6 libxext6 -y 40 | COPY ./src /src -------------------------------------------------------------------------------- /mlpipeline/pipeline.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import kfp 4 | from kfp.aws import use_aws_secret 5 | import kfp.dsl as dsl 6 | from kfp import components 7 | import json 8 | 9 | logging.basicConfig(level=logging.INFO, format='%(name)s - %(levelname)s - %(message)s') 10 | 11 | 12 | def create_preprocess_component(): 13 | component = kfp.components.load_component_from_file('./components/preprocess/component.yaml') 14 | return component 15 | 16 | def create_train_component(): 17 | component = kfp.components.load_component_from_file('./components/train/component.yaml') 18 | return component 19 | 20 | def mlpipeline(): 21 | preprocess_component = create_preprocess_component() 22 | preprocess_task = preprocess_component() 23 | 24 | train_component = create_train_component() 25 | train_task = train_component(data_path=preprocess_task.outputs['output_path']) 26 | 27 | deployment_op = components.load_component_from_url('https://raw.githubusercontent.com/kubeflow/pipelines/master/components/kubeflow/kfserving/component.yaml') 28 | container_spec = {"image": "thanhhau097/ocrdeployment", "port":5000, "name": "custom-container", "command": "python3 /src/main.py --model_s3_path {}".format(train_task.outputs['output_path'])} 29 | container_spec = json.dumps(container_spec) 30 | deployment_op( 31 | action='apply', 32 | model_name='custom-simple', 33 | custom_model_spec=container_spec, 34 | namespace='kubeflow-user', 35 | watch_timeout="1800" 36 | ) 37 | 38 | kfp.compiler.Compiler().compile(mlpipeline, 'mlpipeline.yaml') -------------------------------------------------------------------------------- /docs/problem/index.md: -------------------------------------------------------------------------------- 1 | # Bài toán 2 | Trong blog này, chúng ta sẽ xây dựng một hệ thống học máy cho bài toán đọc hình ảnh chữ viết tay. 3 | 4 | ## Phân tích bài toán 5 | ![alt text](./images/service.png "Bài toán") 6 | - **Bài toán**: Chuyển đổi hình ảnh chữ viết thành dạng văn bản, ứng dụng trên điện thoại di động, máy tính bảng để người dùng có thể viết thay vì gõ phím. 7 | 8 | - **Dữ liệu**: 9 | - Bộ dữ liệu hình ảnh chữ viết [CinnamonOCR](https://drive.google.com/drive/folders/1Qa2YA6w6V5MaNV-qxqhsHHoYFRK5JB39) 10 | - Dữ liệu công khai (public) và dữ liệu tổng hợp (synthetic data) 11 | 12 | - **Phương pháp đánh giá**: 13 | - Độ chính xác của mô hình > 90% 14 | - Thời gian dự đoán cho mỗi hình ảnh < 0.1s 15 | 16 | - **Yêu cầu khác**: 17 | - Có thể ứng dụng trực tiếp trên điện thoại hoặc thông qua API 18 | 19 | ## Thiết kế 20 | ![alt text](./images/problem.png "Hệ thống học máy") 21 | 22 | Đây là thiết kế đơn giản cho hệ thống mà chúng ta cần xây dựng. Hệ thống nhận dữ liệu, xử lý và trả về API để người dùng có thể sử dụng. Chúng ta sẽ lưu trữ dữ liệu người dùng vào cơ sở dữ liệu nhằm mục đích cải thiện mô hình trong tương lai. 23 | 24 | Bài trước: [Tổng quan về một hệ thống học máy (Machine Learning System)](../overview/index.md) 25 | 26 | Bài tiếp theo: [Lưu trữ và gán nhãn dữ liệu](../data/index.md) 27 | 28 | [Về Trang chủ](../index.md) 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ## Machine Learning Engineering Tutorial 2 | Hướng dẫn xây dựng hệ thống trí tuệ nhân tạo/học máy dành cho các kĩ sư học máy. 3 | 4 | ### 1. Giới thiệu 5 | Trong blog này, chúng ta sẽ cùng nhau xây dựng một phần mềm ứng dụng các mô hình học máy trong thực tế, bằng cách sử dụng các công cụ mã nguồn mở. Đây là blog hướng dẫn chi tiết dành cho các bạn muốn theo con đường làm một kĩ học máy (Machine Learning Engineer), cũng như các kĩ sư phần mềm muốn ứng dụng các mô hình học máy vào sản phẩm của mình. 6 | 7 | ### 2. Mục lục 8 | 1. [Tổng quan về một hệ thống học máy (Machine Learning System)](./overview/index.md) 9 | 2. [Phân tích yêu cầu bài toán](./problem/index.md) 10 | 3. [Lưu trữ và gán nhãn dữ liệu](./data/index.md) 11 | 4. [Huấn luyện mô hình học máy](./training/index.md) 12 | 5. [Triển khai và áp dụng mô hình học máy](./deployment/index.md) 13 | 6. [Xây dựng hệ thống huấn luyện mô hình bằng Kubeflow](./kubeflow/index.md) 14 | 7. [Kết nối các thành phần của hệ thống học máy](./pipeline/index.md) (đang cập nhật) 15 | 8. [Xây dựng cơ sở hạ tầng cho mô hình học máy bằng Terraform](./terraform/index.md) (đang cập nhật) 16 | 17 | ### 3. Công cụ 18 | Trong blog này, chúng ta sẽ sử dụng các công nghệ và các công cụ được áp dụng trong các dự án học máy thực tế, vì vậy người đọc cần có kiến thức cơ bản về phát triển phần mềm và các công cụ phát triển phần mềm. 19 | 20 | Trong blog, chúng ta sẽ sử dụng các công cụ và thư viện sau: 21 | 1. Cơ bản: Python, Git, Pytorch, AWS, DVC, Docker, Kubernetes 22 | 2. Các thư viện mã nguồn mở có sẵn: Label Studio, Cometml, Kubeflow, KFServing. 23 | 24 | Trong blog này, chúng ta sẽ cùng nhau sử dụng các công cụ được liệt kê ở trên để xây dựng một hệ thống machine learning hoàn chỉnh. 25 | 26 | ### 4. Đóng góp và ý kiến 27 | Nếu có bất kì ý kiến đóng góp và sửa đổi nào, bạn vui lòng tạo *issues* và *pull requests*. Cảm ơn các bạn đã quan tâm! 28 | 29 | ### Về tác giả 30 | **Tác giả:** Nguyễn Thành Hậu 31 | 32 | **Email:** thanhhau097@gmail.com 33 | 34 | -------------------------------------------------------------------------------- /mlpipeline/components/preprocess/src/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from pathlib import Path 4 | 5 | 6 | os.environ["AWS_ACCESS_KEY_ID"] = "**************************" 7 | os.environ["AWS_SECRET_ACCESS_KEY"] = "**************************" 8 | 9 | # download data from label studio 10 | print("DOWNLOADING DATA") 11 | import requests 12 | 13 | url = "https://***********.ngrok.io/api/projects/1/export" 14 | querystring = {"exportType":"JSON"} 15 | headers = { 16 | 'authorization': "Token ******************************", 17 | } 18 | response = requests.request("GET", url, headers=headers, params=querystring) 19 | 20 | # TODO: split train/val/test/user_data label from this json file 21 | user_data = [] 22 | train_data = [] 23 | val_data = [] 24 | test_data = [] 25 | for item in response.json(): 26 | if 's3://ocrpipeline/data/user_data' in item['data']['captioning']: 27 | user_data.append(item) 28 | elif 's3://ocrpipeline/data/train' in item['data']['captioning']: 29 | train_data.append(item) 30 | elif 's3://ocrpipeline/data/test' in item['data']['captioning']: 31 | test_data.append(item) 32 | elif 's3://ocrpipeline/data/validation' in item['data']['captioning']: 33 | val_data.append(item) 34 | 35 | def save_labels(folder, data): 36 | if not os.path.exists(folder): 37 | os.makedirs(folder, exist_ok=True) 38 | label_path = os.path.join(folder, 'label_studio_data.json') 39 | with open(label_path, 'w') as f: 40 | json.dump(data, f) 41 | 42 | save_labels('./data/train/', train_data) 43 | save_labels('./data/test/', test_data) 44 | save_labels('./data/validation/', val_data) 45 | save_labels('./data/user_data/', user_data) 46 | 47 | # preprocess 48 | print("PREPROCESSING DATA") 49 | def convert_label_studio_format_to_ocr_format(label_studio_json_path, output_path): 50 | with open(label_studio_json_path, 'r') as f: 51 | data = json.load(f) 52 | 53 | ocr_data = {} 54 | 55 | for item in data: 56 | image_name = os.path.basename(item['data']['captioning']) 57 | 58 | text = '' 59 | for value_item in item['annotations'][0]['result']: 60 | if value_item['from_name'] == 'caption': 61 | text = value_item['value']['text'][0] 62 | ocr_data[image_name] = text 63 | 64 | with open(output_path, 'w') as f: 65 | json.dump(ocr_data, f, indent=4) 66 | 67 | print('Successfully converted ', label_studio_json_path) 68 | convert_label_studio_format_to_ocr_format('./data/train/label_studio_data.json', './data/train/labels.json') 69 | convert_label_studio_format_to_ocr_format('./data/validation/label_studio_data.json', './data/validation/labels.json') 70 | convert_label_studio_format_to_ocr_format('./data/test/label_studio_data.json', './data/test/labels.json') 71 | convert_label_studio_format_to_ocr_format('./data/user_data/label_studio_data.json', './data/user_data/labels.json') 72 | 73 | # upload these file to s3 74 | print("UPLOADING DATA") 75 | import logging 76 | import boto3 77 | from botocore.exceptions import ClientError 78 | 79 | 80 | def upload_file_to_s3(file_name, bucket, object_name=None): 81 | """Upload a file to an S3 bucket 82 | 83 | :param file_name: File to upload 84 | :param bucket: Bucket to upload to 85 | :param object_name: S3 object name. If not specified then file_name is used 86 | :return: True if file was uploaded, else False 87 | """ 88 | 89 | # If S3 object_name was not specified, use file_name 90 | if object_name is None: 91 | object_name = file_name 92 | 93 | # Upload the file 94 | s3_client = boto3.client('s3') 95 | try: 96 | response = s3_client.upload_file(file_name, bucket, object_name) 97 | except ClientError as e: 98 | logging.error(e) 99 | return False 100 | return True 101 | 102 | 103 | upload_file_to_s3('data/train/labels.json', bucket='ocrpipeline', object_name='data/train/labels.json') 104 | upload_file_to_s3('data/validation/labels.json', bucket='ocrpipeline', object_name='data/validation/labels.json') 105 | upload_file_to_s3('data/test/labels.json', bucket='ocrpipeline', object_name='data/test/labels.json') 106 | upload_file_to_s3('data/user_data/labels.json', bucket='ocrpipeline', object_name='data/user_data/labels.json') 107 | 108 | data_path = 's3://ocrpipeline/data' 109 | Path('/output_path.txt').write_text(data_path) -------------------------------------------------------------------------------- /docs/overview/index.md: -------------------------------------------------------------------------------- 1 | # Tổng quan về một hệ thống học máy 2 | Trí tuệ nhân tạo đang là một trong những lĩnh vực đang được chú trọng phát triển và ứng dụng rất nhiều trong thời gian gần đây, trong mọi lĩnh vực từ nông nghiệp, công nghiệp đến dịch vụ, và nó đã mang lại nhiều lợi ích cho các doanh nghiệp nhằm giúp tối ưu hoá quy trình cũng như cắt giảm chi phí. 3 | 4 | Để ứng dụng được các nghiên cứu về trí tuệ nhân tạo vào thực tế, các kỹ sư cần phải biết cách thiết kế và xây dựng một hệ thống học máy đảm bảo được các yêu cầu về kinh tế cũng như kĩ thuật, nhằm mang lại sản phẩm tối ưu nhất để ứng dụng cho các doanh nghiệp, tổ chức. 5 | 6 | ## Quy trình của một hệ thống học máy 7 | ![alt text](./images/ml_system.png "Hệ thống học máy") 8 | 9 | ### 1. Xây dựng yêu cầu cho hệ thống 10 | 11 | Để xây dựng một hệ thống học máy, trước tiên chúng ta cần phải phân tích yêu cầu của hệ thống. 12 | 13 | - **Bài toán**: Chúng ta cần xác định vấn đề đặt ra của bài toán mà chúng ta cần giải quyết, mục tiêu của bài toán là gì, có thể là giảm chi phí con người, tăng năng suất lao động bằng cách tự động hoá một phần quy trình hiện tại. 14 | 15 | - **Dữ liệu**: Dữ liệu đầu vào của bài toán là gì? Chúng ta có thể sử dụng những loại dữ liệu nào (hình ảnh, âm thanh, ngôn ngữ, cơ sở dữ liệu) để làm đầu vào cho bài toán cần giải quyết? Số lượng dữ liệu mà chúng ta có hoặc cần có để huấn luyện các mô hình học máy. 16 | 17 | - **Phương pháp đánh giá**: Chúng ta cần đưa ra cách đánh giá cho hệ thống mà chúng ta thiết kế. Cách đánh giá có thể là độ chính xác của mô hình, thời gian xử lý của mô hình. Chúng ta cần phải có một cách đánh giá có thể đo lường được bằng con số cụ thể. 18 | 19 | - **Yêu cầu khác**: Ngoài ra chúng ta cần phải xem xét các ràng buộc về thời gian phát triển hệ thống, về tài nguyên mà chúng ta có thể sử dụng để phát triển và triển khai mô hình, ví dụ chúng ta phải triển khai trên mobile thay vì triển khai trên hệ thống có GPU. Tất cả những yêu cầu này cần được xem xét trước khi thực hiện việc xây dựng hệ thống. 20 | 21 | ### 2. Dữ liệu 22 | 23 | Dữ liệu mà một thành phần quan trọng và tất nhiên không thể thiếu để xây dựng một hệ thống. Trong thực tế, phần lớn thời gian phát triển hệ thống học máy tập trung vào việc thu thập, phân tích, gán nhãn và làm sạch dữ liệu. Ngoài việc phân loại và thu thập dữ liệu cần có, chúng ta cũng cần phải quan tâm đến một số vấn đề như: 24 | - **Lưu trữ dữ liệu**: chúng ta cần phải tìm hiểu cách mà chúng ta lưu trữ dữ liệu, dữ liệu ở lưu cục bộ, hay lưu đám mây (cloud) hay trực tiếp trên các thiết bị (on-device). Chúng ta cũng cần phải đảm bảo dữ liệu được lưu trữ một cách an toàn. 25 | - **Gán nhãn dữ liệu**: để có một mô hình học máy tốt, chúng ta cần phải đảm bảo dữ liệu được gán nhãn một cách đồng nhất và đảm bảo được chất lượng cũng như thời gian gán nhãn. Ngoài việc tự gán nhãn bằng cách sử dụng nội bộ, chúng ta có thể thuê công ty gán nhãn dữ liệu thực hiện việc này, hoặc sử dụng các nền tảng crowdsource labeling để thực hiện việc gán nhãn dữ liệu. 26 | - **Data Versioning**: để đảm bảo việc phát triển các mô hình học máy, chúng ta cần thực hiện đánh giá các mô hình trên cùng một phiên bản của dữ liệu. Thêm vào đó, dữ liệu cũng có thể được cập nhật liên tục theo thời gian, vì vậy việc đánh nhãn cho các phiên bản dữ liệu là một công việc cần thiết. 27 | 28 | ### 3. Xây dựng và tối ưu mô hình học máy 29 | 30 | 31 | Khi tiếp cận với một bài toán học máy, chúng ta cần phải xem xét và phân loại bài toán này để sử dụng mô hình học máy phù hợp với bài toán. Chúng ta cần phải phân tích các thành phần hoặc các bước cần phải thực hiện để có thể giải quyết bài toán, liệu chúng ta chỉ cần sử dụng một thuật toán/mô hình hay cần phải kết hợp các thuật toán khác nhau. 32 | 33 | - **Lựa chọn mô hình**: Thông thường để bắt đầu giải quyết một bài toán, chúng ta có thể xây dựng các thuật toán đơn giản để đánh giá kết quả và làm `baseline`, sau đó chúng ta có thể dần đi đến những phương pháp phức tạp hơn. 34 | - **Huấn luyện mô hình**: khi huấn luyện các mô hình học máy, chúng ta cần phải quan tâm đến các yếu tố ảnh hưởng đến chất lượng và kết quả của mô hình như chất lượng của dữ liệu, kiến trúc/thuật toán, các siêu tham số mà chúng ta sử dụng. Từ đó chúng ta có thể phân tích và cải thiện kết quả của các mô hình dựa trên các yếu tố này. 35 | - **Quản lý thí nghiệm**: Để có thể huấn luyện các mô hình học máy một cách hiệu quả, chúng ta cần phải quản lý và phân tích các thí nghiệm và kết quả của nó để có đánh giá và tìm phương pháp cải thiện. Chúng ta có thể sử dụng các thư viện để quản lý thí nghiệm như Tensorboard, Cometml, Weights & Bias,... 36 | - **Tối ưu siêu tham số**: Khi huấn luyện các mô hình, đặc biệt là các mô hình học sâu, các siêu tham số (hyper-parameters) có ảnh hưởng rất nhiều đến chất lượng của mô hình. Chúng ta có thể sử dụng các phương pháp và thuật toán để tìm ra siêu tham số tốt nhất ví dụ như: Bayes, grid search, random search,... 37 | 38 | ### 4. Triển khai mô hình 39 | Sau khi huấn luyện mô hình, chúng ta cần triển khai mô hình này vào thực tế. Có nhiều yêu cầu cũng như môi trường khác nhau mà chúng ta cần phải triển khai tuỳ thuộc vào bài toán như: triển khai trên mobile thông qua các ứng dụng, triển khai mô hình trên nền tảng đám mây và cung cấp API cho người dùng, triển khai trên các máy của khách hàng bằng cách gửi các docker image, hoặc chúng ta cũng có thể triển khai thông qua các extensions, SDK,... 40 | 41 | ## Công cụ phát triển 42 | Ở trong hình quy trình của một hệ thống học máy ở đầu trang, chúng ta có thể lựa chọn các thành phần phù hợp cho từng bước trong quy trình. Trong blog này, chúng ta sẽ sử dụng các thư viện mã nguồn mở có sẵn và được sử dụng nhiều trong thực tế. 43 | 1. Lưu trữ dữ liệu: Amazon Simple Storage Service (S3) 44 | 2. Gán nhãn dữ liệu: Label Studio 45 | 3. Data and Model Versioning: Data Version Control (DVC) 46 | 4. Xây dựng và huấn luyện mô hình: Pytorch 47 | 5. Quản lý thí nghiệm và tối ưu siêu tham số: Cometml 48 | 6. ML Workflows: Kubeflow 49 | 7. Giao diện demo: Gradio 50 | 8. Triển khai mô hình: Nuclio 51 | 9. Xây dựng cơ sở hạ tầng: Terraform 52 | ## Tài liệu tham khảo 53 | 1. [Machine Learning Systems Design - Chip Huyen](https://github.com/chiphuyen/machine-learning-systems-design) 54 | 2. [Full Stack Deep Learning](https://fullstackdeeplearning.com/) 55 | 56 | Bài tiếp theo: [Phân tích yêu cầu bài toán](../problem/index.md) 57 | 58 | [Về Trang chủ](../index.md) 59 | 60 | -------------------------------------------------------------------------------- /docs/deployment/index.md: -------------------------------------------------------------------------------- 1 | # Triển khai mô hình học máy 2 | 3 | Trong các phần trước, chúng ta đã thực hiện việc huấn luyện mô hình và lưu trữ mô hình. Sau khi huấn luyện mô hình, chúng ta cần triển khai mô hình bằng cách áp dụng vào một ứng dụng web hoặc mobile, hoặc chúng ta có thể trả về kết quả thông qua API. Trong phần này, chúng ta sẽ sử dụng công cụ Gradio để xây dựng ứng dụng web và sử dụng Flask để xây dựng API. 4 | 5 | ## Ứng dụng web 6 | Gradio là thư viện giúp chúng ta có thể tạo một ứng dụng web đơn giản để demo cho mô hình học máy của mình chỉ với một số dòng code. Ngoài ra, Gradio còn cho phép chúng ta sửa đổi giao diện của ứng dụng tùy theo đầu vào và đầu ra của mô hình học máy. Các bạn có thể tìm hiểu têm về Gradio ở đây: https://gradio.app/ 7 | 8 | Đầu tiên chúng ta cần cài đặt thư viện Gradio: 9 | ``` 10 | pip install gradio 11 | ``` 12 | 13 | Với bài toán của chúng ta, đầu vào là ảnh và trả về kết quả là chuỗi kí tự, vì vậy ta khởi tạo giao diện như sau: 14 | 15 | ```python 16 | import gradio 17 | 18 | # load the best model 19 | model.load_state_dict(torch.load('best_model.pth')) 20 | model = model.to(device) 21 | interface = gradio.Interface(predict, "image", "text") 22 | 23 | interface.launch() 24 | ``` 25 | 26 | Truy cập vào đường dẫn đến ứng dụng web [http://127.0.0.1:7860/](http://127.0.0.1:7862/), và thực hiện tải lên một ảnh để dự đoán và nhấn submit, chúng ta có: 27 | 28 | ![alt text](./images/gradio.png "Ứng dụng web") 29 | 30 | Như vậy chúng ta đã xây dựng được một ứng dụng web đơn giản bằng Gradio để demo cho mô hình học máy của mình. 31 | 32 | ## Xây dựng API bằng Flask 33 | Trong phần này, chúng ta sẽ sử dụng `Flask` để viết API cho mô hình học máy của mình, các lập trình viên khác có thể sử dụng mô hình học máy của chúng ta bằng cách gọi đến API này để lấy kết quả. 34 | 35 | Quy trình xây dựng một API tương đối đơn giản, chúng ta thực hiện như sau: 36 | 37 | ```python 38 | import os 39 | from flask import Flask, flash, request, redirect, url_for 40 | from werkzeug.utils import secure_filename 41 | 42 | UPLOAD_FOLDER = './data/user_data/' 43 | ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) 44 | 45 | app = Flask(__name__) 46 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER 47 | app.secret_key = "super secret key" 48 | 49 | 50 | @app.route("/predict", methods=['POST']) 51 | def predict_flask(): 52 | if request.method == 'POST': 53 | # check if the post request has the file part 54 | if 'file' not in request.files: 55 | flash('No file part') 56 | return redirect(request.url) 57 | file = request.files['file'] 58 | # if user does not select file, browser also 59 | # submit a empty part without filename 60 | if file.filename == '': 61 | flash('No selected file') 62 | return redirect(request.url) 63 | if file: 64 | print('found file') 65 | filename = secure_filename(file.filename) 66 | filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) 67 | file.save(filepath) 68 | 69 | image = cv2.imread(filepath) 70 | output = predict(image) 71 | return output 72 | 73 | ``` 74 | 75 | Để thử nghiệm API, chúng ta thực hiện câu lệnh sau trong Terminal: 76 | ``` 77 | curl -i -X POST "http://0.0.0.0:5000/predict" -F file=@ 78 | ``` 79 | 80 | trong đó `filepath là đường dẫn đến dữ liệu ` 81 | 82 | 83 | Một công việc cần thiết chúng ta cần làm là lưu trữ dữ liệu mà người dùng tải lên nhằm mục đích phân tích độ chính xác của mô hình trên dữ liệu thật và cải thiện mô hình sau này. Để lưu trực tiếp lên AWS S3 thay vì lưu vào thư mục ở máy cá nhân, chúng ta cần sử dụng thư viện `boto3`. Hàm sử dụng để ubload lên `AWS S3` như sau: 84 | ```python 85 | import logging 86 | import boto3 87 | from botocore.exceptions import ClientError 88 | 89 | 90 | def upload_file_to_s3(file_name, bucket, object_name=None): 91 | """Upload a file to an S3 bucket 92 | 93 | :param file_name: File to upload 94 | :param bucket: Bucket to upload to 95 | :param object_name: S3 object name. If not specified then file_name is used 96 | :return: True if file was uploaded, else False 97 | """ 98 | 99 | # If S3 object_name was not specified, use file_name 100 | if object_name is None: 101 | object_name = file_name 102 | 103 | # Upload the file 104 | s3_client = boto3.client('s3') 105 | try: 106 | response = s3_client.upload_file(file_name, bucket, object_name) 107 | except ClientError as e: 108 | logging.error(e) 109 | return False 110 | return True 111 | ``` 112 | 113 | Để có thể upload hình ảnh mà người dùng tải lên `Gradio` hay gọi API, chúng ta sẽ sửa hàm `predict` thành như sau: 114 | ```python 115 | import uuid 116 | 117 | 118 | def predict(image): 119 | # upload image to aws s3 120 | filename = str(uuid.uuid4()) + '.png' 121 | cv2.imwrite(filename, image) 122 | upload_file_to_s3(filename, bucket='ocrpipeline', 'data/user_data/images') 123 | 124 | model.eval() 125 | batch = [{'image': image, 'label': [1]}] 126 | images = collate_wrapper(batch)[0] 127 | 128 | images = images.to(device) 129 | 130 | outputs = model(images) 131 | outputs = outputs.permute(1, 0, 2) 132 | output = outputs[0] 133 | 134 | out_best = list(torch.argmax(output, -1)) # [2:] 135 | out_best = [k for k, g in itertools.groupby(out_best)] 136 | pred_text = get_label_from_indices(out_best) 137 | 138 | return pred_text 139 | ``` 140 | 141 | Như vậy mỗi lần người dùng muốn tải lên một ảnh để lấy chuỗi kí tự, chúng ta có thể lưu trữ hình ảnh mà người dùng tải lên để thuận tiện cho việc phân tích và cải thiện mô hình. Ngoài ra, chúng ta cũng nên lưu lại dự đoán của mô hình để có thể đánh giá được độ chính xác của mô hình trên dữ liệu thật mà người dùng tải lên. Coi như đó là một bài tập nhỏ dành cho các bạn. 142 | 143 | Sau khi sửa hàm `predict`, chúng ta sẽ khởi tạo lại `Gradio Interface` hoặc `Flask API` như đã làm ở bước trước để có thể sử dụng hàm `predict` mới. Chúng ta có thể kiếm tra liệu hình ảnh đã được tải lên AWS S3 hay chưa bằng cách: 144 | ``` 145 | aws s3 ls s3://ocrpipeline/data/user_data/ 146 | ``` 147 | 148 | ## Tổng kết 149 | Trong bài này, chúng ta đã cùng nhau xây dựng một ứng dụng web và API cho mô hình học máy của mình, cũng như lưu trữ dữ liệu của người dùng tải lên để có thể cải thiện mô hình sau này. Trong các bài tiếp theo, chúng ta sẽ mở rộng và nâng cấp API bằng cách sử dụng thư viện Nuclio để có thể đảm bảo ứng dụng luôn hoạt động ổn định với số lượng lớn `request`. 150 | 151 | Bài trước: [Huấn luyện mô hình học máy](../training/index.md) 152 | 153 | Bài tiếp theo: [Xây dựng hệ thống huấn luyện mô hình bằng Kubeflow](../kubeflow/index.md) 154 | 155 | [Về Trang chủ](../index.md) 156 | 157 | -------------------------------------------------------------------------------- /mlpipeline/components/deployment/src/main.py: -------------------------------------------------------------------------------- 1 | from botocore.retries import bucket 2 | 3 | 4 | def deploy_model(model_s3_path: str): 5 | CHARACTERS = "aáàạảãăắằẳẵặâấầẩẫậbcdđeéèẹẻẽêếềệểễfghiíìịỉĩjklmnoóòọỏõôốồộổỗơớờợởỡpqrstuúùụủũưứừựửữvxyýỳỷỹỵzwAÁÀẠẢÃĂẮẰẲẴẶÂẤẦẨẪẬBCDĐEÉÈẸẺẼÊẾỀỆỂỄFGHIÍÌỊỈĨJKLMNOÓÒỌỎÕÔỐỒỘỔỖƠỚỜỢỞỠPQRSTUÚÙỤỦŨƯỨỪỰỬỮVXYÝỲỴỶỸZW0123456789 .,-/()'#+:" 6 | PAD_token = 0 # Used for padding short sentences 7 | SOS_token = 1 # Start-of-sentence token 8 | EOS_token = 2 # End-of-sentence token 9 | 10 | CHAR2INDEX = {"PAD": PAD_token, "SOS": SOS_token, "EOS": EOS_token} 11 | INDEX2CHAR = {PAD_token: "PAD", SOS_token: "SOS", EOS_token: "EOS"} 12 | 13 | for i, c in enumerate(CHARACTERS): 14 | CHAR2INDEX[c] = i + 3 15 | INDEX2CHAR[i + 3] = c 16 | 17 | def get_indices_from_label(label): 18 | indices = [] 19 | for char in label: 20 | # if CHAR2INDEX.get(char) is not None: 21 | indices.append(CHAR2INDEX[char]) 22 | 23 | indices.append(EOS_token) 24 | return indices 25 | 26 | def get_label_from_indices(indices): 27 | label = "" 28 | for index in indices: 29 | if index == EOS_token: 30 | break 31 | elif index == PAD_token: 32 | continue 33 | else: 34 | label += INDEX2CHAR[index.item()] 35 | 36 | return label 37 | 38 | import torch.nn as nn 39 | import torch.nn.functional as F 40 | 41 | 42 | class VGG_FeatureExtractor(nn.Module): 43 | """ FeatureExtractor of CRNN (https://arxiv.org/pdf/1507.05717.pdf) """ 44 | 45 | def __init__(self, input_channel, output_channel=512): 46 | super(VGG_FeatureExtractor, self).__init__() 47 | self.output_channel = [int(output_channel / 8), int(output_channel / 4), 48 | int(output_channel / 2), output_channel] # [64, 128, 256, 512] 49 | self.ConvNet = nn.Sequential( 50 | nn.Conv2d(input_channel, self.output_channel[0], 3, 1, 1), nn.ReLU(True), 51 | nn.MaxPool2d(2, 2), # 64x16x50 52 | nn.Conv2d(self.output_channel[0], self.output_channel[1], 3, 1, 1), nn.ReLU(True), 53 | nn.MaxPool2d(2, 2), # 128x8x25 54 | nn.Conv2d(self.output_channel[1], self.output_channel[2], 3, 1, 1), nn.ReLU(True), # 256x8x25 55 | nn.Conv2d(self.output_channel[2], self.output_channel[2], 3, 1, 1), nn.ReLU(True), 56 | nn.MaxPool2d((2, 1), (2, 1)), # 256x4x25 57 | nn.Conv2d(self.output_channel[2], self.output_channel[3], 3, 1, 1, bias=False), 58 | nn.BatchNorm2d(self.output_channel[3]), nn.ReLU(True), # 512x4x25 59 | nn.Conv2d(self.output_channel[3], self.output_channel[3], 3, 1, 1, bias=False), 60 | nn.BatchNorm2d(self.output_channel[3]), nn.ReLU(True), 61 | nn.MaxPool2d((2, 1), (2, 1)), # 512x2x25 62 | nn.Conv2d(self.output_channel[3], self.output_channel[3], 2, 1, 0), nn.ReLU(True)) # 512x1x24 63 | 64 | def forward(self, input): 65 | return self.ConvNet(input) 66 | 67 | 68 | class BidirectionalGRU(nn.Module): 69 | def __init__(self, input_size, hidden_size, output_size): 70 | super(BidirectionalGRU, self).__init__() 71 | 72 | self.rnn = nn.GRU(input_size, hidden_size, bidirectional=True) 73 | self.embedding = nn.Linear(hidden_size * 2, output_size) 74 | 75 | def forward(self, x): 76 | recurrent, hidden = self.rnn(x) 77 | T, b, h = recurrent.size() 78 | t_rec = recurrent.view(T * b, h) 79 | 80 | output = self.embedding(t_rec) # [T * b, nOut] 81 | output = output.view(T, b, -1) 82 | 83 | return output, hidden 84 | 85 | class CTCModel(nn.Module): 86 | def __init__(self, inner_dim=512, num_chars=65): 87 | super().__init__() 88 | self.encoder = VGG_FeatureExtractor(3, inner_dim) 89 | self.AdaptiveAvgPool = nn.AdaptiveAvgPool2d((None, 1)) 90 | self.rnn_encoder = BidirectionalGRU(inner_dim, 256, 256) 91 | self.num_chars = num_chars 92 | self.decoder = nn.Linear(256, self.num_chars) 93 | 94 | def forward(self, x, labels=None, max_label_length=None, device=None, training=True): 95 | # ---------------- CNN ENCODER -------------- 96 | x = self.encoder(x) 97 | # print('After CNN:', x.size()) 98 | 99 | # ---------------- CNN TO RNN ---------------- 100 | x = x.permute(3, 0, 1, 2) # from B x C x H x W -> W x B x C x H 101 | x = self.AdaptiveAvgPool(x) 102 | size = x.size() 103 | x = x.reshape(size[0], size[1], size[2] * size[3]) 104 | 105 | # ----------------- RNN ENCODER --------------- 106 | encoder_outputs, last_hidden = self.rnn_encoder(x) 107 | # print('After RNN', x.size()) 108 | 109 | # --------------- CTC DECODER ------------------- 110 | # batch_size = encoder_outputs.size()[1] 111 | outputs = self.decoder(encoder_outputs) 112 | 113 | return outputs 114 | 115 | import uuid 116 | 117 | import logging 118 | import boto3 119 | from botocore.exceptions import ClientError 120 | 121 | 122 | def upload_file_to_s3(file_name, bucket, object_name=None): 123 | """Upload a file to an S3 bucket 124 | 125 | :param file_name: File to upload 126 | :param bucket: Bucket to upload to 127 | :param object_name: S3 object name. If not specified then file_name is used 128 | :return: True if file was uploaded, else False 129 | """ 130 | 131 | # If S3 object_name was not specified, use file_name 132 | if object_name is None: 133 | object_name = file_name 134 | 135 | # Upload the file 136 | s3_client = boto3.client('s3') 137 | try: 138 | response = s3_client.upload_file(file_name, bucket, object_name) 139 | except ClientError as e: 140 | logging.error(e) 141 | return False 142 | return True 143 | 144 | import boto3 145 | import torch 146 | 147 | import os 148 | os.environ["AWS_ACCESS_KEY_ID"] = "*********************" 149 | os.environ["AWS_SECRET_ACCESS_KEY"] = "*********************" 150 | 151 | s3 = boto3.client('s3') 152 | bucket_name = model_s3_path.split('s3://')[1].split('/')[0] 153 | object_name = model_s3_path.split('s3://' + bucket_name)[1][1:] 154 | model_path = 'best_model.pth' 155 | s3.download_file(bucket_name, object_name, model_path) 156 | 157 | model = CTCModel(inner_dim=128, num_chars=len(CHAR2INDEX)) 158 | device = 'cpu' if not torch.cuda.is_available() else 'cuda' 159 | model.load_state_dict(torch.load(model_path, map_location=device)) 160 | model = model.to(device) 161 | 162 | def predict(image): 163 | # upload image to aws s3 164 | filename = str(uuid.uuid4()) + '.png' 165 | cv2.imwrite(filename, image) 166 | upload_file_to_s3(filename, bucket='ocrpipeline', object_name='data/user_data/images/') 167 | 168 | model.eval() 169 | batch = [{'image': image, 'label': [1]}] 170 | images = collate_wrapper(batch)[0] 171 | 172 | images = images.to(device) 173 | 174 | outputs = model(images) 175 | outputs = outputs.permute(1, 0, 2) 176 | output = outputs[0] 177 | 178 | out_best = list(torch.argmax(output, -1)) # [2:] 179 | out_best = [k for k, g in itertools.groupby(out_best)] 180 | pred_text = get_label_from_indices(out_best) 181 | 182 | return pred_text 183 | 184 | 185 | # import gradio 186 | # device = 'cpu' if not torch.cuda.is_available() else 'cuda' 187 | # # model.load_state_dict(torch.load('best_model.pth')) 188 | # model = model.to(device) 189 | # interface = gradio.Interface(predict, "image", "text") 190 | 191 | # interface.launch() 192 | 193 | import os 194 | from flask import Flask, flash, request, redirect, url_for 195 | from werkzeug.utils import secure_filename 196 | 197 | UPLOAD_FOLDER = './data/user_data/' 198 | ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) 199 | 200 | app = Flask(__name__) 201 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER 202 | app.secret_key = "super secret key" 203 | 204 | 205 | @app.route("/predict", methods=['POST']) 206 | def upload_file(): 207 | if request.method == 'POST': 208 | # check if the post request has the file part 209 | if 'file' not in request.files: 210 | flash('No file part') 211 | return redirect(request.url) 212 | file = request.files['file'] 213 | # if user does not select file, browser also 214 | # submit a empty part without filename 215 | if file.filename == '': 216 | flash('No selected file') 217 | return redirect(request.url) 218 | if file: 219 | print('found file') 220 | filename = secure_filename(file.filename) 221 | filepath = os.path.join(filename) 222 | file.save(filepath) 223 | 224 | image = cv2.imread(filepath) 225 | output = predict(image) 226 | return output 227 | 228 | 229 | app.run(debug=False, threaded=False, host='0.0.0.0', port=5000) 230 | 231 | if __name__ == '__main__': 232 | import argparse 233 | parser = argparse.ArgumentParser(description='Deployment') 234 | parser.add_argument('--model_s3_path', type=str) 235 | 236 | args = parser.parse_args() 237 | 238 | deploy_model(model_s3_path=args.model_s3_path) 239 | 240 | -------------------------------------------------------------------------------- /mlpipeline/mlpipeline.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Workflow 3 | metadata: 4 | generateName: mlpipeline- 5 | annotations: {pipelines.kubeflow.org/kfp_sdk_version: 1.7.0, pipelines.kubeflow.org/pipeline_compilation_time: '2021-08-13T17:29:40.859670', 6 | pipelines.kubeflow.org/pipeline_spec: '{"name": "Mlpipeline"}'} 7 | labels: {pipelines.kubeflow.org/kfp_sdk_version: 1.7.0} 8 | spec: 9 | entrypoint: mlpipeline 10 | templates: 11 | - name: kubeflow-serve-model-using-kfserving 12 | container: 13 | args: 14 | - -u 15 | - kfservingdeployer.py 16 | - --action 17 | - apply 18 | - --model-name 19 | - custom-simple 20 | - --model-uri 21 | - '' 22 | - --canary-traffic-percent 23 | - '100' 24 | - --namespace 25 | - kubeflow-user 26 | - --framework 27 | - '' 28 | - --custom-model-spec 29 | - '{"image": "thanhhau097/ocrdeployment", "port": 5000, "name": "custom-container", 30 | "command": "python3 /src/main.py --model_s3_path {{inputs.parameters.ocr-train-output_path}}"}' 31 | - --autoscaling-target 32 | - '0' 33 | - --service-account 34 | - '' 35 | - --enable-istio-sidecar 36 | - "True" 37 | - --output-path 38 | - /tmp/outputs/InferenceService_Status/data 39 | - --inferenceservice-yaml 40 | - '{}' 41 | - --watch-timeout 42 | - '1800' 43 | - --min-replicas 44 | - '-1' 45 | - --max-replicas 46 | - '-1' 47 | - --request-timeout 48 | - '60' 49 | command: [python] 50 | image: quay.io/aipipeline/kfserving-component:v0.5.1 51 | inputs: 52 | parameters: 53 | - {name: ocr-train-output_path} 54 | outputs: 55 | artifacts: 56 | - {name: kubeflow-serve-model-using-kfserving-InferenceService-Status, path: /tmp/outputs/InferenceService_Status/data} 57 | metadata: 58 | labels: 59 | pipelines.kubeflow.org/kfp_sdk_version: 1.7.0 60 | pipelines.kubeflow.org/pipeline-sdk-type: kfp 61 | pipelines.kubeflow.org/enable_caching: "true" 62 | annotations: {pipelines.kubeflow.org/component_spec: '{"description": "Serve 63 | Models using Kubeflow KFServing", "implementation": {"container": {"args": 64 | ["-u", "kfservingdeployer.py", "--action", {"inputValue": "Action"}, "--model-name", 65 | {"inputValue": "Model Name"}, "--model-uri", {"inputValue": "Model URI"}, 66 | "--canary-traffic-percent", {"inputValue": "Canary Traffic Percent"}, "--namespace", 67 | {"inputValue": "Namespace"}, "--framework", {"inputValue": "Framework"}, 68 | "--custom-model-spec", {"inputValue": "Custom Model Spec"}, "--autoscaling-target", 69 | {"inputValue": "Autoscaling Target"}, "--service-account", {"inputValue": 70 | "Service Account"}, "--enable-istio-sidecar", {"inputValue": "Enable Istio 71 | Sidecar"}, "--output-path", {"outputPath": "InferenceService Status"}, "--inferenceservice-yaml", 72 | {"inputValue": "InferenceService YAML"}, "--watch-timeout", {"inputValue": 73 | "Watch Timeout"}, "--min-replicas", {"inputValue": "Min Replicas"}, "--max-replicas", 74 | {"inputValue": "Max Replicas"}, "--request-timeout", {"inputValue": "Request 75 | Timeout"}], "command": ["python"], "image": "quay.io/aipipeline/kfserving-component:v0.5.1"}}, 76 | "inputs": [{"default": "create", "description": "Action to execute on KFServing", 77 | "name": "Action", "type": "String"}, {"default": "", "description": "Name 78 | to give to the deployed model", "name": "Model Name", "type": "String"}, 79 | {"default": "", "description": "Path of the S3 or GCS compatible directory 80 | containing the model.", "name": "Model URI", "type": "String"}, {"default": 81 | "100", "description": "The traffic split percentage between the candidate 82 | model and the last ready model", "name": "Canary Traffic Percent", "type": 83 | "String"}, {"default": "", "description": "Kubernetes namespace where the 84 | KFServing service is deployed.", "name": "Namespace", "type": "String"}, 85 | {"default": "", "description": "Machine Learning Framework for Model Serving.", 86 | "name": "Framework", "type": "String"}, {"default": "{}", "description": 87 | "Custom model runtime container spec in JSON", "name": "Custom Model Spec", 88 | "type": "String"}, {"default": "0", "description": "Autoscaling Target Number", 89 | "name": "Autoscaling Target", "type": "String"}, {"default": "", "description": 90 | "ServiceAccount to use to run the InferenceService pod", "name": "Service 91 | Account", "type": "String"}, {"default": "True", "description": "Whether 92 | to enable istio sidecar injection", "name": "Enable Istio Sidecar", "type": 93 | "Bool"}, {"default": "{}", "description": "Raw InferenceService serialized 94 | YAML for deployment", "name": "InferenceService YAML", "type": "String"}, 95 | {"default": "300", "description": "Timeout seconds for watching until InferenceService 96 | becomes ready.", "name": "Watch Timeout", "type": "String"}, {"default": 97 | "-1", "description": "Minimum number of InferenceService replicas", "name": 98 | "Min Replicas", "type": "String"}, {"default": "-1", "description": "Maximum 99 | number of InferenceService replicas", "name": "Max Replicas", "type": "String"}, 100 | {"default": "60", "description": "Specifies the number of seconds to wait 101 | before timing out a request to the component.", "name": "Request Timeout", 102 | "type": "String"}], "name": "Kubeflow - Serve Model using KFServing", "outputs": 103 | [{"description": "Status JSON output of InferenceService", "name": "InferenceService 104 | Status", "type": "String"}]}', pipelines.kubeflow.org/component_ref: '{"digest": 105 | "84f27d18805744db98e4d08804ea3c0e6ca5daa1a3a90dd057b5323f19d9dd2c", "url": 106 | "https://raw.githubusercontent.com/kubeflow/pipelines/master/components/kubeflow/kfserving/component.yaml"}', 107 | pipelines.kubeflow.org/arguments.parameters: '{"Action": "apply", "Autoscaling 108 | Target": "0", "Canary Traffic Percent": "100", "Custom Model Spec": "{\"image\": 109 | \"thanhhau097/ocrdeployment\", \"port\": 5000, \"name\": \"custom-container\", 110 | \"command\": \"python3 /src/main.py --model_s3_path {{inputs.parameters.ocr-train-output_path}}\"}", 111 | "Enable Istio Sidecar": "True", "Framework": "", "InferenceService YAML": 112 | "{}", "Max Replicas": "-1", "Min Replicas": "-1", "Model Name": "custom-simple", 113 | "Model URI": "", "Namespace": "kubeflow-user", "Request Timeout": "60", 114 | "Service Account": "", "Watch Timeout": "1800"}'} 115 | - name: mlpipeline 116 | dag: 117 | tasks: 118 | - name: kubeflow-serve-model-using-kfserving 119 | template: kubeflow-serve-model-using-kfserving 120 | dependencies: [ocr-train] 121 | arguments: 122 | parameters: 123 | - {name: ocr-train-output_path, value: '{{tasks.ocr-train.outputs.parameters.ocr-train-output_path}}'} 124 | - {name: ocr-preprocessing, template: ocr-preprocessing} 125 | - name: ocr-train 126 | template: ocr-train 127 | dependencies: [ocr-preprocessing] 128 | arguments: 129 | parameters: 130 | - {name: ocr-preprocessing-output_path, value: '{{tasks.ocr-preprocessing.outputs.parameters.ocr-preprocessing-output_path}}'} 131 | - name: ocr-preprocessing 132 | container: 133 | args: [] 134 | command: [python, /src/main.py] 135 | image: thanhhau097/ocrpreprocess 136 | outputs: 137 | parameters: 138 | - name: ocr-preprocessing-output_path 139 | valueFrom: {path: /output_path.txt} 140 | artifacts: 141 | - {name: ocr-preprocessing-output_path, path: /output_path.txt} 142 | metadata: 143 | labels: 144 | pipelines.kubeflow.org/kfp_sdk_version: 1.7.0 145 | pipelines.kubeflow.org/pipeline-sdk-type: kfp 146 | pipelines.kubeflow.org/enable_caching: "true" 147 | annotations: {pipelines.kubeflow.org/component_spec: '{"description": "Preprocess 148 | OCR model", "implementation": {"container": {"command": ["python", "/src/main.py"], 149 | "fileOutputs": {"output_path": "/output_path.txt"}, "image": "thanhhau097/ocrpreprocess"}}, 150 | "name": "OCR Preprocessing", "outputs": [{"description": "Output Path", 151 | "name": "output_path", "type": "String"}]}', pipelines.kubeflow.org/component_ref: '{"digest": 152 | "d5922a4c63432cb79f05fc02cd1b6c0f0c881e0e3883573d5cadef1eb379087c", "url": 153 | "./components/preprocess/component.yaml"}'} 154 | - name: ocr-train 155 | container: 156 | args: [] 157 | command: [python, /src/main.py, --data_path, '{{inputs.parameters.ocr-preprocessing-output_path}}'] 158 | image: thanhhau097/ocrtrain 159 | inputs: 160 | parameters: 161 | - {name: ocr-preprocessing-output_path} 162 | outputs: 163 | parameters: 164 | - name: ocr-train-output_path 165 | valueFrom: {path: /output_path.txt} 166 | artifacts: 167 | - {name: ocr-train-output_path, path: /output_path.txt} 168 | metadata: 169 | labels: 170 | pipelines.kubeflow.org/kfp_sdk_version: 1.7.0 171 | pipelines.kubeflow.org/pipeline-sdk-type: kfp 172 | pipelines.kubeflow.org/enable_caching: "true" 173 | annotations: {pipelines.kubeflow.org/component_spec: '{"description": "Training 174 | OCR model", "implementation": {"container": {"command": ["python", "/src/main.py", 175 | "--data_path", {"inputValue": "data_path"}], "fileOutputs": {"output_path": 176 | "/output_path.txt"}, "image": "thanhhau097/ocrtrain"}}, "inputs": [{"description": 177 | "s3 path to data", "name": "data_path"}], "name": "OCR Train", "outputs": 178 | [{"description": "Output Path", "name": "output_path", "type": "String"}]}', 179 | pipelines.kubeflow.org/component_ref: '{"digest": "382c44ee886fc59d95f01989ba7765df9ec2321b7c706a90a6f9c76f70cf2ab0", 180 | "url": "./components/train/component.yaml"}', pipelines.kubeflow.org/arguments.parameters: '{"data_path": 181 | "{{inputs.parameters.ocr-preprocessing-output_path}}"}'} 182 | arguments: 183 | parameters: [] 184 | serviceAccountName: pipeline-runner 185 | -------------------------------------------------------------------------------- /docs/data/index.md: -------------------------------------------------------------------------------- 1 | # Data Pipeline 2 | Trong phần này, chúng ta sẽ thực hiện việc lưu trữ và gán nhãn dữ liệu. 3 | 4 | ## 1. Phân tích dữ liệu 5 | Trước khi giải quyết một bài toán, chúng ta cần phải thực hiện công việc phân tích dữ liệu để đưa ra phương pháp gán nhãn phù hợp và hiệu quả. Bài toán mà chúng ta sẽ cùng nhau giải quyết trong blog này tương đối đơn giản về mặt dữ liệu, với đầu vào là hình ảnh của một dòng chữ viết tay và chúng ta cần trả về kết quả dãy kí tự mà máy đọc được từ hình ảnh đó. 6 | 7 | ```python 8 | import os 9 | import cv2 10 | from matplotlib import pyplot as plt 11 | ``` 12 | 13 | ### Data Sample 14 | 15 | 16 | ```python 17 | DATA_SAMPLE_FOLDER = './Challenge 1_ Handwriting OCR for Vietnamese Address/0825_DataSamples 1' 18 | ``` 19 | 20 | 21 | ```python 22 | for image_name in os.listdir(DATA_SAMPLE_FOLDER)[:6]: 23 | if '.json' in image_name: 24 | continue 25 | 26 | image_path = os.path.join(DATA_SAMPLE_FOLDER, image_name) 27 | image = cv2.imread(image_path) 28 | plt.imshow(image) 29 | plt.show() 30 | ``` 31 | 32 | 33 | 34 | > ![png](./images/output_3_0.png) 35 | 36 | 37 | 38 | 39 | 40 | > ![png](./images/output_3_1.png) 41 | 42 | 43 | 44 | 45 | 46 | > ![png](./images/output_3_2.png) 47 | 48 | 49 | 50 | 51 | 52 | > ![png](./images/output_3_3.png) 53 | 54 | 55 | 56 | 57 | 58 | > ![png](./images/output_3_4.png) 59 | 60 | 61 | 62 | ### Dataset 63 | Thông thường, khi tiếp cận các bài toán học máy, chúng ta cần phân tích dữ liệu và chia dữ liệu thành các tập huấn luyện (training set), tập đánh giá (validation set) và tập thử nghiệm (testing set). 64 | 65 | Trong bộ dữ liệu mà chúng ta sử dụng ở đây, tập dữ liệu huấn luyện và tập thử nghiệm đã được chia sẵn, vì vậy, chúng ta sẽ chia tập dữ liệu huấn luyện có sẵn thành hai tập: tập dữ liệu huấn luyện và tập đánh giá. 66 | 67 | 68 | ```python 69 | TRAIN_FOLDER = './Challenge 1_ Handwriting OCR for Vietnamese Address/0916_Data Samples 2' 70 | TEST_FOLDER = './Challenge 1_ Handwriting OCR for Vietnamese Address/1015_Private Test' 71 | ``` 72 | 73 | 74 | ```python 75 | train_image_paths = [os.path.join(TRAIN_FOLDER, image_name) 76 | for image_name in os.listdir(TRAIN_FOLDER) 77 | if '.json' not in image_name] 78 | train_image_paths[:5] 79 | ``` 80 | 81 | 82 | > ['./Challenge 1_ Handwriting OCR for Vietnamese Address/0916_Data Samples 2/0768_samples.png', 83 | > './Challenge 1_ Handwriting OCR for Vietnamese Address/0916_Data Samples 2/0238_samples.png', 84 | > './Challenge 1_ Handwriting OCR for Vietnamese Address/0916_Data Samples 2/0898_samples.png', 85 | > './Challenge 1_ Handwriting OCR for Vietnamese Address/0916_Data Samples 2/0907_samples.png', 86 | > './Challenge 1_ Handwriting OCR for Vietnamese Address/0916_Data Samples 2/0071_samples.png'] 87 | 88 | 89 | 90 | ```python 91 | print('Number of training images:', len(train_image_paths)) 92 | ``` 93 | 94 | > Number of training images: 1823 95 | 96 | 97 | 98 | ```python 99 | test_image_paths = [os.path.join(TEST_FOLDER, image_name) 100 | for image_name in os.listdir(TEST_FOLDER) 101 | if '.json' not in image_name] 102 | test_image_paths[:5] 103 | ``` 104 | 105 | 106 | 107 | 108 | > ['./Challenge 1_ Handwriting OCR for Vietnamese Address/1015_Private Test/0232_tests.png', 109 | > './Challenge 1_ Handwriting OCR for Vietnamese Address/1015_Private Test/0004_tests.png', 110 | > './Challenge 1_ Handwriting OCR for Vietnamese Address/1015_Private Test/0374_tests.png', 111 | > './Challenge 1_ Handwriting OCR for Vietnamese Address/1015_Private Test/0142_tests.png', 112 | > './Challenge 1_ Handwriting OCR for Vietnamese Address/1015_Private Test/0468_tests.png'] 113 | 114 | 115 | 116 | 117 | ```python 118 | print('Number of testing images:', len(test_image_paths)) 119 | ``` 120 | 121 | > Number of testing images: 549 122 | 123 | 124 | #### Split datasets 125 | 126 | 127 | ```python 128 | NEW_TRAIN_FOLDER = './data/train/images' 129 | NEW_VALIDATION_FOLDER = './data/validation/images' 130 | NEW_TEST_FOLDER = './data/test/images' 131 | 132 | for folder in [NEW_TRAIN_FOLDER, NEW_VALIDATION_FOLDER, NEW_TEST_FOLDER]: 133 | if not os.path.exists(folder): 134 | os.makedirs(folder, exist_ok=True) 135 | ``` 136 | 137 | 138 | ```python 139 | import random 140 | 141 | validation_image_paths = random.choices(train_image_paths, k=int(0.2*len(train_image_paths))) 142 | train_image_paths = list(set(train_image_paths).difference(set(validation_image_paths))) 143 | ``` 144 | 145 | 146 | ```python 147 | print('Number of training images:', len(train_image_paths)) 148 | print('Number of validation images:', len(validation_image_paths)) 149 | print('Number of testing images:', len(test_image_paths)) 150 | ``` 151 | 152 | > Number of training images: 1497 153 | > Number of validation images: 364 154 | > Number of testing images: 549 155 | 156 | 157 | 158 | ```python 159 | import shutil 160 | 161 | def copy_images_to_folder(image_paths, to_folder): 162 | for path in image_paths: 163 | shutil.copy2(path, to_folder) 164 | 165 | copy_images_to_folder(train_image_paths, NEW_TRAIN_FOLDER) 166 | copy_images_to_folder(validation_image_paths, NEW_VALIDATION_FOLDER) 167 | copy_images_to_folder(test_image_paths, NEW_TEST_FOLDER) 168 | ``` 169 | 170 | ## 2, Lưu trữ dữ liệu 171 | Để đảm bảo dữ liệu được an toàn và bảo mật, cũng như có khả năng cập nhật dữ liệu mới, chúng ta sẽ sử dụng AWS S3 để lưu trữ. AWS S3 cung cấp CLI interface, SDK để chúng ta có thể tương tác một cách dễ dàng hơn với dữ liệu của mình. 172 | 173 | ### Tạo S3 bucket 174 | Để tạo một S3 bucket, chúng ta cần có một tài khoản AWS, sau đó truy cập vào [AWS Console](https://console.aws.amazon.com/) rồi chọn [S3 Service](https://s3.console.aws.amazon.com/s3/home?region=ap-southeast-1#) -> [**Create bucket**](https://s3.console.aws.amazon.com/s3/bucket/create?region=ap-southeast-1) 175 | 176 | Ở đây, chúng ta nhập các thông tin cần thiết: `Bucket name`, `AWS Region` sau đó chọn `Create bucket` để tạo bucket. Ví dụ ở đây chúng ta sẽ đặt tên cho bucket là `ocrpipeline` và đặt tại Singapore. 177 | 178 | ![alt text](./images/create_s3_bucket.png "Tạo S3 bucket") 179 | 180 | ### AWS Credentials 181 | Để có thể tương tác với AWS S3 bằng CLI, chúng ta cần phải có AWS Credentials. Chúng ta có thể tạo AWS Credentials tại [Identity and Access Management (IAM)](https://console.aws.amazon.com/iam/home?region=ap-southeast-1#/security_credentials), chọn mục *Access keys (access key ID and secret access key)*, sau đó tiến hành tạo *Access Key* bằng cách chọn *Create New Access Key*. Bạn hãy lưu hai key này vào nơi an toàn, chúng ta sẽ không thể nhìn thấy `Secret Access Key` khi đóng cửa sổ này. 182 | 183 | Sau khi tạo *Access Key*, chúng ta cần cài đặt chúng lên máy của mình bằng cách sử dụng `AWS CLI`. Trước hết chúng ta cần phải cài đặt AWS CLI bằng Terminal: 184 | 185 | ``` 186 | pip install awscli 187 | ``` 188 | 189 | Sau đó thực hiện cài đặt các key mà chúng ta vừa tạo ra bằng cách sử dụng câu lệnh: 190 | ``` 191 | aws configure 192 | ``` 193 | Nhập các thông tin cần thiết vào 194 | ``` 195 | > AWS Access Key ID [****************4ZHS]: 196 | > AWS Secret Access Key [****************DNb9]: 197 | > Default region name [ap-southeast-1]: 198 | > Default output format [json]: 199 | ``` 200 | 201 | Chúng ta thử nghiệm việc thêm access keys thành công bằng câu lệnh sau trong Terminal: 202 | ``` 203 | aws s3 ls 204 | ``` 205 | 206 | Kết quả thu được là danh sách các bucket hiện tại mà chúng ta đang có: 207 | ``` 208 | 202x-xx-xx xx:xx:xx ocrpipeline 209 | ``` 210 | 211 | ### Tải dữ liệu lên AWS S3 212 | Chúng ta tải dữ liệu lên S3 bucket như sau: 213 | ``` 214 | aws s3 sync ./data s3://ocrpipeline/data/ 215 | ``` 216 | 217 | ## Gán nhãn dữ liệu 218 | Trong blog này, chúng ta sẽ sử dụng [Label Studio](https://github.com/heartexlabs/label-studio) để gán nhãn. Label Studio là một phần mềm gán nhãn mã nguồn mở, hỗ trợ nhiều loại dữ liệu như hình ảnh, âm thanh, video hay dữ liệu chuỗi thời gian. Ngoài ra chúng ta cũng có thể tích hợp các API để phục vụ cho việc prelabeling, giúp quá trình gán nhãn được hoàn thành nhanh hơn. Chúng ta sẽ thực hiện việc tích hợp API này ở phần sau của blog. Các bạn có thể tìm hiểu thêm về các tính năng của Label Studio ở đây: https://github.com/heartexlabs/label-studio 219 | 220 | (Ngoài ra, nếu chúng ta cần phải label một số lượng lớn dữ liệu, chúng ta có thể thuê các bên cung cấp dịch vụ gán nhãn dữ liệu như [Annoit](https://annoit.com), giúp chúng ta có được dữ liệu nhanh hơn và số lượng lớn hơn phục vụ cho bài toán của mình) 221 | 222 | Để cài đặt Label Studio, chúng ta thực hiện câu lệnh sau trên Terminal: 223 | ``` 224 | pip install label-studio 225 | ``` 226 | 227 | Và khởi tạo Label Studio như sau: 228 | ``` 229 | label-studio 230 | ``` 231 | 232 | Sau khi khởi tạo Label Studio, chúng ta truy cập vào đường dẫn mặc định khi Label Studio khởi chạy: [http://localhost:8080](http://localhost:8080). Tiếp theo chúng ta cần đăng kí tài khoản để thực hiện việc gán nhãn dữ liệu. 233 | 234 | Việc tiếp theo chúng ta cần làm là tạo một dự án mới và cài đặt `labeling interface` cho dự án. Chọn `Create` ở góc trên bên phải trang web và nhập các thông tin như tên dự án và mô tả của dự án. Sau đó chuyển qua mục `Labeling Setup` để cài đặt `labeling interface` cho dự án. Với bài toán này, chúng ta có thể chọn template là `Optical Character Recognition` hoặc `Image Captioning`. Trong template `Optical Character Recognition`, chúng ta có thể đánh `bounding box` cho từng dòng chữ, nhưng do dữ liệu của chúng ta đã được cắt sẵn ra từng dòng, vì vậy chúng ta có thể lựa chọn template `Image Captioning` để gán nhãn cho bài toán của mình. Ngoài ra, chúng ta cũng có thể sửa đổi `labeling interface` tuỳ ý phụ thuộc vào dữ liệu của mình, bằng cách nhấn vào mục `Code/Visual`. Ở đây, mình sẽ thêm một thuộc tính mới để xác định chất lượng của bức ảnh có tốt hay không bằng cách thêm vào mục `Code` như sau: 235 | ```javascript 236 | 237 | 238 |
239 |