처음은 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
https://docs.bentoml.org/en/latest/tutorial.html
https://chaloalto.tistory.com/17
http://solarisailab.com/archives/2703
https://www.tensorflow.org/tfx/tutorials/serving/rest_simple
'개발' 카테고리의 다른 글
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 |