[AI] Android Studio, flutter project에 딥러닝 모델 배포, 적용하기 (Pytorch-lite)
간만의 포스팅 전 간단한 근황 토크..! 나는 현재 회사에 취직하여 AI 연구원으로 업무를 하고 있다!
일단 이건 AI와 직접적으로 관련이 된 건 아니지만, (굳이 따지자면 Android Studio에 더 맞을듯...) 새 카테고리도 만들었겠다, 아주 연관이 없는 건 아니니, 배운 점에 대하여 글을 남겨보려고 한다!
일을 한지는 이제 막 2개월이 넘었지만 아직 배울 것이 태산같다. 취업에 대해서는 조만간 회고를 남겨야겠다 ㅎㅎㅎ!
아무튼, 2개월동안 신입으로서 맡은 내 업무는 출시 예정인 애플리케이션의 성능을 높이는 것이었다.
그리고 그 과정에서 딥러닝을 모델 및 파이썬을 사용했다.
하지만 출시 예정인 애플리케이션은 파이썬이 아닌 플러터 / 안드로이드...!
물론 지금은 다 해냈지만, 개발할 당시에는 안드로이드 스튜디오/플러터 + DL 모델 적용에 관련해서 레퍼런스도 많이 없고, 있다고 해도 outdated 해서 더 이상 작동하지 않기도 했다. stackoverflow 엉아들 이게 맞아...?.
그런데 막상 적용하고 보니 그렇게 어려운 부분이 아니라, 안드로이드 프로젝트(kotlin)와 플러터 프로젝트(dart + kotlin)에서 DL 모델을 적용하는 과정을 블로그에 정리해보고자 한다.
Android Studio (Kotlin)
1. 모델 준비
먼저 안드로이드 스튜디오에서 사용할 모델을 준비한다.
안드로이드에서 모델을 배포하기 위해서는 모델을 TorchScript 형식으로 변환하여야한다.
필자의 경우, 모바일 환경인 점을 고려해서 .pth (=.pt) 모델을 pytorch-lite, 즉 .ptl 모델로 변환하여 사용했다.
❗왜 Pytorch-lite 를 사용하는 것이 더 좋을까?
Pytorch-lite의 경우 모바일에서 더 효율적이고, 기존의 Pytorch 모델보다 더 가볍고 빠르다.
자세한 내용은 공식 홈페이지를 참고하자!
ptl 모델을 생성하는 것은 크게 어렵지 않다!
기존의 모델을 저장하는 과정에서, 모바일 앱에 배포하기 위해 다음과 같이 TorchScript 모델 최적화(Optimization)만 추가적으로 진행하면 된다. (실질적으로 최적화 하는 코드는 마지막 2줄만 해당)
import torch
from torch.utils.mobile_optimizer import optimize_for_mobile
# 편의를 위해 torch.hub에서 모델을 가져왔지만, pretrained 한 모델 또한 사용 가능
model = torch.hub.load('pytorch/vision:v0.7.0', 'deeplabv3_resnet50', pretrained=True)
model.eval()
scriptedm = torch.jit.script(model)
scriptedm.save("deeplabv3_scripted.pt") # 기존 pt 모델 저장
optimized_scripted_module = optimize_for_mobile(scriptedm)
optimized_scripted_module._save_for_lite_interpreter("deeplabv3_scripted_lite.ptl") # ptl 모델 저장
공식 홈페이지에 따르면, optimize_for_mobile 메서드는 다음과 같은 최적화를 진행한다고 한다. (CPU 기준)
- Conv2D and BatchNorm fusion which folds Conv2d-BatchNorm2d into Conv2d;
- Insert and fold prepacked ops which rewrites the model graph to replace 2D convolutions and linear ops with their prepacked counterparts.
- ReLU and hardtanh fusion which rewrites graph by finding ReLU/hardtanh ops and fuses them together.
- Dropout removal which removes dropout nodes from this module when training is false.
- Conv packed params hoisting which moves convolution packed params to the root module, so that the convolution structs can be deleted. This decreases model size without impacting numerics.
2. 환경 세팅
2-1) Assets 폴더 생성
먼저 프로젝트의 파일에 Assets 폴더를 생성해준다! 기본 Directory 생성처럼 하면 안되고,
위 사진 처럼 New → Folder → Assets Folder 를 통해 생성해주자.
그리고 그 Assets 폴더에 사용하고자 하는 모델을 넣으면 된다.
2-2) build.gradle 설정
android {
aaptOptions {
noCompress "ptl"
}
}
dependencies {
...
implementation 'org.pytorch:pytorch_android_lite:1.12.1'
implementation 'org.pytorch:pytorch_android_torchvision_lite:1.10.0'
}
dependencies 버전은 프로젝트에 따라 상이할 수 있으나, 필자는 위와 같이 설정했고, 실행 가능했다.
build 설정 시 발생할 수 있는 오류에 대해 알아보자
- checkDebugDuplicateClasses FAILED / DuplicateClasses 👉 pytorch 버전 설정이 잘못되어 나오는 오류
- 2 files found with path 'lib/arm64-v8a/libc++_shared.so' from inputs 👉 이 또한 버전 때문에 발생할 수 있는데, build.gradle 에서 다음과 같은 코드를 추가하여 해결할 수 있다.
android {
packagingOptions {
pickFirst 'lib/x86/libc++_shared.so'
pickFirst 'lib/x86_64/libc++_shared.so'
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
pickFirst 'lib/arm64-v8a/libc++_shared.so'
}
}
2-3) Model load
모델을 로드하기 위해서는, 2-1) 에서 저장한 모델의 경로가 필요하다.
먼저 모델의 경로는 다음 메서드를 통해 가져올 수 있다.
// asset 파일의 경로를 return 하는 메서드
fun assetFilePath(context: Context, asset: String): String {
val file = File(context.filesDir, asset)
try {
val inpStream: InputStream = context.assets.open(asset)
try {
val outStream = FileOutputStream(file, false)
val buffer = ByteArray(4 * 1024)
var read: Int
while (true) {
read = inpStream.read(buffer)
if (read == -1) {
break
}
outStream.write(buffer, 0, read)
}
outStream.flush()
} catch (ex: Exception) {
ex.printStackTrace()
}
return file.absolutePath
} catch (e: Exception) {
e.printStackTrace()
}
return ""
}
메서드를 추가했다면 아래 코드를 통해 모델의 경로를 불러오고, 모델을 로드하자.
import org.pytorch.LiteModuleLoader
...
val assetPath = assetFilePath(this, "deeplabv7.ptl")
val module = LiteModuleLoader.load(assetPath)
(모델 로드 시, 버전이 일치하지 않아 오류가 생길 수 있다.
이 경우, error message에서 요구하는 버전과 모델의 버전을 일치시켜주면 되는데, 그 부분은 레퍼런스가 많으니 이 포스팅에서는 생략하겠다!)
그 후 모델의 입출력, 즉 input과 output을 python에서의 타입과 동일하게 맞춰주어야 한다.
필자의 경우, DeepLabV3 이라는 모델을 사용했고 참고를 위해 해당 부분 코드를 올려본다.
val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
photo,
TensorImageUtils.TORCHVISION_NORM_MEAN_RGB,
TensorImageUtils.TORCHVISION_NORM_STD_RGB
)
val outTensors = module.forward(IValue.from(inputTensor)).toDictStringKey()
// 텐서 키의 'out'은 semantic masks를 포함하고 있음
val outputTensor = outTensors["out"]!!.toTensor()
DeepLabV3의 경우, 모델의 출력이 딕셔너리이기 때문에 toDictStringKey() 메서드를 사용하여 결과를 추출한다.
하지만 다른 모델의 경우, Tensor, Tuple of Tensors 등, 다양한 타입이 될 수 있다.
outputTensor를 이제 필요에 맞게 변경하여 사용하면 된다!
Flutter + Android Studio native code (Kotlin)
그.런.데!
회사에서 사용한 환경은 플러터였다. (두둥)
사실 신입이기도 해서, 아마 제품 소스코드 위에서 직접 개발을 한 게 아니라 작은 프로젝트에서 진행 → 해당 코드를 제품 코드에 add 하는 방식으로 진행이 되었었다. 그래서 알고보니 프로젝트가 순수 kotlin이 아닌 flutter 였던 것...!
큰 틀은 비슷했기에 migration 작업은 간단히 끝났으나,
Assets 파일을 가져오는 것이 정말 골치아팠다.
Android Studio에 Assets 파일이 있는 것 처럼, flutter에도 기본 Assets 파일이 있었다.
사실 처음에는 거기에 모델 파일을 추가하고, 보통 dart 에서 불러오는 것 처럼 pubspec.yaml 를 통해 불러오려고 했으나 1차 실패...
flutter:
assets:
- assets/model/
stackoverflow에서 관련된 게시글 또한 보았으나, 내 경우 Plugin 이 아닌 MainActivity(FlutterFragmentActivity)였고, 이 경우 레퍼런스가 굉장히 적었다. (무려 github에 FlutterFragmentActivity, LiteModuleLoader.load / Module.load 검색 시, 나오는 결과 없음) 암튼 결과적으로, flutter의 native code에서 프로젝트 내 파일을 불러오는 것은 굉장히 간단하고 쉬웠다. 그저 그것을 찾는 과정이 오래걸렸을 뿐...
👉 본론으로 넘어가서,
android > app > src > main 아래, java 또는 kotlin 폴더가 위치한 경로에 동일하게 assets 폴더를 생성해주도록 하자.
그리고 모델을 로드하는 부분에서 다음과 같이 하면 된다!
val module = LiteModuleLoader.load(MainActivity().assetFilePath(getApplicationContext(), "deeplabv7.ptl"));
assetFilePath 메서드는 위와 동일하게 가져오되, load 하는 부분을 약간 변경해서 android/app/src/main/assets 안의 파일을 가져올 수 있었다! 파일의 위치로 인해서 Android Studio의 assets 폴더로 인식이 되는 것 같았다.
여기에 다 정리하지는 않았지만, 많은 오류들과 다양한 시행착오들을 겪었었다.
모든 것이 그러하듯, 할 때는 오래 걸리고 어려웠는데 결과를 보면 간단한 걸 왜 못했지! 싶은 것이 많은 것 같다.
앞으로도 뭐든지 차근차근 실행하며, 성장하는 개발자가 되도록 하자!
[참고]
https://pytorch.org/tutorials/beginner/deeplabv3_on_android.html