[ML Project] Bone Age Assessment(2)

김현우
None
Published in
9 min readFeb 6, 2021

휴먼스케이프 Software engineer Covy입니다.

이번 포스트에는 저번 포스트에 이어서 헬스케어와 딥러닝을 접목한 주제인 Bone Age Assessment를 발전시킨 내용에 대해서 공유드리려고 합니다.

저번 포스트에서는 Pytorch framework를 이용해서
1. csv file 읽는 방법
2. Model Save/Load를 하는 방법,
3. Model Test를 하는 방법,
4. Jupyter Notebook을 활용한 단일/다중 Test 및 결과 가시화를 하는 방법
에 대해서 설명드렸습니다.

이번 포스트에서는
저번에 생성한 모델을 서빙하는 Flask 서버
이미지를 업로드하여 골 연령 판독 요청을 보내주는 React 클라이언트
각각 구현한 뒤 Heroku, Netlify를 이용해 배포하여 간단하게 서비스를 구현하는 과정을 알아보려고 합니다.

Bone Age Assessment

Implementing Flask Server

Flask로 서버를 구축하는 것은 생각보다 간단했습니다.

특히 복잡한 관계형 데이터베이스가 필요하지 않은 저의 경우에는 단순히 요청으로 들어온 이미지를 학습한 모델을 통과시켜 얻어낸 데이터를 반환해주면 되었습니다.

전체적인 세팅은 이 곳을 참고하였고, 최종적으로 완성한 app.py 는 아래와 같습니다.

간단하게 세 가지 요소에 대해서 설명을 드리겠습니다.

먼저

app = Flask(__name__)
CORS(app)

위 구문 같은 경우에는 Flask 인스턴스를 현재 파일의 name으로 생성하고 CORS 세팅을 해줍니다. 저 같은 경우에는 origin을 따로 설정에 두지 않았기 때문에 모든 도메인에서의 요청을 허용합니다.

다음으로

if __name__ == '__main__':
port = int(os.environ.get('PORT', 5000))
app.run(debug=True, host='0.0.0.0', port=port)

if __name__ ‘__main__’ 구문 같은 경우에는 현재 파일이 스크립트의 시작점일 경우에 app을 실행합니다. 이는 (그럴 일은 별로 없지만) app.py가 다른 파일로부터 import 되어 실행되었을 경우에 앱을 실행 명령을 내리는 일이 없도록 하는 방어기제입니다.

port = int(os.environ.get(‘PORT’, 5000)) 구문 같은 경우에는 제가 Heroku를 통해 배포를 진행할 것이기 때문에 이를 서빙해줄 컴퓨터의 os의 환경변수에서 port 정보를 가져온 것입니다.

이후 app.run()에서
debug=True 를 두어 이후에 개발하다가 오류가 난 부분들에 대해서 트래킹을 진행했으며,
host=’0.0.0.0' 을 두어 모든 public address ip에서의 서버 접근을 허용했고,
port=port 를 두어앞서 os의 환경변수에서 받아온 port 번호로 서버가 listen 하도록 설정했습니다.

마지막으로

@app.route("/api/v1/predict/", methods=["POST"])
def predict(){
image_data_url = request.json['data_url']
predicted_bone_age = test(image_data_url)
return jsonify({'predicted_bone_age': prediceted_bone_age})

위 구문은 제 서버에서 유일하게 존재하는 기능적인 API 입니다.

@app.route(“/api/v1/predict/”, methods=[“POST”]) 를 두어 아래에서 정의할 함수가 불려질 route의 조건을 서술해 주었고

정의한 route로 POST request가 왔을 때 response를 산출하기위해 실행한 함수로 predict()를 정의했습니다.

predict()에서는 request의 body의 ‘data_url’ 이라는 key로부터 얻은 value를 image_data_url에 저장하고 이를 bone_age_assessment_model에서 정의한 test함수를 import 하여 불러와서 이 함수의 parameter로 전달합니다.

그렇게 계산해낸 예측 골 연령을 jsonify해서 산출해줍니다.

이러면 저 test() 함수를 궁금해하실 분들이 많을 것 같습니다.

여기서 구현한 함수는 이전 포스트에서 구현했던 ipynb 파일의 코드와 크게 다르지는 않습니다.

다만, 실제 입력을 base64 dataUrl image로 요청받아서 이를 numpy array로 변환하는 코드가 추가되었습니다.

imageObject = Image.open(BytesIO(base64.b64decode(
re.sub("data:image/png;base64", '', imagePath)))).convert('L')
image = np.array(imageObject)

위 코드가 base64 dataUrl image에서 앞의 “data:image/png;base64” 을 제거하고 decode한 이후 BytesIO를 이용해 바이너리 스트림으로 읽어들인 후 PIL image 객체로 만드는 과정입니다.

(참고로… 글 쓰면서 알게 된 건데 이 부분 때문에 현재 png가 아닌 이미지를 올리면 에러가 나네요. 수정해야겠어요..! ㅠ_ㅠ)

나머지 과정들은 저번 포스트를 참고해주시면 감사하겠습니다 :)

Implementing Gatsby + React Client

Gatsby + React로 클라이언트를 구현하는 것도 어렵지 않았습니다.

Gatsby 는 정적 HTML 생성기로, 개발부터 배포까지 크게 손댈 부분 많이 없이 정적 사이트를 만들 수 있다는 점에서 토이 프로젝트/블로깅을 하기에 적합하다고 많이들 판단하고 있다고 합니다.

저는 특히, 배포부분을 Netlify와 함께 편하게 진행하기 위해서 사용했으며, 많은 Gatsby Starter 중에 gatsby-starter-default 를 사용해 구현을 했습니다.

gatsby starter로 초기 세팅을 하고 나면 index.js 가 생성되는데 image upload를 위한 로직을 구현한 것이 위 코드입니다.

특별한 점은 FileReader로 이미지 자체를 dataUrl로 읽어들인 후 axios.post()를 이용해서 post request 를 보낸다는 점입니다.

이후 e.target.value = “” 를 실행해줌으로써 같은 이미지를 올리더라도 요청이 한 번 더 가도록 설정해 주었습니다. (event trigger 조건이 onChange 이기 때문입니다.)

이 로직을 이용한 JSX 부분은 아래와 같습니다.

file upload component는 react-bootstrap의 form을 사용해 구현했습니다.

이미지 업로드 시도 ~ response 수신까지는 페이지에 Spinner를 띄워주었고, 성공/실패 여부에 따라서 form의 색상을 바꾸어주었습니다.

여기서 실패의 세부적인 원인까지는 나누지 않았고, 요청이 실패한 모든 경우를 통틀어서 작업했습니다.

Bone Age Assessment

이렇게 제가 구현한 것을 테스트해보시려면 이 곳으로 가시면 됩니다.

Main Page

위 화면이 사이트의 메인 페이지이고,

upload image

위와 같은 이미지를 선택해주면…

Assessment Result

위와 같이 골 연령 판독 결과가 나타나게 됩니다.

Conclusion

이렇게 길다면 길고 짧다면 짧은 기간 동안 Bone Age Assessment Project를 진행해보았습니다. 진행하면서 느낀 점이 상당히 많았는데…

ML Model을 서빙할 수 있는 서버를 구축해볼 수 있었다는 점과 이미지 업로드를 구현할 수 있었다는 점은 나름 신선한 점이었습니다.

기존에는 Anaconda를 이용해 ML 학습환경을 구축하며 사용하고 있었는데, 배포를 위해서는 pip로 설치한 virtualenv에 모든 필요 library들을 몰아서 설치해야 pip freeze > requirements.txt command로 깔끔하게 requirements를 뽑아낼 수 있다는 것을 뼈저리게 느낄 수 있었습니다.

Django로 초기 ML Model 서버를 구현했는데 구현하고 보니 DB도 필요없고 불필요하게 무거운 것 같아서 과감하면서도 현명하게(?) 버렸습니다. 찾아보니 복잡한 DB Model 설계가 필요한 게 아니면 Flask로 많이들 한다는 것을 알 수 있었습니다.

ML Model이 용량이 상당히 커서 (> 100MB) Github에 올라가지 않아 상당히 어려움을 겪었습니다. 우선 자동 배포를 포기하고 docker로 말아서 배포를 성공했는데 그마저도 크고 작은 이슈들이 조금 있어서 이후에는 CI/CD를 위해서 조금 현명한 방법을 모색하고 싶다고 느껴졌습니다.

다음 프로젝트를 기대하면서 골 연령 판독기 — Bone Age Assessment를 여기서 끝마치겠습니다.

--

--