들어가며

최근 Backen-GPT와 관련된 글(https://beoks.tistory.com/91)을 올렸습니다. 동시에 GPT는 현재 Backend에 적용하기에는 다양한 한계가 존재한다고 평가했는데, 그래도 응용할 수 있는 방법이 없을까 고민하다 MockAPI에는 적용할만하다고 생각해 만들어보게 되었습니다.

디자인

기존 MockAPI는 주로 모바일이나 프론트엔드 개발에서 아직 벡엔드가 구현되지 않았을 때 API와 관련된 프론트 기능을 구현할 때 사용되었습니다. 하지만 스캐폴딩 코드에 해당하기에 MockAPI를 구현하는데 시간을 많이 쓰는 건 올바르지 않습니다. 따라서 목적과 초기 데이터베이스 상태를 설정하면 바로 원하는 API를 구현할 수 있도록 최대한 간단한 디자인을 목표로 삼아 아래와 같은 인터페이스를 생각했습니다.

//대학교 학생 관리라는 목적과 초기 학생 데이터베이스를 정의한다.
const API_DESCRIPTION = "University Studenet Management"
const INIT_DB={students : [{name : "foo",email:"email@email.com",grade:4}]}
const studentMockAPI = createMockAPI(API_DESCRIPTION,INIT_DB);
// 바로 원하는 API를 호출한다.
console.log(await studentMockAPI.get("/student"));
// {message: "Successfully retrieved data",State:"success",data:{students:[{"name":"foo","email":"email@email.com","grade":4}]}}
console.log(await studentMockAPI.post("/student",{name:"bar",email:"bar@bar.com",grade:3}))
// {message:"Student added successfully.",State:"Success",data:{name:"bar",email:"bar@bar.com",grade:3}}

구현

GPT 요청 구현

GPT API에 요청하는 기능은 간단하고 공식문서에도 잘 설명되어 있어서 어렵지 않게 구현했습니다.

  • OpenAI API 키를 발급한다.
  • 환경변수에 키를 등록한다.
  • 아래와 같이 프롬프트(질문)을 요청으로 보내고 응답으로 받는 코드를 작성한다.
import { Configuration,OpenAIApi } from "openai";

if(!process.env.OPENAI_API_KEY){
    throw new Error("OPENAI_API_KEY is not set");
}

const configuration = new Configuration({
    apiKey: process.env.OPENAI_API_KEY,
  });

const openai = new OpenAIApi(configuration);

const requestOpenAI=(prompt : string): Promise<string| undefined>=>
    openai.createCompletion({
        model: "text-davinci-003",
        prompt: prompt,
      }).then((response) => {
        return response.data.choices[0].text;
      });

GPT 응답 테스트

효과적인 mockAPI를 위해선 GPT가 어떤 형식으로 대답하는지 분석해야합니다.

처음 작성한 프롬프트 형식은 다음과 같습니다.

const createRequestText=(api : string, data : object,database : object,apiDescription:string): string=>{
    return `
    ${apiDescription} API Call (indexes are zero-indexed) : ${api}\\n\\n
    RequestBody : ${JSON.stringify(data)}\\n\\n
    Database State : ${JSON.stringify(database)}\\n\\n
    Output the API response prefixed with 'Response:' and as json.
    Then output the new database state as json, prefixed with 'State:'.
    If the API call is only requesting data, then don't change the database state, 
    but base your 'API Response' off what's in the database.
    `
}

테스트를 위해서 아래와 같은 학생 관리 API 예제를 만들었습니다.

async function test(){
    const studentMockAPI = createMockAPI("University Studenet Management",{students : [{name : "foo",email:"email@email.com",grade:4}]});

    console.log(await studentMockAPI.get("/get_all_student"));
    console.log(await studentMockAPI.post("/add_student",{name:"bar",email:"bar@bar.com",grade:3}))
    console.log(await studentMockAPI.get("/get_all_student"));
    console.log(await studentMockAPI.delete("/delete_student(grade=4)"))
    console.log(await studentMockAPI.get("/get_all_student"));
    console.log(await studentMockAPI.put("/revise_student(grade=3)",{name : "barbar",email : "barbar@bar.com",grade:5}))
    console.log(await studentMockAPI.get("/get_all_student"));
}

위 테스트한 결과, GPT의 응답이 비일관적인 것을 확인했습니다. 따라서 응답에서 API 응답과 갱신된 데이터베이스를 파싱하는 과정에서 에러가 발생했습니다. 이를 해결하기 위해선 정확한 응답 형식을 규정해야했습니다.

Regex 조건 추가

이번엔 “Total output must look like this:'Response: (.)\nState:(.)' with compact json.” 라는 문장을 프롬프트에 추가했습니다. 그 결과, 응답을 API 응답과 데이터베이스로 파싱하는 코드는 잘 동작했습니다.

const createRequestText=(api : string, data : object,database : object,apiDescription:string): string=>{
    return `
    ${apiDescription} API Call (indexes are zero-indexed) : ${api}\\n\\n
    RequestBody : ${JSON.stringify(data)}\\n\\n
    Database State : ${JSON.stringify(database)}\\n\\n
    Output the API response prefixed with 'Response:' and as json.
    Then output the new database state as json, prefixed with 'State:'.
    Total output must look like this:'Response: (.*)\\nState:(.*)' with compact json.
    If the API call is only requesting data, then don't change the database state, 
    but base your 'API Response' off what's in the database.
    `
}

그러나 이번엔 응답이 비일관적이라는 문제가 생겼습니다. 어떤 응답은 메시지만, 다른 건 상태코드만 등 제각각인 형태는 MockAPI를 사용하는데 문제가 될 수 있습니다.

API 응답 일관화

이번엔 “The API response must look like this:'{message: "(.)",status:"{status code}",data:{(.)}}' with compact json. with compact json.” 라는 문장을 추가해 API 응답 형식이 일관되도록 해보았습니다.

그 결과, 아래와 같이 비교적 일관된 Json 결과들을 받을 수 있었습니다.

{"message":"Students retrieved successfully.","status":200,"data":[{"name":"foo","email":"email@email.com","grade":4}]}
{"message":"Student added","status":200,"data":{"name":"bar","email":"bar@bar.com","grade":3}}
{message: "All student retrieved successfully", status:"200", data: [{"name":"foo","email":"email@email.com","grade":4},{"name":"bar","email":"bar@bar.com","grade":3}]}
{message: "Student deleted successfully.",status: "200",data:{}}
{"message":"Fetched all student data successfully.","status":200,"data":[{"name":"bar","email":"bar@bar.com","grade":3}]}
{"message":"Student updated","status":200,"data":{"name":"barbar","email":"barbar@bar.com","grade":5}}
{"message": "Successfully retrieved all student records", "status": 200, "data": {"students": [{"name": "barbar", "email": "barbar@bar.com", "grade": 5}]}}

그러나 아직 부족합니다. 같은 get_all_student API에 대해서 서로 다른 메시지를 반환하고 있습니다. 좀 더 일관화하기 위한 방법이 필요합니다.

GPT 요청 파라미터 튜닝

GPT Completion 요청 시에는 프롬프트 뿐만 아니라 여러 설정값을 튜닝할 수 있습니다. 여기서 창의적인 답변 정도를 뜻하는 temperature 파라미터의 값을 0으로 설정하고 추가적으로 다른 파라미터도 설정후 요청을 해보았습니다.

const requestOpenAI=(prompt : string): Promise<string| undefined>=>
    openai.createCompletion({
        model: "text-davinci-003",
        temperature:0,
        max_tokens: 100,
        top_p:1,
        frequency_penalty:0,
        best_of:1,
        prompt: prompt,
      }).then((response) => {
        return response.data.choices[0].text;
      });

그 결과 더욱 일관적인 API 응답을 얻을 수 있었습니다. 이 정도면 MockAPI로 사용할 수 있을 것 같습니다.

{"message":"Successfully retrieved all students","status":200,"data":[{"name":"foo","email":"email@email.com","grade":4}]}
{"message":"Student added successfully","status":200,"data":{"name":"bar","email":"bar@bar.com","grade":3}}
{"message":"Successfully retrieved all students","status":200,"data":[{"name":"foo","email":"email@email.com","grade":4},{"name":"bar","email":"bar@bar.com","grade":3}]}
{"message":"Student deleted successfully","status":200,"data":{}}
{"message":"Successfully retrieved all students","status":200,"data":[{"name":"bar","email":"bar@bar.com","grade":3}]}
{"message":"Student successfully updated","status":200,"data":{"name":"barbar","email":"barbar@bar.com","grade":5}}
{"message":"Successfully retrieved all students","status":200,"data":[{"name":"barbar","email":"barbar@bar.com","grade":5}]}

응답 형식 설정

하지만 사용자에 따라서 다른 종류의 API 응답형식이 필요할 수 있습니다. 이를 위해 응답 형식을 설정할 수 있는

디자인

그래서 이번엔 새로운 조건을 설정할 수 있는 기능 디자인을 구상했습니다.

//대학교 학생 관리라는 목적과 초기 학생 데이터베이스를 정의한다.
const API_DESCRIPTION = "University Studenet Management"
const INIT_DB={students : [{name : "foo",email:"email@email.com",grade:4}]}
const RESPONSE_FORMAT = '{*status:"{status code}",data:{(.*)}}'
const ERROR_RESPONSE_FORMAT='{message : {(.)},status:"{stattus code}"}'
const studentMockAPI = createMockAPI(API_DESCRIPTION,INIT_DB)
	.setSuccessResponseFormat(CONDITION)
	.setFailureResponseFormat(ERROR_RESPONSE_FORMAT)

그 결과 아래와 같이 API 수행 성공과 실패에 따라서 다른 응답 형식을 설정할 수 있었습니다.

console.log(await studentMockAPI.delete("/student(grade=5)"));
//{"status":"404","message":"No student found with grade 5"}
console.log(await studentMockAPI.get("/students"));
//{"data":[{"name":"foo","email":"email@email.com","grade":4}], "status":200}

결론

  • GPT를 이용해서 생각보다 괜찮은 MockAPI를 만들 수 있다.
  • 그러나 GPT가 항상 일정한 응답을 하지 않으므로 에러가 가끔 발생할 수 있으므로 이 경우 직접 mock API를 만들어야 한다.
  • 일반적인 mock API 보다 속도가 느리다. 따라서, 규모가 큰 테스트 사용에는 적합하지 않으며 일부 유닛테스트 사용하는 것이 적합하다.
  • 저장할 수 있는 데이터 크기에 한계가 존재한다. backend-GPT 에서는 1KB 정도가 한계라고 한다.

Npm

https://www.npmjs.com/package/@beoks/gpt-mock-api

들어가며

최근 GeeksNews에서 GPT를 이용한 벡엔드 서버에 관한 기사를 봤습니다(https://news.hada.io/topic?id=8341). 벡엔드 개발자로서는 굉장히 흥미로운 주제이기 때문에 어떻게 구현되어있는지 알아보았습니다. 이 포스트에서는 그 결과와 한계에 대해서 설명해보고자 합니다.

Backend-GPT란?

기존 벡엔드 서버는 코드를 이용해서 구현했습니다. Backend-GPT는 코드를 이용한 구현은 버그, 리뷰 필요 그리고 우리가 원하는 비즈니스 로직이 아닌 우리가 말하는 것만 수행한다고 비판하는 것에서 시작합니다. 비즈니스 로직을 처리하기 위해선 인간의 지능이 적절한 형태라고 말합니다. 이 문제를 해결하기 위해 LLM(Large-Language Model, ex.GPT) 을 벡엔드 서버로 처리자로 구현한 것이 Backend-GPT입니다.

Backend-GPT의 구현

코드를 분석한 결과 구현은 매우 간단합니다. 초기 데이터베이스 상태를 표현하는 JSON과 전달된 API를 GPT 모델에 전달하고, 계산된 API 결과와 데이터베이스 결과를 받아옵니다. 그리고 다시 API가 요청이 오면 갱신된 데이터베이스 결과와 API를 다시 전달합니다. 코드를 자세히 알아보면 다음과 같습니다.(2023.1.30 기준, 로그와 같은 불필요한 코드는 임의로 삭제했습니다.)

import json
from flask import Flask
import ray
ray.init()
from flask_cors import CORS
import requests

@ray.remote
def gpt3(input):
    response = requests.post(
    "<https://dashboard.scale.com/spellbook/api/app/kw1n3er6>",
    json={
            "input": input
        },
    headers={"Authorization":"Basic cld6n7eoo0065sr1acbwczykv"}
    )
    return response.text

def dict_to_json(d):
    return d.__dict__

app = Flask(__name__)
CORS(app)
db = json.load(open('db.json','r'))

@app.route('//') --------------- 1.
def api(app_name, api_call):
    db = json.load(open('db.json','r'))--------------- 2.
    gpt3_input = f"""{db[app_name]["prompt"]}
API Call (indexes are zero-indexed):
{api_call}
Database State:
{db[app_name]["state"]}
Output the API response prefixed with 'API response:'. Then output the new database state as json, prefixed with 'New Database State:'. If the API call is only requesting data, then don't change the database state, but base your 'API Response' off what's in the database.
""" --------------- 3.
    completion = ray.get(gpt3.remote(gpt3_input)) --------------- 4.
    completion = json.loads(completion)["text"]

    future1 = gpt3.remote(f"{completion}\\n\\nAPI Response as valid json (as above, ignoring new database state): ")
    future2 = gpt3.remote(f"{completion}\\n\\nThe value of 'New Database State' above (as json):")
    response = json.loads(ray.get(future1).strip())["text"].strip()
    new_state = json.loads(json.loads(ray.get(future2).strip())["text"].strip())
    db[app_name]["state"] = new_state
--------------- 5.
    json.dump(db, open('db.json', 'w'), indent=4, default=dict_to_json) --------------- 6.
    return response --------------- 7.

if __name__ == "__main__":
    app.run()
  1. Flask로 HTTP API 인터페이스 구현
  2. API 요청이 오면 현재 데이터베이스 상태를 로드
  3. API와 데이터베이스 상태를 바탕으로 GPT에 질문할 문장을 생성
  4. GPT에 질문 전송
  5. 데이터 수신 및 파싱
  6. 데이터베이스 상태 업데이트
  7. 응답 전달

한계

위 코드를 보면 알겠지만, 데이터를 전달할 때마다 데이터베이스 상태를 전달합니다. GPT가 수신할 수 있는 문장 길이는 제한되어 있기 때문에, 레포지토리에는 1KB 내외의 데이터만 가능하다고 명시되어 있습니다. 여기서 제가 추가적으로 생각하는 한계를 포함하면 다음과 같습니다.

  • 매우 제한적인 데이터베이스 용량 ~ 1KB
  • GPT API 사용으로 인한 매우 제한적인 트래픽
  • 파일시스템 기반의 동시성 처리 문제
  • 매번 데이터베이스 전체 상태 전달과 GPT 계산으로 인한 낮은 속도
  • 높은 추상화로 인한 최적화 설정 한계
  • 동일한 결과 미보장(GPT가 지속적으로 학습을 할 경우)

응용

위 한계로 인해서 응용할 수 있는 조건은 다음과 같이 매우 제한적입니다.

  1. 1KB 이내의 데이터 용량만 다루는 경우
  2. 사용자가 매우 적은 경우(정확한 수치는 계산 필요)
  3. 동시성 처리가 필요없거나 동시 사용자가 한 명으로 보장된 경우
  4. 빠른 속도가 필요하지 않은 경우
  5. 결과의 동일성이 반드시 보장되지 않아도 되는 경우

평가

위 응용 조건으로 인해서 현재 Backend-GPT 를 현업에 도입하는 것은 매우 어려울 것으로 보입니다. 그러나 별도의 코드 작성없이 서버 기능을 구현한다는 점에서 높은 효과성을 가지고 있습니다. 개발과정에서 위 응용조건에 부합하는 경우는 MVP 개발이나 Mock API 정도가 있을 것 같습니다. 

Reference

들어가며

지난 시간까지 데이터를 수집하고 Tensorflow Lite 모델을 만드는 과정까지 진행해보았다. 이번에는 이 모델 파일을 Android에 삽입해서 실제 구동까지 진행해보도록 하겠다.

TFlite 모델에 메타데이터 추가하기

이 부분 때문에 필자는 몇 시간동안 삽질을 했다. 이전 장에서 만든 .tflite 파일을 그대로 안드로이드에 사용하게 되면 에러가 발생한다:(. 구글링을 하고 stack overflow에 업로드하는 등 여러 노력을 통해서 솔루션을 찾아냈다. 에러의 내용은 tflite모델에 NormalizationOptions이 추가되어 있어야 한다는 뜻이었다. 이는 메타 데이터의 일종인데 이를 삽입하기 위해선 추가적인 작업이 필요했다.

addMetaData.ipynb
0.01MB

위 코드를 사용하면 필요한 메타 데이터를 추가 할 수 있다. 코드를 보면 아래의 부분을 볼 수 있는데 여기에 자신이 메타데이터를 추가할 tflite 파일과 label.txt의 경로를 지정한 후 그대로 실행시켜주면 된다.

Android Studio Demo APP에 삽입하기

자 이제 마지막 단계이다. github.com/tensorflow/examples/tree/master/lite/examples/object_detection/android이 링크에 들어가게 되면 Android에서 Object Detection 모델을 실행할 수 있는 코드를 다운로드할 수 있다. Readme의 지침을 잘 따라서 모델을 Asset 디렉터리에 삽입하게 되면 비로소 모델을 실행시킬 수 있을 것이다.

 

마무리

예전 코드를 참조해서 그런지 필자는 중간에 여러번 에러가 발생해서 굉장히 힘들었다... 이 글을 본 사람들은 부디 필자처럼 삽질을 하지 않았으면 좋겠다. ㅎㅎ 지금까지 안드로이드에서 동작하는 Object Detection 애플리케이션을 만드는 과정이었다.

Reference

1. www.tensorflow.org/lite/convert/metadata

 

TensorFlow Lite 모델에 메타 데이터 추가

TensorFlow Lite 메타 데이터는 모델 설명에 대한 표준을 제공합니다. 메타 데이터는 모델이 수행하는 작업과 입력 / 출력 정보에 대한 중요한 지식 소스입니다. 메타 데이터는 TensorFlow Lite 호스팅 모

www.tensorflow.org

2. github.com/tensorflow/examples/tree/master/lite/examples/object_detection/android

 

tensorflow/examples

TensorFlow examples. Contribute to tensorflow/examples development by creating an account on GitHub.

github.com

 

들어가며

지난 시간에는 roboflow를 사용해서 데이터를 업로드하여 처리하는 방법에 대해서 알아보았다. 이번에는 데이터와 Tensorflow 프레임워크를 사용해서 모델을 만들어보도록 하겠다. 추가적으로 컨버터를 사용해서 Tensorflow Lite 모델까지 변환하는 방법에 대해서 알아보도록 하겠다. (이 장에서는 필수적으로 수정해야 할 부분에 대해서만 언급하겠다. 코드에 대해서 자세히 알고 싶다면 Colab내부의 주석을 참고하도록 하자)

데이터 다운로드

colab.research.google.com/drive/1aIobwtqWggRbLbe606_WzVGtFiWZT9tr?usp=sharing

 

Roboflow-TFLite-Object-Detection_Bicycle_Helmet.ipynb

Colaboratory notebook

colab.research.google.com

위 링크는 모델을 학습시키고 Tensorflow Lite 모델로 변환하기 위한 모든 코드가 포함되어있다. 

Prepare Data부분에서 위와 같은 코드를 찾을 수 있을 것이다. 여기에 이전 장에서 진행했던 Roboflow 데이터셋의 링크를 삽입해야 한다. Roboflow 튜토리얼을 잘 따랐다면 대시보드에 자신이 만든 버전이 있는 것을 확인할 수 있을 것이다. 

여기에서 Export Your Dataset부분에서 포맷을 정하고 링크를 받을 수 있는 부분이 있을 것이다.

우선 COCO포맷을 정한다음 GetLink를 클릭하게 되면 Jupyter탭에서 아래와 같은 부분이 나올 것이다.

 !curl -L "링크" > roboflow.zip; unzip roboflow.zip; rm roboflow.zip

자신의 링크를 복사하여 colab에 [COCO Json Link Here!]라고 표시된 부분에 복사하도록 하자.

그리고 이번에는 Format을 Tensorflow TFRecord로 변경하여 위 과정을 반복한 후 [Tensorflow TFRecord Link Here!]라고 표시된 부분에 링크를 삽입하도록 한다. 이렇게 하면 데이터를 다운로드할 수 있다.

파일 이름 수정

Prepare tfrecord file 세션 아래의 코드를 보면 다음과 같은 코드가 있을 것이다.

각 roboflow에서 다운로드한 파일의 이름은 데이터셋에 따라서 다르기 때문에 여기에 자신이 설명한 데이터셋 이름을 업데이트해주어야 한다. colab 내부에 해당 경로를 따라가서 자신의 파일이 어떤 이름으로 정해져 있는지 확인 후 업데이트하도록 하자.

마무리

특별한 에로사항이 없다면 위에서 언급한 코드만 바꾸어 주어도 자동으로 모델을 학습하고 Tensorflow Lite로 모델을 변경한 다음 자신의 드라이브에 모델을 업데이트할 것이다. 만약 어떤 에러가 발생한다면 github.com/BEOKS/Bicycle-Helmet-Wearing-Detection 여기에 issue를 써준다면 수정하도록 하겠다.

Reference

1. How to Train a Custom TensorFlow Lite Object Detection Model, https://blog.roboflow.com/how-to-train-a-tensorflow-lite-object-detection-model/

 

How to Train a Custom TensorFlow Lite Object Detection Model

In this post, we walk through the steps to train and export a custom TensorFlow Lite object detection model with your own object detection dataset to detect your own custom objects. If you need a fast model on lower-end hardware, this post is for you. Whet

blog.roboflow.com

2. MobileNet SSD, https://github.com/tensorflow/models/tree/master/research/object_detection

 

tensorflow/models

Models and examples built with TensorFlow. Contribute to tensorflow/models development by creating an account on GitHub.

github.com

 

+ Recent posts