개발

BentoML 사용기

내공얌냠 2022. 12. 3. 14:26

처음은 Tensorflow Serving

처음에는 tensorflow serving 을 사용하고자 했습니다. AWS에 올리고 Docker로 쉽게 설치할 수 있기에 바로 시도해볼 수 있었습니다. 설치와 실행은 했는데, Request 호출할 때 아래와 같은 에러 메세지가 발생하였습니다.

ConnectionError: HTTPSConnectionPool(host='AWS주소', port=8501): 
Max retries exceeded with url: /v1/models/model/versions/1:predict 
(Caused by NewConnectionError('<urllib3.connection.VerifiedHTTPSConnection object at 0x7f5653971700>
: Failed to establish a new connection: [Errno 110] Connection timed out'))

인바운드 아웃바운드 다 열어줬음에도 발생해서 도무지 모르겠더군요. 구글링을 해보니 east-west-1 같이 지역설정에 따라서 달라진다고 해서 기본적으로 설정되어있던 도쿄에서 버지니아주로 지역을 변경했습니다. (다시 깔았씁니다..) 그랬더니 실행이 조금 되다가 다시 저런 메세지가 발생했습니다. 그래서 포기.. 하고 제 로컬에 다운로드 받는 방법으로 노선을 변경했습니다. 로컬에 설치되어있던 도커를 이용해서 tensorflow serving을 pull 받았는데, 또 오류 발생. 구글링해서 찾아보니 m1 silicon 은 지원하지 않는다는.. 메세지였습니다. 컴퓨터를 바꿀 수도 없고,, 그래서 다른 방법을 찾아보니 BentoML 이 있더군요.

그래서 BentoML 

구글링해서 기본적으로 설치를 하고 기존 학습한 모델을 불러와서 다시 저장했습니다.

케라스를 이용하여 모델을 학습시켰기에 문서를 보고 따라했습니다.

import tensorflow as tf
import bentoml

model_path = 'models/'
resnet_best = 'resnet50_3_tuned1.h5'

resnet_model = tf.keras.models.load_model(model_path + resnet_best)
bentoml.keras.save_model("bentoresnet50", resnet_model)

bentoml 홈 경로로 지정된 폴더 안에 아래와 같은 형태로 모델이 저장됩니다.

이제 service.py를 만들어서 서비스를 올려봅시다. 사용법은 공식 문서를 보고 따라하시면 됩니다.

저 같은 경우 손상모를 분류하는 모델이어서 0: 정상, 1: 약손상, 2: 극손상 이렇게 라벨이 분류되어 normal, little, lot 을 반환해주기 위해 소스코드를 조금 넣었습니다.

import bentoml

import numpy as np
from bentoml.io import Image
from bentoml.io import JSON
from bentoml.io import NumpyNdarray
from json import JSONEncoder
import json

runner = bentoml.keras.get("bentoresnet50:2yjy44tr2k4zzqvp").to_runner()

svc = bentoml.Service("bentoresnet50", runners=[runner])

class NumpyArrayEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, numpy.ndarray):
            return obj.tolist()
        return JSONEncoder.default(self, obj)

@svc.api(input=Image(), output=JSON())
async def predict(img):
    img = img.resize((224, 224))
    arr = np.array(img)
    arr = np.expand_dims(arr, axis=0)
    preds = await runner.async_run(arr)
    tag = ['normal', 'little', 'lot']
    predict = preds.tolist()
    numpyData = {"result": tag[(predict[0].index(max(predict[0])))]}
    encodedNumpyData = json.dumps(numpyData, cls=NumpyArrayEncoder)
    print('@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@' + tag[predict.index(max(predict))])
    print("1 list:", predict[0])
    print("2 max:", max(predict[0]))
    print("3 index:", predict[0].index(max(predict[0])))
    print("4 tag:", tag[(predict[0].index(max(predict[0])))])
    
    return encodedNumpyData

접속해보면 swagger 형태로 나옵니다.

호출해봅시다.

curl -i -X POST "localhost:3000/predict" -H "Content-Type: multipart/form-data" \
-F "image=@001_0_00046.jpg"

curl 명령어로 post를 날리고, 서버를 돌린 커맨드 창에는 정상적으로 실행된 것을 확인할 수 있습니다.

그 다음

앱에서 호출하는 것을 원했기 때문에 간단한 앱을 만들었습니다. iOS 앱을 만들기 위해 Swift(Storyboard)를 사용하여 이미지를 선택하고, 선택한 이미지를 Post 로 날리는 코드입니다.

//
//  ViewController.swift
//  alpaco-mini-proj
//
//  Created by 전민정 on 12/1/22.
//

import UIKit

struct Response { // or Decodable
  let result: String?
}

extension Response: Decodable {
    init(from decoder: Decoder) throws {
        self.result = try decoder.singleValueContainer().decode(String.self)
    }
}

extension Data {
    mutating func append(_ string: String) {
        if let data = string.data(using: .utf8) {
            append(data)
        }
    }
}

struct Media {
    let key: String
    let fileName: String
    let data: Data
    let mimeType: String

    init?(withImage image: UIImage, forKey key: String) {
        self.key = key
        self.mimeType = "image/jpg"
        self.fileName = "\(arc4random()).jpeg"

        guard let data = image.jpegData(compressionQuality: 0.5) else { return nil }
        self.data = data
    }
}

class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    @IBOutlet weak var imageView: UIImageView!
    
    @IBOutlet weak var resultLabel: UILabel!
    @IBOutlet weak var getResult: UIButton!
    
    let imagePicker = UIImagePickerController()
    
    @IBAction func loadImageButtonTapped(_ sender: UIButton) {
        imagePicker.allowsEditing = false
        imagePicker.sourceType = .photoLibrary
            
        present(imagePicker, animated: true, completion: nil)
    }
    
    @IBAction func getResultButtonTap(_ sender: UIButton) {
        
        // let url = URL(string: "http://localhost:3000/predict")!
        
        let url = URL(string: "http://localhost:3000/predict")!
        let boundary = generateBoundary()
        var request = URLRequest(url: url)

        guard let mediaImage = Media(withImage: imageView.image!, forKey: "file") else { return }

        request.httpMethod = "POST"

        request.allHTTPHeaderFields = [
                    "X-User-Agent": "ios",
                    "Accept-Language": "en",
                    "Accept": "application/json",
                    "Content-Type": "multipart/form-data; boundary=\(boundary)"
                ]
        
        let dataBody = createDataBody(media: [mediaImage], boundary: boundary)
        request.httpBody = dataBody
        
        let task = URLSession.shared.dataTask(with: request as URLRequest) { data, _, error in
            DispatchQueue.main.async {
                do {
                    if let data = data {
                        print(data)
                        let res = try JSONDecoder().decode(Response.self, from: data)
                        let resultValue = res.result
                        if resultValue?.contains("normal") == true {
                            self.resultLabel.text = "정상입니다!"
                        } else if resultValue?.contains("little") == true {
                            self.resultLabel.text = "약손상입니다!"
                        } else if resultValue?.contains("lot") == true {
                            self.resultLabel.text = "극손상입니다!"
                        }
                        // self.resultLabel.text = "success..."
                    }
                
                } catch {
                }
            }
        }
        task.resume()
        
    }
    
    func createRequestBody(imageData: Data, boundary: String, attachmentKey: String, fileName: String) -> Data{
        let lineBreak = "\r\n"
        var requestBody = Data()

        requestBody.append("\(lineBreak)--\(boundary + lineBreak)" .data(using: .utf8)!)
        requestBody.append("Content-Disposition: form-data; name=\"\(attachmentKey)\"; filename=\"\(fileName)\"\(lineBreak)" .data(using: .utf8)!)
        requestBody.append("Content-Type: image/jpeg \(lineBreak + lineBreak)" .data(using: .utf8)!) // you can change the type accordingly if you want to
        requestBody.append(imageData)
        requestBody.append("\(lineBreak)--\(boundary)--\(lineBreak)" .data(using: .utf8)!)

        return requestBody
    }
     
    func generateBoundary() -> String {
        return "Boundary-\(NSUUID().uuidString)"
    }

    func createDataBody(media: [Media]?, boundary: String) -> Data {

        let lineBreak = "\r\n"
        var body = Data()

        if let media = media {
            for photo in media {
                body.append("--\(boundary + lineBreak)")
                body.append("Content-Disposition: form-data; name=\"\(photo.key)\"; filename=\"\(photo.fileName)\"\(lineBreak)")
                body.append("Content-Type: image/jpeg\(lineBreak + lineBreak)")
                body.append(photo.data)
                body.append(lineBreak)
            }
        }

        body.append("--\(boundary)--\(lineBreak)")

        return body
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        imagePicker.delegate = self
    }
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let pickedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
            imageView.contentMode = .scaleAspectFit
            imageView.image = pickedImage
        }

        dismiss(animated: true, completion: nil)
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        dismiss(animated: true, completion: nil)
    }

}

 

그래서 결과는

실행화면은 아래 동영상을 참고해주세요.

 

 

참고했던 사이트

https://docs.bentoml.org/en/latest/frameworks/keras.html#keras

 

Keras

Compatibility: BentoML requires TensorFlow version 2.7.3 or higher to be installed. Saving a Keras Model: The following example loads a pre-trained ResNet50 model. After the Keras model is ready, u...

docs.bentoml.org

https://docs.bentoml.org/en/latest/tutorial.html

 

Tutorial: Intro to BentoML

time expected: 10 minutes In this tutorial, we will focus on online model serving with BentoML, using a classification model trained with scikit-learn and the Iris dataset. By the end of this tutor...

docs.bentoml.org

https://afsdzvcx123.tistory.com/entry/%EC%9D%B8%EA%B3%B5%EC%A7%80%EB%8A%A5-TensorFlow-Serving-Docker-Container-%EC%8B%A4%ED%96%89-%ED%95%98%EA%B8%B0-REST-API

 

[인공지능] TensorFlow Serving - Docker Container 실행 하기, REST API

참조 https://www.tensorflow.org/tfx/guide/serving http://solarisailab.com/archives/2703 소개 Docker를 이용하여 TensorFlow Serving 실행하는 방법을 정리합니다. Docker를 이용한 TensorFlow Serving 실행 모델을 SavedModel 포맷으

afsdzvcx123.tistory.com

https://chaloalto.tistory.com/17

 

[모델 배포하기(2/2)] TF-Serving 예제

출처 : http://solarisailab.com/archives/2703 36. 텐서플로우 서빙(TensorFlow Serving)을 이용한 딥러닝(Deep Learning) 모델 추론을 위한 REST API 서버 구 이번 시간에는 텐서플로우 서빙(TensorFlow Serving)을 이용해서

chaloalto.tistory.com

 

http://solarisailab.com/archives/2703

 

36. 텐서플로우 서빙(TensorFlow Serving)을 이용한 딥러닝(Deep Learning) 모델 추론을 위한 REST API 서버 구

이번 시간에는 텐서플로우 서빙(TensorFlow Serving)을 이용해서 딥러닝(Deep Learning) 모델 추론을 위한 REST API 서버를 구현하는 방법을 알아보자. [1] 텐서플로우 서빙(TensorFlow Serving) 텐서플로우 서빙(Te

solarisailab.com

https://www.tensorflow.org/tfx/tutorials/serving/rest_simple

 

TensorFlow Serving으로 TensorFlow 모델 학습 및 제공  |  TFX

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English TensorFlow Serving으로 TensorFlow 모델 학습 및 제공 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하

www.tensorflow.org

 

728x90
반응형