BentoML 사용기

2022. 12. 3. 14:26· 개발
목차
  1. 처음은 Tensorflow Serving
  2. 그래서 BentoML 
  3. 그 다음
  4. 그래서 결과는
  5.  
  6.  
  7. 참고했던 사이트

처음은 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
반응형

'개발' 카테고리의 다른 글

Image Resize and Padding  (0) 2022.12.17
Model Ensamble in Keras  (0) 2022.12.03
learning rate for more improving  (0) 2022.11.29
reshape vs resize  (0) 2022.11.28
추천시스템 로드맵  (0) 2022.11.26
  1. 처음은 Tensorflow Serving
  2. 그래서 BentoML 
  3. 그 다음
  4. 그래서 결과는
  5.  
  6.  
  7. 참고했던 사이트
'개발' 카테고리의 다른 글
  • Image Resize and Padding
  • Model Ensamble in Keras
  • learning rate for more improving
  • reshape vs resize
내공얌냠
내공얌냠
내공냠냠
내공냠냠내공냠냠
내공얌냠
내공냠냠
내공얌냠
전체
오늘
어제
  • 분류 전체보기 (255) N
    • 개발 (113)
      • mediapipe (16)
      • insightface (5)
      • JongjuAR (3)
    • 자료구조 알고리즘 (79)
      • 코딩테스트 (64)
      • 이론 (15)
    • 공부 (54) N
      • 단행본 (8) N
      • 튜토리얼 (19)
      • 논문 (15)
      • 복기 (5)
    • 참여 (5)

블로그 메뉴

  • 홈
  • 태그
  • 미디어로그
  • 위치로그
  • 방명록

공지사항

인기 글

태그

  • python telegrambot
  • flutter 행사 후기
  • ios google places api
  • flutter 행사
  • google mediapipe
  • postgresql 재설치
  • 테디노트의 랭체인을 활용한 rag 비법노트 기본편 후기
  • mediapipe translate
  • 플러터 튜토리얼
  • 컴퓨터 비전
  • 딥러닝 기반 음성인식 기초
  • flutter
  • postgresql install in mac
  • 컴퓨터 비전 책 추천
  • vscode 스프링 설치
  • 컴퓨터 비전 기초
  • speaker adaptation tts
  • flutter tutorial
  • git tutorial
  • 테디노트의 랭체인을 활용한 rag 비법노트 기본편
  • 깃 튜토리얼
  • 음성인식 튜토리얼
  • 미디어파이프
  • 구글 미디어파이프
  • 머신러닝이란
  • flutter conference
  • mediapipe
  • 플러터
  • 테디노트 rag 기본편
  • 음성인식 기초

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.2
내공얌냠
BentoML 사용기
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.