아이폰 앱을 개발하다 온디바이스로 얼굴 인식 모델을 구동시켜야 하는 일이 생겼다.
찾은 오픈소스로 공개되어 있는 face recognition 모델은 deepface라는 모델이었다.
학습된 weights가 모두 공개되어 있어서 자유롭게 가져다 쓸 수 있었다.
https://github.com/serengil/deepface?ref=hackernoon.com
GitHub - serengil/deepface: A Lightweight Face Recognition and Facial Attribute Analysis (Age, Gender, Emotion and Race) Library
A Lightweight Face Recognition and Facial Attribute Analysis (Age, Gender, Emotion and Race) Library for Python - GitHub - serengil/deepface: A Lightweight Face Recognition and Facial Attribute Ana...
github.com
https://github.com/serengil/deepface_models/releases
Releases · serengil/deepface_models
Pre-trained models for deepface python library. Contribute to serengil/deepface_models development by creating an account on GitHub.
github.com
공개된 weights를 보면 .h5 확장자로 여러 모델의 weights가 추출되어 있는 것을 볼 수 있다.
이 공개된 weights들을 가지고 core ML에서 구동할 수 있는 모델을 만들어보려 한다.
.h5 파일 core ML(.mlmodel) 파일로 변환하기
.mlmodel로 파일 변환을 하는 건 python 패키지인 coremltools가 지원하고 있다.
coremltools.convert()로 mlmodel 파일로 변환할 수 있는데, 위 공개된 weight에서는 하나 문제가 있었다.
model의 메타 데이터가 없다는 에러가 계속 떴는데,
어찌 생각해보면 당연한 것이 우리는 weights만 가지고 있고, 모델이 어떤 레이어로 구성되어 있는지 등의 모델에 대한 정보는 아는 게 없으니 그런 에러가 뜰만도 했다.
그래서 deepface gitHub을 까보기 시작했다.
많은 모델 중 하나를 선택해야 했는데, 아래 예시 코드는 vgg의 예시다.
그리고 이렇게 생긴 모델 구조를 찾았다.
원본은 아래 링크에서 볼 수 있다. https://github.com/serengil/deepface/blob/master/deepface/basemodels/VGGFace.py
GitHub - serengil/deepface: A Lightweight Face Recognition and Facial Attribute Analysis (Age, Gender, Emotion and Race) Library
A Lightweight Face Recognition and Facial Attribute Analysis (Age, Gender, Emotion and Race) Library for Python - GitHub - serengil/deepface: A Lightweight Face Recognition and Facial Attribute Ana...
github.com
모델이 어떤 layer와 shape, activation function으로 구성되어 있는지 모두 나와있었다.
이걸 찾아냈으니 우리가 할 일은 이 모델에 weights만 얹어서 mlmodel로 변환하는 일이다.
우선 필요한 패키지를 모두 로드해준다.
tf_version에 대한 import 분기로직은 deepface 레포에서 그대로 베껴왔다. (모델 레이어 쌓는 데에 필요한 패키지들이다)
import coremltools
import tensorflow as tf
tf_version = int(tf.__version__.split(".", maxsplit=1)[0])
if tf_version == 1:
from keras.models import Model, Sequential
from keras.layers import (
Convolution2D,
ZeroPadding2D,
MaxPooling2D,
Flatten,
Dropout,
Activation,
)
else:
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras.layers import (
Convolution2D,
ZeroPadding2D,
MaxPooling2D,
Flatten,
Dropout,
Activation,
)
그러고 나서는 찾은 model 구성하는 코드와 이 모델에 위에서 다운받은 weight를 넣어주면 된다.
# 다운 받아놓은 weights의 경로
weights_path = './vgg_face_weights.h5'
# 모델 구성
model = Sequential()
model.add(ZeroPadding2D((1, 1), input_shape=(224, 224, 3)))
model.add(Convolution2D(64, (3, 3), activation="relu"))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(64, (3, 3), activation="relu"))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(128, (3, 3), activation="relu"))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(128, (3, 3), activation="relu"))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(256, (3, 3), activation="relu"))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(256, (3, 3), activation="relu"))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(256, (3, 3), activation="relu"))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, (3, 3), activation="relu"))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, (3, 3), activation="relu"))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, (3, 3), activation="relu"))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, (3, 3), activation="relu"))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, (3, 3), activation="relu"))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, (3, 3), activation="relu"))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))
model.add(Convolution2D(4096, (7, 7), activation="relu"))
model.add(Dropout(0.5))
model.add(Convolution2D(4096, (1, 1), activation="relu"))
model.add(Dropout(0.5))
model.add(Convolution2D(2622, (1, 1)))
model.add(Flatten())
model.add(Activation("softmax"))
# 모델에 다운 받아놓은 가중치 추가
model.load_weights(weights_path)
이렇게 되면 모델이 성공적으로 만들어졌다.
그럼 우리가 원래 하려 했던대로 coremltools를 활용해 이 모델을 우리가 core ML에서 쓸 수 있는 형태로 바꿔주면 된다.
converted_model = coremltools.convert(model, convert_to="mlprogram")
converted_model.save("converted_model")
XCode에 모델 불러오기
모델을 성공적으로 변환했다면 이제 작업 중인 프로젝트에 얹어야 할 차례다.
모델을 불러오는 건 의외로 간단하다. 그냥 원하는 경로에 추출된 .mlmodel 파일을 드래그 앤 드랍 하면 된다.
그런데, 가끔 이런 에러가 뜰 때가 있다.
XCode13 이후에서 나는 버그라고 하는데, 아직 고쳐지지 않은 것 같다.
그런데 사실 모델을 불러오고 사용하는 데에는 큰 문제가 없다. UI 상의 버그라고 한다.
그래도 나는 이 화면이 불편해서 이런저런 방법을 찾다가 변환될 때 생성된 com.apple.CoreML 파일을 그대로 붙여오니까 제대로 보여지는 걸 확인했다.
import한 모델 사용하기
이렇게 모델을 불러왔다면 이제 코드 상에서 이 모델을 사용해 prediction을 해볼 차례만 남았다.
모델을 성공적으로 Xcode에 import 했다면 .mlmodel 파일명 그대로 자동으로 클래스가 생성되어 있을 것이다.
내 경우는 vgg_face_model이었다.
let model = try vgg_face_model()로 모델을 불러올 수 있고,
여기에 prediction()이라는 메서드를 써서 직접 모델을 사용해 볼 수 있다.
prediction에 어떤 input을 줘야 모델이 돌아가는지는 자동으로 완성되어 보인다.
내 경우는 미리 python에서 돌려본 모델에서 알 수 있었던 zero_padding2d_input이라는 걸 모델의 input으로 사용했다.
class func predict(_ imageArray: MLMultiArray) throws -> MLMultiArray? {
let model = try vgg_face_model()
let output = try? model.prediction(zero_padding2d_input: imageArray)
return output?.Identity
}
이렇게 되면 output에 모델이 직접 prediction한 결과가 담기고,
내가 찾은 모델에서는 내가 필요한 정보가 Identity에 담겨있기 때문에 output에서 Identity를 찾아오도록 했다.
output에서도 역시 그 안에서 어떤 값들에 접근이 가능한지 모델 메타데이터에서 자동으로 불러와 자동완성 해준다.
+) UIImage를 MLMultiArray로 변환하기
내가 쓴 모델을 파이썬에서 돌렸을 땐 np.array를 input과 output으로 사용하는 모델이었다.
이 모델을 coremltools에서 coreML 모델로 변환하고 나니 input, output 값이 MLMultiArray가 되었다.
그럼 우린 사진을 받아 모델로 predict를 하고 그 임베딩 값을 output으로 받아야 하므로,
Swift UIKit에서 사진을 다루는 객체인 UIImage를 모델이 이해할 수 있는 MLMultiArray로 변환해주는 작업이 필요했다.
또, 동시에 제각각의 이미지 사이즈를 모델이 원하는 사이즈인 244x244로 변환하는 작업 역시 필요했다.
모델은 이미지를 [1, 244, 244, 3]으로 받고 있었다.
순서대로, 이미지 장수, 너비, 높이, rgb다.
그래서 이미지를 resize하고 난 후 원하는 shape의 MLMultiArray로 변환하는 코드는 아래와 같다.
class func convertUIImageToMLMultiArray(image: UIImage) -> MLMultiArray? {
guard let resizedImage = image.resize(newSize: CGSize(width: 224, height: 224)),
let pixelBuffer = resizedImage.normalized() else {
// Handle the conversion error
return nil
}
guard let multiArray = try? MLMultiArray(shape: [1, 224, 224, 3], dataType: .float32) else {
// Handle the MLMultiArray creation error
return nil
}
let imageChannels = 3
let imageWidth = 224
let imageHeight = 224
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
if let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) {
let bufferPointer = baseAddress.assumingMemoryBound(to: UInt8.self)
var pixelIndex = 0
for row in 0..<imageHeight {
for col in 0..<imageWidth {
let offset = (row * imageWidth + col) * imageChannels
for channel in 0..<imageChannels {
let pixelValue = bufferPointer[offset + channel]
multiArray[pixelIndex] = NSNumber(value: Float32(pixelValue))
pixelIndex += 1
}
}
}
}
return multiArray
}
결과
결론적으로 이렇게 iOS 앱에서 사진을 촬영해 그 값으로 얼굴을 인식할 수 있는 온디바이스 모델이 완성됐다!